@@ -1,20 +1,24 @@
2module JSONSchema.Validator.Draft4.Any where
1module JSONSchema.Validator.Draft4.Any where
4−import Import
3+import Import hiding ((<>))
5+import Data.Aeson.TH (constructorTagModifier)
6+import Data.Char (toLower)
7+import qualified Data.HashMap.Strict as HM
6−import Data.List.NonEmpty (NonEmpty((:|)))
8+import Data.List.NonEmpty (NonEmpty((:|)))
7−import qualified Data.List.NonEmpty as NE
9+import qualified Data.List.NonEmpty as NE
8−import qualified Data.Scientific as SCI
10+import qualified Data.Scientific as SCI
9−import Data.Semigroup (Semigroup) -- for older GHCs
11+import Data.Semigroup
10−import Data.Set (Set)
12+import Data.Set (Set)
11−import qualified Data.Set as S
13+import qualified Data.Set as Set
14+import Data.Text.Encoding.Error (UnicodeException)
12−import qualified JSONPointer as JP
15+import qualified JSONPointer as JP
13−
16+import Network.HTTP.Types.URI (urlDecode)
14−import qualified JSONSchema.Validator.Utils as UT
17+
15−import JSONSchema.Validator.Reference (URIBaseAndFragment,
18+import JSONSchema.Validator.Reference (BaseURI(..), Scope(..),
16− resolveFragment,
19+ URIAndFragment,
17 resolveReference)
20 resolveReference)
21+import qualified JSONSchema.Validator.Utils as UT
19--------------------------------------------------
23--------------------------------------------------
@@ -39,42 +43,156 @@ data RefInvalid err
39 -- Also note that ideally we would enforce in the type system that any
43 -- Also note that ideally we would enforce in the type system that any
40 -- failing references be dealt with before valididation. Then this could
44 -- failing references be dealt with before valididation. Then this could
41 -- be removed entirely.
45 -- be removed entirely.
42− | RefPointerResolution Text
46+ | RefPointerResolution JSONPointerError
43− | RefLoop Text VisitedSchemas URIBaseAndFragment
47+ | RefLoop Text VisitedSchemas URIAndFragment
44 | RefInvalid Text Value (NonEmpty err)
48 | RefInvalid Text Value (NonEmpty err)
45 -- ^ 'Text' is the URI and 'Value' is the linked schema.
49 -- ^ 'Text' is the URI and 'Value' is the linked schema.
46 deriving (Eq, Show)
50 deriving (Eq, Show)
48newtype VisitedSchemas
52newtype VisitedSchemas
49− = VisitedSchemas { _unVisited :: [URIBaseAndFragment] }
53+ = VisitedSchemas { _unVisited :: [URIAndFragment] }
50 deriving (Eq, Show, Semigroup, Monoid)
54 deriving (Eq, Show, Semigroup, Monoid)
53 :: forall err schema. (FromJSON schema, ToJSON schema)
57 :: forall err schema. (FromJSON schema, ToJSON schema)
55− -> Maybe Text
58+ => (Text -> Maybe schema)
56− -> (Maybe Text -> Maybe schema)
59+ -- ^ Look up a schema.
60+ -> (BaseURI -> schema -> BaseURI)
61+ -- ^ Update scope (needed after moving deeper into nested schemas).
57− -> (VisitedSchemas -> Maybe Text -> schema -> Value -> [err])
62+ -> (VisitedSchemas -> Scope schema -> schema -> Value -> [err])
60 -> Maybe (RefInvalid err)
68 -> Maybe (RefInvalid err)
61−refVal visited scope getRef f (Ref reference) x
69+refVal getRef updateScope val visited scope (Ref reference) x
62 | (mURI, mFragment) `elem` _unVisited visited =
70 | (mURI, mFragment) `elem` _unVisited visited =
63 Just (RefLoop reference visited (mURI, mFragment))
71 Just (RefLoop reference visited (mURI, mFragment))
64− | otherwise =
72+ | otherwise = leftToMaybe $ do
65− case getRef mURI of
73+
66− Nothing -> Just (RefResolution reference)
74+ -- Get the referenced document
68− case resolveFragment mFragment schema of
76+ (newScope, doc) <- first RefResolution
69− Nothing -> Just (RefPointerResolution reference)
77+ $ getDocument getRef updateScope scope mURI reference
79+ -- Get the correct subschema within that document.
81+ res <- case mFragment of
82+ Nothing -> Right (newScope, doc)
83+ Just fragment -> first RefPointerResolution
84+ $ resolveFragment updateScope newScope fragment
85+ let (finalScope, schema) = res
87+ -- Check if that schema is valid.
71− let newVisited = (VisitedSchemas [(mURI, mFragment)]
89+ let newVisited = VisitedSchemas [(_documentURI newScope, mFragment)]
72− <> visited)
90+ <> visited
73− errs = f newVisited mURI s x
91+ failures = val newVisited finalScope schema x
74− in RefInvalid reference (toJSON schema)
92+ first (RefInvalid reference (toJSON schema))
75− <$> NE.nonEmpty errs
94+ $ NE.nonEmpty failures
77− (mURI, mFragment) = resolveReference scope reference
96+ mURI :: Maybe Text
97+ mFragment :: Maybe Text
98+ (mURI, mFragment) = resolveReference (_currentBaseURI scope) reference
101+ :: forall schema. (Text -> Maybe schema)
102+ -> (BaseURI -> schema -> BaseURI)
106+ -> Either Text (Scope schema, schema)
107+ -- ^ 'Left' is the URI of the document we failed to resolve.
108+getDocument getRef updateScope scope mURI reference =
109+ case mURI <* fst (resolveReference (BaseURI Nothing) reference) of
110+ Nothing -> Right topOfThisDoc
114+ Just s -> Right ( Scope s mURI (updateScope (BaseURI mURI) s)
118+ topOfThisDoc :: (Scope schema, schema)
120+ ( scope { _currentBaseURI =
121+ updateScope (BaseURI (_documentURI scope))
122+ (_topLevelDocument scope)
124+ , _topLevelDocument scope
127+data JSONPointerError
128+ = URLDecodingError UnicodeException
129+ -- ^ Aspirationally internal.
130+ | FormatError JP.FormatError
131+ | ResolutionError JP.ResolutionError
132+ | SubschemaDecodingError Text
133+ -- ^ Aspirationally internal.
137+ :: (FromJSON schema, ToJSON schema)
138+ => (BaseURI -> schema -> BaseURI)
141+ -> Either JSONPointerError (Scope schema, schema)
142+resolveFragment updateScope scope fragment = do
143+ urlDecoded <- first URLDecodingError
148+ JP.Pointer tokens <- first FormatError (JP.unescape urlDecoded)
149+ let acc = (toJSON (_topLevelDocument scope), _currentBaseURI scope)
150+ (schemaVal, base) <- foldM go acc tokens
151+ schema <- first SubschemaDecodingError (fromJSONEither schemaVal)
152+ pure (scope { _currentBaseURI = base }, schema)
154+ -- We have to step through the document JSON Pointer token
155+ -- by JSON Pointer token so that we can update the scope
156+ -- based on each @"id"@ we encounter.
158+ -- TODO: Do we need specialized code to skip @"id"@s such
159+ -- as property keys that aren't meant to change scope?
160+ -- Perhaps this should be added to the language agnostic
161+ -- test suite as well.
163+ -- In the meantime 'newBaseURIFromFragment' drops all keys
164+ -- from JSON objects except "id", which at least prevents
165+ -- SubschemaDecodingError in a situation where one of the
166+ -- values we step through isn't a valid schema.
167+ go :: (Value, BaseURI)
169+ -> Either JSONPointerError (Value, BaseURI)
170+ go (lastVal, baseURI) tok = do
171+ v <- first ResolutionError (JP.resolveToken tok lastVal)
172+ newBase <- newBaseURIFromFragment updateScope baseURI v
175+-- | Update the 'BaseURI' (the store of the current "id" value)
176+-- after resolving one token of a JSON Pointer and stepping into
178+newBaseURIFromFragment
180+ => (BaseURI -> schema -> BaseURI)
183+ -> Either JSONPointerError BaseURI
184+newBaseURIFromFragment updateScope baseURI v =
187+ let hmWithOnlyId = case HM.lookup idKey hm of
189+ Just idVal -> HM.singleton idKey idVal
190+ schema <- first SubschemaDecodingError (fromJSONEither (Object hmWithOnlyId))
191+ Right (updateScope baseURI schema)
79--------------------------------------------------
197--------------------------------------------------
@@ -109,7 +227,10 @@ instance Arbitrary EnumValidator where
109 Just ne -> pure (EnumValidator ne)
227 Just ne -> pure (EnumValidator ne)
111 toUnique :: [Value] -> [Value]
229 toUnique :: [Value] -> [Value]
112− toUnique = fmap UT._unOrdValue . S.toList . S.fromList . fmap UT.OrdValue
230+ toUnique = fmap UT._unOrdValue
114data EnumInvalid
235data EnumInvalid
115 = EnumInvalid EnumValidator Value
236 = EnumInvalid EnumValidator Value
@@ -117,9 +238,9 @@ data EnumInvalid
118enumVal :: EnumValidator -> Value -> Maybe EnumInvalid
239enumVal :: EnumValidator -> Value -> Maybe EnumInvalid
119enumVal a@(EnumValidator vs) x
240enumVal a@(EnumValidator vs) x
120− | not (UT.allUniqueValues' vs) = Nothing
241+ | not (UT.allUniqueValues vs) = Nothing
121− | x `elem` vs = Nothing
242+ | x `elem` vs = Nothing
122− | otherwise = Just $ EnumInvalid a x
243+ | otherwise = Just $ EnumInvalid a x
124--------------------------------------------------
245--------------------------------------------------
@@ -137,10 +258,25 @@ instance FromJSON TypeContext where
137 TypeContext <$> o .: "type"
258 TypeContext <$> o .: "type"
139data TypeValidator
260data TypeValidator
140− = TypeValidatorString Text
261+ = TypeValidatorString SchemaType
141− | TypeValidatorArray (Set Text)
262+ | TypeValidatorArray (Set SchemaType)
142 deriving (Eq, Show)
263 deriving (Eq, Show)
265+instance Semigroup TypeValidator where
270+ | otherwise = TypeValidatorArray (setFromTypeValidator x
272+ setFromTypeValidator y)
274+ isEmpty :: TypeValidator -> Bool
275+ isEmpty (TypeValidatorString _) = False
276+ isEmpty (TypeValidatorArray ts) = Set.null ts
278+ stimes = stimesIdempotent
144instance FromJSON TypeValidator where
280instance FromJSON TypeValidator where
145 parseJSON v = fmap TypeValidatorString (parseJSON v)
281 parseJSON v = fmap TypeValidatorString (parseJSON v)
146 <|> fmap TypeValidatorArray (parseJSON v)
282 <|> fmap TypeValidatorArray (parseJSON v)
@@ -150,40 +286,63 @@ instance ToJSON TypeValidator where
150 toJSON (TypeValidatorArray ts) = toJSON ts
286 toJSON (TypeValidatorArray ts) = toJSON ts
152instance Arbitrary TypeValidator where
288instance Arbitrary TypeValidator where
153− arbitrary = oneof [ TypeValidatorString <$> UT.arbitraryText
289+ arbitrary = oneof [ TypeValidatorString <$> arbitrary
154− , TypeValidatorArray <$> UT.arbitrarySetOfText
290+ , TypeValidatorArray <$> arbitrary
301+ deriving (Eq, Ord, Show, Bounded, Enum, Generic)
303+instance FromJSON SchemaType where
304+ parseJSON = genericParseJSON
306+ { constructorTagModifier = fmap toLower . drop 6 }
308+instance ToJSON SchemaType where
309+ toJSON = genericToJSON
311+ { constructorTagModifier = fmap toLower . drop 6 }
313+instance Arbitrary SchemaType where
314+ arbitrary = arbitraryBoundedEnum
157data TypeValidatorInvalid
316data TypeValidatorInvalid
158 = TypeValidatorInvalid TypeValidator Value
317 = TypeValidatorInvalid TypeValidator Value
159 deriving (Eq, Show)
318 deriving (Eq, Show)
161typeVal :: TypeContext -> Value -> Maybe TypeValidatorInvalid
320typeVal :: TypeContext -> Value -> Maybe TypeValidatorInvalid
162typeVal (TypeContext tv) x
321typeVal (TypeContext tv) x
163− | S.null matches = Just (TypeValidatorInvalid tv x)
322+ | Set.null matches = Just (TypeValidatorInvalid tv x)
164− | otherwise = Nothing
323+ | otherwise = Nothing
166 -- There can be more than one match because a 'Value' can be both a
325 -- There can be more than one match because a 'Value' can be both a
167 -- @"number"@ and an @"integer"@.
326 -- @"number"@ and an @"integer"@.
168− matches :: Set Text
327+ matches :: Set SchemaType
169− matches = S.intersection okTypes (setFromTypeValidator tv)
328+ matches = Set.intersection okTypes (setFromTypeValidator tv)
171− okTypes :: Set Text
330+ okTypes :: Set SchemaType
172 okTypes =
331 okTypes =
173 case x of
332 case x of
174− Null -> S.singleton "null"
333+ Null -> Set.singleton SchemaNull
175− (Array _) -> S.singleton "array"
334+ (Array _) -> Set.singleton SchemaArray
176− (Bool _) -> S.singleton "boolean"
335+ (Bool _) -> Set.singleton SchemaBoolean
177− (Object _) -> S.singleton "object"
336+ (Object _) -> Set.singleton SchemaObject
178− (String _) -> S.singleton "string"
337+ (String _) -> Set.singleton SchemaString
179 (Number y) ->
338 (Number y) ->
180 if SCI.isInteger y
339 if SCI.isInteger y
181− then S.fromList ["number", "integer"]
340+ then Set.fromList [SchemaNumber, SchemaInteger]
182− else S.singleton "number"
341+ else Set.singleton SchemaNumber
184-- | Internal.
343-- | Internal.
185−setFromTypeValidator :: TypeValidator -> Set Text
344+setFromTypeValidator :: TypeValidator -> Set SchemaType
186−setFromTypeValidator (TypeValidatorString t) = S.singleton t
345+setFromTypeValidator (TypeValidatorString t) = Set.singleton t
187setFromTypeValidator (TypeValidatorArray ts) = ts
346setFromTypeValidator (TypeValidatorArray ts) = ts
189--------------------------------------------------
348--------------------------------------------------
@@ -240,8 +399,7 @@ anyOfVal
241 -> Maybe (AnyOfInvalid err)
400 -> Maybe (AnyOfInvalid err)
242anyOfVal f (AnyOf subSchemas) x
401anyOfVal f (AnyOf subSchemas) x
243− -- Replace with @null@ once we drop GHC 7.8:
402+ | any (null . snd) perhapsFailures = Nothing
244− | any (((==) 0 . length) . snd) perhapsFailures = Nothing
245 | otherwise = AnyOfInvalid <$> NE.nonEmpty failures
403 | otherwise = AnyOfInvalid <$> NE.nonEmpty failures
247 perhapsFailures :: [(JP.Index, [err])]
405 perhapsFailures :: [(JP.Index, [err])]