servant-routes-golden (empty) → 0.1.0.0
raw patch · 7 files changed
+348/−0 lines, 7 filesdep +QuickCheckdep +aesondep +aeson-pretty
Dependencies added: QuickCheck, aeson, aeson-pretty, base, hspec, hspec-core, hspec-golden, servant, servant-routes, servant-routes-golden, text
Files
- CHANGELOG.md +16/−0
- LICENSE +30/−0
- README.md +107/−0
- servant-routes-golden.cabal +99/−0
- src/Servant/API/Routes/Golden.hs +54/−0
- test/Servant/API/Routes/GoldenSpec.hs +41/−0
- test/Spec.hs +1/−0
+ CHANGELOG.md view
@@ -0,0 +1,16 @@+# Changelog++All notable changes to `servant-routes-golden` will be documented in this file.++The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),+and this project adheres to [Haskell Package Versioning Policy](https://pvp.haskell.org).++## [Unreleased]++## [0.1.0.0] - 10.06.2025++- Use the `ToJSON` instance of `Routes` to generate golden tests for an API+- Test suite for the package uses these golden tests on a dummy API++[unreleased]: https://github.com/fpringle/servant-routes/compare/golden-v0.1.0.0...HEAD+[0.1.0.0]: https://github.com/fpringle/servant-routes/releases/tag/golden-v0.1.0.0
+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2025, Frederick Pringle++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++ * Redistributions of source code must retain the above copyright+ notice, this list of conditions and the following disclaimer.++ * Redistributions in binary form must reproduce the above+ copyright notice, this list of conditions and the following+ disclaimer in the documentation and/or other materials provided+ with the distribution.++ * Neither the name of Frederick Pringle nor the names of other+ contributors may be used to endorse or promote products derived+ from this software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,107 @@+# servant-routes-golden++This package lets us define [Golden tests](https://ro-che.info/articles/2017-12-04-golden-tests) using the `HasRoutes` class from [servant-routes](https://github.com/fpringle/servant-routes/blob/main/servant-routes#readme) and the [hspec-golden](https://hackage.haskell.org/package/hspec-golden) library.++See `Servant.API.Routes.Golden` for reference documentation.++## Example++```haskell+-- file src/API.hs+module API where++import Servant.API++type UserAPI =+ "users"+ :> ( "list" :> Get '[JSON] [User]+ :<|> "create" :> ReqBody '[JSON] UserCreateData :> Post '[JSON] UserID+ :<|> "detail" :> QueryParam' '[Required] "id" UserID :> Get '[JSON] User+ )++-- file test/APISpec.hs++module APISpec where++import API+import Servant.API.Routes.Golden+import Hspec++spec :: Spec+spec =+ it "UserAPI" $ goldenRoutes @UserAPI (show ''UserAPI)+```++We can run `cabal test` to generate the starting "golden file":++```bash+$ cabal test+API+ UserAPI [✔]+ First time execution. Golden file created.+```++Of course, if we run the test again, it should pass:++```bash+$ cabal test+API+ UserAPI [✔]+ Golden and Actual output didn't change+```++But let's say we change the API definition slightly:++```diff+type UserAPI =+ "users"+ :> ( "list" :> Get '[JSON] [User]+- :<|> "create" :> ReqBody '[JSON] UserCreateData :> Post '[JSON] UserID++ :<|> "create-new" :> ReqBody '[JSON] UserCreateData :> Post '[JSON] UserID+ :<|> "detail" :> QueryParam' '[Required] "id" UserID :> Get '[JSON] User+ )+```++Then when we run the tests again:++```bash+$ cabal test+API+ UserAPI [✘]+ Files golden and actual not match++Failures:++ test/APISpec.hs:9:3: + 1) Servant.API.Routes.Golden UserAPI+ expected: {+ "/users/create": {+ "POST": {+ "auths": [],+ "description": null,+ "method": "POST",+ "params": [],+ "path": "/users/create",+ "request_body": "UserCreateData",+ "request_headers": [],+ "response": {+ @@ 45 lines omitted @@+ + but got: {+ "/users/create-new": {+ "POST": {+ "auths": [],+ "description": null,+ "method": "POST",+ "params": [],+ "path": "/users/create-new",+ "request_body": "UserCreateData",+ "request_headers": [],+ "response": {+ @@ 45 lines omitted @@+```++This forces us to either:++- acknowledge that the golden files should be updated, and do so by running the [hgold CLI](https://github.com/stackbuilders/hspec-golden?tab=readme-ov-file#install-cli), or+- realise that our changes resulted in a change to the API which we didn't anticipate, so we have to fix them.
+ servant-routes-golden.cabal view
@@ -0,0 +1,99 @@+cabal-version: 3.0+name: servant-routes-golden+version: 0.1.0.0+synopsis: Golden test your Servant APIs using `servant-routes`+description:+ See the documentation of 'Servant.API.Routes.Golden'.+license: BSD-3-Clause+license-file: LICENSE+author: Frederick Pringle+maintainer: freddyjepringle@gmail.com+copyright: Copyright(c) Frederick Pringle 2025+homepage: https://github.com/fpringle/servant-routes+bug-reports: https://github.com/fpringle/servant-routes/issues+category: Servant, Web+build-type: Simple+extra-doc-files: CHANGELOG.md+ README.md++tested-with:+ GHC == 8.8.4+ , GHC == 8.10.7+ , GHC == 9.0.2+ , GHC == 9.2.4+ , GHC == 9.2.8+ , GHC == 9.4.2+ , GHC == 9.4.5+ , GHC == 9.6.1+ , GHC == 9.6.7+ , GHC == 9.8.2+ , GHC == 9.10.2+ , GHC == 9.12.2++source-repository head+ type: git+ location: https://github.com/fpringle/servant-routes+ subdir: servant-routes-golden++common warnings+ ghc-options: -Wall -Wno-unused-do-bind++common deps+ build-depends:+ , base >= 4.13 && < 5+ , aeson >= 2.0 && < 2.3+ , text >= 1.2 && < 2.2+ , aeson-pretty >= 0.8.8 && < 0.9+ , hspec-core >= 2.10.0 && < 2.12+ , hspec-golden >= 0.2 && < 0.3+ , servant-routes >= 0.1.0.0 && < 0.2++common extensions+ default-extensions:+ DeriveGeneric+ DerivingVia + FlexibleContexts+ FlexibleInstances+ LambdaCase+ NamedFieldPuns+ OverloadedStrings+ PackageImports+ RecordWildCards+ ScopedTypeVariables+ TypeApplications+ ViewPatterns+ DataKinds+ AllowAmbiguousTypes+ TypeFamilies+ TypeOperators++library+ import:+ warnings+ , deps+ , extensions+ ghc-options: -Wunused-packages+ exposed-modules:+ Servant.API.Routes.Golden+ hs-source-dirs: src+ default-language: Haskell2010++test-suite servant-routes-golden-test+ import:+ warnings+ , deps+ , extensions+ default-language: Haskell2010+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ other-modules:+ Servant.API.Routes.GoldenSpec+ build-tool-depends:+ hspec-discover:hspec-discover+ ghc-options: -Wno-orphans+ build-depends:+ , servant-routes-golden+ , servant >= 0.17 && < 0.21+ , hspec >= 2.9 && < 2.12+ , QuickCheck >= 2.14 && < 2.16
+ src/Servant/API/Routes/Golden.hs view
@@ -0,0 +1,54 @@+{- |+Module : Servant.API.Routes.Golden+Copyright : (c) Frederick Pringle, 2025+License : BSD-3-Clause+Maintainer : frederick.pringle@fpringle.com++The 'HasRoutes' class allows us to generate a list of 'Route's from a Servant+API type. Using "hspec-golden", we can generate automatic 'G.Golden' tests from+these APIs. If such a test fails, we know that the shape of our API has changed.+Therefore we must either:++- decide that the shape change is correct, and acknowledge that the golden files+ should be updated, by running the+ [hgold CLI](https://github.com/stackbuilders/hspec-golden?tab=readme-ov-file#install-cli), or+- realise that our changes resulted in a change to the API which we didn't+ intend/anticipate, so we have to fix them.+-}+module Servant.API.Routes.Golden+ ( -- * Generating golden tests using HasRoutes+ goldenRoutes+ , goldenRoutesSpec+ )+where++import Control.Monad ((>=>))+import qualified Data.Aeson as A+import qualified Data.Aeson.Encode.Pretty as P+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Encoding as TLE+import qualified Data.Text.Lazy.IO as TL+import Servant.API.Routes+import qualified Test.Hspec.Core.Spec as H+import qualified Test.Hspec.Golden as G++{- | Given an API type with a 'HasRoutes' instance, we can create a 'G.Golden' test+on the t'Routes' representation of that API. This can be used to automatically track+changes to the API shape during testing. For a concrete example see the+[README](https://github.com/fpringle/servant-routes/blob/main/servant-routes-golden/README.md).+-}+goldenRoutes :: forall api. (HasRoutes api) => String -> G.Golden A.Value+goldenRoutes name =+ (G.defaultGolden name "")+ { G.output = A.toJSON . Routes $ getRoutes @api+ , G.encodePretty = T.unpack . TL.toStrict . pretty+ , G.writeToFile = \fp -> TL.writeFile fp . pretty+ , G.readFromFile = A.eitherDecodeFileStrict @A.Value >=> either fail pure+ }+ where+ pretty = TLE.decodeUtf8 . P.encodePretty' (P.defConfig {P.confCompare = compare})++-- | Generate a 'H.Spec' for your API type.+goldenRoutesSpec :: forall api. (HasRoutes api) => String -> H.Spec+goldenRoutesSpec = H.it "Generates the correct Routes" . goldenRoutes @api
+ test/Servant/API/Routes/GoldenSpec.hs view
@@ -0,0 +1,41 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE TemplateHaskellQuotes #-}++module Servant.API.Routes.GoldenSpec+ ( spec+ )+where++import qualified Data.Text as T+import GHC.Generics+import Servant.API+import Servant.API.Routes.Golden+import Test.Hspec as H++type SubAPI = ReqBody '[JSON] String :> Post '[JSON] Int++type SubAPI2 = Header "h1" T.Text :> "x" :> ("y" :> Put '[JSON] String :<|> SubAPI)++type SubAPI3 =+ Header "h2" T.Text+ :> "x2"+ :> ( "y1" :> Description "desc" :> Put '[JSON] String+ :<|> "z1" :> Header' '[Optional] "h3" Int :> Get '[JSON] [Integer]+ )++#if MIN_VERSION_servant(0,19,0)+data API mode = API+ { ep1 :: mode :- "sub1" :> SubAPI+ , ep2 :: mode :- "sub2" :> SubAPI2+ , ep3 :: mode :- "sub3" :> SubAPI3+ } deriving Generic+#endif++spec :: H.Spec+spec = do+ it "SubAPI" $ goldenRoutes @SubAPI (show ''SubAPI)+ it "SubAPI2" $ goldenRoutes @SubAPI2 (show ''SubAPI2)+ it "SubAPI3" $ goldenRoutes @SubAPI3 (show ''SubAPI3)+#if MIN_VERSION_servant(0,19,0)+ it "NamedRoutes API" $ goldenRoutes @(NamedRoutes API) (show ''API)+#endif
+ test/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}