git.iko.soy/typeable/hjsonschema
on master help sign in
0.8. See changelog.txt.
commit2369d30a499917bdfa396a006ceb4846caf98fae authorIan Grant Jeffries <ian@housejeffries.com> dateMon 19 Oct 2015, 00:03 (11 years ago) parentfce6aa7
16 files changed+314−207
Example.hs+70−42 README.md+41−13 changelog.txt+11 hjsonschema.cabal+19−21 logo.jpg src/Data/JsonSchema.hs+43−37 src/Data/JsonSchema/Core.hs+20−13 src/Data/JsonSchema/Draft4.hs+4−2 src/Data/JsonSchema/Draft4/Any.hs+14−17 src/Data/JsonSchema/Draft4/Arrays.hs+17−8 src/Data/JsonSchema/Draft4/Numbers.hs+2−2 src/Data/JsonSchema/Draft4/Objects.hs+1−1 src/Data/JsonSchema/Draft4/Objects/Properties.hs+7−10 src/Data/JsonSchema/Helpers.hs+7−7 src/Data/JsonSchema/Reference.hs+56−30 tests/Lib.hs+2−4
Example.hs+70−42
@@ -13,14 +13,24 @@ import qualified Data.Vector as V
1313
1414import qualified Data.JsonSchema as JS
1515
16−-- Data.JsonSchema isn't designed to be imported qualified,
17−-- but we're doing it here to make it obvious what's coming
18−-- from the lib.
16+-- Data.JsonSchema is designed to be imported unqualified.
17+-- We're just being explicit here for example purposes.
18+
19+main :: IO ()
20+main = do
21+ simpleExample
22+ fullExample
23+
24+--------------------------------------------------
25+-- * Example Schema and Data
26+--------------------------------------------------
1927
2028schemaData :: JS.RawSchema
2129schemaData = JS.RawSchema
22− { JS._rsURI = ""
23− , JS._rsObject = schemaJSON
30+ { JS._rsURI = Nothing
31+ -- ^ If your schema has relative references to other schemas
32+ -- then you'll need to include this.
33+ , JS._rsData = schemaJSON
2434 }
2535 where
2636 schemaJSON :: HashMap Text Value
@@ -29,47 +39,65 @@ schemaData = JS.RawSchema
2939badData :: Value
3040badData = Array $ V.fromList ["foo", "foo"]
3141
32−main :: IO ()
33−main = do
42+--------------------------------------------------
43+-- * Simple Example
44+--------------------------------------------------
45+
46+-- | Since our schema contains no references to other schemas,
47+-- we can skip the fetchGraph step.
48+simpleExample :: IO ()
49+simpleExample = do
50+ schema <- compileSchema (JS.SchemaGraph schemaData H.empty) schemaData
51+ checkResults (JS.validate schema badData)
52+
53+--------------------------------------------------
54+-- * Full Example
55+--------------------------------------------------
3456
57+-- | For a schema that might contain references to other schemas.
58+fullExample :: IO ()
59+fullExample = do
3560 let currentSchemaCache = H.empty
36− newSchemaCache <- (<> currentSchemaCache) <$> fetchGraph schemaData
61+ schemaGraph <- fetchGraph currentSchemaCache schemaData
3762
38− schema <- compileSchema newSchemaCache schemaData
63+ -- We aren't going to use it for anything this time, but if we were caching
64+ -- remote schemas this would be our new cache.
65+ --
66+ -- let newSchemaCache = _cachedSchemas schemaData <> currentSchemaGraph
67+
68+ schema <- compileSchema schemaGraph schemaData
3969 checkResults (JS.validate schema badData)
4070
4171 where
42−
43− -- Not necessary in this case, but it serves as an example.
44− --
45− -- Note that Graphs are used to handle internal as well as external references.
46− -- So even if a schema doesn't reference outside documents, you still need
47− -- to generate a real Graph (and not just use mempty) if it refences itself.
48− fetchGraph :: JS.RawSchema -> IO JS.Graph
49− fetchGraph rs = do
50− eitherGraph <- JS.fetchReferencedSchemas JS.draft4 rs H.empty
72+ fetchGraph :: HashMap Text (HashMap Text Value) -> JS.RawSchema -> IO JS.SchemaGraph
73+ fetchGraph graph rs = do
74+ eitherGraph <- JS.fetchReferencedSchemas JS.draft4 graph rs
5175 case eitherGraph of
52− Left e -> error $ "Failed to fetch graph with error: " <> T.unpack e
53− Right graph -> return graph
54−
55− compileSchema :: JS.Graph -> JS.RawSchema -> IO (JS.Schema JS.Draft4Failure)
56− compileSchema graph rs =
57− case JS.compileDraft4 graph rs of
58− Left failure -> error $ "Not a valid schema: " <> show failure
59− Right schema -> return schema
60−
61− checkResults :: [JS.ValidationFailure JS.Draft4Failure] -> IO ()
62− checkResults [] = error "OHNO we validated bad data!"
63− checkResults [JS.ValidationFailure JS.UniqueItems (JS.FailureInfo x y)] =
64− putStrLn . unlines $
65− [ ""
66− , "Success. We got a UniqueItems error as expected."
67− , "Here's the relevant part of the bad JSON document:"
68− , ""
69− , " " <> show y
70− , ""
71− , "And here's the content of the validator that caught it:"
72− , ""
73− , " " <> show x
74− ]
75− checkResults x = error $ "OHNO we got a different failure than we expected: " <> show x
76+ Left e -> error $ "Failed to fetch graph with error: " <> T.unpack e
77+ Right g -> return g
78+
79+--------------------------------------------------
80+-- * Helper Functions
81+--------------------------------------------------
82+
83+compileSchema :: JS.SchemaGraph -> JS.RawSchema -> IO (JS.Schema JS.Draft4Failure)
84+compileSchema graph rs =
85+ case JS.compileDraft4 graph rs of
86+ Left failure -> error $ "Not a valid schema: " <> show failure
87+ Right schema -> return schema
88+
89+checkResults :: [JS.ValidationFailure JS.Draft4Failure] -> IO ()
90+checkResults [] = error "OHNO we validated bad data!"
91+checkResults [JS.ValidationFailure JS.UniqueItems (JS.FailureInfo x y)] =
92+ putStrLn . unlines $
93+ [ ""
94+ , "Success. We got a UniqueItems error as expected."
95+ , "Here's the relevant part of the bad JSON document:"
96+ , ""
97+ , " " <> show y
98+ , ""
99+ , "And here's the content of the validator that caught it:"
100+ , ""
101+ , " " <> show x
102+ ]
103+checkResults x = error $ "OHNO we got a different failure than we expected: " <> show x
README.md+41−13
@@ -1,27 +1,55 @@
1−# Intro
1+![hjsonschema logo](./logo.jpg)
22
3−An implementation of [JSON Schema](http://json-schema.org/) Draft 4 in haskell.
3+A Haskell implementation of the current [JSON Schema](http://json-schema.org/) specification (Draft 4).
44
5−# Status
5+[Hackage](https://hackage.haskell.org/package/hjsonschema) / [GitHub](https://github.com/seagreen/hjsonschema) / [Travis CI](https://travis-ci.org/seagreen/hjsonschema)
66
7−Still in development. Lacks solid code to handle changing resolution scope.
7+# Example
88
9−Also note that hjsonschema uses the [regexpr](https://hackage.haskell.org/package/regexpr-0.5.4) regular expression library for the "pattern" validator. This isn't compatible with the ECMA 262 regex dialect, which the [spec](http://json-schema.org/latest/json-schema-validation.html#anchor33) requires.
9+See [Example.hs on GitHub](https://github.com/seagreen/hjsonschema/blob/master/Example.hs).
1010
11−# Example
11+# Tests
12+
13+## Install
14+
15+`git submodule update --init`
16+
17+## Run
18+
19+Will run self-contained:
20+
21+`cabal test local`
22+
23+Will start an HTTP server temporarily on port 1234:
1224
13−See [Example.hs](./Example.hs).
25+`cabal test remote`
1426
15−# Install Tests
27+# Details
1628
17− git submodule update --init
29+## Goals
1830
19−# Notes
31++ Be a correct and fast implementation of the spec.
32+
33++ Be a useful reference for implementers in other languages. Haskell's high level nature, expressive type system and referential transparency suit this purpose well.
34+
35+## Good Parts
36+
37++ Passes all the tests in the [language agnostic test suite](https://github.com/json-schema/JSON-Schema-Test-Suite).
38+
39++ Very modular, which should make it easy to support future versions of the specification.
40+
41+## Bad Parts
42+
43++ Uses the [regexpr](https://hackage.haskell.org/package/regexpr-0.5.4) regular expression library for the "pattern" validator. It should use a library based on the ECMA 262 regex dialect, which the [spec](http://json-schema.org/latest/json-schema-validation.html#anchor33) requires.
44+
45+## Notes
2046
2147+ `draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema).
2248
23−# Credits
49+## Credits
50+
51+[TJ Weigel](http://tjweigel.com/) created the logo.
2452
25−Thanks to [Julian Berman](https://github.com/Julian) for the fantastic test suite.
53+[Tim Baumann](https://github.com/timjb) wrote [aeson-schema](https://hackage.haskell.org/package/aeson-schema), on which hjsonschema's test code and its implementation of `SchemaGraph` were based.
2654
27−Also thanks to Tim Baumann for his [aeson-schema](https://hackage.haskell.org/package/aeson-schema) library. Hjsonschema's test code and its implementation of `Graph` both originally came from there.
55+[Julian Berman](https://github.com/Julian) maintains the fantastic [language agnostic test suite](https://github.com/json-schema/JSON-Schema-Test-Suite).
changelog.txt+11
@@ -1,3 +1,14 @@
1+# 0.8
2+
3++ Improve scope updating and resolving.
4++ Rename RawSchema's _rsObject field to _rsData.
5++ Make RawSchema's _rsURI field a Maybe. This way schemas without a starting
6+URI can say so explicitly with Nothing instead of with "".
7++ Rename Graph to SchemaGraph. Declare it with data instead of type. Give it a
8+field referencing the starting schema. This field is used to find the curent
9+schema if no URI is in scope and a self-referencing $ref is found (e.g. "#").
10++ Change the order of the last two arguments to fetchReferencedSchemas.
11+
112# 0.7.1
213
314+ Support GHC 7.8 again.
hjsonschema.cabal+19−21
@@ -1,5 +1,5 @@
11name: hjsonschema
2−version: 0.7.1.0
2+version: 0.8.0.0
33synopsis: JSON Schema library
44homepage: https://github.com/seagreen/hjsonschema
55license: MIT
@@ -35,7 +35,7 @@ library
3535 OverloadedStrings
3636 other-extensions: TemplateHaskell
3737 ghc-options: -Wall
38− build-depends: aeson >= 0.7 && < 0.10
38+ build-depends: aeson >= 0.7 && < 0.11
3939 -- 4.6 doesn't have a Traversable Either instance:
4040 , base >= 4.7 && < 4.9
4141 , bytestring >= 0.10 && < 0.11
@@ -44,7 +44,7 @@ library
4444 , hashable >= 1.2 && < 1.3
4545 , hjsonpointer >= 0.2 && < 0.3
4646 , http-client >= 0.4.9 && < 0.5
47− , http-types >= 0.8 && < 0.9
47+ , http-types >= 0.8 && < 0.10
4848 , mtl >= 2.2.1 && < 2.3
4949 , regexpr >= 0.5 && < 0.6
5050 , scientific >= 0.3 && < 0.4
@@ -53,17 +53,6 @@ library
5353 , text >= 1.2 && < 1.3
5454 , vector >= 0.10 && < 0.12
5555
56−executable example
57− main-is: Example.hs
58− default-language: Haskell2010
59− ghc-options: -Wall
60− build-depends: aeson
61− , base
62− , hjsonschema
63− , text
64− , unordered-containers
65− , vector
66−
6756test-suite local
6857 type: exitcode-stdio-1.0
6958 hs-source-dirs: tests
@@ -76,7 +65,6 @@ test-suite local
7665 , base
7766 , bytestring
7867 , hjsonschema
79− , unordered-containers
8068 , text
8169 , vector
8270 , directory >= 1.2 && < 1.3
@@ -98,17 +86,27 @@ test-suite remote
9886 , base
9987 , bytestring
10088 , hjsonschema
101− , unordered-containers
10289 , text
10390 , vector
104− , directory >= 1.2 && < 1.3
105− , filepath >= 1.3 && < 1.4
106− , HUnit >= 1.2 && < 1.3
107− , test-framework >= 0.8 && < 0.9
108− , test-framework-hunit >= 0.3 && < 0.4
91+ , directory
92+ , filepath
93+ , HUnit
94+ , test-framework
95+ , test-framework-hunit
10996 , wai-app-static
11097 , warp
11198
99+executable example
100+ main-is: Example.hs
101+ default-language: Haskell2010
102+ ghc-options: -Wall
103+ build-depends: aeson
104+ , base
105+ , hjsonschema
106+ , text
107+ , unordered-containers
108+ , vector
109+
112110source-repository head
113111 type: git
114112 location: git://github.com/seagreen/hjsonschema.git
logo.jpgnew file

Binary file not shown.

src/Data/JsonSchema.hs+43−37
@@ -8,14 +8,11 @@ module Data.JsonSchema
88 , module Data.JsonSchema.Draft4
99 ) where
1010
11−
1211import Control.Monad.Except (MonadError, MonadIO, join, throwError)
1312import qualified Data.ByteString.Lazy as LBS
1413import Data.Foldable
1514import qualified Data.HashMap.Strict as H
1615import Data.String
17−import qualified Data.Text as T
18−import qualified Data.Vector as V
1916
2017import Data.JsonSchema.Core
2118import Data.JsonSchema.Draft4
@@ -24,57 +21,66 @@ import Data.JsonSchema.Reference
2421import Import
2522
2623-- For GHCs before 7.10:
27−import Prelude hiding (sequence)
24+import Prelude hiding (concat, sequence)
2825
2926-- | Take a schema. Retrieve every document either it or its
30−-- subschemas include via the "$ref" keyword. Load a 'Graph' out
27+-- subschemas include via the "$ref" keyword. Load a 'SchemaGraph' out
3128-- with them.
32−--
33−-- TODO: This function's URL processing is hacky and needs improvement.
34−fetchReferencedSchemas :: Spec err -> RawSchema -> Graph -> IO (Either Text Graph)
29+fetchReferencedSchemas :: Spec err -> SchemaCache -> RawSchema -> IO (Either Text SchemaGraph)
3530fetchReferencedSchemas = fetchReferencedSchemas' HE.defaultFetch
3631
3732-- | A version of fetchReferencedSchemas where the function to make requests
3833-- is provided by the user. This allows restrictions to be added, e.g. rejecting
39−-- non-local URLs.
34+-- non-local URIs.
4035fetchReferencedSchemas'
4136 -- The `Functor m` declaration is for GHCs before 7.10.
4237 :: forall m e t err. (MonadIO m, Functor m, MonadError t e, Traversable e, IsString t)
4338 => (Text -> m (e LBS.ByteString))
4439 -> Spec err
40+ -> SchemaCache
4541 -> RawSchema
46− -> Graph
47− -> m (e Graph)
48−fetchReferencedSchemas' fetchRef spec rawSchema graph =
49− let startingGraph = H.insert (_rsURI rawSchema) (_rsObject rawSchema) graph
50− in foldlM (modFetch fetch) (return startingGraph) (includeSubschemas rawSchema)
42+ -> m (e SchemaGraph)
43+fetchReferencedSchemas' fetchRef spec cache rawSchema = do
44+ let startingCache = case _rsURI rawSchema of
45+ Nothing -> cache
46+ Just uri -> H.insert uri (_rsData rawSchema) cache
47+ fmap (SchemaGraph rawSchema) <$> foldlM fetch (return startingCache) (includeSubschemas rawSchema)
5148
5249 where
53− includeSubschemas :: RawSchema -> Vector RawSchema
50+ -- TODO: use a fold here
51+ includeSubschemas :: RawSchema -> [RawSchema]
5452 includeSubschemas r =
55− let newId = newResolutionScope (_rsURI r) (_rsObject r)
56− xs = H.intersectionWith (\(ValSpec f _) x -> f newId x) (_unSpec spec) (_rsObject r)
57− ys = V.concat . H.elems $ xs -- TODO: optimize
58− in V.cons r . V.concat . V.toList $ includeSubschemas <$> ys
59−
60− modFetch :: (Graph -> RawSchema -> m (e Graph)) -> e Graph -> RawSchema -> m (e Graph)
61− modFetch f eg rs = join <$> sequence (flip f rs <$> eg)
53+ let scope = _rsURI r `newResolutionScope` _rsData r
54+ xs = H.intersectionWith (\(ValSpec f _) x -> f scope x) (_unSpec spec) (_rsData r)
55+ in r : (concat . concat . H.elems $ fmap includeSubschemas <$> xs)
6256
63− fetch :: Graph -> RawSchema -> m (e Graph)
64− fetch g r =
65− case H.lookup "$ref" (_rsObject r) >>= HE.toTxt >>= refAndPointer of
66− Nothing -> return (return g)
67− Just (s, _) ->
68− let url = (_rsURI r `combineIdAndRef` s)
69− in if T.length url <= 0 || H.member url g || not ("://" `T.isInfixOf` url)
70− then return (return g)
71− else modDec (decodeResponse url) =<< fetchRef url
57+ fetch :: e SchemaCache -> RawSchema -> m (e SchemaCache)
58+ fetch eg r = join <$> sequence (run <$> eg)
7259 where
73− decodeResponse :: Text -> LBS.ByteString -> m (e Graph)
74− decodeResponse url bts =
60+ run :: SchemaCache -> m (e SchemaCache)
61+ run g = do
62+ -- Resolving the new scope is necessary here because of situations like this:
63+ --
64+ -- {
65+ -- "id": "http://localhost:1234/",
66+ -- "items": {
67+ -- "id": "folder/",
68+ -- "items": {"$ref": "folderInteger.json"}
69+ -- }
70+ -- }
71+ let scope = newResolutionScope (_rsURI r) (_rsData r)
72+ case resolveReference scope <$> (H.lookup "$ref" (_rsData r) >>= HE.toTxt) of
73+ Just (Just uri,_) ->
74+ if not (isRemoteReference uri) || H.member uri g
75+ then return (return g)
76+ else fetchRef uri >>= decodeResponse g uri
77+ _ -> return (return g)
78+
79+ decodeResponse :: SchemaCache -> Text -> e LBS.ByteString -> m (e SchemaCache)
80+ decodeResponse g uri eBts = join <$> sequence (runDecode g uri <$> eBts)
81+
82+ runDecode :: SchemaCache -> Text -> LBS.ByteString -> m (e SchemaCache)
83+ runDecode g uri bts =
7584 case eitherDecode bts of
7685 Left e -> return $ throwError (fromString e)
77− Right obj -> fetchReferencedSchemas' fetchRef spec (RawSchema url obj) g
78−
79− modDec :: (LBS.ByteString -> m (e Graph)) -> e LBS.ByteString -> m (e Graph)
80− modDec f eBts = join <$> sequence (f <$> eBts)
86+ Right obj -> fmap _cachedSchemas <$> fetchReferencedSchemas' fetchRef spec g (RawSchema (Just uri) obj)
src/Data/JsonSchema/Core.hs+20−13
@@ -15,7 +15,7 @@ import Prelude hiding (concat, sequence)
1515-- * Primary API
1616--------------------------------------------------
1717
18−compile :: forall err. Spec err -> Graph -> RawSchema -> Schema err
18+compile :: forall err. Spec err -> SchemaGraph -> RawSchema -> Schema err
1919compile spec g (RawSchema t o) =
2020 let maybeValidators = H.intersectionWith f (_unSpec spec) o
2121 in Schema . catMaybes . H.elems $ maybeValidators
@@ -35,14 +35,21 @@ newtype Spec err = Spec { _unSpec :: HashMap Text (ValSpec err) }
3535newtype Schema err = Schema { _unSchema :: [Value -> [ValidationFailure err]] }
3636
3737data RawSchema = RawSchema
38− { _rsURI :: Text
39− , _rsObject :: HashMap Text Value
40− }
38+ { _rsURI :: !(Maybe Text)
39+ -- ^ NOTE: Must not include a URI fragment.
40+ , _rsData :: !(HashMap Text Value)
41+ } deriving (Show)
4142
42−-- | A mapping of URLs to schemas.
43+-- | A set of RawSchemas, split into a HashMap.
4344--
44−-- Each key/value pair provides the components of a RawSchema.
45−type Graph = HashMap Text (HashMap Text Value)
45+-- Keys correspond to Schema _rsURIs. Values correspond to Schema _rsObjects.
46+type SchemaCache = HashMap Text (HashMap Text Value)
47+
48+data SchemaGraph = SchemaGraph
49+ { _startingSchema :: !RawSchema
50+ -- ^ The schema initially referenced by '#'.
51+ , _cachedSchemas :: !SchemaCache
52+ } deriving (Show)
4653
4754--------------------------------------------------
4855-- * Validators
@@ -55,7 +62,7 @@ data ValSpec err = ValSpec EmbeddedSchemas (ValidatorConstructor err [Validation
5562-- This is used by 'Data.JsonSchema.fetchRefs' to find all the
5663-- subschemas in a document. This allows it to process only
5764-- "$ref"s and "id"s that are actual schema keywords.
58−type EmbeddedSchemas = Text -> Value -> Vector RawSchema
65+type EmbeddedSchemas = Maybe Text -> Value -> [RawSchema]
5966
6067-- | This is what's used to write most validators in practice.
6168--
@@ -68,17 +75,17 @@ type EmbeddedSchemas = Text -> Value -> Vector RawSchema
6875-- and modifyName for this purpose.
6976type ValidatorConstructor schemaErr valErr
7077 = Spec schemaErr
71− -> Graph
78+ -> SchemaGraph
7279 -> RawSchema
7380 -> Value
7481 -> Maybe (Value -> valErr)
7582
7683data ValidationFailure err = ValidationFailure
77− { _failureName :: err
78− , _failureInfo :: FailureInfo
84+ { _failureName :: !err
85+ , _failureInfo :: !FailureInfo
7986 } deriving (Show)
8087
8188data FailureInfo = FailureInfo
82− { _validatingData :: Value
83− , _offendingData :: Value
89+ { _validatingData :: !Value
90+ , _offendingData :: !Value
8491 } deriving (Show)
src/Data/JsonSchema/Draft4.hs+4−2
@@ -123,7 +123,9 @@ isValidSchema :: RawSchema -> [ValidationFailure Draft4Failure]
123123isValidSchema r =
124124 case decode . LBS.fromStrict $ $(embedFile "draft4.json") of
125125 Nothing -> error "Schema decode failed (this should never happen)"
126− Just s -> validate (compile draft4 H.empty $ RawSchema "" s) $ Object (_rsObject r)
126+ Just s ->
127+ let a = RawSchema Nothing s
128+ in validate (compile draft4 (SchemaGraph a mempty) a) $ Object (_rsData r)
127129
128130-- | Check that a 'RawSchema' conforms to the JSON Schema Draft 4
129131-- master schema document. Compile it if it does.
@@ -133,7 +135,7 @@ isValidSchema r =
133135--
134136-- NOTE: It's not actually required to run 'isValidSchema' on
135137-- prospective draft 4 schemas at all.
136−compileDraft4 :: Graph -> RawSchema -> Either [ValidationFailure Draft4Failure] (Schema Draft4Failure)
138+compileDraft4 :: SchemaGraph -> RawSchema -> Either [ValidationFailure Draft4Failure] (Schema Draft4Failure)
137139compileDraft4 g r =
138140 case isValidSchema r of
139141 [] -> Right (compile draft4 g r)
src/Data/JsonSchema/Draft4/Any.hs+14−17
@@ -3,11 +3,9 @@ module Data.JsonSchema.Draft4.Any where
33
44import Control.Monad
55import qualified Data.HashMap.Strict as H
6−import Data.JsonPointer
6+import Data.Maybe
77import Data.Scientific
8−import Data.Text.Encoding
98import qualified Data.Vector as V
10−import Network.HTTP.Types.URI
119
1210import Data.JsonSchema.Core
1311import Data.JsonSchema.Helpers
@@ -93,26 +91,25 @@ oneOf _ _ _ _ = Nothing
9391
9492notValidator :: ValidatorConstructor err [FailureInfo]
9593notValidator spec g s val@(Object o) = do
96− let sub = compile spec g (RawSchema (_rsURI s) o)
94+ let subSchema = compile spec g $ RawSchema (_rsURI s) o
9795 Just $ \x ->
98− case validate sub x of
96+ case validate subSchema x of
9997 [] -> pure (FailureInfo val x)
10098 _ -> mempty
10199notValidator _ _ _ _ = Nothing
102100
103−-- http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03
101+-- JSON Reference Draft Document:
104102--
105−-- TODO: Any members other than "$ref" in a JSON Reference object SHALL be
106−-- ignored.
103+-- http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03
107104ref :: ValidatorConstructor err [ValidationFailure err]
108105ref spec g s (String val) = do
109− (reference, pointer) <- refAndPointer (_rsURI s `combineIdAndRef` val)
110− r <- RawSchema reference <$> H.lookup reference g
111− let urlDecoded = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
112− p <- eitherToMaybe $ jsonPointer urlDecoded
113− case resolvePointer p (Object $ _rsObject r) of
114− Right (Object o) ->
115− let compiled = compile spec g $ RawSchema (_rsURI r) o
116− in Just $ validate compiled
117− _ -> Nothing
106+ let (mUri, mFragment) = resolveReference (_rsURI s) val
107+ r <- RawSchema mUri <$> getReference mUri
108+ let o = resolveFragment mFragment (_rsData r)
109+ return . validate . compile spec g $ RawSchema (_rsURI r) o
110+ where
111+ getReference :: Maybe Text -> Maybe (HashMap Text Value)
112+ getReference Nothing = Just . _rsData . _startingSchema $ g
113+ getReference (Just t) = H.lookup t (_cachedSchemas g)
114+
118115ref _ _ _ _ = Nothing
src/Data/JsonSchema/Draft4/Arrays.hs+17−8
@@ -9,7 +9,14 @@ import Data.JsonSchema.Core
99import Data.JsonSchema.Helpers
1010import Import
1111
12−data ItemsFailure err = Items err | AdditionalItemsBool | AdditionalItemsObject err
12+data ItemsFailure err
13+ = Items err
14+ | AdditionalItemsBool
15+ | AdditionalItemsObject err
16+
17+data AdditionalItemsFailure err
18+ = AdditionalBool
19+ | AdditionalObject err
1320
1421-- | A combination of items and additionalItems.
1522items :: ValidatorConstructor err [ValidationFailure (ItemsFailure err)]
@@ -22,36 +29,38 @@ items spec g s (Object o) =
2229items spec g s (Array vs) = do
2330 os <- traverse toObj vs
2431 let subSchemas = compile spec g . RawSchema (_rsURI s) <$> V.toList os
25− mAdditionalItems = additionalItems spec g s =<< H.lookup "additionalItems" (_rsObject s)
32+ mAdditionalItems = additionalItems spec g s =<< H.lookup "additionalItems" (_rsData s)
2633 Just $ \x ->
2734 case x of
2835 Array ys ->
2936 let extras = V.drop (V.length os) ys
3037 itemFailures = join $ fmap (modifyFailureName Items) <$> zipWith validate subSchemas (V.toList ys)
3138 additionalItemFailures = runMaybeVal mAdditionalItems (Array extras)
32− in itemFailures <> additionalItemFailures
39+ in itemFailures <> fmap (modifyFailureName f) additionalItemFailures
3340 _ -> mempty
41+ where
42+ f :: AdditionalItemsFailure err -> ItemsFailure err
43+ f AdditionalBool = AdditionalItemsBool
44+ f (AdditionalObject err) = AdditionalItemsObject err
3445items _ _ _ _ = Nothing
3546
3647-- | Not included directly in the 'draft4' spec hashmap because it always
3748-- validates data unless 'items' is also present. This is because 'items'
3849-- defaults to {}.
39−--
40−-- TODO: Should have its own error type instead of sharing with Items.
41−additionalItems :: ValidatorConstructor err [ValidationFailure (ItemsFailure err)]
50+additionalItems :: ValidatorConstructor err [ValidationFailure (AdditionalItemsFailure err)]
4251additionalItems _ _ _ val@(Bool v) =
4352 Just $ \x ->
4453 case x of
4554 Array ys ->
4655 if not v && V.length ys > 0
47− then pure $ ValidationFailure AdditionalItemsBool (FailureInfo val x)
56+ then pure $ ValidationFailure AdditionalBool (FailureInfo val x)
4857 else mempty
4958 _ -> mempty
5059additionalItems spec g s (Object o) =
5160 let subSchema = compile spec g (RawSchema (_rsURI s) o)
5261 in Just $ \x ->
5362 case x of
54− Array ys -> V.toList ys >>= fmap (modifyFailureName AdditionalItemsObject) . validate subSchema
63+ Array ys -> V.toList ys >>= fmap (modifyFailureName AdditionalObject) . validate subSchema
5564 _ -> mempty
5665additionalItems _ _ _ _ = Nothing
5766
src/Data/JsonSchema/Draft4/Numbers.hs+2−2
@@ -37,7 +37,7 @@ maximumVal _ _ s val@(Number n) =
3737 where
3838 checkExclusive :: (Scientific -> Scientific -> Bool, MaximumFailure)
3939 checkExclusive =
40− case H.lookup "exclusiveMaximum" (_rsObject s) of
40+ case H.lookup "exclusiveMaximum" (_rsData s) of
4141 Just (Bool a) -> if a then ((>=), ExclusiveMaximum) else ((>), Maximum)
4242 _ -> ((>), Maximum)
4343maximumVal _ _ _ _ = Nothing
@@ -55,7 +55,7 @@ minimumVal _ _ s val@(Number n) =
5555 where
5656 checkExclusive :: (Scientific -> Scientific -> Bool, MinimumFailure)
5757 checkExclusive =
58− case H.lookup "exclusiveMinimum" (_rsObject s) of
58+ case H.lookup "exclusiveMinimum" (_rsData s) of
5959 Just (Bool a) -> if a then ((<=), ExclusiveMinimum) else ((<), Minimum)
6060 _ -> ((<), Minimum)
6161minimumVal _ _ _ _ = Nothing
src/Data/JsonSchema/Draft4/Objects.hs+1−1
@@ -85,7 +85,7 @@ dependencies spec g s val@(Object o) = do
8585 in schemaFailures <> propertyFailures
8686 _ -> mempty
8787 where
88− toSchemaDep :: Spec a -> Graph -> (Text, Value) -> [(Text, Schema a)]
88+ toSchemaDep :: Spec a -> SchemaGraph -> (Text, Value) -> [(Text, Schema a)]
8989 toSchemaDep spc gr (t, Object ob) = pure (t, compile spc gr $ RawSchema (_rsURI s) ob)
9090 toSchemaDep _ _ _ = mempty
9191
src/Data/JsonSchema/Draft4/Objects/Properties.hs+7−10
@@ -30,8 +30,8 @@ data AdditionalPropertiesFailure err
3030properties :: forall err. ValidatorConstructor err [ValidationFailure (PropertiesFailure err)]
3131properties spec g s val = do
3232 let mProps = propertiesUnmatched val
33− mPatProp = patternUnmatched spec g s =<< H.lookup "patternProperties" (_rsObject s)
34− mAddProp = runAdditionalProperties spec g s =<< H.lookup "additionalProperties" (_rsObject s)
33+ mPatProp = patternUnmatched spec g s =<< H.lookup "patternProperties" (_rsData s)
34+ mAddProp = runAdditionalProperties spec g s =<< H.lookup "additionalProperties" (_rsData s)
3535 when (isNothing mProps && isNothing mPatProp && isNothing mAddProp) Nothing
3636 Just $ \x ->
3737 case x of
@@ -61,12 +61,9 @@ properties spec g s val = do
6161
6262patternProperties :: ValidatorConstructor err [ValidationFailure (PatternPropertiesFailure err)]
6363patternProperties spec g s val = do
64− when (H.member "properties" (_rsObject s)) Nothing
64+ when (H.member "properties" (_rsData s)) Nothing
6565 let mPatternProps = patternUnmatched spec g s val
66− -- TODO: checking additionalProperties as well doesn't help with tests.
67− -- Make sure we're doing the correct thing, then get tests for this
68− -- merged into the test suite.
69− let mAdditionalProps = runAdditionalProperties spec g s =<< H.lookup "additionalProperties" (_rsObject s)
66+ let mAdditionalProps = runAdditionalProperties spec g s =<< H.lookup "additionalProperties" (_rsData s)
7067 when (isNothing mPatternProps && isNothing mAdditionalProps) Nothing
7168 Just $ \x ->
7269 case x of
@@ -78,7 +75,7 @@ patternProperties spec g s val = do
7875
7976patternUnmatched
8077 :: Spec err
81− -> Graph
78+ -> SchemaGraph
8279 -> RawSchema
8380 -> Value
8481 -> Maybe (Value -> ([ValidationFailure err], Value))
@@ -122,8 +119,8 @@ patternUnmatched _ _ _ _ = Nothing
122119
123120additionalProperties :: ValidatorConstructor err [ValidationFailure (AdditionalPropertiesFailure err)]
124121additionalProperties spec g s val = do
125− when (H.member "properties" (_rsObject s)) Nothing
126− when (H.member "patternProperties" (_rsObject s)) Nothing
122+ when (H.member "properties" (_rsData s)) Nothing
123+ when (H.member "patternProperties" (_rsData s)) Nothing
127124 runAdditionalProperties spec g s val
128125
129126-- | An additionalProperties validator than never disables itself.
src/Data/JsonSchema/Helpers.hs+7−7
@@ -1,3 +1,4 @@
1+
12module Data.JsonSchema.Helpers where
23
34import Control.Exception
@@ -24,7 +25,7 @@ objEmbed t (Object o) = pure (RawSchema t o)
2425objEmbed _ _ = mempty
2526
2627arrayEmbed :: EmbeddedSchemas
27−arrayEmbed t (Array vs) = objEmbed t =<< vs
28+arrayEmbed t (Array vs) = objEmbed t =<< V.toList vs
2829arrayEmbed _ _ = mempty
2930
3031objOrArrayEmbed :: EmbeddedSchemas
@@ -33,28 +34,27 @@ objOrArrayEmbed t v@(Array _) = arrayEmbed t v
3334objOrArrayEmbed _ _ = mempty
3435
3536objMembersEmbed :: EmbeddedSchemas
36−objMembersEmbed t (Object o) = objEmbed t =<< V.fromList (H.elems o)
37+objMembersEmbed t (Object o) = objEmbed t =<< H.elems o
3738objMembersEmbed _ _ = mempty
3839
3940--------------------------------------------------
4041-- * Modify Validators for use in Specs
4142--------------------------------------------------
4243
43−-- | TODO: Is there something easier to replace these fmaps with?
4444giveName
4545 :: forall err. err
4646 -> ValidatorConstructor err [FailureInfo]
4747 -> ValidatorConstructor err [ValidationFailure err]
48−giveName err = (fmap.fmap.fmap.fmap.fmap.fmap.fmap) (ValidationFailure err)
48+giveName err f spec g rs v = (fmap.fmap) (ValidationFailure err) <$> f spec g rs v
4949
5050modifyName
5151 :: forall valErr schemaErr. (valErr -> schemaErr)
5252 -> ValidatorConstructor schemaErr [ValidationFailure valErr]
5353 -> ValidatorConstructor schemaErr [ValidationFailure schemaErr]
54−modifyName failureHandler = (fmap.fmap.fmap.fmap.fmap.fmap.fmap) f
54+modifyName failureHandler f spec g rs v = (fmap.fmap) modErr <$> f spec g rs v
5555 where
56− f :: ValidationFailure valErr -> ValidationFailure schemaErr
57− f (ValidationFailure a b) = ValidationFailure (failureHandler a) b
56+ modErr :: ValidationFailure valErr -> ValidationFailure schemaErr
57+ modErr (ValidationFailure a b) = ValidationFailure (failureHandler a) b
5858
5959-- | It's important to know if an object's a validator (even if it will never run,
6060-- like the definitions validator) because parts of it might be referenced by other
src/Data/JsonSchema/Reference.hs+56−30
@@ -1,39 +1,65 @@
1−-- | TODO: The code handling resolution scope updates
2−-- is hacky and needs improvement.
31
42module Data.JsonSchema.Reference where
53
6−import qualified Data.HashMap.Strict as H
7−import qualified Data.Text as T
4+import qualified Data.HashMap.Strict as H
5+import Data.JsonPointer
6+import qualified Data.Text as T
7+import Data.Text.Encoding
8+import Network.HTTP.Types.URI
89
910import Import
1011
11−combineIdAndRef :: Text -> Text -> Text
12−combineIdAndRef a b
13− | "://" `T.isInfixOf` b = b
14− | T.length a < 1 || T.length b < 1 = a <> b
15− | T.last a == '#' && T.head b == '#' = a <> T.tail b
16− | otherwise = a <> b
17−
18−combineIds :: Text -> Text -> Text
19−combineIds a b
20− | b == "#" || b == "" = a
21− | "://" `T.isInfixOf` b = b
22− | T.length a < 1 || T.length b < 1 = a <> b
23− | T.last a == '#' && T.head b == '#' = a <> T.tail b
24− | otherwise = a <> b
25−
26−newResolutionScope :: Text -> HashMap Text Value -> Text
27−newResolutionScope t o =
12+type URIBase = Maybe Text
13+type URIBaseAndFragment = (Maybe Text, Maybe Text)
14+
15+newResolutionScope :: URIBase -> HashMap Text Value -> URIBase
16+newResolutionScope mScope o =
2817 case H.lookup "id" o of
29− Just (String idKeyword) -> t `combineIds` idKeyword
30− _ -> t
18+ Just (String t) -> fst . baseAndFragment $ resolveScopeAgainst mScope t
19+ _ -> mScope
20+
21+resolveReference :: URIBase -> Text -> URIBaseAndFragment
22+resolveReference mScope t = baseAndFragment $ resolveScopeAgainst mScope t
23+
24+isRemoteReference :: Text -> Bool
25+isRemoteReference uri = "://" `T.isInfixOf` uri
26+
27+resolveFragment :: Maybe Text -> HashMap Text Value -> HashMap Text Value
28+resolveFragment Nothing hm = hm
29+resolveFragment (Just pointer) hm =
30+ let urlDecoded = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
31+ in case jsonPointer urlDecoded of
32+ Left _ -> hm
33+ Right p ->
34+ case resolvePointer p (Object hm) of
35+ Right (Object hm') -> hm'
36+ _ -> hm
37+
38+--------------------------------------------------
39+-- * Internal
40+--------------------------------------------------
41+
42+baseAndFragment :: Text -> URIBaseAndFragment
43+baseAndFragment = f . T.splitOn "#"
44+ where
45+ f :: [Text] -> URIBaseAndFragment
46+ f [x] = (g x, Nothing)
47+ f [x,y] = (g x, g y)
48+ f _ = (Nothing, Nothing)
49+
50+ g "" = Nothing
51+ g x = Just x
3152
32−refAndPointer :: Text -> Maybe (Text, Text)
33−refAndPointer val = getParts $ T.splitOn "#" val
53+resolveScopeAgainst :: Maybe Text -> Text -> Text
54+resolveScopeAgainst Nothing t = t
55+resolveScopeAgainst (Just scope) t
56+ | isRemoteReference t = t
57+ | otherwise = smartAppend
3458 where
35− getParts :: [Text] -> Maybe (Text, Text)
36− getParts [] = Just ("","")
37− getParts [x] = Just (x,"")
38− getParts [x,y] = Just (x,y)
39− getParts _ = Nothing
59+ -- There shouldn't be a fragment at the end of a scope URI,
60+ -- but just in case a user leaves one in we want to be sure
61+ -- to cut it off before appending.
62+ smartAppend :: Text
63+ smartAppend = case baseAndFragment scope of
64+ (Just base,_) -> base <> t
65+ _ -> t
tests/Lib.hs+2−4
@@ -9,7 +9,6 @@ import Data.Aeson
99import Data.Aeson.TH
1010import qualified Data.ByteString.Lazy as LBS
1111import Data.Char (toLower)
12−import qualified Data.HashMap.Strict as H
1312import Data.JsonSchema
1413import Data.Monoid
1514import Data.Text (Text)
@@ -37,8 +36,7 @@ data SchemaTestCase = SchemaTestCase
3736 }
3837
3938instance FromJSON RawSchema where
40− parseJSON = withObject "Schema" $ \o ->
41− return RawSchema { _rsURI = "", _rsObject = o }
39+ parseJSON = withObject "Schema" $ return . RawSchema Nothing
4240
4341instance FromJSON SchemaTest where
4442 parseJSON = withObject "SchemaTest" $ \o -> SchemaTest
@@ -71,7 +69,7 @@ toTest st =
7169 testCase (T.unpack $ _stDescription st) $ do
7270 sanityCheckTest (_stSchema st)
7371 forM_ (_stCases st) $ \sc -> do
74− g <- assertRight =<< fetchReferencedSchemas draft4 (_stSchema st) H.empty
72+ g <- assertRight =<< fetchReferencedSchemas draft4 mempty (_stSchema st)
7573 let res = validate (compile draft4 g $ _stSchema st) (_scData sc)
7674 if _scValid sc
7775 then assertValid sc res