feat(apis): version negotiation

This commit is contained in:
Gregor Kleen 2021-02-15 23:18:35 +01:00
parent 39c0c44c2a
commit 76e0bcf693
12 changed files with 311 additions and 28 deletions

View File

@ -174,6 +174,7 @@ dependencies:
- network-uri
- psqueues
- nonce
- semver
other-extensions:
- GeneralizedNewtypeDeriving
- IncoherentInstances

View File

@ -0,0 +1,31 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Data.SemVer.Instances
() where
import ClassyPrelude
import qualified Data.SemVer as SemVer
import qualified Data.SemVer.Constraint as SemVer (Constraint(..))
import qualified Data.SemVer.Constraint as SemVer.Constraint
import Web.HttpApiData
instance ToHttpApiData SemVer.Version where
toUrlPiece = SemVer.toText
instance ToHttpApiData SemVer.Constraint where
toUrlPiece SemVer.CAny = "*"
toUrlPiece (SemVer.CLt v) = "<" <> toUrlPiece v
toUrlPiece (SemVer.CLtEq v) = "<=" <> toUrlPiece v
toUrlPiece (SemVer.CGt v) = ">" <> toUrlPiece v
toUrlPiece (SemVer.CGtEq v) = ">=" <> toUrlPiece v
toUrlPiece (SemVer.CEq v) = toUrlPiece v
toUrlPiece (SemVer.CAnd a b) = toUrlPiece a <> " " <> toUrlPiece b
toUrlPiece (SemVer.COr a b) = toUrlPiece a <> " || " <> toUrlPiece b
instance FromHttpApiData SemVer.Version where
parseUrlPiece = first pack . SemVer.fromText
instance FromHttpApiData SemVer.Constraint where
parseUrlPiece = first pack . SemVer.Constraint.fromText

View File

@ -133,8 +133,8 @@ type UniWorXContext = UniWorX ': '[]
type ServantHandler = ServantHandlerFor UniWorX
type ServantDB = ServantDBFor UniWorX
class (HasServer (ServantApiUnproxy proxy) UniWorXContext, Servant.HasRoute (ServantApiUnproxy proxy)) => ServantApiDispatchUniWorX proxy where
servantServer' :: ServantApi proxy -> ServerT (ServantApiUnproxy proxy) ServantHandler
class (HasServer (ServantApiUnproxy' proxy) UniWorXContext, Servant.HasRoute (ServantApiUnproxy' proxy)) => ServantApiDispatchUniWorX proxy where
servantServer' :: ServantApi proxy -> ServerT (ServantApiUnproxy' proxy) ServantHandler
instance ServantApiDispatchUniWorX proxy => ServantApiDispatch UniWorXContext ServantHandler UniWorX proxy where
servantContext _ app _ = return $ app :. EmptyContext

View File

@ -4,6 +4,7 @@ module Foundation.Servant.Types
( CaptureBearerRestriction, CaptureBearerRestriction'
, CaptureBearerToken, CaptureBearerToken'
, CaptureCryptoID', CaptureCryptoID, CaptureCryptoUUID, CaptureCryptoFileName
, ApiVersion, apiVersionToSemVer, matchesApiVersion
) where
import ClassyPrelude
@ -13,20 +14,34 @@ import Servant.API
import Servant.API.Description
import Servant.Swagger
import Servant.Docs
import Servant.Server
import Servant.Server.Internal.Router
import Servant.Server.Internal.RouteResult
import Servant.Server.Internal.Delayed
import Servant.Server.Internal.ErrorFormatter
-- import Servant.Server.Internal.DelayedIO
import Control.Lens
import Network.Wai (mapResponseHeaders, requestHeaders)
import Control.Lens hiding (Context)
import Data.UUID (UUID)
import Data.CaseInsensitive (CI)
import Data.CryptoID.Class.ImplicitNamespace
import Data.CryptoID.Instances ()
import GHC.TypeLits (Symbol, KnownSymbol)
import GHC.TypeLits
import Data.Swagger (ToParamSchema)
import Data.Kind (Type)
import qualified Data.SemVer as SemVer
import qualified Data.SemVer.Constraint as SemVer (Constraint)
import qualified Data.SemVer.Constraint as SemVer.Constraint
import Data.SemVer.Instances ()
type CaptureBearerRestriction = CaptureBearerRestriction' '[Required]
data CaptureBearerRestriction' (mods :: [Type]) (restr :: Type)
@ -39,6 +54,26 @@ type CaptureCryptoID = CaptureCryptoID' '[]
type CaptureCryptoUUID = CaptureCryptoID UUID
type CaptureCryptoFileName = CaptureCryptoID (CI FilePath)
data ApiVersion (major :: Nat) (minor :: Nat) (patch :: Nat)
apiVersionToSemVer :: forall major minor patch p.
( KnownNat major, KnownNat minor, KnownNat patch )
=> p (ApiVersion major minor patch)
-> SemVer.Version
apiVersionToSemVer _ = SemVer.version
(fromIntegral . natVal $ Proxy @major)
(fromIntegral . natVal $ Proxy @minor)
(fromIntegral . natVal $ Proxy @patch)
[]
[]
matchesApiVersion :: forall major minor patch p.
( KnownNat major, KnownNat minor, KnownNat patch )
=> p (ApiVersion major minor patch)
-> SemVer.Constraint
-> Bool
matchesApiVersion _ = SemVer.Constraint.satisfies . apiVersionToSemVer $ Proxy @(ApiVersion major minor patch)
instance HasLink sub => HasLink (CaptureBearerRestriction' mods restr :> sub) where
type MkLink (CaptureBearerRestriction' mods restr :> sub) r = MkLink sub r
@ -52,6 +87,10 @@ instance (HasLink sub, ToHttpApiData ciphertext) => HasLink (CaptureCryptoID' mo
type MkLink (CaptureCryptoID' mods ciphertext sym plaintext :> sub) r = MkLink (Capture' mods sym (CryptoID ciphertext plaintext) :> sub) r
toLink toA _ = toLink toA $ Proxy @(Capture' mods sym (CryptoID ciphertext plaintext) :> sub)
instance HasLink sub => HasLink (ApiVersion major minor patch :> sub) where
type MkLink (ApiVersion major minor patch :> sub) r = MkLink sub r
toLink toA _ = toLink toA $ Proxy @sub
instance HasSwagger sub => HasSwagger (CaptureBearerRestriction' mods restr :> sub) where
toSwagger _ = toSwagger $ Proxy @sub
@ -61,6 +100,9 @@ instance HasSwagger sub => HasSwagger (CaptureBearerToken' mods :> sub) where
instance (HasSwagger sub, ToParamSchema ciphertext, KnownSymbol sym, KnownSymbol (FoldDescription mods)) => HasSwagger (CaptureCryptoID' mods ciphertext sym plaintext :> sub) where
toSwagger _ = toSwagger $ Proxy @(Capture' mods sym (CryptoID ciphertext plaintext) :> sub)
instance HasSwagger sub => HasSwagger (ApiVersion major minor patch :> sub) where
toSwagger _ = toSwagger $ Proxy @sub
instance HasDocs sub => HasDocs (CaptureBearerRestriction' mods restr :> sub) where
docsFor _ (endpoint, action) = docsFor (Proxy @sub) (endpoint, action')
where action' = action & notes <>~ [DocNote "Bearer restrictions" ["The behaviour of this route dependes on the restrictions stored for it in the bearer token used for authorization"]]
@ -72,4 +114,152 @@ instance HasDocs sub => HasDocs (CaptureBearerToken' mods :> sub) where
instance (ToCapture (Capture sym ciphertext), KnownSymbol sym, HasDocs sub) => HasDocs (CaptureCryptoID' mods ciphertext sym plaintext :> sub) where
docsFor _ = docsFor $ Proxy @(Capture' mods sym ciphertext :> sub)
type instance IsElem' (CaptureCryptoID' mods ciphertext sym plaintext :> sub) api = IsElem (Capture' mods sym (CryptoID ciphertext plaintext) :> sub) api
type family ApiVersionSub major minor patch sup sub where
ApiVersionSub major minor patch (ApiVersion major' minor' patch') sub = ApiVersion major' minor' patch' :> sub
ApiVersionSub major minor patch sup sub = sup :> (ApiVersion major minor patch :> sub)
instance HasServer (ApiVersionSub major minor patch sup sub) context => HasServer (ApiVersion major minor patch :> ((sup :: Type) :> sub)) context where
type ServerT (ApiVersion major minor patch :> (sup :> sub)) m = ServerT (ApiVersionSub major minor patch sup sub) m
hoistServerWithContext _ = hoistServerWithContext $ Proxy @(ApiVersionSub major minor patch sup sub)
route _ = route $ Proxy @(ApiVersionSub major minor patch sup sub)
instance HasServer (sup :> (ApiVersion major minor patch :> sub)) context => HasServer (ApiVersion major minor patch :> ((sup :: Symbol) :> sub)) context where
type ServerT (ApiVersion major minor patch :> (sup :> sub)) m = ServerT (sup :> (ApiVersion major minor patch :> sub)) m
hoistServerWithContext _ = hoistServerWithContext $ Proxy @(sup :> (ApiVersion major minor patch :> sub))
route _ = route $ Proxy @(sup :> (ApiVersion major minor patch :> sub))
instance ( HasServer (ApiVersion major minor patch :> a) context
, HasServer (ApiVersion major minor patch :> b) context
, SBoolI (IsLT (CmpVersion (FinalApiVersion (ApiVersion major minor patch :> a)) (FinalApiVersion (ApiVersion major minor patch :> b))))
) => HasServer (ApiVersion major minor patch :> (a :<|> b)) context where
type ServerT (ApiVersion major minor patch :> (a :<|> b)) m = ServerT (ApiVersion major minor patch :> a) m :<|> ServerT (ApiVersion major minor patch :> b) m
hoistServerWithContext _ = hoistServerWithContext $ Proxy @((ApiVersion major minor patch :> a) :<|> (ApiVersion major minor patch :> b))
route Proxy context server = choice'
(route (Proxy @(ApiVersion major minor patch :> a)) context $ (\(a :<|> _) -> a) <$> server)
(route (Proxy @(ApiVersion major minor patch :> b)) context $ (\(_ :<|> b) -> b) <$> server)
where
choice' :: forall env' a'. Router' env' a' -> Router' env' a' -> Router' env' a'
choice' = case (sbool :: SBool (IsLT (CmpVersion (FinalApiVersion (ApiVersion major minor patch :> a)) (FinalApiVersion (ApiVersion major minor patch :> b))))) of
STrue -> flip choice
SFalse -> choice
routeWithApiVersion :: forall api context env major minor patch.
( HasServer api context
, KnownNat major, KnownNat minor, KnownNat patch
, HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters
)
=> Proxy (ApiVersion major minor patch)
-> Proxy api -> Context context -> Delayed env (Server api) -> Router env
routeWithApiVersion _ _ context subserver = RawRouter $ \env req ((. addVersion) -> cont) -> case maybe (pure SemVer.Constraint.CAny) parseHeader . lookup versionRequestHeaderName $ requestHeaders req of
Left parseErr -> cont $ FailFatal err400 { errBody = encodeUtf8 . fromStrict $ "Could not parse version constraint: " <> parseErr }
Right vHdr -> if
| version `SemVer.Constraint.satisfies` vHdr -> runRouterEnv notFound (route (Proxy @api) context subserver) env req cont
| otherwise -> cont $ Fail err400 { errBody = encodeUtf8 "Requested version could not be satisfied" }
where addVersion (Fail sError) = Fail sError { errHeaders = addVersionHeader $ errHeaders sError}
addVersion (FailFatal sError) = FailFatal sError { errHeaders = addVersionHeader $ errHeaders sError }
addVersion (Route resp) = Route $ mapResponseHeaders addVersionHeader resp
addVersionHeader hdrs
| has (folded . _1 . only versionHeaderName) hdrs = hdrs
| otherwise = hdrs <> pure (versionHeaderName, versionHeader)
version = apiVersionToSemVer $ Proxy @(ApiVersion major minor patch)
versionHeaderName = "API-Version"
versionRequestHeaderName = "Accept-API-Version"
versionHeader = encodeUtf8 $ SemVer.toText version
notFound = notFoundErrorFormatter . getContextEntry $ mkContextWithErrorFormatter context
instance ( HasServer (Verb method statusCode contentTypes a) context
, KnownNat major, KnownNat minor, KnownNat patch
, HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters
) => HasServer (ApiVersion major minor patch :> Verb method statusCode contentTypes a) context where
type ServerT (ApiVersion major minor patch :> Verb method statusCode contentTypes a) m = ServerT (Verb method statusCode contentTypes a) m
hoistServerWithContext _ = hoistServerWithContext $ Proxy @(Verb method statusCode contentTypes a)
route _ = routeWithApiVersion (Proxy @(ApiVersion major minor patch)) (Proxy @(Verb method statusCode contentTypes a))
instance ( HasServer (NoContentVerb method) context
, KnownNat major, KnownNat minor, KnownNat patch
, HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters
) => HasServer (ApiVersion major minor patch :> NoContentVerb method) context where
type ServerT (ApiVersion major minor patch :> NoContentVerb method) m = ServerT (NoContentVerb method) m
hoistServerWithContext _ = hoistServerWithContext $ Proxy @(NoContentVerb method)
route _ = routeWithApiVersion (Proxy @(ApiVersion major minor patch)) (Proxy @(NoContentVerb method))
instance ( HasDocs (ApiVersionSub major minor patch sup sub)
) => HasDocs (ApiVersion major minor patch :> ((sup :: Type) :> sub)) where
docsFor _ = docsFor $ Proxy @(ApiVersionSub major minor patch sup sub)
instance ( HasDocs (sup :> (ApiVersion major minor patch :> sub))
) => HasDocs (ApiVersion major minor patch :> ((sup :: Symbol) :> sub)) where
docsFor _ = docsFor $ Proxy @(sup :> (ApiVersion major minor patch :> sub))
instance ( HasDocs (ApiVersion major minor patch :> a)
, HasDocs (ApiVersion major minor patch :> b)
) => HasDocs (ApiVersion major minor patch :> (a :<|> b)) where
docsFor _ = docsFor $ Proxy @(ApiVersion major minor patch :> a :<|> ApiVersion major minor patch :> b)
apiVersionDocNote :: forall major minor patch.
( KnownNat major, KnownNat minor, KnownNat patch )
=> Proxy (ApiVersion major minor patch)
-> DocNote
apiVersionDocNote p = DocNote "Versioning" ["This route is provided in version " <> SemVer.toString (apiVersionToSemVer p)]
instance ( HasDocs (Verb method statusCode contentTypes a)
, KnownNat major, KnownNat minor, KnownNat patch
) => HasDocs (ApiVersion major minor patch :> Verb method statusCode contentTypes a) where
docsFor _ (endpoint, action) = docsFor (Proxy @(Verb method statusCode contentTypes a)) (endpoint, action')
where action' = action & notes <>~ [apiVersionDocNote $ Proxy @(ApiVersion major minor patch)]
instance ( HasDocs (NoContentVerb method)
, KnownNat major, KnownNat minor, KnownNat patch
) => HasDocs (ApiVersion major minor patch :> NoContentVerb method) where
docsFor _ (endpoint, action) = docsFor (Proxy @(NoContentVerb method)) (endpoint, action')
where action' = action & notes <>~ [apiVersionDocNote $ Proxy @(ApiVersion major minor patch)]
type family FinalApiVersion api where
FinalApiVersion (ApiVersion major minor patch :> sub) = AlternativeMaybe (FinalApiVersion sub) ('Just (ApiVersion major minor patch))
FinalApiVersion (sup :> sub) = FinalApiVersion sub
FinalApiVersion (a :<|> b) = MaxMaybe (CmpVersion (FinalApiVersion a) (FinalApiVersion b)) (FinalApiVersion a) (FinalApiVersion b)
FinalApiVersion (Verb method statusCode contentTypes a) = 'Nothing
FinalApiVersion (NoContentVerb method) = 'Nothing
type family MaxMaybe ord a b where
MaxMaybe _ a 'Nothing = a
MaxMaybe _ 'Nothing b = b
MaxMaybe 'LT _ b = b
MaxMaybe _ a _ = a
type family MappendOrdering a b where
MappendOrdering 'EQ b = b
MappendOrdering a _ = a
type family AlternativeMaybe a b where
AlternativeMaybe ('Just a) _ = 'Just a
AlternativeMaybe _ ('Just b) = 'Just b
AlternativeMaybe _ _ = 'Nothing
type family CmpVersion x y where
CmpVersion 'Nothing 'Nothing = 'EQ
CmpVersion 'Nothing _ = 'GT
CmpVersion _ 'Nothing = 'LT
CmpVersion ('Just (ApiVersion major minor patch)) ('Just (ApiVersion major' minor' patch')) = MappendOrdering (CmpNat major major') (MappendOrdering (CmpNat minor minor') (CmpNat patch patch'))
type family IsLT x where
IsLT 'LT = 'True
IsLT _ = 'False
type instance IsElem' sa (CaptureCryptoID' mods ciphertext sym plaintext :> sb) = IsElem sa (Capture' mods sym (CryptoID ciphertext plaintext) :> sb)
type instance IsElem' sa (ApiVersion major minor patch :> sb) = IsElem sa sb

View File

@ -200,6 +200,7 @@ import Text.Shakespeare.Text.Instances as Import ()
import Ldap.Client.Instances as Import ()
import Data.MultiSet.Instances as Import ()
import Control.Arrow.Instances as Import ()
import Data.SemVer.Instances as Import ()
import Crypto.Hash as Import (Digest, SHA3_256, SHA3_512)
import Crypto.Random as Import (ChaChaDRG, Seed)

View File

@ -37,7 +37,7 @@ data ExternalApis mode = ExternalApis
} deriving (Generic)
type ServantApiExternalApis = ServantApi ExternalApis
type instance ServantApiUnproxy ExternalApis = ToServantApi ExternalApis
type instance ServantApiUnproxy ExternalApis = ApiVersion 1 0 0 :> ToServantApi ExternalApis
instance ToCapture (Capture "external-api" UUID) where

View File

@ -1,7 +1,8 @@
{-# OPTIONS_GHC -fno-warn-unused-foralls #-}
{-# LANGUAGE UndecidableInstances #-}
module Yesod.Servant
( ServantApiUnproxy, ServantApiDirect
( ServantApiUnproxy, ServantApiUnproxy', ServantApiDirect
, HasRoute(..)
, ServantApi(..), getServantApi
, ServantApiDispatch(..)
@ -83,7 +84,7 @@ import Data.Typeable (eqT, typeRep)
import Network.URI
import Network.URI.Lens
import GHC.TypeLits (KnownSymbol, symbolVal)
import GHC.TypeLits (KnownSymbol, symbolVal, KnownNat)
import Text.Read (Read(readPrec), readP_to_Prec, readPrec_to_P)
import Text.Show (showParen, showString)
@ -114,7 +115,7 @@ escapedSymbol :: forall sym. KnownSymbol sym => Proxy sym -> Text
escapedSymbol _ = pack . escapeURIString isUnreserved . symbolVal $ Proxy @sym
class HasLink api => HasRoute api where
parseServantRoute :: forall proxy. ServantApiUnproxy proxy ~ api => ([Text], [(Text, Text)]) -> Maybe (Route (ServantApi proxy))
parseServantRoute :: forall proxy. ServantApiUnproxy' proxy ~ api => ([Text], [(Text, Text)]) -> Maybe (Route (ServantApi proxy))
instance HasRoute EmptyAPI where
parseServantRoute _ = Nothing
@ -212,6 +213,9 @@ instance (HasRoute sub, Typeable mods, KnownSymbol sym, Typeable plaintext, ToHt
ServantApiRoute (_ :: Proxy endpoint) f ps' qs' -> ServantApiRoute (Proxy @(CaptureCryptoID' mods ciphertext sym plaintext :> endpoint)) (f . ($ v)) (toUrlPieceInjective v : ps') qs'
parseServantRoute _ = Nothing
instance (HasRoute sub, KnownNat major, KnownNat minor, KnownNat patch) => HasRoute (ApiVersion major minor patch :> sub) where
parseServantRoute args = parseServantRoute @sub @(ServantApiDirect sub) args <&> \case
ServantApiRoute (_ :: Proxy endpoint) f ps qs -> ServantApiRoute (Proxy @(ApiVersion major minor patch :> endpoint)) f ps qs
data ServantApi (proxy :: k) = ServantApi
@ -223,13 +227,18 @@ getServantApi _ = ServantApi
type family ServantApiUnproxy (proxy :: k) :: Type
type ServantApiUnproxy' :: forall k. forall (proxy :: k) -> Type
type family ServantApiUnproxy' proxy where
ServantApiUnproxy' @Type (ServantApiDirect api) = api
ServantApiUnproxy' @k' proxy = ServantApiUnproxy proxy
data ServantApiDirect (api :: Type)
type instance ServantApiUnproxy (ServantApiDirect api) = api
instance HasRoute (ServantApiUnproxy proxy) => RenderRoute (ServantApi proxy) where
instance HasRoute (ServantApiUnproxy' proxy) => RenderRoute (ServantApi proxy) where
data Route (ServantApi proxy)
= forall endpoint.
( IsElem endpoint (ServantApiUnproxy proxy) ~ (() :: Constraint)
( IsElem endpoint (ServantApiUnproxy' proxy) ~ (() :: Constraint)
, HasRoute endpoint
, Typeable endpoint
)
@ -237,24 +246,24 @@ instance HasRoute (ServantApiUnproxy proxy) => RenderRoute (ServantApi proxy) wh
(Proxy endpoint)
(forall a. MkLink endpoint a -> a)
[Text] (HashMap Text [Text])
renderRoute (ServantApiRoute (_ :: Proxy endpoint) f _ _) = f $ safeLink' renderServantRoute (Proxy @(ServantApiUnproxy proxy)) (Proxy @endpoint)
renderRoute (ServantApiRoute (_ :: Proxy endpoint) f _ _) = f $ safeLink' renderServantRoute (Proxy @(ServantApiUnproxy' proxy)) (Proxy @endpoint)
instance HasRoute (ServantApiUnproxy proxy) => Eq (Route (ServantApi proxy)) where
instance HasRoute (ServantApiUnproxy' proxy) => Eq (Route (ServantApi proxy)) where
(ServantApiRoute (_ :: Proxy endpoint) _ ps qs) == (ServantApiRoute (_ :: Proxy endpoint') _ ps' qs')
= case eqT @endpoint @endpoint' of
Just Refl -> ps == ps' && qs == qs'
Nothing -> False
instance HasRoute (ServantApiUnproxy proxy) => Ord (Route (ServantApi proxy)) where
instance HasRoute (ServantApiUnproxy' proxy) => Ord (Route (ServantApi proxy)) where
compare (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) (ServantApiRoute (_ :: Proxy endpoint') _ ps' qs')
= case eqT @endpoint @endpoint' of
Just Refl -> compare ps ps' <> compare qs qs'
Nothing -> typeRep (Proxy @endpoint) `compare` typeRep (Proxy @endpoint')
instance HasRoute (ServantApiUnproxy proxy) => Hashable (Route (ServantApi proxy)) where
instance HasRoute (ServantApiUnproxy' proxy) => Hashable (Route (ServantApi proxy)) where
hashWithSalt salt (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) = salt `hashWithSalt` typeRep (Proxy @endpoint) `hashWithSalt` ps `hashWithSalt` qs
instance HasRoute (ServantApiUnproxy proxy) => Read (Route (ServantApi proxy)) where
instance HasRoute (ServantApiUnproxy' proxy) => Read (Route (ServantApi proxy)) where
readPrec = readP_to_Prec $ \d -> do
when (d > 10) . void $ R.char '('
R.skipSpaces
@ -276,7 +285,7 @@ instance HasRoute (ServantApiUnproxy proxy) => Read (Route (ServantApi proxy)) w
R.skipSpaces
when (d > 10) . void $ R.char ')'
maybe (fail "Could not parse servant route") return $ parseServantRoute (ps, ifoldMap (fmap . (,)) qs)
instance HasRoute (ServantApiUnproxy proxy) => Show (Route (ServantApi proxy)) where
instance HasRoute (ServantApiUnproxy' proxy) => Show (Route (ServantApi proxy)) where
showsPrec d (ServantApiRoute (_ :: Proxy endpoint) _ ps qs) = showParen (d > 10)
$ showString "ServantApiRoute "
. showsPrec 11 (typeRep $ Proxy @endpoint)
@ -285,18 +294,18 @@ instance HasRoute (ServantApiUnproxy proxy) => Show (Route (ServantApi proxy)) w
. showString " "
. showsPrec 11 qs
instance HasRoute (ServantApiUnproxy proxy) => ParseRoute (ServantApi proxy) where
instance HasRoute (ServantApiUnproxy' proxy) => ParseRoute (ServantApi proxy) where
parseRoute = parseServantRoute
newtype ServantErrorResponse
= ServantErrorResponse { getServantErrorResponse :: W.Response }
class (HasServer (ServantApiUnproxy proxy) context, HasRoute (ServantApiUnproxy proxy), HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters) => ServantApiDispatch context m master proxy | master proxy -> context m where
class (HasServer (ServantApiUnproxy' proxy) context, HasRoute (ServantApiUnproxy' proxy), HasContextEntry (context .++ DefaultErrorFormatters) ErrorFormatters) => ServantApiDispatch context m master proxy | master proxy -> context m where
servantContext :: ServantApi proxy -> master -> Request -> Yesod.HandlerFor master (Context context)
servantHoist :: ServantApi proxy -> master -> Request -> Context context -> (forall a. m a -> Handler a)
servantMiddleware :: ServantApi proxy -> master -> Context context -> Middleware
servantYesodMiddleware :: ServantApi proxy -> master -> Yesod.HandlerFor master Middleware
servantServer :: ServantApi proxy -> master -> ServerT (ServantApiUnproxy proxy) m
servantServer :: ServantApi proxy -> master -> ServerT (ServantApiUnproxy' proxy) m
instance ServantApiDispatch context m master proxy => YesodSubDispatch (ServantApi proxy) master where
yesodSubDispatch YesodSubRunnerEnv{..} req
@ -316,7 +325,7 @@ instance ServantApiDispatch context m master proxy => YesodSubDispatch (ServantA
yesodMiddleware <- servantYesodMiddleware proxy master
ctx <- servantContext proxy master req
let server' = hoistServerWithContext (Proxy @(ServantApiUnproxy proxy)) (Proxy @context) (servantHoist proxy master req ctx) (servantServer proxy master)
let server' = hoistServerWithContext (Proxy @(ServantApiUnproxy' proxy)) (Proxy @context) (servantHoist proxy master req ctx) (servantServer proxy master)
toTypedContent = error "Servant handler did not shortcircuit"
sendResponse res = case yesodError of
Just err -> do
@ -358,14 +367,14 @@ instance ServantApiDispatch context m master proxy => YesodSubDispatch (ServantA
| otherwise = Nothing
fmap toTypedContent . withUnliftIO $ \UnliftIO{..} ->
(yesodMiddleware . servantMiddleware proxy master ctx $ serveWithContext (Proxy @(ServantApiUnproxy proxy)) ctx server') req $ unliftIO . sendResponse
(yesodMiddleware . servantMiddleware proxy master ctx $ serveWithContext (Proxy @(ServantApiUnproxy' proxy)) ctx server') req $ unliftIO . sendResponse
servantApiLink :: forall p1 p2 proxy endpoint.
( IsElem endpoint (ServantApiUnproxy proxy) ~ (() :: Constraint), HasRoute (ServantApiUnproxy proxy), HasLink endpoint, Typeable endpoint )
( IsElem endpoint (ServantApiUnproxy' proxy) ~ (() :: Constraint), HasRoute (ServantApiUnproxy' proxy), HasLink endpoint, Typeable endpoint )
=> p1 proxy
-> p2 endpoint
-> MkLink endpoint (Route (ServantApi proxy))
servantApiLink _ _ = safeLink' (fromMaybe (error "Could not parse result of safeLink'") . guardEndpoint . parseServantRoute @(ServantApiUnproxy proxy) . renderServantRoute) (Proxy @(ServantApiUnproxy proxy)) (Proxy @endpoint)
servantApiLink _ _ = safeLink' (fromMaybe (error "Could not parse result of safeLink'") . guardEndpoint . parseServantRoute @(ServantApiUnproxy' proxy) . renderServantRoute) (Proxy @(ServantApiUnproxy' proxy)) (Proxy @endpoint)
where
guardEndpoint :: Maybe (Route (ServantApi proxy)) -> Maybe (Route (ServantApi proxy))
guardEndpoint x@(Just (ServantApiRoute (_ :: Proxy endpoint') _ _ _))
@ -532,7 +541,7 @@ mkYesodApi (nameBase -> masterN) ress = do
Just tn <- lift $ lookupTypeName subsiteType
TyConI (TySynD _ [] (ConT conN `AppT` apiT)) <- lift $ reify tn
guard $ conN == ''ServantApi
return $ pure (ps <> resourcePieces, ConT ''ServantApiUnproxy `AppT` apiT, map pack resourceAttrs)
return $ pure (ps <> resourcePieces, ConT ''ServantApiUnproxy' `AppT` apiT, map pack resourceAttrs)
toPiecesApi ps (ResourceParent _ _ ps' cs)
= lift . fmap concat $ mapMaybeM (toPiecesApi (ps <> ps')) cs
apiRess <- concat <$> mapMaybeM (toPiecesApi []) ress

View File

@ -80,6 +80,7 @@ extra-deps:
- hlint-test-0.1.0.0@sha256:e427c0593433205fc629fb05b74c6b1deb1de72d1571f26142de008f0d5ee7a9,1814
- network-arbitrary-0.6.0.0@sha256:a7034d63295dfc41cf559ee705fc95cac9a9a01b4715300f590eaa237b5ffd48,2506
- servant-quickcheck-0.0.10.0@sha256:1d5849d703c2487752f8fc7391cca7c998ee24f54ca0bb72d238bf99b64ac667,3755
- servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
resolver: nightly-2021-01-11
compiler: ghc-8.10.3

View File

@ -359,6 +359,13 @@ packages:
sha256: 37dab60111c71d011fc4964e9a8b4b05ac544bc0ba8155e895518680066c2adb
original:
hackage: servant-quickcheck-0.0.10.0@sha256:1d5849d703c2487752f8fc7391cca7c998ee24f54ca0bb72d238bf99b64ac667,3755
- completed:
hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
pantry-tree:
size: 325
sha256: 04f12c7bef2c3f9a25d94eb9489752ed498db8e243069fe95838dbb51df1dcb3
original:
hackage: servant-flatten-0.2@sha256:276896f7c5cdec5b8f8493f6205fded0cc602d050b58fdb09a6d7c85c3bb0837,1234
snapshots:
- completed:
size: 562265

View File

@ -29,5 +29,8 @@ instance HasGenRequest sub => HasGenRequest (CaptureBearerToken' mods :> sub) wh
instance HasGenRequest sub => HasGenRequest (CaptureBearerRestriction' mods restr :> sub) where
genRequest _ = genRequest $ Proxy @sub
instance HasGenRequest sub => HasGenRequest (ApiVersion major minor patch :> sub) where
genRequest _ = genRequest $ Proxy @sub
spec :: Spec
spec = return ()

View File

@ -12,7 +12,7 @@ import Servant.QuickCheck.Internal.HasGenRequest (HasGenRequest(..))
import qualified Network.HTTP.Client as HTTP
import qualified Network.HTTP.Types.URI as URI
import Yesod.Servant (HasRoute(..), ServantApi, ServantApiUnproxy)
import Yesod.Servant (HasRoute(..), ServantApi, ServantApiUnproxy')
import Foundation.ServantSpec ()
import ServantApi.ExternalApis.TypeSpec ()
@ -36,9 +36,9 @@ instance Arbitrary (Route EmbeddedStatic) where
params <- replicateM paramNum $ (,) <$> printableText' <*> printableText
return $ embeddedResourceR path params
instance (HasRoute (ServantApiUnproxy api), HasGenRequest (ServantApiUnproxy api)) => Arbitrary (Route (ServantApi api)) where
instance (HasRoute (ServantApiUnproxy' api), HasGenRequest (ServantApiUnproxy' api)) => Arbitrary (Route (ServantApi api)) where
arbitrary = do
genReq <- view _2 . genRequest $ Proxy @(ServantApiUnproxy api)
genReq <- view _2 . genRequest $ Proxy @(ServantApiUnproxy' api)
let req = genReq $ BaseUrl Http "" 0 ""
p = filter (not . null) . URI.decodePathSegments $ HTTP.path req
qs = over (traverse . _2) (fromMaybe mempty) . URI.parseQueryText $ HTTP.queryString req

View File

@ -7,6 +7,8 @@ module Model.TypesSpec
import TestImport
import Settings
import Utils (guardOn)
import Data.Aeson (Value)
import qualified Data.Aeson as Aeson
import qualified Data.Aeson.Types as Aeson
@ -46,6 +48,10 @@ import qualified Data.Text.Lazy as LT
import Text.Blaze.Html.Renderer.Text (renderHtml)
import qualified Data.SemVer as SemVer
import qualified Data.SemVer.Constraint as SemVer (Constraint)
import qualified Data.SemVer.Constraint as SemVer.Constraint
instance Arbitrary Season where
@ -330,6 +336,36 @@ instance Arbitrary ExternalApiConfig where
arbitrary = genericArbitrary
shrink = genericShrink
instance Arbitrary SemVer.Version where
arbitrary = SemVer.version
<$> fmap getNonNegative arbitrary
<*> fmap getNonNegative arbitrary
<*> fmap getNonNegative arbitrary
<*> arbitrary
<*> arbitrary
instance Arbitrary SemVer.Identifier where
arbitrary = -- oneof
-- [ SemVer.numeric . getNonNegative <$> arbitrary -- Numeric does not roundtrip
{- , -} fmap (\s -> fromMaybe (error $ "Generated invalid SemVer.Identifier: " <> s) . SemVer.textual $ pack s) . listOf1 . elements $ ['A'..'Z'] <> ['a'..'z'] {- <> ['0'..'9'] -} <> ['-']
-- ]
deriving instance Generic SemVer.Constraint
instance Arbitrary SemVer.Constraint where
-- Syntax has no brackets; so be very careful about nesting
arbitrary = sized $ \n -> oneof $ catMaybes
[ pure unitary
, guardOn (n > 1) conj
, guardOn (n > 1) disj
]
where unitary = oneof
[ pure SemVer.Constraint.CAny
, elements [SemVer.Constraint.CLt, SemVer.Constraint.CLtEq, SemVer.Constraint.CGt, SemVer.Constraint.CGtEq, SemVer.Constraint.CEq] <*> arbitrary
]
conj = SemVer.Constraint.CAnd <$> unitary <*> sized (\n -> oneof $ catMaybes [pure unitary, guardOn (n > 1) $ scale (`div` 2) conj])
disj = SemVer.Constraint.COr <$> unitary <*> scale (`div` 2) arbitrary
spec :: Spec
@ -435,6 +471,10 @@ spec = do
[ eqLaws, ordLaws, finiteLaws, showReadLaws, pathPieceLaws, boundedEnumLaws ]
lawsCheckHspec (Proxy @(WorkflowScope TermIdentifier SchoolShorthand SqlBackendKey))
[ eqLaws, ordLaws, showLaws, showReadLaws, pathPieceLaws, jsonLaws, persistFieldLaws, binaryLaws ]
lawsCheckHspec (Proxy @SemVer.Version)
[ eqLaws, ordLaws, showLaws, hashableLaws, httpApiDataLaws ]
lawsCheckHspec (Proxy @SemVer.Constraint)
[ eqLaws, showLaws, httpApiDataLaws ]
describe "TermIdentifier" $ do
it "has compatible encoding/decoding to/from Text" . property $