servant-docs-simple (empty) → 0.1.0.0
raw patch · 12 files changed
+1054/−0 lines, 12 filesdep +aesondep +aeson-prettydep +base
Dependencies added: aeson, aeson-pretty, base, bytestring, hspec, hspec-core, prettyprinter, raw-strings-qq, servant, servant-docs-simple, text
Files
- CHANGELOG.md +11/−0
- LICENSE +21/−0
- README.md +296/−0
- servant-docs-simple.cabal +96/−0
- src/Servant/Docs/Simple.hs +41/−0
- src/Servant/Docs/Simple/Parse.hs +186/−0
- src/Servant/Docs/Simple/Render.hs +124/−0
- test/Spec.hs +12/−0
- test/Test/Servant/Docs/Simple.hs +18/−0
- test/Test/Servant/Docs/Simple/Parse.hs +23/−0
- test/Test/Servant/Docs/Simple/Render.hs +17/−0
- test/Test/Servant/Docs/Simple/Samples.hs +209/−0
+ CHANGELOG.md view
@@ -0,0 +1,11 @@+# Changelog++`servant-docs-simple` uses [PVP Versioning][1].+The changelog is available [on GitHub][2].++## 0.1.0.0++* Initially created.++[1]: https://pvp.haskell.org+[2]: https://github.com/Holmusk/servant-docs-simple/releases
+ LICENSE view
@@ -0,0 +1,21 @@+MIT License++Copyright (c) 2020 kwannoel++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,296 @@+# servant-docs-simple++[](https://travis-ci.org/holmusk/servant-docs-simple)+[](https://hackage.haskell.org/package/servant-docs-simple)+[](http://stackage.org/lts/package/servant-docs-simple)+[](http://stackage.org/nightly/package/servant-docs-simple)+[](LICENSE)++# Introduction++This library uses [Data.Typeable](http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Typeable.html)+to generate documentation for [Servant](https://hackage.haskell.org/package/servant) API types.++It relies on the `typeRep` of Servant's combinators and other datatypes used in+the API to generate the documentation.++**Why do we need this?**+++1) We need the `API Format types` inside the documentation. They can be used as keys+while querying an API endpoint, to look up examples, fields, and other+miscellaneous details.++2) We want to generate documentation from the information our `API` type provides, without having to write instances.++[**In-depth explanation here**](https://github.com/Holmusk/servant-docs-simple#faq)++# Functionality++## Servant.Docs.Simple.Parse++*Parses the API into a documentation friendly structure*++**API type**++``` haskell+type API = "hello" :> "world" :> Request :> Response+type Request = ReqBody '[()] ()+type Response = Post '[()] ()++```++**Intermediate structure**++``` haskell+Endpoints [Node "/hello/world" + (Details [ Node "RequestBody" (Details [ Node "Format" + (Detail "': * () ('[] *)")+ , Node "ContentType"+ (Detail "()")+ ])+ , Node "RequestType" (Detail "'POST")+ , Node "Response" (Details [ Node "Format"+ (Detail "': * () ('[] *)")+ , Node "ContentType"+ (Detail "()")+ ])+ ])]+```++## Servant.Docs.Simple.Render++*Renders the intermediate structure into common documentation formats*++**Intermediate structure**++``` haskell+Endpoints [Node "/hello/world" + (Details [ Node "RequestBody" (Details [ Node "Format" + (Detail "': * () ('[] *)")+ , Node "ContentType"+ (Detail "()")+ ])+ , Node "RequestType" (Detail "'POST")+ , Node "Response" (Details [ Node "Format"+ (Detail "': * () ('[] *)")+ , Node "ContentType"+ (Detail "()")+ ])+ ])]+```+++**JSON**++``` json+{+ "/hello/world": {+ "Response": {+ "Format": "': * () ('[] *)",+ "ContentType": "()"+ },+ "RequestType": "'POST",+ "RequestBody": {+ "Format": "': * () ('[] *)",+ "ContentType": "()"+ }+ }+}+```++**Text**++``` text+/hello/world:+RequestBody:+ Format: ': * () ('[] *)+ ContentType: ()+RequestType: 'POST+Response:+ Format: ': * () ('[] *)+ ContentType: ()+```++## Servant.Docs.Simple++*Provides functions to write rendered formats to file/stdout*++**Using this script**++``` haskell+-- stack --system-ghc runghc --package servant-docs-simple+{-# LANGUAGE DataKinds #-}+{-# LANGUAGE TypeApplications #-}+{-# LANGUAGE TypeOperators #-}+++module Main where++import Data.Aeson (Value)+import Data.Text (Text)++import Servant.API ((:>), Post, ReqBody)+import Servant.Docs.Simple (document, documentWith, stdoutJson, stdoutPlainText, writeDocsJson,+ writeDocsPlainText)+import Servant.Docs.Simple.Render (Json (..), PlainText (..))++-- Our API type+type API = "hello" :> "world" :> Request :> Response+type Request = ReqBody '[()] ()+type Response = Post '[()] ()++main :: IO ()+main = do+ -- Writes to the file $PWD/docsJson+ writeDocsJson @API "docs.json"++ -- Writes to the file $PWD/docsPlainText+ writeDocsPlainText @API "docs.txt" +```++**Expected Output**++*Files should be generated relative to $PWD*++``` sh+$ ls | grep docs+docs.json+docs.txt+```++*docs.json*++``` json+{+ "/hello/world": {+ "Response": {+ "Format": "': * () ('[] *)",+ "ContentType": "()"+ },+ "RequestType": "'POST",+ "RequestBody": {+ "Format": "': * () ('[] *)",+ "ContentType": "()"+ }+ }+}+```++*docs.txt*++``` text+/hello/world:+RequestBody:+ Format: ': * () ('[] *)+ ContentType: ()+RequestType: 'POST+Response:+ Format: ': * () ('[] *)+ ContentType: ()+```++# Tutorials++**Click on these links for tutorials**++[Generating plaintext/JSON documentation from api types](https://github.com/Holmusk/servant-docs-simple/blob/master/examples/render.hs)++[Generating the intermediate documentation structure](https://github.com/Holmusk/servant-docs-simple/blob/master/examples/parse.hs)++[Writing our own rendering format](https://github.com/Holmusk/servant-docs-simple/blob/master/examples/format.hs)++**To run tutorial scripts**++``` sh+# clone this repository+git clone git@github.com:Holmusk/servant-docs-simple.git++# run the examples+stack examples/<source file>+```++# FAQ++**What's the usecase for this?**++1) Lightweight documentation solution.++2) Keeping documentation for types in one place++ Suppose we have a library of format types. These are shared between different+ languages and formats. If we centralize all the information in this+ library, we have one source of truth.++ ``` haskell+ -- --- Format Types ---+ -- | |+ -- v v+ type singleAPI = "hello" :> ReqBody '[JSON] User :> POST '[JSON] Message+ data User = User Name Password deriving Typeable + data Messsage = Message Id Content deriving Typeable++ ```++ We use these format types in our `API` as illustrated above.+ Developers can use these format types to index the library, to find information+ (examples, instances, ...) they need about the type.++**What is required to achieve this?**++1) We need documentation to be generated from the API type, without writing+extra instances for each **format type** we use.++ We can do this with `typeRep` from `Data.Typeable`.++ ``` sh+ $ stack ghci+ Prelude> import Data.Typeable+ Prelude Data.Typeable> data Alignment = Good | Bad deriving Typeable+ Prelude Data.Typeable> typeRep (Proxy :: Proxy Alignment)+ Alignment+ ```++2) We need access to the API format types (`Email`, `Users`, ...) in documentation.++ This is so we can index our library with these types to get the relevant+ fields, examples for these.++**Why don't we use Servant.Docs?**++1) `Servant.Docs` generates documentation **only if** `Format Types` have+ implemented the necessary instances.++ ``` haskell+ -- Instances for format types; they provide examples for format types++ instance ToSample User where+ toSamples _ = <some example>++ instance ToSample Message where+ toSamples _ = <some example>+ ```++ Hence, we cannot generate documentation solely from the `Format Type`.++2) In documentation generated by `Servant.Docs`, the format types mentioned above (`User`,+`Message`) are not included. This means we can't use them to index our library+to look for the relevant information.++**What are the trade-offs?**++1) Currently `Servant.Docs.Simple` does not support as many formats as+`Servant.Docs` (via `Servant.Docs.Pandoc`).++2) Examples are not mandatory as you do not have to write instances for your format+types.++ This means that **if developers don't provide examples**, the generated+ documentation would be **without them**.++ We can still provide these examples through:++ - The shared library of types++ - Using `Servant combinators` such as `Summary` and `Description`.+ These are generated as part of Documentation through `Servant.Docs.Simple`.
+ servant-docs-simple.cabal view
@@ -0,0 +1,96 @@+cabal-version: 2.4+name: servant-docs-simple+version: 0.1.0.0+synopsis: Generate documentation via TypeRep for Servant API++description:+ This library uses [Data.Typeable](http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Typeable.html)+ to generate documentation for [Servant](https://hackage.haskell.org/package/servant) API types.++ It relies on the `typeRep` of Servant's combinators and other datatypes used in+ the API to generate the documentation.++homepage: https://github.com/Holmusk/servant-docs-simple+bug-reports: https://github.com/Holmusk/servant-docs-simple/issues+license: MIT+license-file: LICENSE+author: kwannoel+maintainer: Holmusk <tech@holmusk.com>+copyright: 2020 Holmusk+category: Utility+build-type: Simple+extra-doc-files: README.md+ CHANGELOG.md+extra-source-files: README.md+tested-with: GHC == 8.6.5+ GHC == 8.8.2++source-repository head+ type: git+ location: https://github.com/Holmusk/servant-docs-simple.git++common common-options+ build-depends: base >= 4.11.1.0 && < 4.14+ , aeson >= 1.4.6 && < 1.5+ , aeson-pretty >= 0.8.8 && < 0.9+ , bytestring >= 0.10.8.2 && < 0.11+ , prettyprinter >= 1.2.1.1 && < 1.7+ , servant >= 0.16 && < 0.18+ , text >= 1.2.3.1 && < 1.3++ ghc-options: -Wall+ -Wcompat+ -Widentities+ -Wincomplete-uni-patterns+ -Wincomplete-record-updates+ if impl(ghc >= 8.0)+ ghc-options: -Wredundant-constraints+ if impl(ghc >= 8.2)+ ghc-options: -fhide-source-paths+ if impl(ghc >= 8.4)+ ghc-options: -Wmissing-export-lists+ -Wpartial-fields+ if impl(ghc >= 8.8)+ ghc-options: -Wmissing-deriving-strategies++ default-language: Haskell2010+ default-extensions: AllowAmbiguousTypes+ ConstraintKinds+ DataKinds+ DerivingStrategies+ FlexibleInstances+ FunctionalDependencies+ OverloadedStrings+ PolyKinds+ ScopedTypeVariables+ TypeApplications+ TypeFamilies+ TypeOperators++library+ import: common-options+ hs-source-dirs: src++ exposed-modules: Servant.Docs.Simple+ Servant.Docs.Simple.Parse+ Servant.Docs.Simple.Render++test-suite servant-docs-simple-test+ import: common-options+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs++ build-depends: servant-docs-simple+ , hspec >= 2.7.1 && < 2.8+ , hspec-core >= 2.7.1 && < 2.8+ , raw-strings-qq >= 1.1 && < 1.2++ other-modules: Test.Servant.Docs.Simple+ Test.Servant.Docs.Simple.Render+ Test.Servant.Docs.Simple.Parse+ Test.Servant.Docs.Simple.Samples++ ghc-options: -threaded+ -rtsopts+ -with-rtsopts=-N
+ src/Servant/Docs/Simple.hs view
@@ -0,0 +1,41 @@+-- | Documentation for API endpoints++module Servant.Docs.Simple ( document+ , documentWith+ , stdoutJson+ , stdoutPlainText+ , writeDocsJson+ , writeDocsPlainText+ ) where++import Data.Aeson.Encode.Pretty (encodePretty)+import qualified Data.ByteString.Lazy as B (writeFile)+import qualified Data.ByteString.Lazy.Char8 as BC (putStrLn)+import qualified Data.Text.IO as T (putStrLn, writeFile)+import Servant.Docs.Simple.Parse (HasParsable (..))+import Servant.Docs.Simple.Render (Json (..), PlainText (..), Renderable (..))+++-- | Write documentation as PlainText to file+writeDocsPlainText :: forall api. HasParsable api => FilePath -> IO ()+writeDocsPlainText fp = T.writeFile fp . getPlainText $ document @api++-- | Write documentation as JSON to file+writeDocsJson :: forall api. HasParsable api => FilePath -> IO ()+writeDocsJson fp = B.writeFile fp . encodePretty . getJson $ documentWith @api @Json++-- | Write documentation as PlainText to stdout+stdoutPlainText :: forall api. HasParsable api => IO ()+stdoutPlainText = T.putStrLn . getPlainText $ document @api++-- | Write documentation as JSON to stdout+stdoutJson :: forall api. HasParsable api => IO ()+stdoutJson = BC.putStrLn . encodePretty . getJson $ documentWith @api @Json++-- | Convert API type into PlainText format+document :: forall api. HasParsable api => PlainText+document = documentWith @api @PlainText++-- | Convert API type into specified formats+documentWith :: forall api a. (HasParsable api, Renderable a) => a+documentWith = render @a (parse @api)
+ src/Servant/Docs/Simple/Parse.hs view
@@ -0,0 +1,186 @@+{- | Parse Servant API into documentation+-}+{-# LANGUAGE UndecidableInstances #-}++module Servant.Docs.Simple.Parse (HasParsable (..)) where+++import Data.Foldable (fold)+import Data.Proxy+import Data.Text (Text, pack)+import Data.Typeable (Typeable, typeRep)+import GHC.TypeLits (KnownSymbol, Symbol, symbolVal)++import Servant.API ((:>), AuthProtect, BasicAuth, Capture', CaptureAll, Description, EmptyAPI,+ Header', HttpVersion, IsSecure, QueryFlag, QueryParam', QueryParams, RemoteHost,+ ReqBody', StreamBody', Summary, Vault, Verb)+import qualified Servant.API.TypeLevel as S (Endpoints)++import Servant.Docs.Simple.Render (Details (..), Endpoints (..), Node (..))++-- | Flattens API into type level list of 'Endpoints'+class HasParsable api where+ parse :: Endpoints++instance HasCollatable (S.Endpoints a) => HasParsable a where+ parse = collate @(S.Endpoints a)++instance {-# OVERLAPPING #-} HasParsable EmptyAPI where+ parse = collate @'[]++-- | Folds api endpoints into documentation+class HasCollatable api where+ -- | Folds list of endpoints to documentation+ collate :: Endpoints++instance (HasDocumentApi api, HasCollatable b) => HasCollatable (api ': b) where+ collate = Endpoints $ documentEndpoint @api : previous+ where Endpoints previous = collate @b++instance HasCollatable '[] where+ collate = Endpoints []++-- | Folds an api endpoint into documentation+documentEndpoint :: forall a. HasDocumentApi a => Node+documentEndpoint = document @a "" []++-- | Folds an api endpoint into documentation+class HasDocumentApi api where++ -- | We use this to destructure the API type and convert it into documentation+ document :: Text -- ^ Route documentation+ -> [Node] -- ^ Everything else documentation+ -> Node -- ^ Generated documentation for the route++-- | Static route documentation+instance (HasDocumentApi b, KnownSymbol route) => HasDocumentApi ((route :: Symbol) :> b) where+ document r = document @b formatted+ where formatted = fold [r, "/", fragment]+ fragment = symbolVal' @route++-- | Capture documentation+instance (HasDocumentApi b, KnownSymbol dRoute, Typeable t) => HasDocumentApi (Capture' m (dRoute :: Symbol) t :> b) where+ document r = document @b formatted+ where formatted = fold [r, "/", "{", var, "::", format, "}"]+ var = symbolVal' @dRoute+ format = typeText @t++-- | CaptureAll documentation+instance (HasDocumentApi b, KnownSymbol dRoute, Typeable t) => HasDocumentApi (CaptureAll (dRoute :: Symbol) t :> b) where+ document r = document @b formatted+ where formatted = fold [r, "/", "{", var, "::", format, "}"]+ var = symbolVal' @dRoute+ format = typeText @t++-- | Request HttpVersion documentation+instance HasDocumentApi b => HasDocumentApi (HttpVersion :> b) where+ document r a = document @b r (a <> [desc])+ where desc = Node "Captures Http Version" (Detail "True")++-- | IsSecure documentation+instance HasDocumentApi b => HasDocumentApi (IsSecure :> b) where+ document r a = document @b r (a <> [desc])+ where desc = Node "SSL Only" (Detail "True")++-- | Request Remote host documentation+instance HasDocumentApi b => HasDocumentApi (RemoteHost :> b) where+ document r a = document @b r (a <> [desc])+ where desc = Node "Captures RemoteHost/IP" (Detail "True")++-- | Description documentation+instance (HasDocumentApi b, KnownSymbol desc) => HasDocumentApi (Description (desc :: Symbol) :> b) where+ document r a = document @b r (a <> [desc])+ where desc = Node "Description" (Detail $ symbolVal' @desc)++-- | Summary documentation+instance (HasDocumentApi b, KnownSymbol s) => HasDocumentApi (Summary (s :: Symbol) :> b) where+ document r a = document @b r (a <> [desc])+ where desc = Node "Summary" (Detail $ symbolVal' @s)++-- | Vault documentation+instance HasDocumentApi b => HasDocumentApi (Vault :> b) where+ document r a = document @b r (a <> [desc])+ where desc = Node "Vault" (Detail "True")++-- | Basic authentication documentation+instance (HasDocumentApi b, KnownSymbol realm, Typeable a) => HasDocumentApi (BasicAuth (realm :: Symbol) a :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "Basic Authentication" $+ Details [ Node "Realm" (Detail realm)+ , Node "UserData" (Detail userData)+ ]+ realm = symbolVal' @realm+ userData = typeText @a++-- | Authentication documentation+instance (HasDocumentApi b, KnownSymbol token) => HasDocumentApi (AuthProtect (token :: Symbol) :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "Authentication" (Detail authDoc)+ authDoc = symbolVal' @token++-- | Request header documentation+instance (HasDocumentApi b, KnownSymbol ct, Typeable typ) => HasDocumentApi (Header' m (ct :: Symbol) typ :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "RequestHeaders" $+ Details [ Node "Name" (Detail $ symbolVal' @ct)+ , Node "ContentType" (Detail $ typeText @typ)+ ]++-- | Query flag documentation+instance (HasDocumentApi b, KnownSymbol param) => HasDocumentApi (QueryFlag (param :: Symbol) :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "QueryFlag" $+ Details [ Node "Param" (Detail $ symbolVal' @param) ]++-- | Query param documentation+instance (HasDocumentApi b, KnownSymbol param, Typeable typ) => HasDocumentApi (QueryParam' m (param :: Symbol) typ :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "QueryParam" $+ Details [ Node "Param" (Detail $ symbolVal' @param)+ , Node "ContentType" (Detail $ typeText @typ)+ ]++-- | Query params documentation+instance (HasDocumentApi b, KnownSymbol param, Typeable typ) => HasDocumentApi (QueryParams (param :: Symbol) typ :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "QueryParams" $+ Details [ Node "Param" (Detail $ symbolVal' @param)+ , Node "ContentType" (Detail $ typeText @typ)+ ]++-- | Request body documentation+instance (HasDocumentApi b, Typeable ct, Typeable typ) => HasDocumentApi (ReqBody' m ct typ :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "RequestBody" $+ Details [ Node "Format" (Detail $ typeText @ct)+ , Node "ContentType" (Detail $ typeText @typ)+ ]++-- | Stream body documentation+instance (HasDocumentApi b, Typeable ct, Typeable typ) => HasDocumentApi (StreamBody' m ct typ :> b) where+ document r a = document @b r (a <> [formatted])+ where formatted = Node "StreamBody" $+ Details [ Node "Format" (Detail $ typeText @ct)+ , Node "ContentType" (Detail $ typeText @typ)+ ]++-- | Response documentation+-- Terminates here as responses are last parts of api endpoints+-- Note that request type information (GET, POST etc...) is contained here+instance (Typeable m, Typeable ct, Typeable typ) => HasDocumentApi (Verb m s ct typ) where+ document r a = Node r $+ Details (a <> [ requestType+ , response+ ])+ where requestType = Node "RequestType" (Detail $ typeText @m)+ response = Node "Response" $+ Details [ Node "Format" (Detail $ typeText @ct)+ , Node "ContentType" (Detail $ typeText @typ)+ ]++-- | Internal Helper utilities+typeText :: forall a. (Typeable a) => Text+typeText = pack . show . typeRep $ Proxy @a++symbolVal' :: forall n. KnownSymbol n => Text+symbolVal' = pack . symbolVal $ Proxy @n
+ src/Servant/Docs/Simple/Render.hs view
@@ -0,0 +1,124 @@+-- | Provides an Intermediate documentation structure ('Endpoints'), and renderable formats ('Renderable')+module Servant.Docs.Simple.Render ( Details (..)+ , Endpoints (..)+ , Node (..)+ , Renderable (..)+ , Json (..)+ , Pretty (..)+ , PlainText (..)+ ) where++import Data.Aeson (ToJSON (..), Value (..), object, (.=))+import Data.List (intersperse)+import Data.Text (Text, pack)+import Data.Text.Prettyprint.Doc (Doc, cat, line, nest, pretty, vcat, vsep)++-- | Intermediate documentation structure, a linked-list of endpoints (__'Node's__)+--+-- API type:+--+-- > type API = "users" :> ( "update" :> Response '[()] ()+-- > :<|> "get" :> Response '[()] ()+-- > )+--+-- Parsed into Endpoints:+--+-- > Endpoints [ Node "/users/update"+-- > (Details [ Node "Response"+-- > (Details [ Node "Format" (Detail "': * () ('[] *)")+-- > , Node "ContentType" (Detail "()")+-- > ])+-- > ])+-- > , Node "/users/get"+-- > (Details [ Node "Response"+-- > (Details [ Node "Format" (Detail "': * () ('[] *)")+-- > , Node "ContentType" (Detail "()")+-- > ])+-- > ])+-- > ]+--+-- For a breakdown reference 'Node'+--+-- For more examples reference [Test.Servant.Docs.Simple.Samples](https://github.com/Holmusk/servant-docs-simple/blob/master/test/Test/Servant/Docs/Simple/Samples.hs)+--+newtype Endpoints = Endpoints [Node] deriving stock (Eq, Show)++-- | Key-Value pair for endpoint parameters and their values+--+-- __Example 1__+--+-- An endpoint is represented as a node, with the __route__ as its parameter and its Details as its value+--+-- > Node "/users/get" <Details>+--+-- __Example 2__+--+-- Details of each endpoint can also be represented as nodes+--+-- Given the following:+--+-- > Response '[()] ()+--+-- This can be interpreted as a __Response__ parameter, with a value of 2 Details, __Format__ and __ContentType__+--+-- In turn, this:+--+-- > Format: @'[()]@+--+-- can be interpreted as a __Format__ parameter with a value of __@'[()]@__.+--+-- And so parsing __@Response '[()] ()@__ comes together as:+--+-- > Node "Response" --- Parameter+-- > (Details [ Node "Format" -- Parameter ---+-- > (Detail "': * () ('[] *)") -- Value |+-- > , Node "ContentType" -- Parameter | Value+-- > (Detail "()") -- Value |+-- > ]) ---+--++data Node = Node Text -- ^ Parameter name+ Details -- ^ Parameter value(s)+ deriving stock (Eq, Show)++-- | Value representation; see 'Endpoints' and 'Node' documentation for a clearer picture+data Details = Details [Node] -- ^ List of Parameter-Value pairs+ | Detail Text -- ^ Single Value+ deriving stock (Eq, Show)++-- | Convert __Endpoints__ into different documentation formats+class Renderable a where+ render :: Endpoints -> a++-- | Conversion to JSON using Data.Aeson+newtype Json = Json { getJson :: Value } deriving stock (Eq, Show)+instance Renderable Json where+ render = Json . toJSON++instance ToJSON Endpoints where+ toJSON (Endpoints endpoints) = toJSON $ Details endpoints++instance ToJSON Details where+ toJSON (Detail t) = String t+ toJSON (Details ls) = object . fmap jsonify $ ls+ where jsonify (Node name details) = name .= toJSON details++-- | Conversion to prettyprint+newtype Pretty ann = Pretty { getPretty :: Doc ann }++instance Renderable (Pretty ann) where+ render = Pretty . prettyPrint++prettyPrint :: Endpoints -> Doc ann+prettyPrint (Endpoints ls) = vcat . intersperse line $ toDoc 0 <$> ls++toDoc :: Int -> Node -> Doc ann+toDoc i (Node t d) = case d of+ Detail a -> cat [pretty t, ": ", pretty a]+ Details as -> nest i . vsep $ pretty t <> ":"+ : (toDoc (i + 4) <$> as)++-- | Conversion to plaintext+newtype PlainText = PlainText { getPlainText :: Text } deriving stock (Eq, Show)+instance Renderable PlainText where+ render = PlainText . pack . show . getPretty . render
+ test/Spec.hs view
@@ -0,0 +1,12 @@+import Test.Hspec+import Test.Hspec.Core.Spec (sequential)++import Test.Servant.Docs.Simple (mainSpec)+import Test.Servant.Docs.Simple.Parse (parseSpec)+import Test.Servant.Docs.Simple.Render (renderSpec)++main :: IO ()+main = hspec $ sequential $ do+ renderSpec+ parseSpec+ mainSpec
+ test/Test/Servant/Docs/Simple.hs view
@@ -0,0 +1,18 @@+-- | Unit tests for generating documentation of api++module Test.Servant.Docs.Simple (mainSpec) where+++import Test.Hspec (Spec, describe, it, shouldBe)+import Test.Servant.Docs.Simple.Samples++import Servant.Docs.Simple (document, documentWith)+import Servant.Docs.Simple.Render (Json)+++mainSpec :: Spec+mainSpec = describe "Generates Documentation" $ do+ it "should generate Plaintext" $+ document @ApiComplete `shouldBe` apiCompletePlainText+ it "should generate JSON" $+ documentWith @ApiComplete @Json `shouldBe` apiCompleteJson
+ test/Test/Servant/Docs/Simple/Parse.hs view
@@ -0,0 +1,23 @@+-- | Unit tests for generating documentation of api++module Test.Servant.Docs.Simple.Parse (parseSpec) where+++import Servant.API (EmptyAPI)+import Test.Hspec (Spec, describe, it, shouldBe)++import Test.Servant.Docs.Simple.Samples (ApiComplete, ApiMultiple, apiCompleteParsed,+ apiMultipleParsed)++import Servant.Docs.Simple.Parse (parse)+import Servant.Docs.Simple.Render (Endpoints (..))+++parseSpec :: Spec+parseSpec = describe "Parses API to Document Tree" $ do+ it "parses an EmptyAPI Details" $+ parse @EmptyAPI `shouldBe` Endpoints []+ it "parses all Servant API Combinators" $+ parse @ApiComplete `shouldBe` apiCompleteParsed+ it "parses an API with multiple endpoints" $+ parse @ApiMultiple `shouldBe` apiMultipleParsed
+ test/Test/Servant/Docs/Simple/Render.hs view
@@ -0,0 +1,17 @@+-- | Unit tests for generating documentation of api++module Test.Servant.Docs.Simple.Render (renderSpec) where+++import Test.Hspec (Spec, describe, it, shouldBe)+import Test.Servant.Docs.Simple.Samples (apiCompleteJson, apiCompleteParsed, apiCompletePlainText)++import Servant.Docs.Simple.Render (Json (..), PlainText (..), render)+++renderSpec :: Spec+renderSpec = describe "Renders Details" $ do+ it "should render as JSON" $+ render @Json apiCompleteParsed `shouldBe` apiCompleteJson+ it "should render as PlainText" $+ render @PlainText apiCompleteParsed `shouldBe` apiCompletePlainText
+ test/Test/Servant/Docs/Simple/Samples.hs view
@@ -0,0 +1,209 @@+-- | Contains sample Servant API types and their formatted counterparts+{-# LANGUAGE QuasiQuotes #-}++module Test.Servant.Docs.Simple.Samples (ApiComplete, ApiMultiple, apiCompletePlainText,+ apiCompleteParsed, apiCompleteJson, apiMultipleParsed) where++import Data.Aeson (Value (String), object)+import Servant.API ((:<|>), (:>), AuthProtect, BasicAuth, Capture, CaptureAll, Description, Header,+ HttpVersion, IsSecure, Post, QueryFlag, QueryParam, QueryParams, RemoteHost,+ ReqBody, StreamBody, Summary, Vault)++import Text.RawString.QQ (r)++import Servant.Docs.Simple.Render (Details (..), Endpoints (..), Json (..), Node (..),+ PlainText (..))+++type ApiComplete = StaticRouteTest :> DynRouteTest :> CaptureAllTest :> ApiDetails++apiCompleteParsed :: Endpoints+apiCompleteParsed = Endpoints [ Node "/test_route/{test::()}/{test::()}" apiDetails ]++apiCompleteJson :: Json+apiCompleteJson = Json (object [ ( "/test_route/{test::()}/{test::()}"+ , object [ ( "StreamBody"+ , object [ ( "Format", String "()")+ , ( "ContentType", String "()")+ ])++ , ( "Summary",String "sampleText" )++ , ( "Basic Authentication"+ , object [ ( "UserData", String "()")+ , ( "Realm", String "local")+ ])++ , ( "Captures Http Version", String "True")++ , ( "QueryParam"+ , object [ ( "Param",String "test")+ , ( "ContentType",String "()")+ ])++ , ( "Authentication", String "TEST_JWT")++ , ( "Response"+ , object [ ( "Format", String "': * () ('[] *)")+ , ( "ContentType",String "()")+ ])++ , ( "RequestType", String "'POST")++ , ( "Vault", String "True")++ , ( "Captures RemoteHost/IP", String "True")++ , ( "RequestHeaders"+ , object [ ( "Name",String "test")+ , ( "ContentType",String "()")+ ])++ , ( "SSL Only",String "True")++ , ( "QueryParams"+ , object [ ( "Param",String "test")+ , ( "ContentType",String "()")+ ])++ , ( "Description", String "sampleText")++ , ( "QueryFlag", object [( "Param",String "test")])++ , ( "RequestBody"+ , object [ ( "Format", String "': * () ('[] *)")+ , ( "ContentType",String "()")+ ])++ ])])++apiCompletePlainText :: PlainText+apiCompletePlainText = PlainText [r|/test_route/{test::()}/{test::()}:+Captures Http Version: True+SSL Only: True+Captures RemoteHost/IP: True+Description: sampleText+Summary: sampleText+Vault: True+Basic Authentication:+ Realm: local+ UserData: ()+Authentication: TEST_JWT+RequestHeaders:+ Name: test+ ContentType: ()+QueryFlag:+ Param: test+QueryParam:+ Param: test+ ContentType: ()+QueryParams:+ Param: test+ ContentType: ()+RequestBody:+ Format: ': * () ('[] *)+ ContentType: ()+StreamBody:+ Format: ()+ ContentType: ()+RequestType: 'POST+Response:+ Format: ': * () ('[] *)+ ContentType: ()|]++type ApiMultiple = "route1" :> DynRouteTest :> CaptureAllTest :> ApiDetails+ :<|> "route2" :> DynRouteTest :> CaptureAllTest :> ApiDetails+ :<|> "route3" :> DynRouteTest :> CaptureAllTest :> ApiDetails++apiMultipleParsed :: Endpoints+apiMultipleParsed = Endpoints [ Node "/route1/{test::()}/{test::()}" apiDetails+ , Node "/route2/{test::()}/{test::()}" apiDetails+ , Node "/route3/{test::()}/{test::()}" apiDetails+ ]+++type ApiDetails = HttpVersionTest+ :> IsSecureTest+ :> RemoteHostTest+ :> DescriptionTest+ :> SummaryTest+ :> VaultTest+ :> BasicAuthTest+ :> AuthTest+ :> HeaderTest+ :> QueryFlagTest+ :> QueryParamTest+ :> QueryParamsTest+ :> ReqBodyTest+ :> StreamBodyTest+ :> ResponseTest++apiDetails :: Details+apiDetails = Details [ Node "Captures Http Version" (Detail "True")+ , Node "SSL Only" (Detail "True")+ , Node "Captures RemoteHost/IP" (Detail "True")+ , Node "Description" (Detail "sampleText")+ , Node "Summary" (Detail "sampleText")+ , Node "Vault" (Detail "True")+ , Node "Basic Authentication" (Details [ Node "Realm" (Detail "local")+ , Node "UserData" (Detail "()")])++ , Node "Authentication" (Detail "TEST_JWT")++ , Node "RequestHeaders" (Details [ Node "Name" (Detail "test")+ , Node "ContentType" (Detail "()")])++ , Node "QueryFlag" (Details [Node "Param" (Detail "test")])++ , Node "QueryParam" (Details [ Node "Param" (Detail "test")+ , Node "ContentType" (Detail "()")])++ , Node "QueryParams" (Details [ Node "Param" (Detail "test")+ , Node "ContentType" (Detail "()")])++ , Node "RequestBody" (Details [ Node "Format" (Detail "': * () ('[] *)")+ , Node "ContentType" (Detail "()")])++ , Node "StreamBody" (Details [ Node "Format" (Detail "()")+ , Node "ContentType" (Detail "()")])++ , Node "RequestType" (Detail "'POST")++ , Node "Response" (Details [ Node "Format" (Detail "': * () ('[] *)")+ , Node "ContentType" (Detail "()")])]++type StaticRouteTest = "test_route"++type DynRouteTest = Capture "test" ()++type CaptureAllTest = CaptureAll "test" ()++type HttpVersionTest = HttpVersion++type IsSecureTest = IsSecure++type RemoteHostTest = RemoteHost++type DescriptionTest = Description "sampleText"++type SummaryTest = Summary "sampleText"++type VaultTest = Vault++type BasicAuthTest = BasicAuth "local" ()++type AuthTest = AuthProtect "TEST_JWT"++type HeaderTest = Header "test" ()++type QueryFlagTest = QueryFlag "test"++type QueryParamTest = QueryParam "test" ()++type QueryParamsTest = QueryParams "test" ()++type ReqBodyTest = ReqBody '[()] ()++type StreamBodyTest = StreamBody () ()++type ResponseTest = Post '[()] ()