module Data.Aeson.Validation
(
Schema
, schema
, validate
, bool
, true
, false
, number
, theNumber
, someNumber
, integer
, theInteger
, someInteger
, string
, theString
, someString
, regex
, datetime
, Field
, Path
, object
, object'
, (.:)
, (.:?)
, array
, sizedArray
,
set
, sizedSet
, tuple
, anything
, nullable
) where
import Data.Aeson.Validation.Internal
import Control.Monad ((>=>))
import Control.Monad.Reader (MonadReader, ask, local, runReader)
import Data.Aeson (Value(..))
import Data.Foldable
import Data.Hashable (Hashable(..))
import Data.HashMap.Strict (HashMap)
import Data.HashSet (HashSet)
import Data.List.NonEmpty (NonEmpty)
import Data.Maybe (isJust)
import Data.Scientific (Scientific, floatingOrInteger, isInteger)
import Data.Semigroup
import Data.Sequence (Seq)
import Data.Text (Text)
import Data.Text.Encoding (encodeUtf8)
import Data.Time.ISO8601 (parseISO8601)
import Data.Vector (Vector)
import GHC.Generics (Generic)
import Lens.Micro hiding (set)
import Prelude.Compat
import Text.Regex.PCRE.Light (Regex)
import qualified Data.HashMap.Strict as HashMap
import qualified Data.HashSet as HashSet
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Text as Text
import qualified Data.Vector as Vector
import qualified Text.Regex.PCRE.Light as Regex
newtype FieldMap
= FieldMap
{ unFieldMap :: HashMap (Pair Demand Text) (Pair FieldMap [Schema]) }
instance Monoid FieldMap where
mempty = FieldMap mempty
mappend (FieldMap x) (FieldMap y) = FieldMap (mappend x y)
data Pair a b
= Pair !a !b
deriving (Eq, Generic)
instance (Hashable a, Hashable b) => Hashable (Pair a b)
data Context
= Empty
| Property !Text Context
| Index !Int Context
class A schema where
someNumber :: (Scientific -> Bool) -> schema
someInteger :: (Integer -> Bool) -> schema
someString :: (Text -> Bool) -> schema
instance A Schema where
someNumber p = SSomeNumber p Nothing
someInteger p = SSomeNumber (either nope p . floatingOrInteger) Nothing
where
nope :: Double -> Bool
nope _ = False
someString p = SSomeString p Nothing
instance (a ~ Text) => A (a -> Schema) where
someNumber p s = SSomeNumber p (Just s)
someInteger p s =
SSomeNumber (either nope p . floatingOrInteger) (Just s)
where
nope :: Double -> Bool
nope _ = False
someString p s = SSomeString p (Just s)
bool :: Schema
bool = SBool
true :: Schema
true = STrue
false :: Schema
false = SFalse
number :: Schema
number = SNumber
theNumber :: Scientific -> Schema
theNumber = STheNumber
integer :: Schema
integer = SInteger
theInteger :: Integer -> Schema
theInteger = STheInteger
string :: Schema
string = SString
theString :: Text -> Schema
theString = STheString
regex :: Text -> Schema
regex r = SSomeString p (Just ("matches " <> r))
where
p :: Text -> Bool
p s = has (_Just . _head) (Regex.match r' (encodeUtf8 s) [])
r' :: Regex
r' = Regex.compile (encodeUtf8 r) [Regex.utf8]
datetime :: Schema
datetime = SDateTime
object :: [Field] -> Schema
object xs = SObject NotStrict (flatten NotStrict xs)
object' :: [Field] -> Schema
object' xs = SObject Strict (flatten Strict xs)
flatten :: Strict -> [Field] -> [ShallowField]
flatten s xs = mapFields (foldr step mempty xs)
where
step
:: Field
-> HashMap (Pair Demand Text) (Pair FieldMap [Schema])
-> HashMap (Pair Demand Text) (Pair FieldMap [Schema])
step (Field req path sch) = go path
where
go :: Path
-> HashMap (Pair Demand Text) (Pair FieldMap [Schema])
-> HashMap (Pair Demand Text) (Pair FieldMap [Schema])
go = \case
Leaf key ->
HashMap.alter
(\case
Nothing -> Just (Pair mempty [sch])
Just (Pair m schs) -> Just (Pair m (sch : schs)))
(Pair req key)
Link key path' ->
HashMap.alter
(\case
Nothing -> val mempty []
Just (Pair m schs) -> val m schs)
(Pair req key)
where
val :: FieldMap -> [Schema] -> Maybe (Pair FieldMap [Schema])
val m ss = Just (Pair (FieldMap (go path' (unFieldMap m))) ss)
mapFields
:: HashMap (Pair Demand Text) (Pair FieldMap [Schema]) -> [ShallowField]
mapFields = HashMap.toList >=> go
where
go :: (Pair Demand Text, Pair FieldMap [Schema]) -> [ShallowField]
go (Pair req key, Pair m ss) =
case mapFields (unFieldMap m) of
[] -> fields
fs -> objField fs : fields
where
fields :: [ShallowField]
fields = map (ShallowField req key) ss
objField :: [ShallowField] -> ShallowField
objField fs = ShallowField
{ fieldDemand = req
, fieldKey = key
, fieldSchema = SObject s fs
}
(.:) :: Path -> Schema -> Field
(.:) = Field Req
infixr 5 .:
(.:?) :: Path -> Schema -> Field
(.:?) = Field Opt
infixr 5 .:?
array :: Schema -> Schema
array = SArray NotUnique minBound maxBound
sizedArray :: Int -> Int -> Schema -> Schema
sizedArray = SArray NotUnique
set :: Schema -> Schema
set = SArray Unique minBound maxBound
sizedSet :: Int -> Int -> Schema -> Schema
sizedSet = SArray Unique
tuple :: [Schema] -> Schema
tuple = STuple
anything :: Schema
anything = SAnything
nullable :: Schema -> Schema
nullable = SNullable
schema :: Schema -> Value -> Bool
schema s v = null (validate s v)
validate :: Schema -> Value -> [Text]
validate s v =
toList (runReader (validate_ (normalize s) v) Empty)
validate_
:: (Applicative m, MonadReader Context m) => Schema -> Value -> m (Seq Text)
validate_ = \case
SBool -> validateBool
STrue -> validateTrue
SFalse -> validateFalse
SNumber -> validateNumber
SInteger -> validateInteger
STheNumber n -> validateTheNumber n
STheInteger n -> validateTheInteger n
SSomeNumber p s -> validateSomeNumber p s
SString -> validateString
STheString s -> validateTheString s
SSomeString p s -> validateSomeString p s
SDateTime -> validateDateTime
SObject s xs -> validateObject s xs
SArray u x y s -> validateArray u x y s
STuple ss -> validateTuple ss
SAnything -> const (pure mempty)
SNullable s -> validateNullable s
SAlts ss -> validateAlts ss
SNegate s0 -> case s0 of
SBool -> validateNotBool
STrue -> validateNotTrue
SFalse -> validateNotFalse
SNumber -> validateNotNumber
SInteger -> validateNotInteger
STheNumber n -> validateNotTheNumber n
STheInteger n -> validateNotTheInteger n
SSomeNumber p s -> validateNotSomeNumber p s
SString -> validateNotString
STheString s -> validateNotTheString s
SSomeString p s -> validateNotSomeString p s
SDateTime -> validateNotDateTime
SObject s xs -> validateNotObject s xs
SArray u x y s -> validateNotArray u x y s
STuple ss -> validateNotTuple ss
SAnything -> const (err "negate anything")
SNullable s -> validateNotNullable s
SAlts ss -> validateNotAlts ss
SNegate s -> validate_ s
validateBool :: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateBool = \case
Bool _ -> pure mempty
v -> mismatch "a bool" (valType v)
validateTrue :: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateTrue = \case
Bool b | b -> pure mempty
| otherwise -> mismatch "true" "false"
v -> mismatch "true" (valType v)
validateFalse :: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateFalse = \case
Bool b | b -> mismatch "false" "true"
| otherwise -> pure mempty
v -> mismatch "false" (valType v)
validateNumber
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateNumber = \case
Number _ -> pure mempty
v -> mismatch "a number" (valType v)
validateInteger
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateInteger = \case
Number n | isInteger n -> pure mempty
| otherwise -> mismatch "an integer" (tshow n)
v -> mismatch "an integer" (valType v)
validateTheNumber
:: (Applicative m, MonadReader Context m)
=> Scientific -> Value -> m (Seq Text)
validateTheNumber n = \case
Number m | isclose n m -> pure mempty
| otherwise -> mismatch (tshow n) (tshow m)
v -> mismatch (tshow n) (valType v)
validateTheInteger
:: (Applicative m, MonadReader Context m) => Integer -> Value -> m (Seq Text)
validateTheInteger n = \case
Number m | fromInteger n == m -> pure mempty
| otherwise -> mismatch (tshow n) (tshow m)
v -> mismatch (tshow n) (valType v)
validateSomeNumber
:: (Applicative m, MonadReader Context m)
=> (Scientific -> Bool) -> Maybe Text -> Value -> m (Seq Text)
validateSomeNumber p msg = \case
Number n | p n -> pure mempty
| otherwise -> failedPredicate msg
v -> mismatch "a number" (valType v)
validateString
:: (Applicative m, MonadReader Context m)
=> Value -> m (Seq Text)
validateString = \case
String _ -> pure mempty
v -> mismatch "a string" (valType v)
validateTheString
:: (Applicative m, MonadReader Context m)
=> Text -> Value -> m (Seq Text)
validateTheString s = \case
String s' | s == s' -> pure mempty
| otherwise -> mismatch (tshow s) (tshow s')
v -> mismatch (tshow s) (valType v)
validateSomeString
:: (Applicative m, MonadReader Context m)
=> (Text -> Bool) -> Maybe Text -> Value -> m (Seq Text)
validateSomeString p msg = \case
String s | p s -> pure mempty
| otherwise -> failedPredicate msg
v -> mismatch "a string" (valType v)
validateDateTime
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateDateTime = \case
String s | isDateTime s -> pure mempty
| otherwise -> mismatch "a datetime" (tshow s)
v -> mismatch "a string" (valType v)
validateObject
:: (Applicative m, MonadReader Context m)
=> Strict -> [ShallowField] -> Value -> m (Seq Text)
validateObject s xs = \case
Object obj ->
case s of
NotStrict -> mconcat <$> mapM (validateField obj) xs
Strict -> validateObject' xs obj
v -> mismatch "an object" (valType v)
validateObject'
:: (Applicative m, MonadReader Context m)
=> [ShallowField] -> HashMap Text Value -> m (Seq Text)
validateObject' [] obj =
case HashMap.keys obj of
[] -> pure mempty
ks -> err ("extra fields: " <> Text.intercalate ", " (map tshow ks))
validateObject' (x:xs) obj = (<>)
<$> validateField obj x
<*> validateObject' xs (HashMap.delete (fieldKey x) obj)
validateField
:: (Applicative m, MonadReader Context m)
=> HashMap Text Value -> ShallowField -> m (Seq Text)
validateField obj = \case
ShallowField Req key s ->
case HashMap.lookup key obj of
Nothing -> err ("missing field " <> tshow key)
Just v -> local (Property key) (validate_ s v)
ShallowField Opt key s ->
case HashMap.lookup key obj of
Nothing -> pure mempty
Just v -> local (Property key) (validate_ s v)
validateArray
:: forall m. (Applicative m, MonadReader Context m)
=> Unique -> Int -> Int -> Schema -> Value -> m (Seq Text)
validateArray uniq x y sch = \case
Array v -> do
errs1 <-
case sch of
SAnything -> pure mempty
_ ->
mconcat <$>
imapM (\n val -> local (Index n) (validate_ sch val))
(Vector.toList v)
errs2 <-
if uniq == Unique
then uniqCheck v
else pure mempty
errs3 <- boundsCheck (length v)
pure (errs1 <> errs2 <> errs3)
v -> mismatch "an array" (valType v)
where
boundsCheck :: Int -> m (Seq Text)
boundsCheck len
| len >= x && len <= y = pure mempty
| otherwise =
case (x == minBound, y == maxBound) of
(True, True) -> error "impossible"
(True, False) -> mismatch ("<= " <> tshow y <> " elements") (tshow len)
(False, True) -> mismatch (">= " <> tshow x <> " elements") (tshow len)
(False, False) ->
mismatch
("between " <> tshow x <> " and " <> tshow y <>
" elements (inclusive)")
(tshow len)
uniqCheck :: Vector Value -> m (Seq Text)
uniqCheck v =
if length v /= length (toSet v)
then err "array does not contain unique elements"
else pure mempty
where
toSet :: Vector Value -> HashSet Value
toSet = Vector.foldr' HashSet.insert mempty
validateTuple
:: forall m.
(Applicative m, MonadReader Context m)
=> [Schema] -> Value -> m (Seq Text)
validateTuple ss0 = \case
Array v0 -> go 0 ss0 (toList v0)
where
go :: Int -> [Schema] -> [Value] -> m (Seq Text)
go _ [] [] = pure mempty
go !n (s:ss) (v:vs) = (<>)
<$> local (Index n) (validate_ s v)
<*> go (n+1) ss vs
go _ _ _ =
mismatch (tshow (length ss0) <> " elements")
(tshow (length v0) <> " elements")
v -> mismatch "an array" (valType v)
validateNullable
:: (Applicative m, MonadReader Context m)
=> Schema -> Value -> m (Seq Text)
validateNullable s v =
if v /= Null
then validate_ s v
else pure mempty
validateAlts
:: forall m.
(Applicative m, MonadReader Context m)
=> NonEmpty Schema -> Value -> m (Seq Text)
validateAlts ss0 val = go (NonEmpty.toList ss0)
where
go :: [Schema] -> m (Seq Text)
go [] = error "impossible"
go [s] = validate_ s val
go (s:ss) = do
errs1 <- validate_ s val
errs2 <- go ss
if null errs1 || null errs2
then pure mempty
else pure (errs1 <> errs2)
validateNotBool
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateNotBool v =
case v of
Bool b -> mismatch "anything but a bool" (if b then "true" else "false")
_ -> pure mempty
validateNotTrue
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateNotTrue = \case
Bool True -> mismatch "anything but true" "true"
_ -> pure mempty
validateNotFalse
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateNotFalse = \case
Bool False -> mismatch "anything but false" "false"
_ -> pure mempty
validateNotNumber
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateNotNumber v =
case v of
Number n -> mismatch "anything but a number" (tshow n)
_ -> pure mempty
validateNotInteger
:: (Applicative m, MonadReader Context m) => Value -> m (Seq Text)
validateNotInteger = \case
Number n | isInteger n ->
mismatch "anything but an integer" (tshow m)
where
m :: Integer
m = either nope id (floatingOrInteger n)
nope :: Double -> Integer
nope = error "impossible"
_ -> pure mempty
validateNotTheNumber
:: (Applicative m, MonadReader Context m)
=> Scientific -> Value -> m (Seq Text)
validateNotTheNumber n v =
case v of
Number m | isclose n m -> mismatch ("anything but " <> tshow n) (tshow m)
_ -> pure mempty
validateNotTheInteger
:: (Applicative m, MonadReader Context m)
=> Integer -> Value -> m (Seq Text)
validateNotTheInteger n v =
case v of
Number m | fromInteger n == m ->
mismatch ("anything but " <> tshow n) (tshow m)
_ -> pure mempty
validateNotSomeNumber
:: (Applicative m, MonadReader Context m)
=> (Scientific -> Bool) -> Maybe Text -> Value -> m (Seq Text)
validateNotSomeNumber p msg = \case
Number n | p n -> passedPredicate msg
_ -> pure mempty
validateNotString
:: (Applicative m, MonadReader Context m)
=> Value -> m (Seq Text)
validateNotString v =
case v of
String s -> mismatch "anything but a string" (tshow s)
_ -> pure mempty
validateNotTheString
:: (Applicative m, MonadReader Context m)
=> Text -> Value -> m (Seq Text)
validateNotTheString s = \case
String s' | s == s' -> mismatch ("anything but " <> tshow s) (tshow s)
_ -> pure mempty
validateNotSomeString
:: (Applicative m, MonadReader Context m)
=> (Text -> Bool) -> Maybe Text -> Value -> m (Seq Text)
validateNotSomeString p msg = \case
String s | p s -> passedPredicate msg
_ -> pure mempty
validateNotDateTime
:: (Applicative m, MonadReader Context m)
=> Value -> m (Seq Text)
validateNotDateTime = \case
String s | isDateTime s -> mismatch "anything but a datetime" (tshow s)
_ -> pure mempty
validateNotObject
:: (Applicative m, MonadReader Context m)
=> Strict -> [ShallowField] -> Value -> m (Seq Text)
validateNotObject sch xs val = do
errs <- validateObject sch xs val
if null errs
then err "passed object schema"
else pure mempty
validateNotArray
:: (Applicative m, MonadReader Context m)
=> Unique -> Int -> Int -> Schema -> Value -> m (Seq Text)
validateNotArray uniq x y sch val = do
errs <- validateArray uniq x y sch val
if null errs
then err ("passed " <> typ <> " schema")
else pure mempty
where
typ =
case uniq of
Unique -> "set"
NotUnique -> "array"
validateNotTuple
:: (Applicative m, MonadReader Context m)
=> [Schema] -> Value -> m (Seq Text)
validateNotTuple ss val = do
errs <- validateTuple ss val
if null errs
then err "passed tuple schema"
else pure mempty
validateNotNullable
:: (Applicative m, MonadReader Context m)
=> Schema -> Value -> m (Seq Text)
validateNotNullable s = \case
Null -> err "passed nullable schema"
v -> validate_ (negate s) v
validateNotAlts
:: (Applicative m, MonadReader Context m)
=> NonEmpty Schema -> Value -> m (Seq Text)
validateNotAlts ss val =
mconcat <$> mapM (\s -> validate_ (negate s) val) (toList ss)
err
:: (Applicative m, MonadReader Context m)
=> Text -> m (Seq Text)
err msg = do
ctx <- ask
pure (pure (prefix ctx <> msg))
where
prefix :: Context -> Text
prefix = maybe mempty (\s -> "in context " <> s <> ", ") . prettyPrintContext
prettyPrintContext :: Context -> Maybe Text
prettyPrintContext = \case
Empty -> Nothing
Property key ctx -> Just (go ctx <> tshow key)
Index idx ctx -> Just (go ctx <> tshow idx)
where
go :: Context -> Text
go = \case
Empty -> ""
Property key ctx -> go ctx <> tshow key <> "."
Index idx ctx -> go ctx <> tshow idx <> "."
mismatch
:: (Applicative m, MonadReader Context m) => Text -> Text -> m (Seq Text)
mismatch x y = err ("expected " <> x <> " but found " <> y)
failedPredicate
:: (Applicative m, MonadReader Context m) => Maybe Text -> m (Seq Text)
failedPredicate msg =
err (maybe "failed predicate" ("failed predicate: " <>) msg)
passedPredicate
:: (Applicative m, MonadReader Context m) => Maybe Text -> m (Seq Text)
passedPredicate msg =
err (maybe "passed predicate" ("passed predicate: " <>) msg)
valType :: Value -> Text
valType = \case
Null -> "null"
Bool _ -> "a bool"
Number _ -> "a number"
String _ -> "a string"
Object _ -> "an object"
Array _ -> "an array"
tshow :: Show a => a -> Text
tshow = Text.pack . show
isclose :: Scientific -> Scientific -> Bool
isclose n m = abs (n-m) <= 1e-9 * max (abs n) (abs m)
isDateTime :: Text -> Bool
isDateTime = isJust . parseISO8601 . Text.unpack
imapM :: Monad m => (Int -> a -> m b) -> [a] -> m [b]
imapM f xs = mapM (uncurry f) (zip [0..] xs)