module Data.API.Error
(
JSONError(..)
, JSONWarning
, Expected(..)
, FormatExpected(..)
, Position
, Step(..)
, inField
, prettyJSONErrorPositions
, prettyJSONError
, prettyStep
, expectedArray
, expectedBool
, expectedInt
, expectedObject
, expectedString
, badFormat
, ValueError(..)
, ValidateFailure(..)
, ValidateWarning
, ApplyFailure(..)
, TypeKind(..)
, MigrateFailure(..)
, MigrateWarning
, prettyMigrateFailure
, prettyValidateFailure
, prettyValueError
, prettyValueErrorPosition
) where
import Data.API.Changes.Types
import Data.API.PP
import Data.API.NormalForm
import Data.API.Types
import Data.API.Utils
import qualified Data.Aeson as JS
import Data.Aeson.TH
import qualified Data.Graph as Graph
import Data.List
import Data.Map ( Map )
import qualified Data.Map as Map
import qualified Data.SafeCopy as SC
import Data.Set ( Set )
import qualified Data.Set as Set
import qualified Data.Text as T
import Data.Time
data JSONError = Expected Expected String JS.Value
| BadFormat FormatExpected String T.Text
| MissingField
| MissingAlt [String]
| UnexpectedField
| UnexpectedEnumVal [T.Text] T.Text
| IntRangeError String Int IntRange
| UTCRangeError String UTCTime UTCRange
| RegexError String T.Text RegEx
| SyntaxError String
deriving (Eq, Show)
type JSONWarning = JSONError
data Expected = ExpArray
| ExpBool
| ExpInt
| ExpObject
| ExpString
deriving (Eq, Show)
data FormatExpected = FmtBinary
| FmtUTC
| FmtOther
deriving (Eq, Show)
expectedArray, expectedBool, expectedInt, expectedObject, expectedString
:: JS.Value -> JSONError
expectedArray = Expected ExpArray "Array"
expectedBool = Expected ExpBool "Bool"
expectedInt = Expected ExpInt "Int"
expectedObject = Expected ExpObject "Object"
expectedString = Expected ExpString "String"
badFormat :: String -> T.Text -> JSONError
badFormat = BadFormat FmtOther
prettyJSONError :: JSONError -> String
prettyJSONError (Expected _ s v) = "When expecting " ++ s ++ ", encountered "
++ x ++ " instead"
where
x = case v of
JS.Object _ -> "Object"
JS.Array _ -> "Array"
JS.String _ -> "String"
JS.Number _ -> "Number"
JS.Bool _ -> "Boolean"
JS.Null -> "Null"
prettyJSONError (BadFormat _ s t) = "Could not parse as " ++ s ++ " the string " ++ show t
prettyJSONError MissingField = "Field missing from Object"
prettyJSONError (MissingAlt xs) = "Missing alternative, expecting one of: "
++ intercalate ", " xs
prettyJSONError UnexpectedField = "Unexpected field in Object"
prettyJSONError (UnexpectedEnumVal xs t) = "Unexpected enum value " ++ show t
++ ", expecting one of: "
++ T.unpack (T.intercalate ", " xs)
prettyJSONError (IntRangeError s i r) = s ++ ": " ++ show i ++ " not in range " ++ show r
prettyJSONError (UTCRangeError s u r) = s ++ ": " ++ show u ++ " not in range " ++ show r
prettyJSONError (RegexError s _ t) = s ++ ": failed to match RE: " ++ show t
prettyJSONError (SyntaxError e) = "JSON syntax error: " ++ e
type Position = [Step]
data Step = InField T.Text | InElem Int
deriving (Eq, Show)
inField :: FieldName -> Step
inField fn = InField (_FieldName fn)
prettyStep :: Step -> String
prettyStep (InField f) = " in the field " ++ show f
prettyStep (InElem i) = " in array index " ++ show i
instance PPLines Step where
ppLines s = [prettyStep s]
prettyJSONErrorPositions :: [(JSONError, Position)] -> String
prettyJSONErrorPositions xs = unlines $ concatMap help xs
where
help (e, pos) = prettyJSONError e : map prettyStep pos
data ValueError
= JSONError JSONError
| CustomMigrationError String JS.Value
| InvalidAPI ApplyFailure
deriving (Eq, Show)
data ValidateFailure
= ChangelogOutOfOrder { vfLaterVersion :: VersionExtra
, vfEarlierVersion :: VersionExtra }
| CannotDowngrade { vfFromVersion :: VersionExtra
, vfToVersion :: VersionExtra }
| ApiInvalid { vfInvalidVersion :: VersionExtra
, vfMissingDeclarations :: Set TypeName }
| ChangelogEntryInvalid { vfSuccessfullyApplied :: [APITableChange]
, vfFailedToApply :: APIChange
, vfApplyFailure :: ApplyFailure }
| ChangelogIncomplete { vfChangelogVersion :: VersionExtra
, vfTargetVersion :: VersionExtra
, vfDifferences :: Map TypeName (MergeResult NormTypeDecl NormTypeDecl) }
deriving (Eq, Show)
data ValidateWarning = ValidateWarning
deriving Show
data ApplyFailure
= TypeExists { afExistingType :: TypeName }
| TypeDoesNotExist { afMissingType :: TypeName }
| TypeWrongKind { afTypeName :: TypeName
, afExpectedKind :: TypeKind }
| TypeInUse { afTypeName :: TypeName }
| TypeMalformed { afType :: APIType
, afMissingTypes :: Set TypeName }
| DeclMalformed { afTypeName :: TypeName
, afDecl :: NormTypeDecl
, afMissingTypes :: Set TypeName }
| FieldExists { afTypeName :: TypeName
, afTypeKind :: TypeKind
, afExistingField :: FieldName }
| FieldDoesNotExist { afTypeName :: TypeName
, afTypeKind :: TypeKind
, afMissingField :: FieldName }
| FieldBadDefaultValue { afTypeName :: TypeName
, afFieldName :: FieldName
, afFieldType :: APIType
, afBadDefault :: DefaultValue }
| DefaultMissing { afTypeName :: TypeName
, afFieldName :: FieldName }
| TableChangeError { afCustomMessage :: String }
deriving (Eq, Show)
data TypeKind = TKRecord | TKUnion | TKEnum | TKNewtype | TKTypeSynonym
deriving (Eq, Show)
data MigrateFailure
= ValidateFailure ValidateFailure
| ValueError ValueError Position
deriving (Eq, Show)
type MigrateWarning = ValidateWarning
prettyMigrateFailure :: MigrateFailure -> String
prettyMigrateFailure = unlines . ppLines
prettyValidateFailure :: ValidateFailure -> String
prettyValidateFailure = unlines . ppLines
prettyValueError :: ValueError -> String
prettyValueError = unlines . ppLines
prettyValueErrorPosition :: (ValueError, Position) -> String
prettyValueErrorPosition = unlines . ppLines
instance PP TypeKind where
pp TKRecord = "record"
pp TKUnion = "union"
pp TKEnum = "enum"
pp TKNewtype = "newtype"
pp TKTypeSynonym = "type"
ppATypeKind :: TypeKind -> String
ppATypeKind TKRecord = "a record"
ppATypeKind TKUnion = "a union"
ppATypeKind TKEnum = "an enum"
ppATypeKind TKNewtype = "a newtype"
ppATypeKind TKTypeSynonym = "a type synonym"
ppMemberWord :: TypeKind -> String
ppMemberWord TKRecord = "field"
ppMemberWord TKUnion = "alternative"
ppMemberWord TKEnum = "value"
ppMemberWord TKNewtype = "member"
ppMemberWord TKTypeSynonym = "member"
instance PPLines MigrateFailure where
ppLines (ValidateFailure x) = ppLines x
ppLines (ValueError x ps) = ppLines x ++ map prettyStep ps
instance PPLines ValidateFailure where
ppLines (ChangelogOutOfOrder later earlier) =
["Changelog out of order: version " ++ pp later
++ " appears after version " ++ pp earlier]
ppLines (CannotDowngrade from to) =
["Cannot downgrade from version " ++ pp from
++ " to version " ++ pp to]
ppLines (ApiInvalid ver missing) =
["Missing declarations in API version " ++ pp ver ++ ": " ++ pp missing]
ppLines (ChangelogEntryInvalid succs change af) =
ppLines af ++ ("when applying the change" : indent (ppLines change))
++ if not (null succs)
then "after successfully applying the changes:"
: indent (ppLines succs)
else []
ppLines (ChangelogIncomplete ver ver' diffs) =
("Changelog incomplete! Differences between log version ("
++ showVersionExtra ver ++ ") and latest version (" ++ showVersionExtra ver' ++ "):")
: indent (ppDiffs diffs)
ppDiffs :: Map TypeName (MergeResult NormTypeDecl NormTypeDecl) -> [String]
ppDiffs = concatMap (uncurry ppDiff) . sortDiffs . Map.toList
sortDiffs :: [(TypeName, MergeResult NormTypeDecl NormTypeDecl)]
-> [(TypeName, MergeResult NormTypeDecl NormTypeDecl)]
sortDiffs = reverse . Graph.flattenSCCs . Graph.stronglyConnComp . map f
where
f (tn, mr) = ((tn, mr), tn, Set.toList (mergeResultFreeVars mr))
mergeResultFreeVars :: MergeResult NormTypeDecl NormTypeDecl -> Set TypeName
mergeResultFreeVars (OnlyInLeft x) = typeDeclFreeVars x
mergeResultFreeVars (OnlyInRight x) = typeDeclFreeVars x
mergeResultFreeVars (InBoth x y) = typeDeclFreeVars x `Set.union` typeDeclFreeVars y
ppDiff :: TypeName -> MergeResult NormTypeDecl NormTypeDecl -> [String]
ppDiff t (OnlyInLeft _) = ["removed " ++ pp t]
ppDiff t (OnlyInRight d) = ("added " ++ pp t ++ " ") `inFrontOf` ppLines d
ppDiff t (InBoth (NRecordType flds) (NRecordType flds')) =
("changed record " ++ pp t)
: (concatMap (uncurry (ppDiffFields "field")) $ Map.toList $ diffMaps flds flds')
ppDiff t (InBoth (NUnionType alts) (NUnionType alts')) =
("changed union " ++ pp t)
: (concatMap (uncurry (ppDiffFields "alternative")) $ Map.toList $ diffMaps alts alts')
ppDiff t (InBoth (NEnumType vals) (NEnumType vals')) =
("changed enum " ++ pp t)
: (map (\ x -> " alternative removed " ++ pp x) $ Set.toList $ vals Set.\\ vals')
++ (map (\ x -> " alternative added " ++ pp x) $ Set.toList $ vals' Set.\\ vals)
ppDiff t (InBoth _ _) = ["incompatible definitions of " ++ pp t]
ppDiffFields :: String -> FieldName -> MergeResult APIType APIType -> [String]
ppDiffFields s f (OnlyInLeft _) = [" " ++ s ++ " removed " ++ pp f]
ppDiffFields s f (OnlyInRight ty) = [" " ++ s ++ " added " ++ pp f ++ " :: " ++ pp ty]
ppDiffFields s f (InBoth ty ty') = [ " incompatible types for " ++ s ++ " " ++ pp f
, " changelog type: " ++ pp ty
, " latest version type: " ++ pp ty' ]
instance PPLines ApplyFailure where
ppLines (TypeExists t) = ["Type " ++ pp t ++ " already exists"]
ppLines (TypeDoesNotExist t) = ["Type " ++ pp t ++ " does not exist"]
ppLines (TypeWrongKind t k) = ["Type " ++ pp t ++ " is not " ++ ppATypeKind k]
ppLines (TypeInUse t) = ["Type " ++ pp t ++ " is in use, so it cannot be modified"]
ppLines (TypeMalformed ty xs) = ["Type " ++ pp ty
++ " is malformed, missing declarations:"
, " " ++ pp xs]
ppLines (DeclMalformed t _ xs) = [ "Declaration of " ++ pp t
++ " is malformed, missing declarations:"
, " " ++ pp xs]
ppLines (FieldExists t k f) = ["Type " ++ pp t ++ " already has the "
++ ppMemberWord k ++ " " ++ pp f]
ppLines (FieldDoesNotExist t k f) = ["Type " ++ pp t ++ " does not have the "
++ ppMemberWord k ++ " " ++ pp f]
ppLines (FieldBadDefaultValue _ _ ty v) = ["Default value " ++ pp v
++ " is not compatible with the type " ++ pp ty]
ppLines (DefaultMissing t f) = ["Field " ++ pp f ++ " does not have a default value, but "
++ pp t ++ " occurs in the database"]
ppLines (TableChangeError s) = ["Error when detecting changed tables:", " " ++ s]
instance PPLines ValueError where
ppLines (JSONError e) = [prettyJSONError e]
ppLines (CustomMigrationError e v) = [ "Custom migration error:", " " ++ e
, "when migrating value"] ++ indent (ppLines v)
ppLines (InvalidAPI af) = "Invalid API detected during value migration:"
: indent (ppLines af)
$(deriveJSON defaultOptions ''JSONError)
$(deriveJSON defaultOptions ''Expected)
$(deriveJSON defaultOptions ''FormatExpected)
$(deriveJSON defaultOptions ''Step)
$(SC.deriveSafeCopy 1 'SC.base ''Step)