feat(avs): add SetRampDrivingLicence and InfoRampDrivingLicence to AVS interface

This commit is contained in:
Steffen Jost 2022-09-07 18:00:28 +02:00
parent 227970e4b1
commit a1272e38b7
2 changed files with 72 additions and 23 deletions

View File

@ -8,7 +8,7 @@ module Model.Types.Avs
) where ) where
import Import.NoModel hiding ((.=)) import Import.NoModel hiding ((.=))
--import Utils.Lens hiding ((.=)) import Utils.Lens.TH
import qualified Data.Set as Set import qualified Data.Set as Set
import qualified Data.Text as Text import qualified Data.Text as Text
@ -34,7 +34,7 @@ o .:?~ key = o .: key <|> maybe empty parseJSON go
-- | `SloppyBool` successfully parses different variations of true/false -- | `SloppyBool` successfully parses different variations of true/false
newtype SloppyBool = SloppyBool { sloppyBool :: Bool } newtype SloppyBool = SloppyBool { sloppyBool :: Bool }
deriving (Bounded, Enum, Eq, Ord, Read, Show, Generic, Typeable) deriving (Bounded, Enum, Eq, Ord, Generic, Typeable)
instance ToJSON SloppyBool where instance ToJSON SloppyBool where
toJSON (SloppyBool True) = "true" toJSON (SloppyBool True) = "true"
@ -53,18 +53,34 @@ instance FromJSON SloppyBool where
parseJSON invalid = prependFailure "parsing SloppyBool failed, " $ fail $ "expected Bool or String encoding boolean. Found " ++ show invalid parseJSON invalid = prependFailure "parsing SloppyBool failed, " $ fail $ "expected Bool or String encoding boolean. Found " ++ show invalid
type AvsPersonId = Int -- AVS Datatypes
type AvsPersonId = Int -- Could be a newtype, but then toJSON/fromJSON might be a pain?
type AvsLicence = Char data AvsLicence = AvsNoLicence | AvsLicenceVorfeld | AvsLicenceRollfeld
licenceVorfeld :: AvsLicence deriving (Bounded, Enum, Eq, Ord, Read, Show, Generic, Typeable)
licenceVorfeld = 'F'
licenceRollfeld :: AvsLicence instance ToJSON AvsLicence where
licenceRollfeld = 'R' toJSON AvsNoLicence = Number 0
toJSON AvsLicenceVorfeld = Number 1
toJSON AvsLicenceRollfeld = Number 2
instance FromJSON AvsLicence where
parseJSON (Number n) | n == 0 = pure AvsNoLicence
| n == 1 = pure AvsLicenceVorfeld
| n == 2 = pure AvsLicenceRollfeld
parseJSON invalid = prependFailure "parsing AvsLicence failed, " $ fail $ "expected Int value being 0, 1 or 2. Found " ++ show invalid
-- | Ought to be identical to QualificationShortname!
licence2char :: AvsLicence -> Char
licence2char AvsNoLicence = '0'
licence2char AvsLicenceVorfeld = 'F'
licence2char AvsLicenceRollfeld = 'R'
data AvsDataCardColor = AvsCardColorGrün | AvsCardColorBlau | AvsCardColorRot | AvsCardColorGelb | AvsCardColorMisc Text data AvsDataCardColor = AvsCardColorGrün | AvsCardColorBlau | AvsCardColorRot | AvsCardColorGelb | AvsCardColorMisc Text
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (NFData)
instance ToJSON AvsDataCardColor where instance ToJSON AvsDataCardColor where
toJSON AvsCardColorGrün = "Grün" toJSON AvsCardColorGrün = "Grün"
@ -82,6 +98,7 @@ instance FromJSON AvsDataCardColor where
_ -> pure $ AvsCardColorMisc t _ -> pure $ AvsCardColorMisc t
parseJSON invalid = prependFailure "parsing AvsDataCardColor failed, " (typeMismatch "String" invalid) parseJSON invalid = prependFailure "parsing AvsDataCardColor failed, " (typeMismatch "String" invalid)
data AvsDataPersonCard = AvsDataPersonCard data AvsDataPersonCard = AvsDataPersonCard
{ avsDataValid :: Bool -- card currently valid? Note that AVS encodes booleans as JSON String "true" and "false" and not as JSON booleans { avsDataValid :: Bool -- card currently valid? Note that AVS encodes booleans as JSON String "true" and "false" and not as JSON booleans
, avsDataValidTo :: Maybe Day -- always Nothing if returned with AvsResponseStatus , avsDataValidTo :: Maybe Day -- always Nothing if returned with AvsResponseStatus
@ -96,6 +113,7 @@ data AvsDataPersonCard = AvsDataPersonCard
, avsDataVersionNo :: Text , avsDataVersionNo :: Text
} }
deriving (Eq, Ord, Read, Show, Generic, Typeable) deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriving anyclass (NFData)
{- Automatically derived Ord instance should prioritize avsDataValid and avsDataValidTo. Checked in test/Model.TypesSpec {- Automatically derived Ord instance should prioritize avsDataValid and avsDataValidTo. Checked in test/Model.TypesSpec
instance Ord AvsDataPersonCard where instance Ord AvsDataPersonCard where
@ -139,6 +157,10 @@ instance ToJSON AvsDataPersonCard where
, "PostalCode" .= avsDataPostalCode , "PostalCode" .= avsDataPostalCode
] ]
derivePersistFieldJSON ''AvsDataPersonCard
makeLenses_ ''AvsDataPersonCard -- not possible here due to module import cycle
data AvsStatusPerson = AvsStatusPerson data AvsStatusPerson = AvsStatusPerson
{ avsStatusPersonID :: AvsPersonId { avsStatusPersonID :: AvsPersonId
, avsStatusPersonCardStatus :: Set AvsDataPersonCard , avsStatusPersonCardStatus :: Set AvsDataPersonCard
@ -171,6 +193,19 @@ deriveJSON defaultOptions
} ''AvsDataPerson } ''AvsDataPerson
data AvsDataLicence = AvsDataLicence
{ avsLicencePersonId :: AvsPersonId
, avsLicenceLicence :: AvsLicence
}
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions
{ fieldLabelModifier = dropCamel 2
, omitNothingFields = True
, tagSingleConstructors = False
, rejectUnknownFields = False
} ''AvsDataLicence
-------------- --------------
-- Responses -- -- Responses --
@ -221,6 +256,11 @@ newtype AvsQueryStatus = AvsQueryStatus (Set AvsPersonId)
deriveJSON defaultOptions ''AvsQueryStatus deriveJSON defaultOptions ''AvsQueryStatus
newtype AvsGetLicences = AvsGetLicences (Set AvsDataLicence)
deriving (Eq, Ord, Read, Show, Generic, Typeable)
deriveJSON defaultOptions ''AvsGetLicences
----------------------- -----------------------
-- Utility Functions -- -- Utility Functions --
----------------------- -----------------------
@ -229,20 +269,21 @@ deriveJSON defaultOptions ''AvsQueryStatus
-- first argument is a lower bound for avsDataValidTo, usually current day -- first argument is a lower bound for avsDataValidTo, usually current day
-- Note that avsDataValidTo is Nothing if retrieved via AvsResponseStatus (simply use isJust on result in this case) -- Note that avsDataValidTo is Nothing if retrieved via AvsResponseStatus (simply use isJust on result in this case)
getValidLicence :: Maybe Day -> AvsLicence -> Set AvsDataPersonCard -> Maybe AvsDataPersonCard getValidLicence :: Maybe Day -> AvsLicence -> Set AvsDataPersonCard -> Maybe AvsDataPersonCard
getValidLicence cutoff licence cards = Set.lookupMax validLicenceCards getValidLicence cutoff licence' cards = Set.lookupMax validLicenceCards
where where
licence = licence2char licence'
validLicenceCards = Set.filter cardMatch cards validLicenceCards = Set.filter cardMatch cards
cardMatch AvsDataPersonCard{..} = cardMatch AvsDataPersonCard{..} =
avsDataValid && (avsDataValidTo >= cutoff) && (licence `Set.member` avsDataCardAreas) avsDataValid && (avsDataValidTo >= cutoff) && (licence `Set.member` avsDataCardAreas)
guessLicencseAddress :: Set AvsDataPersonCard -> Maybe Text guessLicenceAddress :: Set AvsDataPersonCard -> Maybe (Maybe Text, Text)
guessLicencseAddress cards guessLicenceAddress cards
| Just c <- Set.lookupMax cards | Just c <- Set.lookupMax cards
, AvsDataPersonCard{..} <- Set.foldr pickLicenceAddress c cards , AvsDataPersonCard{..} <- Set.foldr pickLicenceAddress c cards
, Just street <- avsDataStreet , Just street <- avsDataStreet
, Just pcode <- avsDataPostalCode , Just pcode <- avsDataPostalCode
, Just city <- avsDataCity , Just city <- avsDataCity
= Just $ Text.unlines [street, Text.unwords [pcode, city]] = Just (avsDataFirm, Text.unlines [street, Text.unwords [pcode, city]])
| otherwise = Nothing | otherwise = Nothing
hasAddress :: AvsDataPersonCard -> Bool hasAddress :: AvsDataPersonCard -> Bool
@ -264,6 +305,8 @@ pickLicenceAddress a b
where where
pickBetter' :: (AvsDataPersonCard -> Bool) -> Maybe AvsDataPersonCard pickBetter' :: (AvsDataPersonCard -> Bool) -> Maybe AvsDataPersonCard
pickBetter' = pickBetter a b pickBetter' = pickBetter a b
licenceRollfeld = licence2char AvsLicenceRollfeld
licenceVorfeld = licence2char AvsLicenceVorfeld
{- Note: {- Note:
For Semigroup Ordering, (<>) ignores the righthand side except for EQ; this can be conveniently be used like so For Semigroup Ordering, (<>) ignores the righthand side except for EQ; this can be conveniently be used like so

View File

@ -1,7 +1,7 @@
module Utils.Avs where module Utils.Avs where
import Import.NoModel hiding ((.=)) import Import.NoModel
import Utils.Lens hiding ((.=)) import Utils.Lens
import Servant import Servant
import Servant.Client import Servant.Client
@ -13,9 +13,11 @@ import Model.Types.Avs
------------- -------------
-- AVS API -- -- AVS API --
------------- -------------
type AVS = BasicAuth "avs_fradrive" String :> "FraVSMService" :> "v1" :> (AVSPersonSearch :<|> AVSPersonStatus) type AVS = BasicAuth "avs_fradrive" String :> "FraVSMService" :> "v1" :> (AVSPersonSearch :<|> AVSPersonStatus :<|> AVSSetRampLicence :<|> AVSGetRampLicences)
type AVSPersonSearch = "PersonSearch" :> ReqBody '[JSON] AvsQueryPerson :> Post '[JSON] AvsResponsePerson type AVSPersonSearch = "PersonSearch" :> ReqBody '[JSON] AvsQueryPerson :> Post '[JSON] AvsResponsePerson
type AVSPersonStatus = "PersonStatus" :> ReqBody '[JSON] AvsQueryStatus :> Post '[JSON] AvsResponseStatus type AVSPersonStatus = "PersonStatus" :> ReqBody '[JSON] AvsQueryStatus :> Post '[JSON] AvsResponseStatus
type AVSSetRampLicence = "SetRampDrivingLicence" :> ReqBody '[JSON] AvsDataLicence :> Post '[JSON] ()
type AVSGetRampLicences = "InfoRampDrivingLicence" :> Post '[JSON] AvsGetLicences
avsApi :: Proxy AVS avsApi :: Proxy AVS
avsApi = Proxy avsApi = Proxy
@ -30,20 +32,23 @@ data AvsQuery where
-} -}
data AvsQuery = AvsQuery data AvsQuery = AvsQuery
{ avsQueryPerson :: forall m. MonadIO m => AvsQueryPerson -> m (Either ClientError AvsResponsePerson) { avsQueryPerson :: forall m. MonadIO m => AvsQueryPerson -> m (Either ClientError AvsResponsePerson)
, avsQueryStatus :: forall m. MonadIO m => AvsQueryStatus -> m (Either ClientError AvsResponseStatus) , avsQueryStatus :: forall m. MonadIO m => AvsQueryStatus -> m (Either ClientError AvsResponseStatus)
, avsQuerySetLicence :: forall m. MonadIO m => AvsDataLicence -> m (Either ClientError ())
, avsQueryGetLicences :: forall m. MonadIO m => m (Either ClientError AvsGetLicences)
} }
makeLenses_ ''AvsQuery makeLenses_ ''AvsQuery
mkAvsQuery :: BaseUrl -> BasicAuthData -> ClientEnv -> AvsQuery mkAvsQuery :: BaseUrl -> BasicAuthData -> ClientEnv -> AvsQuery
mkAvsQuery baseUrl basicAuth cliEnv = AvsQuery mkAvsQuery baseUrl basicAuth cliEnv = AvsQuery
{ avsQueryPerson = \q -> liftIO $ catch404toEmpty <$> runClientM (rawQueryPerson q) cliEnv { avsQueryPerson = \q -> liftIO $ catch404toEmpty <$> runClientM (rawQueryPerson q) cliEnv
, avsQueryStatus = \q -> liftIO $ runClientM (rawQueryStatus q) cliEnv , avsQueryStatus = \q -> liftIO $ runClientM (rawQueryStatus q) cliEnv
, avsQuerySetLicence = \q -> liftIO $ runClientM (rawQuerySetLicence q) cliEnv
, avsQueryGetLicences = liftIO $ runClientM rawQueryGetLicences cliEnv
} }
where where
(rawQueryPerson :<|> rawQueryStatus) = client avsApi basicAuth (rawQueryPerson :<|> rawQueryStatus :<|> rawQuerySetLicence :<|> rawQueryGetLicences) = client avsApi basicAuth
catch404toEmpty :: Either ClientError AvsResponsePerson -> Either ClientError AvsResponsePerson catch404toEmpty :: Either ClientError AvsResponsePerson -> Either ClientError AvsResponsePerson
catch404toEmpty (Left (FailureResponse (requestPath -> (base, _path)) (statusCode . responseStatusCode -> 404))) catch404toEmpty (Left (FailureResponse (requestPath -> (base, _path)) (statusCode . responseStatusCode -> 404)))
| baseUrl == base = Right $ AvsResponsePerson mempty -- WORKAROUND: AVS server erroneously returns 404 if no matching person could be found in its database! | baseUrl == base = Right $ AvsResponsePerson mempty -- WORKAROUND: AVS server erroneously returns 404 if no matching person could be found in its database!
@ -55,4 +60,5 @@ mkAvsQuery baseUrl basicAuth cliEnv = AvsQuery
- add query to Auth.LDAP.campusUserMatr - add query to Auth.LDAP.campusUserMatr
- add query to Auth.LDAP.campusLogin - add query to Auth.LDAP.campusLogin
- jobs.Handler.dispatchJobSynchroniseLdap - jobs.Handler.dispatchJobSynchroniseLdap
-} -}