jsons-to-schema-0.1.0.0: src/JSONSchema/Draft4/SchemaUnification.hs
{-|
Module : JSONSchema.Draft4.SchemaUnification
Description : Unification of multiple schemas
Copyright : (c) Gareth Tan, 2017
License : MIT
Contains a function to combine multiple JSON schemas
together.
-}
module JSONSchema.Draft4.SchemaUnification
( unifySchemas
) where
import Protolude hiding ((<>))
import qualified Data.HashMap.Lazy as HM
import qualified Data.Set as DS
import qualified JSONSchema.Draft4 as D4
import qualified JSONSchema.Validator.Draft4.Array as V4Arr
import qualified JSONSchema.Validator.Draft4.Object as V4Obj
import Data.Semigroup ((<>))
import qualified JSONSchema.Draft4.Internal.Utils as Utils
{-| The primary function used to combine multiple JSON schemas.
This relation cannot be the binary operation of a monoid because
the empty schema will act to remove the `required` property of
any JSON Schema it is unified with.
This relation is also not commutative because we arbitrarily select
from the alternatives for properties such as the schema @version@
that cannot be unified. In addition, when one items schema is
an array and another is an object, we simply choose arbitrarily
because there is no sensible way of unifying the schemas while
preserving all relevant information.
-}
unifySchemas :: D4.Schema -> D4.Schema -> D4.Schema
unifySchemas nextSchema =
unifyObjectConstraints nextSchema .
unifyArrayConstraints nextSchema .
unifyStringConstraints nextSchema .
unifyNumericConstraints nextSchema .
unifyAnyInstanceConstraints nextSchema .
unifyNonvalidatingConstraints nextSchema
-- The linear unifier extracts an array of Maybes and filters them
-- to only the Just values. We then use foldr1May to fold across the
-- list.. If there were no Just values, then
-- the foldr1 fails and we get Nothing. Otherwise, the present Just
-- values are folded together using foldF.
-- |The alternative unifier applies the binary function if both are Just, returns the identity
-- if only one is Just, and Nothing if both are Nothing.
altUnifier :: (a -> a -> a) -> (b -> Maybe a) -> b -> b -> Maybe a
altUnifier binF getter next acc = applied <|> getter next <|> getter acc
where applied = applicativeUnifier binF getter next acc
-- The applicative unifier applies the binary function but returns Nothing if either
-- is Nothing in the manner of an applicative.
applicativeUnifier :: (a -> a -> a) -> (b -> Maybe a) -> b -> b -> Maybe a
applicativeUnifier binF getter next acc = binF <$> getter next <*> getter acc
unifyNonvalidatingConstraints :: D4.Schema -> D4.Schema -> D4.Schema
unifyNonvalidatingConstraints nextSchema accSchema = accSchema {
D4._schemaVersion = altUnifier const D4._schemaVersion nextSchema accSchema
, D4._schemaId = altUnifier const D4._schemaId nextSchema accSchema
, D4._schemaRef = altUnifier const D4._schemaRef nextSchema accSchema
, D4._schemaDefinitions = altUnifier const D4._schemaDefinitions nextSchema accSchema
, D4._schemaDependencies = altUnifier const D4._schemaDependencies nextSchema accSchema
, D4._schemaOther = foldr HM.union HM.empty (fmap D4._schemaOther [nextSchema, accSchema])
}
unifyNumericConstraints :: D4.Schema -> D4.Schema -> D4.Schema
unifyNumericConstraints nextSchema accSchema =
accSchema
{ D4._schemaMaximum = maxConstraint
, D4._schemaExclusiveMaximum = emaxConstraint
, D4._schemaMinimum = minConstraint
, D4._schemaExclusiveMinimum = eminConstraint
, D4._schemaMultipleOf = altUnifier const D4._schemaMultipleOf nextSchema accSchema
}
where
schemas = [nextSchema, accSchema]
maxes = fmap D4._schemaMaximum schemas
emaxes = fmap D4._schemaExclusiveMaximum schemas
mins = fmap D4._schemaMinimum schemas
emins = fmap D4._schemaExclusiveMinimum schemas
(maxConstraint, emaxConstraint) =
Utils.computeMaximumConstraints maxes emaxes
(minConstraint, eminConstraint) =
Utils.computeMinimumConstraints mins emins
unifyStringConstraints :: D4.Schema -> D4.Schema -> D4.Schema
unifyStringConstraints nextSchema accSchema = accSchema {
D4._schemaMaxLength = altUnifier max D4._schemaMaxLength nextSchema accSchema
, D4._schemaMinLength = altUnifier min D4._schemaMinLength nextSchema accSchema
, D4._schemaPattern = altUnifier const D4._schemaPattern nextSchema accSchema
}
unifyArrayConstraints :: D4.Schema -> D4.Schema -> D4.Schema
unifyArrayConstraints nextSchema accSchema = accSchema {
D4._schemaItems = altUnifier unifyItems D4._schemaItems nextSchema accSchema
, D4._schemaAdditionalItems = altUnifier uAdditional D4._schemaAdditionalItems nextSchema accSchema
, D4._schemaMaxItems = altUnifier max D4._schemaMaxItems nextSchema accSchema
, D4._schemaMinItems = altUnifier min D4._schemaMinItems nextSchema accSchema
, D4._schemaUniqueItems = altUnifier (&&) D4._schemaUniqueItems nextSchema accSchema
}
where
uAdditional :: V4Arr.AdditionalItems D4.Schema -> V4Arr.AdditionalItems D4.Schema -> V4Arr.AdditionalItems D4.Schema
uAdditional (V4Arr.AdditionalBool b1) (V4Arr.AdditionalBool b2) =
V4Arr.AdditionalBool $ b1 || b2
-- allowing additional objects (True) is always at least as permissive as any schema
-- all schemas are at least as permissive as not allowing any additional objects (False)
uAdditional bln@(V4Arr.AdditionalBool b) obj@(V4Arr.AdditionalObject s) = if b then bln else obj
uAdditional obj@(V4Arr.AdditionalObject s) bln@(V4Arr.AdditionalBool b) = if b then bln else obj
uAdditional (V4Arr.AdditionalObject o1) (V4Arr.AdditionalObject o2) =
V4Arr.AdditionalObject $ unifySchemas o1 o2
unifyItems :: V4Arr.Items D4.Schema -> V4Arr.Items D4.Schema -> V4Arr.Items D4.Schema
unifyItems (V4Arr.ItemsObject o1) (V4Arr.ItemsObject o2) = V4Arr.ItemsObject $ unifySchemas o1 o2
unifyItems (V4Arr.ItemsArray xs) (V4Arr.ItemsArray ys) = V4Arr.ItemsArray $
fmap (uncurry unifySchemas) (Utils.zipWithPadding D4.emptySchema D4.emptySchema xs ys)
unifyItems x y = x -- possibly: merge the object schema into each of the tuple schemas? (zip (repeat o1) xs)
-- If one object does not require properties, then none of them can require
-- any properties. If the `required` array would be empty, we return Nothing
-- instead (the V4 metaschema says the required array cannot be empty)
unifyObjectConstraints :: D4.Schema -> D4.Schema -> D4.Schema
unifyObjectConstraints nextSchema accSchema = accSchema {
D4._schemaRequired = Utils.setToMaybeSet =<< -- to avoid empty lists, which are not allowed
applicativeUnifier DS.intersection D4._schemaRequired nextSchema accSchema
, D4._schemaProperties =
altUnifier (HM.unionWith unifySchemas) D4._schemaProperties nextSchema accSchema
, D4._schemaAdditionalProperties =
altUnifier unify D4._schemaAdditionalProperties nextSchema accSchema
, D4._schemaMaxProperties = altUnifier max D4._schemaMaxProperties nextSchema accSchema
, D4._schemaMinProperties = altUnifier min D4._schemaMinProperties nextSchema accSchema
, D4._schemaPatternProperties = altUnifier (HM.unionWith unifySchemas) D4._schemaPatternProperties nextSchema accSchema
}
where
unify :: V4Obj.AdditionalProperties D4.Schema -> V4Obj.AdditionalProperties D4.Schema -> V4Obj.AdditionalProperties D4.Schema
unify (V4Obj.AdditionalPropertiesBool b1) (V4Obj.AdditionalPropertiesBool b2) =
V4Obj.AdditionalPropertiesBool $ b1 || b2
-- allowing additional objects (True) is always at least as permissive as any schema
-- all schemas are at least as permissive as not allowing any additional objects (False)
unify bln@(V4Obj.AdditionalPropertiesBool b) obj@(V4Obj.AdditionalPropertiesObject s) = if b then bln else obj
unify obj@(V4Obj.AdditionalPropertiesObject s) bln@(V4Obj.AdditionalPropertiesBool b) = if b then bln else obj
unify (V4Obj.AdditionalPropertiesObject o1) (V4Obj.AdditionalPropertiesObject o2) =
V4Obj.AdditionalPropertiesObject $ unifySchemas o1 o2
unifyAnyInstanceConstraints :: D4.Schema -> D4.Schema -> D4.Schema
unifyAnyInstanceConstraints nextSchema accSchema = accSchema {
D4._schemaType = altUnifier (<>) D4._schemaType nextSchema accSchema
, D4._schemaEnum = altUnifier const D4._schemaEnum nextSchema accSchema
, D4._schemaAllOf = altUnifier const D4._schemaAllOf nextSchema accSchema
, D4._schemaAnyOf = altUnifier const D4._schemaAnyOf nextSchema accSchema
, D4._schemaOneOf = altUnifier const D4._schemaOneOf nextSchema accSchema
, D4._schemaNot = altUnifier const D4._schemaNot nextSchema accSchema
}