hjsonschema 0.9.0.0 → 0.10.0.0
raw patch · 36 files changed
+1080/−797 lines, 36 filesdep +pcre-heavydep −regexprdep ~aesondep ~basedep ~file-embed
Dependencies added: pcre-heavy
Dependencies removed: regexpr
Dependency ranges changed: aeson, base, file-embed, filepath, hjsonpointer, http-client, semigroups, text, unordered-containers, vector
Files
- JSON-Schema-Test-Suite/remotes/integer.json +3/−0
- JSON-Schema-Test-Suite/remotes/subSchemas.json +8/−0
- README.md +11/−11
- changelog.txt +10/−0
- draft4.json +0/−150
- examples/CustomSchema.hs +13/−8
- examples/Full.hs +34/−0
- examples/Main.hs +0/−19
- examples/PrettyShowFailure.hs +24/−18
- examples/Simple.hs +39/−0
- examples/Standard.hs +0/−52
- hjsonschema.cabal +25/−29
- src/Data/JsonSchema/Draft4.hs +102/−36
- src/Data/JsonSchema/Draft4/Failure.hs +1/−16
- src/Data/JsonSchema/Draft4/Internal.hs +57/−70
- src/Data/JsonSchema/Fetch.hs +124/−84
- src/Data/Validator/Draft4/Any.hs +7/−8
- src/Data/Validator/Draft4/Array.hs +25/−25
- src/Data/Validator/Draft4/Number.hs +7/−7
- src/Data/Validator/Draft4/Object.hs +10/−10
- src/Data/Validator/Draft4/Object/Properties.hs +30/−27
- src/Data/Validator/Draft4/String.hs +9/−6
- src/Data/Validator/Failure.hs +7/−7
- src/Data/Validator/Reference.hs +37/−13
- src/draft4.json +150/−0
- test/Local.hs +54/−0
- test/Local/Failure.hs +55/−0
- test/Local/Filesystem.hs +56/−0
- test/Local/Reference.hs +51/−0
- test/Local/schema-with-ref.json +3/−0
- test/Local/schema.json +3/−0
- test/Remote.hs +26/−0
- test/Shared.hs +99/−0
- tests/Local.hs +0/−76
- tests/Remote.hs +0/−26
- tests/Shared.hs +0/−99
+ JSON-Schema-Test-Suite/remotes/integer.json view
@@ -0,0 +1,3 @@+{+ "type": "integer"+}
+ JSON-Schema-Test-Suite/remotes/subSchemas.json view
@@ -0,0 +1,8 @@+{+ "integer": {+ "type": "integer"+ }, + "refToInteger": {+ "$ref": "#/integer"+ }+}
README.md view
@@ -8,23 +8,21 @@ # Example -See [here](https://github.com/seagreen/hjsonschema/blob/master/examples/Example.hs).+See [here](https://github.com/seagreen/hjsonschema/blob/master/examples/Simple.hs). # Tests -## Install--`git submodule update --init`+Run all tests: -## Run+`stack test` -Will run self-contained:+Run only local tests: -`cabal test local`+`stack test hjsonschema:local` -Will start an HTTP server temporarily on port 1234:+Run remote tests (makes GETs to json-schema.org, also temporarily starts an HTTP server on port 1234): -`cabal test remote`+`stack test hjsonschema:remote` # Details @@ -42,11 +40,13 @@ ## Bad Parts -+ 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.++ Uses the [pcre-heavy](https://hackage.haskell.org/package/pcre-heavy) 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. ## Notes -+ `draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema).++ `JSON-Schema-Test-Suite` is vendored from commit # aabcb3427745ade7a0b4d49ff016ad7eda8b898b [here](https://github.com/json-schema-org/JSON-Schema-Test-Suite).+++ `src/draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema). ## Credits
changelog.txt view
@@ -1,3 +1,13 @@+# 0.10+++ Rewrite fetching internals.++ Fix reference resolution defects, add more tests.++ Switch to a Perl style regex library, which is closer to ECMAScript regexes+than the previous Posix style one.++ Add one-step validation functions ('fetchFilesystemAndValidate' and 'fetchHTTPAndValidate').++ Alias the validation failure type exported by 'Data.JsonSchema.Draft4' to+'Invalid', change its field names.+ # 0.9 + Partial rewrite. The API of the library has changed, see the examples
− draft4.json
@@ -1,150 +0,0 @@-{- "id": "http://json-schema.org/draft-04/schema#",- "$schema": "http://json-schema.org/draft-04/schema#",- "description": "Core schema meta-schema",- "definitions": {- "schemaArray": {- "type": "array",- "minItems": 1,- "items": { "$ref": "#" }- },- "positiveInteger": {- "type": "integer",- "minimum": 0- },- "positiveIntegerDefault0": {- "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]- },- "simpleTypes": {- "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]- },- "stringArray": {- "type": "array",- "items": { "type": "string" },- "minItems": 1,- "uniqueItems": true- }- },- "type": "object",- "properties": {- "id": {- "type": "string",- "format": "uri"- },- "$schema": {- "type": "string",- "format": "uri"- },- "title": {- "type": "string"- },- "description": {- "type": "string"- },- "default": {},- "multipleOf": {- "type": "number",- "minimum": 0,- "exclusiveMinimum": true- },- "maximum": {- "type": "number"- },- "exclusiveMaximum": {- "type": "boolean",- "default": false- },- "minimum": {- "type": "number"- },- "exclusiveMinimum": {- "type": "boolean",- "default": false- },- "maxLength": { "$ref": "#/definitions/positiveInteger" },- "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },- "pattern": {- "type": "string",- "format": "regex"- },- "additionalItems": {- "anyOf": [- { "type": "boolean" },- { "$ref": "#" }- ],- "default": {}- },- "items": {- "anyOf": [- { "$ref": "#" },- { "$ref": "#/definitions/schemaArray" }- ],- "default": {}- },- "maxItems": { "$ref": "#/definitions/positiveInteger" },- "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },- "uniqueItems": {- "type": "boolean",- "default": false- },- "maxProperties": { "$ref": "#/definitions/positiveInteger" },- "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },- "required": { "$ref": "#/definitions/stringArray" },- "additionalProperties": {- "anyOf": [- { "type": "boolean" },- { "$ref": "#" }- ],- "default": {}- },- "definitions": {- "type": "object",- "additionalProperties": { "$ref": "#" },- "default": {}- },- "properties": {- "type": "object",- "additionalProperties": { "$ref": "#" },- "default": {}- },- "patternProperties": {- "type": "object",- "additionalProperties": { "$ref": "#" },- "default": {}- },- "dependencies": {- "type": "object",- "additionalProperties": {- "anyOf": [- { "$ref": "#" },- { "$ref": "#/definitions/stringArray" }- ]- }- },- "enum": {- "type": "array",- "minItems": 1,- "uniqueItems": true- },- "type": {- "anyOf": [- { "$ref": "#/definitions/simpleTypes" },- {- "type": "array",- "items": { "$ref": "#/definitions/simpleTypes" },- "minItems": 1,- "uniqueItems": true- }- ]- },- "allOf": { "$ref": "#/definitions/schemaArray" },- "anyOf": { "$ref": "#/definitions/schemaArray" },- "oneOf": { "$ref": "#/definitions/schemaArray" },- "not": { "$ref": "#" }- },- "dependencies": {- "exclusiveMaximum": [ "maximum" ],- "exclusiveMinimum": [ "minimum" ]- },- "default": {}-}
examples/CustomSchema.hs view
@@ -1,16 +1,21 @@-{-# LANGUAGE OverloadedStrings #-}---- | A custom schema made up of one validator from 'Data.Validator.Draft4'--- and one original validator.+-- | A custom schema specification using one validator from+-- 'Data.Validator.Draft4' and one original validator. -- -- This is a simple example because it doesn't allow references (so it -- doesn't need to define an 'embed' function @Schema -> [Schema]@ for use -- with 'fetchReferencedSchemas'.+--+-- For a full example see 'Data.JsonSchema.Draft4' and its submodules.+-- Code use between schema specifications will likely be OK but not great.+-- All the 'Data.Validator' code as well as the 'Data.JsonSchema.Fetch' code+-- is reusable, but there's a lot of boilerplate to tie it together. module CustomSchema where +import Control.Applicative import Data.Aeson import Data.Maybe (maybeToList)+import Data.Monoid import Data.Text (Text) import qualified Data.Text as T @@ -21,7 +26,7 @@ oddLength :: Bool -> Text -> Maybe (FR.Failure () ) oddLength b t | b == odd (T.length t) = Nothing- | otherwise = Just (FR.Failure () (Bool b) mempty)+ | otherwise = Just (FR.Invalid () (Bool b) mempty) data CustomError = MaxLength@@ -34,8 +39,8 @@ , _schemaOddLength :: Maybe Bool } --- | Since every 'Schema' is valid we don't need to bother defining something--- like 'Data.JsonSchema.Draft4.checkValidity' for this schema.+-- | If our 'Schema's themselves could be invalid we might want to write+-- something like 'Data.JsonSchema.Draft4.schemaValidityy' for them. validate :: Schema -> Value -> [FR.Failure CustomError] validate s (String x) = concat [ f _schemaMaxLength (FR.setFailure MaxLength) (fmap maybeToList . VA.maxLength)@@ -56,7 +61,7 @@ example = case validate schema badData of [] -> error "We validated bad data."- [FR.Failure OddLength _ _] -> putStrLn "Success."+ [FR.Invalid OddLength _ _] -> return () -- Success. _ -> error "We got a different failure than expected." where schema :: Schema
+ examples/Full.hs view
@@ -0,0 +1,34 @@+-- | Step by step validation using 'D4.referencesViaFilesystem' and+-- 'D4.checkSchema'. This means the actual validation invovles no IO.++module Full where++import Data.Aeson++import qualified Data.JsonSchema.Draft4 as D4++schema :: D4.Schema+schema = D4.emptySchema { D4._schemaRef = Just "./unique.json" }++schemaContext :: D4.SchemaWithURI D4.Schema+schemaContext = D4.SchemaWithURI+ { D4._swSchema = schema+ , D4._swURI = Just "./examples/json/imaginary.json"+ }++badData :: Value+badData = toJSON (["foo", "foo"] :: [String])++example :: IO ()+example = do+ res <- D4.referencesViaFilesystem schemaContext+ case res of+ Left _ -> error "Couldn't fetch referenced schemas."+ Right references -> do+ let validate = case D4.checkSchema references schemaContext of+ Left _ -> error "Not a valid schema."+ Right f -> f+ case validate badData of+ [] -> error "We validated bad data."+ [D4.Invalid (D4.Ref D4.UniqueItems) _ _] -> return () -- Success.+ e -> error "We got a different failure than expected."
− examples/Main.hs
@@ -1,19 +0,0 @@--module Main where--import qualified CustomSchema as C-import qualified PrettyShowFailure as P-import qualified Standard as S--main :: IO ()-main = do- putStrLn "# Standard Example"- S.example- putStrLn ""-- putStrLn "# PrettyShowFailure Example"- P.example- putStrLn ""-- putStrLn "# CustomSchema Example"- C.example
examples/PrettyShowFailure.hs view
@@ -1,4 +1,11 @@-{-# LANGUAGE OverloadedStrings #-}+-- | 'D4.Invalid's contain a JSON Pointer to the subset of the data that+-- caused validation to fail, but they don't contain that data itself.+--+-- If you want to display the invalid subset of the data here's how you+-- resolve the JSON Pointer (which has the field name+-- 'D4._invalidOffendingData') against the original data.+--+-- NOTE: You have to have hjsonpointer in your build-depends. module PrettyShowFailure where @@ -11,35 +18,34 @@ badData :: Value badData = toJSON [1, 2 :: Int] -failure :: D4.Failure-failure = D4.Failure- { D4._failureValidatorsCalled = D4.Items D4.MultipleOf- , D4._failureFinalValidator = Number 2- , D4._failureOffendingData = AP.Pointer [AP.Token "0"]+failure :: D4.Invalid+failure = D4.Invalid+ { D4._invalidValidatorsCalled = D4.Items D4.MultipleOf+ , D4._invalidFinalValidator = Number 2+ , D4._invalidOffendingData = AP.Pointer [AP.Token "0"] } -subsetOfData :: Value-subsetOfData =- -- This is the only really interesting part of this example. To resolve the- -- pointer to the part of the data that triggered the validation failure we- -- have to have hjsonpointer in our build-depends and use 'AP.resolve'.- case AP.resolve (D4._failureOffendingData failure) badData of+example :: IO ()+example =+ case AP.resolve (D4._invalidOffendingData failure) badData of Left _ -> error "Couldn't resolve pointer."- Right v -> v+ Right _ -> return () -- Success. We could feed the 'Right' value into+ -- the otherwise unused 'msg' if we wanted to+ -- display it. -example :: IO ()-example = putStrLn . unlines $+msg :: Value -> String+msg subsetOfData = unlines [ "Invalid data. Here's the sequence of validators that caught it:" , ""- , " " <> show (D4._failureValidatorsCalled failure)+ , " " <> show (D4._invalidValidatorsCalled failure) , "" , "Here's the contents of the final validator in that sequence:" , ""- , " " <> show (D4._failureFinalValidator failure)+ , " " <> show (D4._invalidFinalValidator failure) , "" , "Here's a JSON Pointer to the invalid part of the data:" , ""- , " " <> show (D4._failureOffendingData failure)+ , " " <> show (D4._invalidOffendingData failure) , "" , "Here's the invalid part of the data:" , ""
+ examples/Simple.hs view
@@ -0,0 +1,39 @@+-- | Fetch any referenced schemas, check that our original schema is itself+-- valid, then validate our data.+--+-- To fetch schemas using HTTP instead of from the filesystem use+-- 'D4.fetchHTTPAndValidate'.++module Simple where++import Data.Aeson+import qualified Data.List.NonEmpty as NE++import qualified Data.JsonSchema.Draft4 as D4++schema :: D4.Schema+schema = D4.emptySchema { D4._schemaRef = Just "./unique.json" }++schemaContext :: D4.SchemaWithURI D4.Schema+schemaContext = D4.SchemaWithURI+ { D4._swSchema = schema+ , D4._swURI = Just "./examples/json/imaginary.json"+ -- ^ For this example we're pretending we found 'schema' at this location.+ -- Its relative links will be resolved from here.+ }++badData :: Value+badData = toJSON (["foo", "foo"] :: [String])++example :: IO ()+example = do+ res <- D4.fetchFilesystemAndValidate schemaContext badData+ case res of+ Right () -> error "We validated bad data."+ Left (D4.FVRead _) -> error ("Error fetching a referenced schema"+ ++ " (either during IO or parsing).")+ Left (D4.FVSchema _) -> error "Our 'schema' itself was invalid."+ Left (D4.FVData failures) ->+ case NE.toList failures of+ [D4.Invalid (D4.Ref D4.UniqueItems) _ _] -> return () -- Success.+ _ -> error "Got more invalidation reasons than we expected."
− examples/Standard.hs
@@ -1,52 +0,0 @@-{-# LANGUAGE OverloadedStrings #-}--module Standard where--import Data.Aeson-import qualified Data.HashMap.Strict as H-import qualified Data.Vector as V--import qualified Data.JsonSchema.Draft4 as D4--schema :: D4.Schema-schema = D4.emptySchema { D4._schemaUniqueItems = Just True }--schemaContext :: D4.SchemaContext D4.Schema-schemaContext = D4.SchemaContext- { D4._scURI = Nothing- -- ^ If your schema has relative references to other schemas- -- then you'll need to give its URI here.- , D4._scSchema = schema- }--badData :: Value-badData = Array (V.fromList ["foo", "foo"])--example :: IO ()-example = do- -- Since we know our schema doesn't reference any other schemas we could- -- skip this step and use @SchemaCache schema mempty@ instead.- --- -- If we make a mistake and out schema does include references to other- -- schemas then those references will always return 'D4.RefResolution'- -- validation failures.- cache <- makeCache-- let validate = case D4.checkSchema cache schemaContext of- Left _ -> error "Not a valid schema."- Right f -> f-- case validate badData of- [] -> error "We validated bad data."- [D4.Failure D4.UniqueItems _ _] -> putStrLn "Success."- _ -> error "We got a different failure than expected."-- where- makeCache :: IO (D4.SchemaCache D4.Schema)- makeCache = do- -- A HashMap of URIs (in the form of Text) to schemas.- let currentlyStoredSchemas = H.empty- res <- D4.fetchReferencedSchemas currentlyStoredSchemas schemaContext- case res of- Left _ -> error "Couldn't fetch referenced schemas."- Right cache -> return cache
hjsonschema.cabal view
@@ -1,5 +1,5 @@ name: hjsonschema-version: 0.9.0.0+version: 0.10.0.0 synopsis: JSON Schema library homepage: https://github.com/seagreen/hjsonschema license: MIT@@ -11,10 +11,11 @@ cabal-version: >=1.10 tested-with: GHC == 7.8.4, GHC == 7.10.2 extra-source-files: changelog.txt- draft4.json+ JSON-Schema-Test-Suite/remotes/*.json JSON-Schema-Test-Suite/tests/draft4/*.json README.md- tests/Shared.hs+ src/draft4.json+ test/Local/*.json library hs-source-dirs: src@@ -44,37 +45,48 @@ , base >= 4.7 && < 4.9 , bytestring >= 0.10 && < 0.11 , containers >= 0.5 && < 0.6- , file-embed >= 0.0.8 && < 0.0.10+ , file-embed >= 0.0.8 && < 0.1+ , filepath >= 1.3 && < 1.5 , hjsonpointer >= 0.3 && < 0.4- , http-client >= 0.4.9 && < 0.5+ , http-client >= 0.4 && < 0.5 , http-types >= 0.8 && < 0.10+ , pcre-heavy >= 1.0 && < 1.1 , QuickCheck >= 2.8.1 && < 2.9- , regexpr >= 0.5 && < 0.6 , scientific >= 0.3 && < 0.4- , semigroups >= 0.18.0 && < 0.19+ , semigroups >= 0.18 && < 0.19 , unordered-containers >= 0.2 && < 0.3- , text >= 1.2 && < 1.3+ , text >= 1.1 && < 1.3 , vector >= 0.10 && < 0.12 test-suite local type: exitcode-stdio-1.0- hs-source-dirs: tests+ hs-source-dirs: test+ examples main-is: Local.hs- other-modules: Shared+ other-modules: Local.Failure+ , Local.Filesystem+ , Local.Reference+ , Shared+ -- from ./examples:+ , CustomSchema+ , Full+ , PrettyShowFailure+ , Simple default-language: Haskell2010 ghc-options: -Wall -fno-warn-orphans default-extensions: OverloadedStrings build-depends: aeson , base , bytestring+ , filepath , hjsonpointer , hjsonschema+ , semigroups , text , QuickCheck , unordered-containers , vector , directory >= 1.2 && < 1.3- , filepath >= 1.3 && < 1.5 , HUnit >= 1.2 && < 1.4 , tasty >= 0.11 && < 0.12 , tasty-hunit >= 0.9 && < 0.10@@ -82,7 +94,7 @@ test-suite remote type: exitcode-stdio-1.0- hs-source-dirs: tests+ hs-source-dirs: test main-is: Remote.hs other-modules: Shared default-language: Haskell2010@@ -92,32 +104,16 @@ , async , base , bytestring+ , filepath , hjsonschema , text , vector , directory- , filepath , HUnit , tasty , tasty-hunit , wai-app-static , warp--executable example- hs-source-dirs: examples- main-is: Main.hs- other-modules: CustomSchema- , PrettyShowFailure- , Standard- default-language: Haskell2010- ghc-options: -Wall- build-depends: aeson- , base- , hjsonpointer- , hjsonschema- , text- , unordered-containers- , vector source-repository head type: git
src/Data/JsonSchema/Draft4.hs view
@@ -4,67 +4,133 @@ module Data.JsonSchema.Draft4 ( Schema(..) , emptySchema- , checkSchema + -- * One-step validation+ , HTTPValidationFailure(..)+ , fetchHTTPAndValidate+ , FilesystemValidationFailure(..)+ , fetchFilesystemAndValidate+ -- * Fetching tools- , SchemaContext(..)- , SchemaCache(..)- , fetchReferencedSchemas+ , SchemaWithURI(..)+ , ReferencedSchemas(..)+ , HTTPFailure(..)+ , referencesViaHTTP+ , FilesystemFailure(..)+ , referencesViaFilesystem -- * Failure- , Failure(..)+ , Invalid+ , FR.Failure(..) , ValidatorChain(..) -- * Other Draft 4 things exported just in case+ , checkSchema , schemaValidity , IN.runValidate+ , draft4Spec ) where +import Control.Applicative+import Control.Arrow (left) import Data.Aeson import qualified Data.ByteString.Lazy as LBS import Data.FileEmbed import qualified Data.HashMap.Strict as H+import Data.List.NonEmpty (NonEmpty)+import qualified Data.List.NonEmpty as N import Data.Maybe (fromMaybe) -import Data.JsonSchema.Draft4.Failure+import Data.JsonSchema.Draft4.Failure (Invalid, ValidatorChain(..)) import qualified Data.JsonSchema.Draft4.Internal as IN import Data.JsonSchema.Draft4.Schema-import Data.JsonSchema.Fetch (SchemaCache(..),- SchemaContext(..),- URISchemaMap)+import Data.JsonSchema.Fetch (FilesystemFailure(..),+ HTTPFailure(..),+ ReferencedSchemas(..),+ SchemaWithURI(..)) import qualified Data.JsonSchema.Fetch as FE-import Data.Validator.Reference (baseAndFragment)-import Import+import qualified Data.Validator.Failure as FR --- | Check the validity of a schema and return a function to validate data.+data HTTPValidationFailure+ = HVRequest HTTPFailure+ | HVSchema (NonEmpty Invalid)+ | HVData (NonEmpty Invalid)+ deriving Show++fetchHTTPAndValidate+ :: SchemaWithURI Schema+ -> Value+ -> IO (Either HTTPValidationFailure ())+fetchHTTPAndValidate sw v = do+ res <- referencesViaHTTP sw+ pure (g =<< f =<< left HVRequest res)+ where+ f :: ReferencedSchemas Schema+ -> Either HTTPValidationFailure (Value -> [Invalid])+ f references = left HVSchema (checkSchema references sw)++ g :: (Value -> [Invalid]) -> Either HTTPValidationFailure ()+ g validate = case N.nonEmpty (validate v) of+ Nothing -> Right ()+ Just failures -> Left (HVData failures)++data FilesystemValidationFailure+ = FVRead FilesystemFailure+ | FVSchema (NonEmpty Invalid)+ | FVData (NonEmpty Invalid)+ deriving Show++fetchFilesystemAndValidate+ :: SchemaWithURI Schema+ -> Value+ -> IO (Either FilesystemValidationFailure ())+fetchFilesystemAndValidate sw v = do+ res <- referencesViaFilesystem sw+ pure (g =<< f =<< left FVRead res)+ where+ f :: ReferencedSchemas Schema+ -> Either FilesystemValidationFailure (Value -> [Invalid])+ f references = left FVSchema (checkSchema references sw)++ g :: (Value -> [Invalid]) -> Either FilesystemValidationFailure ()+ g validate = case N.nonEmpty (validate v) of+ Nothing -> Right ()+ Just failures -> Left (FVData failures)++-- | Check the that a schema itself is valid.+--+-- Return a function to validate data. checkSchema- :: SchemaCache Schema- -> SchemaContext Schema- -> Either [Failure] (Value -> [Failure])-checkSchema sg sc =- case schemaValidity (_scSchema sc) of- [] -> Right (IN.runValidate sg sc)- es -> Left es+ :: ReferencedSchemas Schema+ -> SchemaWithURI Schema+ -> Either (NonEmpty Invalid) (Value -> [Invalid])+checkSchema referenced schemaWithURI =+ case N.nonEmpty . schemaValidity . _swSchema $ schemaWithURI of+ Nothing -> Right (IN.runValidate referenced schemaWithURI)+ Just failures -> Left failures -fetchReferencedSchemas- :: URISchemaMap Schema- -> SchemaContext Schema- -> IO (Either Text (SchemaCache Schema))-fetchReferencedSchemas =- FE.fetchReferencedSchemas IN.embedded _schemaId _schemaRef+draft4Spec :: FE.Spec Schema+draft4Spec = FE.Spec IN.embedded _schemaId _schemaRef --- | In normal situations just use 'checkSchema', which is a combination of--- 'schemaValidity' and 'runValidate'.-schemaValidity :: Schema -> [Failure]-schemaValidity = IN.runValidate cache (SchemaContext Nothing d4) . toJSON+referencesViaHTTP+ :: SchemaWithURI Schema+ -> IO (Either HTTPFailure (ReferencedSchemas Schema))+referencesViaHTTP = FE.referencesViaHTTP' draft4Spec++referencesViaFilesystem+ :: SchemaWithURI Schema+ -> IO (Either FilesystemFailure (ReferencedSchemas Schema))+referencesViaFilesystem = FE.referencesViaFilesystem' draft4Spec++-- | Check that a schema itself is valid.+schemaValidity :: Schema -> [Invalid]+schemaValidity = IN.runValidate referenced (SchemaWithURI d4 Nothing) . toJSON where d4 :: Schema d4 = fromMaybe (error "Schema decode failed (this should never happen)")- . decode . LBS.fromStrict $ $(embedFile "draft4.json")+ . decode . LBS.fromStrict $ $(embedFile "src/draft4.json") - -- @fst . baseAndFragment@ is necessary to remove the trailing "#",- -- otherwise cache lookups to resolve internal references will fail.- cache :: SchemaCache Schema- cache = SchemaCache d4 $ case _schemaId d4 >>= fst . baseAndFragment of- Nothing -> mempty- Just uri -> H.singleton uri d4+ referenced :: ReferencedSchemas Schema+ referenced = ReferencedSchemas+ d4+ (H.singleton "http://json-schema.org/draft-04/schema" d4)
src/Data/JsonSchema/Draft4/Failure.hs view
@@ -1,24 +1,9 @@ module Data.JsonSchema.Draft4.Failure where -import qualified Data.Aeson.Pointer as P- import qualified Data.Validator.Failure as FR-import Import --- | A version of 'FR.Failure' specialized for JSON Schema Draft 4.-data Failure = Failure- -- NOTE: We use a data type here instead of a newtype to provide a cleaner- -- API for those importing only 'Data.JsonSchema.Draft4' (which should be- -- the vast majority of users). Downsides: slower and hacky. I'd appreciate- -- feedback on this.- { _failureValidatorsCalled :: !ValidatorChain- , _failureFinalValidator :: !Value- , _failureOffendingData :: !P.Pointer- } deriving (Eq, Show)--specializeForDraft4 :: FR.Failure ValidatorChain -> Failure-specializeForDraft4 (FR.Failure a b c) = Failure a b c+type Invalid = FR.Failure ValidatorChain data ValidatorChain = MultipleOf
src/Data/JsonSchema/Draft4/Internal.hs view
@@ -10,8 +10,8 @@ import Data.JsonSchema.Draft4.Failure import Data.JsonSchema.Draft4.Schema-import Data.JsonSchema.Fetch (SchemaCache(..),- SchemaContext(..))+import Data.JsonSchema.Fetch (ReferencedSchemas(..),+ SchemaWithURI(..)) import qualified Data.Validator.Draft4.Any as AN import qualified Data.Validator.Draft4.Array as AR import qualified Data.Validator.Draft4.Number as NU@@ -19,7 +19,7 @@ import qualified Data.Validator.Draft4.String as ST import Data.Validator.Failure (modFailure, setFailure) import qualified Data.Validator.Failure as FR-import Data.Validator.Reference (newResolutionScope)+import Data.Validator.Reference (updateResolutionScope) import Import -- For GHCs before 7.10:@@ -71,28 +71,15 @@ checkDependency (OB.SchemaDependency s) = Just s ----------------------------------------------------- * Validation (Exported from 'Data.JsonSchema.Draft4')------------------------------------------------------- | In normal situations just use 'checkSchema', which is a combination of--- 'schemaValidity' and 'runValidate'.-runValidate- :: SchemaCache Schema- -> SchemaContext Schema- -> Value- -> [Failure]-runValidate = (fmap.fmap.fmap.fmap) specializeForDraft4 validateAny---------------------------------------------------- -- * Validation (Main internal functions) -------------------------------------------------- -validateAny- :: SchemaCache Schema- -> SchemaContext Schema+runValidate+ :: ReferencedSchemas Schema+ -> SchemaWithURI Schema -> Value- -> [FR.Failure ValidatorChain]-validateAny cache sc x = concat+ -> [Invalid]+runValidate referenced sw x = concat [ f _schemaEnum (setFailure Enum) (fmap maybeToList . AN.enumVal) , f _schemaType (setFailure TypeValidator) (fmap maybeToList . AN.typeVal) , f _schemaAllOf (modFailure AllOf) (AN.allOf recurse)@@ -102,48 +89,48 @@ , refFailures ] <> specificValidators where- specificValidators :: [FR.Failure ValidatorChain]+ specificValidators :: [Invalid] specificValidators = case x of- Number y -> validateNumber (_scSchema sc) y- String y -> validateString (_scSchema sc) y- Array y -> validateArray cache sc y- Object y -> validateObject cache sc y+ Number y -> validateNumber (_swSchema sw) y+ String y -> validateString (_swSchema sw) y+ Array y -> validateArray referenced sw y+ Object y -> validateObject referenced sw y _ -> mempty - f = runSingle (_scSchema sc) x+ f = runSingle (_swSchema sw) x - recurse = descendNextLevel cache sc+ recurse = descendNextLevel referenced sw -- Since the results of the 'AN.ref' validator are fairly complicated [1] -- it's simpler not to use our 'f' helper function for it. -- -- [1] A list of errors wrapped in a 'Maybe' where 'Nothing' represents -- if resolving the reference itself failed.- refFailures :: [FR.Failure ValidatorChain]+ refFailures :: [Invalid] refFailures =- case _schemaRef (_scSchema sc) of+ case _schemaRef (_swSchema sw) of Nothing -> mempty Just reference ->- maybe [FR.Failure RefResolution (toJSON reference) mempty]+ maybe [FR.Invalid RefResolution (toJSON reference) mempty] (fmap (modFailure Ref)) $ AN.ref scope getReference- (\a b -> validateAny cache (SchemaContext a b))+ (\a b -> runValidate referenced (SchemaWithURI b a)) reference x where scope :: Maybe Text- scope = newResolutionScope (_scURI sc) (_schemaId (_scSchema sc))+ scope = updateResolutionScope (_swURI sw) (_schemaId (_swSchema sw)) getReference :: Maybe Text -> Maybe Schema- getReference Nothing = Just (_startingSchema cache)- getReference (Just t) = H.lookup t (_cachedSchemas cache)+ getReference Nothing = Just (_rsStarting referenced)+ getReference (Just t) = H.lookup t (_rsSchemaMap referenced) validateString :: Schema -> Text- -> [FR.Failure ValidatorChain]+ -> [Invalid] validateString schema x = concat [ f _schemaMaxLength (setFailure MaxLength) (fmap maybeToList . ST.maxLength) , f _schemaMinLength (setFailure MinLength) (fmap maybeToList . ST.minLength)@@ -155,7 +142,7 @@ validateNumber :: Schema -> Scientific- -> [FR.Failure ValidatorChain]+ -> [Invalid] validateNumber schema x = concat [ f _schemaMultipleOf (setFailure MultipleOf) (fmap maybeToList . NU.multipleOf) , f _schemaMaximum@@ -179,11 +166,11 @@ fMin NU.ExclusiveMinimum = ExclusiveMinimum validateArray- :: SchemaCache Schema- -> SchemaContext Schema+ :: ReferencedSchemas Schema+ -> SchemaWithURI Schema -> Vector Value- -> [FR.Failure ValidatorChain]-validateArray cache (SchemaContext mUri schema) x = concat+ -> [Invalid]+validateArray referenced (SchemaWithURI schema mUri) x = concat [ f _schemaMaxItems (setFailure MaxItems) (fmap maybeToList . AR.maxItems) , f _schemaMinItems (setFailure MinItems) (fmap maybeToList . AR.minItems) , f _schemaUniqueItems (setFailure UniqueItems) (fmap maybeToList . AR.uniqueItems)@@ -194,18 +181,18 @@ where f = runSingle schema x - recurse = descendNextLevel cache (SchemaContext mUri schema)+ recurse = descendNextLevel referenced (SchemaWithURI schema mUri) fItems (AR.Items err) = Items err- fItems AR.AdditionalItemsBoolFailure = AdditionalItemsBool- fItems (AR.AdditionalItemsObjectFailure err) = AdditionalItemsObject err+ fItems AR.AdditionalItemsBoolInvalid = AdditionalItemsBool+ fItems (AR.AdditionalItemsObjectInvalid err) = AdditionalItemsObject err validateObject- :: SchemaCache Schema- -> SchemaContext Schema+ :: ReferencedSchemas Schema+ -> SchemaWithURI Schema -> HashMap Text Value- -> [FR.Failure ValidatorChain]-validateObject cache (SchemaContext mUri schema) x = concat+ -> [Invalid]+validateObject referenced (SchemaWithURI schema mUri) x = concat [ f _schemaMaxProperties (setFailure MaxProperties) (fmap maybeToList . OB.maxProperties) , f _schemaMinProperties (setFailure MinProperties) (fmap maybeToList . OB.minProperties) , f _schemaRequired (setFailure Required) (fmap maybeToList . OB.required)@@ -232,49 +219,49 @@ where f = runSingle schema x - recurse = descendNextLevel cache (SchemaContext mUri schema)+ recurse = descendNextLevel referenced (SchemaWithURI schema mUri) - fDeps (OB.SchemaDependencyFailure err) = SchemaDependency err- fDeps OB.PropertyDependencyFailure = PropertyDependency+ fDeps (OB.SchemaDependencyInvalid err) = SchemaDependency err+ fDeps OB.PropertyDependencyInvalid = PropertyDependency - fProp (OB.PropertiesFailure err) = Properties err- fProp (OB.PropPatternFailure err) = PatternProperties err- fProp (OB.PropAdditionalFailure a) =+ fProp (OB.PropertiesInvalid err) = Properties err+ fProp (OB.PropPatternInvalid err) = PatternProperties err+ fProp (OB.PropAdditionalInvalid a) = case a of- OB.APBoolFailure -> AdditionalPropertiesBool- OB.APObjectFailure err -> AdditionalPropertiesObject err+ OB.APBoolInvalid -> AdditionalPropertiesBool+ OB.APObjectInvalid err -> AdditionalPropertiesObject err - fPatProp (OB.PPFailure err) = PatternProperties err- fPatProp (OB.PPAdditionalPropertiesFailure a) =+ fPatProp (OB.PPInvalid err) = PatternProperties err+ fPatProp (OB.PPAdditionalPropertiesInvalid a) = case a of- OB.APBoolFailure -> AdditionalPropertiesBool- OB.APObjectFailure err -> AdditionalPropertiesObject err+ OB.APBoolInvalid -> AdditionalPropertiesBool+ OB.APObjectInvalid err -> AdditionalPropertiesObject err - fAddProp OB.APBoolFailure = AdditionalPropertiesBool- fAddProp (OB.APObjectFailure err) = AdditionalItemsObject err+ fAddProp OB.APBoolInvalid = AdditionalPropertiesBool+ fAddProp (OB.APObjectInvalid err) = AdditionalItemsObject err -------------------------------------------------- -- * Validation (Internal utils) -------------------------------------------------- descendNextLevel- :: SchemaCache Schema- -> SchemaContext Schema+ :: ReferencedSchemas Schema+ -> SchemaWithURI Schema -> Schema -> Value- -> [FR.Failure ValidatorChain]-descendNextLevel cache (SchemaContext mUri schema) =- validateAny cache . SchemaContext scope+ -> [Invalid]+descendNextLevel referenced (SchemaWithURI schema mUri) =+ runValidate referenced . flip SchemaWithURI scope where scope :: Maybe Text- scope = newResolutionScope mUri (_schemaId schema)+ scope = updateResolutionScope mUri (_schemaId schema) runSingle :: Schema -> dta -> (Schema -> Maybe val)- -> (err -> FR.Failure ValidatorChain)+ -> (err -> Invalid) -> (val -> dta -> [err])- -> [FR.Failure ValidatorChain]+ -> [Invalid] runSingle schema dta field modifyError validate = maybe mempty (\val -> modifyError <$> validate val dta) (field schema)
src/Data/JsonSchema/Fetch.hs view
@@ -2,22 +2,34 @@ module Data.JsonSchema.Fetch where -import Control.Exception (SomeException(..), catch)+import Control.Arrow (left)+import Control.Exception (catch) import qualified Data.ByteString.Lazy as LBS+import qualified Data.ByteString as BS import qualified Data.HashMap.Strict as H import qualified Data.Text as T import Network.HTTP.Client -import Data.Validator.Reference (isRemoteReference,- newResolutionScope,- resolveReference)+import Data.Validator.Reference (resolveReference,+ updateResolutionScope) import Import -- For GHCs before 7.10: import Prelude hiding (concat, sequence) -data SchemaContext schema = SchemaContext- { _scURI :: !(Maybe Text)+--------------------------------------------------+-- * Types+--------------------------------------------------++data Spec schema = Spec+ { _ssEmbedded :: schema -> [schema]+ , _ssGetId :: schema -> Maybe Text+ , _ssGetRef :: schema -> Maybe Text+ }++data SchemaWithURI schema = SchemaWithURI+ { _swSchema :: !schema+ , _swURI :: !(Maybe Text) -- ^ Must not include a URI fragment, e.g. use -- "http://example.com/foo" not "http://example.com/foo#bar". --@@ -26,108 +38,136 @@ -- when resolving references contained in the schema. -- TODO: Make the no URI fragment requirement unnecessary.-- , _scSchema :: !schema } deriving (Eq, Show) -- | Keys are URIs (without URI fragments). type URISchemaMap schema = HashMap Text schema -data SchemaCache schema = SchemaCache- { _startingSchema :: !schema+data ReferencedSchemas schema = ReferencedSchemas+ { _rsStarting :: !schema -- ^ Used to resolve relative references.- , _cachedSchemas :: !(URISchemaMap schema)+ , _rsSchemaMap :: !(URISchemaMap schema) } deriving (Eq, Show) +--------------------------------------------------+-- * Fetch via HTTP+--------------------------------------------------++data HTTPFailure+ = HTTPParseFailure Text+ | HTTPRequestFailure HttpException+ deriving Show+ -- | Take a schema. Retrieve every document either it or its subschemas--- include via the "$ref" keyword. Load a 'SchemaCache' out with them.-fetchReferencedSchemas+-- include via the "$ref" keyword.+referencesViaHTTP' :: forall schema. FromJSON schema- => (schema -> [schema])- -> (schema -> Maybe Text)- -> (schema -> Maybe Text)- -> URISchemaMap schema- -> SchemaContext schema- -> IO (Either Text (SchemaCache schema))-fetchReferencedSchemas embedded getId getRef cache sc = do+ => Spec schema+ -> SchemaWithURI schema+ -> IO (Either HTTPFailure (ReferencedSchemas schema))+referencesViaHTTP' spec sw = do manager <- newManager defaultManagerSettings- catch (Right <$> f manager) handler+ let f = referencesMethodAgnostic (get manager) spec sw+ catch (left HTTPParseFailure <$> f) handler where- f manager = fetchReferencedSchemas' embedded getId getRef- (simpleGET manager) cache sc+ get :: Manager -> Text -> IO LBS.ByteString+ get man url = do+ request <- parseUrl (T.unpack url)+ responseBody <$> httpLbs request man - handler :: SomeException -> IO (Either Text (SchemaCache schema))- handler e = pure . Left . T.pack . show $ e+ handler+ :: HttpException+ -> IO (Either HTTPFailure (ReferencedSchemas schema))+ handler = pure . Left . HTTPRequestFailure --- | Based on 'Network.Http.Conduit.simpleHttp' from http-conduit.-simpleGET :: Manager -> Text -> IO LBS.ByteString-simpleGET man url = do- req <- parseUrl (T.unpack url)- responseBody <$> httpLbs req { requestHeaders = ("Connection", "close")- : requestHeaders req- } man+--------------------------------------------------+-- * Fetch via Filesystem+-------------------------------------------------- +data FilesystemFailure+ = FSParseFailure Text+ | FSReadFailure IOError+ deriving Show++referencesViaFilesystem'+ :: forall schema. FromJSON schema+ => Spec schema+ -> SchemaWithURI schema+ -> IO (Either FilesystemFailure (ReferencedSchemas schema))+referencesViaFilesystem' spec sw = catch (left FSParseFailure <$> f) handler+ where+ f :: IO (Either Text (ReferencedSchemas schema))+ f = referencesMethodAgnostic readFile' spec sw++ readFile' :: Text -> IO LBS.ByteString+ readFile' = fmap LBS.fromStrict . BS.readFile . T.unpack++ handler+ :: IOError+ -> IO (Either FilesystemFailure (ReferencedSchemas schema))+ handler = pure . Left . FSReadFailure++--------------------------------------------------+-- * Method Agnostic Fetching Tools+--------------------------------------------------+ -- | A version of 'fetchReferencedSchema's where the function to fetch -- schemas is provided by the user. This allows restrictions to be added, -- e.g. rejecting non-local URIs.-fetchReferencedSchemas'+referencesMethodAgnostic :: forall schema. FromJSON schema- => (schema -> [schema])- -> (schema -> Maybe Text)- -> (schema -> Maybe Text)- -> (Text -> IO LBS.ByteString)+ => (Text -> IO LBS.ByteString)+ -> Spec schema+ -> SchemaWithURI schema+ -> IO (Either Text (ReferencedSchemas schema))+referencesMethodAgnostic fetchRef spec sw =+ (fmap.fmap) (ReferencedSchemas (_swSchema sw))+ (foldFunction fetchRef spec mempty sw)++foldFunction+ :: forall schema. FromJSON schema+ => (Text -> IO LBS.ByteString)+ -> Spec schema -> URISchemaMap schema- -> SchemaContext schema- -> IO (SchemaCache schema)-fetchReferencedSchemas' embedded getId getRef fetchRef cache sc = do- let startingCache = case _scURI sc of- Nothing -> cache- Just uri -> H.insert uri (_scSchema sc) cache- SchemaCache (_scSchema sc) <$> foldlM fetchRecursively- startingCache- (includeSubschemas embedded getId sc)+ -> SchemaWithURI schema+ -> IO (Either Text (URISchemaMap schema))+foldFunction fetchRef spec@(Spec _ _ getRef) referenced sw =+ foldlM f (Right referenced) (includeSubschemas spec sw) where- fetchRecursively- :: URISchemaMap schema- -> SchemaContext schema- -> IO (URISchemaMap schema)- fetchRecursively g (SchemaContext mUri schema) = do- -- Resolving the new scope is necessary here because of situations- -- like this:- --- -- {- -- "id": "http://localhost:1234/",- -- "items": {- -- "id": "folder/",- -- "items": {"$ref": "folderInteger.json"}- -- }- -- }- let scope = newResolutionScope mUri (getId schema)- case resolveReference scope <$> getRef schema of- Just (Just uri,_) ->- if not (isRemoteReference uri) || H.member uri g- then pure g- else do- bts <- fetchRef uri- case eitherDecode bts of- Left e -> ioError (userError e)- Right schm -> _cachedSchemas <$>- fetchReferencedSchemas' embedded getId getRef fetchRef- g (SchemaContext (Just uri) schm)- _ -> pure g+ f :: Either Text (URISchemaMap schema)+ -> SchemaWithURI schema+ -> IO (Either Text (URISchemaMap schema))+ f (Left e) _ = pure (Left e)+ f (Right g) (SchemaWithURI schema mUri) =+ case newRef of+ Nothing -> pure (Right g)+ Just uri -> do+ bts <- fetchRef uri+ case eitherDecode bts of+ Left e -> pure . Left . T.pack $ e+ Right schm -> foldFunction fetchRef spec (H.insert uri schm g)+ (SchemaWithURI schm (Just uri))+ where+ newRef :: Maybe Text+ newRef+ | Just (Just uri,_) <- resolveReference mUri <$> getRef schema+ = case H.lookup uri g of+ Nothing -> Just uri+ Just _ -> Nothing+ | otherwise = Nothing -- | Return the schema passed in as an argument, as well as every -- subschema contained within it. includeSubschemas :: forall schema.- (schema -> [schema])- -> (schema -> Maybe Text)- -> SchemaContext schema- -> [SchemaContext schema]-includeSubschemas embedded getId (SchemaContext mUri schema) =- SchemaContext mUri schema- : (includeSubschemas embedded getId =<< subSchemas)+ Spec schema+ -> SchemaWithURI schema+ -> [SchemaWithURI schema]+includeSubschemas spec@(Spec embedded getId _) (SchemaWithURI schema mUri) =+ SchemaWithURI schema mUri+ : (includeSubschemas spec =<< subSchemas) where- subSchemas :: [SchemaContext schema]- subSchemas = SchemaContext (newResolutionScope mUri (getId schema))- <$> embedded schema+ subSchemas :: [SchemaWithURI schema]+ subSchemas =+ (\a -> SchemaWithURI a (updateResolutionScope mUri (getId schema)))+ <$> embedded schema
src/Data/Validator/Draft4/Any.hs view
@@ -12,11 +12,11 @@ import Data.Validator.Failure import Data.Validator.Utils-import Data.Validator.Reference+import Data.Validator.Reference (resolveFragment, resolveReference) import Import -- For GHCs before 7.10:-import Prelude hiding (any)+import Prelude hiding (any, elem) -------------------------------------------------- -- * $ref@@ -81,11 +81,10 @@ enumVal :: EnumVal -> Value -> Maybe (Failure ()) enumVal (EnumVal vs) x- | null vs = Nothing | not (allUniqueValues' vs) = Nothing | x `elem` vs = Nothing | otherwise =- Just $ Failure () (toJSON (NonEmpty' vs)) mempty+ Just $ Invalid () (toJSON (NonEmpty' vs)) mempty -------------------------------------------------- -- * type@@ -115,7 +114,7 @@ isJsonType :: Value -> Set Text -> Maybe (Failure ()) isJsonType x ts- | S.null (S.intersection okTypes ts) = Just (Failure () (toJSON ts) mempty)+ | S.null (S.intersection okTypes ts) = Just (Invalid () (toJSON ts) mempty) | otherwise = Nothing where okTypes :: Set Text@@ -150,7 +149,7 @@ -> Maybe (Failure ()) anyOf f subSchemas x | any null (flip f x <$> subSchemas) = Nothing- | otherwise = Just $ Failure () (toJSON (NonEmpty' subSchemas)) mempty+ | otherwise = Just $ Invalid () (toJSON (NonEmpty' subSchemas)) mempty oneOf :: forall err schema. ToJSON schema@@ -160,7 +159,7 @@ -> Maybe (Failure ()) oneOf f subSchemas x | length successes == 1 = Nothing- | otherwise = Just $ Failure ()+ | otherwise = Just $ Invalid () (toJSON (NonEmpty' subSchemas)) mempty where@@ -175,5 +174,5 @@ -> Maybe (Failure ()) notVal f schema x = case f schema x of- [] -> Just (Failure () (toJSON schema) mempty)+ [] -> Just (Invalid () (toJSON schema) mempty) _ -> Nothing
src/Data/Validator/Draft4/Array.hs view
@@ -15,30 +15,30 @@ maxItems :: Int -> Vector Value -> Maybe (Failure ()) maxItems n xs | n < 0 = Nothing- | V.length xs > n = Just (Failure () (toJSON n) mempty)+ | V.length xs > n = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing -- | The spec requires "minItems" to be non-negative. minItems :: Int -> Vector Value -> Maybe (Failure ()) minItems n xs | n < 0 = Nothing- | V.length xs < n = Just (Failure () (toJSON n) mempty)+ | V.length xs < n = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing uniqueItems :: Bool -> Vector Value -> Maybe (Failure ()) uniqueItems True xs | allUniqueValues xs = Nothing- | otherwise = Just (Failure () (Bool True) mempty)+ | otherwise = Just (Invalid () (Bool True) mempty) uniqueItems False _ = Nothing -------------------------------------------------- -- * items -------------------------------------------------- -data ItemsFailure err+data ItemsInvalid err = Items err- | AdditionalItemsBoolFailure- | AdditionalItemsObjectFailure err+ | AdditionalItemsBoolInvalid+ | AdditionalItemsObjectInvalid err deriving (Eq, Show) data Items schema@@ -65,10 +65,10 @@ -> Maybe (AdditionalItems schema) -> Items schema -> Vector Value- -> [Failure (ItemsFailure err)]+ -> [Failure (ItemsInvalid err)] items f _ (ItemsObject subSchema) xs = zip [0..] (V.toList xs) >>= g where- g :: (Int, Value) -> [Failure (ItemsFailure err)]+ g :: (Int, Value) -> [Failure (ItemsInvalid err)] g (index,x) = modFailure Items . addToPath (P.Token (T.pack (show index))) <$> f subSchema x@@ -79,15 +79,15 @@ indexedValues :: [(Int, Value)] indexedValues = zip [0..] (V.toList xs) - itemFailures :: [Failure (ItemsFailure err)]+ itemFailures :: [Failure (ItemsInvalid err)] itemFailures = join (zipWith g subSchemas indexedValues) where- g :: schema -> (Int, Value) -> [Failure (ItemsFailure err)]+ g :: schema -> (Int, Value) -> [Failure (ItemsInvalid err)] g schema (index,x) = modFailure Items . addToPath (P.Token (T.pack (show index))) <$> f schema x - additionalItemFailures :: [Failure (ItemsFailure err)]+ additionalItemFailures :: [Failure (ItemsInvalid err)] additionalItemFailures = case mAdditional of Nothing -> mempty@@ -105,9 +105,9 @@ -- represent invalid data so they actually represent the correct -- offsets. correctIndexes- :: Failure (AdditionalItemsFailure err)- -> Failure (AdditionalItemsFailure err)- correctIndexes (Failure a b c) = Failure a b (fixIndex c)+ :: Failure (AdditionalItemsInvalid err)+ -> Failure (AdditionalItemsInvalid err)+ correctIndexes (Invalid a b c) = Invalid a b (fixIndex c) where fixIndex :: P.Pointer -> P.Pointer fixIndex (P.Pointer (tok:toks)) =@@ -117,18 +117,18 @@ (P.Token . T.pack . show $ n + length subSchemas):toks fixIndex (P.Pointer []) = P.Pointer [] - correctName :: AdditionalItemsFailure err -> ItemsFailure err- correctName AdditionalBoolFailure = AdditionalItemsBoolFailure- correctName (AdditionalObjectFailure err) =- AdditionalItemsObjectFailure err+ correctName :: AdditionalItemsInvalid err -> ItemsInvalid err+ correctName AdditionalBoolInvalid = AdditionalItemsBoolInvalid+ correctName (AdditionalObjectInvalid err) =+ AdditionalItemsObjectInvalid err -------------------------------------------------- -- * additionalItems -------------------------------------------------- -data AdditionalItemsFailure err- = AdditionalBoolFailure- | AdditionalObjectFailure err+data AdditionalItemsInvalid err+ = AdditionalBoolInvalid+ | AdditionalObjectInvalid err deriving (Eq, Show) data AdditionalItems schema@@ -154,15 +154,15 @@ (schema -> Value -> [Failure err]) -> AdditionalItems schema -> Vector Value- -> [Failure (AdditionalItemsFailure err)]+ -> [Failure (AdditionalItemsInvalid err)] additionalItems _ (AdditionalBool b) xs | b = mempty- | V.length xs > 0 = pure (Failure AdditionalBoolFailure (Bool b) mempty)+ | V.length xs > 0 = pure (Invalid AdditionalBoolInvalid (Bool b) mempty) | otherwise = mempty additionalItems f (AdditionalObject subSchema) xs = zip [0..] (V.toList xs) >>= g where- g :: (Int, Value) -> [Failure (AdditionalItemsFailure err)]- g (index,x) = modFailure AdditionalObjectFailure+ g :: (Int, Value) -> [Failure (AdditionalItemsInvalid err)]+ g (index,x) = modFailure AdditionalObjectInvalid . addToPath (P.Token (T.pack (show index))) <$> f subSchema x
src/Data/Validator/Draft4/Number.hs view
@@ -11,10 +11,10 @@ multipleOf :: Scientific -> Scientific -> Maybe (Failure ()) multipleOf n x | n <= 0 = Nothing- | x `mod'` n /= 0 = Just (Failure () (toJSON n) mempty)+ | x `mod'` n /= 0 = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing -data MaximumFailure+data MaximumInvalid = Maximum | ExclusiveMaximum deriving (Eq, Show)@@ -23,16 +23,16 @@ :: Bool -> Scientific -> Scientific- -> Maybe (Failure MaximumFailure)+ -> Maybe (Failure MaximumInvalid) maximumVal exclusiveMaximum n x- | x `greaterThan` n = Just (Failure err (toJSON n) mempty)+ | x `greaterThan` n = Just (Invalid err (toJSON n) mempty) | otherwise = Nothing where (greaterThan, err) = if exclusiveMaximum then ((>=), ExclusiveMaximum) else ((>), Maximum) -data MinimumFailure+data MinimumInvalid = Minimum | ExclusiveMinimum deriving (Eq, Show)@@ -41,9 +41,9 @@ :: Bool -> Scientific -> Scientific- -> Maybe (Failure MinimumFailure)+ -> Maybe (Failure MinimumInvalid) minimumVal exclusiveMinimum n x- | x `lessThan` n = Just (Failure err (toJSON n) mempty)+ | x `lessThan` n = Just (Invalid err (toJSON n) mempty) | otherwise = Nothing where (lessThan, err) = if exclusiveMinimum
src/Data/Validator/Draft4/Object.hs view
@@ -25,14 +25,14 @@ maxProperties :: Int -> HashMap Text Value -> Maybe (Failure ()) maxProperties n x | n < 0 = Nothing- | H.size x > n = Just (Failure () (toJSON n) mempty)+ | H.size x > n = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing -- | The spec requires "minProperties" to be non-negative. minProperties :: Int -> HashMap Text Value -> Maybe (Failure ()) minProperties n x | n < 0 = Nothing- | H.size x < n = Just (Failure () (toJSON n) mempty)+ | H.size x < n = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing --------------------------------------------------@@ -80,7 +80,7 @@ -- instead of specialized versions. | S.null ts = Nothing | H.null (H.difference hm x) = Nothing- | otherwise = Just (Failure () (toJSON ts) mempty)+ | otherwise = Just (Invalid () (toJSON ts) mempty) where hm :: HashMap Text Bool hm = foldl (\b a -> H.insert a True b) mempty ts@@ -89,9 +89,9 @@ -- * dependencies -------------------------------------------------- -data DependencyFailure err- = SchemaDependencyFailure err- | PropertyDependencyFailure+data DependencyInvalid err+ = SchemaDependencyInvalid err+ | PropertyDependencyInvalid deriving (Eq, Show) data Dependency schema@@ -128,17 +128,17 @@ :: forall err schema. (schema -> Value -> [Failure err]) -> HashMap Text (Dependency schema) -> HashMap Text Value- -> [Failure (DependencyFailure err)]+ -> [Failure (DependencyInvalid err)] dependencies f hm x = concat . fmap (uncurry g) . H.toList $ hm where- g :: Text -> Dependency schema -> [Failure (DependencyFailure err)]+ g :: Text -> Dependency schema -> [Failure (DependencyInvalid err)] g k (SchemaDependency schema) | H.member k x =- modFailure SchemaDependencyFailure <$> f schema (Object x)+ modFailure SchemaDependencyInvalid <$> f schema (Object x) | otherwise = mempty g k (PropertyDependency ts) | H.member k x && not allPresent =- pure $ Failure PropertyDependencyFailure+ pure $ Invalid PropertyDependencyInvalid (toJSON (H.singleton k ts)) mempty | otherwise = mempty
src/Data/Validator/Draft4/Object/Properties.hs view
@@ -7,7 +7,8 @@ import qualified Data.Aeson.Pointer as P import qualified Data.HashMap.Strict as H import qualified Data.Text as T-import Text.RegexPR+import Data.Text.Encoding (encodeUtf8)+import qualified Text.Regex.PCRE.Heavy as RE import Data.Validator.Failure import Import@@ -18,10 +19,10 @@ -- * properties -------------------------------------------------- -data PropertiesFailure err- = PropertiesFailure err- | PropPatternFailure err- | PropAdditionalFailure (AdditionalPropertiesFailure err)+data PropertiesInvalid err+ = PropertiesInvalid err+ | PropPatternInvalid err+ | PropAdditionalInvalid (AdditionalPropertiesInvalid err) deriving (Eq, Show) -- | In order of what's tried: "properties", "patternProperties",@@ -33,11 +34,11 @@ -> Maybe (AdditionalProperties schema) -> HashMap Text schema -> HashMap Text Value- -> [Failure (PropertiesFailure err)]+ -> [Failure (PropertiesInvalid err)] properties f mPat mAdd propertiesHm x =- fmap (modFailure PropertiesFailure) propFailures- <> fmap (modFailure PropPatternFailure) patternFailures- <> fmap (modFailure PropAdditionalFailure) additionalFailures+ fmap (modFailure PropertiesInvalid) propFailures+ <> fmap (modFailure PropPatternInvalid) patternFailures+ <> fmap (modFailure PropAdditionalInvalid) additionalFailures where propertiesAndUnmatched :: ([Failure err], Remaining) propertiesAndUnmatched = ( failures@@ -63,7 +64,7 @@ Nothing -> remaining1 Just val -> snd . val . _unRemaining $ remaining1 - additionalFailures :: [Failure (AdditionalPropertiesFailure err)]+ additionalFailures :: [Failure (AdditionalPropertiesInvalid err)] additionalFailures = case additionalProperties f <$> mAdd of Nothing -> mempty Just val -> val (_unRemaining remaining2)@@ -72,9 +73,9 @@ -- * patternProperties -------------------------------------------------- -data PatternPropertiesFailure err- = PPFailure err- | PPAdditionalPropertiesFailure (AdditionalPropertiesFailure err)+data PatternPropertiesInvalid err+ = PPInvalid err+ | PPAdditionalPropertiesInvalid (AdditionalPropertiesInvalid err) deriving (Eq, Show) patternProperties@@ -83,17 +84,17 @@ -> Maybe (AdditionalProperties schema) -> HashMap Text schema -> HashMap Text Value- -> [Failure (PatternPropertiesFailure err)]+ -> [Failure (PatternPropertiesInvalid err)] patternProperties f mAdd patternPropertiesHm x =- fmap (modFailure PPFailure) ppFailures- <> fmap (modFailure PPAdditionalPropertiesFailure) addFailures+ fmap (modFailure PPInvalid) ppFailures+ <> fmap (modFailure PPAdditionalPropertiesInvalid) addFailures where patternProps :: ([Failure err], Remaining) patternProps = patternAndUnmatched f patternPropertiesHm x (ppFailures, remaining) = patternProps - addFailures :: [Failure (AdditionalPropertiesFailure err)]+ addFailures :: [Failure (AdditionalPropertiesInvalid err)] addFailures = case additionalProperties f <$> mAdd of Nothing -> mempty Just val -> val (_unRemaining remaining)@@ -126,9 +127,11 @@ -> schema -> [schema] checkKey k acc r subSchema =- case matchRegexPR (T.unpack r) (T.unpack k) of- Nothing -> acc- Just _ -> pure subSchema <> acc+ case RE.compileM (encodeUtf8 r) mempty of+ Left _ -> acc+ Right re -> if k RE.=~ re+ then pure subSchema <> acc+ else acc runVals :: [Failure err]@@ -146,9 +149,9 @@ -- * additionalProperties -------------------------------------------------- -data AdditionalPropertiesFailure err- = APBoolFailure- | APObjectFailure err+data AdditionalPropertiesInvalid err+ = APBoolInvalid+ | APObjectInvalid err deriving (Eq, Show) data AdditionalProperties schema@@ -174,14 +177,14 @@ (schema -> Value -> [Failure err]) -> AdditionalProperties schema -> HashMap Text Value- -> [Failure (AdditionalPropertiesFailure err)]+ -> [Failure (AdditionalPropertiesInvalid err)] additionalProperties _ (AdditionalPropertiesBool False) x- | H.size x > 0 = pure $ Failure APBoolFailure (Bool False) mempty+ | H.size x > 0 = pure $ Invalid APBoolInvalid (Bool False) mempty | otherwise = mempty additionalProperties _ (AdditionalPropertiesBool True) _ = mempty additionalProperties f (AdditionalPropertiesObject schema) x = H.toList x >>= g where- g :: (Text, Value) -> [Failure (AdditionalPropertiesFailure err)]- g (k,v) = modFailure APObjectFailure+ g :: (Text, Value) -> [Failure (AdditionalPropertiesInvalid err)]+ g (k,v) = modFailure APObjectInvalid . addToPath (P.Token k) <$> f schema v
src/Data/Validator/Draft4/String.hs view
@@ -3,7 +3,8 @@ import Data.Aeson import qualified Data.Text as T-import Text.RegexPR+import Data.Text.Encoding (encodeUtf8)+import qualified Text.Regex.PCRE.Heavy as RE import Data.Validator.Failure import Import@@ -12,18 +13,20 @@ maxLength :: Int -> Text -> Maybe (Failure ()) maxLength n x | n <= 0 = Nothing- | T.length x > n = Just (Failure () (toJSON n) mempty)+ | T.length x > n = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing -- | The spec requires "minLength" to be non-negative. minLength :: Int -> Text -> Maybe (Failure ()) minLength n x | n <= 0 = Nothing- | T.length x < n = Just (Failure () (toJSON n) mempty)+ | T.length x < n = Just (Invalid () (toJSON n) mempty) | otherwise = Nothing patternVal :: Text -> Text -> Maybe (Failure ()) patternVal t x =- case matchRegexPR (T.unpack t) (T.unpack x) of- Nothing -> Just (Failure () (toJSON t) mempty)- Just _ -> Nothing+ case RE.compileM (encodeUtf8 t) mempty of+ Left _ -> Just (Invalid () (toJSON t) mempty)+ Right re -> if x RE.=~ re+ then Nothing+ else Just (Invalid () (toJSON t) mempty)
src/Data/Validator/Failure.hs view
@@ -23,20 +23,20 @@ -- by the validators it uses into a single error sum type for that schema. -- The schema's validate function will return a 'ValidationFailure' with -- that sum type as its type argument.-data Failure err = Failure- { _failureValidatorsCalled :: !err- , _failureFinalValidator :: !Value- , _failureOffendingData :: !P.Pointer+data Failure err = Invalid+ { _invalidValidatorsCalled :: !err+ , _invalidFinalValidator :: !Value+ , _invalidOffendingData :: !P.Pointer } deriving (Eq, Show) setFailure :: b -> Failure a -> Failure b-setFailure e (Failure _ a b) = Failure e a b+setFailure e (Invalid _ a b) = Invalid e a b modFailure :: (a -> b) -> Failure a -> Failure b-modFailure f v@(Failure a _ _) = v { _failureValidatorsCalled = f a }+modFailure f v@(Invalid a _ _) = v { _invalidValidatorsCalled = f a } addToPath :: P.Token -> Failure a -> Failure a-addToPath tok v@(Failure _ _ a) = v { _failureOffendingData = addToken a }+addToPath tok v@(Invalid _ _ a) = v { _invalidOffendingData = addToken a } where addToken :: P.Pointer -> P.Pointer addToken (P.Pointer ts) = P.Pointer (ts <> pure tok)
src/Data/Validator/Reference.hs view
@@ -1,28 +1,43 @@ +-- | JSON Reference is described here:+-- <http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03>+--+-- And is extended for JSON Schema here:+-- <http://json-schema.org/latest/json-schema-core.html#anchor26>+--+-- Notes on where these functions are used:+--+-- * 'Data.JsonSchema.Fetch.includeSubschemas' uses 'updateResolutionScope'.+--+-- * 'Data.JsonSchema.Fetch.foldFunction' uses 'resolveReference'.+--+-- * 'Data.JsonSchema.Draft4.Internal' uses 'updateResolutionScope'+-- throughout.+--+-- * 'Data.Validator.Draft4.Any.ref' uses 'resolveReference' and+-- 'resolveFragment'.+ module Data.Validator.Reference where import qualified Data.Aeson.Pointer as P import qualified Data.Text as T import Data.Text.Encoding-import Network.HTTP.Types.URI+import Network.HTTP.Types.URI (urlDecode)+import System.FilePath ((</>), dropFileName) import Import type URIBase = Maybe Text type URIBaseAndFragment = (Maybe Text, Maybe Text) -newResolutionScope :: URIBase -> Maybe Text -> URIBase-newResolutionScope mScope idKeyword =- case idKeyword of- Just t -> fst . baseAndFragment $ resolveScopeAgainst mScope t- _ -> mScope+updateResolutionScope :: URIBase -> Maybe Text -> URIBase+updateResolutionScope mScope idKeyword+ | Just t <- idKeyword = fst . baseAndFragment $ resolveScopeAgainst mScope t+ | otherwise = mScope resolveReference :: URIBase -> Text -> URIBaseAndFragment resolveReference mScope t = baseAndFragment $ resolveScopeAgainst mScope t -isRemoteReference :: Text -> Bool-isRemoteReference uri = "://" `T.isInfixOf` uri- resolveFragment :: (FromJSON schema, ToJSON schema, Show schema) => Maybe Text@@ -38,9 +53,12 @@ Success schema' -> Just schema' ----------------------------------------------------- * Internal+-- * Helpers -------------------------------------------------- +isRemoteReference :: Text -> Bool+isRemoteReference = T.isInfixOf "://"+ baseAndFragment :: Text -> URIBaseAndFragment baseAndFragment = f . T.splitOn "#" where@@ -62,6 +80,12 @@ -- but just in case a user leaves one in we want to be sure -- to cut it off before appending. smartAppend :: Text- smartAppend = case baseAndFragment scope of- (Just base,_) -> base <> t- _ -> t+ smartAppend =+ case baseAndFragment scope of+ (Just base,_) ->+ case T.unpack t of+ -- We want "/foo" and "#/bar" to combine into+ -- "/foo#/bar" not "/foo/#/bar".+ '#':_ -> base <> t+ _ -> T.pack (dropFileName (T.unpack base) </> T.unpack t)+ _ -> t
+ src/draft4.json view
@@ -0,0 +1,150 @@+{+ "id": "http://json-schema.org/draft-04/schema#",+ "$schema": "http://json-schema.org/draft-04/schema#",+ "description": "Core schema meta-schema",+ "definitions": {+ "schemaArray": {+ "type": "array",+ "minItems": 1,+ "items": { "$ref": "#" }+ },+ "positiveInteger": {+ "type": "integer",+ "minimum": 0+ },+ "positiveIntegerDefault0": {+ "allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]+ },+ "simpleTypes": {+ "enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]+ },+ "stringArray": {+ "type": "array",+ "items": { "type": "string" },+ "minItems": 1,+ "uniqueItems": true+ }+ },+ "type": "object",+ "properties": {+ "id": {+ "type": "string",+ "format": "uri"+ },+ "$schema": {+ "type": "string",+ "format": "uri"+ },+ "title": {+ "type": "string"+ },+ "description": {+ "type": "string"+ },+ "default": {},+ "multipleOf": {+ "type": "number",+ "minimum": 0,+ "exclusiveMinimum": true+ },+ "maximum": {+ "type": "number"+ },+ "exclusiveMaximum": {+ "type": "boolean",+ "default": false+ },+ "minimum": {+ "type": "number"+ },+ "exclusiveMinimum": {+ "type": "boolean",+ "default": false+ },+ "maxLength": { "$ref": "#/definitions/positiveInteger" },+ "minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },+ "pattern": {+ "type": "string",+ "format": "regex"+ },+ "additionalItems": {+ "anyOf": [+ { "type": "boolean" },+ { "$ref": "#" }+ ],+ "default": {}+ },+ "items": {+ "anyOf": [+ { "$ref": "#" },+ { "$ref": "#/definitions/schemaArray" }+ ],+ "default": {}+ },+ "maxItems": { "$ref": "#/definitions/positiveInteger" },+ "minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },+ "uniqueItems": {+ "type": "boolean",+ "default": false+ },+ "maxProperties": { "$ref": "#/definitions/positiveInteger" },+ "minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },+ "required": { "$ref": "#/definitions/stringArray" },+ "additionalProperties": {+ "anyOf": [+ { "type": "boolean" },+ { "$ref": "#" }+ ],+ "default": {}+ },+ "definitions": {+ "type": "object",+ "additionalProperties": { "$ref": "#" },+ "default": {}+ },+ "properties": {+ "type": "object",+ "additionalProperties": { "$ref": "#" },+ "default": {}+ },+ "patternProperties": {+ "type": "object",+ "additionalProperties": { "$ref": "#" },+ "default": {}+ },+ "dependencies": {+ "type": "object",+ "additionalProperties": {+ "anyOf": [+ { "$ref": "#" },+ { "$ref": "#/definitions/stringArray" }+ ]+ }+ },+ "enum": {+ "type": "array",+ "minItems": 1,+ "uniqueItems": true+ },+ "type": {+ "anyOf": [+ { "$ref": "#/definitions/simpleTypes" },+ {+ "type": "array",+ "items": { "$ref": "#/definitions/simpleTypes" },+ "minItems": 1,+ "uniqueItems": true+ }+ ]+ },+ "allOf": { "$ref": "#/definitions/schemaArray" },+ "anyOf": { "$ref": "#/definitions/schemaArray" },+ "oneOf": { "$ref": "#/definitions/schemaArray" },+ "not": { "$ref": "#" }+ },+ "dependencies": {+ "exclusiveMaximum": [ "maximum" ],+ "exclusiveMinimum": [ "minimum" ]+ },+ "default": {}+}
+ test/Local.hs view
@@ -0,0 +1,54 @@++module Main where++import Control.Applicative+import Control.Monad (unless)+import Data.Aeson+import Data.List (isSuffixOf)+import Data.Monoid+import System.Directory (getDirectoryContents)+import Test.Tasty (TestTree, defaultMain, testGroup)+import qualified Test.Tasty.HUnit as HU+import Test.Tasty.QuickCheck (testProperty)++-- Examples+import qualified CustomSchema+import qualified Full+import qualified PrettyShowFailure+import qualified Simple++import Data.JsonSchema.Draft4+import Local.Failure (correctPaths)+import Local.Filesystem (fetchFromFilesystem)+import Local.Reference (referenceTests)+import Shared (isLocal, readSchemaTests, toTest)++dir :: String+dir = "JSON-Schema-Test-Suite/tests/draft4"++main :: IO ()+main = do+ filenames <- filter isLocal . filter (".json" `isSuffixOf`) <$> getDirectoryContents dir+ ts <- readSchemaTests dir filenames+ defaultMain . testGroup "Tests not requiring an HTTP server" $+ testGroup "Check that examples compile and don't throw errors" exampleTests+ : testGroup "QuickCheck tests" quickCheckTests+ : testGroup "Report the path to invalid data correctly" correctPaths+ : testGroup "Test the referencesViaFilesystem function" fetchFromFilesystem+ : testGroup "Test the Reference module" referenceTests+ : fmap toTest ts++quickCheckTests :: [TestTree]+quickCheckTests =+ [testProperty "Invert schemas through JSON without change" invertSchema]+ where+ invertSchema :: Schema -> Bool+ invertSchema a = Just a == decode (encode a)++exampleTests :: [TestTree]+exampleTests =+ [ HU.testCase "CustomSchema example" CustomSchema.example+ , HU.testCase "Full example" Full.example+ , HU.testCase "PrettyShowFailure example" PrettyShowFailure.example+ , HU.testCase "Simple example" Simple.example+ ]
+ test/Local/Failure.hs view
@@ -0,0 +1,55 @@++module Local.Failure where++import Data.Aeson+import qualified Data.Aeson.Pointer as P+import Data.Monoid+import Test.Tasty (TestTree)+import qualified Test.Tasty.HUnit as HU++import Data.JsonSchema.Draft4+import qualified Data.Validator.Draft4 as VA++correctPaths :: [TestTree]+correctPaths =+ [ HU.testCase "Items object" itemsObject+ , HU.testCase "Items array" itemsArray+ ]++itemsObject :: IO ()+itemsObject = HU.assertEqual "Path to invalid data"+ (P.Pointer [P.Token "0"])+ (_invalidOffendingData failure)+ where+ [failure] = runValidate (ReferencedSchemas schema mempty)+ sw (toJSON [[True, True]])++ schema :: Schema+ schema = emptySchema+ { _schemaItems = Just (VA.ItemsObject (emptySchema { _schemaUniqueItems = Just True }))+ }++ sw :: SchemaWithURI Schema+ sw = SchemaWithURI+ { _swSchema = schema+ , _swURI = Nothing+ }++itemsArray :: IO ()+itemsArray = HU.assertEqual "Path to invalid data"+ (P.Pointer [P.Token "0"])+ (_invalidOffendingData failure)+ where+ [failure] = runValidate (ReferencedSchemas schema mempty)+ sw (toJSON [[True, True]])++ schema :: Schema+ schema = emptySchema+ { _schemaItems = Just (VA.ItemsArray [emptySchema { _schemaUniqueItems = Just True }])+ }++ sw :: SchemaWithURI Schema+ sw = SchemaWithURI+ { _swSchema = schema+ , _swURI = Nothing+ }
+ test/Local/Filesystem.hs view
@@ -0,0 +1,56 @@++module Local.Filesystem where++import Control.Applicative+import Data.Aeson+import Data.Monoid+import Test.Tasty (TestTree)+import qualified Test.Tasty.HUnit as HU+import Data.Text (Text)++import Data.JsonSchema.Draft4++fetchFromFilesystem :: [TestTree]+fetchFromFilesystem =+ [ HU.testCase+ "readFile exceptions are turned into Lefts"+ readFileExceptions++ , HU.testCase+ "Relative reference to local file"+ (resolve "test/Local/schema.json")+ , HU.testCase+ "Chained relative references to local files"+ (resolve "./test/Local/schema-with-ref.json")+ ]++readFileExceptions :: IO ()+readFileExceptions = do+ res <- referencesViaFilesystem (SchemaWithURI schema Nothing)+ case res of+ Left (FSReadFailure _) -> pure ()+ a -> error (msg <> show a)+ where+ schema :: Schema+ schema = emptySchema { _schemaRef = Just "does-not-exist.json" }++ msg :: String+ msg = "expected referencesViaFilesystem to return ReadFailure,"+ <> " instead got: "++-- * Helpers++resolve :: Text -> IO ()+resolve ref = do+ let schema = emptySchema { _schemaRef = Just ref }+ res <- fetchFilesystemAndValidate (SchemaWithURI schema Nothing) badData+ case res of+ Left (FVData _) -> pure ()+ a -> error (msg <> show a)+ where+ badData :: Value+ badData = toJSON [True, True]++ msg :: String+ msg = "expected fetchFilesystemAndValidate to return"+ <> " Left (FVData [_]), instead got: "
+ test/Local/Reference.hs view
@@ -0,0 +1,51 @@++module Local.Reference where++import Test.Tasty (TestTree)+import qualified Test.Tasty.HUnit as HU++import Data.Validator.Reference++referenceTests :: [TestTree]+referenceTests =+ [ HU.testCase "updateResolutionScope test cases" updateResolutionScopeTests+ , HU.testCase "resolveReference test cases" resolveReferenceTests+ ]++updateResolutionScopeTests :: IO ()+updateResolutionScopeTests = do+ HU.assertEqual+ "case 1 result"+ Nothing+ (updateResolutionScope Nothing Nothing)+ HU.assertEqual+ "case 2 result"+ Nothing+ (updateResolutionScope Nothing (Just "#"))+ HU.assertEqual+ "case 3 result"+ (Just "foo")+ (updateResolutionScope Nothing (Just "foo"))+ HU.assertEqual+ "case 4 result"+ (Just "/./bar") -- TODO: Normalize after updateResolutionScope.+ (updateResolutionScope (Just "/foo") (Just "./bar"))++resolveReferenceTests :: IO ()+resolveReferenceTests = do+ HU.assertEqual+ "case 1 result"+ (Just "/bar", Nothing)+ (resolveReference (Just "/foo") "bar")+ HU.assertEqual+ "case 2 result"+ (Just "/baz", Nothing)+ (resolveReference (Just "/foo/bar") "/baz")+ HU.assertEqual+ "case 3 result"+ (Nothing, Just "/bar")+ (resolveReference Nothing "#/bar")+ HU.assertEqual+ "case 4 result"+ (Just "/foo", Just "/bar")+ (resolveReference (Just "/foo") "#/bar")
+ test/Local/schema-with-ref.json view
@@ -0,0 +1,3 @@+{+ "$ref": "./schema.json"+}
+ test/Local/schema.json view
@@ -0,0 +1,3 @@+{+ "uniqueItems": true+}
+ test/Remote.hs view
@@ -0,0 +1,26 @@+module Main where++import Control.Applicative+import Control.Concurrent.Async (withAsync)+import Data.List (isSuffixOf)+import Network.Wai.Application.Static (defaultFileServerSettings,+ staticApp)+import Network.Wai.Handler.Warp (run)+import System.Directory (getDirectoryContents)+import Test.Tasty (defaultMain, testGroup)++import Shared (isLocal, readSchemaTests,+ toTest)++dir :: String+dir = "JSON-Schema-Test-Suite/tests/draft4"++main :: IO ()+main = withAsync serve $ \_ -> do+ filenames <- filter (not . isLocal) . filter (".json" `isSuffixOf`)+ <$> getDirectoryContents dir+ ts <- readSchemaTests dir filenames+ defaultMain . testGroup "Tests that run an HTTP server" $ toTest <$> ts++serve :: IO ()+serve = run 1234 . staticApp . defaultFileServerSettings $ "JSON-Schema-Test-Suite/remotes"
@@ -0,0 +1,99 @@+{-# LANGUAGE DeriveGeneric #-}++module Shared where++import Control.Applicative+import Control.Monad+import Data.Aeson+import Data.Aeson.TH+import qualified Data.ByteString.Lazy as LBS+import Data.Char (toLower)+import Data.Monoid+import Data.Text (Text)+import qualified Data.Text as T+import GHC.Generics+import System.FilePath ((</>))+import Test.Tasty (TestTree)+import qualified Test.Tasty.HUnit as HU++import qualified Data.JsonSchema.Draft4 as D4++isLocal :: String -> Bool+isLocal file = (file /= "definitions.json")+ && (file /= "ref.json")+ && (file /= "refRemote.json")++data SchemaTest = SchemaTest+ { _stDescription :: Text+ , _stSchema :: D4.Schema+ , _stCases :: [SchemaTestCase]+ }++data SchemaTestCase = SchemaTestCase+ { _scDescription :: Text+ , _scData :: Value+ , _scValid :: Bool+ } deriving Generic++instance FromJSON SchemaTest where+ parseJSON = withObject "SchemaTest" $ \o -> SchemaTest+ <$> o .: "description"+ <*> o .: "schema"+ <*> o .: "tests" -- Perhaps "cases" would have been a more descriptive key.++instance FromJSON SchemaTestCase where+ parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fmap toLower . drop 3 }++readSchemaTests :: String -> [String] -> IO [SchemaTest]+readSchemaTests dir jsonFiles = concatMapM fileToCases jsonFiles+ where+ -- Each file contains an array of SchemaTests, not just one.+ fileToCases :: String -> IO [SchemaTest]+ fileToCases name = do+ let fullPath = dir </> name+ jsonBS <- LBS.readFile fullPath+ case eitherDecode jsonBS of+ Left e -> fail $ "couldn't parse file '" <> fullPath <> "': " <> e+ Right schemaTests -> pure $ prependFileName name <$> schemaTests++ prependFileName :: String -> SchemaTest -> SchemaTest+ prependFileName fileName s = s+ { _stDescription = T.pack fileName <> ": " <> _stDescription s+ }++ concatMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b]+ concatMapM f xs = liftM concat (mapM f xs)++toTest :: SchemaTest -> TestTree+toTest st =+ HU.testCase (T.unpack (_stDescription st)) $ do+ forM_ (_stCases st) $ \sc -> do+ g <- assertRight =<< D4.referencesViaHTTP (D4.SchemaWithURI (_stSchema st) Nothing)+ validate <- assertRight . D4.checkSchema g $ D4.SchemaWithURI (_stSchema st) Nothing+ let res = validate (_scData sc)+ if _scValid sc+ then assertValid sc res+ else assertInvalid sc res++assertValid :: SchemaTestCase -> [D4.Invalid] -> HU.Assertion+assertValid _ [] = pure ()+assertValid sc errs =+ HU.assertFailure $ unlines+ [ " Failed to validate data"+ , " Description: " <> T.unpack (_scDescription sc)+ , " Data: " <> show (_scData sc)+ , " Validation failures: " <> show errs+ ]++assertInvalid :: SchemaTestCase -> [D4.Invalid] -> HU.Assertion+assertInvalid sc [] =+ HU.assertFailure $ unlines+ [ " Validated invalid data"+ , " Description: " <> T.unpack (_scDescription sc)+ , " Data: " <> show (_scData sc)+ ]+assertInvalid _ _ = pure ()++assertRight :: (Show a) => Either a b -> IO b+assertRight (Left e) = HU.assertFailure (show e) >> fail "assertRight failed"+assertRight (Right b) = pure b
− tests/Local.hs
@@ -1,76 +0,0 @@--module Main where--import Control.Applicative-import Control.Monad (unless)-import Data.Aeson-import qualified Data.Aeson.Pointer as P-import Data.List (isSuffixOf)-import Data.Monoid-import System.Directory (getDirectoryContents)-import Test.Tasty (TestTree, defaultMain, testGroup)-import qualified Test.Tasty.HUnit as HU-import Test.Tasty.QuickCheck (testProperty)--import Data.JsonSchema.Draft4 (Failure(..), Schema(..),- SchemaCache(..), SchemaContext(..),- emptySchema, runValidate)-import qualified Data.Validator.Draft4 as VA-import Shared (isLocal, readSchemaTests, toTest)--dir :: String-dir = "JSON-Schema-Test-Suite/tests/draft4"--main :: IO ()-main = do- filenames <- filter isLocal . filter (".json" `isSuffixOf`) <$> getDirectoryContents dir- ts <- readSchemaTests dir filenames- defaultMain . testGroup "Tests not requiring an HTTP server" $- testProperty "Invert schemas through JSON without change" invertSchema- : testGroup "Make paths to invalid data correctly" correctPaths- : fmap toTest ts--invertSchema :: Schema -> Bool-invertSchema a = Just a == decode (encode a)--correctPaths :: [TestTree]-correctPaths =- [ HU.testCase "Items object" itemsObject- , HU.testCase "Items array" itemsArray- ]--itemsObject :: IO ()-itemsObject = HU.assertEqual "Path to invalid data"- (P.Pointer [P.Token "0"])- (_failureOffendingData failure)- where- [failure] = runValidate (SchemaCache schema mempty) sc (toJSON [[True, True]])-- schema :: Schema- schema = emptySchema- { _schemaItems = Just (VA.ItemsObject (emptySchema { _schemaUniqueItems = Just True }))- }-- sc :: SchemaContext Schema- sc = SchemaContext- { _scURI = Nothing- , _scSchema = schema- }--itemsArray :: IO ()-itemsArray = HU.assertEqual "Path to invalid data"- (P.Pointer [P.Token "0"])- (_failureOffendingData failure)- where- [failure] = runValidate (SchemaCache schema mempty) sc (toJSON [[True, True]])-- schema :: Schema- schema = emptySchema- { _schemaItems = Just (VA.ItemsArray [emptySchema { _schemaUniqueItems = Just True }])- }-- sc :: SchemaContext Schema- sc = SchemaContext- { _scURI = Nothing- , _scSchema = schema- }
− tests/Remote.hs
@@ -1,26 +0,0 @@-module Main where--import Control.Applicative-import Control.Concurrent.Async (withAsync)-import Data.List (isSuffixOf)-import Network.Wai.Application.Static (defaultFileServerSettings,- staticApp)-import Network.Wai.Handler.Warp (run)-import System.Directory (getDirectoryContents)-import Test.Tasty (defaultMain, testGroup)--import Shared (isLocal, readSchemaTests,- toTest)--dir :: String-dir = "JSON-Schema-Test-Suite/tests/draft4"--main :: IO ()-main = withAsync serve $ \_ -> do- filenames <- filter (not . isLocal) . filter (".json" `isSuffixOf`)- <$> getDirectoryContents dir- ts <- readSchemaTests dir filenames- defaultMain . testGroup "Tests that run an HTTP server" $ toTest <$> ts--serve :: IO ()-serve = run 1234 . staticApp . defaultFileServerSettings $ "JSON-Schema-Test-Suite/remotes"
@@ -1,99 +0,0 @@-{-# LANGUAGE DeriveGeneric #-}--module Shared where--import Control.Applicative-import Control.Monad-import Data.Aeson-import Data.Aeson.TH-import qualified Data.ByteString.Lazy as LBS-import Data.Char (toLower)-import Data.Monoid-import Data.Text (Text)-import qualified Data.Text as T-import GHC.Generics-import System.FilePath ((</>))-import Test.Tasty (TestTree)-import qualified Test.Tasty.HUnit as HU--import qualified Data.JsonSchema.Draft4 as D4--isLocal :: String -> Bool-isLocal file = (file /= "definitions.json")- && (file /= "ref.json")- && (file /= "refRemote.json")--data SchemaTest = SchemaTest- { _stDescription :: Text- , _stSchema :: D4.Schema- , _stCases :: [SchemaTestCase]- }--data SchemaTestCase = SchemaTestCase- { _scDescription :: Text- , _scData :: Value- , _scValid :: Bool- } deriving Generic--instance FromJSON SchemaTest where- parseJSON = withObject "SchemaTest" $ \o -> SchemaTest- <$> o .: "description"- <*> o .: "schema"- <*> o .: "tests" -- Perhaps "cases" would have been a more descriptive key.--instance FromJSON SchemaTestCase where- parseJSON = genericParseJSON defaultOptions { fieldLabelModifier = fmap toLower . drop 3 }--readSchemaTests :: String -> [String] -> IO [SchemaTest]-readSchemaTests dir jsonFiles = concatMapM fileToCases jsonFiles- where- -- Each file contains an array of SchemaTests, not just one.- fileToCases :: String -> IO [SchemaTest]- fileToCases name = do- let fullPath = dir </> name- jsonBS <- LBS.readFile fullPath- case eitherDecode jsonBS of- Left e -> fail $ "couldn't parse file '" <> fullPath <> "': " <> e- Right schemaTests -> pure $ prependFileName name <$> schemaTests-- prependFileName :: String -> SchemaTest -> SchemaTest- prependFileName fileName s = s- { _stDescription = T.pack fileName <> ": " <> _stDescription s- }-- concatMapM :: (Monad m) => (a -> m [b]) -> [a] -> m [b]- concatMapM f xs = liftM concat (mapM f xs)--toTest :: SchemaTest -> TestTree-toTest st =- HU.testCase (T.unpack (_stDescription st)) $ do- forM_ (_stCases st) $ \sc -> do- g <- assertRight =<< D4.fetchReferencedSchemas mempty (D4.SchemaContext Nothing (_stSchema st))- validate <- assertRight . D4.checkSchema g $ D4.SchemaContext Nothing (_stSchema st)- let res = validate (_scData sc)- if _scValid sc- then assertValid sc res- else assertInvalid sc res--assertValid :: SchemaTestCase -> [D4.Failure] -> HU.Assertion-assertValid _ [] = pure ()-assertValid sc errs =- HU.assertFailure $ unlines- [ " Failed to validate data"- , " Description: " <> T.unpack (_scDescription sc)- , " Data: " <> show (_scData sc)- , " Validation failures: " <> show errs- ]--assertInvalid :: SchemaTestCase -> [D4.Failure] -> HU.Assertion-assertInvalid sc [] =- HU.assertFailure $ unlines- [ " Validated invalid data"- , " Description: " <> T.unpack (_scDescription sc)- , " Data: " <> show (_scData sc)- ]-assertInvalid _ _ = pure ()--assertRight :: (Show a) => Either a b -> IO b-assertRight (Left e) = HU.assertFailure (show e) >> fail "assertRight failed"-assertRight (Right b) = pure b