typesafe-hs-0.1.0.0: src/TypeSafe/Wire/Error.hs
-- | Pure error-body codecs and total parsing, with no status classification.
module TypeSafe.Wire.Error
( LocationSegment (..),
ValidationError (..),
HTTPValidationError (..),
HTTPBadRequestError (..),
BadRequestDetail (..),
ErrorDetail (..),
ErrorBody (..),
parseErrorBody,
)
where
import Data.Aeson
import Data.Aeson.Encoding qualified as Encoding
import Data.Aeson.KeyMap qualified as KeyMap
import Data.Aeson.Types (parseMaybe)
import Data.ByteString.Lazy qualified as LBS
import Data.Text (Text)
import Data.Text.Encoding qualified as Text
import Data.Vector (Vector)
import GHC.Generics (Generic)
import TypeSafe.Wire.Internal.Number
optionalPair :: (ToJSON a) => Key -> Maybe a -> Encoding.Series
optionalPair k = maybe mempty (k .=)
optionalValue :: (ToJSON a) => Key -> Maybe a -> [(Key, Value)]
optionalValue k = maybe [] (\v -> [k .= v])
-- | A field name or bounded integral array index.
data LocationSegment = LocationField !Text | LocationIndex !Int
deriving stock (Generic, Eq, Show)
instance ToJSON LocationSegment where
toJSON (LocationField t) = toJSON t
toJSON (LocationIndex n) = toJSON n
toEncoding (LocationField t) = toEncoding t
toEncoding (LocationIndex n) = toEncoding n
instance FromJSON LocationSegment where
parseJSON (String t) = pure (LocationField t)
parseJSON v = LocationIndex <$> parseBoundedIntegral v
-- | One validation failure, with its path, explanation, code, optional text preview and context.
data ValidationError = ValidationError
{ loc :: !(Vector LocationSegment),
msg :: !(Text),
type_ :: !(Text),
input :: !(Maybe Text),
ctx :: !(Maybe Object)
}
deriving stock (Generic, Eq, Show)
instance ToJSON ValidationError where
toJSON x = object $ [] <> ["loc" .= x.loc] <> ["msg" .= x.msg] <> ["type" .= x.type_] <> optionalValue "input" x.input <> optionalValue "ctx" x.ctx
toEncoding x = pairs $ "loc" .= x.loc <> "msg" .= x.msg <> "type" .= x.type_ <> optionalPair "input" x.input <> optionalPair "ctx" x.ctx
instance FromJSON ValidationError where
parseJSON = withObject "ValidationError" $ \o -> ValidationError <$> o .: "loc" <*> o .: "msg" <*> o .: "type" <*> o .:! "input" <*> o .:! "ctx"
-- | Validation envelope; detail may be absent, but a present detail must be an array.
data HTTPValidationError = HTTPValidationError
{ detail :: !(Maybe (Vector ValidationError))
}
deriving stock (Generic, Eq, Show)
instance ToJSON HTTPValidationError where
toJSON x = object $ [] <> optionalValue "detail" x.detail
toEncoding x = pairs $ optionalPair "detail" x.detail
instance FromJSON HTTPValidationError where
parseJSON = withObject "HTTPValidationError" $ \o -> HTTPValidationError <$> o .:! "detail"
-- | Application error detail is either text or an arbitrary JSON object.
data BadRequestDetail = BadRequestDetailText !Text | BadRequestDetailObject !Object
deriving stock (Generic, Eq, Show)
instance ToJSON BadRequestDetail where
toJSON (BadRequestDetailText t) = String t
toJSON (BadRequestDetailObject o) = Object o
toEncoding (BadRequestDetailText t) = toEncoding t
toEncoding (BadRequestDetailObject o) = toEncoding o
instance FromJSON BadRequestDetail where
parseJSON (String t) = pure (BadRequestDetailText t)
parseJSON (Object o) = pure (BadRequestDetailObject o)
parseJSON _ = fail "expected a string or object detail"
-- | Application-validation detail, required as text or an arbitrary object.
data HTTPBadRequestError = HTTPBadRequestError
{ detail :: !(BadRequestDetail)
}
deriving stock (Generic, Eq, Show)
instance ToJSON HTTPBadRequestError where
toJSON x = object $ [] <> ["detail" .= x.detail]
toEncoding x = pairs $ "detail" .= x.detail
instance FromJSON HTTPBadRequestError where
parseJSON = withObject "HTTPBadRequestError" $ \o -> HTTPBadRequestError <$> o .: "detail"
-- | Observed structured detail with a required message and optional error_type.
data ErrorDetail = ErrorDetail
{ errorType :: !(Maybe Text),
message :: !(Text)
}
deriving stock (Generic, Eq, Show)
instance ToJSON ErrorDetail where
toJSON x = object $ [] <> optionalValue "error_type" x.errorType <> ["message" .= x.message]
toEncoding x = pairs $ optionalPair "error_type" x.errorType <> "message" .= x.message
instance FromJSON ErrorDetail where
parseJSON = withObject "ErrorDetail" $ \o -> ErrorDetail <$> o .:! "error_type" <*> o .: "message"
-- | Recognized body shapes, or complete JSON, text and empty fallbacks.
data ErrorBody
= ErrorBodyValidation !HTTPValidationError
| ErrorBodyDetail !ErrorDetail
| ErrorBodyDetailText !Text
| ErrorBodyJson !Value
| ErrorBodyText !Text
| ErrorBodyEmpty
deriving stock (Generic, Eq, Show)
-- | Parse bytes without needing a status code. Invalid UTF-8 is replaced (R6).
parseErrorBody :: LBS.ByteString -> ErrorBody
parseErrorBody bytes
| LBS.null bytes = ErrorBodyEmpty
| otherwise = case eitherDecode bytes of
Left _ -> ErrorBodyText (Text.decodeUtf8Lenient (LBS.toStrict bytes))
Right value -> classify value
where
classify value@(Object o) = case KeyMap.lookup "detail" o of
Just (Array _) | Just body <- parseMaybe parseJSON value -> ErrorBodyValidation body
Just obj@(Object _) | Just d <- parseMaybe parseJSON obj -> ErrorBodyDetail d
Just (String t) -> ErrorBodyDetailText t
_ -> ErrorBodyJson value
classify value = ErrorBodyJson value