git.iko.soy/typeable/hjsonschema
on master help sign in
Switch to separate JSON Pointer lib; other changes.

+ Switch to a separate JSON Pointer lib + Add helper function to validate schemas (as opposed to data) + Improve tests

commit6d8a402a5ee3b21a56d3bf7acf85a03845adf782 authorIan Grant Jeffries <ian@housejeffries.com> dateSun 22 Feb 2015, 21:01 (12 years ago) parent7e63fa8
11 files changed+263−117
README.md+6−6 draft4.json+150 hjsonschema.cabal+20−15 src/Data/JsonSchema.hs+27−9 src/Data/JsonSchema/Core.hs+2−2 src/Data/JsonSchema/JsonReference.hs → src/Data/JsonSchema/Reference.hs+7−36 src/Data/JsonSchema/Utils.hs−2 src/Data/JsonSchema/Validators.hs+10−11 tests/Lib.hs+35−30 tests/Local.hs+3−3 tests/Remote.hs+3−3
README.md+6−6
@@ -1,12 +1,10 @@
11# Intro
22
3−An implementation of [JSON Schema](http://json-schema.org/) v4 in haskell.
3+An implementation of [JSON Schema](http://json-schema.org/) Draft 4 in haskell.
44
55# Status
66
7−Still in development. Its handling of the `id` keyword passes tests, but is probably incorrect.
8−
9−Also lacks documentation and example code.
7+Still in development. Lacks documentation and solid code to fetch remote schemas.
108
119# Install Tests
1210
@@ -23,10 +21,12 @@ Note that the tests require an internet connection.
2321
2422# Notes
2523
26−This uses the [regexpr](https://hackage.haskell.org/package/regexpr-0.5.4) regular expression library fo the "pattern" validator. I have no idea if this is compatible with the ECMA 262 regex dialect, which the [spec](http://json-schema.org/latest/json-schema-validation.html#anchor33) requires.
24++ This uses the [regexpr](https://hackage.haskell.org/package/regexpr-0.5.4) regular expression library fo the "pattern" validator. I have no idea if this is compatible with the ECMA 262 regex dialect, which the [spec](http://json-schema.org/latest/json-schema-validation.html#anchor33) requires.
25+
26++ `draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema).
2727
2828# Credits
2929
3030Thanks to [Julian Berman](https://github.com/Julian) for the fantastic test suite.
3131
32−Also thanks to Tim Baumann for his [aeson-schema](https://hackage.haskell.org/package/aeson-schema) library. Hjsonschema is based on Aeson-Schema, and some code is directly from there.
32+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 come from Aeson-Schema.
draft4.jsonnew file+150
@@ -0,0 +1,150 @@
1+{
2+ "id": "http://json-schema.org/draft-04/schema#",
3+ "$schema": "http://json-schema.org/draft-04/schema#",
4+ "description": "Core schema meta-schema",
5+ "definitions": {
6+ "schemaArray": {
7+ "type": "array",
8+ "minItems": 1,
9+ "items": { "$ref": "#" }
10+ },
11+ "positiveInteger": {
12+ "type": "integer",
13+ "minimum": 0
14+ },
15+ "positiveIntegerDefault0": {
16+ "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
17+ },
18+ "simpleTypes": {
19+ "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
20+ },
21+ "stringArray": {
22+ "type": "array",
23+ "items": { "type": "string" },
24+ "minItems": 1,
25+ "uniqueItems": true
26+ }
27+ },
28+ "type": "object",
29+ "properties": {
30+ "id": {
31+ "type": "string",
32+ "format": "uri"
33+ },
34+ "$schema": {
35+ "type": "string",
36+ "format": "uri"
37+ },
38+ "title": {
39+ "type": "string"
40+ },
41+ "description": {
42+ "type": "string"
43+ },
44+ "default": {},
45+ "multipleOf": {
46+ "type": "number",
47+ "minimum": 0,
48+ "exclusiveMinimum": true
49+ },
50+ "maximum": {
51+ "type": "number"
52+ },
53+ "exclusiveMaximum": {
54+ "type": "boolean",
55+ "default": false
56+ },
57+ "minimum": {
58+ "type": "number"
59+ },
60+ "exclusiveMinimum": {
61+ "type": "boolean",
62+ "default": false
63+ },
64+ "maxLength": { "$ref": "#/definitions/positiveInteger" },
65+ "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
66+ "pattern": {
67+ "type": "string",
68+ "format": "regex"
69+ },
70+ "additionalItems": {
71+ "anyOf": [
72+ { "type": "boolean" },
73+ { "$ref": "#" }
74+ ],
75+ "default": {}
76+ },
77+ "items": {
78+ "anyOf": [
79+ { "$ref": "#" },
80+ { "$ref": "#/definitions/schemaArray" }
81+ ],
82+ "default": {}
83+ },
84+ "maxItems": { "$ref": "#/definitions/positiveInteger" },
85+ "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
86+ "uniqueItems": {
87+ "type": "boolean",
88+ "default": false
89+ },
90+ "maxProperties": { "$ref": "#/definitions/positiveInteger" },
91+ "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
92+ "required": { "$ref": "#/definitions/stringArray" },
93+ "additionalProperties": {
94+ "anyOf": [
95+ { "type": "boolean" },
96+ { "$ref": "#" }
97+ ],
98+ "default": {}
99+ },
100+ "definitions": {
101+ "type": "object",
102+ "additionalProperties": { "$ref": "#" },
103+ "default": {}
104+ },
105+ "properties": {
106+ "type": "object",
107+ "additionalProperties": { "$ref": "#" },
108+ "default": {}
109+ },
110+ "patternProperties": {
111+ "type": "object",
112+ "additionalProperties": { "$ref": "#" },
113+ "default": {}
114+ },
115+ "dependencies": {
116+ "type": "object",
117+ "additionalProperties": {
118+ "anyOf": [
119+ { "$ref": "#" },
120+ { "$ref": "#/definitions/stringArray" }
121+ ]
122+ }
123+ },
124+ "enum": {
125+ "type": "array",
126+ "minItems": 1,
127+ "uniqueItems": true
128+ },
129+ "type": {
130+ "anyOf": [
131+ { "$ref": "#/definitions/simpleTypes" },
132+ {
133+ "type": "array",
134+ "items": { "$ref": "#/definitions/simpleTypes" },
135+ "minItems": 1,
136+ "uniqueItems": true
137+ }
138+ ]
139+ },
140+ "allOf": { "$ref": "#/definitions/schemaArray" },
141+ "anyOf": { "$ref": "#/definitions/schemaArray" },
142+ "oneOf": { "$ref": "#/definitions/schemaArray" },
143+ "not": { "$ref": "#" }
144+ },
145+ "dependencies": {
146+ "exclusiveMaximum": [ "maximum" ],
147+ "exclusiveMinimum": [ "minimum" ]
148+ },
149+ "default": {}
150+}
hjsonschema.cabal+20−15
@@ -1,6 +1,6 @@
11name: hjsonschema
2−version: 0.3.0.0
3−synopsis: Haskell implementation of JSON Schema v4.
2+version: 0.4.0.0
3+synopsis: Haskell implementation of JSON Schema Draft 4.
44homepage: https://github.com/seagreen/hjsonschema
55license: MIT
66license-file: MIT-LICENSE.txt
@@ -9,31 +9,36 @@ maintainer: ian@housejeffries.com
99category: Data
1010build-type: Simple
1111cabal-version: >=1.10
12−extra-source-files: README.md
12+extra-source-files: draft4.json
1313 JSON-Schema-Test-Suite/tests/draft4/*.json
14+ README.md
1415 tests/Lib.hs
1516
1617library
1718 hs-source-dirs: src
1819 exposed-modules: Data.JsonSchema
19− , Data.JsonSchema.JsonReference
20+ , Data.JsonSchema.Reference
2021 , Data.JsonSchema.Utils
2122 , Data.JsonSchema.Validators
2223 other-modules: Data.JsonSchema.Core
2324 default-language: Haskell2010
2425 default-extensions: OverloadedStrings
26+ other-extensions: TemplateHaskell
2527 ghc-options: -Wall
26− build-depends: aeson >= 0.8 && < 0.9
27− , base >= 4.7 && < 4.8
28− , bytestring >= 0.10 && < 0.11
29− , hashable >= 1.2 && < 1.3
30− , lens >= 4.7 && < 4.8
31− , regexpr >= 0.5 && < 0.6
32− , scientific >= 0.3 && < 0.4
33− , unordered-containers >= 0.2 && < 0.3
34− , text >= 1.2 && < 1.3
35− , vector >= 0.10 && < 0.11
36− , wreq >= 0.3 && < 0.4
28+ build-depends: aeson >= 0.8 && < 0.9
29+ , base >= 4.7 && < 4.8
30+ , bytestring >= 0.10 && < 0.11
31+ , file-embed >= 0.0.8 && < 0.0.9
32+ , hashable >= 1.2 && < 1.3
33+ , hjsonpointer >= 0.1 && < 0.2
34+ , http-types >= 0.8 && < 0.9
35+ , lens >= 4.7 && < 4.8
36+ , regexpr >= 0.5 && < 0.6
37+ , scientific >= 0.3 && < 0.4
38+ , unordered-containers >= 0.2 && < 0.3
39+ , text >= 1.2 && < 1.3
40+ , vector >= 0.10 && < 0.11
41+ , wreq >= 0.3 && < 0.4
3742
3843test-suite local
3944 type: exitcode-stdio-1.0
src/Data/JsonSchema.hs+27−9
@@ -1,21 +1,26 @@
1+{-# LANGUAGE TemplateHaskell #-}
2+
13module Data.JsonSchema
24 ( module Data.JsonSchema
35 , module Data.JsonSchema.Core
46 ) where
57
68import Control.Applicative
9+import Data.Aeson
10+import Data.ByteString.Lazy (fromStrict)
11+import Data.FileEmbed
712import Data.Foldable
8−import qualified Data.HashMap.Strict as H
13+import qualified Data.HashMap.Strict as H
914import Data.JsonSchema.Core
10−import Data.JsonSchema.JsonReference
15+import Data.JsonSchema.Reference
1116import Data.JsonSchema.Utils
1217import Data.JsonSchema.Validators
1318import Data.Maybe
14−import Data.Text (Text)
15−import qualified Data.Text as T
16−import Data.Vector (Vector)
17−import qualified Data.Vector as V
18−import Prelude hiding (foldr)
19+import Data.Text (Text)
20+import qualified Data.Text as T
21+import Data.Vector (Vector)
22+import qualified Data.Vector as V
23+import Prelude hiding (foldr)
1924
2025draft4 :: Spec
2126draft4 = Spec $ H.fromList
@@ -56,14 +61,14 @@ fetchRefs spec a graph =
5661 -- TODO: optimize
5762 includeSubschemas :: RawSchema -> Vector RawSchema
5863 includeSubschemas r =
59− let newId = updateId (_rsURI r) (_rsObject r)
64+ let newId = newResolutionScope (_rsURI r) (_rsObject r)
6065 xs = H.intersectionWith (\(_,f) x -> f newId x) (_unSpec spec) (_rsObject r)
6166 ys = V.concat . H.elems $ xs
6267 in V.cons r . V.concat . V.toList $ includeSubschemas <$> ys
6368
6469 fetch :: Either Text Graph -> RawSchema -> IO (Either Text Graph)
6570 fetch (Right g) r =
66− case H.lookup "$ref" (_rsObject r) >>= toTxt >>= refAndP of
71+ case H.lookup "$ref" (_rsObject r) >>= toTxt >>= refAndPointer of
6772 Nothing -> return (Right g)
6873 Just (s, _) ->
6974 let t = (_rsURI r `combineIdAndRef` s)
@@ -75,3 +80,16 @@ fetchRefs spec a graph =
7580 Left e -> return (Left e)
7681 Right obj -> fetchRefs spec (RawSchema t obj) g
7782 fetch leftGraph _ = return leftGraph
83+
84+-- | Check if a schema is valid or not. Just a helper function
85+-- built out of the JSON Schema document which describes valid
86+-- draft 4 schemas and the normal 'compile' and 'validate' functions.
87+isValidSchema :: RawSchema -> Vector ValErr
88+isValidSchema r =
89+ case decode . fromStrict $ $(embedFile "draft4.json") of
90+ Nothing -> V.singleton "Schema decode failed (this should never happen)"
91+ Just s -> do
92+ let draft4Schema = RawSchema { _rsURI = "", _rsObject = s }
93+ validate
94+ (compile draft4 H.empty draft4Schema)
95+ (Object . _rsObject $ r)
src/Data/JsonSchema/Core.hs+2−2
@@ -3,7 +3,7 @@ module Data.JsonSchema.Core where
33import Data.Aeson
44import Data.HashMap.Strict (HashMap)
55import qualified Data.HashMap.Strict as H
6−import Data.JsonSchema.JsonReference
6+import Data.JsonSchema.Reference
77import Data.Maybe
88import Data.Text (Text)
99import Data.Vector (Vector)
@@ -34,7 +34,7 @@ compile spec g (RawSchema t o) =
3434 V.fromList . catMaybes . H.elems $ H.intersectionWith f (_unSpec spec) o
3535 where
3636 f :: (ValidatorGen, a) -> Value -> Maybe Validator
37− f (vGen,_) = vGen spec g $ RawSchema (updateId t o) o
37+ f (vGen,_) = vGen spec g $ RawSchema (newResolutionScope t o) o
3838
3939validate :: Schema -> Value -> Vector ValErr
4040validate s x = s >>= ($ x)
src/Data/JsonSchema/JsonReference.hs → src/Data/JsonSchema/Reference.hsrenamed+7−36
@@ -1,6 +1,4 @@
1−-- | There should be a JSON Reference library for haskell.
2−
3−module Data.JsonSchema.JsonReference where
1+module Data.JsonSchema.Reference where
42
53import Control.Exception
64import Control.Lens
@@ -12,10 +10,8 @@ import qualified Data.HashMap.Strict as H
1210import Data.Monoid
1311import Data.Text (Text)
1412import qualified Data.Text as T
15−import qualified Data.Vector as V
1613import Network.Wreq
1714import Prelude hiding (foldr)
18−import Text.Read (readMaybe)
1915
2016combineIdAndRef :: Text -> Text -> Text
2117combineIdAndRef a b
@@ -32,14 +28,14 @@ combineIds a b
3228 | T.last a == '#' && T.head b == '#' = a <> T.tail b
3329 | otherwise = a <> b
3430
35−updateId :: Text -> HashMap Text Value -> Text
36−updateId t o =
31+newResolutionScope :: Text -> HashMap Text Value -> Text
32+newResolutionScope t o =
3733 case H.lookup "id" o of
38− Just (String idVal) -> t `combineIds` idVal
39− _ -> t
34+ Just (String idKeyword) -> t `combineIds` idKeyword
35+ _ -> t
4036
41−refAndP :: Text -> Maybe (Text, Text)
42−refAndP val = getParts $ T.splitOn "#" val
37+refAndPointer :: Text -> Maybe (Text, Text)
38+refAndPointer val = getParts $ T.splitOn "#" val
4339 where
4440 getParts :: [Text] -> Maybe (Text, Text)
4541 getParts [] = Just ("","")
@@ -67,28 +63,3 @@ safeGet url =
6763 where
6864 handler :: SomeException -> IO (Either Text ByteString)
6965 handler e = return . Left . T.pack . show $ e
70−
71−-- | There should be a JSON Pointer library.
72−--
73−-- Spec: http://tools.ietf.org/html/draft-ietf-appsawg-json-pointer-07
74−jsonPointer :: Text -> Value -> Maybe Value
75−jsonPointer pntr = resolve (T.splitOn "/" pntr)
76− where
77− resolve :: [Text] -> Value -> Maybe Value
78− resolve (referenceToken:ts) a =
79− let t = unescape referenceToken
80− in case T.length t of
81− 0 -> resolve ts a
82− _ ->
83− case a of
84− (Object b) -> H.lookup t b >>= resolve ts
85− (Array c) -> do
86− n <- readMaybe (T.unpack t)
87− when (n < 0 || n + 1 > V.length c) Nothing
88− resolve ts (c V.! n)
89− _ -> Nothing
90− resolve _ a = Just a
91−
92− -- TODO: do more things need to be escaped?
93− unescape :: Text -> Text
94− unescape t = T.replace "%25" "%" $ T.replace "~0" "~" $ T.replace "~1" "/" t
src/Data/JsonSchema/Utils.hs−2
@@ -1,5 +1,3 @@
1−{-# LANGUAGE OverloadedLists #-}
2−
31module Data.JsonSchema.Utils where
42
53import Data.Aeson
src/Data/JsonSchema/Validators.hs+10−11
@@ -39,16 +39,19 @@ import Data.Fixed (mod')
3939import Data.Hashable
4040import Data.HashMap.Strict (HashMap)
4141import qualified Data.HashMap.Strict as H
42+import Data.JsonPointer
4243import Data.JsonSchema.Core
43−import Data.JsonSchema.JsonReference
44+import Data.JsonSchema.Reference
4445import Data.JsonSchema.Utils
4546import Data.Maybe
4647import Data.Monoid
4748import Data.Text (Text)
4849import qualified Data.Text as T
50+import Data.Text.Encoding
4951import Data.Traversable
5052import Data.Vector (Vector)
5153import qualified Data.Vector as V
54+import Network.HTTP.Types.URI
5255import Text.RegexPR
5356
5457--------------------------------------------------
@@ -513,17 +516,13 @@ notValidator _ _ _ _ = Nothing
513516-- ignored.
514517ref :: ValidatorGen
515518ref spec g s (String val) = do
516− (reference, pointer) <- refAndP (_rsURI s `combineIdAndRef` val)
519+ (reference, pointer) <- refAndPointer (_rsURI s `combineIdAndRef` val)
517520 r <- RawSchema reference <$> H.lookup reference g
518− sc <- pointerToSchema pointer r
519− Just $ validate (compile spec g sc)
520− where
521− pointerToSchema :: Text -> RawSchema -> Maybe RawSchema
522− pointerToSchema pntr rawS = do
523− mVal <- jsonPointer pntr (Object . _rsObject $ rawS)
524− case mVal of
525− (Object o) -> Just $ RawSchema (_rsURI rawS) o
526− _ -> Nothing
521+ let p = decodeUtf8 . urlDecode True . encodeUtf8 $ pointer
522+ case jsonPointer p >>= resolvePointer (Object $ _rsObject r) of
523+ Right (Object o) ->
524+ Just $ validate $ compile spec g $ RawSchema (_rsURI r) o
525+ _ -> Nothing
527526ref _ _ _ _ = Nothing
528527
529528noVal :: ValidatorGen
tests/Lib.hs+35−30
@@ -14,11 +14,17 @@ import Data.JsonSchema
1414import Data.Monoid
1515import Data.Text (Text)
1616import qualified Data.Text as T
17+import Data.Vector (Vector)
1718import qualified Data.Vector as V
1819import System.FilePath ((</>))
19−import Test.Framework
20−import Test.Framework.Providers.HUnit
21−import qualified Test.HUnit as HU
20+import Test.Framework (Test)
21+import Test.Framework.Providers.HUnit (testCase)
22+import Test.HUnit hiding (Test)
23+
24+isLocal :: String -> Bool
25+isLocal file = (file /= "definitions.json")
26+ && (file /= "ref.json")
27+ && (file /= "refRemote.json")
2228
2329data SchemaTest = SchemaTest
2430 { _stDescription :: Text
@@ -34,7 +40,7 @@ data SchemaTestCase = SchemaTestCase
3440
3541instance FromJSON RawSchema where
3642 parseJSON = withObject "Schema" $ \o ->
37− return $ RawSchema "" o
43+ return RawSchema { _rsURI = "", _rsObject = o }
3844
3945instance FromJSON SchemaTest where
4046 parseJSON = withObject "SchemaTest" $ \o -> SchemaTest
@@ -42,11 +48,6 @@ instance FromJSON SchemaTest where
4248 <*> o .: "schema"
4349 <*> o .: "tests" -- I wish this were "cases"
4450
45−isLocal :: String -> Bool
46−isLocal file = (file /= "definitions.json")
47− && (file /= "ref.json")
48− && (file /= "refRemote.json")
49−
5051readSchemaTests :: String -> [String] -> IO [SchemaTest]
5152readSchemaTests dir jsonFiles = concatMapM fileToCases jsonFiles
5253 where
@@ -68,33 +69,37 @@ readSchemaTests dir jsonFiles = concatMapM fileToCases jsonFiles
6869 concatMapM f xs = liftM concat (mapM f xs)
6970
7071toTest :: SchemaTest -> Test
71−toTest st = testGroup groupName (mkCase <$> _stCases st)
72− where
73− groupName :: String
74− groupName = T.unpack $ _stDescription st
72+toTest st =
73+ testCase (T.unpack $ _stDescription st) $ do
74+ assertEqual "schema validity errors" V.empty (isValidSchema . _stSchema $ st)
75+ forM_ (_stCases st) $ \sc -> do
76+ g <- assertRight =<< fetchRefs draft4 (_stSchema st) H.empty
77+ let es = validate (compile draft4 g $ _stSchema st) (_scData sc)
78+ if _scValid sc
79+ then assertValid sc es
80+ else assertInvalid sc es
7581
76− mkCase :: SchemaTestCase -> Test
77− mkCase sc = testCase caseName assertion
78− where
79− caseName = T.unpack $ _scDescription sc
80− assertion = if _scValid sc
81− then assertValid (_stSchema st) (_scData sc)
82− else assertInvalid (_stSchema st) (_scData sc)
82+assertValid :: SchemaTestCase -> Vector ValErr -> Assertion
83+assertValid sc es =
84+ unless (V.length es == 0) $ assertFailure $ unlines
85+ [ " Failed to validate data"
86+ , " Description: " <> T.unpack (_scDescription sc)
87+ , " Data: " <> show (_scData sc)
88+ , " Errors: " <> show es
89+ ]
8390
84−assertValid, assertInvalid :: RawSchema -> Value -> HU.Assertion
85−assertValid r v = do
86− g <- assertRight =<< fetchRefs draft4 r H.empty
87− let es = validate (compile draft4 g r) v
88− unless (V.length es == 0) $ HU.assertFailure (show es)
89−assertInvalid r v = do
90− g <- assertRight =<< fetchRefs draft4 r H.empty
91− let es = validate (compile draft4 g r) v
92− when (V.length es == 0) $ HU.assertFailure "expected a validation error"
91+assertInvalid :: SchemaTestCase -> Vector ValErr -> Assertion
92+assertInvalid sc es =
93+ unless (V.length es > 0) $ assertFailure $ unlines
94+ [ " Failed to invalidate data"
95+ , " Description: " <> T.unpack (_scDescription sc)
96+ , " Data: " <> show (_scData sc)
97+ ]
9398
9499assertRight :: (Show a) => Either a b -> IO b
95100assertRight a =
96101 case a of
97− Left e -> HU.assertFailure (show e) >> fail "assertRight failed"
102+ Left e -> assertFailure (show e) >> fail "assertRight failed"
98103 Right b -> return b
99104
100105$(deriveFromJSON defaultOptions { fieldLabelModifier = map toLower . drop 3 } ''SchemaTestCase)
tests/Local.hs+3−3
@@ -8,9 +8,9 @@ import Test.Framework
88
99main :: IO ()
1010main = do
11− files <- filter isLocal . filter (".json" `isSuffixOf`)
12− <$> getDirectoryContents dir
13− ts <- readSchemaTests dir files
11+ filenames <- filter isLocal . filter (".json" `isSuffixOf`)
12+ <$> getDirectoryContents dir
13+ ts <- readSchemaTests dir filenames
1414 defaultMain (toTest <$> ts)
1515
1616 where
tests/Remote.hs+3−3
@@ -8,9 +8,9 @@ import Test.Framework
88
99main :: IO ()
1010main = do
11− files <- filter (not . isLocal) . filter (".json" `isSuffixOf`)
12− <$> getDirectoryContents dir
13− ts <- readSchemaTests dir files
11+ filenames <- filter (not . isLocal) . filter (".json" `isSuffixOf`)
12+ <$> getDirectoryContents dir
13+ ts <- readSchemaTests dir filenames
1414 defaultMain (toTest <$> ts)
1515
1616 where