packages feed

keiki-codec-json-test-0.2.0.0: README.md

# 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
  ( regFileCodecPropsEq
  , regFileShapeSensitivitySpec
  , someKnownShape
  )
import Keiki.Codec.JSON.Test.Golden
  ( SlotGolden (..)
  , slotGoldenSpec
  )
import Keiki.Codec.JSON.Test.GoldenFile
  ( regFileGoldenFileSpec
  )

-- 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\""
    })

  -- Whole-snapshot backward compatibility. `myFixture` is a fixed
  -- `RegFile MySlots`; add the JSON file to extra-source-files.
  regFileGoldenFileSpec
    "MySlots"
    "test/golden/my-slots.value.json"
    myFixture

  -- Round-trip + determinism over the snapshot's slot list.
  describe "props: MySlots" (regFileCodecPropsEq @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.

`regFileCodecPropsEq` is the preferred round-trip helper: its inductive
`EqRegFile` comparison checks decoded slot values. The older
`regFileCodecProps` remains available for slot types without `Eq`, but it compares
re-encoded bytes and therefore cannot detect lossy decodes that re-encode
identically (notably `Just Nothing -> null -> Nothing`).

When an intentional wire change requires regenerating whole-snapshot files, use
the guarded two-run workflow:

```sh
KEIKI_UPDATE_GOLDENS=1 cabal test   # rewrites files, then fails deliberately
cabal test                          # must pass with the variable unset
git diff test/golden/               # review every changed byte
```

## 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: 11 examples, 0 failures.