packages feed

servant-util 0.2 → 0.3

raw patch · 13 files changed

+542/−32 lines, 13 filesdep +openapi3dep +servant-openapi3

Dependencies added: openapi3, servant-openapi3

Files

CHANGES.md view
@@ -1,6 +1,11 @@ Unreleased ===== +0.3+===++* Added support for `openapi3` by implementing necessary instances.+ 0.2 ===== 
README.md view
@@ -78,10 +78,10 @@ ```haskell  type GetBooks-    :> SortingParams+    =  SortingParams          ["isbn" ?: Isbn, "name" ?: Text, "author" ?: Text]         '["isbn" ?: 'Asc Isbn]-    =  Get '[JSON] [Book]+    :> Get '[JSON] [Book]  ``` @@ -134,8 +134,8 @@     ["isbn" ?: Asc Isbn]  type GetBooks-    :> SortingParamsOf Book  -- same as the definition at the section's top-    =  Get '[JSON] [Book]+    =  SortingParamsOf Book  -- same as the definition at the section's top+    :> Get '[JSON] [Book]  ``` @@ -157,8 +157,8 @@ ```haskell  type GetBooks-    :> FilteringParams ["isbn" ?: 'AutoFilter Isbn, "name" ?: 'AutoFilter Text]-    =  Get '[JSON] [Book]+    =  FilteringParams ["isbn" ?: 'AutoFilter Isbn, "name" ?: 'AutoFilter Text]+    :> Get '[JSON] [Book]  ``` @@ -196,8 +196,8 @@ ```haskell  type GetBooks-    :> FilteringParams ["hasLongName" ?: ManualFilter Bool]-    =  Get '[JSON] [Book]+    =  FilteringParams ["hasLongName" ?: ManualFilter Bool]+    :> Get '[JSON] [Book]  ``` @@ -244,8 +244,8 @@ import Servant.Util.Dummy (paginate)  type GetBooks-    :> PaginationParams-    =  Get '[JSON] [Book]+    =  PaginationParams+    :> Get '[JSON] [Book]  getBooks :: ToServer GetBooks getBooks pagination = paginate pagination <$> getAllBooks
+ examples/BooksOpenApi.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE DeriveAnyClass     #-}+{-# LANGUAGE DerivingStrategies #-}++module BooksOpenApi where++import Universum++import Data.Aeson (FromJSON, ToJSON)+import Data.Aeson.TH (defaultOptions, deriveJSON)+import Data.OpenApi (OpenApi, ToParamSchema, ToSchema)+import Fmt (Buildable (..), (+|), (|+))+import qualified Network.Wai.Handler.Warp as Warp+import Servant (FromHttpApiData, Get, JSON, PostCreated, QueryParam, ReqBody, Server, serve,+                (:<|>) (..), (:>))+import Servant.OpenApi (toOpenApi)+import Servant.Swagger.UI (SwaggerSchemaUI, swaggerSchemaUIServer)++import Servant.Util++newtype Isbn = Isbn Word64+    deriving stock (Eq, Show, Generic)+    deriving newtype (ToJSON, FromJSON, ToSchema, ToParamSchema, FromHttpApiData)++type instance SupportedFilters Isbn = '[FilterMatching, FilterComparing]++type instance ParamDescription Isbn = "ISBN of a book"+type instance ParamDescription Text = "Text"++data Book = Book+    { isbn     :: Isbn+    , bookName :: Text+    , author   :: Text+    }+  deriving (Generic)+  deriving anyclass (ToSchema)++deriveJSON defaultOptions 'Book++newtype Password = Password Text+  deriving stock (Generic)+  deriving newtype (FromHttpApiData, ToParamSchema)++type GetBooks+    =  SortingParams+         '["isbn" ?: Isbn, "name" ?: Text, "author" ?: Text]+         '["isbn" ?: 'Asc Isbn]+    :> FilteringParams ["isbn" ?: 'AutoFilter Isbn, "name" ?: 'AutoFilter Text]+    :> PaginationParams ('DefPageSize 20)+    :> Get '[JSON] [Book]++type AddBook+    =  QueryParam "password" Password+    :> ReqBody '[JSON] Book+    :> PostCreated '[JSON] Isbn++type BooksAPI = "books" :> (+    GetBooks :<|>+    AddBook+  )++openapi :: OpenApi+openapi = toOpenApi @BooksAPI Proxy++booksHandlers :: Server BooksAPI+booksHandlers =+    (\_sorting _filtering _pagination -> return [])+    :<|>+    (\_ book -> return (isbn book))++warpSettings :: Warp.Settings+warpSettings = Warp.defaultSettings+    & Warp.setHost "127.0.0.1"+    & Warp.setPort 8090++instance Buildable Isbn where+    build (Isbn i) = "isbn:" <> build i++instance Buildable Password where+    build _ = "<password>"++instance Buildable Book where+    build Book{..} =+        "{ isbn = " +| isbn |++        ", title = " +| bookName |++        ", author = " +| author |++        "}"++instance Buildable (ForResponseLog Isbn) where+    build = buildForResponse++instance Buildable (ForResponseLog Book) where+    build = buildForResponse++instance Buildable (ForResponseLog [Book]) where+    build = buildListForResponse (take 5)++serveBooksServer :: IO ()+serveBooksServer =+    Warp.runSettings warpSettings $+    serverWithLogging loggingConfig (Proxy @BooksAPI) $ \(Proxy :: Proxy api) ->+    serve @(SwaggerSchemaUI "swagger-ui" "swagger.json" :<|> api) Proxy+      (swaggerSchemaUIServer openapi :<|> booksHandlers)+  where+    loggingConfig = ServantLogConfig $ \_ -> putTextLn
servant-util.cabal view
@@ -1,13 +1,13 @@ cabal-version: 1.12 --- This file has been generated from package.yaml by hpack version 0.33.0.+-- This file has been generated from package.yaml by hpack version 0.34.6. -- -- see: https://github.com/sol/hpack ----- hash: 936cf3a2b61cfa35f3cca3316f3946e3e890066234f3e915b0190b1b41bb84a6+-- hash: f0ae0577e7cde4002a074bd5fada6d918ea3dc45baee134d38fa8e002ad66d9f  name:           servant-util-version:        0.2+version:        0.3 synopsis:       Servant servers utilities. description:    Basement for common Servant combinators like filtering, sorting, pagination and semantical logging. category:       Servant, Web@@ -73,7 +73,39 @@       Paths_servant_util   hs-source-dirs:       src-  default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings OverloadedLabels PatternSynonyms RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeFamilies TypeOperators UndecidableInstances ViewPatterns TypeApplications+  default-extensions:+      AllowAmbiguousTypes+      BangPatterns+      ConstraintKinds+      DataKinds+      DefaultSignatures+      DeriveDataTypeable+      DeriveGeneric+      EmptyCase+      FlexibleContexts+      FlexibleInstances+      FunctionalDependencies+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NoImplicitPrelude+      OverloadedStrings+      OverloadedLabels+      PatternSynonyms+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeFamilies+      TypeOperators+      UndecidableInstances+      ViewPatterns+      TypeApplications   ghc-options: -Wall   build-depends:       QuickCheck@@ -88,6 +120,7 @@     , lens     , megaparsec     , mtl+    , openapi3     , pretty-terminal     , reflection     , regex-posix@@ -95,6 +128,7 @@     , servant     , servant-client     , servant-client-core+    , servant-openapi3     , servant-server     , servant-swagger     , servant-swagger-ui@@ -111,10 +145,43 @@   main-is: Main.hs   other-modules:       Books+      BooksOpenApi       Paths_servant_util   hs-source-dirs:       examples-  default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings OverloadedLabels PatternSynonyms RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeFamilies TypeOperators UndecidableInstances ViewPatterns TypeApplications+  default-extensions:+      AllowAmbiguousTypes+      BangPatterns+      ConstraintKinds+      DataKinds+      DefaultSignatures+      DeriveDataTypeable+      DeriveGeneric+      EmptyCase+      FlexibleContexts+      FlexibleInstances+      FunctionalDependencies+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NoImplicitPrelude+      OverloadedStrings+      OverloadedLabels+      PatternSynonyms+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeFamilies+      TypeOperators+      UndecidableInstances+      ViewPatterns+      TypeApplications   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N -O2   build-depends:       QuickCheck@@ -129,6 +196,7 @@     , lens     , megaparsec     , mtl+    , openapi3     , pretty-terminal     , reflection     , regex-posix@@ -136,6 +204,7 @@     , servant     , servant-client     , servant-client-core+    , servant-openapi3     , servant-server     , servant-swagger     , servant-swagger-ui@@ -166,7 +235,39 @@       Paths_servant_util   hs-source-dirs:       tests-  default-extensions: AllowAmbiguousTypes BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveDataTypeable DeriveGeneric EmptyCase FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns NoImplicitPrelude OverloadedStrings OverloadedLabels PatternSynonyms RankNTypes RecordWildCards ScopedTypeVariables StandaloneDeriving TemplateHaskell TupleSections TypeFamilies TypeOperators UndecidableInstances ViewPatterns TypeApplications+  default-extensions:+      AllowAmbiguousTypes+      BangPatterns+      ConstraintKinds+      DataKinds+      DefaultSignatures+      DeriveDataTypeable+      DeriveGeneric+      EmptyCase+      FlexibleContexts+      FlexibleInstances+      FunctionalDependencies+      GADTs+      GeneralizedNewtypeDeriving+      LambdaCase+      MultiParamTypeClasses+      MultiWayIf+      NamedFieldPuns+      NoImplicitPrelude+      OverloadedStrings+      OverloadedLabels+      PatternSynonyms+      RankNTypes+      RecordWildCards+      ScopedTypeVariables+      StandaloneDeriving+      TemplateHaskell+      TupleSections+      TypeFamilies+      TypeOperators+      UndecidableInstances+      ViewPatterns+      TypeApplications   ghc-options: -Wall -threaded -rtsopts -with-rtsopts=-N   build-tool-depends:       hspec-discover:hspec-discover@@ -186,6 +287,7 @@     , lens     , megaparsec     , mtl+    , openapi3     , pretty-terminal     , reflection     , regex-posix@@ -193,6 +295,7 @@     , servant     , servant-client     , servant-client-core+    , servant-openapi3     , servant-server     , servant-swagger     , servant-swagger-ui
src/Servant/Util/Combinators/ErrorResponses.hs view
@@ -10,11 +10,13 @@ import Universum  import Control.Lens (at, (<>~), (?~))+import qualified Data.OpenApi as O import qualified Data.Swagger as S-import Data.Swagger.Declare (runDeclare)+import qualified Data.Swagger.Declare as S (runDeclare) import GHC.TypeLits (KnownSymbol, Symbol) import Servant (HasServer (..), (:>)) import Servant.Client (HasClient (..))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Swagger (HasSwagger (..))  import Servant.Util.Combinators.Logging@@ -87,6 +89,7 @@     routeWithLog = inRouteServer @(ErrorResponses errors :> LoggingApiRec config lcontext subApi) route id  +-- Swagger instances class KnownErrorCodes (errors :: [ErrorDesc]) where     errorCodesToSwagger :: S.Swagger -> S.Swagger @@ -102,7 +105,7 @@       where         code = fromIntegral (natVal @code Proxy)         desc = symbolValT @desc-        (defs, excSchema) = runDeclare (S.declareSchemaRef (Proxy @exc)) mempty+        (defs, excSchema) = S.runDeclare (S.declareSchemaRef (Proxy @exc)) mempty         response = mempty             & S.description .~ desc             & S.schema ?~ excSchema@@ -111,6 +114,30 @@          , KnownErrorCodes errors          ) => HasSwagger (ErrorResponses errors :> subApi) where     toSwagger _ = toSwagger (Proxy @subApi) & errorCodesToSwagger @errors++-- OpenApi instances+class KnownErrorCodesOpenApi (errors :: [ErrorDesc]) where+    errorCodesToOpenApi :: O.OpenApi -> O.OpenApi++instance KnownErrorCodesOpenApi '[] where+    errorCodesToOpenApi = id++instance (KnownNat code, KnownSymbol desc, O.ToSchema exc, KnownErrorCodesOpenApi es) =>+         KnownErrorCodesOpenApi ('ErrorDesc code exc desc ': es) where+    -- In the absence of a media type we cannot insert a response schema.+    errorCodesToOpenApi openapi = openapi+        & O.allOperations . O.responses . O.responses . at code ?~ O.Inline response+        & errorCodesToOpenApi @es+      where+        code = fromIntegral (natVal @code Proxy)+        desc = symbolValT @desc+        response = mempty+            & O.description .~ desc++instance ( HasOpenApi subApi+         , KnownErrorCodesOpenApi errors+         ) => HasOpenApi (ErrorResponses errors :> subApi) where+    toOpenApi _ = toOpenApi (Proxy @subApi) & errorCodesToOpenApi @errors  --------------------------------------------------------------------------- -- Helpers
src/Servant/Util/Combinators/Filtering/Swagger.hs view
@@ -6,10 +6,13 @@ import Universum  import Control.Lens ((<>~))+import qualified Data.HashMap.Strict.InsOrd as HM+import qualified Data.OpenApi as O import qualified Data.Swagger as S import qualified Data.Text as T import GHC.TypeLits (KnownSymbol) import Servant.API ((:>))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Swagger (HasSwagger (..))  import Servant.Util.Combinators.Filtering.Base@@ -30,6 +33,25 @@         }     } +-- | Make a 'O.Param' for a filtering query parameter.+filterOpenApiParam :: forall a. O.ToParamSchema a => Text -> Text -> O.Param+filterOpenApiParam name desc =+    O.Param+    { O._paramName = name+    , O._paramDescription = Just desc+    , O._paramRequired = Just False+    , O._paramDeprecated = Just False+    , O._paramIn = O.ParamQuery+    , O._paramAllowEmptyValue = Just True+    , O._paramAllowReserved = Nothing+    , O._paramStyle = Just O.StyleDeepObject+    , O._paramExample = Nothing+    , O._paramExamples = HM.empty+    , O._paramExplode = Just True+    , O._paramSchema = Just . O.Inline $ O.toParamSchema (Proxy @a)+    }++ parenValueDesc :: forall a. KnownSymbol (ParamDescription a) => Text parenValueDesc = "(" <> stripTrailingDot (symbolValT @(ParamDescription a)) <> ")"   where@@ -82,15 +104,28 @@ class FilterKindHasSwagger (fk :: FilterKind Type) where     filterKindSwagger :: Text -> S.Param -instance DescribedParam a => FilterKindHasSwagger ('ManualFilter a) where+instance DescribedSwaggerParam a => FilterKindHasSwagger ('ManualFilter a) where     filterKindSwagger name = filterSwaggerParam @a name (manualFilterDesc @a) -instance (DescribedParam a, AutoFiltersOpsDesc (SupportedFilters a)) =>+instance (DescribedSwaggerParam a, AutoFiltersOpsDesc (SupportedFilters a)) =>          FilterKindHasSwagger ('AutoFilter a) where     filterKindSwagger name = filterSwaggerParam @a name (autoFilterDesc @a ops)       where         ops = autoFiltersOpsDesc @(SupportedFilters a) +-- | Get documentation for the given filter kind.+class FilterKindHasOpenApi (fk :: FilterKind Type) where+    filterKindOpenApi :: Text -> O.Param++instance DescribedOpenApiParam a => FilterKindHasOpenApi ('ManualFilter a) where+    filterKindOpenApi name = filterOpenApiParam @a name (manualFilterDesc @a)++instance (DescribedOpenApiParam a, AutoFiltersOpsDesc (SupportedFilters a)) =>+         FilterKindHasOpenApi ('AutoFilter a) where+    filterKindOpenApi name = filterOpenApiParam @a name (autoFilterDesc @a ops)+      where+        ops = autoFiltersOpsDesc @(SupportedFilters a)+ -- | Get documentation for given filtering params. class FilterParamsHaveSwagger (params :: [TyNamedFilter]) where     filterParamsSwagger :: [S.Param]@@ -109,3 +144,22 @@          HasSwagger (FilteringParams params :> api) where     toSwagger _ = toSwagger (Proxy @api)         & S.allOperations . S.parameters <>~ map S.Inline (filterParamsSwagger @params)++-- | Get documentation for given filtering params.+class FilterParamsHaveOpenApi (params :: [TyNamedFilter]) where+    filterParamsOpenApi :: [O.Param]++instance FilterParamsHaveOpenApi '[] where+  filterParamsOpenApi = mempty++instance ( KnownSymbol name, FilterKindHasOpenApi fk+         , FilterParamsHaveOpenApi params+         ) =>+         FilterParamsHaveOpenApi ('TyNamedParam name fk ': params) where+    filterParamsOpenApi =+        filterKindOpenApi @fk (symbolValT @name) : filterParamsOpenApi @params++instance (HasOpenApi api, ReifyParamsNames params, FilterParamsHaveOpenApi params) =>+         HasOpenApi (FilteringParams params :> api) where+    toOpenApi _ = toOpenApi (Proxy @api)+        & O.allOperations . O.parameters <>~ map O.Inline (filterParamsOpenApi @params)
src/Servant/Util/Combinators/Logging.hs view
@@ -31,6 +31,7 @@ import Control.Monad.Error.Class (catchError, throwError) import Data.Constraint (Dict (..)) import Data.Default (Default (..))+import Data.OpenApi (OpenApi) import Data.Reflection (Reifies (..), reify) import Data.Swagger (Swagger) import Data.Time.Clock.POSIX (getPOSIXTime)@@ -41,6 +42,7 @@                     ReflectMethod (..), ReqBody, SBoolI, Summary, Verb, (:<|>) (..), (:>)) import Servant.API.Modifiers (FoldRequired, foldRequiredArgument) import Servant.Client (HasClient (..))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Server (Handler (..), HasServer (..), Server, ServerError (..)) import Servant.Swagger (HasSwagger (..)) import Servant.Swagger.UI.Core (SwaggerUiHtml)@@ -544,6 +546,9 @@ instance Buildable (ForResponseLog Swagger) where     build _ = "Swagger specification" +instance Buildable (ForResponseLog OpenApi) where+    build _ = "OpenApi specification"+ instance Buildable (ForResponseLog (SwaggerUiHtml dir api)) where     build _ = "Accessed documentation UI" @@ -562,6 +567,10 @@ instance HasSwagger api =>          HasSwagger (LoggingMod mod :> api) where     toSwagger _ = toSwagger (Proxy @api)++instance HasOpenApi api =>+         HasOpenApi (LoggingMod mod :> api) where+    toOpenApi _ = toOpenApi (Proxy @api)  -- | Apply logging to the given server. serverWithLogging
src/Servant/Util/Combinators/Pagination.hs view
@@ -14,11 +14,13 @@  import Control.Lens ((<>~), (?~)) import Data.Default (Default (..))+import qualified Data.OpenApi as O import qualified Data.Swagger as S import qualified Data.Text as T import Servant (DefaultErrorFormatters, ErrorFormatters, HasContextEntry, HasServer (..),                 QueryParam, (:>)) import Servant.Client (HasClient (..))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Swagger (HasSwagger (..))  import Servant.Server.Internal.Context (type (.++))@@ -144,6 +146,7 @@      hoistClientMonad pm _ hst subCli = hoistClientMonad pm (Proxy @subApi) hst . subCli +-- Swagger instance instance (HasSwagger api, KnownPaginationPageSize settings) =>          HasSwagger (PaginationParams settings :> api) where     toSwagger _ = toSwagger (Proxy @api)@@ -179,6 +182,42 @@             & S.type_ ?~ S.SwaggerInteger             & S.format ?~ "int32"             & S.pattern ?~ "^\\d*[1-9]\\d*$"+        defaultPageSizeDesc = case settingDefPageSize @settings of+          Nothing -> "By default, no limit will be applied."+          Just s  -> "Defaults to " <> show (unPositive s) <> "."++-- OpenApi instance+instance (HasOpenApi api, KnownPaginationPageSize settings) =>+         HasOpenApi (PaginationParams settings :> api) where+    toOpenApi _ = toOpenApi (Proxy @api)+        & O.allOperations . O.parameters <>~ [O.Inline offsetParam, O.Inline limitParam]+      where+        offsetParam :: O.Param+        limitParam :: O.Param+        offsetParam = mempty+            & O.name .~ "offset"+            & O.description ?~+                "Pagination parameter. How many items to skip from the beginning."+            & O.required ?~ False+            & O.in_ .~ O.ParamQuery+            & O.schema ?~ O.Inline offsetParamSchema+        offsetParamSchema = mempty+            & O.type_ ?~ O.OpenApiInteger+            & O.format ?~ "int32"++        limitParam = mempty+            & O.name .~ "limit"+            & O.description ?~ mconcat+                [ "Pagination parameter. Maximum number of items to return.\n"+                , defaultPageSizeDesc+                ]+            & O.required ?~ False+            & O.in_ .~ O.ParamQuery+            & O.schema ?~ O.Inline limitParamSchema+        limitParamSchema = mempty+            & O.type_ ?~ O.OpenApiInteger+            & O.format ?~ "int32"+            & O.pattern ?~ "^\\d*[1-9]\\d*$"         defaultPageSizeDesc = case settingDefPageSize @settings of           Nothing -> "By default, no limit will be applied."           Just s  -> "Defaults to " <> show (unPositive s) <> "."
src/Servant/Util/Combinators/Sorting/Swagger.hs view
@@ -6,15 +6,18 @@ import Universum  import Control.Lens ((<>~), (?~))+import qualified Data.OpenApi as O import qualified Data.Swagger as S import qualified Data.Text as T import Fmt (build, fmt, unlinesF) import Servant.API ((:>))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Swagger (HasSwagger (..))  import Servant.Util.Combinators.Sorting.Base import Servant.Util.Common +-- Swagger instance instance (HasSwagger api, ReifySortingItems base, ReifyParamsNames provided) =>          HasSwagger (SortingParams provided base :> api) where     toSwagger _ = toSwagger (Proxy @api)@@ -41,6 +44,51 @@         paramSchema = mempty             & S.type_ ?~ S.SwaggerString             & S.pattern ?~ "^" <> fieldPattern <> "(," <> fieldPattern <> ")*" <> "$"+        fieldPattern =+            "(" <> "[+-](" <> allowedFieldsPattern <> ")+" <> "|" <>+            "(asc|desc)\\((" <> allowedFieldsPattern <> ")+\\))"+        allowedFields = reifyParamsNames @provided+        allowedFieldsDesc = mconcat+            [ " Fields allowed for this endpoint: "+            , mconcat . intersperse ", " $+                  map ((<> "`") . ("`" <>) . build) allowedFields+            ]+        allowedFieldsPattern = T.intercalate "|" allowedFields+        baseFields = reifySortingItems @base+        baseFieldsDesc+            | null baseFields = ""+            | otherwise = mconcat+                [ " Base sorting (always applied, lexicographically last): "+                , "`"+                , mconcat . intersperse ", " $ map build baseFields+                , "`"+                ]++-- OpenApi instance+instance (HasOpenApi api, ReifySortingItems base, ReifyParamsNames provided) =>+         HasOpenApi (SortingParams provided base :> api) where+    toOpenApi _ = toOpenApi (Proxy @api)+        & O.allOperations . O.parameters <>~ [O.Inline param]+      where+        param = mempty+            & O.name .~ "sortBy"+            & O.description ?~ fmt do+              unlinesF+                [ "Allows lexicographical sorting on fields."+                , "General format is one of:"+                , "  * `+field1,-field2`"+                , "  * `asc(field1),desc(field2)`"+                , ""+                , allowedFieldsDesc+                , baseFieldsDesc+                , " "+                ]+            & O.required ?~ False+            & O.in_ .~ O.ParamQuery+            & O.schema ?~ O.Inline paramSchema+        paramSchema = mempty+            & O.type_ ?~ O.OpenApiString+            & O.pattern ?~ "^" <> fieldPattern <> "(," <> fieldPattern <> ")*" <> "$"         fieldPattern =             "(" <> "[+-](" <> allowedFieldsPattern <> ")+" <> "|" <>             "(asc|desc)\\((" <> allowedFieldsPattern <> ")+\\))"
src/Servant/Util/Combinators/Tag.hs view
@@ -8,10 +8,12 @@  import Control.Lens (at, (?~)) import qualified Data.HashSet.InsOrd as HS+import qualified Data.OpenApi as O import qualified Data.Swagger as S import GHC.TypeLits (ErrorMessage (..), KnownSymbol, Symbol, TypeError) import Servant (HasServer (..), StdMethod, Verb, (:<|>), (:>)) import Servant.Client (HasClient (..))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Swagger (HasSwagger (..))  import Servant.Util.Combinators.Logging@@ -35,6 +37,7 @@          HasLoggingServer config lcontext (Tag name :> subApi) ctx where     routeWithLog = inRouteServer @(Tag name :> LoggingApiRec config lcontext subApi) route id +-- Swagger instance instance (HasSwagger subApi, KnownSymbol name) =>          HasSwagger (Tag name :> subApi) where     toSwagger _ = toSwagger (Proxy @subApi)@@ -42,6 +45,14 @@       where         name = symbolValT @name +-- OpenApi instance+instance (HasOpenApi subApi, KnownSymbol name) =>+         HasOpenApi (Tag name :> subApi) where+    toOpenApi _ = toOpenApi (Proxy @subApi)+        & O.allOperations . O.tags . at name ?~ ()+      where+        name = symbolValT @name+ -- | Whether to enable some type-level checks for 'Tag's and 'TagsDescription's -- correspondence. data TagsVerification@@ -82,6 +93,7 @@     AllApiTags (api1 :<|> api2) = AllApiTags api1 `UnionSorted` AllApiTags api2     AllApiTags (Verb (method :: StdMethod) (code :: Nat) ctx a) = '[] +-- Swagger instances -- | Extract tags defined by this mapping. class ReifyTagsFromMapping (mapping :: [TyNamedParam Symbol]) where     reifyTagsFromMapping :: HS.InsOrdHashSet S.Tag@@ -125,3 +137,48 @@          HasSwagger (TagDescriptions 'VerifyTags mapping :> api) where     toSwagger _ = toSwagger (Proxy @api)         & S.tags .~ reifyTagsFromMapping @mapping++-- OpenApi instances+-- | Extract tags defined by this mapping.+class ReifyTagsFromMapping' (mapping :: [TyNamedParam Symbol]) where+    reifyTagsFromMapping' :: HS.InsOrdHashSet O.Tag++instance ReifyTagsFromMapping' '[] where+    reifyTagsFromMapping' = mempty++instance ( KnownSymbol name, KnownSymbol desc+         , ReifyTagsFromMapping' mapping+         , ParamsContainNoName mapping name+         ) =>+         ReifyTagsFromMapping' ('TyNamedParam name desc ': mapping) where+    reifyTagsFromMapping' =+        O.Tag+        { O._tagName = symbolValT @name+        , O._tagDescription = Just $ symbolValT @desc+        , O._tagExternalDocs = Nothing+        } `HS.insert` reifyTagsFromMapping' @mapping++instance ( HasOpenApi api+         , ReifyTagsFromMapping' mapping+         ) =>+         HasOpenApi (TagDescriptions 'NoVerifyTags mapping :> api) where+    toOpenApi _ = toOpenApi (Proxy @api)+        & O.tags .~ reifyTagsFromMapping' @mapping++instance ( HasOpenApi api+         , ReifyTagsFromMapping' mapping+         , missingMapping ~ (AllApiTags api // TyNamedParamsNames mapping)+         , If (missingMapping == '[])+            (() :: Constraint)+            (TypeError ('Text "Following tags have no mapping specified in \+                        \TagDescriptions: " ':<>: 'ShowType missingMapping))+         , extraMapping ~ (TyNamedParamsNames mapping // AllApiTags api)+         , If (extraMapping == '[])+            (() :: Constraint)+            (TypeError ('Text "Mappings for the following names specified in \+                              \TagDescriptions are unused: "+                        ':<>: 'ShowType extraMapping))+         ) =>+         HasOpenApi (TagDescriptions 'VerifyTags mapping :> api) where+    toOpenApi _ = toOpenApi (Proxy @api)+        & O.tags .~ reifyTagsFromMapping' @mapping
src/Servant/Util/Swagger.hs view
@@ -1,6 +1,7 @@ module Servant.Util.Swagger     ( ParamDescription-    , DescribedParam+    , DescribedOpenApiParam+    , DescribedSwaggerParam     , paramDescription      , QueryFlagDescription@@ -9,34 +10,38 @@     ) where  import Universum+import qualified Data.OpenApi.Lens as OT+import qualified Data.Swagger.Lens as ST  import Control.Exception (assert)-import Control.Lens (_head, ix, makePrisms, zoom, (?=))+import Control.Lens (_head, ix, zoom, (?=))+import qualified Data.OpenApi as O import qualified Data.Swagger as S import GHC.TypeLits (KnownSymbol, Symbol) import Servant (Capture', Description, EmptyAPI, NoContent, QueryFlag, QueryParam', Raw, StdMethod,                 Verb, (:<|>), (:>))+import Servant.OpenApi (HasOpenApi (..)) import Servant.Swagger (HasSwagger (..))  import Servant.Util.Common -makePrisms ''S.Referenced- ---------------------------------------------------------------------------- -- Parameter description ----------------------------------------------------------------------------  -- | Description of parameter. ----- Unfortunatelly, @servant-swagger@ package, when deriving description of+-- Unfortunately, @servant-swagger@ package, when deriving description of -- an endpoint parameter, fills its description for you and makes you implement -- just 'ParamSchema' which has no description field. -- To circumvent that you can define description in instance of this type family -- and later override swagger derivation accordingly. type family ParamDescription a :: Symbol -type DescribedParam a = (S.ToParamSchema a, KnownSymbol (ParamDescription a))+type DescribedSwaggerParam a = (S.ToParamSchema a, KnownSymbol (ParamDescription a)) +type DescribedOpenApiParam a = (O.ToParamSchema a, KnownSymbol (ParamDescription a))+ -- | Set description according to 'ParamDescription' definition. paramDescription     :: forall a proxy.@@ -62,9 +67,20 @@       where         desc404L :: Traversal' S.Swagger Text         desc404L = S.allOperations . S.responses . S.responses .-                   ix 404 . _Inline . S.description+                   ix 404 . ST._Inline . S.description         pureDesc404 = toSwagger (Proxy @api) ^? desc404L +instance (HasOpenApi (Capture' mods sym a :> api), HasOpenApi  api) =>+         HasOpenApi  (SwaggerCapture mods sym a :> api) where+    toOpenApi _ =+        toOpenApi (Proxy @(Capture' mods sym a :> api))+            & desc404L .~ fromMaybe "" pureDesc404+      where+        desc404L :: Traversal' O.OpenApi Text+        desc404L = O.allOperations . O.responses . O.responses .+                   ix 404 . OT._Inline . O.description+        pureDesc404 = toOpenApi (Proxy @api) ^? desc404L+ ---------------------------------------------------------------------------- -- QueryParam description ----------------------------------------------------------------------------@@ -82,9 +98,20 @@       where         desc404L :: Traversal' S.Swagger Text         desc404L = S.allOperations . S.responses . S.responses .-                   ix 404 . _Inline . S.description+                   ix 404 . ST._Inline . S.description         pureDesc404 = toSwagger (Proxy @api) ^? desc404L +instance (HasOpenApi (QueryParam' mods sym a :> api), HasOpenApi api) =>+         HasOpenApi (SwaggerQueryParam mods sym a :> api) where+    toOpenApi _ =+      toOpenApi (Proxy @(QueryParam' mods sym a :> api))+          & desc404L .~ fromMaybe "" pureDesc404+      where+        desc404L :: Traversal' O.OpenApi Text+        desc404L = O.allOperations . O.responses . O.responses .+                   ix 404 . OT._Inline . O.description+        pureDesc404 = toOpenApi (Proxy @api) ^? desc404L+ ---------------------------------------------------------------------------- -- Query flag description ----------------------------------------------------------------------------@@ -101,13 +128,25 @@ instance (HasSwagger subApi, KnownSymbol name, KnownSymbol (QueryFlagDescription name)) =>          HasSwagger (SwaggerQueryFlag name :> subApi) where     toSwagger _ = toSwagger (Proxy @(QueryFlag name :> subApi)) `executingState` do-        zoom (S.allOperations . S.parameters . _head . _Inline) $ do+        zoom (S.allOperations . S.parameters . _head . ST._Inline) $ do             paramName <- use S.name             assert (name == paramName) pass             S.description ?= desc       where         name = symbolValT @name         desc = symbolValT @(QueryFlagDescription name)++instance (HasOpenApi subApi, KnownSymbol name, KnownSymbol (QueryFlagDescription name)) =>+         HasOpenApi (SwaggerQueryFlag name :> subApi) where+    toOpenApi _ = toOpenApi (Proxy @(QueryFlag name :> subApi)) `executingState` do+        zoom (O.allOperations . O.parameters . _head . OT._Inline) $ do+            paramName <- use O.name+            assert (name == paramName) pass+            O.description ?= desc+      where+        name = symbolValT @name+        desc = symbolValT @(QueryFlagDescription name)+  ---------------------------------------------------------------------------- -- Global
tests/Test/Servant/Filtering/ImplSpec.hs view
@@ -6,12 +6,14 @@  import qualified Data.Aeson as Aeson import qualified Data.Aeson.TH as Aeson-import Data.Swagger (ToSchema)+import qualified Data.OpenApi as O (ToSchema)+import qualified Data.Swagger as S (ToSchema) import Data.Time.Calendar.OrdinalDate (fromOrdinalDate) import Data.Time.Clock (UTCTime (..)) import Servant.API (Get, JSON, (:>)) import Servant.API.Generic (ToServantApi, (:-)) import Servant.Server.Generic (AsServer)+import Servant.OpenApi (toOpenApi) import Servant.Swagger (toSwagger)  import Test.Hspec (Spec, aroundAll, describe, it)@@ -28,7 +30,8 @@ newtype Isbn = Isbn Word64   deriving (Show, Eq, Ord, Generic) -instance ToSchema Isbn+instance O.ToSchema Isbn+instance S.ToSchema Isbn  Aeson.deriveJSON Aeson.defaultOptions ''Isbn @@ -39,7 +42,8 @@   , createdAt :: UTCTime   } deriving (Show, Eq, Generic) -instance ToSchema Book+instance O.ToSchema Book+instance S.ToSchema Book  Aeson.deriveJSON Aeson.defaultOptions ''Book @@ -119,6 +123,16 @@   writeFile "filter-test-swagger.json" $     decodeUtf8 . Aeson.encode $     toSwagger $ Proxy @(ToServantApi ApiMethods)++-- OpenApi+---------------------------------------------------------------------------++-- You can try this with ghci+printFilterOpenApi :: IO ()+printFilterOpenApi =+  writeFile "filter-test-openapi.json" $+    decodeUtf8 . Aeson.encode $+    toOpenApi $ Proxy @(ToServantApi ApiMethods)  -- Tests ---------------------------------------------------------------------------
tests/Test/Servant/Pagination/ImplSpec.hs view
@@ -8,6 +8,7 @@ import Servant.API (Get, JSON, (:>)) import Servant.API.Generic (ToServantApi, (:-)) import Servant.Server.Generic (AsServer)+import Servant.OpenApi (toOpenApi) import Servant.Swagger (toSwagger)  import Test.Hspec (Spec, aroundAll, describe, it)@@ -54,6 +55,16 @@   writeFile "pagination-test-swagger.json" $     decodeUtf8 . Aeson.encode $     toSwagger $ Proxy @(ToServantApi ApiMethods)++-- OpenApi+---------------------------------------------------------------------------++-- You can try this with ghci+printPaginationOpenApi :: IO ()+printPaginationOpenApi =+  writeFile "pagination-test-openapi.json" $+    decodeUtf8 . Aeson.encode $+    toOpenApi $ Proxy @(ToServantApi ApiMethods)  -- Tests ---------------------------------------------------------------------------