packages feed

keiki-0.2.0.0: test/Keiki/Generics/THSpec.hs

{-# LANGUAGE TemplateHaskell #-}
-- Every splice emits a complete helper family; individual examples exercise
-- complementary subsets of those generated bindings.
{-# OPTIONS_GHC -Wno-unused-top-binds #-}

module Keiki.Generics.THSpec (spec) where

import Data.Map.Strict qualified as Map
import Data.Maybe (isNothing)
import Data.Proxy (Proxy (..))
import Data.Set qualified as Set
import GHC.Generics (Generic)
import Keiki.Core
import Keiki.Generics ()
import Keiki.Generics.TH
import Test.Hspec

-- A toy aggregate used purely to exercise the TH splices in
-- isolation. Two commands: one record-payload, one singleton. The
-- spec asserts the splices produce identifiers with the expected
-- behaviour — name, match/build, and the @inp@ projection.

data ToyData = ToyData {x :: Int, y :: Int}
  deriving (Eq, Show, Generic)

data ToyCmd
  = DoIt ToyData
  | NoArgs
  deriving (Eq, Show, Generic)

type ToyRegs =
  '[ '("x", Int),
     '("y", Int)
   ]

$( deriveAggregateCtors
     ''ToyCmd
     ''ToyRegs
     [ ("DoIt", "DoIt"),
       ("NoArgs", "NoArgs")
     ]
 )

-- | Empty register file populated with sentinel values. The 'inp'
-- projection test below reads from the input symbol via
-- 'TInpCtorField', not from the register file, so the values here do
-- not matter — they only need to be present so the carrier
-- type-checks. The 'KnownSymbol' constraints on each 'Proxy' are
-- discharged by the slot-list types in scope.
toyRegs :: RegFile ToyRegs
toyRegs = RCons (Proxy @"x") 0 (RCons (Proxy @"y") 0 RNil)

-- A second toy aggregate with distinct constructor names, used to
-- exercise the zero-spec @*All@ splices: 'deriveAggregateCtorsAll'
-- enumerates every command constructor, 'deriveWireCtorsAll' every
-- event constructor, each defaulting its short-name suffix to the
-- constructor's own name. Distinct names keep the generated
-- identifiers from colliding with the 'ToyCmd' splice output above.

data WidgetData = WidgetData {wa :: Int, wb :: Int}
  deriving (Eq, Show, Generic)

data AutoCmd
  = MakeWidget WidgetData
  | Sweep
  deriving (Eq, Show, Generic)

type AutoRegs =
  '[ '("wa", Int),
     '("wb", Int)
   ]

data GadgetData = GadgetData {gz :: Int}
  deriving (Eq, Show, Generic)

data AutoEvent
  = WidgetMade GadgetData
  | Swept
  deriving (Eq, Show, Generic)

$(deriveAggregateCtorsAll ''AutoCmd ''AutoRegs)
$(deriveWireCtorsAll ''AutoEvent)

autoRegs :: RegFile AutoRegs
autoRegs = RCons (Proxy @"wa") 0 (RCons (Proxy @"wb") 0 RNil)

-- A third toy aggregate with fresh constructor names, derived through
-- the single fused 'deriveAggregate' splice. The assertions below
-- prove one splice produced both the command-side
-- (@inCtorFoo@ / @inpFoo@ / @isTick@) and event-side (@wireFizzed@ /
-- @FizzedTermFields@) declarations.

data FooData = FooData {fa :: Int}
  deriving (Eq, Show, Generic)

data FusedCmd
  = Foo FooData
  | Tick
  deriving (Eq, Show, Generic)

type FusedRegs =
  '[ '("fa", Int)]

data FizzData = FizzData {fb :: Int}
  deriving (Eq, Show, Generic)

data FusedEvent
  = Fizzed FizzData
  deriving (Eq, Show, Generic)

$(deriveAggregate ''FusedCmd ''FusedRegs ''FusedEvent)

fusedRegs :: RegFile FusedRegs
fusedRegs = RCons (Proxy @"fa") 0 RNil

-- A fourth toy aggregate exercising 'deriveAggregateCtorsWith': one
-- constructor is overridden to an abbreviated short name, one keeps its
-- own name by default, and one is excluded entirely. The assertions
-- prove the overridden helper carries the abbreviated identifier, the
-- default helper keeps its constructor name, and the excluded
-- constructor produced no helper (the latter is verified by the fact
-- that this module compiles only because it never references
-- @inCtorSkipped@).

data OverData = OverData {oa :: Int}
  deriving (Eq, Show, Generic)

data OverCmd
  = LongCommandName OverData -- overridden to short "Brief"
  | Plain OverData -- default short "Plain"
  | Skipped -- excluded entirely
  deriving (Eq, Show, Generic)

type OverRegs = '[ '("oa", Int)]

$( deriveAggregateCtorsWith
     ''OverCmd
     ''OverRegs
     defaultDeriveCtorOptions
       { suffixOverrides = Map.fromList [("LongCommandName", "Brief")],
         excludeCtors = Set.fromList ["Skipped"]
       }
 )

overRegs :: RegFile OverRegs
overRegs = RCons (Proxy @"oa") 0 RNil

-- A fifth toy aggregate exercising 'deriveWireCtorsWith' on the event
-- side: one constructor is overridden to an abbreviated short name, one
-- keeps its own name, and one is excluded. Mirrors the OverCmd fixture.

data EvtData = EvtData {ea :: Int}
  deriving (Eq, Show, Generic)

data OverEvent
  = SomethingHappenedAtLength EvtData -- override -> "Happened"
  | Routine EvtData -- default -> "Routine"
  | Ignored -- excluded
  deriving (Eq, Show, Generic)

$( deriveWireCtorsWith
     ''OverEvent
     defaultDeriveWireOptions
       { suffixOverridesW = Map.fromList [("SomethingHappenedAtLength", "Happened")],
         excludeCtorsW = Set.fromList ["Ignored"]
       }
 )

spec :: Spec
spec = do
  describe "deriveAggregateCtors on a record-payload constructor (DoIt)" $ do
    it "names the InCtor after the source ctor" $
      icName inCtorDoIt `shouldBe` "DoIt"

    it "matches a DoIt value and yields a populated RegFile" $
      let payload = ToyData 17 23
          regfile = case icMatch inCtorDoIt (DoIt payload) of
            Just rf -> rf
            Nothing -> error "icMatch returned Nothing on DoIt"
       in (regfile ! #x, regfile ! #y) `shouldBe` (17, 23)

    it "rejects a non-DoIt value" $
      isNothing (icMatch inCtorDoIt NoArgs) `shouldBe` True

    it "rebuilds DoIt from a populated RegFile" $
      let payload = ToyData 17 23
          rf = case icMatch inCtorDoIt (DoIt payload) of
            Just r -> r
            Nothing -> error "set-up icMatch failed"
       in icBuild inCtorDoIt rf `shouldBe` DoIt payload

    it "evalTerm (inpDoIt #x) on a DoIt input reads the x field" $
      evalTerm (inpDoIt #x) toyRegs (DoIt (ToyData 5 9)) `shouldBe` 5

    it "evalPred isDoIt agrees with constructor match" $ do
      evalPred isDoIt toyRegs (DoIt (ToyData 0 0)) `shouldBe` True
      evalPred isDoIt toyRegs NoArgs `shouldBe` False

  describe "deriveAggregateCtors on a singleton constructor (NoArgs)" $ do
    it "names the InCtor after the source ctor" $
      icName inCtorNoArgs `shouldBe` "NoArgs"

    it "matches the NoArgs singleton" $
      case icMatch inCtorNoArgs NoArgs of
        Just _ -> pure ()
        Nothing -> expectationFailure "icMatch returned Nothing on NoArgs"

    it "rejects a record-payload value" $
      isNothing (icMatch inCtorNoArgs (DoIt (ToyData 0 0))) `shouldBe` True

    it "evalPred isNoArgs agrees with constructor match" $ do
      evalPred isNoArgs toyRegs NoArgs `shouldBe` True
      evalPred isNoArgs toyRegs (DoIt (ToyData 0 0)) `shouldBe` False

  describe "deriveAggregateCtorsAll (no spec list)" $ do
    it "discovers the record-payload command and names it after the ctor" $
      icName inCtorMakeWidget `shouldBe` "MakeWidget"

    it "matches MakeWidget and yields a populated RegFile" $
      let regfile = case icMatch inCtorMakeWidget (MakeWidget (WidgetData 3 4)) of
            Just rf -> rf
            Nothing -> error "icMatch returned Nothing on MakeWidget"
       in (regfile ! #wa, regfile ! #wb) `shouldBe` (3, 4)

    it "evalTerm (inpMakeWidget #wa) reads the wa field" $
      evalTerm (inpMakeWidget #wa) autoRegs (MakeWidget (WidgetData 5 9)) `shouldBe` 5

    it "discovers the singleton command and its guard" $ do
      icName inCtorSweep `shouldBe` "Sweep"
      evalPred isSweep autoRegs Sweep `shouldBe` True
      evalPred isSweep autoRegs (MakeWidget (WidgetData 0 0)) `shouldBe` False

  describe "deriveWireCtorsAll (no spec list)" $ do
    it "discovers the record-payload event and rebuilds it" $ do
      wcName wireWidgetMade `shouldBe` "WidgetMade"
      wcBuild wireWidgetMade (7, ()) `shouldBe` WidgetMade (GadgetData 7)

    it "discovers the singleton event and rebuilds it" $ do
      wcName wireSwept `shouldBe` "Swept"
      wcBuild wireSwept () `shouldBe` Swept

  describe "deriveAggregate (fused command + event)" $ do
    it "generates the command-side InCtor" $ do
      icName inCtorFoo `shouldBe` "Foo"
      evalTerm (inpFoo #fa) fusedRegs (Foo (FooData 11)) `shouldBe` 11

    it "generates the command-side singleton guard" $ do
      evalPred isTick fusedRegs Tick `shouldBe` True
      evalPred isTick fusedRegs (Foo (FooData 0)) `shouldBe` False

    it "generates the event-side WireCtor" $ do
      wcName wireFizzed `shouldBe` "Fizzed"
      wcBuild wireFizzed (13, ()) `shouldBe` Fizzed (FizzData 13)

  describe "deriveAggregateCtorsWith (overrides + excludes)" $ do
    it "uses the override short name for the identifier" $
      icName inCtorBrief `shouldBe` "LongCommandName"

    it "matches and reads through the overridden helper" $
      evalTerm (inpBrief #oa) overRegs (LongCommandName (OverData 7))
        `shouldBe` 7

    it "defaults the non-overridden constructor to its own name" $
      icName inCtorPlain `shouldBe` "Plain"

    it "the override guard distinguishes the two record ctors" $ do
      evalPred isBrief overRegs (LongCommandName (OverData 0)) `shouldBe` True
      evalPred isBrief overRegs (Plain (OverData 0)) `shouldBe` False

  -- The excluded constructor 'Skipped' generates no helpers: there is
  -- no inCtorSkipped / isSkipped in scope. Referencing one would fail
  -- to compile, which is the intended behaviour.

  describe "deriveWireCtorsWith (overrides + excludes)" $ do
    it "uses the override short name for the event identifier" $
      wcName wireHappened `shouldBe` "SomethingHappenedAtLength"

    it "rebuilds the overridden event through the abbreviated helper" $
      wcBuild wireHappened (5, ()) `shouldBe` SomethingHappenedAtLength (EvtData 5)

    it "defaults the non-overridden event to its own name" $ do
      wcName wireRoutine `shouldBe` "Routine"
      wcBuild wireRoutine (9, ()) `shouldBe` Routine (EvtData 9)

-- The excluded event constructor 'Ignored' generates no helper: there
-- is no wireIgnored in scope. Referencing it would fail to compile.