From 1a6b30d00c74f57d9aeef59d0d2f83b5fa3a9ca2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9ophile=20Choutri?= Date: Thu, 29 Aug 2024 14:04:01 +0200 Subject: [PATCH] Cookbook --- .readthedocs.yml | 23 +++ cabal.project | 55 +----- doc/conf.py | 13 +- doc/cookbook/basic-streaming/Streaming.lhs | 4 +- doc/cookbook/cabal.project | 22 --- doc/cookbook/cabal.project.local | 0 doc/cookbook/file-upload/FileUpload.lhs | 2 +- doc/cookbook/generic/Generic.lhs | 2 +- doc/cookbook/index.rst | 1 + doc/cookbook/multiverb/MultiVerb.lhs | 178 ++++++++++++++++++ doc/cookbook/multiverb/multiverb.cabal | 34 ++++ doc/cookbook/testing/Testing.lhs | 4 +- .../using-free-client/UsingFreeClient.lhs | 4 +- doc/requirements.txt | 8 +- 14 files changed, 258 insertions(+), 92 deletions(-) create mode 100644 .readthedocs.yml delete mode 100644 doc/cookbook/cabal.project delete mode 100644 doc/cookbook/cabal.project.local create mode 100644 doc/cookbook/multiverb/MultiVerb.lhs create mode 100644 doc/cookbook/multiverb/multiverb.cabal diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 000000000..fe06ed1d8 --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,23 @@ +# Read the Docs configuration file for Sphinx projects +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +# Required +version: 2 + +# Set the OS, Python version and other tools you might need +build: + os: ubuntu-22.04 + tools: + python: "3.12" + # You can also specify other tool versions: + # nodejs: "20" + # rust: "1.70" + # golang: "1.20" + +# Build documentation in the "docs/" directory with Sphinx +sphinx: + configuration: docs/conf.py + # You can configure Sphinx to use a different builder, for instance use the dirhtml builder for simpler URLs + # builder: "dirhtml" + # Fail on all warnings to avoid broken references + # fail_on_warning: true diff --git a/cabal.project b/cabal.project index 42cd81019..285b1f3f3 100644 --- a/cabal.project +++ b/cabal.project @@ -1,56 +1,5 @@ -packages: - servant/ - servant-auth/servant-auth - servant-auth/servant-auth-client - servant-auth/servant-auth-docs - servant-auth/servant-auth-server - servant-auth/servant-auth-swagger - - servant-client-core/ - servant-client/ - servant-docs/ - servant-foreign/ - servant-http-streams/ - servant-quickcheck/ - servant-server/ - servant-swagger/ - - doc/tutorial/ - --- servant streaming -packages: - servant-machines/ - servant-conduit/ - servant-pipes/ - --- servant GHCJS --- packages: --- servant-jsaddle/ - --- Cookbooks -packages: - doc/cookbook/basic-auth - doc/cookbook/curl-mock - doc/cookbook/custom-errors - doc/cookbook/basic-streaming - doc/cookbook/db-postgres-pool - doc/cookbook/db-sqlite-simple - doc/cookbook/file-upload - doc/cookbook/generic - doc/cookbook/hoist-server-with-context - doc/cookbook/https - doc/cookbook/jwt-and-basic-auth - doc/cookbook/pagination - -- doc/cookbook/sentry - -- Commented out because servant-quickcheck currently doesn't build. - -- doc/cookbook/testing - doc/cookbook/uverb - doc/cookbook/structuring-apis - doc/cookbook/using-custom-monad - doc/cookbook/using-free-client - -- doc/cookbook/open-id-connect - doc/cookbook/managed-resource - doc/cookbook/openapi3 +import: ./cabal/libraries/cabal.config +import: ./cabal/cookbook/cabal.config tests: True optimization: False diff --git a/doc/conf.py b/doc/conf.py index a34d7743b..3ac4b54f0 100644 --- a/doc/conf.py +++ b/doc/conf.py @@ -39,7 +39,12 @@ # The suffix(es) of source filenames. # You can specify multiple suffix as a list of string: # -source_suffix = ['.rst', '.md', '.lhs'] +source_suffix = { + '.rst': 'restructuredtext', + '.md': 'markdown', + '.lhs': 'markdown', +} + # The master toctree document. master_doc = 'index' @@ -63,7 +68,7 @@ # # This is also used if you do content translation via gettext catalogs. # Usually you set "language" from the command line for these cases. -language = None +language = 'en' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. @@ -166,6 +171,4 @@ # -- Markdown ------------------------------------------------------------- -source_parsers = { - '.lhs': CommonMarkParser, -} +extensions.append('recommonmark') diff --git a/doc/cookbook/basic-streaming/Streaming.lhs b/doc/cookbook/basic-streaming/Streaming.lhs index e027d8b87..7e4a4f90d 100644 --- a/doc/cookbook/basic-streaming/Streaming.lhs +++ b/doc/cookbook/basic-streaming/Streaming.lhs @@ -123,7 +123,7 @@ main = do go !acc (S.Yield _ s) = go (acc + 1) s _ -> do putStrLn "Try:" - putStrLn "cabal new-run cookbook-basic-streaming server" - putStrLn "cabal new-run cookbook-basic-streaming client 10" + putStrLn "cabal run cookbook-basic-streaming server" + putStrLn "cabal run cookbook-basic-streaming client 10" putStrLn "time curl -H 'Accept: application/json' localhost:8000/slow/5" ``` diff --git a/doc/cookbook/cabal.project b/doc/cookbook/cabal.project deleted file mode 100644 index f82acd464..000000000 --- a/doc/cookbook/cabal.project +++ /dev/null @@ -1,22 +0,0 @@ -packages: - basic-auth/ - curl-mock/ - db-mysql-basics/ - db-sqlite-simple/ - db-postgres-pool/ - using-custom-monad/ - jwt-and-basic-auth/ - hoist-server-with-context/ - file-upload/ - structuring-apis/ - https/ - pagination/ - sentry/ - testing/ - open-id-connect/ - ../../servant - ../../servant-server - ../../servant-client-core - ../../servant-client - ../../servant-docs - ../../servant-foreign diff --git a/doc/cookbook/cabal.project.local b/doc/cookbook/cabal.project.local deleted file mode 100644 index e69de29bb..000000000 diff --git a/doc/cookbook/file-upload/FileUpload.lhs b/doc/cookbook/file-upload/FileUpload.lhs index 87a294d34..17962907b 100644 --- a/doc/cookbook/file-upload/FileUpload.lhs +++ b/doc/cookbook/file-upload/FileUpload.lhs @@ -113,7 +113,7 @@ main = withSocketsDo . bracket (forkIO startServer) killThread $ \_threadid -> d If you run this, you should get: ``` bash -$ cabal new-build cookbook-file-upload +$ cabal build cookbook-file-upload [...] $ dist-newstyle/build/x86_64-linux/ghc-8.2.1/cookbook-file-upload-0.1/x/cookbook-file-upload/build/cookbook-file-upload/cookbook-file-upload Inputs: diff --git a/doc/cookbook/generic/Generic.lhs b/doc/cookbook/generic/Generic.lhs index 45180230a..c7ba443c5 100644 --- a/doc/cookbook/generic/Generic.lhs +++ b/doc/cookbook/generic/Generic.lhs @@ -107,7 +107,7 @@ main = do ("run-custom-monad":_) -> do putStrLn "Starting cookbook-generic with a custom monad at http://localhost:8000" run 8000 (appMyMonad AppCustomState) - _ -> putStrLn "To run, pass 'run' argument: cabal new-run cookbook-generic run" + _ -> putStrLn "To run, pass 'run' argument: cabal run cookbook-generic run" ``` ## Using generics together with a custom monad diff --git a/doc/cookbook/index.rst b/doc/cookbook/index.rst index 79a0179bf..426ca5afb 100644 --- a/doc/cookbook/index.rst +++ b/doc/cookbook/index.rst @@ -28,6 +28,7 @@ you name it! using-free-client/UsingFreeClient.lhs custom-errors/CustomErrors.lhs uverb/UVerb.lhs + multiverb/MultiVerb.lhs basic-auth/BasicAuth.lhs basic-streaming/Streaming.lhs jwt-and-basic-auth/JWTAndBasicAuth.lhs diff --git a/doc/cookbook/multiverb/MultiVerb.lhs b/doc/cookbook/multiverb/MultiVerb.lhs new file mode 100644 index 000000000..a760e7a58 --- /dev/null +++ b/doc/cookbook/multiverb/MultiVerb.lhs @@ -0,0 +1,178 @@ +# MultiVerb: Powerful endpoint types + +`MultiVerb` allows you to represent an API endpoint with multiple response types, status codes and headers. + +## Preliminaries + +```haskell +{-# LANGUAGE GHC2021 #-} +{-# LANGUAGE DataKinds #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE DerivingVia #-} + +import GHC.Generics +import Generics.SOP qualified as GSOP +import Network.Wai.Handler.Warp as Warp + +import Servant.API +import Servant.API.MultiVerb +import Servant.Server +import Servant.Server.Generic +``` + +## Writing an endpoint + +Let us create an endpoint that captures an 'Int' and has the following logic: + +* If the number is negative, we return status code 400 and an empty body; +* If the number is even, we return a 'Bool' in the response body; +* If the number is odd, we return another 'Int' in the response body. + + +Let us list all possible HTTP responses: +```haskell + +type Responses = + '[ RespondEmpty 400 "Negative" + , Respond 200 "Odd number" Int + , Respond 200 "Even number" Bool + ] +``` + +Let us create the return type: + +```haskell +data Result + = NegativeNumber + | Odd Int + | Even Bool + deriving stock (Generic) + deriving (AsUnion Responses) + via GenericAsUnion Responses Result + +instance GSOP.Generic Result +``` + +These deriving statements above tie together the responses and the return values, and the order in which they are defined matters. For instance, if `Even` and `Odd` had switched places in the definition of `Result`, this would provoke an error: + +``` +• No instance for ‘AsConstructor + ((:) @Type Int ('[] @Type)) (Respond 200 "Even number" Bool)’ + arising from the 'deriving' clause of a data type declaration +``` + +> If you would prefer to write an intance of 'AsUnion' by yourself, read more in +the “Implementing AsUnion manually” section. + +Finally, let us write our endpoint description: + +```haskell +type MultipleChoicesInt = + Capture "int" Int + :> MultiVerb + 'GET + '[JSON] + Responses + Result +``` + +## Integration in a routing table + +We want to integrate our endpoint into a wider routing table with another +endpoint: `version`, which returns the version of the API + +```haskell +data Routes mode = Routes + { choicesRoutes :: mode :- "choices" :> Choices + , version :: mode :- "version" :> Get '[JSON] Int + } + deriving stock (Generic) +``` + +```haskell +type Choices = NamedRoutes Choices' +data Choices' mode = Choices' + { choices :: mode :- MultipleChoicesInt + } + deriving stock (Generic) + +choicesServer :: Choices' AsServer +choicesServer = + Choices' + { choices = choicesHandler + } + +routesServer :: Routes AsServer +routesServer = + Routes + { choicesRoutes = choicesServer + , version = versionHandler + } + +choicesHandler :: Int -> Handler Result +choicesHandler parameter = + if parameter < 0 + then pure NegativeNumber + else + if even parameter + then pure $ Odd 3 + else pure $ Even True + +versionHandler :: Handler Int +versionHandler = pure 1 +``` + +We can now plug everything together: + + +```haskell +main :: IO () +main = do + putStrLn "Starting server on http://localhost:5000" + let server = genericServe routesServer + Warp.run 5000 server +``` + +Now let us run the server and observe how it behaves: + +``` +$ http http://localhost:5000/version +HTTP/1.1 200 OK +Content-Type: application/json;charset=utf-8 +Date: Thu, 29 Aug 2024 14:22:20 GMT +Server: Warp/3.4.1 +Transfer-Encoding: chunked + +1 +``` + + +``` +$ http http://localhost:5000/choices/3 +HTTP/1.1 200 OK +Content-Type: application/json;charset=utf-8 +Date: Thu, 29 Aug 2024 14:22:30 GMT +Server: Warp/3.4.1 +Transfer-Encoding: chunked + +true +``` + +``` +$ http http://localhost:5000/choices/2 +HTTP/1.1 200 OK +Content-Type: application/json;charset=utf-8 +Date: Thu, 29 Aug 2024 14:22:33 GMT +Server: Warp/3.4.1 +Transfer-Encoding: chunked + +3 +``` + +``` +$ http http://localhost:5000/choices/-432 +HTTP/1.1 400 Bad Request +Date: Thu, 29 Aug 2024 14:22:41 GMT +Server: Warp/3.4.1 +Transfer-Encoding: chunked +``` diff --git a/doc/cookbook/multiverb/multiverb.cabal b/doc/cookbook/multiverb/multiverb.cabal new file mode 100644 index 000000000..57199de36 --- /dev/null +++ b/doc/cookbook/multiverb/multiverb.cabal @@ -0,0 +1,34 @@ +cabal-version: 3.0 +name: cookbook-multiverb +version: 0.0.1 +synopsis: MultiVerb cookbook +homepage: http://docs.servant.dev/ +license: BSD-3-Clause +license-file: ../../../servant/LICENSE +author: Servant Contributors +maintainer: haskell-servant-maintainers@googlegroups.com +category: Servant +build-type: Simple + +executable cookbook-multiverb + main-is: MultiVerb.lhs + build-depends: base < 5 + , aeson >= 2.2 + , aeson-pretty >= 0.8.8 + , async + , http-client + , mtl + , servant + , servant-client + , generics-sop + , sop-core + , servant-server + , servant-swagger + , string-conversions + , swagger2 + , wai + , warp + default-language: Haskell2010 + ghc-options: -Wall -pgmL markdown-unlit + build-tool-depends: markdown-unlit:markdown-unlit + diff --git a/doc/cookbook/testing/Testing.lhs b/doc/cookbook/testing/Testing.lhs index 0f9d1fc7f..1a7c61e72 100644 --- a/doc/cookbook/testing/Testing.lhs +++ b/doc/cookbook/testing/Testing.lhs @@ -175,7 +175,7 @@ businessLogicSpec = Let's run our tests and see what happens: ``` -$ cabal new-test all +$ cabal test all POST /user should create a user with a high enough ID should fail with a too-small ID FAILED [1] @@ -364,7 +364,7 @@ Out of the box, `hspec-wai` provides a lot of useful tools for us to run tests against our application. What happens when we run these tests? ``` -$ cabal new-test all +$ cabal test all ... GET /docs diff --git a/doc/cookbook/using-free-client/UsingFreeClient.lhs b/doc/cookbook/using-free-client/UsingFreeClient.lhs index 8b668582c..27893e3fe 100644 --- a/doc/cookbook/using-free-client/UsingFreeClient.lhs +++ b/doc/cookbook/using-free-client/UsingFreeClient.lhs @@ -66,8 +66,8 @@ main = do test _ -> do putStrLn "Try:" - putStrLn "cabal new-run cookbook-using-free-client server" - putStrLn "cabal new-run cookbook-using-free-client client" + putStrLn "cabal run cookbook-using-free-client server" + putStrLn "cabal run cookbook-using-free-client client" ``` ## Test diff --git a/doc/requirements.txt b/doc/requirements.txt index c507e4386..f7081a039 100644 --- a/doc/requirements.txt +++ b/doc/requirements.txt @@ -1,4 +1,4 @@ -recommonmark==0.5.0 -Sphinx==1.8.4 -sphinx_rtd_theme>=0.4.2 -jinja2<3.1.0 +myst-parser ==4.0.0 +Sphinx ==7.4.7 +sphinx_rtd_theme ==2.0.0 +jinja2 ==3.1.4