git.iko.soy/typeable/hjsonschema
on master help sign in
Fetching improvemnts

Fix reference resolution defects, clean up the internals of fetching, add one-step validation functions, add more tests.

commit4db6441635df1565d3278db7a935493f1e6270c9 authorIan Grant Jeffries <ian@housejeffries.com> dateMon 18 Apr 2016, 10:18 (11 years ago) parent00b0734
16 files changed+484−232
README.md+1−1 examples/Standard.hs+7−10 hjsonschema.cabal+11−7 src/Data/JsonSchema/Draft4.hs+94−32 src/Data/JsonSchema/Draft4/Internal.hs+32−32 src/Data/JsonSchema/Fetch.hs+124−84 src/Data/Validator/Draft4/Any.hs+1−1 src/Data/Validator/Reference.hs+37−13 draft4.json → src/draft4.json tests/Local.hs+8−50 tests/Local/Failure.hs+55 tests/Local/Filesystem.hs+55 tests/Local/Reference.hs+51 tests/Local/schema-with-ref.json+3 tests/Local/schema.json+3 tests/Shared.hs+2−2
README.md+1−1
@@ -46,7 +46,7 @@ Will start an HTTP server temporarily on port 1234:
4646
4747## Notes
4848
49−+ `draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema).
49++ `src/draft4.json` is from commit # cc8ec81ce0abe2385ebd6c2a6f2d6deb646f874a [here](https://github.com/json-schema/json-schema).
5050
5151## Credits
5252
examples/Standard.hs+7−10
@@ -3,7 +3,6 @@
33module Standard where
44
55import Data.Aeson
6−import qualified Data.HashMap.Strict as H
76import qualified Data.Vector as V
87
98import qualified Data.JsonSchema.Draft4 as D4
@@ -11,12 +10,12 @@ import qualified Data.JsonSchema.Draft4 as D4
1110schema :: D4.Schema
1211schema = D4.emptySchema { D4._schemaUniqueItems = Just True }
1312
14−schemaContext :: D4.SchemaContext D4.Schema
15−schemaContext = D4.SchemaContext
16− { D4._scURI = Nothing
13+schemaContext :: D4.SchemaWithURI D4.Schema
14+schemaContext = D4.SchemaWithURI
15+ { D4._swSchema = schema
16+ , D4._swURI = Nothing
1717 -- ^ If your schema has relative references to other schemas
1818 -- then you'll need to give its URI here.
19− , D4._scSchema = schema
2019 }
2120
2221badData :: Value
@@ -25,7 +24,7 @@ badData = Array (V.fromList ["foo", "foo"])
2524example :: IO ()
2625example = do
2726 -- Since we know our schema doesn't reference any other schemas we could
28− -- skip this step and use @SchemaCache schema mempty@ instead.
27+ -- skip this step and use @ReferencedSchemas schema mempty@ instead.
2928 --
3029 -- If we make a mistake and out schema does include references to other
3130 -- schemas then those references will always return 'D4.RefResolution'
@@ -42,11 +41,9 @@ example = do
4241 _ -> error "We got a different failure than expected."
4342
4443 where
45− makeCache :: IO (D4.SchemaCache D4.Schema)
44+ makeCache :: IO (D4.ReferencedSchemas D4.Schema)
4645 makeCache = do
47− -- A HashMap of URIs (in the form of Text) to schemas.
48− let currentlyStoredSchemas = H.empty
49− res <- D4.fetchReferencedSchemas currentlyStoredSchemas schemaContext
46+ res <- D4.referencesViaHTTP schemaContext
5047 case res of
5148 Left _ -> error "Couldn't fetch referenced schemas."
5249 Right cache -> return cache
hjsonschema.cabal+11−7
@@ -11,10 +11,11 @@ build-type: Simple
1111cabal-version: >=1.10
1212tested-with: GHC == 7.8.4, GHC == 7.10.2
1313extra-source-files: changelog.txt
14− draft4.json
15− JSON-Schema-Test-Suite/tests/draft4/*.json
14+ JSON-Schema-Test-Suite/remotes/*
15+ JSON-Schema-Test-Suite/tests/draft4/*
1616 README.md
17− tests/Shared.hs
17+ src/draft4.json
18+ tests/Local/*.json
1819
1920library
2021 hs-source-dirs: src
@@ -45,6 +46,7 @@ library
4546 , bytestring >= 0.10 && < 0.11
4647 , containers >= 0.5 && < 0.6
4748 , file-embed >= 0.0.8 && < 0.0.10
49+ , filepath >= 1.3 && < 1.5
4850 , hjsonpointer >= 0.3 && < 0.4
4951 , http-client >= 0.4.9 && < 0.5
5052 , http-types >= 0.8 && < 0.10
@@ -60,13 +62,17 @@ test-suite local
6062 type: exitcode-stdio-1.0
6163 hs-source-dirs: tests
6264 main-is: Local.hs
63− other-modules: Shared
65+ other-modules: Local.Failure
66+ , Local.Filesystem
67+ , Local.Reference
68+ , Shared
6469 default-language: Haskell2010
6570 ghc-options: -Wall -fno-warn-orphans
6671 default-extensions: OverloadedStrings
6772 build-depends: aeson
6873 , base
6974 , bytestring
75+ , filepath
7076 , hjsonpointer
7177 , hjsonschema
7278 , text
@@ -74,7 +80,6 @@ test-suite local
7480 , unordered-containers
7581 , vector
7682 , directory >= 1.2 && < 1.3
77− , filepath >= 1.3 && < 1.5
7883 , HUnit >= 1.2 && < 1.4
7984 , tasty >= 0.11 && < 0.12
8085 , tasty-hunit >= 0.9 && < 0.10
@@ -92,11 +97,11 @@ test-suite remote
9297 , async
9398 , base
9499 , bytestring
100+ , filepath
95101 , hjsonschema
96102 , text
97103 , vector
98104 , directory
99− , filepath
100105 , HUnit
101106 , tasty
102107 , tasty-hunit
@@ -119,7 +124,6 @@ test-suite example
119124 , tasty
120125 , tasty-hunit
121126 , text
122− , unordered-containers
123127 , vector
124128
125129source-repository head
src/Data/JsonSchema/Draft4.hs+94−32
@@ -4,67 +4,129 @@
44module Data.JsonSchema.Draft4
55 ( Schema(..)
66 , emptySchema
7− , checkSchema
7+
8+ -- * One-step validation
9+ , HTTPValidationFailure(..)
10+ , fetchHTTPAndValidate
11+ , FilesystemValidationFailure(..)
12+ , fetchFilesystemAndValidate
813
914 -- * Fetching tools
10− , SchemaContext(..)
11− , SchemaCache(..)
12− , fetchReferencedSchemas
15+ , SchemaWithURI(..)
16+ , ReferencedSchemas(..)
17+ , HTTPFailure(..)
18+ , referencesViaHTTP
19+ , FilesystemFailure(..)
20+ , referencesViaFilesystem
1321
1422 -- * Failure
1523 , Failure(..)
1624 , ValidatorChain(..)
1725
1826 -- * Other Draft 4 things exported just in case
27+ , checkSchema
1928 , schemaValidity
2029 , IN.runValidate
30+ , draft4Spec
2131 ) where
2232
33+import Control.Arrow (left)
2334import Data.Aeson
2435import qualified Data.ByteString.Lazy as LBS
2536import Data.FileEmbed
2637import qualified Data.HashMap.Strict as H
38+import Data.List.NonEmpty (NonEmpty)
39+import qualified Data.List.NonEmpty as N
2740import Data.Maybe (fromMaybe)
2841
2942import Data.JsonSchema.Draft4.Failure
3043import qualified Data.JsonSchema.Draft4.Internal as IN
3144import Data.JsonSchema.Draft4.Schema
32−import Data.JsonSchema.Fetch (SchemaCache(..),
33− SchemaContext(..),
34− URISchemaMap)
45+import Data.JsonSchema.Fetch (FilesystemFailure(..),
46+ HTTPFailure(..),
47+ ReferencedSchemas(..),
48+ SchemaWithURI(..))
3549import qualified Data.JsonSchema.Fetch as FE
36−import Data.Validator.Reference (baseAndFragment)
37−import Import
50+
51+data HTTPValidationFailure
52+ = HVRequest HTTPFailure
53+ | HVSchema (NonEmpty Failure)
54+ | HVData (NonEmpty Failure)
55+ deriving Show
56+
57+fetchHTTPAndValidate
58+ :: SchemaWithURI Schema
59+ -> Value
60+ -> IO (Either HTTPValidationFailure ())
61+fetchHTTPAndValidate sw v = do
62+ res <- referencesViaHTTP sw
63+ pure (g =<< f =<< left HVRequest res)
64+ where
65+ f :: ReferencedSchemas Schema
66+ -> Either HTTPValidationFailure (Value -> [Failure])
67+ f references = left HVSchema (checkSchema references sw)
68+
69+ g :: (Value -> [Failure]) -> Either HTTPValidationFailure ()
70+ g validate = case N.nonEmpty (validate v) of
71+ Nothing -> Right ()
72+ Just failures -> Left (HVData failures)
73+
74+data FilesystemValidationFailure
75+ = FVFilesystem FilesystemFailure
76+ | FVSchema (NonEmpty Failure)
77+ | FVData (NonEmpty Failure)
78+ deriving Show
79+
80+fetchFilesystemAndValidate
81+ :: SchemaWithURI Schema
82+ -> Value
83+ -> IO (Either FilesystemValidationFailure ())
84+fetchFilesystemAndValidate sw v = do
85+ res <- referencesViaFilesystem sw
86+ pure (g =<< f =<< left FVFilesystem res)
87+ where
88+ f :: ReferencedSchemas Schema
89+ -> Either FilesystemValidationFailure (Value -> [Failure])
90+ f references = left FVSchema (checkSchema references sw)
91+
92+ g :: (Value -> [Failure]) -> Either FilesystemValidationFailure ()
93+ g validate = case N.nonEmpty (validate v) of
94+ Nothing -> Right ()
95+ Just failures -> Left (FVData failures)
3896
3997-- | Check the validity of a schema and return a function to validate data.
4098checkSchema
41− :: SchemaCache Schema
42− -> SchemaContext Schema
43− -> Either [Failure] (Value -> [Failure])
44−checkSchema sg sc =
45− case schemaValidity (_scSchema sc) of
46− [] -> Right (IN.runValidate sg sc)
47− es -> Left es
48−
49−fetchReferencedSchemas
50− :: URISchemaMap Schema
51− -> SchemaContext Schema
52− -> IO (Either Text (SchemaCache Schema))
53−fetchReferencedSchemas =
54− FE.fetchReferencedSchemas IN.embedded _schemaId _schemaRef
99+ :: ReferencedSchemas Schema
100+ -> SchemaWithURI Schema
101+ -> Either (NonEmpty Failure) (Value -> [Failure])
102+checkSchema referenced schemaWithURI =
103+ case N.nonEmpty . schemaValidity . _swSchema $ schemaWithURI of
104+ Nothing -> Right (IN.runValidate referenced schemaWithURI)
105+ Just failures -> Left failures
106+
107+draft4Spec :: FE.Spec Schema
108+draft4Spec = FE.Spec IN.embedded _schemaId _schemaRef
109+
110+referencesViaHTTP
111+ :: SchemaWithURI Schema
112+ -> IO (Either HTTPFailure (ReferencedSchemas Schema))
113+referencesViaHTTP = FE.referencesViaHTTP' draft4Spec
114+
115+referencesViaFilesystem
116+ :: SchemaWithURI Schema
117+ -> IO (Either FilesystemFailure (ReferencedSchemas Schema))
118+referencesViaFilesystem = FE.referencesViaFilesystem' draft4Spec
55119
56120-- | In normal situations just use 'checkSchema', which is a combination of
57121-- 'schemaValidity' and 'runValidate'.
58122schemaValidity :: Schema -> [Failure]
59−schemaValidity = IN.runValidate cache (SchemaContext Nothing d4) . toJSON
123+schemaValidity = IN.runValidate referenced (SchemaWithURI d4 Nothing) . toJSON
60124 where
61125 d4 :: Schema
62126 d4 = fromMaybe (error "Schema decode failed (this should never happen)")
63− . decode . LBS.fromStrict $ $(embedFile "draft4.json")
64−
65− -- @fst . baseAndFragment@ is necessary to remove the trailing "#",
66− -- otherwise cache lookups to resolve internal references will fail.
67− cache :: SchemaCache Schema
68− cache = SchemaCache d4 $ case _schemaId d4 >>= fst . baseAndFragment of
69− Nothing -> mempty
70− Just uri -> H.singleton uri d4
127+ . decode . LBS.fromStrict $ $(embedFile "src/draft4.json")
128+
129+ referenced :: ReferencedSchemas Schema
130+ referenced = ReferencedSchemas
131+ d4
132+ (H.singleton "http://json-schema.org/draft-04/schema" d4)
src/Data/JsonSchema/Draft4/Internal.hs+32−32
@@ -10,8 +10,8 @@ import Data.Scientific
1010
1111import Data.JsonSchema.Draft4.Failure
1212import Data.JsonSchema.Draft4.Schema
13−import Data.JsonSchema.Fetch (SchemaCache(..),
14− SchemaContext(..))
13+import Data.JsonSchema.Fetch (ReferencedSchemas(..),
14+ SchemaWithURI(..))
1515import qualified Data.Validator.Draft4.Any as AN
1616import qualified Data.Validator.Draft4.Array as AR
1717import qualified Data.Validator.Draft4.Number as NU
@@ -19,7 +19,7 @@ import qualified Data.Validator.Draft4.Object as OB
1919import qualified Data.Validator.Draft4.String as ST
2020import Data.Validator.Failure (modFailure, setFailure)
2121import qualified Data.Validator.Failure as FR
22−import Data.Validator.Reference (newResolutionScope)
22+import Data.Validator.Reference (updateResolutionScope)
2323import Import
2424
2525-- For GHCs before 7.10:
@@ -77,8 +77,8 @@ embedded schema = concat
7777-- | In normal situations just use 'checkSchema', which is a combination of
7878-- 'schemaValidity' and 'runValidate'.
7979runValidate
80− :: SchemaCache Schema
81− -> SchemaContext Schema
80+ :: ReferencedSchemas Schema
81+ -> SchemaWithURI Schema
8282 -> Value
8383 -> [Failure]
8484runValidate = (fmap.fmap.fmap.fmap) specializeForDraft4 validateAny
@@ -88,11 +88,11 @@ runValidate = (fmap.fmap.fmap.fmap) specializeForDraft4 validateAny
8888--------------------------------------------------
8989
9090validateAny
91− :: SchemaCache Schema
92− -> SchemaContext Schema
91+ :: ReferencedSchemas Schema
92+ -> SchemaWithURI Schema
9393 -> Value
9494 -> [FR.Failure ValidatorChain]
95−validateAny cache sc x = concat
95+validateAny referenced sw x = concat
9696 [ f _schemaEnum (setFailure Enum) (fmap maybeToList . AN.enumVal)
9797 , f _schemaType (setFailure TypeValidator) (fmap maybeToList . AN.typeVal)
9898 , f _schemaAllOf (modFailure AllOf) (AN.allOf recurse)
@@ -105,15 +105,15 @@ validateAny cache sc x = concat
105105 specificValidators :: [FR.Failure ValidatorChain]
106106 specificValidators =
107107 case x of
108− Number y -> validateNumber (_scSchema sc) y
109− String y -> validateString (_scSchema sc) y
110− Array y -> validateArray cache sc y
111− Object y -> validateObject cache sc y
108+ Number y -> validateNumber (_swSchema sw) y
109+ String y -> validateString (_swSchema sw) y
110+ Array y -> validateArray referenced sw y
111+ Object y -> validateObject referenced sw y
112112 _ -> mempty
113113
114− f = runSingle (_scSchema sc) x
114+ f = runSingle (_swSchema sw) x
115115
116− recurse = descendNextLevel cache sc
116+ recurse = descendNextLevel referenced sw
117117
118118 -- Since the results of the 'AN.ref' validator are fairly complicated [1]
119119 -- it's simpler not to use our 'f' helper function for it.
@@ -122,23 +122,23 @@ validateAny cache sc x = concat
122122 -- if resolving the reference itself failed.
123123 refFailures :: [FR.Failure ValidatorChain]
124124 refFailures =
125− case _schemaRef (_scSchema sc) of
125+ case _schemaRef (_swSchema sw) of
126126 Nothing -> mempty
127127 Just reference ->
128128 maybe [FR.Failure RefResolution (toJSON reference) mempty]
129129 (fmap (modFailure Ref))
130130 $ AN.ref scope
131131 getReference
132− (\a b -> validateAny cache (SchemaContext a b))
132+ (\a b -> validateAny referenced (SchemaWithURI b a))
133133 reference
134134 x
135135 where
136136 scope :: Maybe Text
137− scope = newResolutionScope (_scURI sc) (_schemaId (_scSchema sc))
137+ scope = updateResolutionScope (_swURI sw) (_schemaId (_swSchema sw))
138138
139139 getReference :: Maybe Text -> Maybe Schema
140− getReference Nothing = Just (_startingSchema cache)
141− getReference (Just t) = H.lookup t (_cachedSchemas cache)
140+ getReference Nothing = Just (_rsStarting referenced)
141+ getReference (Just t) = H.lookup t (_rsSchemaMap referenced)
142142
143143validateString
144144 :: Schema
@@ -179,11 +179,11 @@ validateNumber schema x = concat
179179 fMin NU.ExclusiveMinimum = ExclusiveMinimum
180180
181181validateArray
182− :: SchemaCache Schema
183− -> SchemaContext Schema
182+ :: ReferencedSchemas Schema
183+ -> SchemaWithURI Schema
184184 -> Vector Value
185185 -> [FR.Failure ValidatorChain]
186−validateArray cache (SchemaContext mUri schema) x = concat
186+validateArray referenced (SchemaWithURI schema mUri) x = concat
187187 [ f _schemaMaxItems (setFailure MaxItems) (fmap maybeToList . AR.maxItems)
188188 , f _schemaMinItems (setFailure MinItems) (fmap maybeToList . AR.minItems)
189189 , f _schemaUniqueItems (setFailure UniqueItems) (fmap maybeToList . AR.uniqueItems)
@@ -194,18 +194,18 @@ validateArray cache (SchemaContext mUri schema) x = concat
194194 where
195195 f = runSingle schema x
196196
197− recurse = descendNextLevel cache (SchemaContext mUri schema)
197+ recurse = descendNextLevel referenced (SchemaWithURI schema mUri)
198198
199199 fItems (AR.Items err) = Items err
200200 fItems AR.AdditionalItemsBoolFailure = AdditionalItemsBool
201201 fItems (AR.AdditionalItemsObjectFailure err) = AdditionalItemsObject err
202202
203203validateObject
204− :: SchemaCache Schema
205− -> SchemaContext Schema
204+ :: ReferencedSchemas Schema
205+ -> SchemaWithURI Schema
206206 -> HashMap Text Value
207207 -> [FR.Failure ValidatorChain]
208−validateObject cache (SchemaContext mUri schema) x = concat
208+validateObject referenced (SchemaWithURI schema mUri) x = concat
209209 [ f _schemaMaxProperties (setFailure MaxProperties) (fmap maybeToList . OB.maxProperties)
210210 , f _schemaMinProperties (setFailure MinProperties) (fmap maybeToList . OB.minProperties)
211211 , f _schemaRequired (setFailure Required) (fmap maybeToList . OB.required)
@@ -232,7 +232,7 @@ validateObject cache (SchemaContext mUri schema) x = concat
232232 where
233233 f = runSingle schema x
234234
235− recurse = descendNextLevel cache (SchemaContext mUri schema)
235+ recurse = descendNextLevel referenced (SchemaWithURI schema mUri)
236236
237237 fDeps (OB.SchemaDependencyFailure err) = SchemaDependency err
238238 fDeps OB.PropertyDependencyFailure = PropertyDependency
@@ -258,16 +258,16 @@ validateObject cache (SchemaContext mUri schema) x = concat
258258--------------------------------------------------
259259
260260descendNextLevel
261− :: SchemaCache Schema
262− -> SchemaContext Schema
261+ :: ReferencedSchemas Schema
262+ -> SchemaWithURI Schema
263263 -> Schema
264264 -> Value
265265 -> [FR.Failure ValidatorChain]
266−descendNextLevel cache (SchemaContext mUri schema) =
267− validateAny cache . SchemaContext scope
266+descendNextLevel referenced (SchemaWithURI schema mUri) =
267+ validateAny referenced . flip SchemaWithURI scope
268268 where
269269 scope :: Maybe Text
270− scope = newResolutionScope mUri (_schemaId schema)
270+ scope = updateResolutionScope mUri (_schemaId schema)
271271
272272runSingle
273273 :: Schema
src/Data/JsonSchema/Fetch.hs+124−84
@@ -2,22 +2,34 @@
22
33module Data.JsonSchema.Fetch where
44
5−import Control.Exception (SomeException(..), catch)
5+import Control.Arrow (left)
6+import Control.Exception (catch)
67import qualified Data.ByteString.Lazy as LBS
8+import qualified Data.ByteString as BS
79import qualified Data.HashMap.Strict as H
810import qualified Data.Text as T
911import Network.HTTP.Client
1012
11−import Data.Validator.Reference (isRemoteReference,
12− newResolutionScope,
13− resolveReference)
13+import Data.Validator.Reference (resolveReference,
14+ updateResolutionScope)
1415import Import
1516
1617-- For GHCs before 7.10:
1718import Prelude hiding (concat, sequence)
1819
19−data SchemaContext schema = SchemaContext
20− { _scURI :: !(Maybe Text)
20+--------------------------------------------------
21+-- * Types
22+--------------------------------------------------
23+
24+data Spec schema = Spec
25+ { _ssEmbedded :: schema -> [schema]
26+ , _ssGetId :: schema -> Maybe Text
27+ , _ssGetRef :: schema -> Maybe Text
28+ }
29+
30+data SchemaWithURI schema = SchemaWithURI
31+ { _swSchema :: !schema
32+ , _swURI :: !(Maybe Text)
2133 -- ^ Must not include a URI fragment, e.g. use
2234 -- "http://example.com/foo" not "http://example.com/foo#bar".
2335 --
@@ -26,108 +38,136 @@ data SchemaContext schema = SchemaContext
2638 -- when resolving references contained in the schema.
2739
2840 -- TODO: Make the no URI fragment requirement unnecessary.
29−
30− , _scSchema :: !schema
3141 } deriving (Eq, Show)
3242
3343-- | Keys are URIs (without URI fragments).
3444type URISchemaMap schema = HashMap Text schema
3545
36−data SchemaCache schema = SchemaCache
37− { _startingSchema :: !schema
46+data ReferencedSchemas schema = ReferencedSchemas
47+ { _rsStarting :: !schema
3848 -- ^ Used to resolve relative references.
39− , _cachedSchemas :: !(URISchemaMap schema)
49+ , _rsSchemaMap :: !(URISchemaMap schema)
4050 } deriving (Eq, Show)
4151
52+--------------------------------------------------
53+-- * Fetch via HTTP
54+--------------------------------------------------
55+
56+data HTTPFailure
57+ = HTTPParseFailure Text
58+ | RequestFailure HttpException
59+ deriving Show
60+
4261-- | Take a schema. Retrieve every document either it or its subschemas
43−-- include via the "$ref" keyword. Load a 'SchemaCache' out with them.
44−fetchReferencedSchemas
62+-- include via the "$ref" keyword.
63+referencesViaHTTP'
4564 :: forall schema. FromJSON schema
46− => (schema -> [schema])
47− -> (schema -> Maybe Text)
48− -> (schema -> Maybe Text)
49− -> URISchemaMap schema
50− -> SchemaContext schema
51− -> IO (Either Text (SchemaCache schema))
52−fetchReferencedSchemas embedded getId getRef cache sc = do
65+ => Spec schema
66+ -> SchemaWithURI schema
67+ -> IO (Either HTTPFailure (ReferencedSchemas schema))
68+referencesViaHTTP' spec sw = do
5369 manager <- newManager defaultManagerSettings
54− catch (Right <$> f manager) handler
70+ let f = referencesMethodAgnostic (get manager) spec sw
71+ catch (left HTTPParseFailure <$> f) handler
5572 where
56− f manager = fetchReferencedSchemas' embedded getId getRef
57− (simpleGET manager) cache sc
73+ get :: Manager -> Text -> IO LBS.ByteString
74+ get man url = do
75+ request <- parseUrl (T.unpack url)
76+ responseBody <$> httpLbs request man
5877
59− handler :: SomeException -> IO (Either Text (SchemaCache schema))
60− handler e = pure . Left . T.pack . show $ e
78+ handler
79+ :: HttpException
80+ -> IO (Either HTTPFailure (ReferencedSchemas schema))
81+ handler = pure . Left . RequestFailure
6182
62−-- | Based on 'Network.Http.Conduit.simpleHttp' from http-conduit.
63−simpleGET :: Manager -> Text -> IO LBS.ByteString
64−simpleGET man url = do
65− req <- parseUrl (T.unpack url)
66− responseBody <$> httpLbs req { requestHeaders = ("Connection", "close")
67− : requestHeaders req
68− } man
83+--------------------------------------------------
84+-- * Fetch via Filesystem
85+--------------------------------------------------
86+
87+data FilesystemFailure
88+ = FSParseFailure Text
89+ | ReadFailure IOError
90+ deriving Show
91+
92+referencesViaFilesystem'
93+ :: forall schema. FromJSON schema
94+ => Spec schema
95+ -> SchemaWithURI schema
96+ -> IO (Either FilesystemFailure (ReferencedSchemas schema))
97+referencesViaFilesystem' spec sw = catch (left FSParseFailure <$> f) handler
98+ where
99+ f :: IO (Either Text (ReferencedSchemas schema))
100+ f = referencesMethodAgnostic readFile' spec sw
101+
102+ readFile' :: Text -> IO LBS.ByteString
103+ readFile' = fmap LBS.fromStrict . (BS.readFile . T.unpack)
104+
105+ handler
106+ :: IOError
107+ -> IO (Either FilesystemFailure (ReferencedSchemas schema))
108+ handler = pure . Left . ReadFailure
109+
110+--------------------------------------------------
111+-- * Method Agnostic Fetching Tools
112+--------------------------------------------------
69113
70114-- | A version of 'fetchReferencedSchema's where the function to fetch
71115-- schemas is provided by the user. This allows restrictions to be added,
72116-- e.g. rejecting non-local URIs.
73−fetchReferencedSchemas'
117+referencesMethodAgnostic
118+ :: forall schema. FromJSON schema
119+ => (Text -> IO LBS.ByteString)
120+ -> Spec schema
121+ -> SchemaWithURI schema
122+ -> IO (Either Text (ReferencedSchemas schema))
123+referencesMethodAgnostic fetchRef spec sw =
124+ (fmap.fmap) (ReferencedSchemas (_swSchema sw))
125+ (foldFunction fetchRef spec mempty sw)
126+
127+foldFunction
74128 :: forall schema. FromJSON schema
75− => (schema -> [schema])
76− -> (schema -> Maybe Text)
77− -> (schema -> Maybe Text)
78− -> (Text -> IO LBS.ByteString)
129+ => (Text -> IO LBS.ByteString)
130+ -> Spec schema
79131 -> URISchemaMap schema
80− -> SchemaContext schema
81− -> IO (SchemaCache schema)
82−fetchReferencedSchemas' embedded getId getRef fetchRef cache sc = do
83− let startingCache = case _scURI sc of
84− Nothing -> cache
85− Just uri -> H.insert uri (_scSchema sc) cache
86− SchemaCache (_scSchema sc) <$> foldlM fetchRecursively
87− startingCache
88− (includeSubschemas embedded getId sc)
132+ -> SchemaWithURI schema
133+ -> IO (Either Text (URISchemaMap schema))
134+foldFunction fetchRef spec@(Spec _ _ getRef) referenced sw =
135+ foldlM f (Right referenced) (includeSubschemas spec sw)
89136 where
90− fetchRecursively
91− :: URISchemaMap schema
92− -> SchemaContext schema
93− -> IO (URISchemaMap schema)
94− fetchRecursively g (SchemaContext mUri schema) = do
95− -- Resolving the new scope is necessary here because of situations
96− -- like this:
97− --
98− -- {
99− -- "id": "http://localhost:1234/",
100− -- "items": {
101− -- "id": "folder/",
102− -- "items": {"$ref": "folderInteger.json"}
103− -- }
104− -- }
105− let scope = newResolutionScope mUri (getId schema)
106− case resolveReference scope <$> getRef schema of
107− Just (Just uri,_) ->
108− if not (isRemoteReference uri) || H.member uri g
109− then pure g
110− else do
111− bts <- fetchRef uri
112− case eitherDecode bts of
113− Left e -> ioError (userError e)
114− Right schm -> _cachedSchemas <$>
115− fetchReferencedSchemas' embedded getId getRef fetchRef
116− g (SchemaContext (Just uri) schm)
117− _ -> pure g
137+ f :: Either Text (URISchemaMap schema)
138+ -> SchemaWithURI schema
139+ -> IO (Either Text (URISchemaMap schema))
140+ f (Left e) _ = pure (Left e)
141+ f (Right g) (SchemaWithURI schema mUri) =
142+ case newRef of
143+ Nothing -> pure (Right g)
144+ Just uri -> do
145+ bts <- fetchRef uri
146+ case eitherDecode bts of
147+ Left e -> pure . Left . T.pack $ e
148+ Right schm -> foldFunction fetchRef spec (H.insert uri schm g)
149+ (SchemaWithURI schm (Just uri))
150+ where
151+ newRef :: Maybe Text
152+ newRef
153+ | Just (Just uri,_) <- resolveReference mUri <$> getRef schema
154+ = case H.lookup uri g of
155+ Nothing -> Just uri
156+ Just _ -> Nothing
157+ | otherwise = Nothing
118158
119159-- | Return the schema passed in as an argument, as well as every
120160-- subschema contained within it.
121161includeSubschemas
122162 :: forall schema.
123− (schema -> [schema])
124− -> (schema -> Maybe Text)
125− -> SchemaContext schema
126− -> [SchemaContext schema]
127−includeSubschemas embedded getId (SchemaContext mUri schema) =
128− SchemaContext mUri schema
129− : (includeSubschemas embedded getId =<< subSchemas)
163+ Spec schema
164+ -> SchemaWithURI schema
165+ -> [SchemaWithURI schema]
166+includeSubschemas spec@(Spec embedded getId _) (SchemaWithURI schema mUri) =
167+ SchemaWithURI schema mUri
168+ : (includeSubschemas spec =<< subSchemas)
130169 where
131− subSchemas :: [SchemaContext schema]
132− subSchemas = SchemaContext (newResolutionScope mUri (getId schema))
133− <$> embedded schema
170+ subSchemas :: [SchemaWithURI schema]
171+ subSchemas =
172+ (\a -> SchemaWithURI a (updateResolutionScope mUri (getId schema)))
173+ <$> embedded schema
src/Data/Validator/Draft4/Any.hs+1−1
@@ -12,7 +12,7 @@ import qualified Data.Set as S
1212
1313import Data.Validator.Failure
1414import Data.Validator.Utils
15−import Data.Validator.Reference
15+import Data.Validator.Reference (resolveFragment, resolveReference)
1616import Import
1717
1818-- For GHCs before 7.10:
src/Data/Validator/Reference.hs+37−13
@@ -1,28 +1,43 @@
11
22module Data.Validator.Reference where
33
4+-- | JSON Reference is described here:
5+-- <http://tools.ietf.org/html/draft-pbryan-zyp-json-ref-03>
6+--
7+-- And is extended for JSON Schema here:
8+-- <http://json-schema.org/latest/json-schema-core.html#anchor26>
9+--
10+-- Notes on where these functions are used:
11+--
12+-- * 'Data.JsonSchema.Fetch.includeSubschemas' uses 'updateResolutionScope'.
13+--
14+-- * 'Data.JsonSchema.Fetch.foldFunction' uses 'resolveReference'.
15+--
16+-- * 'Data.JsonSchema.Draft4.Internal' uses 'updateResolutionScope'
17+-- throughout.
18+--
19+-- * 'Data.Validator.Draft4.Any.ref' uses 'resolveReference' and
20+-- 'resolveFragment'.
21+
422import qualified Data.Aeson.Pointer as P
523import qualified Data.Text as T
624import Data.Text.Encoding
7−import Network.HTTP.Types.URI
25+import Network.HTTP.Types.URI (urlDecode)
26+import System.FilePath ((</>), dropFileName)
827
928import Import
1029
1130type URIBase = Maybe Text
1231type URIBaseAndFragment = (Maybe Text, Maybe Text)
1332
14−newResolutionScope :: URIBase -> Maybe Text -> URIBase
15−newResolutionScope mScope idKeyword =
16− case idKeyword of
17− Just t -> fst . baseAndFragment $ resolveScopeAgainst mScope t
18− _ -> mScope
33+updateResolutionScope :: URIBase -> Maybe Text -> URIBase
34+updateResolutionScope mScope idKeyword
35+ | Just t <- idKeyword = fst . baseAndFragment $ resolveScopeAgainst mScope t
36+ | otherwise = mScope
1937
2038resolveReference :: URIBase -> Text -> URIBaseAndFragment
2139resolveReference mScope t = baseAndFragment $ resolveScopeAgainst mScope t
2240
23−isRemoteReference :: Text -> Bool
24−isRemoteReference uri = "://" `T.isInfixOf` uri
25−
2641resolveFragment
2742 :: (FromJSON schema, ToJSON schema, Show schema)
2843 => Maybe Text
@@ -38,9 +53,12 @@ resolveFragment (Just pointer) schema = do
3853 Success schema' -> Just schema'
3954
4055--------------------------------------------------
41−-- * Internal
56+-- * Helpers
4257--------------------------------------------------
4358
59+isRemoteReference :: Text -> Bool
60+isRemoteReference = T.isInfixOf "://"
61+
4462baseAndFragment :: Text -> URIBaseAndFragment
4563baseAndFragment = f . T.splitOn "#"
4664 where
@@ -62,6 +80,12 @@ resolveScopeAgainst (Just scope) t
6280 -- but just in case a user leaves one in we want to be sure
6381 -- to cut it off before appending.
6482 smartAppend :: Text
65− smartAppend = case baseAndFragment scope of
66− (Just base,_) -> base <> t
67− _ -> t
83+ smartAppend =
84+ case baseAndFragment scope of
85+ (Just base,_) ->
86+ case T.unpack t of
87+ -- We want "/foo" and "#/bar" to combine into
88+ -- "/foo#/bar" not "/foo/#/bar".
89+ '#':_ -> base <> t
90+ _ -> T.pack (dropFileName (T.unpack base) </> T.unpack t)
91+ _ -> t
draft4.json → src/draft4.jsonrenamed

The content is the same.

tests/Local.hs+8−50
@@ -4,18 +4,16 @@ module Main where
44import Control.Applicative
55import Control.Monad (unless)
66import Data.Aeson
7−import qualified Data.Aeson.Pointer as P
87import Data.List (isSuffixOf)
98import Data.Monoid
109import System.Directory (getDirectoryContents)
11−import Test.Tasty (TestTree, defaultMain, testGroup)
12−import qualified Test.Tasty.HUnit as HU
10+import Test.Tasty (defaultMain, testGroup)
1311import Test.Tasty.QuickCheck (testProperty)
1412
15−import Data.JsonSchema.Draft4 (Failure(..), Schema(..),
16− SchemaCache(..), SchemaContext(..),
17− emptySchema, runValidate)
18−import qualified Data.Validator.Draft4 as VA
13+import Data.JsonSchema.Draft4
14+import Local.Failure (correctPaths)
15+import Local.Filesystem (fetchFromFilesystem)
16+import Local.Reference (referenceTests)
1917import Shared (isLocal, readSchemaTests, toTest)
2018
2119dir :: String
@@ -27,50 +25,10 @@ main = do
2725 ts <- readSchemaTests dir filenames
2826 defaultMain . testGroup "Tests not requiring an HTTP server" $
2927 testProperty "Invert schemas through JSON without change" invertSchema
30− : testGroup "Make paths to invalid data correctly" correctPaths
28+ : testGroup "Report the path to invalid data correctly" correctPaths
29+ : testGroup "Test the referencesViaFilesystem function" fetchFromFilesystem
30+ : testGroup "Test the Reference module" referenceTests
3131 : fmap toTest ts
3232
3333invertSchema :: Schema -> Bool
3434invertSchema a = Just a == decode (encode a)
35−
36−correctPaths :: [TestTree]
37−correctPaths =
38− [ HU.testCase "Items object" itemsObject
39− , HU.testCase "Items array" itemsArray
40− ]
41−
42−itemsObject :: IO ()
43−itemsObject = HU.assertEqual "Path to invalid data"
44− (P.Pointer [P.Token "0"])
45− (_failureOffendingData failure)
46− where
47− [failure] = runValidate (SchemaCache schema mempty) sc (toJSON [[True, True]])
48−
49− schema :: Schema
50− schema = emptySchema
51− { _schemaItems = Just (VA.ItemsObject (emptySchema { _schemaUniqueItems = Just True }))
52− }
53−
54− sc :: SchemaContext Schema
55− sc = SchemaContext
56− { _scURI = Nothing
57− , _scSchema = schema
58− }
59−
60−itemsArray :: IO ()
61−itemsArray = HU.assertEqual "Path to invalid data"
62− (P.Pointer [P.Token "0"])
63− (_failureOffendingData failure)
64− where
65− [failure] = runValidate (SchemaCache schema mempty) sc (toJSON [[True, True]])
66−
67− schema :: Schema
68− schema = emptySchema
69− { _schemaItems = Just (VA.ItemsArray [emptySchema { _schemaUniqueItems = Just True }])
70− }
71−
72− sc :: SchemaContext Schema
73− sc = SchemaContext
74− { _scURI = Nothing
75− , _scSchema = schema
76− }
tests/Local/Failure.hsnew file+55
@@ -0,0 +1,55 @@
1+
2+module Local.Failure where
3+
4+import Data.Aeson
5+import qualified Data.Aeson.Pointer as P
6+import Data.Monoid
7+import Test.Tasty (TestTree)
8+import qualified Test.Tasty.HUnit as HU
9+
10+import Data.JsonSchema.Draft4
11+import qualified Data.Validator.Draft4 as VA
12+
13+correctPaths :: [TestTree]
14+correctPaths =
15+ [ HU.testCase "Items object" itemsObject
16+ , HU.testCase "Items array" itemsArray
17+ ]
18+
19+itemsObject :: IO ()
20+itemsObject = HU.assertEqual "Path to invalid data"
21+ (P.Pointer [P.Token "0"])
22+ (_failureOffendingData failure)
23+ where
24+ [failure] = runValidate (ReferencedSchemas schema mempty)
25+ sw (toJSON [[True, True]])
26+
27+ schema :: Schema
28+ schema = emptySchema
29+ { _schemaItems = Just (VA.ItemsObject (emptySchema { _schemaUniqueItems = Just True }))
30+ }
31+
32+ sw :: SchemaWithURI Schema
33+ sw = SchemaWithURI
34+ { _swSchema = schema
35+ , _swURI = Nothing
36+ }
37+
38+itemsArray :: IO ()
39+itemsArray = HU.assertEqual "Path to invalid data"
40+ (P.Pointer [P.Token "0"])
41+ (_failureOffendingData failure)
42+ where
43+ [failure] = runValidate (ReferencedSchemas schema mempty)
44+ sw (toJSON [[True, True]])
45+
46+ schema :: Schema
47+ schema = emptySchema
48+ { _schemaItems = Just (VA.ItemsArray [emptySchema { _schemaUniqueItems = Just True }])
49+ }
50+
51+ sw :: SchemaWithURI Schema
52+ sw = SchemaWithURI
53+ { _swSchema = schema
54+ , _swURI = Nothing
55+ }
tests/Local/Filesystem.hsnew file+55
@@ -0,0 +1,55 @@
1+
2+module Local.Filesystem where
3+
4+import Data.Aeson
5+import Data.Monoid
6+import Test.Tasty (TestTree)
7+import qualified Test.Tasty.HUnit as HU
8+import Data.Text (Text)
9+
10+import Data.JsonSchema.Draft4
11+
12+fetchFromFilesystem :: [TestTree]
13+fetchFromFilesystem =
14+ [ HU.testCase
15+ "readFile exceptions are turned into Lefts"
16+ readFileExceptions
17+
18+ , HU.testCase
19+ "Relative reference to local file"
20+ (resolve "tests/Local/schema.json")
21+ , HU.testCase
22+ "Chained relative references to local files"
23+ (resolve "./tests/Local/schema-with-ref.json")
24+ ]
25+
26+readFileExceptions :: IO ()
27+readFileExceptions = do
28+ res <- referencesViaFilesystem (SchemaWithURI schema Nothing)
29+ case res of
30+ Left (ReadFailure _) -> pure ()
31+ a -> error (msg <> show a)
32+ where
33+ schema :: Schema
34+ schema = emptySchema { _schemaRef = Just "does-not-exist.json" }
35+
36+ msg :: String
37+ msg = "expected referencesViaFilesystem to return ReadFailure,"
38+ <> " instead got: "
39+
40+-- * Helpers
41+
42+resolve :: Text -> IO ()
43+resolve ref = do
44+ let schema = emptySchema { _schemaRef = Just ref }
45+ res <- fetchFilesystemAndValidate (SchemaWithURI schema Nothing) badData
46+ case res of
47+ Left (FVData _) -> pure ()
48+ a -> error (msg <> show a)
49+ where
50+ badData :: Value
51+ badData = toJSON [True, True]
52+
53+ msg :: String
54+ msg = "expected fetchFilesystemAndValidate to return"
55+ <> " Left (FVData [_]), instead got: "
tests/Local/Reference.hsnew file+51
@@ -0,0 +1,51 @@
1+
2+module Local.Reference where
3+
4+import Test.Tasty (TestTree)
5+import qualified Test.Tasty.HUnit as HU
6+
7+import Data.Validator.Reference
8+
9+referenceTests :: [TestTree]
10+referenceTests =
11+ [ HU.testCase "updateResolutionScope test cases" updateResolutionScopeTests
12+ , HU.testCase "resolveReference test cases" resolveReferenceTests
13+ ]
14+
15+updateResolutionScopeTests :: IO ()
16+updateResolutionScopeTests = do
17+ HU.assertEqual
18+ "case 1 result"
19+ Nothing
20+ (updateResolutionScope Nothing Nothing)
21+ HU.assertEqual
22+ "case 2 result"
23+ Nothing
24+ (updateResolutionScope Nothing (Just "#"))
25+ HU.assertEqual
26+ "case 3 result"
27+ (Just "foo")
28+ (updateResolutionScope Nothing (Just "foo"))
29+ HU.assertEqual
30+ "case 4 result"
31+ (Just "/./bar") -- TODO: Normalize after updateResolutionScope.
32+ (updateResolutionScope (Just "/foo") (Just "./bar"))
33+
34+resolveReferenceTests :: IO ()
35+resolveReferenceTests = do
36+ HU.assertEqual
37+ "case 1 result"
38+ (Just "/bar", Nothing)
39+ (resolveReference (Just "/foo") "bar")
40+ HU.assertEqual
41+ "case 2 result"
42+ (Just "/baz", Nothing)
43+ (resolveReference (Just "/foo/bar") "/baz")
44+ HU.assertEqual
45+ "case 3 result"
46+ (Nothing, Just "/bar")
47+ (resolveReference Nothing "#/bar")
48+ HU.assertEqual
49+ "case 4 result"
50+ (Just "/foo", Just "/bar")
51+ (resolveReference (Just "/foo") "#/bar")
tests/Local/schema-with-ref.jsonnew file+3
@@ -0,0 +1,3 @@
1+{
2+ "$ref": "./schema.json"
3+}
tests/Local/schema.jsonnew file+3
@@ -0,0 +1,3 @@
1+{
2+ "uniqueItems": true
3+}
tests/Shared.hs+2−2
@@ -68,8 +68,8 @@ toTest :: SchemaTest -> TestTree
6868toTest st =
6969 HU.testCase (T.unpack (_stDescription st)) $ do
7070 forM_ (_stCases st) $ \sc -> do
71− g <- assertRight =<< D4.fetchReferencedSchemas mempty (D4.SchemaContext Nothing (_stSchema st))
72− validate <- assertRight . D4.checkSchema g $ D4.SchemaContext Nothing (_stSchema st)
71+ g <- assertRight =<< D4.referencesViaHTTP (D4.SchemaWithURI (_stSchema st) Nothing)
72+ validate <- assertRight . D4.checkSchema g $ D4.SchemaWithURI (_stSchema st) Nothing
7373 let res = validate (_scData sc)
7474 if _scValid sc
7575 then assertValid sc res