{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE ExistentialQuantification #-}
{-# LANGUAGE OverloadedStrings #-}

-- | This module provides the core error handling types and type classes for the Railway-Oriented monad.
--
-- The module defines a hierarchy of error types, built around two complementary records:
-- 'PublicErrorInfo' for data safe to expose to end users, and 'InternalErrorInfo' for
-- diagnostic data intended only for logging and monitoring.
-- These types work together to provide a flexible, type-safe error handling system that supports
-- error accumulation and serialization to JSON.
--
-- == Quick Start
--
-- === Simple errors
--
-- Implement 'HasErrorInfo' with just 'errorPublicMessage'. Derive 'Data.Data.Data' to get an
-- automatic error 'errorCode' derived from the constructor name:
--
-- >>> {-# LANGUAGE DeriveDataTypeable #-}
-- >>>
-- >>> data UserError = NameEmpty | EmailInvalid
-- >>>   deriving (Show, Data)
-- >>>
-- >>> instance HasErrorInfo UserError where
-- >>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
-- >>>   errorPublicMessage EmailInvalid = "Email format is invalid"
--
-- Note: the error code is derived directly from the constructor name, so renaming
-- a constructor silently changes its code. Treat constructor names as part of your
-- public API contract when using this approach.
--
-- === Full control
--
-- Override any individual field method when you need custom behaviour. You can override
-- as many or as few as you need — all non-required methods have sensible defaults:
--
-- >>> instance HasErrorInfo UserError where
-- >>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
-- >>>   errorPublicMessage EmailInvalid = "Email format is invalid"
-- >>>
-- >>>   errorCode NameEmpty    = "UserNameEmpty"
-- >>>   errorCode EmailInvalid = "UserEmailInvalid"
-- >>>
-- >>>   -- Override internal fields only when you have extra diagnostic context:
-- >>>   errorSeverity EmailInvalid = Critical
-- >>>   errorInternalMessage NameEmpty = Just "name field was empty string after trimming"
--
-- === Running your Railway
--
-- 1. Throw errors with 'Monad.Rail.Types.throwError':
--
-- >>> throwError NameEmpty
--
-- 2. Run and handle the result:
--
-- >>> result <- runRail myComputation
-- >>> case result of
-- >>>   Right value -> putStrLn "Success!"
-- >>>   Left errors -> print errors  -- Automatically serializes to JSON
module Monad.Rail.Error
  ( ErrorSeverity (..),
    PublicErrorInfo (..),
    InternalErrorInfo (..),
    SomeErrorDetails (..),
    HasErrorInfo (..),
    publicErrorInfo,
    internalErrorInfo,
    SomeError (..),
    UnhandledException (..),
    Failure (..),
  )
where

import qualified Control.Exception as E
import Data.Aeson (ToJSON (..), object, (.=))
import Data.Data (Data, toConstr)
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import qualified Data.Text as T
import Data.Typeable (Typeable)
import GHC.Stack (CallStack, prettyCallStack)

-- | Represents the severity level of an application error.
--
-- Severity levels are used to categorize errors by their importance and urgency.
-- This information is useful for logging, monitoring, and error handling strategies.
data ErrorSeverity
  = -- | Indicates a standard error that occurred during the execution of a Railway.
    -- Standard errors are recoverable and do not require immediate attention.
    Error
  | -- | Indicates a critical error that occurred during the execution of a Railway.
    -- Critical errors may require immediate attention and indicate serious problems
    -- that could affect system stability.
    Critical
  deriving (ErrorSeverity -> ErrorSeverity -> Bool
(ErrorSeverity -> ErrorSeverity -> Bool)
-> (ErrorSeverity -> ErrorSeverity -> Bool) -> Eq ErrorSeverity
forall a. (a -> a -> Bool) -> (a -> a -> Bool) -> Eq a
$c== :: ErrorSeverity -> ErrorSeverity -> Bool
== :: ErrorSeverity -> ErrorSeverity -> Bool
$c/= :: ErrorSeverity -> ErrorSeverity -> Bool
/= :: ErrorSeverity -> ErrorSeverity -> Bool
Eq, Int -> ErrorSeverity -> ShowS
[ErrorSeverity] -> ShowS
ErrorSeverity -> String
(Int -> ErrorSeverity -> ShowS)
-> (ErrorSeverity -> String)
-> ([ErrorSeverity] -> ShowS)
-> Show ErrorSeverity
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> ErrorSeverity -> ShowS
showsPrec :: Int -> ErrorSeverity -> ShowS
$cshow :: ErrorSeverity -> String
show :: ErrorSeverity -> String
$cshowList :: [ErrorSeverity] -> ShowS
showList :: [ErrorSeverity] -> ShowS
Show, Eq ErrorSeverity
Eq ErrorSeverity =>
(ErrorSeverity -> ErrorSeverity -> Ordering)
-> (ErrorSeverity -> ErrorSeverity -> Bool)
-> (ErrorSeverity -> ErrorSeverity -> Bool)
-> (ErrorSeverity -> ErrorSeverity -> Bool)
-> (ErrorSeverity -> ErrorSeverity -> Bool)
-> (ErrorSeverity -> ErrorSeverity -> ErrorSeverity)
-> (ErrorSeverity -> ErrorSeverity -> ErrorSeverity)
-> Ord ErrorSeverity
ErrorSeverity -> ErrorSeverity -> Bool
ErrorSeverity -> ErrorSeverity -> Ordering
ErrorSeverity -> ErrorSeverity -> ErrorSeverity
forall a.
Eq a =>
(a -> a -> Ordering)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> Bool)
-> (a -> a -> a)
-> (a -> a -> a)
-> Ord a
$ccompare :: ErrorSeverity -> ErrorSeverity -> Ordering
compare :: ErrorSeverity -> ErrorSeverity -> Ordering
$c< :: ErrorSeverity -> ErrorSeverity -> Bool
< :: ErrorSeverity -> ErrorSeverity -> Bool
$c<= :: ErrorSeverity -> ErrorSeverity -> Bool
<= :: ErrorSeverity -> ErrorSeverity -> Bool
$c> :: ErrorSeverity -> ErrorSeverity -> Bool
> :: ErrorSeverity -> ErrorSeverity -> Bool
$c>= :: ErrorSeverity -> ErrorSeverity -> Bool
>= :: ErrorSeverity -> ErrorSeverity -> Bool
$cmax :: ErrorSeverity -> ErrorSeverity -> ErrorSeverity
max :: ErrorSeverity -> ErrorSeverity -> ErrorSeverity
$cmin :: ErrorSeverity -> ErrorSeverity -> ErrorSeverity
min :: ErrorSeverity -> ErrorSeverity -> ErrorSeverity
Ord, Int -> ErrorSeverity
ErrorSeverity -> Int
ErrorSeverity -> [ErrorSeverity]
ErrorSeverity -> ErrorSeverity
ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
ErrorSeverity -> ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
(ErrorSeverity -> ErrorSeverity)
-> (ErrorSeverity -> ErrorSeverity)
-> (Int -> ErrorSeverity)
-> (ErrorSeverity -> Int)
-> (ErrorSeverity -> [ErrorSeverity])
-> (ErrorSeverity -> ErrorSeverity -> [ErrorSeverity])
-> (ErrorSeverity -> ErrorSeverity -> [ErrorSeverity])
-> (ErrorSeverity
    -> ErrorSeverity -> ErrorSeverity -> [ErrorSeverity])
-> Enum ErrorSeverity
forall a.
(a -> a)
-> (a -> a)
-> (Int -> a)
-> (a -> Int)
-> (a -> [a])
-> (a -> a -> [a])
-> (a -> a -> [a])
-> (a -> a -> a -> [a])
-> Enum a
$csucc :: ErrorSeverity -> ErrorSeverity
succ :: ErrorSeverity -> ErrorSeverity
$cpred :: ErrorSeverity -> ErrorSeverity
pred :: ErrorSeverity -> ErrorSeverity
$ctoEnum :: Int -> ErrorSeverity
toEnum :: Int -> ErrorSeverity
$cfromEnum :: ErrorSeverity -> Int
fromEnum :: ErrorSeverity -> Int
$cenumFrom :: ErrorSeverity -> [ErrorSeverity]
enumFrom :: ErrorSeverity -> [ErrorSeverity]
$cenumFromThen :: ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
enumFromThen :: ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
$cenumFromTo :: ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
enumFromTo :: ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
$cenumFromThenTo :: ErrorSeverity -> ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
enumFromThenTo :: ErrorSeverity -> ErrorSeverity -> ErrorSeverity -> [ErrorSeverity]
Enum)

instance ToJSON ErrorSeverity where
  toJSON :: ErrorSeverity -> Value
toJSON ErrorSeverity
Error = Value
"Error"
  toJSON ErrorSeverity
Critical = Value
"Critical"

-- | An existential wrapper that can hold any value with 'ToJSON', 'Show', and 'Typeable'
-- constraints.
--
-- This allows 'errorDetails' and 'PublicErrorInfo' to carry structured detail values
-- of any type, deferring JSON serialization until needed while preserving the ability
-- to recover the concrete type via 'Data.Typeable.cast'.
--
-- Example:
--
-- >>> SomeErrorDetails ("usr_123" :: Text)
-- >>> SomeErrorDetails (42 :: Int)
-- >>> SomeErrorDetails (object ["field" .= ("email" :: Text)])
data SomeErrorDetails = forall a. (ToJSON a, Show a, Typeable a) => SomeErrorDetails a

instance Show SomeErrorDetails where
  show :: SomeErrorDetails -> String
show (SomeErrorDetails a
a) = a -> String
forall a. Show a => a -> String
show a
a

instance ToJSON SomeErrorDetails where
  toJSON :: SomeErrorDetails -> Value
toJSON (SomeErrorDetails a
a) = a -> Value
forall a. ToJSON a => a -> Value
toJSON a
a

-- | Contains the public-facing information about an application error.
--
-- All fields in this record are safe to expose to end users and will be included
-- in JSON serialization. This record is the only part of an error that flows into
-- API responses.
--
-- See 'InternalErrorInfo' for the complementary record holding sensitive diagnostic data.
data PublicErrorInfo = PublicErrorInfo
  { -- | A human-readable message for end users.
    -- This message should be clear, helpful, and safe to display to clients.
    -- It must not contain sensitive information such as database connection details,
    -- internal IP addresses, or stack traces.
    --
    -- Example: @\"Invalid email format\"@
    PublicErrorInfo -> Text
publicMessage :: Text,
    -- | A machine-readable error code that categorizes the type of error.
    --
    -- Error codes are useful for:
    --
    -- * Logging and monitoring systems
    -- * Creating error statistics and metrics
    -- * Routing errors to appropriate handlers
    -- * Building error catalogs
    --
    -- Example codes: @\"UserNameEmpty\"@, @\"DbConnectionFailed\"@
    PublicErrorInfo -> Text
code :: Text,
    -- | Optional context details associated with the error.
    --
    -- This field can hold any value with 'ToJSON', 'Show', and 'Typeable'
    -- constraints, providing additional context about the error that is safe
    -- to share with the caller. For example:
    --
    -- * Affected resource identifiers
    -- * Custom business logic data
    --
    -- The existential 'SomeErrorDetails' wrapper preserves the original value type,
    -- allowing downstream code to recover the concrete type via 'Data.Typeable.cast'
    -- while still supporting JSON serialization.
    --
    -- Example: @Just (SomeErrorDetails (object [\"resourceId\" .= (\"usr_123\" :: Text)]))@
    PublicErrorInfo -> Maybe SomeErrorDetails
details :: Maybe SomeErrorDetails
  }
  deriving (Int -> PublicErrorInfo -> ShowS
[PublicErrorInfo] -> ShowS
PublicErrorInfo -> String
(Int -> PublicErrorInfo -> ShowS)
-> (PublicErrorInfo -> String)
-> ([PublicErrorInfo] -> ShowS)
-> Show PublicErrorInfo
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> PublicErrorInfo -> ShowS
showsPrec :: Int -> PublicErrorInfo -> ShowS
$cshow :: PublicErrorInfo -> String
show :: PublicErrorInfo -> String
$cshowList :: [PublicErrorInfo] -> ShowS
showList :: [PublicErrorInfo] -> ShowS
Show)

instance ToJSON PublicErrorInfo where
  toJSON :: PublicErrorInfo -> Value
toJSON PublicErrorInfo
pub =
    [Pair] -> Value
object ([Pair] -> Value) -> [Pair] -> Value
forall a b. (a -> b) -> a -> b
$
      [Maybe Pair] -> [Pair]
forall a. [Maybe a] -> [a]
catMaybes
        [ Pair -> Maybe Pair
forall a. a -> Maybe a
Just (Key
"message" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= PublicErrorInfo -> Text
publicMessage PublicErrorInfo
pub),
          Pair -> Maybe Pair
forall a. a -> Maybe a
Just (Key
"code" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= PublicErrorInfo -> Text
code PublicErrorInfo
pub),
          (Key
"details" Key -> SomeErrorDetails -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (SomeErrorDetails -> Pair) -> Maybe SomeErrorDetails -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> PublicErrorInfo -> Maybe SomeErrorDetails
details PublicErrorInfo
pub
        ]

-- | Contains internal diagnostic information about an application error.
--
-- This record implements 'ToJSON' so it can be serialized for server-side logging
-- and monitoring. However, 'SomeError'\'s 'ToJSON' instance delegates only to
-- 'PublicErrorInfo', so none of these fields ever appear in public API responses.
--
-- See 'PublicErrorInfo' for the complementary record holding user-facing data.
data InternalErrorInfo = InternalErrorInfo
  { -- | An optional technical message for administrators and logs.
    -- This message can contain sensitive infrastructure details, stack traces,
    -- database information, and other diagnostic data.
    --
    -- 'Nothing' means the public message is sufficient for diagnostic purposes.
    --
    -- Example: @Just \"Failed to connect to replica database at 192.168.1.5:5432\"@
    InternalErrorInfo -> Maybe Text
internalMessage :: Maybe Text,
    -- | The severity level of the error, indicating how critical it is.
    -- See 'ErrorSeverity' for available levels.
    InternalErrorInfo -> ErrorSeverity
severity :: ErrorSeverity,
    -- | An optional runtime exception associated with the error, if any.
    --
    -- This field is useful for capturing the underlying exception that caused
    -- the error, such as a database connection timeout or file I\/O error.
    -- It is intended for logging and debugging purposes only.
    InternalErrorInfo -> Maybe SomeException
exception :: Maybe E.SomeException,
    -- | The Haskell call stack at the point the error was constructed.
    --
    -- Populate this field by adding a 'GHC.Stack.HasCallStack' constraint to the
    -- function that builds the error and passing 'GHC.Stack.callStack'. Serialized
    -- as a human-readable string via 'GHC.Stack.prettyCallStack'.
    --
    -- Note: the field name @callStack@ shadows the 'GHC.Stack.callStack' implicit-parameter
    -- accessor when both are in scope. Qualify the latter as @GHC.Stack.callStack@ to avoid
    -- ambiguity.
    InternalErrorInfo -> Maybe CallStack
callStack :: Maybe CallStack
  }
  deriving (Int -> InternalErrorInfo -> ShowS
[InternalErrorInfo] -> ShowS
InternalErrorInfo -> String
(Int -> InternalErrorInfo -> ShowS)
-> (InternalErrorInfo -> String)
-> ([InternalErrorInfo] -> ShowS)
-> Show InternalErrorInfo
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> InternalErrorInfo -> ShowS
showsPrec :: Int -> InternalErrorInfo -> ShowS
$cshow :: InternalErrorInfo -> String
show :: InternalErrorInfo -> String
$cshowList :: [InternalErrorInfo] -> ShowS
showList :: [InternalErrorInfo] -> ShowS
Show)

instance ToJSON InternalErrorInfo where
  toJSON :: InternalErrorInfo -> Value
toJSON InternalErrorInfo
internal =
    [Pair] -> Value
object ([Pair] -> Value) -> [Pair] -> Value
forall a b. (a -> b) -> a -> b
$
      [Maybe Pair] -> [Pair]
forall a. [Maybe a] -> [a]
catMaybes
        [ Pair -> Maybe Pair
forall a. a -> Maybe a
Just (Key
"severity" Key -> ErrorSeverity -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.= InternalErrorInfo -> ErrorSeverity
severity InternalErrorInfo
internal),
          (Key
"message" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Text -> Pair) -> Maybe Text -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> InternalErrorInfo -> Maybe Text
internalMessage InternalErrorInfo
internal,
          (Key
"exception" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Text -> Pair) -> (SomeException -> Text) -> SomeException -> Pair
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
T.pack (String -> Text)
-> (SomeException -> String) -> SomeException -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. SomeException -> String
forall e. Exception e => e -> String
E.displayException (SomeException -> Pair) -> Maybe SomeException -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> InternalErrorInfo -> Maybe SomeException
exception InternalErrorInfo
internal,
          (Key
"callStack" Key -> Text -> Pair
forall v. ToJSON v => Key -> v -> Pair
forall e kv v. (KeyValue e kv, ToJSON v) => Key -> v -> kv
.=) (Text -> Pair) -> (CallStack -> Text) -> CallStack -> Pair
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Text
T.pack (String -> Text) -> (CallStack -> String) -> CallStack -> Text
forall b c a. (b -> c) -> (a -> b) -> a -> c
. CallStack -> String
prettyCallStack (CallStack -> Pair) -> Maybe CallStack -> Maybe Pair
forall (f :: * -> *) a b. Functor f => (a -> b) -> f a -> f b
<$> InternalErrorInfo -> Maybe CallStack
callStack InternalErrorInfo
internal
        ]

-- | A type class for converting custom error types into serializable error information.
--
-- Implement 'errorPublicMessage' — the only required method — to integrate any error type
-- with the Railway error system. All other methods have defaults and can be overridden
-- individually as needed.
--
-- Use 'publicErrorInfo' and 'internalErrorInfo' to assemble the corresponding records
-- from an instance.
--
-- == Simple errors: implement 'errorPublicMessage' only
--
-- Derive 'Data.Data.Data' and implement 'errorPublicMessage'. The 'errorCode' default derives
-- the error code from the constructor name via 'Data.Data.toConstr':
--
-- >>> {-# LANGUAGE DeriveDataTypeable #-}
-- >>>
-- >>> data UserError = NameEmpty | EmailInvalid
-- >>>   deriving (Show, Data)
-- >>>
-- >>> instance HasErrorInfo UserError where
-- >>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
-- >>>   errorPublicMessage EmailInvalid = "Email format is invalid"
-- >>> -- publicErrorInfo NameEmpty
-- >>> --   = PublicErrorInfo { publicMessage = "Name cannot be empty"
-- >>> --                     , code          = "NameEmpty"
-- >>> --                     , details       = Nothing }
--
-- == Full control: override any field method
--
-- Override individual methods when you need custom codes, details, or internal context.
-- Methods you do not override keep their defaults:
--
-- >>> instance HasErrorInfo UserError where
-- >>>   errorPublicMessage NameEmpty    = "Name cannot be empty"
-- >>>   errorPublicMessage EmailInvalid = "Email format is invalid"
-- >>>
-- >>>   errorCode NameEmpty    = "UserNameEmpty"
-- >>>   errorCode EmailInvalid = "UserEmailInvalid"
-- >>>
-- >>>   errorSeverity _               = Critical
-- >>>   errorInternalMessage NameEmpty = Just "name field was empty string after trimming"
--
-- == Note on 'errorCallStack' and 'GHC.Stack.callStack'
--
-- The assembled 'InternalErrorInfo' record has a field named @callStack@. If both
-- 'InternalErrorInfo' and 'GHC.Stack' are imported unqualified, the name @callStack@
-- may be ambiguous. Qualify 'GHC.Stack.callStack' to avoid ambiguity.
class HasErrorInfo e where
  -- | A human-readable message safe to display to end users. This is the only required method.
  errorPublicMessage :: e -> Text

  -- | A machine-readable error code. Defaults to the constructor name via 'Data.Data.toConstr'.
  --
  -- Override when you need a code that differs from the constructor name.
  --
  -- Example: @errorCode NameEmpty = \"UserNameEmpty\"@
  errorCode :: e -> Text
  default errorCode :: (Data e) => e -> Text
  errorCode e
e = String -> Text
T.pack (Constr -> String
forall a. Show a => a -> String
show (e -> Constr
forall a. Data a => a -> Constr
toConstr e
e))

  -- | Optional details safe to share with callers. Defaults to 'Nothing'.
  --
  -- Wrap your value with 'SomeErrorDetails' to store it:
  --
  -- >>> errorDetails MyError = Just (SomeErrorDetails (object ["field" .= ("email" :: Text)]))
  errorDetails :: e -> Maybe SomeErrorDetails
  errorDetails e
_ = Maybe SomeErrorDetails
forall a. Maybe a
Nothing

  -- | Severity level of the error. Defaults to 'Error'.
  errorSeverity :: e -> ErrorSeverity
  errorSeverity e
_ = ErrorSeverity
Error

  -- | An optional technical message for logs, safe to contain sensitive details.
  -- Defaults to 'Nothing'.
  errorInternalMessage :: e -> Maybe Text
  errorInternalMessage e
_ = Maybe Text
forall a. Maybe a
Nothing

  -- | An optional underlying runtime exception. Defaults to 'Nothing'.
  errorException :: e -> Maybe E.SomeException
  errorException e
_ = Maybe SomeException
forall a. Maybe a
Nothing

  -- | The Haskell call stack at the point the error was constructed. Defaults to 'Nothing'.
  --
  -- Populate by adding 'GHC.Stack.HasCallStack' to the function that builds the error
  -- and passing @Just GHC.Stack.callStack@.
  errorCallStack :: e -> Maybe CallStack
  errorCallStack e
_ = Maybe CallStack
forall a. Maybe a
Nothing

-- | Assembles a 'PublicErrorInfo' from a 'HasErrorInfo' instance.
--
-- This is the canonical way to obtain the public-facing error record for logging
-- or serialization. The result contains only data safe to expose to end users.
publicErrorInfo :: (HasErrorInfo e) => e -> PublicErrorInfo
publicErrorInfo :: forall e. HasErrorInfo e => e -> PublicErrorInfo
publicErrorInfo e
e =
  PublicErrorInfo
    { publicMessage :: Text
publicMessage = e -> Text
forall e. HasErrorInfo e => e -> Text
errorPublicMessage e
e,
      code :: Text
code = e -> Text
forall e. HasErrorInfo e => e -> Text
errorCode e
e,
      details :: Maybe SomeErrorDetails
details = e -> Maybe SomeErrorDetails
forall e. HasErrorInfo e => e -> Maybe SomeErrorDetails
errorDetails e
e
    }

-- | Assembles an 'InternalErrorInfo' from a 'HasErrorInfo' instance.
--
-- This is the canonical way to obtain the internal diagnostic record for
-- server-side logging and monitoring. Never include this in API responses.
internalErrorInfo :: (HasErrorInfo e) => e -> InternalErrorInfo
internalErrorInfo :: forall e. HasErrorInfo e => e -> InternalErrorInfo
internalErrorInfo e
e =
  InternalErrorInfo
    { internalMessage :: Maybe Text
internalMessage = e -> Maybe Text
forall e. HasErrorInfo e => e -> Maybe Text
errorInternalMessage e
e,
      severity :: ErrorSeverity
severity = e -> ErrorSeverity
forall e. HasErrorInfo e => e -> ErrorSeverity
errorSeverity e
e,
      exception :: Maybe SomeException
exception = e -> Maybe SomeException
forall e. HasErrorInfo e => e -> Maybe SomeException
errorException e
e,
      callStack :: Maybe CallStack
callStack = e -> Maybe CallStack
forall e. HasErrorInfo e => e -> Maybe CallStack
errorCallStack e
e
    }

-- | Wrapper for unhandled exceptions that can be used as an error type.
--
-- This type captures a 'E.SomeException' thrown in 'IO' and makes it
-- compatible with the Railway error system via its 'HasErrorInfo' instance.
-- It is the error type produced by 'Monad.Rail.Types.tryRail' when an IO action throws.
--
-- The 'publicMessage' of the assembled 'PublicErrorInfo' is intentionally generic so
-- that internal details are never accidentally exposed to end users. The original
-- exception is stored in the 'exception' field of 'InternalErrorInfo' for logging
-- and debugging.
--
-- 'unhandledCode' lets you assign a domain-specific error code when you catch
-- exceptions manually, rather than relying on the default @\"UnhandledException\"@:
--
-- >>> import qualified Control.Exception as E
-- >>>
-- >>> safeQuery :: Rail Row
-- >>> safeQuery = do
-- >>>   result <- liftIO $ E.try runQuery
-- >>>   case result of
-- >>>     Right row -> pure row
-- >>>     Left ex   -> throwUnhandledExceptionWithCode "DbQueryFailed" ex
--
-- Or use 'Monad.Rail.Types.throwUnhandledException' when the default code is sufficient:
--
-- >>> safeQuery :: Rail Row
-- >>> safeQuery = do
-- >>>   result <- liftIO $ E.try runQuery
-- >>>   case result of
-- >>>     Right row -> pure row
-- >>>     Left ex   -> throwUnhandledException ex
--
-- When using 'Monad.Rail.Types.tryRail', the code defaults to @\"UnhandledException\"@ and the
-- call stack is captured automatically at the call site.
data UnhandledException = UnhandledException
  { -- | Optional machine-readable error code exposed in 'PublicErrorInfo'.
    -- When 'Nothing', defaults to @\"UnhandledException\"@.
    -- Set via 'Monad.Rail.Types.throwUnhandledExceptionWithCode' or 'Monad.Rail.Types.tryRailWithCode'.
    UnhandledException -> Maybe Text
unhandledCode :: Maybe Text,
    -- | The original exception.
    UnhandledException -> SomeException
unhandledException :: E.SomeException,
    -- | Optional Haskell call stack at the catch site.
    -- Populated automatically by 'Monad.Rail.Types.tryRail' via 'GHC.Stack.HasCallStack'.
    UnhandledException -> Maybe CallStack
unhandledCallStack :: Maybe CallStack,
    -- | Optional public message override.
    --
    -- When 'Nothing', falls back to @\"An unexpected error occurred\"@. Set this via
    -- 'Monad.Rail.Types.tryRailWithError' to surface a domain-specific message.
    UnhandledException -> Maybe Text
unhandledMessage :: Maybe Text
  }

instance Show UnhandledException where
  show :: UnhandledException -> String
show UnhandledException
ue = String
"Unhandled exception: " String -> ShowS
forall a. Semigroup a => a -> a -> a
<> SomeException -> String
forall e. Exception e => e -> String
E.displayException (UnhandledException -> SomeException
unhandledException UnhandledException
ue)

instance HasErrorInfo UnhandledException where
  errorPublicMessage :: UnhandledException -> Text
errorPublicMessage UnhandledException
ue = Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe Text
"An unexpected error occurred" (UnhandledException -> Maybe Text
unhandledMessage UnhandledException
ue)
  errorCode :: UnhandledException -> Text
errorCode UnhandledException
ue = Text -> Maybe Text -> Text
forall a. a -> Maybe a -> a
fromMaybe Text
"UnhandledException" (UnhandledException -> Maybe Text
unhandledCode UnhandledException
ue)
  errorSeverity :: UnhandledException -> ErrorSeverity
errorSeverity UnhandledException
_ = ErrorSeverity
Critical
  errorInternalMessage :: UnhandledException -> Maybe Text
errorInternalMessage UnhandledException
ue = Text -> Maybe Text
forall a. a -> Maybe a
Just (String -> Text
T.pack (SomeException -> String
forall e. Exception e => e -> String
E.displayException (UnhandledException -> SomeException
unhandledException UnhandledException
ue)))
  errorException :: UnhandledException -> Maybe SomeException
errorException UnhandledException
ue = SomeException -> Maybe SomeException
forall a. a -> Maybe a
Just (UnhandledException -> SomeException
unhandledException UnhandledException
ue)
  errorCallStack :: UnhandledException -> Maybe CallStack
errorCallStack UnhandledException
ue = UnhandledException -> Maybe CallStack
unhandledCallStack UnhandledException
ue

-- | A wrapper type that can hold any application error implementing 'HasErrorInfo'.
--
-- This existential type allows you to combine errors of different types in the same
-- Railway computation. It uses existential quantification to hide the concrete error type
-- while preserving the ability to extract error information via the 'HasErrorInfo'
-- interface. The 'Typeable' constraint allows recovery of the original error type
-- via 'Data.Typeable.cast' when needed.
--
-- This is particularly useful when you have multiple error sources (e.g., validation errors,
-- database errors, network errors) and want to combine them in a single computation.
--
-- == JSON serialization
--
-- 'SomeError'\'s 'ToJSON' instance serializes __only__ the 'PublicErrorInfo' fields.
-- 'InternalErrorInfo' is intentionally excluded so that sensitive diagnostic data
-- (internal messages, call stacks, exceptions) is never accidentally exposed in API responses.
-- Use 'internalErrorInfo' directly if you need to serialize that data for server-side logging.
--
-- == Example
--
-- >>> data UserError = NameEmpty
-- >>> data DatabaseError = ConnectionFailed
-- >>>
-- >>> instance HasErrorInfo UserError where { ... }
-- >>> instance HasErrorInfo DatabaseError where { ... }
-- >>>
-- >>> validate :: Rail ()
-- >>> validate = do
-- >>>   throwError NameEmpty         -- User error
-- >>>   throwError ConnectionFailed -- Database error
data SomeError
  = forall e.
    (HasErrorInfo e, Show e, Typeable e) =>
    SomeError e

instance ToJSON SomeError where
  toJSON :: SomeError -> Value
toJSON (SomeError e
e) = PublicErrorInfo -> Value
forall a. ToJSON a => a -> Value
toJSON (e -> PublicErrorInfo
forall e. HasErrorInfo e => e -> PublicErrorInfo
publicErrorInfo e
e)

instance Show SomeError where
  show :: SomeError -> String
show (SomeError e
e) = e -> String
forall a. Show a => a -> String
show e
e

instance HasErrorInfo SomeError where
  errorPublicMessage :: SomeError -> Text
errorPublicMessage (SomeError e
e) = e -> Text
forall e. HasErrorInfo e => e -> Text
errorPublicMessage e
e
  errorCode :: SomeError -> Text
errorCode (SomeError e
e) = e -> Text
forall e. HasErrorInfo e => e -> Text
errorCode e
e
  errorDetails :: SomeError -> Maybe SomeErrorDetails
errorDetails (SomeError e
e) = e -> Maybe SomeErrorDetails
forall e. HasErrorInfo e => e -> Maybe SomeErrorDetails
errorDetails e
e
  errorSeverity :: SomeError -> ErrorSeverity
errorSeverity (SomeError e
e) = e -> ErrorSeverity
forall e. HasErrorInfo e => e -> ErrorSeverity
errorSeverity e
e
  errorInternalMessage :: SomeError -> Maybe Text
errorInternalMessage (SomeError e
e) = e -> Maybe Text
forall e. HasErrorInfo e => e -> Maybe Text
errorInternalMessage e
e
  errorException :: SomeError -> Maybe SomeException
errorException (SomeError e
e) = e -> Maybe SomeException
forall e. HasErrorInfo e => e -> Maybe SomeException
errorException e
e
  errorCallStack :: SomeError -> Maybe CallStack
errorCallStack (SomeError e
e) = e -> Maybe CallStack
forall e. HasErrorInfo e => e -> Maybe CallStack
errorCallStack e
e

-- | Represents a collection of one or more application errors accumulated during a Railway computation.
--
-- This type is used as the error type in 'Monad.Rail.Types.RailT' computations. It guarantees that
-- at least one error is always present (using 'NonEmpty'), which is essential for
-- the Railway-Oriented programming model where a failure state must contain error information.
--
-- Multiple errors are accumulated when using the 'Monad.Rail.Types.<!>' operator for validations,
-- allowing you to collect all validation errors before reporting failure.
--
-- == Combining Errors
--
-- Use the 'Semigroup' instance to combine multiple 'Failure' values:
--
-- >>> let err1 = Failure (SomeError e1 :| [])
-- >>> let err2 = Failure (SomeError e2 :| [])
-- >>> let combined = err1 <> err2  -- Contains both errors
newtype Failure = Failure
  { -- | Extracts the non-empty list of errors.
    --
    -- Use this function to access the individual errors for logging, reporting,
    -- or further processing.
    Failure -> NonEmpty SomeError
getErrors :: NonEmpty SomeError
  }
  deriving (Int -> Failure -> ShowS
[Failure] -> ShowS
Failure -> String
(Int -> Failure -> ShowS)
-> (Failure -> String) -> ([Failure] -> ShowS) -> Show Failure
forall a.
(Int -> a -> ShowS) -> (a -> String) -> ([a] -> ShowS) -> Show a
$cshowsPrec :: Int -> Failure -> ShowS
showsPrec :: Int -> Failure -> ShowS
$cshow :: Failure -> String
show :: Failure -> String
$cshowList :: [Failure] -> ShowS
showList :: [Failure] -> ShowS
Show)

instance Semigroup Failure where
  (Failure NonEmpty SomeError
e1) <> :: Failure -> Failure -> Failure
<> (Failure NonEmpty SomeError
e2) = NonEmpty SomeError -> Failure
Failure (NonEmpty SomeError
e1 NonEmpty SomeError -> NonEmpty SomeError -> NonEmpty SomeError
forall a. Semigroup a => a -> a -> a
<> NonEmpty SomeError
e2)

instance ToJSON Failure where
  toJSON :: Failure -> Value
toJSON = NonEmpty SomeError -> Value
forall a. ToJSON a => a -> Value
toJSON (NonEmpty SomeError -> Value)
-> (Failure -> NonEmpty SomeError) -> Failure -> Value
forall b c a. (b -> c) -> (a -> b) -> a -> c
. Failure -> NonEmpty SomeError
getErrors