{- |
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