keiki-0.1.0.0: test/Keiki/StrongSpec.hs
{-# LANGUAGE BlockArguments #-}
-- | Acceptance tests for the 'Data.Profunctor.Strong.Strong' instance
-- on 'SomeSymTransducer' (EP-29 of MasterPlan 9, M2).
--
-- The Strong instance threads an unrelated value through a
-- transducer: @first'@ accepts a pair @(a, c)@ and emits @(b, c)@,
-- where @c@ is read straight from the input and paired with @t@'s
-- output. @second'@ is the symmetric @(c, a) -> (c, b)@.
--
-- Implemented from primitives because MP-8 (EP-24) declined a general
-- @parallel@ combinator. 'Keiki.Profunctor.firstSym' is the one-off
-- equivalent.
--
-- Fixture: 'Keiki.Fixtures.EmailDelivery' wrapped in
-- 'someSymTransducer' with a 'RequestId' threaded through. The tests
-- cover:
--
-- * Forward processing — @first'@ on a sample input
-- @(SendEmail, 42)@ produces @(EmailSent, 42)@.
-- * @second'@ symmetry on the swapped pair.
-- * Survival of 'Keiki.Symbolic.isSingleValuedSym'.
-- * Sentinel preservation: @first' Cat.id == Cat.id@.
module Keiki.StrongSpec (spec) where
import Control.Category qualified as Cat
import Data.Profunctor (Strong (..))
import Data.Time.Calendar (fromGregorian)
import Data.Time.Clock (UTCTime (..), secondsToDiffTime)
import Keiki.Core
import Keiki.Fixtures.EmailDelivery
import Keiki.Profunctor
import Keiki.Symbolic (isSingleValuedSym, withSymPred)
import Test.Hspec
-- * Fixtures ----------------------------------------------------------------
newtype RequestId = RequestId Int
deriving stock (Eq, Show)
sampleAt :: UTCTime
sampleAt = UTCTime (fromGregorian 2026 5 9) (secondsToDiffTime 0)
sampleSendEmail :: EmailCmd
sampleSendEmail =
SendEmail
SendEmailData
{ recipient = "alice@example.com",
subject = "hello",
at = sampleAt
}
sampleEmailEvent :: EmailEvent
sampleEmailEvent =
EmailSent
EmailSentData
{ recipient = "alice@example.com",
subject = "hello",
at = sampleAt
}
someEmail :: SomeSymTransducer EmailCmd EmailEvent
someEmail = someSymTransducer emailDelivery
-- * Specs -------------------------------------------------------------------
spec :: Spec
spec = do
describe "first'" $ do
it "threads an unrelated RequestId through emailDelivery" $ do
let routed :: SomeSymTransducer (EmailCmd, RequestId) (EmailEvent, RequestId)
routed = first' someEmail
requestId = RequestId 42
case routed of
SomeSymTransducer t ->
omega t (initial t) (initialRegs t) (sampleSendEmail, requestId)
`shouldBe` [(sampleEmailEvent, requestId)]
SomeSymIdentity ->
expectationFailure
"first' (someSymTransducer emailDelivery) unexpectedly returned \
\the identity sentinel"
it "preserves Cat.id on the sentinel: first' Cat.id == Cat.id" $ do
let lifted :: SomeSymTransducer (Int, Bool) (Int, Bool)
lifted = first' (Cat.id :: SomeSymTransducer Int Int)
case lifted of
SomeSymIdentity -> pure ()
SomeSymTransducer _ ->
expectationFailure "first' Cat.id should preserve the identity sentinel"
describe "second'" $ do
it "threads an unrelated RequestId through emailDelivery on the second slot" $ do
let routed :: SomeSymTransducer (RequestId, EmailCmd) (RequestId, EmailEvent)
routed = second' someEmail
requestId = RequestId 99
case routed of
SomeSymTransducer t ->
omega t (initial t) (initialRegs t) (requestId, sampleSendEmail)
`shouldBe` [(requestId, sampleEmailEvent)]
SomeSymIdentity ->
expectationFailure
"second' (someSymTransducer emailDelivery) unexpectedly returned \
\the identity sentinel"
it "preserves Cat.id on the sentinel: second' Cat.id == Cat.id" $ do
let lifted :: SomeSymTransducer (Bool, Int) (Bool, Int)
lifted = second' (Cat.id :: SomeSymTransducer Int Int)
case lifted of
SomeSymIdentity -> pure ()
SomeSymTransducer _ ->
expectationFailure "second' Cat.id should preserve the identity sentinel"
describe "isSingleValuedSym survives first' / second'" $ do
it "single-valuedness is preserved across first'" $
case first' someEmail ::
SomeSymTransducer (EmailCmd, RequestId) (EmailEvent, RequestId) of
SomeSymTransducer t ->
isSingleValuedSym (withSymPred t) `shouldBe` True
SomeSymIdentity ->
expectationFailure
"first' on a non-identity wrapper returned the identity sentinel"
it "single-valuedness is preserved across second'" $
case second' someEmail ::
SomeSymTransducer (RequestId, EmailCmd) (RequestId, EmailEvent) of
SomeSymTransducer t ->
isSingleValuedSym (withSymPred t) `shouldBe` True
SomeSymIdentity ->
expectationFailure
"second' on a non-identity wrapper returned the identity sentinel"