diff --git a/package.yaml b/package.yaml index d8a6ce0b4..fe68fd2ff 100644 --- a/package.yaml +++ b/package.yaml @@ -174,6 +174,7 @@ dependencies: - network-uri - psqueues - nonce + - semver other-extensions: - GeneralizedNewtypeDeriving - IncoherentInstances diff --git a/src/Data/SemVer/Instances.hs b/src/Data/SemVer/Instances.hs new file mode 100644 index 000000000..51d60dfb2 --- /dev/null +++ b/src/Data/SemVer/Instances.hs @@ -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 diff --git a/src/Foundation/Servant.hs b/src/Foundation/Servant.hs index dd4b77b0d..c76d6511d 100644 --- a/src/Foundation/Servant.hs +++ b/src/Foundation/Servant.hs @@ -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 diff --git a/src/Foundation/Servant/Types.hs b/src/Foundation/Servant/Types.hs index de751eb82..3e4c8b4d1 100644 --- a/src/Foundation/Servant/Types.hs +++ b/src/Foundation/Servant/Types.hs @@ -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 diff --git a/src/Import/NoModel.hs b/src/Import/NoModel.hs index 552adaab6..dd52b4d26 100644 --- a/src/Import/NoModel.hs +++ b/src/Import/NoModel.hs @@ -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) diff --git a/src/ServantApi/ExternalApis/Type.hs b/src/ServantApi/ExternalApis/Type.hs index 12b7f64e1..181f2bca2 100644 --- a/src/ServantApi/ExternalApis/Type.hs +++ b/src/ServantApi/ExternalApis/Type.hs @@ -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 diff --git a/src/Yesod/Servant.hs b/src/Yesod/Servant.hs index cc2413f13..611727e6c 100644 --- a/src/Yesod/Servant.hs +++ b/src/Yesod/Servant.hs @@ -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 diff --git a/stack.yaml b/stack.yaml index 91987818a..4cd889f17 100644 --- a/stack.yaml +++ b/stack.yaml @@ -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 diff --git a/stack.yaml.lock b/stack.yaml.lock index 085e9fd4f..04e9c994d 100644 --- a/stack.yaml.lock +++ b/stack.yaml.lock @@ -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 diff --git a/test/Foundation/ServantSpec.hs b/test/Foundation/ServantSpec.hs index 94256fd60..57ae11006 100644 --- a/test/Foundation/ServantSpec.hs +++ b/test/Foundation/ServantSpec.hs @@ -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 () diff --git a/test/FoundationSpec.hs b/test/FoundationSpec.hs index 5f9c78300..5166550d8 100644 --- a/test/FoundationSpec.hs +++ b/test/FoundationSpec.hs @@ -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 diff --git a/test/Model/TypesSpec.hs b/test/Model/TypesSpec.hs index 032378e11..8f92ee3e9 100644 --- a/test/Model/TypesSpec.hs +++ b/test/Model/TypesSpec.hs @@ -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 $