packages feed

mockcat-1.2.1.0: src/Test/MockCat/Mock.hs

{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# OPTIONS_GHC -Wno-unrecognised-pragmas #-}
{-# OPTIONS_GHC -Wno-name-shadowing #-}
{-# OPTIONS_GHC -Wno-redundant-constraints #-}

{- | Utilities for constructing verifiable stub functions.
     This module provides the core functions for creating mocks and stubs.

     = Key Functions
     * 'mock': Create a verifiable mock function (records calls).
     * 'stub': Create a pure stub function (no recording).
     * 'mockM': Create a monadic mock function (allows explicit side effects).
-}
module Test.MockCat.Mock
  ( MockBuilder
  , buildMock
  , mock
  , mockM
  , createNamedMockFnWithParams
  , CreateMockFn(..)
  , stub
  , shouldBeCalled
  , times
  , atLeast
  , atMost
  , greaterThan
  , lessThan
  , once
  , never
  , inOrder
  , inPartialOrder
  , inOrderWith
  , inPartialOrderWith
  , calledWith
  , anything
  , withArgs
  , onCase
  , cases
  , casesIO
  , label
  , Label
  , MockDispatch(..)
  , IsMockSpec
  ) where

import Control.Monad.IO.Class (MonadIO(..))
import Control.Monad.State (get, put)
import Control.Concurrent.STM (atomically, modifyTVar')
import Data.Kind (Type)
import Data.Proxy (Proxy(..))
import Data.Typeable (Typeable)
import Prelude hiding (lookup)
import Test.MockCat.Internal.Builder
import Test.MockCat.Internal.Verify (verifyExpectationDirect)
import qualified Test.MockCat.Internal.MockRegistry as MockRegistry ( register )
import Test.MockCat.Internal.Types
import Test.MockCat.Param
import Test.MockCat.Verify
import Test.MockCat.Cons (Head(..), (:>)(..))


-- | Type family to convert raw values to mock parameters.
--   Raw values (like "foo") are converted to Head :> Param a,
--   while existing Param chains remain unchanged.
type family ToMockParams p where
  ToMockParams (Param a :> rest) = Param a :> rest  -- Already a Param chain, keep as is
  ToMockParams (Param a) = Param a                  -- Single Param, keep as is
  ToMockParams (Cases a b) = Cases a b              -- Cases, keep as is
  ToMockParams (IO a) = IO a                        -- IO, keep as is
  ToMockParams (Head :> a) = Head :> a              -- Already has Head, keep as is
  ToMockParams a = Head :> Param a                  -- Raw value, wrap with Head :> Param

-- | Type class for converting values to mock parameters.
class CreateMock p where
  toParams :: p -> ToMockParams p

-- Instance for Param chains (most specific - should match first)
instance {-# OVERLAPPING #-} CreateMock (Param a :> rest) where
  toParams = id

-- Instance for single Param
instance {-# OVERLAPPING #-} CreateMock (Param a) where
  toParams = id

-- Instance for Cases
instance {-# OVERLAPPING #-} CreateMock (Cases a b) where
  toParams = id

-- Instance for IO
instance {-# OVERLAPPING #-} CreateMock (IO a) where
  toParams = id

-- Instance for Head :> Param r (constant values)
instance {-# OVERLAPPING #-} CreateMock (Head :> Param r) where
  toParams = id

-- Instance for raw values (fallback)
-- This handles raw values by wrapping them with Head :> Param
-- We need to ensure this doesn't match Param chains, Cases, IO, or Head types
instance {-# OVERLAPPABLE #-}
  ( ToMockParams b ~ (Head :> Param b)
  , Normalize b ~ Param b
  , Typeable b
  , WrapResult b
  ) => CreateMock b where
    toParams value = Head :> wrapResult value

-- | Label type for naming mock functions.
newtype Label = Label MockName

-- | Label function for naming mock functions.
--   Use it with 'mock' to provide a name for the mock function.
--   
--   @
--   f <- mock (label "mockName") $ "a" ~> "b"
--   @
label :: MockName -> Label
label = Label

-- | Type class for creating mock functions with optional name.
class CreateMockFn a where
  mockImpl :: a

-- | Type class for creating stub functions with optional name.
class CreateStubFn a where
  stubImpl :: a

-- | Type family to distinguish MockSpec from other parameters.
type family IsMockSpec p :: Bool where
  IsMockSpec (MockSpec p e) = 'True
  IsMockSpec other = 'False

-- | Internal class for dispatching named mocks based on parameter type.
--   This resolves overlapping instances between generic params and MockSpec.
--   The 'flag' parameter avoids instance overlap.
class MockDispatch (flag :: Bool) p m fn where
  mockDispatchImpl :: Label -> p -> m fn

-- Generic instance for named mocks (flag ~ 'False)
instance
  ( MonadIO m
  , CreateMock p
  , MockBuilder (ToMockParams p) fn verifyParams
  , Show verifyParams
  , EqParams verifyParams
  , Typeable verifyParams
  , Typeable fn
  , IsMockSpec p ~ 'False
  ) =>
  MockDispatch 'False p m fn
  where
  mockDispatchImpl (Label name) p = do
    let params = toParams p
    BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock (Just name) params
    liftIO $ MockRegistry.register (Just name) recorder fn

-- Specific instance for MockSpec (flag ~ 'True)
instance
  ( MonadIO m
  , MonadWithMockContext m
  , CreateMock params
  , MockBuilder (ToMockParams params) fn verifyParams
  , Show verifyParams
  , EqParams verifyParams
  , Typeable verifyParams
  , Typeable fn
  ) =>
  MockDispatch 'True (MockSpec params [Expectation verifyParams]) m fn
  where
  mockDispatchImpl (Label name) (MockSpec params exps) = do
    BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock (Just name) (toParams params)
    _ <- liftIO $ MockRegistry.register (Just name) recorder fn
    
    WithMockContext ctxRef <- askWithMockContext
    let resolved = ResolvedMock (Just name) recorder
    let verifyAction = mapM_ (verifyExpectationDirect resolved) exps
    liftIO $ atomically $ modifyTVar' ctxRef (++ [verifyAction])

    pure fn

-- | Create a mock function with verification hooks attached (unnamed version).
--   The returned function mimics a pure function (via 'unsafePerformIO') but records its calls for later verification.
--
--   > f <- mock $ "a" ~> "b"
--   > f "a" `shouldBe` "b"
--   > f `shouldBeCalled` "a"
instance {-# OVERLAPPABLE #-}
  ( MonadIO m
  , CreateMock p
  , MockBuilder (ToMockParams p) fn verifyParams
  , Show verifyParams
  , EqParams verifyParams
  , Typeable verifyParams
  , Typeable fn
  ) =>
  CreateMockFn (p -> m fn)
  where
  mockImpl p = do
    let params = toParams p
    BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock Nothing params
    liftIO $ MockRegistry.register Nothing recorder fn

-- | Create a mock function from MockSpec.
--   MockSpec can optionally contain expectations.
--   If expectations are present, they are automatically registered.
--
--   > f <- mock $ any ~> True
--   > f <- mock $ any ~> True `expects` do called once
instance {-# OVERLAPPING #-}
  ( MonadIO m
  , MonadWithMockContext m
  , CreateMock params
  , MockBuilder (ToMockParams params) fn verifyParams
  , Show verifyParams
  , EqParams verifyParams
  , Typeable verifyParams
  , Typeable fn
  ) =>
  CreateMockFn (MockSpec params [Expectation verifyParams] -> m fn)
  where
  mockImpl (MockSpec params exps) = do
    BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock Nothing (toParams params)
    _ <- liftIO $ MockRegistry.register Nothing recorder fn
    
    WithMockContext ctxRef <- askWithMockContext
    let resolved = ResolvedMock Nothing recorder
    let verifyAction = mapM_ (verifyExpectationDirect resolved) exps
    liftIO $ atomically $ modifyTVar' ctxRef (++ [verifyAction])

    pure fn

-- | Create a named mock function.
--   The name is used in error messages to help you identify which mock failed.
--
--   > f <- mock (label "MyAPI") $ "a" ~> "b"
instance {-# OVERLAPPING #-} forall p m fn.
  ( MockDispatch (IsMockSpec p) p m fn
  ) =>
  CreateMockFn (Label -> p -> m fn)
  where
  mockImpl = mockDispatchImpl @(IsMockSpec p)

-- | Create a mock function with verification hooks attached.
--
-- This function can be used in two ways:
--
-- 1. Without a name:
--    @
--    f <- mock $ "a" ~> "b"
--    @
--
-- 2. With a name (using 'label'):
--    @
--    f <- mock (label "mockName") $ "a" ~> "b"
--    @
--
-- The function creates a verifiable stub that records calls
-- and can be verified via the unified 'shouldBeCalled' API.
-- The function internally uses 'unsafePerformIO' to make the returned function
-- appear pure, but it requires 'MonadIO' for creation.
mock :: CreateMockFn a => a
mock = mockImpl

-- | Type class for creating monadic mock functions without unsafePerformIO.
class CreateMockFnM a where
  mockMImpl :: a

instance
  ( MonadIO m
  , CreateMock p
  , MockIOBuilder (ToMockParams p) fn verifyParams
  , LiftFunTo fn fnM m
  , Typeable verifyParams
  , Typeable fnM
  ) =>
  CreateMockFnM (p -> m fnM)
  where
  mockMImpl p = do
    let params = toParams p
    BuiltMock { builtMockFn = fnIO, builtMockRecorder = verifier } <- buildIO Nothing params
    let lifted = liftFunTo (Proxy :: Proxy m) fnIO
    liftIO $ MockRegistry.register Nothing verifier lifted

instance {-# OVERLAPPING #-}
  ( MonadIO m
  , CreateMock p
  , MockIOBuilder (ToMockParams p) fn verifyParams
  , LiftFunTo fn fnM m
  , Typeable verifyParams
  , Typeable fnM
  ) =>
  CreateMockFnM (Label -> p -> m fnM)
  where
  mockMImpl (Label name) p = do
    let params = toParams p
    BuiltMock { builtMockFn = fnIO, builtMockRecorder = verifier } <- buildIO (Just name) params
    let lifted = liftFunTo (Proxy :: Proxy m) fnIO
    liftIO $ MockRegistry.register (Just name) verifier lifted

mockM :: CreateMockFnM a => a
mockM = mockMImpl

-- | Internal function for TH code that already has MockBuilder constraint.
--   This avoids CreateNamedMock instance resolution issues in generated code.
createNamedMockFnWithParams ::
  ( MonadIO m
  , MockBuilder params fn verifyParams
  , Typeable verifyParams
  , Typeable fn
  ) =>
  MockName ->
  params ->
  m fn
createNamedMockFnWithParams name params = do
  BuiltMock { builtMockFn = fn, builtMockRecorder = recorder } <- buildMock (Just name) params
  liftIO $ MockRegistry.register (Just name) recorder fn


-- | Create a pure stub function without verification hooks.
--   Useful when you only need to return values and don't care about verification.
--   This is completely pure and safe.
--
--   > let f = stub $ "a" ~> "b"
instance StubBuilder params fn => CreateStubFn (params -> fn) where
  stubImpl = buildStub Nothing

-- | Create a named pure stub function without verification hooks (named version).
--
-- The provided name is used in failure messages.
-- This function creates a simple stub that returns values based on the provided
-- parameters, but does not support verification. Use 'mock' if you need
-- verification capabilities.
--
-- @
-- let f = stub (label "stubName") $ "a" ~> "b"
-- @
instance {-# OVERLAPPING #-} StubBuilder params fn => CreateStubFn (Label -> params -> fn) where
  stubImpl (Label name) = buildStub (Just name)

{- | Create a pure stub function without verification hooks.

This function can be used in two ways:

1. Without a name:
   @
   let f = stub $ "a" ~> "b"
   @

2. With a name (using 'label'):
   @
   let f = stub (label "stubName") $ "a" ~> "b"
   @

This function creates a simple stub that returns values based on the provided
parameters, but does not support verification. Use 'mock' if you need
verification capabilities.
-}
stub :: CreateStubFn a => a
stub = stubImpl

{- | Register a stub case within a 'Cases' builder. -}
onCase :: a -> Cases a ()
onCase a = Cases $ do
  st <- get
  put (st ++ [a])

{- | Define stub cases from a list of patterns. -}
cases :: [a] -> Cases a ()
cases a = Cases $ put a

{- | IO variant of 'cases'. -}
casesIO :: [a] -> Cases (IO a) ()
casesIO = Cases . (put . map pure)

class LiftFunTo funIO funM (m :: Type -> Type) | funIO m -> funM where
  liftFunTo :: Proxy m -> funIO -> funM

instance MonadIO m => LiftFunTo (IO r) (m r) m where
  liftFunTo _ = liftIO

instance LiftFunTo restIO restM m => LiftFunTo (a -> restIO) (a -> restM) m where
  liftFunTo proxy f a = liftFunTo proxy (f a)