{-| 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 }