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 +43/−0
- README.md +91/−0
- keiki-codec-json-test.cabal +86/−0
- src/Keiki/Codec/JSON/Test.hs +248/−0
- src/Keiki/Codec/JSON/Test/Golden.hs +81/−0
- test/Keiki/Codec/JSON/Test/Demo.hs +45/−0
- test/Spec.hs +51/−0
+ 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+ )+ ]