orville-postgresql-1.0.0.0: src/Orville/PostgreSQL/Marshall/MarshallError.hs
{- |
Copyright : Flipstone Technology Partners 2023
License : MIT
Stability : Stable
@since 1.0.0.0
-}
module Orville.PostgreSQL.Marshall.MarshallError
( MarshallError (MarshallError, marshallErrorDetailLevel, marshallErrorRowIdentifier, marshallErrorDetails)
, renderMarshallError
, MarshallErrorDetails (DecodingError, MissingColumnError)
, renderMarshallErrorDetails
, DecodingErrorDetails (DecodingErrorDetails, decodingErrorValues, decodingErrorMessage)
, renderDecodingErrorDetails
, MissingColumnErrorDetails (MissingColumnErrorDetails, missingColumnName, actualColumnNames)
, renderMissingColumnErrorDetails
)
where
import Control.Exception (Exception)
import qualified Data.ByteString.Char8 as B8
import qualified Data.List as List
import qualified Data.Set as Set
import Orville.PostgreSQL.ErrorDetailLevel (ErrorDetailLevel, redactErrorMessage, redactIdentifierValue, redactNonIdentifierValue, redactSchemaName)
import qualified Orville.PostgreSQL.Raw.PgTextFormatValue as PgTextFormatValue
import qualified Orville.PostgreSQL.Raw.SqlValue as SqlValue
{- |
A 'MarshallError' may be returned from
'Orville.PostgreSQL.Marshall.marshallResultFromSql' when a row being decoded
from the database doesn't meet the expectations of the
'Orville.PostgreSQL.Marshall.SqlMarshaller' that is decoding it.
@since 1.0.0.0
-}
data MarshallError = MarshallError
{ marshallErrorDetailLevel :: ErrorDetailLevel
-- ^ The level of detail that will be used to render this error as a
-- message if 'show' is called.
, marshallErrorRowIdentifier :: [(B8.ByteString, SqlValue.SqlValue)]
-- ^ The identifier of the row that caused the error. This is a list
-- of pairs of column name and value in their raw form from the database
-- to avoid further possible decoding errors when reading the values.
, marshallErrorDetails :: MarshallErrorDetails
-- ^ The detailed information about the error that occurred during
-- decoding.
}
instance Show MarshallError where
show decodingError =
renderMarshallError
(marshallErrorDetailLevel decodingError)
decodingError
instance Exception MarshallError
{- |
Renders a 'MarshallError' to a string using the specified 'ErrorDetailLevel'.
This ignores any 'ErrorDetailLevel' that was captured by default from
the Orville context and uses the specified level of detail instead.
You may want to use this function to render certain errors with a higher
level of detail than you consider safe for (for example) your application
logs while using a lower default error detail level with the 'Show' instance
of 'MarshallError' in case an exception is handled in a more visible section
of code that returns information more publicly (e.g. a request handler for a
public endpoint).
@since 1.0.0.0
-}
renderMarshallError :: ErrorDetailLevel -> MarshallError -> String
renderMarshallError detailLevel marshallError =
let
presentableRowId =
map
(presentSqlColumnValue detailLevel redactIdentifierValue)
(marshallErrorRowIdentifier marshallError)
in
concat
[ "Unable to decode row with identifier ["
, List.intercalate ", " presentableRowId
, "]: "
, renderMarshallErrorDetails detailLevel (marshallErrorDetails marshallError)
]
{- |
A internal helper to present a redacted column name and sql value in an error
message. The redacter function is passed as an argument here so that this
function can be used to present either ID values or general values as
required by the context of the caller.
@since 1.0.0.0
-}
presentSqlColumnValue ::
ErrorDetailLevel ->
(ErrorDetailLevel -> String -> String) ->
(B8.ByteString, SqlValue.SqlValue) ->
String
presentSqlColumnValue detailLevel redacter (columnName, sqlValue) =
let
sqlValueString =
redacter detailLevel $
case SqlValue.toPgValue sqlValue of
Nothing ->
"NULL"
Just pgValue ->
B8.unpack . PgTextFormatValue.toByteString $ pgValue
in
redactSchemaName detailLevel (B8.unpack columnName)
<> " = "
<> sqlValueString
{- |
A 'MarshallErrorDetails' may be returned from
'Orville.PostgreSQL.Marshall.marshallResultFromSql' if the result set being
decoded from the database doesn't meet the expectations of the
'Orville.PostgreSQL.Marshall.SqlMarshaller' that is decoding it.
@since 1.0.0.0
-}
data MarshallErrorDetails
= -- | Indicates that one or more values in a column could not be decoded,
-- either individually or as a group.
DecodingError DecodingErrorDetails
| -- | Indicates that an expected column was not found in the result set.
MissingColumnError MissingColumnErrorDetails
{- |
Renders a 'MarshallErrorDetails' to a 'String' with a specified
'ErrorDetailLevel'.
@since 1.0.0.0
-}
renderMarshallErrorDetails :: ErrorDetailLevel -> MarshallErrorDetails -> String
renderMarshallErrorDetails detailLevel err =
case err of
DecodingError details -> renderDecodingErrorDetails detailLevel details
MissingColumnError details -> renderMissingColumnErrorDetails detailLevel details
{- |
Details about an error that occurred while decoding values found in a SQL
result set.
@since 1.0.0.0
-}
data DecodingErrorDetails = DecodingErrorDetails
{ decodingErrorValues :: [(B8.ByteString, SqlValue.SqlValue)]
, decodingErrorMessage :: String
}
{- |
Renders a 'DecodingErrorDetails' to a 'String' with a specified
'ErrorDetailLevel'.
@since 1.0.0.0
-}
renderDecodingErrorDetails :: ErrorDetailLevel -> DecodingErrorDetails -> String
renderDecodingErrorDetails detailLevel details =
let
presentableErrorValues =
map
(presentSqlColumnValue detailLevel redactNonIdentifierValue)
(decodingErrorValues details)
in
concat
[ "Unable to decode columns from result set: "
, redactErrorMessage detailLevel (decodingErrorMessage details)
, ". Value(s) that failed to decode: ["
, List.intercalate ", " presentableErrorValues
, "]"
]
{- |
Details about a column that was found to be missing in a SQL result set
during decoding.
@since 1.0.0.0
-}
data MissingColumnErrorDetails = MissingColumnErrorDetails
{ missingColumnName :: B8.ByteString
, actualColumnNames :: (Set.Set B8.ByteString)
}
{- |
Renders a 'MissingColumnErrorDetails' to a 'String' with a specified
'ErrorDetailLevel'.
@since 1.0.0.0
-}
renderMissingColumnErrorDetails :: ErrorDetailLevel -> MissingColumnErrorDetails -> String
renderMissingColumnErrorDetails detailLevel details =
let
presentableActualNames =
map
(redactSchemaName detailLevel . B8.unpack)
(Set.toList $ actualColumnNames details)
in
concat
[ "Column "
, redactSchemaName detailLevel (B8.unpack $ missingColumnName details)
, " not found in results set. Actual columns were ["
, List.intercalate ", " presentableActualNames
, "]"
]