kiroku-store-0.3.0.0: test/Test/Category.hs
{- | Pure laws for the category accessor and safe stream-name constructor
('Kiroku.Store.Types.categoryName' and 'streamNameInCategory').
These pin the "category = substring before the first @-@" rule — enforced by
the @streams.category GENERATED ALWAYS AS split_part(stream_name,'-',1)@ column
and re-derived in 'Kiroku.Store.Notification' — so the public accessor cannot
drift from it. The round-trip law is the contract keiro's typed @Category@ API
(ExecPlan #66) builds on.
Pure; no database. Runs inside hspec via 'hspec-hedgehog'.
-}
module Test.Category (spec) where
import Data.Text (Text)
import Data.Text qualified as T
import Hedgehog
import Hedgehog.Gen qualified as Gen
import Hedgehog.Range qualified as Range
import Kiroku.Store.Types (CategoryName (..), StreamName (..), categoryName, streamNameInCategory)
import Test.Hspec
import Test.Hspec.Hedgehog (hedgehog)
{- | A non-empty, dash-free category. Includes @:@ and @$@ to exercise the
sub-namespacing convention (e.g. @"wf:orders"@) without the @-@ boundary.
-}
genCategoryText :: Gen Text
genCategoryText = Gen.text (Range.linear 1 16) (Gen.element categoryChars)
where
categoryChars = ['a' .. 'z'] <> ['0' .. '9'] <> [':', '$', '_']
{- | An arbitrary id segment. May contain @-@: the id lives after the first
@-@, so embedded dashes must not change the leading category.
-}
genSegment :: Gen Text
genSegment = Gen.text (Range.linear 0 16) (Gen.element segmentChars)
where
segmentChars = ['a' .. 'z'] <> ['0' .. '9'] <> ['-', ':', '_']
-- | An arbitrary stream name (may contain dashes anywhere, or none).
genStreamNameText :: Gen Text
genStreamNameText = Gen.text (Range.linear 0 24) (Gen.element nameChars)
where
nameChars = ['a' .. 'z'] <> ['0' .. '9'] <> ['-', ':', '_']
spec :: Spec
spec = describe "Kiroku.Store.Types category rule" $ do
describe "categoryName" $ do
it "takes the substring before the first dash" $
categoryName (StreamName "orders-1") `shouldBe` CategoryName "orders"
it "treats a dashless name as its own category" $
categoryName (StreamName "singleton") `shouldBe` CategoryName "singleton"
it "stops at the first dash even with later dashes" $
categoryName (StreamName "orders-a-b-c") `shouldBe` CategoryName "orders"
it "never yields a category containing a dash" $ hedgehog $ do
name <- forAll genStreamNameText
let CategoryName cat = categoryName (StreamName name)
assert (not (T.isInfixOf "-" cat))
describe "streamNameInCategory" $ do
it "joins category and segment with a dash" $
streamNameInCategory (CategoryName "orders") "1" `shouldBe` StreamName "orders-1"
describe "round-trip" $ do
it "categoryName . streamNameInCategory == id, for dash-free categories" $ hedgehog $ do
cat <- forAll genCategoryText
seg <- forAll genSegment
categoryName (streamNameInCategory (CategoryName cat) seg) === CategoryName cat