packages feed

keiki-codec-json-test (empty) → 0.1.0.0

raw patch · 7 files changed

+645/−0 lines, 7 filesdep +QuickCheckdep +aesondep +base

Dependencies added: QuickCheck, aeson, base, bytestring, hspec, keiki, keiki-codec-json, keiki-codec-json-test, quickcheck-instances, text

Files

+ CHANGELOG.md view
@@ -0,0 +1,43 @@+# Changelog++All notable changes to this package are documented in this file.+The format follows [Keep a Changelog](https://keepachangelog.com/),+and this project adheres to the+[Haskell PVP](https://pvp.haskell.org).+++## [Unreleased]+++## [0.1.0.0] — 2026-06-07++Initial Hackage release. Co-released with `keiki-0.1.0.0` and+`keiki-codec-json-0.1.0.0`.++### Added++- `Keiki.Codec.JSON.Test.Golden` — the case-#10 detector:+  `data SlotGolden a = SlotGolden { sgInput :: a, sgBytes :: LBS.ByteString }`+  and `slotGoldenSpec :: (Aeson.ToJSON a, Aeson.FromJSON a, Eq a, Show a)+  => String -> SlotGolden a -> Hspec.Spec`. Pins a per-slot-type+  golden bytes value; fails loudly when the slot's `ToJSON`+  instance silently changes (the schema-evolution failure mode the+  shape hash cannot detect by design).+- `Keiki.Codec.JSON.Test` — library-ised exposure of the EP-36 M3+  round-trip and sensitivity disciplines:+  - `class ArbitraryRegFile (rs :: [Slot])` with inductive+    `arbRegFile :: Gen (RegFile rs)`.+  - `regFileCodecProps @rs :: Spec` — four QuickCheck properties+    (Value-path round-trip, Encoding-path round-trip, within-path+    determinism on both paths).+  - `data SomeKnownRegFileShape`, `someKnownShape @rs`,+    `regFileShapeSensitivitySpec` — parameterised baseline ++    mutation list; asserts each mutation flips the shape hash.++### Validated against++- GHC 9.12.2 locally on macOS aarch64 and in CI on Ubuntu Linux+  x86_64 (see `.github/workflows/ci.yml`).+- 7 self-test assertions exercising every public helper against a+  toy `Email` slot type and `DemoSlots` / `DemoSlotsRenamed`+  baseline + mutation pair.
+ README.md view
@@ -0,0 +1,91 @@+# keiki-codec-json-test++Property-test toolkit for downstream consumers of+[`keiki-codec-json`](../keiki-codec-json/README.md).++Use this package in test suites that persist `RegFile rs` snapshots as+JSON and want release-time protection against schema drift. Production+code that only needs JSON encoding and decoding should depend on+`keiki-codec-json`, not this package.++## What this is for++The primary tool is the **case-#10 detector**: a per-slot-type golden-byte+test that catches a silent change to a slot type's `Aeson.ToJSON`+instance. This is the schema-evolution failure mode the shape hash+in [`Keiki.Shape`](../src/Keiki/Shape.hs) cannot detect by design —+the hash is over the slot type's *TypeRep*, not over its encoding.+If a consumer edits the `ToJSON` instance to change the on-the-wire+shape, the hash stays the same and old snapshots silently fail to+decode. `slotGoldenSpec` is the contract anchor that makes the drift+loud and obvious.++The secondary helpers expose the in-tree property disciplines as+library functions parameterised over the consumer's own slot list:+Value-path and Encoding-path round-trip, within-path determinism, and+structural sensitivity.++## Using++```haskell+import Data.Proxy (Proxy (..))+import qualified Data.Text as T+import Test.Hspec (describe, hspec)++import Keiki.Codec.JSON.Test+  ( regFileCodecProps+  , regFileShapeSensitivitySpec+  , someKnownShape+  )+import Keiki.Codec.JSON.Test.Golden+  ( SlotGolden (..)+  , slotGoldenSpec+  )++-- Your slot type and slot lists:+-- data Email = Email Text deriving (...)+-- type MySlots = '[ '("email", Email), '("count", Int) ]+-- type MySlotsRenamed = '[ '("emailAddress", Email), '("count", Int) ]++main :: IO ()+main = hspec $ do+  -- Case-#10 detector. Add one slotGoldenSpec per slot type whose+  -- ToJSON / FromJSON instances you want to pin.+  slotGoldenSpec "Email" (SlotGolden+    { sgInput = Email (T.pack "alice@example.com")+    , sgBytes = "\"alice@example.com\""+    })++  -- Round-trip + determinism over the snapshot's slot list.+  describe "props: MySlots" (regFileCodecProps @MySlots)++  -- Sensitivity: every named mutation must flip the shape hash.+  describe "sensitivity: MySlots" $+    regFileShapeSensitivitySpec+      (Proxy @MySlots)+      [ ("rename email", someKnownShape @MySlotsRenamed) ]+```++The toolkit assumes each slot type has `Aeson.ToJSON`,+`Aeson.FromJSON`, `Arbitrary` (for the property suite),+`CanonicalTypeName` (for the shape hash; default `Typeable`-based+instance is usually enough), `Eq`, and `Show`. If your slot type+lacks `Arbitrary`, write one — it is typically a one-liner via+`quickcheck-instances` for the standard library types.++## When you don't need this++`keiki-codec-json` alone is sufficient for production use. This+package is opt-in for test suites. Pulling it in adds `QuickCheck`,+`hspec`, and `quickcheck-instances` as transitive deps; if you only+want the codec in production, do not depend on this package.++## Running the self-test++```sh+cabal test keiki-codec-json-test:keiki-codec-json-test-test+```++The self-test exercises every public helper against a small toy+fixture (`Email`, `DemoSlots`, `DemoSlotsRenamed`). Expected+output: 7 examples, 0 failures.
+ keiki-codec-json-test.cabal view
@@ -0,0 +1,86 @@+cabal-version:   3.0+name:            keiki-codec-json-test+version:         0.1.0.0+synopsis:+  Property-test toolkit for keiki-codec-json downstream consumers.++description:+  Sibling package to keiki-codec-json providing two hspec-callable+  test helpers consumers wire into their own test suites:+  .+  * @Keiki.Codec.JSON.Test.Golden.slotGoldenSpec@ — a per-slot-type+  golden-byte detector that catches EP-36 §4 case #10 (silent+  @Aeson.ToJSON@ instance change), the schema-evolution case the+  shape hash cannot detect by design.+  .+  * @Keiki.Codec.JSON.Test.regFileCodecProps@ /+  @regFileShapeSensitivitySpec@ — library-ised versions of the+  EP-36 M3 round-trip + sensitivity disciplines, parameterised+  over the consumer's own slot list.+  .+  Split into a separate package so consumers of @keiki-codec-json@+  for production do not transitively pick up @QuickCheck@ /+  @hspec@ / @quickcheck-instances@.++license:         BSD-3-Clause+author:          Nadeem Bitar+maintainer:      nadeem@gmail.com+copyright:       2026 Nadeem Bitar+category:        Codec, Testing+build-type:      Simple+tested-with:     GHC >=9.12 && <9.13+extra-doc-files:+  CHANGELOG.md+  README.md++common warnings+  ghc-options:+    -Wall -Wcompat -Widentities -Wincomplete-record-updates+    -Wincomplete-uni-patterns -Wpartial-fields -Wredundant-constraints++common shared-extensions+  default-language:   GHC2024+  default-extensions:+    AllowAmbiguousTypes+    DuplicateRecordFields+    FunctionalDependencies+    OverloadedLabels+    OverloadedRecordDot+    OverloadedStrings+    UndecidableInstances++library+  import:          warnings, shared-extensions+  exposed-modules:+    Keiki.Codec.JSON.Test+    Keiki.Codec.JSON.Test.Golden++  hs-source-dirs:  src+  build-depends:+    , aeson                 ^>=2.2+    , base                  ^>=4.21+    , bytestring            ^>=0.12+    , hspec                 ^>=2.11+    , keiki                 ^>=0.1+    , keiki-codec-json      ^>=0.1+    , QuickCheck            ^>=2.15+    , quickcheck-instances  ^>=0.3+    , text                  ^>=2.1++test-suite keiki-codec-json-test-test+  import:         warnings, shared-extensions+  type:           exitcode-stdio-1.0+  hs-source-dirs: test+  main-is:        Spec.hs+  other-modules:  Keiki.Codec.JSON.Test.Demo+  build-depends:+    , aeson                  ^>=2.2+    , base                   ^>=4.21+    , bytestring             ^>=0.12+    , hspec                  ^>=2.11+    , keiki                  ^>=0.1+    , keiki-codec-json       ^>=0.1+    , keiki-codec-json-test  ^>=0.1+    , QuickCheck             ^>=2.15+    , quickcheck-instances   ^>=0.3+    , text                   ^>=2.1
+ src/Keiki/Codec/JSON/Test.hs view
@@ -0,0 +1,248 @@+-- |+-- Module      : Keiki.Codec.JSON.Test+-- Description : Library-ised round-trip and sensitivity disciplines+--               for downstream @keiki-codec-json@ consumers.+--+-- Wires three helpers into a consumer's @hspec@ test suite:+--+-- * 'regFileCodecProps' — four QuickCheck properties (Value-path and+--   Encoding-path round-trip, within-path determinism on both+--   paths) against the consumer's own slot list. Mirror of the+--   EP-36 M3 in-tree property suite at+--   @keiki-codec-json/test/Keiki/Codec/JSON/PropSpec.hs@,+--   parameterised so the consumer applies it to their own slot list+--   instead of @ExemplarSlots@.+--+-- * 'regFileShapeSensitivitySpec' — for each named schema-evolution+--   mutation the consumer supplies, assert+--   @'regFileShapeHash' mutation /= 'regFileShapeHash' baseline@.+--   Mirror of the EP-36 M3 in-tree sensitivity assertions at+--   @keiki-codec-json/test/Keiki/Codec/JSON/SensitivitySpec.hs@,+--   parameterised over arbitrary baseline + mutation list.+--+-- * 'ArbitraryRegFile' — an inductive QuickCheck generator class+--   for @'Keiki.Core.RegFile' rs@, building a slot value per slot+--   via each slot type's 'Test.QuickCheck.Arbitrary' instance.+--   Lifted verbatim from the in-tree definition at+--   @keiki-codec-json/test/Keiki/Codec/JSON/Fixtures.hs@.+--+-- These helpers re-expose existing EP-36 disciplines through a+-- stable consumer-facing API. The /new/ test surface in+-- @keiki-codec-json-test@ is the case-#10 detector in+-- "Keiki.Codec.JSON.Test.Golden"; see that module's documentation.+module Keiki.Codec.JSON.Test+  ( -- * Arbitrary generator for slot lists+    ArbitraryRegFile (..),++    -- * Round-trip + determinism properties+    regFileCodecProps,++    -- * Sensitivity helper+    SomeKnownRegFileShape (..),+    someKnownShape,+    regFileShapeSensitivitySpec,+  )+where++import Data.Aeson qualified as Aeson+import Data.Aeson.Encoding qualified as AesonEnc+import Data.Proxy (Proxy (..))+import GHC.TypeLits (KnownSymbol)+-- Arbitrary UTCTime, Text, etc.++import Keiki.Codec.JSON+  ( RegFileToJSON,+    regFileFromJSON,+    regFileToEncoding,+    regFileToJSON,+  )+import Keiki.Core (RegFile (..), Slot)+import Keiki.Shape (KnownRegFileShape, regFileShapeHash)+import Test.Hspec (Spec, describe, it, shouldSatisfy)+import Test.QuickCheck (Arbitrary (..), Gen, Property, forAllShow, (===))+import Test.QuickCheck.Instances ()++-- * Arbitrary generator for slot lists --------------------------------------++-- | Inductive QuickCheck generator over a slot list. Any slot list+-- whose slot types all have 'Arbitrary' instances automatically has+-- an 'ArbitraryRegFile' instance through the inductive pair below.+--+-- @+-- type MySlots = '[ '(\"count\", Int), '(\"note\", Text) ]+-- gen :: Gen (RegFile MySlots)+-- gen = arbRegFile+-- @+--+-- Verbatim mirror of the in-tree definition in+-- @keiki-codec-json/test/Keiki/Codec/JSON/Fixtures.hs@, exposed here+-- so external consumers can import the class.+class ArbitraryRegFile (rs :: [Slot]) where+  arbRegFile :: Gen (RegFile rs)++instance ArbitraryRegFile '[] where+  arbRegFile = pure RNil++instance+  ( KnownSymbol s,+    Arbitrary t,+    ArbitraryRegFile rs+  ) =>+  ArbitraryRegFile ('(s, t) ': rs)+  where+  arbRegFile = RCons (Proxy @s) <$> arbitrary <*> arbRegFile++-- * Round-trip + determinism properties -------------------------------------++-- | Run the EP-36 M3 codec property suite against an arbitrary slot+-- list. Four properties, 100 QuickCheck samples each by default+-- (override with @--qc-max-success@):+--+-- * /Value path round-trip:/+--   @'regFileFromJSON' . 'regFileToJSON' === Right@+--   (compared via re-encoded bytes; see implementation note below).+-- * /Encoding path round-trip:/ same, via 'regFileToEncoding' and+--   round-tripping through 'Aeson.decode'.+-- * /Value path within-path determinism (R9):/ re-encoding the same+--   'RegFile' yields byte-equal output.+-- * /Encoding path within-path determinism (R9):/ same.+--+-- Implementation note: 'RegFile' has no 'Eq' or 'Show' instance (the+-- slot list is heterogeneous), so the round-trip assertion uses the+-- re-encoded bytes as the comparison point. If the parsed+-- 'RegFile' re-encodes to the same bytes as the original, the+-- round-trip is structurally exact. This is the same trick the+-- in-tree EP-36 M3 spec uses.+--+-- Type-application invocation form:+--+-- @+-- regFileCodecProps \@MyAppSnapshotSlots+-- @+--+-- The constraint requires (a) the slot list be a 'RegFileToJSON'+-- (auto-derived when each slot's type has 'Aeson.ToJSON' ++-- 'Aeson.FromJSON' + 'KnownSymbol'), and (b) an 'ArbitraryRegFile'+-- instance (auto-derived when each slot's type has 'Arbitrary').+regFileCodecProps ::+  forall rs.+  ( RegFileToJSON rs,+    ArbitraryRegFile rs+  ) =>+  Spec+regFileCodecProps = do+  describe "Roundtrip" $ do+    it "Value path round-trips" $+      forAllShow (arbRegFile @rs) showRf valueRoundTrip+    it "Encoding path round-trips" $+      forAllShow (arbRegFile @rs) showRf encodingRoundTrip++  describe "Determinism (R9 within-path)" $ do+    it "Value path is deterministic" $+      forAllShow (arbRegFile @rs) showRf valueDeterministic+    it "Encoding path is deterministic" $+      forAllShow (arbRegFile @rs) showRf encodingDeterministic+  where+    showRf :: RegFile rs -> String+    showRf = show . regFileToJSON++    valueRoundTrip :: RegFile rs -> Property+    valueRoundTrip rf =+      let bytes = Aeson.encode (regFileToJSON rf)+       in case Aeson.decode bytes of+            Nothing ->+              False+                === error+                  "Aeson.decode failed on our own Value-path encoder output"+            Just v -> case regFileFromJSON @rs v of+              Left msg ->+                False === error ("regFileFromJSON failed: " <> msg)+              Right rf' ->+                Aeson.encode (regFileToJSON rf') === bytes++    encodingRoundTrip :: RegFile rs -> Property+    encodingRoundTrip rf =+      let bytes = AesonEnc.encodingToLazyByteString (regFileToEncoding rf)+       in case Aeson.decode bytes of+            Nothing ->+              False+                === error+                  "Aeson.decode failed on streaming-encoder output"+            Just v -> case regFileFromJSON @rs v of+              Left msg ->+                False === error ("regFileFromJSON failed: " <> msg)+              Right rf' ->+                AesonEnc.encodingToLazyByteString (regFileToEncoding rf')+                  === bytes++    valueDeterministic :: RegFile rs -> Property+    valueDeterministic rf =+      Aeson.encode (regFileToJSON rf)+        === Aeson.encode (regFileToJSON rf)++    encodingDeterministic :: RegFile rs -> Property+    encodingDeterministic rf =+      AesonEnc.encodingToLazyByteString (regFileToEncoding rf)+        === AesonEnc.encodingToLazyByteString (regFileToEncoding rf)++-- * Sensitivity helper ------------------------------------------------------++-- | A type-erased witness that a slot list is hashable. The+-- existential is what lets 'regFileShapeSensitivitySpec' take a+-- heterogeneous list of mutated slot lists in one parameter.+--+-- Construct values via 'someKnownShape' with a type application.+data SomeKnownRegFileShape where+  SomeKnownRegFileShape ::+    (KnownRegFileShape rs) => Proxy rs -> SomeKnownRegFileShape++-- | Convenience constructor for 'SomeKnownRegFileShape'.+--+-- @+-- someKnownShape \@MyMutatedSlots+-- @+--+-- equivalent to+--+-- @+-- SomeKnownRegFileShape ('Proxy' \@MyMutatedSlots)+-- @+someKnownShape ::+  forall rs.+  (KnownRegFileShape rs) =>+  SomeKnownRegFileShape+someKnownShape = SomeKnownRegFileShape (Proxy @rs)++-- | Run the EP-36 M3 sensitivity discipline against a baseline slot+-- list and a list of mutations. For each @(label, mutation)@ pair,+-- the spec asserts @'regFileShapeHash' mutation /= 'regFileShapeHash'+-- baseline@.+--+-- A failure means a structural change (the kind the hash MUST detect+-- per EP-36 R5 — slot rename / add / remove / reorder / type change /+-- newtype wrap / primitive→record / split / type rename) was silently+-- absorbed by the hash. The fix is to investigate why the canonical+-- pre-hash bytes did not differ for the named mutation.+--+-- Worked invocation:+--+-- @+-- regFileShapeSensitivitySpec \@MySlots ('Proxy' \@MySlots)+--   [ (\"add slot\", someKnownShape \@MySlotsPlusOne)+--   , (\"rename\",   someKnownShape \@MySlotsRenamed)+--   ]+-- @+regFileShapeSensitivitySpec ::+  forall baseline.+  (KnownRegFileShape baseline) =>+  Proxy baseline ->+  [(String, SomeKnownRegFileShape)] ->+  Spec+regFileShapeSensitivitySpec p mutations = do+  let baseline = regFileShapeHash p+  describe "Shape-hash sensitivity" $+    mapM_ (assertFlip baseline) mutations+  where+    assertFlip baseline (label, SomeKnownRegFileShape p') =+      it (label <> " flips the hash") $+        regFileShapeHash p' `shouldSatisfy` (/= baseline)
+ src/Keiki/Codec/JSON/Test/Golden.hs view
@@ -0,0 +1,81 @@+-- |+-- Module      : Keiki.Codec.JSON.Test.Golden+-- Description : Per-slot-type golden-byte ToJSON-change detector.+--+-- The lede of @keiki-codec-json-test@. The shape hash from+-- @"Keiki.Shape"@ discriminates snapshots on /structural/ changes+-- (slot rename / add / remove / reorder / type change) but is, by+-- design, /insensitive/ to a slot type's 'Data.Aeson.ToJSON' instance+-- content. If a consumer takes a slot type with one @ToJSON@ instance,+-- persists snapshots, then later edits the same type's @ToJSON@ to+-- emit a different shape (e.g. wrap a bare string in+-- @{"address":...}@), the shape hash remains identical and old+-- snapshots silently fail to decode. This is EP-36 §4 case #10.+--+-- The 'slotGoldenSpec' detector is the contract anchor: it pins a+-- golden bytes value for each slot type and fails loudly the moment+-- the bytes diverge. Two assertions per slot type:+--+-- 1. @'Data.Aeson.encode' (sgInput g) == sgBytes g@ — the @ToJSON@+--    instance still emits the pinned bytes.+-- 2. @'Data.Aeson.decode' (sgBytes g) == Just (sgInput g)@ — the+--    @FromJSON@ instance still parses the pinned bytes back to the+--    original value.+--+-- See the keiki-codec-json-test/README.md for a worked example.+module Keiki.Codec.JSON.Test.Golden+  ( SlotGolden (..),+    slotGoldenSpec,+  )+where++import Data.Aeson qualified as Aeson+import Data.ByteString.Lazy qualified as LBS+import Test.Hspec (Spec, describe, it, shouldBe)++-- | A pinned (input, expected-bytes) golden pair for a slot type.+--+-- Authoring tip: capture @sgBytes@ by running @Aeson.encode@ on the+-- canonical input value once, hand-inspecting the bytes, and pasting+-- the result as a string literal. Future drift in @ToJSON@ trips the+-- detector.+data SlotGolden a = SlotGolden+  { -- | Canonical input value the golden bytes are pinned against.+    sgInput :: a,+    -- | Expected @Aeson.encode (sgInput g)@ output, in lazy+    -- ByteString form for direct equality with 'Aeson.encode'.+    sgBytes :: LBS.ByteString+  }++-- | Run the case-#10 ToJSON-change detector for a slot type. Two+-- assertions inside a @describe@ block:+--+-- * @ToJSON matches golden bytes@ — failure indicates the slot+--   type's @ToJSON@ instance has silently changed since the golden+--   was pinned. The shape hash will NOT catch this; only this+--   detector will.+-- * @FromJSON parses golden bytes back to the input@ — failure+--   indicates either @FromJSON@ has diverged from @ToJSON@, or the+--   golden was authored against bytes the current decoder rejects.+--+-- Wire into an @hspec@ test suite alongside the rest of your specs:+--+-- @+-- spec :: Spec+-- spec = do+--   slotGoldenSpec "Email" (SlotGolden { sgInput = Email "a\@b.c"+--                                      , sgBytes = "\"a\@b.c\"" })+--   ...+-- @+slotGoldenSpec ::+  (Aeson.ToJSON a, Aeson.FromJSON a, Eq a, Show a) =>+  -- | A human-readable label for the slot type (used as the+  -- @describe@ heading; typically the type's name).+  String ->+  SlotGolden a ->+  Spec+slotGoldenSpec name g = describe name $ do+  it "ToJSON matches golden bytes" $+    Aeson.encode (sgInput g) `shouldBe` sgBytes g+  it "FromJSON parses golden bytes back to the input" $+    Aeson.decode (sgBytes g) `shouldBe` Just (sgInput g)
+ test/Keiki/Codec/JSON/Test/Demo.hs view
@@ -0,0 +1,45 @@+{-# LANGUAGE DeriveAnyClass #-}++-- | Toy fixtures for the @keiki-codec-json-test@ self-test suite.+-- A demonstration of the typical consumer pattern: one custom slot+-- type ('Email'), a baseline slot list ('DemoSlots'), and a mutated+-- variant ('DemoSlotsRenamed') used to exercise the sensitivity+-- helper.+module Keiki.Codec.JSON.Test.Demo+  ( Email (..),+    DemoSlots,+    DemoSlotsRenamed,+  )+where++import Data.Aeson qualified as Aeson+import Data.Text (Text)+import GHC.Generics (Generic)+import Keiki.Shape (CanonicalTypeName)+import Test.QuickCheck (Arbitrary (..))+import Test.QuickCheck.Instances ()++-- | A consumer-supplied slot type. Production users would carry+-- richer invariants (e.g. validate the @\@@ separator on+-- construction); for the self-test the structural type is enough.+newtype Email = Email Text+  deriving stock (Eq, Show, Generic)+  deriving newtype (Aeson.ToJSON, Aeson.FromJSON, Arbitrary)+  deriving anyclass (CanonicalTypeName)++-- | A consumer's snapshot slot list. The 'regFileCodecProps' helper+-- runs the EP-36 M3 property discipline against any slot list whose+-- slot types satisfy 'Aeson.ToJSON' / 'Aeson.FromJSON' /+-- 'Arbitrary' / 'CanonicalTypeName'.+type DemoSlots =+  '[ '("email", Email),+     '("count", Int)+   ]++-- | A schema-evolution mutation of 'DemoSlots': @email@ has been+-- renamed to @emailAddress@. The 'regFileShapeSensitivitySpec'+-- helper must observe a hash flip for this mutation.+type DemoSlotsRenamed =+  '[ '("emailAddress", Email),+     '("count", Int)+   ]
+ test/Spec.hs view
@@ -0,0 +1,51 @@+-- | Self-test for @keiki-codec-json-test@. Exercises every public+-- helper against the toy 'Email' / 'DemoSlots' / 'DemoSlotsRenamed'+-- fixtures defined in "Keiki.Codec.JSON.Test.Demo".+--+-- Running this suite is the closest a maintainer can get to "what a+-- downstream consumer's test looks like" without writing an external+-- example consumer. Failures here mean the toolkit's public surface+-- has regressed; consumer-side tests would have failed the same+-- assertions.+module Main (main) where++import Data.Proxy (Proxy (..))+import Data.Text qualified as T+import Keiki.Codec.JSON.Test+  ( regFileCodecProps,+    regFileShapeSensitivitySpec,+    someKnownShape,+  )+import Keiki.Codec.JSON.Test.Demo+  ( DemoSlots,+    DemoSlotsRenamed,+    Email (..),+  )+import Keiki.Codec.JSON.Test.Golden+  ( SlotGolden (..),+    slotGoldenSpec,+  )+import Test.Hspec (describe, hspec)++main :: IO ()+main = hspec $ do+  describe "Keiki.Codec.JSON.Test.Golden.slotGoldenSpec" $ do+    slotGoldenSpec+      "Email"+      ( SlotGolden+          { sgInput = Email (T.pack "a@b.c"),+            sgBytes = "\"a@b.c\""+          }+      )++  describe+    "Keiki.Codec.JSON.Test.regFileCodecProps @DemoSlots"+    (regFileCodecProps @DemoSlots)++  describe "Keiki.Codec.JSON.Test.regFileShapeSensitivitySpec" $+    regFileShapeSensitivitySpec+      (Proxy @DemoSlots)+      [ ( "rename email -> emailAddress",+          someKnownShape @DemoSlotsRenamed+        )+      ]