@@ -0,0 +1,334 @@
1+
2+module Data.JsonSchema.Draft4.Schema where
3+
4+import qualified Data.HashMap.Strict as H
5+import Data.List.NonEmpty (NonEmpty)
6+import Data.Maybe (fromJust, isJust)
7+import Data.Scientific
8+
9+import qualified Data.Validator.Draft4.Any as AN
10+import qualified Data.Validator.Draft4.Array as AR
11+import qualified Data.Validator.Draft4.Object as OB
12+import Data.Validator.Utils
13+import Import
14+
15+data Schema = Schema
16+ { _schemaVersion :: Maybe Text
17+ , _schemaId :: Maybe Text
18+ , _schemaRef :: Maybe Text
19+ , _schemaDefinitions :: Maybe (HashMap Text Schema)
20+ -- ^ A standardized location for embedding schemas
21+ -- to be referenced from elsewhere in the document.
22+ , _schemaOther :: HashMap Text Value
23+ -- ^ Since the JSON document this Schema was built from could
24+ -- contain schemas anywhere (not just in "definitions" or any
25+ -- of the other official keys) we save any leftover key/value
26+ -- pairs not covered by them here.
27+
28+ , _schemaMultipleOf :: Maybe Scientific
29+ , _schemaMaximum :: Maybe Scientific
30+ , _schemaExclusiveMaximum :: Maybe Bool
31+ , _schemaMinimum :: Maybe Scientific
32+ , _schemaExclusiveMinimum :: Maybe Bool
33+
34+ , _schemaMaxLength :: Maybe Int
35+ , _schemaMinLength :: Maybe Int
36+ , _schemaPattern :: Maybe Text
37+
38+ , _schemaMaxItems :: Maybe Int
39+ , _schemaMinItems :: Maybe Int
40+ , _schemaUniqueItems :: Maybe Bool
41+ , _schemaItems :: Maybe (AR.Items Schema)
42+ -- Note that '_schemaAdditionalItems' is left out of 'runValidate'
43+ -- because its functionality is handled by '_schemaItems'. It always
44+ -- validates data unless 'Items' is present.
45+ , _schemaAdditionalItems :: Maybe (AR.AdditionalItems Schema)
46+
47+ , _schemaMaxProperties :: Maybe Int
48+ , _schemaMinProperties :: Maybe Int
49+ , _schemaRequired :: Maybe OB.Required
50+ , _schemaDependencies :: Maybe (HashMap Text (OB.Dependency Schema))
51+ , _schemaProperties :: Maybe (HashMap Text Schema)
52+ , _schemaPatternProperties :: Maybe (HashMap Text Schema)
53+ , _schemaAdditionalProperties :: Maybe (OB.AdditionalProperties Schema)
54+
55+ , _schemaEnum :: Maybe AN.EnumVal
56+ , _schemaType :: Maybe AN.TypeVal
57+ , _schemaAllOf :: Maybe (NonEmpty Schema)
58+ , _schemaAnyOf :: Maybe (NonEmpty Schema)
59+ , _schemaOneOf :: Maybe (NonEmpty Schema)
60+ , _schemaNot :: Maybe Schema
61+ } deriving (Eq, Show)
62+
63+emptySchema :: Schema
64+emptySchema = Schema
65+ { _schemaVersion = Nothing
66+ , _schemaId = Nothing
67+ , _schemaRef = Nothing
68+ , _schemaDefinitions = Nothing
69+ , _schemaOther = mempty
70+
71+ , _schemaMultipleOf = Nothing
72+ , _schemaMaximum = Nothing
73+ , _schemaExclusiveMaximum = Nothing
74+ , _schemaMinimum = Nothing
75+ , _schemaExclusiveMinimum = Nothing
76+
77+ , _schemaMaxLength = Nothing
78+ , _schemaMinLength = Nothing
79+ , _schemaPattern = Nothing
80+
81+ , _schemaMaxItems = Nothing
82+ , _schemaMinItems = Nothing
83+ , _schemaUniqueItems = Nothing
84+ , _schemaItems = Nothing
85+ , _schemaAdditionalItems = Nothing
86+
87+ , _schemaMaxProperties = Nothing
88+ , _schemaMinProperties = Nothing
89+ , _schemaRequired = Nothing
90+ , _schemaDependencies = Nothing
91+ , _schemaProperties = Nothing
92+ , _schemaPatternProperties = Nothing
93+ , _schemaAdditionalProperties = Nothing
94+
95+ , _schemaEnum = Nothing
96+ , _schemaType = Nothing
97+ , _schemaAllOf = Nothing
98+ , _schemaAnyOf = Nothing
99+ , _schemaOneOf = Nothing
100+ , _schemaNot = Nothing
101+ }
102+
103+instance FromJSON Schema where
104+ parseJSON = withObject "Schema" $ \o -> do
105+ a <- o .:? "$schema"
106+ b <- o .:? "id"
107+ c <- o .:? "$ref"
108+ d <- o .:? "definitions"
109+ e <- parseJSON (Object (H.difference o internalSchemaHashMap))
110+
111+ f <- o .:? "multipleOf"
112+ g <- o .:? "maximum"
113+ h <- o .:? "exclusiveMaximum"
114+ i <- o .:? "minimum"
115+ j <- o .:? "exclusiveMinimum"
116+
117+ k <- o .:? "maxLength"
118+ l <- o .:? "minLength"
119+ m <- o .:? "pattern"
120+
121+ n <- o .:? "maxItems"
122+ o' <- o .:? "minItems"
123+ p <- o .:? "uniqueItems"
124+ q <- o .:? "items"
125+ r <- o .:? "additionalItems"
126+
127+ s <- o .:? "maxProperties"
128+ t <- o .:? "minProperties"
129+ u <- o .:? "required"
130+ v <- o .:? "dependencies"
131+ w <- o .:? "properties"
132+ x <- o .:? "patternProperties"
133+ y <- o .:? "additionalProperties"
134+
135+ z <- o .:? "enum"
136+ a2 <- o .:? "type"
137+ b2 <- fmap _unNonEmpty' <$> o .:? "allOf"
138+ c2 <- fmap _unNonEmpty' <$> o .:? "anyOf"
139+ d2 <- fmap _unNonEmpty' <$> o .:? "oneOf"
140+ e2 <- o .:? "not"
141+ pure Schema
142+ { _schemaVersion = a
143+ , _schemaId = b
144+ , _schemaRef = c
145+ , _schemaDefinitions = d
146+ , _schemaOther = e
147+
148+ , _schemaMultipleOf = f
149+ , _schemaMaximum = g
150+ , _schemaExclusiveMaximum = h
151+ , _schemaMinimum = i
152+ , _schemaExclusiveMinimum = j
153+
154+ , _schemaMaxLength = k
155+ , _schemaMinLength = l
156+ , _schemaPattern = m
157+
158+ , _schemaMaxItems = n
159+ , _schemaMinItems = o'
160+ , _schemaUniqueItems = p
161+ , _schemaItems = q
162+ , _schemaAdditionalItems = r
163+
164+ , _schemaMaxProperties = s
165+ , _schemaMinProperties = t
166+ , _schemaRequired = u
167+ , _schemaDependencies = v
168+ , _schemaProperties = w
169+ , _schemaPatternProperties = x
170+ , _schemaAdditionalProperties = y
171+
172+ , _schemaEnum = z
173+ , _schemaType = a2
174+ , _schemaAllOf = b2
175+ , _schemaAnyOf = c2
176+ , _schemaOneOf = d2
177+ , _schemaNot = e2
178+ }
179+
180+instance ToJSON Schema where
181+ -- | The way we resolve JSON Pointers to embedded schemas is by
182+ -- serializing the containing schema to a value and then resolving the
183+ -- pointer against it. This means that FromJSON and ToJSON must be
184+ -- isomorphic.
185+ --
186+ -- This influences the design choices in the library. E.g. right now
187+ -- there are two false values for "exclusiveMaximum" -- Nothing and
188+ -- Just False. We could have condensed them down by using () instead
189+ -- of Bool for "exclusiveMaximum". This would have made writing schemas
190+ -- in haskell easier, but we could no longer round trip through/from
191+ -- JSON without losing information.
192+ toJSON s = Object $ H.union (mapMaybe ($ s) internalSchemaHashMap) (toJSON <$> _schemaOther s)
193+ where
194+ -- 'mapMaybe' is provided by unordered-containers after unordered-container-2.6.0.0,
195+ -- but until that is a little older (and has time to get into Stackage etc.)
196+ -- we use our own implementation.
197+ mapMaybe :: (v1 -> Maybe v2) -> HashMap k v1 -> HashMap k v2
198+ mapMaybe f = fmap fromJust . H.filter isJust . fmap f
199+
200+-- | Internal. Separate from ToJSON because it's also used
201+-- by FromJSON to determine what keys aren't official schema
202+-- keys and therefor should be included in _schemaOther.
203+internalSchemaHashMap :: HashMap Text (Schema -> Maybe Value)
204+internalSchemaHashMap = H.fromList
205+ [ ("$schema" , f _schemaVersion)
206+ , ("id" , f _schemaId)
207+ , ("$ref" , f _schemaRef)
208+ , ("definitions" , f _schemaDefinitions)
209+
210+ , ("multipleOf" , f _schemaMultipleOf)
211+ , ("maximum" , f _schemaMaximum)
212+ , ("exclusiveMaximum" , f _schemaExclusiveMaximum)
213+ , ("minimum" , f _schemaMinimum)
214+ , ("exclusiveMinimum" , f _schemaExclusiveMinimum)
215+
216+ , ("maxLength" , f _schemaMaxLength)
217+ , ("minLength" , f _schemaMinLength)
218+ , ("pattern" , f _schemaPattern)
219+
220+ , ("maxItems" , f _schemaMaxItems)
221+ , ("minItems" , f _schemaMinItems)
222+ , ("uniqueItems" , f _schemaUniqueItems)
223+ , ("items" , f _schemaItems)
224+ , ("additionalItems" , f _schemaAdditionalItems)
225+
226+ , ("maxProperties" , f _schemaMaxProperties)
227+ , ("minProperties" , f _schemaMinProperties)
228+ , ("required" , f _schemaRequired)
229+ , ("dependencies" , f _schemaDependencies)
230+ , ("properties" , f _schemaProperties)
231+ , ("patternProperties" , f _schemaPatternProperties)
232+ , ("additionalProperties", f _schemaAdditionalProperties)
233+
234+ , ("enum" , f _schemaEnum)
235+ , ("type" , f _schemaType)
236+ , ("allOf" , f (fmap NonEmpty' . _schemaAllOf))
237+ , ("anyOf" , f (fmap NonEmpty' . _schemaAnyOf))
238+ , ("oneOf" , f (fmap NonEmpty' . _schemaOneOf))
239+ , ("not" , f _schemaNot)
240+ ]
241+ where
242+ f :: ToJSON a => (Schema -> Maybe a) -> Schema -> Maybe Value
243+ f = (fmap.fmap) toJSON
244+
245+instance Arbitrary Schema where
246+ arbitrary = sized f
247+ where
248+ maybeGen :: Gen a -> Gen (Maybe a)
249+ maybeGen a = oneof [pure Nothing, Just <$> a]
250+
251+ maybeRecurse :: Int -> Gen a -> Gen (Maybe a)
252+ maybeRecurse n a
253+ | n < 1 = pure Nothing
254+ | otherwise = maybeGen $ resize (n `div` 10) a
255+
256+ f :: Int -> Gen Schema
257+ f n = do
258+ a <- maybeGen arbitraryText
259+ b <- maybeGen arbitraryText
260+ c <- maybeGen arbitraryText
261+ -- NOTE: The next two fields are empty to generate cleaner schemas,
262+ -- but note that this means we don't test e.g. the invertability
263+ -- of these fields.
264+ d <- pure Nothing -- _schemaDefinitions
265+ e <- pure mempty -- _otherPairs
266+
267+ f' <- maybeGen arbitraryPositiveScientific
268+ g <- maybeGen arbitraryScientific
269+ h <- arbitrary
270+ i <- maybeGen arbitraryScientific
271+ j <- arbitrary
272+
273+ k <- maybeGen (getPositive <$> arbitrary)
274+ l <- maybeGen (getPositive <$> arbitrary)
275+ m <- maybeGen arbitraryText
276+
277+ n' <- maybeGen (getPositive <$> arbitrary)
278+ o <- maybeGen (getPositive <$> arbitrary)
279+ p <- arbitrary
280+ q <- maybeRecurse n arbitrary
281+ r <- maybeRecurse n arbitrary
282+
283+ s <- maybeGen (getPositive <$> arbitrary)
284+ t <- maybeGen (getPositive <$> arbitrary)
285+ u <- arbitrary
286+ v <- maybeRecurse n arbitraryHashMap
287+ w <- maybeRecurse n arbitraryHashMap
288+ x <- maybeRecurse n arbitraryHashMap
289+ y <- maybeRecurse n arbitrary
290+
291+ z <- arbitrary
292+ a2 <- arbitrary
293+ b2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
294+ c2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
295+ d2 <- maybeRecurse n (_unNonEmpty' <$> arbitrary)
296+ e2 <- maybeRecurse n arbitrary
297+ pure Schema
298+ { _schemaVersion = a
299+ , _schemaId = b
300+ , _schemaRef = c
301+ , _schemaDefinitions = d
302+ , _schemaOther = e
303+
304+ , _schemaMultipleOf = f'
305+ , _schemaMaximum = g
306+ , _schemaExclusiveMaximum = h
307+ , _schemaMinimum = i
308+ , _schemaExclusiveMinimum = j
309+
310+ , _schemaMaxLength = k
311+ , _schemaMinLength = l
312+ , _schemaPattern = m
313+
314+ , _schemaMaxItems = n'
315+ , _schemaMinItems = o
316+ , _schemaUniqueItems = p
317+ , _schemaItems = q
318+ , _schemaAdditionalItems = r
319+
320+ , _schemaMaxProperties = s
321+ , _schemaMinProperties = t
322+ , _schemaRequired = u
323+ , _schemaDependencies = v
324+ , _schemaProperties = w
325+ , _schemaPatternProperties = x
326+ , _schemaAdditionalProperties = y
327+
328+ , _schemaEnum = z
329+ , _schemaType = a2
330+ , _schemaAllOf = b2
331+ , _schemaAnyOf = c2
332+ , _schemaOneOf = d2
333+ , _schemaNot = e2
334+ }