packages feed

rfc-servant (empty) → 0.0.0.1

raw patch · 8 files changed

+589/−0 lines, 8 filesdep +aesondep +amazonkadep +base

Dependencies added: aeson, amazonka, base, binary, blaze-html, data-default, http-types, lens, markdown, natural-transformation, rfc-http-client, rfc-prelude, rfc-psql, rfc-redis, servant, servant-blaze, servant-docs, servant-server, servant-swagger, servant-swagger-ui, swagger2, wai, wreq

Files

+ LICENSE view
@@ -0,0 +1,7 @@+Copyright Robert Fischer (c) 2018++Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:++The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.++THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+ README.md view
@@ -0,0 +1,14 @@+RFC Servant+=============+++This is various extensions for [Servant](https://haskell-servant.readthedocs.io/en/stable/index.html), the+exceedingly wonderful framework for web APIs.++RFC.Servant.Server+--------------------+This contains specific support for a Servant server, including++RFC.Servant.Server.Docs+------------------------+This provides support for generating theoretically human-readable documentation about a Servant API. Formats for output are Swagger, plaintext, or HTML.
+ package.common.yaml view
@@ -0,0 +1,103 @@+- &defaults+  author: Robert Fischer+  maintainer: smokejumperit+rfc@gmail.com+  license: MIT+  license-file: LICENSE++  flags:+    production:+      description: If true, disables failing on warnings and enables aggressive optimizations.+      manual: true+      default: false++  library:+    source-dirs:+      - './src'++  extra-source-files:+    - LICENSE+    - README.md+    - package.yaml+    - package.common.yaml++  default-extensions:+    - ApplicativeDo+    - BangPatterns+    - ConstraintKinds+    - DataKinds+    - DefaultSignatures+    - DeriveFoldable+    - DeriveFunctor+    - DeriveGeneric+    - DeriveLift+    - DeriveTraversable+    - ExistentialQuantification+    - FlexibleContexts+    - FlexibleInstances+    - FunctionalDependencies+    - GADTs+    - GeneralizedNewtypeDeriving+    - InstanceSigs+    - KindSignatures+    - LambdaCase+    - MultiParamTypeClasses+    - MultiWayIf+    - NamedFieldPuns+    - PatternSynonyms+    - RankNTypes+    - ScopedTypeVariables+    - StandaloneDeriving+    - TupleSections+    - TypeApplications+    - TypeFamilies+    - TypeFamilyDependencies+    - TypeOperators  +    - RebindableSyntax+    - MonadFailDesugaring+    - DisambiguateRecordFields++  ghc-options: +    - -Wall+    - -Wcompat +    - -Wincomplete-record-updates +    - -Wincomplete-uni-patterns +    - -Wredundant-constraints +    - -fno-max-relevant-binds +    - -fprint-potential-instances +    - -fprint-explicit-foralls+    - -Wno-missing-monadfail-instances+    - -fno-warn-name-shadowing +    - -fno-warn-tabs +    - -fno-warn-orphans+    - -Wnoncanonical-monad-instances +    - -Wall-missed-specialisations+    - -Wnoncanonical-monadfail-instances +    - -Wimplicit-prelude +    - -Wmissing-exported-signatures +    - -Widentities+    - -j++  when:+  - condition: flag(production)+    then:+      ghc-options:+        - -feager-blackholing +        - -funbox-strict-fields +        - -fstatic-argument-transformation+        - -flate-dmd-anal +        - -fexpose-all-unfoldings +        - -fspecialise-aggressively +        - -fexcess-precision +        - -fmax-simplifier-iterations=15+        - -fsimplifier-phases=4+        - -fstrictness-before=3+        - -funfolding-keeness-factor=1.5+        - -funfolding-use-threshold=150+    else:+      cpp-options:+        - -DDEVELOPMENT+      ghc-options:+        - -freverse-errors+        - -Werror +        - -dcore-lint +        - -dcmm-lint
+ package.yaml view
@@ -0,0 +1,39 @@+name: rfc-servant+version: 0.0.0.1+synopsis: The Servant extensions from the Robert Fischer Commons.+description: >+  Various capabilities supporting @Servant@ development, including creating the concept of a +  "Resource" (which simplifies REST boilerplate).+category: prelude+github: RobertFischer/rfc/rfc-servant+author: Robert Fischer+license: MIT++_common: !include package.common.yaml++<<: *defaults++dependencies:+  - rfc-prelude +  - rfc-redis+  - rfc-psql+  - rfc-http-client+  - base >= 4.10 && < 5+  - data-default >= 0.7+  - aeson >= 1.4+  - binary >= 0.8+  - lens >= 4.16+  - servant >= 0.13+  - servant-server >= 0.13+  - servant-swagger >= 1.1+  - servant-swagger-ui >= 0.2+  - swagger2 >= 2.2+  - wai >= 3.2+  - http-types >= 0.12+  - amazonka >= 1.6+  - natural-transformation >= 0.4+  - wreq >= 0.5+  - servant-docs >= 0.11+  - blaze-html >= 0.9+  - servant-blaze >= 0.8+  - markdown >= 0.1
+ rfc-servant.cabal view
@@ -0,0 +1,76 @@+-- This file has been generated from package.yaml by hpack version 0.28.2.+--+-- see: https://github.com/sol/hpack+--+-- hash: 17a5599d137dac2c032d059d4244abfadacc22f92ac44c7a7e66947335ceb83d++name:           rfc-servant+version:        0.0.0.1+synopsis:       The Servant extensions from the Robert Fischer Commons.+description:    Various capabilities supporting @Servant@ development, including creating the concept of a  "Resource" (which simplifies REST boilerplate).+category:       prelude+homepage:       https://github.com/RobertFischer/rfc#readme+bug-reports:    https://github.com/RobertFischer/rfc/issues+author:         Robert Fischer+maintainer:     smokejumperit+rfc@gmail.com+license:        MIT+license-file:   LICENSE+build-type:     Simple+cabal-version:  >= 1.10+extra-source-files:+    LICENSE+    package.common.yaml+    package.yaml+    README.md++source-repository head+  type: git+  location: https://github.com/RobertFischer/rfc+  subdir: rfc-servant++flag production+  description: If true, disables failing on warnings and enables aggressive optimizations.+  manual: True+  default: False++library+  exposed-modules:+      RFC.Servant.API+      RFC.Servant.Server+      RFC.Servant.Server.ApiDoc+  other-modules:+      Paths_rfc_servant+  hs-source-dirs:+      ./src+  default-extensions: ApplicativeDo BangPatterns ConstraintKinds DataKinds DefaultSignatures DeriveFoldable DeriveFunctor DeriveGeneric DeriveLift DeriveTraversable ExistentialQuantification FlexibleContexts FlexibleInstances FunctionalDependencies GADTs GeneralizedNewtypeDeriving InstanceSigs KindSignatures LambdaCase MultiParamTypeClasses MultiWayIf NamedFieldPuns PatternSynonyms RankNTypes ScopedTypeVariables StandaloneDeriving TupleSections TypeApplications TypeFamilies TypeFamilyDependencies TypeOperators RebindableSyntax MonadFailDesugaring DisambiguateRecordFields+  ghc-options: -Wall -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns -Wredundant-constraints -fno-max-relevant-binds -fprint-potential-instances -fprint-explicit-foralls -Wno-missing-monadfail-instances -fno-warn-name-shadowing -fno-warn-tabs -fno-warn-orphans -Wnoncanonical-monad-instances -Wall-missed-specialisations -Wnoncanonical-monadfail-instances -Wimplicit-prelude -Wmissing-exported-signatures -Widentities -j+  build-depends:+      aeson >=1.4+    , amazonka >=1.6+    , base >=4.10 && <5+    , binary >=0.8+    , blaze-html >=0.9+    , data-default >=0.7+    , http-types >=0.12+    , lens >=4.16+    , markdown >=0.1+    , natural-transformation >=0.4+    , rfc-http-client+    , rfc-prelude+    , rfc-psql+    , rfc-redis+    , servant >=0.13+    , servant-blaze >=0.8+    , servant-docs >=0.11+    , servant-server >=0.13+    , servant-swagger >=1.1+    , servant-swagger-ui >=0.2+    , swagger2 >=2.2+    , wai >=3.2+    , wreq >=0.5+  if flag(production)+    ghc-options: -feager-blackholing -funbox-strict-fields -fstatic-argument-transformation -flate-dmd-anal -fexpose-all-unfoldings -fspecialise-aggressively -fexcess-precision -fmax-simplifier-iterations=15 -fsimplifier-phases=4 -fstrictness-before=3 -funfolding-keeness-factor=1.5 -funfolding-use-threshold=150+  else+    ghc-options: -freverse-errors -Werror -dcore-lint -dcmm-lint+    cpp-options: -DDEVELOPMENT+  default-language: Haskell2010
+ src/RFC/Servant/API.hs view
@@ -0,0 +1,33 @@+{-# LANGUAGE DataKinds         #-}+{-# LANGUAGE NoImplicitPrelude #-}++module RFC.Servant.API+  ( JDelete+  , JGet+  , JPatch+  , JPost+  , JPut+  , JReqBody+  ) where++import           RFC.Prelude ()++import           Servant.API++-- |JSON DELETE+type JDelete a = Delete '[JSON] a++-- |JSON GET+type JGet a = Get '[JSON] a++-- |JSON PATCH+type JPatch a = Get '[JSON] a++-- |JSON POST+type JPost a = Post '[JSON] a++-- |JSON PUT+type JPut a = Post '[JSON] a++-- |JSON Request Body ('ReqBody')+type JReqBody a = ReqBody '[JSON] a
+ src/RFC/Servant/Server.hs view
@@ -0,0 +1,195 @@+{-# LANGUAGE DataKinds            #-}+{-# LANGUAGE ExplicitNamespaces   #-}+{-# LANGUAGE FlexibleContexts     #-}+{-# LANGUAGE FlexibleInstances    #-}+{-# LANGUAGE NoImplicitPrelude    #-}+{-# LANGUAGE OverloadedStrings    #-}+{-# LANGUAGE Rank2Types           #-}+{-# LANGUAGE ScopedTypeVariables  #-}+{-# LANGUAGE TypeOperators        #-}+{-# LANGUAGE TypeSynonymInstances #-}++module RFC.Servant.Server+  ( ApiCtx+  , apiCtxToHandler+  , ResourceDefinition(..)+  , ServerAPI+  , ServerImpl+  , module Servant+  , module Servant.Docs+  , module Servant.HTML.Blaze+  , module Text.Blaze.Html+  , module RFC.Data.IdAnd+  , module RFC.Servant.API+  ) where++import           Control.Monad.Trans.AWS+import           Control.Natural         ( type (~>) )+import           Data.Aeson              as JSON+import           Network.AWS             as AWS+import           Network.Wreq.Session    as Wreq+import           RFC.Data.IdAnd+import           RFC.HTTP.Client+import           RFC.Prelude             hiding ( Handler )+import qualified RFC.Psql                as Psql+import qualified RFC.Redis               as Redis+import           RFC.Servant.API+import           Servant+import           Servant.Docs            hiding ( API )+import           Servant.HTML.Blaze      ( HTML )+import           Servant.Server          ( Handler, runHandler )+import           Text.Blaze.Html++type ApiCtx =+  AWST+    ( ReaderT Wreq.Session+      ( ReaderT Psql.ConnectionPool+        ( ReaderT Redis.ConnectionPool+          Handler+        )+      )+    )++instance MonadUnliftIO Handler where+  withRunInIO iobFactory = liftIO $ iobFactory converter+    where+      converter h = do+        eitherT <- runHandler h+        case eitherT of+          Left err -> throwIO err+          Right a  -> return a+  {-# INLINE withRunInIO #-}++instance HasAPIClient ApiCtx where+  getAPIClient = lift ask+  {-# INLINE getAPIClient #-}++instance (Monad m) => HasAPIClient (ReaderT Wreq.Session m) where+  getAPIClient = ask+  {-# INLINE getAPIClient #-}++instance Redis.HasRedis ApiCtx where+  getRedisPool = lift . lift $ lift ask+  {-# INLINE getRedisPool #-}++apiCtxToHandler :: Wreq.Session -> Redis.ConnectionPool -> Psql.ConnectionPool -> AWS.Env -> ApiCtx ~> Handler+apiCtxToHandler apiClient redisPool psqlPool awsEnv = toHandler+  where+    toHandler :: ApiCtx ~> Handler+    toHandler = withRedis . withPsql . withAPIClient . withAws+      where+        withAPIClient m = runReaderT m apiClient+        withRedis m = runReaderT m redisPool+        withPsql m = runReaderT m psqlPool+        withAws = runAWST awsEnv+{-# INLINE apiCtxToHandler #-}++type FetchAllImpl a = ApiCtx (RefMap a)+type FetchAllAPI a = JGet (RefMap a)+type FetchImpl a = UUID -> ApiCtx (IdAnd a)+type FetchAPI a = Capture "id" UUID :> JGet (IdAnd a)+type CreateImpl a = a -> ApiCtx (IdAnd a)+type CreateAPI a = JReqBody a :> JPost (IdAnd a)+--type PatchImpl a = UUID -> JSON.Patch -> ApiCtx (IdAnd a)+--type PatchAPI a = Capture "id" UUID :> ReqBody '[JSON] JSON.Patch :> Patch '[JSON] (IdAnd a)+type ReplaceImpl a = UUID -> a -> ApiCtx (IdAnd a)+type ReplaceAPI a = Capture "id" UUID :> JReqBody a :> JPost (IdAnd a)+type DeleteImpl = UUID -> ApiCtx ()+type DeleteAPI a = Capture "id" UUID :> JDelete ()++type ServerImpl a =+  (FetchAllImpl a)+  :<|> (FetchImpl a)+  :<|> (CreateImpl a)+  -- :<|> (PatchImpl a)+  :<|> (ReplaceImpl a)+  :<|> DeleteImpl+type ServerAPI a =+  (FetchAllAPI a)+  :<|> (FetchAPI a)+  :<|> (CreateAPI a)+  -- :<|> (PatchAPI a)+  :<|> (ReplaceAPI a)+  :<|> (DeleteAPI a)+++class (FromJSON a, ToJSON a, Show a) => ResourceDefinition a where+++  -- | Provide all UUID of all the children of this resource.+  --   The graph of all resources to children should form a directed acyclic graph.+  resourceChildIds :: IdAnd (Proxy a) -> ApiCtx [UUID]++  -- | Update the child id for the parent at the first 'UUID' argument from the second 'UUID' argument to the third 'UUID' argument.+  resourceUpdateChildId :: IdAnd (Proxy a) -> UUID -> UUID -> ApiCtx ()++  restFetchAll :: FetchAllImpl a+  restFetchAll = idAndsToMap <$> fetchAllResources+  {-# INLINE restFetchAll #-}++  restFetch :: FetchImpl a+  restFetch uuid = do+    maybeResource <- fetchResource uuid+    case maybeResource of+      Nothing -> throwError $ err404+        { errReasonPhrase = "No resource found for id"+        , errBody = toUTF8 $ "Could not find a resource with UUID: " <> show uuid+        }+      Just value -> return $ tupleToIdAnd (uuid, value)+  {-# INLINE restFetch #-}++  restCreate :: CreateImpl a+  restCreate a = do+      maybeId <- createResource a+      case maybeId of+        (Just id) -> restFetch id+        Nothing -> throwIO $ err400+          { errReasonPhrase = "Could not create resource"+          , errBody = toUTF8 $ show a+          }+  {-# INLINE restCreate #-}++{-+  restPatch :: PatchImpl a+  restPatch id patch = do+    (IdAnd (_,original::a)) <- restFetch id+    case JSON.patch patch $ toJSON original of+      Error str -> throwError $ err400+        { errReasonPhrase = "Error applying patch"+        , errBody = toUTF8 str+        }+      Success jsonValue ->+        case JSON.eitherDecode' $ JSON.encode jsonValue of+          Left err -> throwError $ err400+            { errReasonPhrase = "Error rebuilding object after patch"+            , errBody = toUTF8 err+            }+          Right value -> restReplace id value+  {-# INLINE restPatch #-}+-}++  restReplace :: ReplaceImpl a+  restReplace id value = do+      replaceResource newValue+      restFetch id+    where+      newValue = tupleToIdAnd (id,value)+  {-# INLINE restReplace #-}++  restDelete :: Proxy a -> DeleteImpl+  restDelete = deleteResource++  restServer :: ServerImpl a+  restServer =+    restFetchAll+    :<|> restFetch+    :<|> restCreate+    -- :<|> restPatch+    :<|> restReplace+    :<|> (restDelete (Proxy::Proxy a))++  fetchResource :: UUID -> ApiCtx (Maybe a)+  fetchAllResources :: ApiCtx [IdAnd a]+  createResource :: a -> ApiCtx (Maybe UUID)+  replaceResource :: (IdAnd a) -> ApiCtx ()+  deleteResource :: Proxy a -> UUID -> ApiCtx ()
+ src/RFC/Servant/Server/ApiDoc.hs view
@@ -0,0 +1,122 @@+{-# LANGUAGE DataKinds             #-}+{-# LANGUAGE DefaultSignatures     #-}+{-# LANGUAGE FlexibleContexts      #-}+{-# LANGUAGE FlexibleInstances     #-}+{-# LANGUAGE MultiParamTypeClasses #-}+{-# LANGUAGE NoImplicitPrelude     #-}+{-# LANGUAGE UndecidableInstances  #-}++module RFC.Servant.Server.ApiDoc+  ( apiToHtml+  , apiToAscii+  , apiToSwagger+  , apiMiddleware+  , swaggerSchemaOptions+  , ToSchemaRFC+  , module Data.Swagger+  , module Servant.Swagger.UI+  , module Servant.Swagger+  ) where++import           Control.Lens+import qualified Data.Aeson                      as Aeson+import           Data.Aeson.Types                ( fromEncoding, toEncoding )+import qualified Data.Binary.Builder             as Builder+import           Data.Default                    ( def )+import           Data.Monoid                     ( (<>) )+import           Data.Swagger+import           Data.Swagger.Declare+import           Data.Swagger.Internal.Schema    ( GToSchema )+import           Data.Swagger.Internal.TypeShape ( TypeHasSimpleShape )+import           GHC.Generics                    ( Rep )+import           Network.HTTP.Types.Header       ( hContentType )+import           Network.HTTP.Types.Status+import           Network.Wai+import           RFC.Prelude                     hiding ( (<>) )+import           RFC.Servant.Server+import           Servant.Swagger+import           Servant.Swagger.UI+import qualified Text.Blaze.Html.Renderer.String as Blaze+import qualified Text.Markdown                   as MD++apiToHtml :: (HasDocs a) => Proxy a -> Html+apiToHtml = preEscapedToHtml . (MD.markdown mdSettings) . toLazyText . markdown . docs+  where+    mdSettings = def+      { MD.msLinkNewTab = False+      , MD.msAddHeadingId = True+      }++apiToAscii :: HasDocs a => Proxy a -> String+apiToAscii = markdown . docs++apiToSwagger :: (HasSwagger a) => Proxy a -> Swagger+apiToSwagger = toSwagger++apiMiddleware :: (HasDocs a, HasSwagger a) => Proxy a -> Swagger -> Application -> Application+apiMiddleware api addlSwagger application request callback =+  case (reqMethod, reqPath) of+    ("GET", Just doIt) -> doIt+    _                  -> application request callback+  where+    bsToStr :: StrictByteString -> String+    bsToStr = fromMaybe "" . fromUTF8+    html = Blaze.renderHtml $ apiToHtml api+    ascii = apiToAscii api+    swaggerToLbs = Builder.toLazyByteString . fromEncoding . toEncoding+    swagger = swaggerToLbs $ apiToSwagger api <> addlSwagger+    reqMethod = fmap charToUpper . bsToStr $ requestMethod request+    pathInfo = fmap charToLower . bsToStr $ rawPathInfo request+    reqPath =+      case pathInfo of+        "swagger.json"  -> Just serveSwagger+        "/swagger.json" -> Just serveSwagger+        "api.html"      -> Just serveHtml+        "/api.html"     -> Just serveHtml+        "api.txt"       -> Just serveTxt+        "/api.txt"      -> Just serveTxt+        _               -> Nothing+    response contentType body = callback $+      responseLBS status200 [(hContentType, contentType)] body+    serveHtml = response (toUTF8 "text/html") (toUTF8 html)+    serveTxt = response (toUTF8 "text/plain") (toUTF8 ascii)+    serveSwagger = response (toUTF8 "application/json") swagger+++instance ToSample () where+  toSamples _ = [(toText "No value", ())]++swaggerSchemaOptions :: SchemaOptions+swaggerSchemaOptions = SchemaOptions+  { fieldLabelModifier = Aeson.fieldLabelModifier jsonOptions+  , constructorTagModifier = Aeson.constructorTagModifier jsonOptions+  , unwrapUnaryRecords = Aeson.unwrapUnaryRecords jsonOptions+  , datatypeNameModifier = id+  , allNullaryToStringTag = True+  }++class ToSchemaRFC a where+  declareNamedSchemaRFC :: proxy a -> Declare (Definitions Schema) NamedSchema+  default declareNamedSchemaRFC ::+    (Generic a+    , GToSchema (Rep a)+    , TypeHasSimpleShape a "genericDeclareNamedSchemaUnrestricted"+    ) => proxy a -> Declare (Definitions Schema) NamedSchema+  declareNamedSchemaRFC = genericDeclareNamedSchema swaggerSchemaOptions++instance {-# OVERLAPPABLE #-} ToSchemaRFC a => ToSchema a where+  declareNamedSchema = declareNamedSchemaRFC++instance {-# OVERLAPPABLE #-} ToSchema (Ratio a) where+  declareNamedSchema _ = do+    integerSchema <- declareSchemaRef (Proxy :: Proxy Integer)+    return . NamedSchema (Just . toText $ "Ratio") $ mempty+      & type_ .~ SwaggerObject+      & properties .~ (fromList+        [ (toText "numerator", integerSchema)+        , (toText "denominator", integerSchema)+        ])+      & required .~ (toText <$> [ "numerator", "denominator" ])++instance {-# OVERLAPPING #-} ToSample Int16 where+    toSamples _ =  [] -- TODO Impl this