packages feed

cauldron-0.9.0.0: test/codecTests.hs

{-# LANGUAGE ApplicativeDo #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE DerivingVia #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedLists #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecursiveDo #-}
{-# LANGUAGE NoFieldSelectors #-}

module Main (main) where

import Cauldron
import Cauldron.Builder
import Control.Exception (throwIO)
import Data.Foldable qualified
import Data.Function ((&))
import Data.Functor.Identity
import Data.Monoid
import Test.Tasty
import Test.Tasty.HUnit

data Foo
  = EndFoo
  | FooToBar Bar
  deriving stock (Show)

data Bar
  = EndBar
  | BarToFoo Foo
  | BarToBaz Baz
  deriving stock (Show)

data Baz
  = EndBaz
  | BazToFoo Foo
  deriving stock (Show)

newtype Serializer a = Serializer {runSerializer :: a -> String}

makeFooSerializer :: Serializer Bar -> Serializer Foo
makeFooSerializer Serializer {runSerializer = runBar} =
  Serializer
    { runSerializer = \case
        EndFoo -> ".EndFoo"
        FooToBar bar -> ".FooToBar" ++ runBar bar
    }

makeBarSerializer :: Serializer Foo -> Serializer Baz -> Serializer Bar
makeBarSerializer Serializer {runSerializer = runFoo} Serializer {runSerializer = runBaz} =
  Serializer
    { runSerializer = \case
        EndBar -> ".EndBar"
        BarToFoo foo -> ".BarToFoo" ++ runFoo foo
        BarToBaz baz -> ".BarToBar" ++ runBaz baz
    }

makeBazSerializer :: Serializer Foo -> Serializer Baz
makeBazSerializer Serializer {runSerializer = runFoo} =
  Serializer
    { runSerializer = \case
        EndBaz -> ".EndBaz"
        BazToFoo foo -> ".BazToFoo" ++ runFoo foo
    }

cauldron :: Cauldron Identity
cauldron =
  mconcat
    [ recipe @(Serializer Foo) $ val $ wire makeFooSerializer,
      recipe @(Serializer Bar) $ val $ wire makeBarSerializer,
      recipe @(Serializer Baz) $ val $ wire makeBazSerializer
    ]

builder :: Builder Identity ()
builder = mdo
  foo <- _val_ $ makeFooSerializer <$> bar
  bar <- _val_ $ makeBarSerializer <$> foo <*> baz
  baz <- _val_ $ makeBazSerializer <$> foo
  pure ()

builder2 :: Builder Identity ()
builder2 = mdo
  _ <- _val_ $ wire makeFooSerializer
  _ <- _val_ $ wire makeBarSerializer
  _ <- _val_ $ wire makeBazSerializer
  pure ()

builder3 :: Builder Identity ()
builder3 = mdo
  foo <- add $ val_ $ wire makeFooSerializer
  _ <- add $ val_ $ makeBarSerializer <$> foo <*> baz
  baz <- add $ val_ $ wire makeBazSerializer
  pure ()

builderDupErr :: Builder Identity ()
builderDupErr = mdo
  foo1 <- _val_ $ makeFooSerializer <$> bar
  foo2 <- _val_ $ makeFooSerializer <$> bar
  bar <- _val_ $ makeBarSerializer <$> foo1 <*> baz
  baz <- _val_ $ makeBazSerializer <$> foo2
  _ <- _val_ $ makeBazSerializer <$> foo2
  pure ()

newtype Acc = Acc Int
  deriving stock (Show)
  deriving stock (Eq)
  deriving (Semigroup, Monoid) via Sum Int

newtype Bcc = Bcc Int
  deriving stock (Show)
  deriving stock (Eq)
  deriving (Semigroup, Monoid) via Sum Int

cauldronAccums1 :: Cauldron Identity
cauldronAccums1 =
  mconcat
    [ recipe @(Serializer Foo) $ val $ wire $ \sb -> (Acc 5, makeFooSerializer sb),
      recipe @(Serializer Bar) $ val $ wire $ \sf sb -> (Acc 3, makeBarSerializer sf sb),
      recipe @(Serializer Baz) $ val $ wire $ \sf -> (Acc 7, makeBazSerializer sf)
    ]

cauldronAccums2 :: Cauldron Identity
cauldronAccums2 =
  mconcat
    [ recipe @(Serializer Foo) $ val $ wire $ \(_ :: Acc) sb -> makeFooSerializer sb,
      recipe @(Serializer Bar) $ val $ wire $ \sf sb -> (Acc 3, makeBarSerializer sf sb),
      recipe @(Serializer Baz) $ val $ wire $ \sf -> (Acc 7, makeBazSerializer sf)
    ]

cauldronAccumsOops1 :: Cauldron Identity
cauldronAccumsOops1 =
  mconcat
    [ recipe @(Serializer Foo) $ val $ wire $ \(_ :: Acc) sb -> (Acc 5, makeFooSerializer sb),
      recipe @(Serializer Bar) $ val $ wire $ \sf sb -> (Acc 3, makeBarSerializer sf sb),
      recipe @(Serializer Baz) $ val $ wire $ \sf -> (Acc 7, makeBazSerializer sf)
    ]

cauldronAccumsOops2 :: Cauldron Identity
cauldronAccumsOops2 =
  mconcat
    [ recipe @(Serializer Foo) $ val $ wire $ \(_ :: Acc) sb -> (Bcc 5, makeFooSerializer sb),
      recipe @(Serializer Bar) $ val $ wire $ \(_ :: Bcc) sf sb -> (Acc 5, makeBarSerializer sf sb),
      recipe @(Serializer Baz) $ val $ wire $ \sf -> (Acc 7, makeBazSerializer sf)
    ]

tests :: TestTree
tests =
  testGroup
    "All"
    [ testCase "successful cyclic wiring" do makeBasicTest cauldron,
      testCase "successful cyclic wiring - builder" do
        c <- builder & execBuilder & either throwIO pure
        makeBasicTest c,
      testCase "successful cyclic wiring - builder 2" do
        c <- builder2 & execBuilder & either throwIO pure
        makeBasicTest c,
      testCase "successful cyclic wiring - builder 3" do
        c <- builder3 & execBuilder & either throwIO pure
        makeBasicTest c,
      testCase "should fail builder exec" do
        builderDupErr & execBuilder & \case
          Left _ -> do
            -- appendFile "/tmp/foo.txt" $ prettyDuplicateBeans err
            pure ()
          Right _ -> assertFailure "Builder should have failed with duplicate beans error",
      testCase "should fail cycle wiring" do
        Data.Foldable.for_ @[] [("forbid", forbidDepCycles), ("selfdeps", allowSelfDeps)] \(name, fire) ->
          case cook @(Serializer Foo) fire cauldron of
            Left (DependencyCycleError _) -> pure ()
            Left _ -> assertFailure $ "Unexpected error when wiring" ++ name
            Right _ -> assertFailure $ "Unexpected success when wiring" ++ name,
      testCase "cyclic wiring with accums" do
        Data.Foldable.for_ @[]
          [ ("normal", cauldronAccums1, Acc 15),
            ("someConsume", cauldronAccums2, Acc 10)
          ]
          \(name, c, expected) ->
            case cook @Acc allowDepCycles c of
              Left _err -> do
                -- putStrLn $ prettyRecipeError err
                assertFailure $ "could not wire " ++ name
              Right (Identity acc) ->
                assertEqual "experted result" expected acc,
      testCase "wiring with accums" do
        Data.Foldable.for_ @[]
          [ ("aggcyle", cauldronAccumsOops1),
            ("indirectagg", cauldronAccumsOops2)
          ]
          \(name, c) ->
            case cook @(Serializer Foo) allowDepCycles c of
              Left (DependencyCycleError _) -> assertFailure $ "We should be able to wire cycles with accs"
              Left _ -> assertFailure $ "Unexpected error when wiring" ++ name
              Right _ -> pure ()
    ]
  where
    makeBasicTest :: Cauldron Identity -> IO ()
    makeBasicTest theCauldron =
      case cook allowDepCycles theCauldron of
        Left _ -> do
          -- putStrLn $ prettyRecipeError err
          assertFailure "could not wire"
        Right (Identity (Serializer {runSerializer})) -> do
          let value = FooToBar (BarToFoo (FooToBar (BarToBaz EndBaz)))
          assertEqual "experted result" ".FooToBar.BarToFoo.FooToBar.BarToBar.EndBaz" (runSerializer value)

main :: IO ()
main = defaultMain tests