@@ -1,1627 +0,0 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2−
3−module OpenAPI.Checker.Validate.Schema
4− ( JsonType (..)
5− , ForeachType (..)
6− , foldType
7− , forType_
8− , TypedValue (..)
9− , untypeValue
10− , Bound (..)
11− , schemaToFormula
12− , foldLattice
13− , Behave (..)
14− , describeJSONType
15− )
16−where
17−
18−import Algebra.Lattice
19−import Algebra.Lattice.Lifted
20−import Control.Applicative
21−import Control.Arrow
22−import Control.Comonad.Env hiding (env)
23−import Control.Monad.Reader hiding (ask)
24−import qualified Control.Monad.Reader as R
25−import Control.Monad.State
26−import qualified Control.Monad.Trans.Reader as R (liftCatch)
27−import qualified Control.Monad.Trans.Writer as W (liftCatch)
28−import Control.Monad.Writer
29−import qualified Data.Aeson as A
30−import qualified Data.ByteString.Lazy as BSL
31−import Data.Coerce
32−import Data.Foldable
33−import qualified Data.Foldable as F
34−import Data.Functor
35−import Data.Functor.Identity
36−import Data.HList
37−import qualified Data.HashMap.Strict as HM
38−import qualified Data.HashMap.Strict.InsOrd as IOHM
39−import Data.Int
40−import Data.Kind
41−import Data.List (sortBy)
42−import qualified Data.List as L
43−import qualified Data.List.NonEmpty as NE
44−import qualified Data.Map as M
45−import Data.Maybe
46−import Data.OpenApi hiding (get)
47−import Data.Ord
48−import Data.Ratio
49−import Data.Scientific
50−import qualified Data.Set as S
51−import Data.String
52−import Data.Text (Text)
53−import qualified Data.Text as T hiding (singleton)
54−import qualified Data.Text.Encoding as T
55−import Data.Typeable
56−import OpenAPI.Checker.Behavior
57−import OpenAPI.Checker.Memo
58−import OpenAPI.Checker.Orphans ()
59−import OpenAPI.Checker.Paths
60−import qualified OpenAPI.Checker.PathsPrefixTree as P
61−import OpenAPI.Checker.References
62−import OpenAPI.Checker.Subtree
63−import Text.Pandoc.Builder hiding (Format, Null)
64−import Text.Regex.Pcre2
65−
66−-- | Type of a JSON value
67−data JsonType
68− = Null
69− | Boolean
70− | Number
71− | String
72− | Array
73− | Object
74− deriving stock (Eq, Ord, Show)
75−
76−-- | A 'A.Value' whose type we know
77−data TypedValue :: JsonType -> Type where
78− TNull :: TypedValue 'Null
79− TBool :: !Bool -> TypedValue 'Boolean
80− TNumber :: !Scientific -> TypedValue 'Number
81− TString :: !Text -> TypedValue 'String
82− TArray :: !A.Array -> TypedValue 'Array
83− TObject :: !A.Object -> TypedValue 'Object
84−
85−deriving stock instance Eq (TypedValue t)
86−
87−deriving stock instance Ord (TypedValue t)
88−
89−deriving stock instance Show (TypedValue t)
90−
91−untypeValue :: TypedValue t -> A.Value
92−untypeValue TNull = A.Null
93−untypeValue (TBool b) = A.Bool b
94−untypeValue (TNumber n) = A.Number n
95−untypeValue (TString s) = A.String s
96−untypeValue (TArray a) = A.Array a
97−untypeValue (TObject o) = A.Object o
98−
99−data Bound a = Exclusive !a | Inclusive !a
100− deriving stock (Eq, Show, Functor)
101−
102−-- | The order is lexicographical on @a * Bool@.
103−instance Ord a => Ord (Bound a) where
104− Exclusive a `compare` Exclusive b = compare a b
105− Exclusive a `compare` Inclusive b = if a <= b then LT else GT
106− Inclusive a `compare` Exclusive b = if a < b then LT else GT
107− Inclusive a `compare` Inclusive b = compare a b
108−
109−data Property = Property
110− { propRequired :: Bool
111− , propFormula :: ForeachType JsonFormula
112− , propRefSchema :: Traced (Referenced Schema)
113− }
114− deriving stock (Eq, Ord, Show)
115−
116−-- | A primitive structural condition for the "top level" of a JSON value (of a specific type)
117−data Condition :: JsonType -> Type where
118− Exactly :: TypedValue t -> Condition t
119− Maximum :: !(Bound Scientific) -> Condition 'Number
120− Minimum
121− :: !(Down (Bound (Down Scientific)))
122− -> Condition 'Number -- ^ this has the right Ord
123− MultipleOf :: !Scientific -> Condition 'Number
124− NumberFormat :: !Format -> Condition 'Number
125− MaxLength :: !Integer -> Condition 'String
126− MinLength :: !Integer -> Condition 'String
127− Pattern :: !Pattern -> Condition 'String
128− StringFormat :: !Format -> Condition 'String
129− Items
130− :: !(ForeachType JsonFormula)
131− -> !(Traced (Referenced Schema))
132− -> Condition 'Array
133− MaxItems :: !Integer -> Condition 'Array
134− MinItems :: !Integer -> Condition 'Array
135− UniqueItems :: Condition 'Array
136− Properties
137− :: !(M.Map Text Property)
138− -> !(ForeachType JsonFormula) -- ^ formula for additional properties
139− -> !(Maybe (Traced (Referenced Schema))) -- ^ schema for additional properties, Nothing means bottom
140− -> Condition 'Object
141− MaxProperties :: !Integer -> Condition 'Object
142− MinProperties :: !Integer -> Condition 'Object
143−
144−showCondition :: Condition a -> Blocks
145−showCondition = \case
146− (Exactly v) -> para "The value should be:" <> showJSONValue (untypeValue v)
147− (Maximum b) -> para $ "The value should be less than " <> showBound b <> "."
148− (Minimum (Down b)) -> para $ "The value should be more than " <> showBound (getDown <$> b) <> "."
149− (MultipleOf n) -> para $ "The value should be a multiple of " <> show' n <> "."
150− (NumberFormat p) -> para $ "The number should have the following format:" <> code p <> "."
151− (Pattern p) -> para "The value should satisfy the following pattern (regular expression):" <> codeBlock p
152− (StringFormat p) -> para $ "The string should have the following format:" <> code p <> "."
153− (MaxLength p) -> para $ "The length of the string should be less than or equal to " <> show' p <> "."
154− (MinLength p) -> para $ "The length of the string should be more than or equal to " <> show' p <> "."
155− (Items i _) -> para "The items of the array should satisfy:" <> showForEachJsonFormula i
156− (MaxItems n) -> para $ "The length of the array should be less than or equal to " <> show' n <> "."
157− (MinItems n) -> para $ "The length of the array should be more than or equal to " <> show' n <> "."
158− UniqueItems -> para "The elements in the array should be unique."
159− (Properties props additional _) ->
160− bulletList $
161− (M.toList props
162− <&> (\(k, p) ->
163− para (code k)
164− <> para (strong $ if propRequired p then "Required" else "Optional")
165− <> showForEachJsonFormula (propFormula p)))
166− <> [ para (emph "Additional properties")
167− <> showForEachJsonFormula additional
168− ]
169− (MaxProperties n) -> para $ "The maximum number of fields should be " <> show' n <> "."
170− (MinProperties n) -> para $ "The minimum number of fields should be " <> show' n <> "."
171− where
172− showForEachJsonFormula :: ForeachType JsonFormula -> Blocks
173− showForEachJsonFormula i =
174− bulletList $
175− foldType
176− (\t f -> case getJsonFormula $ f i of
177− BottomFormula -> mempty
178− (DNF conds') ->
179− let conds = S.toList <$> S.toList conds'
180− in [ para (describeJSONType t)
181− <> bulletList
182− (conds <&> \case
183− [] -> para "Empty"
184− cond -> bulletList (showCondition <$> cond))
185− ])
186−
187−satisfiesTyped :: TypedValue t -> Condition t -> Bool
188−satisfiesTyped e (Exactly e') = e == e'
189−satisfiesTyped (TNumber n) (Maximum (Exclusive m)) = n < m
190−satisfiesTyped (TNumber n) (Maximum (Inclusive m)) = n <= m
191−satisfiesTyped (TNumber n) (Minimum (Down (Exclusive (Down m)))) = n > m
192−satisfiesTyped (TNumber n) (Minimum (Down (Inclusive (Down m)))) = n >= m
193−satisfiesTyped (TNumber n) (MultipleOf m) = denominator (toRational n / toRational m) == 1 -- TODO: could be better #36
194−satisfiesTyped (TNumber n) (NumberFormat f) = checkNumberFormat f n
195−satisfiesTyped (TString s) (MaxLength m) = fromIntegral (T.length s) <= m
196−satisfiesTyped (TString s) (MinLength m) = fromIntegral (T.length s) >= m
197−satisfiesTyped (TString s) (Pattern p) = isJust $ match p s -- TODO: regex stuff #32
198−satisfiesTyped (TString s) (StringFormat f) = checkStringFormat f s
199−satisfiesTyped (TArray a) (Items f _) = all (`satisfies` f) a
200−satisfiesTyped (TArray a) (MaxItems m) = fromIntegral (F.length a) <= m
201−satisfiesTyped (TArray a) (MinItems m) = fromIntegral (F.length a) >= m
202−satisfiesTyped (TArray a) UniqueItems = S.size (S.fromList $ F.toList a) == F.length a -- TODO: could be better #36
203−satisfiesTyped (TObject o) (Properties props additional _) =
204− all (`HM.member` o) (M.keys (M.filter propRequired props))
205− && all (\(k, v) -> satisfies v $ maybe additional propFormula $ M.lookup k props) (HM.toList o)
206−satisfiesTyped (TObject o) (MaxProperties m) = fromIntegral (HM.size o) <= m
207−satisfiesTyped (TObject o) (MinProperties m) = fromIntegral (HM.size o) >= m
208−
209−checkNumberFormat :: Format -> Scientific -> Bool
210−checkNumberFormat "int32" (toRational -> n) =
211− denominator n == 1
212− && n >= toRational (minBound :: Int32)
213− && n <= toRational (maxBound :: Int32)
214−checkNumberFormat "int64" (toRational -> n) =
215− denominator n == 1
216− && n >= toRational (minBound :: Int64)
217− && n <= toRational (maxBound :: Int64)
218−checkNumberFormat "float" _n = True
219−checkNumberFormat "double" _n = True
220−checkNumberFormat f _n = error $ "Invalid number format: " <> T.unpack f
221−
222−checkStringFormat :: Format -> Text -> Bool
223−checkStringFormat "byte" _s = True -- TODO: regex stuff #32
224−checkStringFormat "binary" _s = True
225−checkStringFormat "date" _s = True
226−checkStringFormat "date-time" _s = True
227−checkStringFormat "password" _s = True
228−checkStringFormat "uuid" _s = True
229−checkStringFormat f _s = error $ "Invalid string format: " <> T.unpack f
230−
231−deriving stock instance Eq (Condition t)
232−
233−deriving stock instance Ord (Condition t)
234−
235−deriving stock instance Show (Condition t)
236−
237−data SomeCondition where
238− SomeCondition :: Typeable t => Condition t -> SomeCondition
239−
240−instance Eq SomeCondition where
241− SomeCondition x == SomeCondition y = case cast x of
242− Just x' -> x' == y
243− Nothing -> False
244−
245−instance Ord SomeCondition where
246− compare (SomeCondition x) (SomeCondition y) = case cast x of
247− Just x' -> compare x' y
248− Nothing -> compare (typeRep x) (typeRep y)
249−
250−deriving stock instance Show SomeCondition
251−
252−-- | A boolean formula (without "not") represented as a Disjunctive Normal Form:
253−-- the formula is a disjunction of a set of conjuncts, each of which is a
254−-- conjunction of a set of some elementary formulas.
255−newtype DNF a
256− = DNF (S.Set (S.Set a))
257− deriving stock (Eq, Ord, Show)
258−
259−disjAdd :: Ord a => DNF a -> S.Set a -> DNF a
260−disjAdd (DNF yss) xs
261− | any (`S.isSubsetOf` xs) yss = DNF yss
262− | otherwise = DNF $ S.insert xs $ S.filter (not . S.isSubsetOf xs) yss
263−
264−instance Ord a => Lattice (DNF a) where
265− xss \/ DNF yss = S.foldl' disjAdd xss yss
266− DNF xss /\ DNF yss =
267− F.foldl' disjAdd bottom $
268− liftA2 S.union (S.toList xss) (S.toList yss)
269−
270−pattern BottomFormula :: DNF a
271−pattern BottomFormula <-
272− DNF (S.null -> True)
273− where
274− BottomFormula = DNF S.empty
275−
276−isSingleton :: S.Set a -> Maybe a
277−isSingleton s
278− | S.size s == 1 = S.lookupMin s
279− | otherwise = Nothing
280−
281−pattern Conjunct :: Ord a => [a] -> S.Set a
282−pattern Conjunct xs <-
283− (S.toList -> xs)
284− where
285− Conjunct = S.fromList
286−
287−{-# COMPLETE Conjunct #-}
288−
289−pattern SingleConjunct :: Ord a => [a] -> DNF a
290−pattern SingleConjunct xs <-
291− DNF (isSingleton -> Just (Conjunct xs))
292− where
293− SingleConjunct xs = DNF $ S.singleton $ Conjunct xs
294−
295−pattern TopFormula :: DNF a
296−pattern TopFormula <-
297− DNF (isSingleton -> Just (S.null -> True))
298− where
299− TopFormula = DNF $ S.singleton S.empty
300−
301−instance Ord a => BoundedJoinSemiLattice (DNF a) where
302− bottom = BottomFormula
303−
304−instance Ord a => BoundedMeetSemiLattice (DNF a) where
305− top = TopFormula
306−
307−foldLattice
308− :: BoundedLattice l
309− => (a -> l)
310− -> DNF a
311− -> l
312−foldLattice f (DNF xss) =
313− S.foldl'
314− (\z w ->
315− z \/ S.foldl' (\x y -> x /\ f y) top w)
316− bottom
317− xss
318−
319−newtype JsonFormula t = JsonFormula {getJsonFormula :: DNF (Condition t)}
320− deriving stock (Eq, Ord, Show)
321− deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
322−
323−satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
324−satisfiesFormula val = foldLattice (satisfiesTyped val) . getJsonFormula
325−
326−data ForeachType (f :: JsonType -> Type) = ForeachType
327− { forNull :: f 'Null
328− , forBoolean :: f 'Boolean
329− , forNumber :: f 'Number
330− , forString :: f 'String
331− , forArray :: f 'Array
332− , forObject :: f 'Object
333− }
334−
335−satisfies :: A.Value -> ForeachType JsonFormula -> Bool
336−satisfies val p = case val of
337− A.Null -> satisfiesFormula TNull $ forNull p
338− A.Bool b -> satisfiesFormula (TBool b) $ forBoolean p
339− A.Number n -> satisfiesFormula (TNumber n) $ forNumber p
340− A.String s -> satisfiesFormula (TString s) $ forString p
341− A.Array a -> satisfiesFormula (TArray a) $ forArray p
342− A.Object o -> satisfiesFormula (TObject o) $ forObject p
343−
344−deriving stock instance (forall x. Typeable x => Eq (f x)) => Eq (ForeachType f)
345−
346−deriving stock instance (forall x. Typeable x => Ord (f x)) => Ord (ForeachType f)
347−
348−deriving stock instance (forall x. Typeable x => Show (f x)) => Show (ForeachType f)
349−
350−foldType
351− :: Monoid m
352− => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m)
353− -> m
354−foldType k =
355− k Null forNull
356− <> k Boolean forBoolean
357− <> k Number forNumber
358− <> k String forString
359− <> k Array forArray
360− <> k Object forObject
361−
362−forType_
363− :: Applicative m
364− => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m ())
365− -> m ()
366−forType_ k = do
367− k Null forNull
368− k Boolean forBoolean
369− k Number forNumber
370− k String forString
371− k Array forArray
372− k Object forObject
373− pure ()
374−
375−instance (forall x. Lattice (f x)) => Lattice (ForeachType f) where
376− f1 \/ f2 =
377− ForeachType
378− { forNull = forNull f1 \/ forNull f2
379− , forBoolean = forBoolean f1 \/ forBoolean f2
380− , forNumber = forNumber f1 \/ forNumber f2
381− , forString = forString f1 \/ forString f2
382− , forArray = forArray f1 \/ forArray f2
383− , forObject = forObject f1 \/ forObject f2
384− }
385− f1 /\ f2 =
386− ForeachType
387− { forNull = forNull f1 /\ forNull f2
388− , forBoolean = forBoolean f1 /\ forBoolean f2
389− , forNumber = forNumber f1 /\ forNumber f2
390− , forString = forString f1 /\ forString f2
391− , forArray = forArray f1 /\ forArray f2
392− , forObject = forObject f1 /\ forObject f2
393− }
394−
395−instance
396− (forall x. BoundedJoinSemiLattice (f x))
397− => BoundedJoinSemiLattice (ForeachType f)
398− where
399− bottom =
400− ForeachType
401− { forNull = bottom
402− , forBoolean = bottom
403− , forNumber = bottom
404− , forString = bottom
405− , forArray = bottom
406− , forObject = bottom
407− }
408−
409−instance
410− (forall x. BoundedMeetSemiLattice (f x))
411− => BoundedMeetSemiLattice (ForeachType f)
412− where
413− top =
414− ForeachType
415− { forNull = top
416− , forBoolean = top
417− , forNumber = top
418− , forString = top
419− , forArray = top
420− , forObject = top
421− }
422−
423−instance Steppable Schema (Referenced Schema) where
424− data Step Schema (Referenced Schema)
425− = AllOfStep Int
426− | OneOfStep Int
427− | AnyOfStep Int
428− | ItemsObjectStep
429− | ItemsArrayStep Int
430− | AdditionalPropertiesStep
431− | NotStep
432− deriving stock (Eq, Ord, Show)
433−
434−instance Steppable (Referenced Schema) (Referenced Schema) where
435− data Step (Referenced Schema) (Referenced Schema)
436− = -- | Invariant (for better memoization only): the "tail" of the trace is
437− -- the "least" of the traces of the conjuncted schemata
438− ConjunctedWith (NE.NonEmpty (Trace (Referenced Schema)))
439− | Partitioned PartitionLocation PartitionChoice
440− deriving stock (Eq, Ord, Show)
441−
442−instance Steppable Schema (Definitions (Referenced Schema)) where
443− data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
444− deriving stock (Eq, Ord, Show)
445−
446−instance Steppable Schema Discriminator where
447− data Step Schema Discriminator = DiscriminatorStep
448− deriving stock (Eq, Ord, Show)
449−
450−instance Steppable Discriminator (Definitions (Referenced Schema)) where
451− data Step Discriminator (Definitions (Referenced Schema)) = DiscriminatorMapping
452− deriving stock (Eq, Ord, Show)
453−
454−parseDiscriminatorValue :: Text -> Referenced Schema
455−parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$ref" A..= v] of
456− A.Success x -> x
457− A.Error _ -> Ref $ Reference v
458−
459−-- | A fake writer monad that doesn't actually record anything and allows lazy recursion.
460−newtype Silent q f r a = Silent {runSilent :: a}
461− deriving stock (Functor)
462− deriving (Applicative, Monad) via Identity
463−
464−instance MonadWriter (P.PathsPrefixTree q f r) (Silent q f r) where
465− tell _ = Silent ()
466− listen (Silent x) = Silent (x, P.empty)
467− pass (Silent (x, _)) = Silent x
468−
469−instance MonadState (MemoState ()) (Silent q f r) where
470− get = Silent $ runIdentity $ runMemo () get
471− put _ = pure ()
472−
473−type MonadProcess m =
474− ( MonadReader (Traced (Definitions Schema)) m
475− , MonadWriter (P.PathsPrefixTree Behave AnIssue 'SchemaLevel) m
476− , MonadState (MemoState ()) m
477− )
478−
479−type SilentM = ReaderT (Traced (Definitions Schema)) (Silent Behave AnIssue 'SchemaLevel)
480−
481−warn :: MonadProcess m => Issue 'SchemaLevel -> m ()
482−warn issue = tell $ P.singleton $ AnItem Root $ anIssue issue
483−
484−-- | Ignore warnings but allow a recursive loop that lazily computes a recursive 'Condition'.
485−silently :: MonadProcess m => SilentM a -> m a
486−silently m = do
487− defs <- R.ask
488− pure . runSilent $ runReaderT m defs
489−
490−warnKnot :: MonadProcess m => KnotTier (ForeachType JsonFormula) () m
491−warnKnot =
492− KnotTier
493− { onKnotFound = warn UnguardedRecursion
494− , onKnotUsed = \_ -> pure bottom
495− , tieKnot = \_ -> pure
496− }
497−
498−processRefSchema
499− :: MonadProcess m
500− => Traced (Referenced Schema)
501− -> m (ForeachType JsonFormula)
502−processRefSchema x = do
503− defs <- R.ask
504− memoWithKnot warnKnot (processSchema $ dereference defs x) (ask x)
505−
506−tracedAllOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
507−tracedAllOf sch =
508− _schemaAllOf (extract sch) <&> \xs ->
509− [traced (ask sch >>> step (AllOfStep i)) x | (i, x) <- zip [0 ..] xs]
510−
511−tracedAnyOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
512−tracedAnyOf sch =
513− _schemaAnyOf (extract sch) <&> \xs ->
514− [traced (ask sch >>> step (AnyOfStep i)) x | (i, x) <- zip [0 ..] xs]
515−
516−tracedOneOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
517−tracedOneOf sch =
518− _schemaOneOf (extract sch) <&> \xs ->
519− [traced (ask sch >>> step (OneOfStep i)) x | (i, x) <- zip [0 ..] xs]
520−
521−tracedItems :: Traced Schema -> Maybe (Either (Traced (Referenced Schema)) [Traced (Referenced Schema)])
522−tracedItems sch =
523− _schemaItems (extract sch) <&> \case
524− OpenApiItemsObject x -> Left $ traced (ask sch >>> step ItemsObjectStep) x
525− OpenApiItemsArray xs ->
526− Right
527− [traced (ask sch >>> step (ItemsArrayStep i)) x | (i, x) <- zip [0 ..] xs]
528−
529−tracedAdditionalProperties :: Traced Schema -> Maybe (Either Bool (Traced (Referenced Schema)))
530−tracedAdditionalProperties sch =
531− _schemaAdditionalProperties (extract sch) <&> \case
532− AdditionalPropertiesAllowed b -> Left b
533− AdditionalPropertiesSchema x -> Right $ traced (ask sch >>> step AdditionalPropertiesStep) x
534−
535−tracedDiscriminator :: Traced Schema -> Maybe (Traced Discriminator)
536−tracedDiscriminator = sequence . stepTraced DiscriminatorStep . fmap _schemaDiscriminator
537−
538−tracedProperties :: Traced Schema -> IOHM.InsOrdHashMap Text (Traced (Referenced Schema))
539−tracedProperties sch =
540− IOHM.mapWithKey
541− (\k -> traced (ask sch >>> step PropertiesStep >>> step (InsOrdHashMapKeyStep k)))
542− (_schemaProperties $ extract sch)
543−
544−-- | Turn a schema into a tuple of 'JsonFormula's that describes the condition
545−-- for every possible type of a JSON value. The conditions are independent, and
546−-- are thus checked independently.
547−processSchema
548− :: MonadProcess m
549− => Traced Schema
550− -> m (ForeachType JsonFormula)
551−processSchema sch@(extract -> Schema {..}) = do
552− let singletonFormula :: Condition t -> JsonFormula t
553− singletonFormula f = JsonFormula $ SingleConjunct [f]
554−
555− allClauses <- case tracedAllOf sch of
556− Nothing -> pure []
557− Just [] -> [] <$ warn (InvalidSchema "no items in allOf")
558− Just xs -> mapM processRefSchema xs
559−
560− anyClause <- case tracedAnyOf sch of
561− Nothing -> pure top
562− Just [] -> bottom <$ warn (InvalidSchema "no items in anyOf")
563− Just xs -> joins <$> mapM processRefSchema xs
564−
565− oneClause <- case tracedOneOf sch of
566− Nothing -> pure top
567− Just [] -> bottom <$ warn (InvalidSchema "no items in oneOf")
568− Just xs -> do
569− checkOneOfDisjoint xs >>= \case
570− True -> pure ()
571− False -> warn (NotSupported "Could not determine that oneOf branches are disjoint")
572− joins <$> mapM processRefSchema xs
573−
574− case _schemaNot of
575− Nothing -> pure ()
576− Just _ -> warn (NotSupported "not clause is unsupported")
577−
578− let typeClause = case _schemaType of
579− Nothing -> top
580− Just OpenApiNull ->
581− bottom
582− { forNull = top
583− }
584− Just OpenApiBoolean ->
585− bottom
586− { forBoolean = top
587− }
588− Just OpenApiNumber ->
589− bottom
590− { forNumber = top
591− }
592− Just OpenApiInteger ->
593− bottom
594− { forNumber = singletonFormula $ MultipleOf 1
595− }
596− Just OpenApiString ->
597− bottom
598− { forString = top
599− }
600− Just OpenApiArray ->
601− bottom
602− { forArray = top
603− }
604− Just OpenApiObject ->
605− bottom
606− { forObject = top
607− }
608−
609− let valueEnum A.Null =
610− bottom
611− { forNull = singletonFormula $ Exactly TNull
612− }
613− valueEnum (A.Bool b) =
614− bottom
615− { forBoolean = singletonFormula $ Exactly $ TBool b
616− }
617− valueEnum (A.Number n) =
618− bottom
619− { forNumber = singletonFormula $ Exactly $ TNumber n
620− }
621− valueEnum (A.String s) =
622− bottom
623− { forString = singletonFormula $ Exactly $ TString s
624− }
625− valueEnum (A.Array a) =
626− bottom
627− { forArray = singletonFormula $ Exactly $ TArray a
628− }
629− valueEnum (A.Object o) =
630− bottom
631− { forObject = singletonFormula $ Exactly $ TObject o
632− }
633− enumClause <- case _schemaEnum of
634− Nothing -> pure top
635− Just [] -> bottom <$ warn (InvalidSchema "no items in enum")
636− Just xs -> pure $ joins (valueEnum <$> xs)
637−
638− let maximumClause = case _schemaMaximum of
639− Nothing -> top
640− Just n ->
641− top
642− { forNumber = singletonFormula $
643− Maximum $
644− case _schemaExclusiveMaximum of
645− Just True -> Exclusive n
646− _ -> Inclusive n
647− }
648−
649− minimumClause = case _schemaMinimum of
650− Nothing -> top
651− Just n ->
652− top
653− { forNumber = singletonFormula $
654− Minimum $
655− Down $
656− case _schemaExclusiveMinimum of
657− Just True -> Exclusive $ Down n
658− _ -> Inclusive $ Down n
659− }
660−
661− multipleOfClause = case _schemaMultipleOf of
662− Nothing -> top
663− Just n ->
664− top
665− { forNumber = singletonFormula $ MultipleOf n
666− }
667−
668− formatClause <- case _schemaFormat of
669− Nothing -> pure top
670− Just f
671− | f `elem` ["int32", "int64", "float", "double"] ->
672− pure
673− top
674− { forNumber = singletonFormula $ NumberFormat f
675− }
676− Just f
677− | f `elem` ["byte", "binary", "date", "date-time", "password", "uuid"] ->
678− pure
679− top
680− { forString = singletonFormula $ StringFormat f
681− }
682− Just f -> top <$ warn (NotSupported $ "Unknown format: " <> f)
683−
684− let maxLengthClause = case _schemaMaxLength of
685− Nothing -> top
686− Just n ->
687− top
688− { forString = singletonFormula $ MaxLength n
689− }
690−
691− minLengthClause = case _schemaMinLength of
692− Nothing -> top
693− Just n ->
694− top
695− { forString = singletonFormula $ MinLength n
696− }
697−
698− patternClause = case _schemaPattern of
699− Nothing -> top
700− Just p ->
701− top
702− { forString = singletonFormula $ Pattern p
703− }
704−
705− itemsClause <- case tracedItems sch of
706− Nothing -> pure top
707− Just (Left rs) -> do
708− f <- silently $ processRefSchema rs
709− pure top {forArray = singletonFormula $ Items f rs}
710− Just (Right _) -> top <$ warn (NotSupported "array in items is not supported")
711−
712− let maxItemsClause = case _schemaMaxItems of
713− Nothing -> top
714− Just n ->
715− top
716− { forArray = singletonFormula $ MaxItems n
717− }
718−
719− minItemsClause = case _schemaMinItems of
720− Nothing -> top
721− Just n ->
722− top
723− { forArray = singletonFormula $ MinItems n
724− }
725−
726− uniqueItemsClause = case _schemaUniqueItems of
727− Just True ->
728− top
729− { forArray = singletonFormula UniqueItems
730− }
731− _ -> top
732−
733− (addProps, addPropSchema) <- case tracedAdditionalProperties sch of
734− Just (Right rs) -> (,Just rs) <$> silently (processRefSchema rs)
735− Just (Left False) -> pure (bottom, Nothing)
736− _ -> pure (top, Just $ traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty)
737− propList <- forM (S.toList . S.fromList $ IOHM.keys _schemaProperties <> _schemaRequired) $ \k -> do
738− (f, psch) <- case IOHM.lookup k $ tracedProperties sch of
739− Just rs -> (,rs) <$> silently (processRefSchema rs)
740− Nothing ->
741− let fakeSchema = traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty
742− in -- The mempty here is incorrect, but if addPropSchema was Nothing, then
743− -- addProps is bottom, and k is in _schemaRequired. We handle this situation
744− -- below and short-circuit the entire Properties condition to bottom
745− pure (addProps, fromMaybe fakeSchema addPropSchema)
746− pure (k, Property (k `elem` _schemaRequired) f psch)
747− let allBottom f = getAll $
748− foldType $ \_ ty -> case getJsonFormula $ ty f of
749− BottomFormula -> All True
750− _ -> All False
751− allTop f = getAll $
752− foldType $ \_ ty -> case getJsonFormula $ ty f of
753− TopFormula -> All True
754− _ -> All False
755− -- remove optional fields whose schemata match that of additional props
756− propMap = M.filter (\p -> propRequired p || propFormula p /= addProps) $ M.fromList propList
757− propertiesClause
758− | any (\p -> propRequired p && allBottom (propFormula p)) propMap =
759− bottom -- if any required field has unsatisfiable schema
760− | M.null propMap
761− , allTop addProps =
762− top -- if all fields are optional and have trivial schemata
763− | otherwise =
764− top
765− { forObject = singletonFormula $ Properties propMap addProps addPropSchema
766− }
767−
768− maxPropertiesClause = case _schemaMaxProperties of
769− Nothing -> top
770− Just n ->
771− top
772− { forObject = singletonFormula $ MaxProperties n
773− }
774−
775− minPropertiesClause = case _schemaMinProperties of
776− Nothing -> top
777− Just n ->
778− top
779− { forObject = singletonFormula $ MinProperties n
780− }
781−
782− nullableClause
783− | Just True <- _schemaNullable =
784− bottom
785− { forNull = singletonFormula $ Exactly TNull
786− }
787− | otherwise = bottom
788−
789− pure $
790− nullableClause
791− \/ meets
792− (allClauses
793− <> [ anyClause
794− , oneClause
795− , typeClause
796− , enumClause
797− , maximumClause
798− , minimumClause
799− , multipleOfClause
800− , formatClause
801− , maxLengthClause
802− , minLengthClause
803− , patternClause
804− , itemsClause
805− , maxItemsClause
806− , minItemsClause
807− , uniqueItemsClause
808− , propertiesClause
809− , maxPropertiesClause
810− , minPropertiesClause
811− ])
812−
813−{- TODO: ReadOnly/WriteOnly #68 -}
814−
815−checkOneOfDisjoint :: MonadProcess m => [Traced (Referenced Schema)] -> m Bool
816−checkOneOfDisjoint = const $ pure True -- TODO #69
817−
818−schemaToFormula
819− :: Traced (Definitions Schema)
820− -> Traced Schema
821− -> (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
822−schemaToFormula defs rs = runWriter . (`runReaderT` defs) . runMemo () $ processSchema rs
823−
824−checkFormulas
825− :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
826− => HList xs
827− -> Behavior 'SchemaLevel
828− -> ProdCons (Traced (Definitions Schema))
829− -> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
830− -> SemanticCompatFormula ()
831−checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
832− case P.toList ep ++ P.toList ec of
833− issues@(_ : _) -> F.for_ issues $ embedFormula beh . anItem
834− [] -> do
835− -- We have the following isomorphisms:
836− -- (A ⊂ X ∪ Y) = (A ⊂ X) \/ (A ⊂ Y)
837− -- (A ⊂ X ∩ Y) = (A ⊂ X) /\ (A ⊂ Y)
838− -- (A ⊂ ⊤) = 1
839− -- (X ∪ Y ⊂ B) = (X ⊂ B) /\ (Y ⊂ B)
840− -- (∅ ⊂ B) = 1
841− -- The remaining cases are, notably, not isomorphisms:
842− -- 1) (A ⊂ ∅) <= 0
843− -- 2) (X ∩ Y ⊂ B) <= (X ⊂ B) \/ (Y ⊂ B)
844− -- 3) (⊤ ⊂ B) <= 0
845− -- Therefore we have the isomorphisms with (∃ and ∀ being the N-ary
846− -- versions of \/ and /\ respectively):
847− -- (⋃_i ⋂_j A[i,j]) ⊂ (⋃_k ⋂_l B[k,l])
848− -- = ∃k ∀l ∀i, (⋂_j A[i,j]) ⊂ B[k,l]
849− -- = ∀i ∃k ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
850− -- with the caveat that the the set over which k ranges is nonempty.
851− -- This is because 1) is not an isomorphism.
852− -- Our disjunction loses information, so it makes sense to nest it as
853− -- deeply as possible, hence we choose the latter representation.
854− --
855− -- We delegate the verification of (⋂_j A[j]) ⊂ B to a separate heuristic
856− -- function, with the understanding that ∃j, A[j] ⊂ B is a sufficient,
857− -- but not necessary condition (because of 2) and 3)).
858− --
859− -- If k ranges over an empty set, we have the isomorphism:
860− -- (⋃_i ⋂_j A[i,j]) ⊂ ∅ = ∀i, (⋂_j A[i,j]) ⊂ ∅
861− -- where we again delegate (⋂_j A[j]) ⊂ ∅ to a heuristic, though here the
862− -- shortcut of ∃j, A[j] ⊂ ∅ hardly helps.
863− --
864− -- Disjunctions tend to erase informative error messages, so we may want
865− -- to avoid them. This can be formally done as follows: if we can
866− -- partition the universal set into a disjoint union of some parts:
867− -- ⊤ = ⊔_α P[α]
868− -- such that the conjuncts in our disjunctive normal form are subordinate
869− -- to the partition:
870− -- ∀i ∃α, (⋂_j A[i,j]) ⊂ P[α]
871− -- ∀k ∃α, (⋂_l B[k,l]) ⊂ P[α]
872− -- then we can partition the sets over which i and k range into partitions
873− -- I[α] and K[α], and then in each "bucket" verify the inclusion in the
874− -- aforementioned way:
875− -- ∀α, (⋃_i∈I[α] ⋂_j A[i,j]) ⊂ (⋃_k∈K[α] ⋂_l B[k,l])
876− -- = ∀α ∀i∈I[α] ∃k∈K[α] ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
877− -- We already somewhat do this by partitioning JSON into types, but we can
878− -- additionally partition e.g. "enum" fields or existence of particular
879− -- properties. This works especially well if we manage to ensure K[α] are
880− -- 1-element sets.
881− --
882− -- Since the set:
883− -- (⋃_i∈I[α] ⋂_j A[i,j]) = (⋃_i ⋂_j A[i,j]) ∩ P[α]
884− -- does not actually appear in the source schema, we need to construct it
885− -- ourselves and come up with a name for it.
886− let typesRestricted = not (anyBottomTypes fp) && anyBottomTypes fc
887− -- Specifically handle the case when a schema's type has been
888− -- restricted from "all" to specific types: if all types were allowed
889− -- in the producer and not all types are allowed in the consumer, it's
890− -- usually easier to say what's left than what's removed
891− when typesRestricted $ issueAt beh $ TypesRestricted $ nonBottomTypes fc
892− forType_ $ \tyName ty -> do
893− let beh' = beh >>> step (OfType tyName)
894− case (getJsonFormula $ ty fp, getJsonFormula $ ty fc) of
895− (DNF pss, BottomFormula) -> unless typesRestricted $ do
896− -- don't repeat the TypesRestricted issue
897− F.for_ pss $ \(Conjunct ps) -> checkContradiction beh' ps
898− (DNF pss, SingleConjunct cs) -> F.for_ pss $ \(Conjunct ps) -> do
899− F.for_ cs $ checkImplication env beh' ps -- avoid disjunction if there's only one conjunct
900− (TopFormula, DNF css) ->
901− -- producer is "open" (allows any value), but consumer has restrictions.
902− -- In this case we want to show which restrictions were added. (instead
903− -- of showing an empty list restrictions that couldn't be satisfied.)
904− F.for_ css $ \(Conjunct cs) -> F.for_ cs $ checkImplication env beh' []
905− (pss', css') -> F.for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
906− (mPart, ProdCons pf cf) -> do
907− let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
908− case (getJsonFormula pf, getJsonFormula cf) of
909− (DNF pss, BottomFormula) -> F.for_ pss $ \(Conjunct ps) -> checkContradiction beh'' ps
910− (DNF pss, SingleConjunct cs) -> F.for_ pss $ \(Conjunct ps) -> do
911− F.for_ cs $ checkImplication env beh'' ps
912− -- unlucky:
913− (DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
914− anyOfAt
915− beh'
916− (issueFromConjunct Nothing ps)
917− [F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
918− pure ()
919− where
920− anyBottomTypes f = getAny $
921− foldType $ \_ ty -> case getJsonFormula $ ty f of
922− BottomFormula -> Any True
923− _ -> mempty
924− nonBottomTypes f = foldType $ \tyName ty -> case getJsonFormula $ ty f of
925− BottomFormula -> mempty
926− _ -> [tyName]
927− issueFromConjunct :: Typeable t => Partition -> [Condition t] -> Issue 'TypedSchemaLevel
928− issueFromConjunct _ ps
929− | Just e <- findExactly ps
930− , all (satisfiesTyped e) ps =
931− EnumDoesntSatisfy $ untypeValue e -- what does this look like when partitioned?
932− issueFromConjunct mPart ps = NoMatchingCondition mPart $ SomeCondition <$> ps
933−
934−data PartitionData
935− = DByEnumValue (DNF (S.Set A.Value))
936− | DByProperties (DNF (S.Set Text, S.Set Text)) -- optional, required
937− deriving stock (Eq, Ord, Show)
938−
939−data PartitionChoice
940− = CByEnumValue (S.Set A.Value)
941− | CByProperties (S.Set Text) (S.Set Text) -- included, excluded
942− deriving stock (Eq, Ord, Show)
943−
944−conjPart :: PartitionData -> PartitionData -> Maybe PartitionData
945−conjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss /\ yss
946−conjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss /\ yss
947−conjPart _ _ = Nothing
948−
949−disjPart :: PartitionData -> PartitionData -> Maybe PartitionData
950−disjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss \/ yss
951−disjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss \/ yss
952−disjPart _ _ = Nothing
953−
954−data PartitionLocation
955− = PHere
956− | PInProperty Text PartitionLocation
957− deriving stock (Eq, Ord, Show)
958−
959−newtype Partitions = Partitions (M.Map PartitionLocation (S.Set PartitionData))
960− deriving stock (Eq, Ord, Show)
961−
962−instance Lattice Partitions where
963− Partitions xss /\ Partitions yss = Partitions $ M.unionWith conj xss yss
964− where
965− conj xs ys = S.fromList . catMaybes $ liftA2 conjPart (S.toList xs) (S.toList ys)
966− Partitions xss \/ Partitions yss = Partitions $ M.intersectionWith disj xss yss
967− where
968− disj xs ys = S.fromList . catMaybes $ liftA2 disjPart (S.toList xs) (S.toList ys)
969−
970−instance BoundedMeetSemiLattice Partitions where
971− top = Partitions M.empty
972−
973−-- The lattice has no bottom, but we use 'Lifted' to adjoin a free bottom element
974−
975−type PartitionM = ReaderT (Traced (Definitions Schema)) (State (MemoState ()))
976−
977−ignoreKnot :: KnotTier (Lifted Partitions) () PartitionM
978−ignoreKnot =
979− KnotTier
980− { onKnotFound = pure ()
981− , onKnotUsed = \_ -> pure bottom
982− , tieKnot = \_ -> pure
983− }
984−
985−singletonPart :: PartitionData -> Lifted Partitions
986−singletonPart = Lift . Partitions . M.singleton PHere . S.singleton
987−
988−partitionSchema :: Traced Schema -> PartitionM (Lifted Partitions)
989−partitionSchema sch = do
990− allClauses <- case tracedAllOf sch of
991− Nothing -> pure []
992− Just xs -> mapM partitionRefSchema xs
993−
994− anyClause <- case tracedAnyOf sch of
995− Nothing -> pure top
996− Just xs -> joins <$> mapM partitionRefSchema xs
997−
998− oneClause <- case tracedOneOf sch of
999− Nothing -> pure top
1000− Just xs -> joins <$> mapM partitionRefSchema xs
1001−
1002− byEnumClause <- case _schemaEnum $ extract sch of
1003− Nothing -> pure top
1004− Just xs ->
1005− pure . singletonPart $
1006− DByEnumValue $ SingleConjunct [S.fromList xs]
1007−
1008− -- We can only partition by presence of a property if additional properties
1009− -- are disallowed, and the property is not optional
1010− let reqd = S.fromList $ _schemaRequired $ extract sch
1011− byPropertiesClause <- case _schemaAdditionalProperties $ extract sch of
1012− Just (AdditionalPropertiesAllowed False) -> do
1013− let props = S.fromList . IOHM.keys . _schemaProperties $ extract sch
1014− pure . singletonPart $
1015− DByProperties $ SingleConjunct [(props S.\\ reqd, props `S.intersection` reqd)]
1016− _ -> pure top
1017−
1018− -- We can partition on something nested in a property only if the property is
1019− -- required
1020− let reqdProps = IOHM.filterWithKey (\k _ -> k `S.member` reqd) $ tracedProperties sch
1021− inPropertiesClauses <- forM (IOHM.toList reqdProps) $ \(k, rs) -> do
1022− f <- partitionRefSchema rs
1023− pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
1024−
1025− pure $ meets $ allClauses <> [anyClause, oneClause, byEnumClause, byPropertiesClause] <> inPropertiesClauses
1026−
1027−partitionRefSchema :: Traced (Referenced Schema) -> PartitionM (Lifted Partitions)
1028−partitionRefSchema x = do
1029− defs <- R.ask
1030− memoWithKnot ignoreKnot (partitionSchema $ dereference defs x) (ask x)
1031−
1032−newtype LiftA f a = LiftA {getLiftA :: f a}
1033− deriving newtype (Functor, Applicative)
1034−
1035−instance (Lattice a, Applicative f) => Lattice (LiftA f a) where
1036− (/\) = liftA2 (/\)
1037− (\/) = liftA2 (\/)
1038−
1039−instance (BoundedJoinSemiLattice a, Applicative f) => BoundedJoinSemiLattice (LiftA f a) where
1040− bottom = pure bottom
1041−
1042−instance (BoundedMeetSemiLattice a, Applicative f) => BoundedMeetSemiLattice (LiftA f a) where
1043− top = pure top
1044−
1045−partitionCondition :: Condition t -> PartitionM (Lifted Partitions)
1046−partitionCondition = \case
1047− Exactly x ->
1048− pure . singletonPart $
1049− DByEnumValue $ SingleConjunct [S.singleton $ untypeValue x]
1050− Properties props _ madd -> do
1051− let byProps = case madd of
1052− Just _ -> top
1053− Nothing ->
1054− singletonPart $
1055− DByProperties $
1056− SingleConjunct
1057− [ ( M.keysSet $ M.filter (not . propRequired) props
1058− , M.keysSet $ M.filter propRequired props
1059− )
1060− ]
1061− inProps <- forM (M.toList $ M.filter propRequired props) $ \(k, prop) -> do
1062− f <- partitionRefSchema $ propRefSchema prop
1063− pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
1064− pure $ byProps /\ meets inProps
1065− _ -> pure top
1066−
1067−partitionJsonFormulas
1068− :: ProdCons (Traced (Definitions Schema))
1069− -> ProdCons (JsonFormula t)
1070− -> Lifted Partitions
1071−partitionJsonFormulas defs pc = producer pcPart \/ consumer pcPart
1072− where
1073− pcPart = partitionFormula <$> defs <*> pc
1074− partitionFormula def (JsonFormula xss) = runIdentity . runMemo () . (`runReaderT` def) $ do
1075− getLiftA . foldLattice (LiftA . partitionCondition) $ xss
1076−
1077−selectPartition :: Lifted Partitions -> Maybe (PartitionLocation, S.Set PartitionChoice)
1078−selectPartition Bottom = Nothing
1079−selectPartition (Lift (Partitions m)) =
1080− go [(loc, part) | (loc, parts) <- sortBy (comparing $ locLength . fst) $ M.toList m, part <- S.toList parts]
1081− where
1082− locLength :: PartitionLocation -> Int
1083− locLength = walk 0
1084− where
1085− walk !n PHere = n
1086− walk !n (PInProperty _ l) = walk (n + 1) l
1087− go [] = Nothing
1088− -- Skip partitioning by property for now
1089− go ((_, DByProperties _) : ps) = go ps
1090− -- Don't partition by enum value at the root (this reports removed enum values as contradictions in their respective partitions)
1091− go ((PHere, DByEnumValue _) : ps) = go ps
1092− go ((loc, DByEnumValue (DNF xss)) : ps)
1093− -- Check that no disjunction branches are unresticted
1094− | Just enums <- traverse (fmap (foldr1 S.intersection) . NE.nonEmpty . S.toList) . S.toList $ xss =
1095− -- TODO: improve
1096− Just (loc, S.map (CByEnumValue . S.singleton) $ S.unions enums)
1097− | otherwise = go ps
1098−
1099−-- This essentially has 3 cases:
1100−-- Nothing -- we have produced a bottom schema
1101−-- Just (False, _) -- there's been no change to the schema
1102−-- Just (True, x) -- x is a new schema
1103−type IntersectionM = ReaderT (Traced (Definitions Schema)) (WriterT Any Maybe)
1104−
1105−mBottom :: IntersectionM a
1106−mBottom = lift . lift $ Nothing
1107−
1108−catchBottom :: IntersectionM a -> IntersectionM a -> IntersectionM a
1109−catchBottom act handler = R.liftCatch (W.liftCatch (\a h -> a <|> h ())) act (\_ -> handler)
1110−
1111−mChange :: IntersectionM ()
1112−mChange = tell $ Any True
1113−
1114−intersectSchema
1115− :: PartitionLocation
1116− -> PartitionChoice
1117− -> Traced Schema
1118− -> IntersectionM Schema
1119−intersectSchema loc part sch = do
1120− allOf' <- forM (tracedAllOf sch) $ \rss ->
1121− -- Assuming i ranges over a nonempty set (checked in processSchema)
1122− -- (⋂_i A[i]) ∩ X = ⋂_i (A[i] ∩ X)
1123− -- If any intersections are empty, the result is empty. If any intersections are a change, the result is a change.
1124− traverse (intersectRefSchema loc part) rss
1125− anyOf' <- forM (tracedAnyOf sch) $ \rss -> do
1126− -- (⋃_i A[i]) ∩ X = ⋃_i (A[i] ∩ X)
1127− -- Collect only the nonempty A[i] ∩ X, unless there are none, in which case the result is empty.
1128− -- If any schema is empty, we remove it from the list which constitutes a change.
1129− mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
1130− case catMaybes mSchemas of
1131− [] -> mBottom
1132− schs -> pure schs
1133− oneOf' <- forM (tracedOneOf sch) $ \rss -> do
1134− -- Same as anyOf'. By intersecting we're only making them more disjoint if anything.
1135− mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
1136− case catMaybes mSchemas of
1137− [] -> mBottom
1138− schs -> pure schs
1139− let sch' = (extract sch) {_schemaAllOf = allOf', _schemaAnyOf = anyOf', _schemaOneOf = oneOf'}
1140− -- Now the local changes:
1141− case loc of
1142− PInProperty k loc' -> case IOHM.lookup k $ tracedProperties sch of
1143− Nothing -> error $ "Partitioning via absent property: " <> T.unpack k
1144− Just prop -> do
1145− prop' <- intersectRefSchema loc' part prop
1146− pure $ sch' {_schemaProperties = IOHM.adjust (const prop') k $ _schemaProperties sch'}
1147− PHere -> case part of
1148− CByEnumValue vals -> do
1149− enum' <- case _schemaEnum sch' of
1150− Nothing -> do
1151− mChange
1152− pure $ S.toList vals
1153− Just xs -> do
1154− when (any (`S.notMember` vals) xs) mChange
1155− case filter (`S.member` vals) xs of
1156− [] -> mBottom
1157− xs' -> pure xs'
1158− pure $ sch' {_schemaEnum = Just enum'}
1159− CByProperties {} -> error "CByProperties not implemented"
1160−
1161−intersectRefSchema
1162− :: PartitionLocation
1163− -> PartitionChoice
1164− -> Traced (Referenced Schema)
1165− -> IntersectionM (Referenced Schema)
1166−intersectRefSchema loc part rs = do
1167− defs <- R.ask
1168− Inline <$> intersectSchema loc part (dereference defs rs)
1169−
1170−intersectCondition :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> Condition t -> DNF (Condition t)
1171−intersectCondition _defs PHere (CByEnumValue values) cond@(Exactly x) =
1172− if untypeValue x `S.member` values then SingleConjunct [cond] else bottom
1173−intersectCondition defs (PInProperty k loc) part cond@(Properties props add madd) = case M.lookup k props of
1174− Nothing -> SingleConjunct [cond] -- shouldn't happen
1175− Just prop -> case runWriterT . (`runReaderT` defs) $ intersectRefSchema loc part $ propRefSchema prop of
1176− Just (rs', Any True) ->
1177− let trs' = traced (ask (propRefSchema prop) >>> step (Partitioned loc part)) rs'
1178− in SingleConjunct [Properties (M.insert k prop {propRefSchema = trs'} props) add madd]
1179− Just (_, Any False) -> SingleConjunct [cond]
1180− Nothing -> bottom
1181−intersectCondition _defs _loc _part cond = SingleConjunct [cond]
1182−
1183−intersectFormula :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> JsonFormula t -> JsonFormula t
1184−intersectFormula defs loc part = JsonFormula . foldLattice (intersectCondition defs loc part) . getJsonFormula
1185−
1186−type Partition = Maybe (PartitionLocation, PartitionChoice)
1187−
1188−tryPartition :: ProdCons (Traced (Definitions Schema)) -> ProdCons (JsonFormula t) -> [(Partition, ProdCons (JsonFormula t))]
1189−tryPartition defs pc = case selectPartition $ partitionJsonFormulas defs pc of
1190− Nothing -> [(Nothing, pc)]
1191− Just (loc, parts) -> [(Just (loc, part), intersectFormula <$> defs <*> pure loc <*> pure part <*> pc) | part <- S.toList parts]
1192−
1193−checkContradiction
1194− :: Behavior 'TypedSchemaLevel
1195− -> [Condition t]
1196− -> SemanticCompatFormula ()
1197−checkContradiction beh _ = issueAt beh NoContradiction -- TODO #70
1198−
1199−tracedConjunct :: NE.NonEmpty (Traced (Referenced Schema)) -> Traced (Referenced Schema)
1200−tracedConjunct refSchemas = case NE.sortWith ask refSchemas of
1201− (rs NE.:| []) -> rs
1202− (rs1 NE.:| rs2 : rss) ->
1203− traced (ask rs1 >>> step (ConjunctedWith $ ask <$> rs2 NE.:| rss)) $
1204− Inline mempty {_schemaAllOf = Just $ extract <$> rs1 : rs2 : rss}
1205−
1206−checkImplication
1207− :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
1208− => HList xs
1209− -> Behavior 'TypedSchemaLevel
1210− -> [Condition t]
1211− -> Condition t
1212− -> SemanticCompatFormula ()
1213−checkImplication env beh prods cons = case findExactly prods of
1214− Just e
1215− | all (satisfiesTyped e) prods ->
1216− if satisfiesTyped e cons
1217− then pure ()
1218− else issueAt beh (EnumDoesntSatisfy $ untypeValue e)
1219− | otherwise -> pure () -- vacuously true
1220− Nothing -> case cons of
1221− -- the above code didn't catch it, so there's no Exactly condition on the lhs
1222− Exactly e -> issueAt beh (NoMatchingEnum $ untypeValue e)
1223− Maximum m -> case findRelevant min (\case Maximum m' -> Just m'; _ -> Nothing) prods of
1224− Just m' ->
1225− if m' <= m
1226− then pure ()
1227− else issueAt beh (MatchingMaximumWeak $ ProdCons {producer = m', consumer = m})
1228− Nothing -> issueAt beh (NoMatchingMaximum m)
1229− Minimum m -> case findRelevant max (\case Minimum m' -> Just m'; _ -> Nothing) prods of
1230− Just m' ->
1231− if m' >= m
1232− then pure ()
1233− else issueAt beh (MatchingMinimumWeak ProdCons {producer = coerce m', consumer = coerce m})
1234− Nothing -> issueAt beh (NoMatchingMinimum (coerce m))
1235− MultipleOf m -> case findRelevant lcmScientific (\case MultipleOf m' -> Just m'; _ -> Nothing) prods of
1236− Just m' ->
1237− if lcmScientific m m' == m'
1238− then pure ()
1239− else issueAt beh (MatchingMultipleOfWeak $ ProdCons {producer = m', consumer = m})
1240− Nothing -> issueAt beh (NoMatchingMultipleOf m)
1241− NumberFormat f ->
1242− if any (\case NumberFormat f' -> f == f'; _ -> False) prods
1243− then pure ()
1244− else issueAt beh (NoMatchingFormat f)
1245− MaxLength m -> case findRelevant min (\case MaxLength m' -> Just m'; _ -> Nothing) prods of
1246− Just m' ->
1247− if m' <= m
1248− then pure ()
1249− else issueAt beh (MatchingMaxLengthWeak $ ProdCons {producer = m', consumer = m})
1250− Nothing -> issueAt beh (NoMatchingMaxLength m)
1251− MinLength m -> case findRelevant max (\case MinLength m' -> Just m'; _ -> Nothing) prods of
1252− Just m' ->
1253− if m' >= m
1254− then pure ()
1255− else issueAt beh (MatchingMinLengthWeak $ ProdCons {producer = m', consumer = m})
1256− Nothing -> issueAt beh (NoMatchingMinLength m)
1257− Pattern p ->
1258− if any (\case Pattern p' -> p == p'; _ -> False) prods
1259− then pure ()
1260− else issueAt beh (NoMatchingPattern p) -- TODO: regex comparison #32
1261− StringFormat f ->
1262− if any (\case StringFormat f' -> f == f'; _ -> False) prods
1263− then pure ()
1264− else issueAt beh (NoMatchingFormat f)
1265− Items _ cons' -> case findRelevant (<>) (\case Items _ rs -> Just (rs NE.:| []); _ -> Nothing) prods of
1266− Just (tracedConjunct -> rs) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
1267− Nothing -> issueAt beh NoMatchingItems
1268− MaxItems m -> case findRelevant min (\case MaxItems m' -> Just m'; _ -> Nothing) prods of
1269− Just m' ->
1270− if m' <= m
1271− then pure ()
1272− else issueAt beh (MatchingMaxItemsWeak ProdCons {producer = m', consumer = m})
1273− Nothing -> issueAt beh (NoMatchingMaxItems m)
1274− MinItems m -> case findRelevant max (\case MinItems m' -> Just m'; _ -> Nothing) prods of
1275− Just m' ->
1276− if m' >= m
1277− then pure ()
1278− else issueAt beh (MatchingMinItemsWeak ProdCons {producer = m', consumer = m})
1279− Nothing -> issueAt beh (NoMatchingMinItems m)
1280− UniqueItems ->
1281− if any (== UniqueItems) $ prods
1282− then pure ()
1283− else issueAt beh NoMatchingUniqueItems
1284− Properties props _ madd -> case findRelevant (<>) (\case Properties props' _ madd' -> Just $ (props', madd') NE.:| []; _ -> Nothing) prods of
1285− Just pm ->
1286− anyOfAt beh NoMatchingProperties $
1287− NE.toList pm <&> \(props', madd') -> do
1288− F.for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
1289− let beh' = beh >>> step (InProperty k)
1290− go sch sch' = checkCompatibility beh' env (ProdCons sch sch')
1291− case (maybe False propRequired $ M.lookup k props', maybe False propRequired $ M.lookup k props) of
1292− -- producer does not require field, but consumer does (can fail)
1293− (False, True) -> issueAt beh' PropertyNowRequired
1294− _ -> pure ()
1295− case (M.lookup k props', madd', M.lookup k props, madd) of
1296− -- (producer, additional producer, consumer, additional consumer)
1297− (Nothing, Nothing, _, _) -> pure () -- vacuously: the producer asserts that this field cannot exist,
1298− -- and the consumer either doesn't require it, or it does and we've already raised an error about it.
1299− (_, _, Nothing, Nothing) -> issueAt beh' UnexpectedProperty
1300− (Just p', _, Just p, _) -> go (propRefSchema p') (propRefSchema p)
1301− (Nothing, Just add', Just p, _) ->
1302− clarifyIssue (AnItem beh' (anIssue AdditionalToProperty)) $
1303− go add' (propRefSchema p)
1304− (Just p', _, Nothing, Just add) ->
1305− clarifyIssue (AnItem beh' (anIssue PropertyToAdditional)) $
1306− go (propRefSchema p') add
1307− (Nothing, Just _, Nothing, Just _) -> pure ()
1308− pure ()
1309− case (madd', madd) of
1310− (Nothing, _) -> pure () -- vacuously
1311− (_, Nothing) -> issueAt beh NoAdditionalProperties
1312− (Just add', Just add) -> checkCompatibility (beh >>> step InAdditionalProperty) env (ProdCons add' add)
1313− pure ()
1314− Nothing -> issueAt beh NoMatchingProperties
1315− MaxProperties m -> case findRelevant min (\case MaxProperties m' -> Just m'; _ -> Nothing) prods of
1316− Just m' ->
1317− if m' <= m
1318− then pure ()
1319− else issueAt beh (MatchingMaxPropertiesWeak ProdCons {producer = m', consumer = m})
1320− Nothing -> issueAt beh (NoMatchingMaxProperties m)
1321− MinProperties m -> case findRelevant max (\case MinProperties m' -> Just m'; _ -> Nothing) prods of
1322− Just m' ->
1323− if m' >= m
1324− then pure ()
1325− else issueAt beh (MatchingMinPropertiesWeak ProdCons {producer = m', consumer = m})
1326− Nothing -> issueAt beh (NoMatchingMinProperties m)
1327− where
1328− findRelevant combine extr =
1329− fmap (foldr1 combine) . NE.nonEmpty . mapMaybe extr
1330− lcmScientific (toRational -> a) (toRational -> b) =
1331− fromRational $ lcm (numerator a) (numerator b) % gcd (denominator a) (denominator b)
1332−
1333−findExactly :: [Condition t] -> Maybe (TypedValue t)
1334−findExactly (Exactly x : _) = Just x
1335−findExactly (_ : xs) = findExactly xs
1336−findExactly [] = Nothing
1337−
1338−instance Issuable 'TypedSchemaLevel where
1339− data Issue 'TypedSchemaLevel
1340− = -- | producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
1341− EnumDoesntSatisfy A.Value
1342− | -- | consumer only expects a specific value which the producer does not produce.
1343− NoMatchingEnum A.Value
1344− | -- | consumer declares a maximum numeric value ($1), producer doesn't
1345− NoMatchingMaximum (Bound Scientific)
1346− | -- | consumer declares a maximum numeric value ($1), producer declares a weaker (higher) limit ($2)
1347− MatchingMaximumWeak (ProdCons (Bound Scientific))
1348− | -- | consumer declares a minimum numeric value, producer doesn't
1349− NoMatchingMinimum (Bound Scientific)
1350− | -- | consumer declares a minimum numeric value ($1), producer declares a weaker (lower) limit ($2)
1351− MatchingMinimumWeak (ProdCons (Bound Scientific))
1352− | -- | consumer declares that the numeric value must be a multiple of $1, producer doesn't
1353− NoMatchingMultipleOf Scientific
1354− | -- | consumer declares that the numeric value must be a multiple of $1, producer declares a weaker condition (multiple of $2)
1355− MatchingMultipleOfWeak (ProdCons Scientific)
1356− | -- | consumer declares a string/number format, producer declares none or a different format (TODO: improve via regex #32)
1357− NoMatchingFormat Format
1358− | -- | consumer declares a maximum length of the string ($1), producer doesn't.
1359− NoMatchingMaxLength Integer
1360− | -- | consumer declares a maximum length of the string ($1), producer declares a weaker (higher) limit ($2)
1361− MatchingMaxLengthWeak (ProdCons Integer)
1362− | -- | consumer declares a minimum length of the string ($1), producer doesn't.
1363− NoMatchingMinLength Integer
1364− | -- | consumer declares a minimum length of the string ($1), producer declares a weaker (lower) limit ($2)
1365− MatchingMinLengthWeak (ProdCons Integer)
1366− | -- | consumer declares the string value must match a regex ($1), producer doesn't declare or declares different regex (TODO: #32)
1367− NoMatchingPattern Pattern
1368− | -- | consumer declares the items of an array must satisfy some condition, producer doesn't
1369− NoMatchingItems
1370− | -- | consumer declares a maximum length of the array ($1), producer doesn't.
1371− NoMatchingMaxItems Integer
1372− | -- | consumer declares a maximum length of the array ($1), producer declares a weaker (higher) limit ($2)
1373− MatchingMaxItemsWeak (ProdCons Integer)
1374− | -- | consumer declares a minimum length of the array ($1), producer doesn't.
1375− NoMatchingMinItems Integer
1376− | -- | consumer declares a minimum length of the array ($1), producer declares a weaker (lower) limit ($2)
1377− MatchingMinItemsWeak (ProdCons Integer)
1378− | -- | consumer declares that items must be unique, producer doesn't
1379− NoMatchingUniqueItems
1380− | -- | consumer declares the properties of an object must satisfy some condition, producer doesn't
1381− NoMatchingProperties
1382− | -- | producer allows additional properties, consumer doesn't
1383− NoAdditionalProperties
1384− | -- | consumer declares a maximum number of properties in the object ($1), producer doesn't.
1385− NoMatchingMaxProperties Integer
1386− | -- | consumer declares a maximum number of properties in the object ($1), producer declares a weaker (higher) limit ($2)
1387− MatchingMaxPropertiesWeak (ProdCons Integer)
1388− | -- | consumer declares a minimum number of properties in the object ($1), producer doesn't.
1389− NoMatchingMinProperties Integer
1390− | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
1391− MatchingMinPropertiesWeak (ProdCons Integer)
1392− | -- | producer declares that the value must satisfy a disjunction of some conditions, but consumer's requirements couldn't be matched against any single one of them (TODO: split heuristic #71)
1393− NoMatchingCondition Partition [SomeCondition]
1394− | -- | producer indicates that values of this type are now allowed, but the consumer does not do so (currently we only check immediate contradictions, c.f. #70)
1395− -- AKA consumer does not have the type
1396− NoContradiction
1397− deriving stock (Eq, Ord, Show)
1398− issueIsUnsupported _ = False
1399− describeIssue Forward (EnumDoesntSatisfy v) = para "The following enum value was removed:" <> showJSONValue v
1400− describeIssue Backward (EnumDoesntSatisfy v) = para "The following enum value was added:" <> showJSONValue v
1401− describeIssue Forward (NoMatchingEnum v) = para "The following enum value has been added:" <> showJSONValue v
1402− describeIssue Backward (NoMatchingEnum v) = para "The following enum value has been removed:" <> showJSONValue v
1403− describeIssue Forward (NoMatchingMaximum b) = para $ "Upper bound has been added:" <> showBound b <> "."
1404− describeIssue Backward (NoMatchingMaximum b) = para $ "Upper bound has been removed:" <> showBound b <> "."
1405− describeIssue _ (MatchingMaximumWeak (ProdCons p c)) = para $ "Upper bound changed from " <> showBound p <> " to " <> showBound c <> "."
1406− describeIssue Forward (NoMatchingMinimum b) = para $ "Lower bound has been added: " <> showBound b <> "."
1407− describeIssue Backward (NoMatchingMinimum b) = para $ "Lower bound has been removed: " <> showBound b <> "."
1408− describeIssue _ (MatchingMinimumWeak (ProdCons p c)) = para $ "Lower bound changed from " <> showBound p <> " to " <> showBound c <> "."
1409− describeIssue Forward (NoMatchingMultipleOf n) = para $ "Value is now a multiple of " <> show' n <> "."
1410− describeIssue Backward (NoMatchingMultipleOf n) = para $ "Value is no longer a multiple of " <> show' n <> "."
1411− describeIssue _ (MatchingMultipleOfWeak (ProdCons p c)) = para $ "Value changed from being a multiple of " <> show' p <> " to being a multiple of " <> show' c <> "."
1412− describeIssue Forward (NoMatchingFormat f) = para $ "Format added: " <> code f <> "."
1413− describeIssue Backward (NoMatchingFormat f) = para $ "Format removed: " <> code f <> "."
1414− describeIssue Forward (NoMatchingMaxLength n) = para $ "Maximum length added: " <> show' n <> "."
1415− describeIssue Backward (NoMatchingMaxLength n) = para $ "Maximum length removed: " <> show' n <> "."
1416− describeIssue _ (MatchingMaxLengthWeak (ProdCons p c)) = para $ "Maximum length of the string changed from " <> show' p <> " to " <> show' c <> "."
1417− describeIssue Forward (NoMatchingMinLength n) = para $ "Minimum length of the string added: " <> show' n <> "."
1418− describeIssue Backward (NoMatchingMinLength n) = para $ "Minimum length of the string removed: " <> show' n <> "."
1419− describeIssue _ (MatchingMinLengthWeak (ProdCons p c)) = para $ "Minimum length of the string changed from " <> show' p <> " to " <> show' c <> "."
1420− describeIssue Forward (NoMatchingPattern p) = para "Pattern (regular expression) added: " <> codeBlock p
1421− describeIssue Backward (NoMatchingPattern p) = para "Pattern (regular expression) removed: " <> codeBlock p
1422− describeIssue Forward NoMatchingItems = para "Array item schema has been added."
1423− describeIssue Backward NoMatchingItems = para "Array item schema has been removed."
1424− describeIssue Forward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been added " <> show' n <> "."
1425− describeIssue Backward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been removed " <> show' n <> "."
1426− describeIssue _ (MatchingMaxItemsWeak (ProdCons p c)) = para $ "Maximum length of the array changed from " <> show' p <> " to " <> show' c <> "."
1427− describeIssue Forward (NoMatchingMinItems n) = para $ "Minimum length of the array added: " <> show' n <> "."
1428− describeIssue Backward (NoMatchingMinItems n) = para $ "Minimum length of the array removed: " <> show' n <> "."
1429− describeIssue _ (MatchingMinItemsWeak (ProdCons p c)) = para $ "Minimum length of the array changed from " <> show' p <> " to " <> show' c <> "."
1430− describeIssue Forward NoMatchingUniqueItems = para "Items are now required to be unique."
1431− describeIssue Backward NoMatchingUniqueItems = para "Items are no longer required to be unique."
1432− describeIssue Forward NoMatchingProperties = para "Property added."
1433− describeIssue Backward NoMatchingProperties = para "Property removed."
1434− describeIssue Forward NoAdditionalProperties = para "Additional properties have been removed."
1435− describeIssue Backward NoAdditionalProperties = para "Additional properties have been added."
1436− describeIssue Forward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been added: " <> show' n <> "."
1437− describeIssue Backward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been removed: " <> show' n <> "."
1438− describeIssue _ (MatchingMaxPropertiesWeak (ProdCons p c)) = para $ "Maximum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
1439− describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
1440− describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
1441− describeIssue _ (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
1442− describeIssue _ (NoMatchingCondition mPart conds) =
1443− para
1444− (case mPart of
1445− Nothing -> "Could not verify that the following conditions hold (please file a bug if you see this)"
1446− Just locPart ->
1447− showPartition locPart
1448− <> " – could not verify that the following conditions hold (please file a bug if you see this):")
1449− <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
1450− describeIssue Forward NoContradiction = para "The value has been removed."
1451− describeIssue Backward NoContradiction = para "The value has been added."
1452−
1453−showJSONValue :: A.Value -> Blocks
1454−showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1455−
1456−showJSONValueInline :: A.Value -> Inlines
1457−showJSONValueInline v = code (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1458−
1459−showBound :: Show a => Bound a -> Inlines
1460−showBound (Inclusive x) = show' x <> " inclusive"
1461−showBound (Exclusive x) = show' x <> " exclusive"
1462−
1463−show' :: Show x => x -> Inlines
1464−show' = str . T.pack . show
1465−
1466−instance Issuable 'SchemaLevel where
1467− data Issue 'SchemaLevel
1468− = -- | Some (openapi-supported) feature that we do not support was encountered in the schema
1469− NotSupported Text
1470− | -- | The schema is actually invalid
1471− InvalidSchema Text
1472− | -- | The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
1473− UnguardedRecursion
1474− | -- | Producer doesn't place any restrictions on the types, but the consumer does. List what types remain available in the consumer.
1475− TypesRestricted [JsonType]
1476− | -- | in the producer this field used to be handled as part of "additionalProperties", and the consumer this is a specific "properties" entry. Only thrown when this change actually causes other issues
1477− AdditionalToProperty
1478− | -- | in the consumer this field used to be handled as part of "additionalProperties", and the producer this is a specific "properties" entry. Only thrown when this change actually causes other issues
1479− PropertyToAdditional
1480− | -- | consumer requires a property that is not required/allowed in the producer
1481− PropertyNowRequired
1482− | -- | producer allows a property that is not allowed in the consumer
1483− UnexpectedProperty
1484− deriving stock (Eq, Ord, Show)
1485− issueIsUnsupported = \case
1486− NotSupported _ -> True
1487− InvalidSchema _ -> True
1488− UnguardedRecursion -> True
1489− _ -> False
1490−
1491− describeIssue _ (NotSupported i) =
1492− para (emph "Encountered a feature that OpenApi Diff does not support: " <> text i <> ".")
1493− describeIssue _ (InvalidSchema i) =
1494− para (emph "The schema is invalid: " <> text i <> ".")
1495− describeIssue _ UnguardedRecursion =
1496− para "Encountered recursion that is too complex for OpenApi Diff to untangle."
1497− describeIssue Forward (TypesRestricted tys) = case tys of
1498− [] -> para "No longer has any valid values." -- weird
1499− _ -> para "Values are now limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
1500− describeIssue Backward (TypesRestricted tys) = case tys of
1501− [] -> para "Any value of any type is now allowed." -- weird
1502− _ -> para "Values are no longer limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
1503− describeIssue Forward AdditionalToProperty = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
1504− describeIssue Backward AdditionalToProperty = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
1505− describeIssue Forward PropertyToAdditional = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
1506− describeIssue Backward PropertyToAdditional = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
1507− describeIssue Forward PropertyNowRequired = para "The property has become required."
1508− describeIssue Backward PropertyNowRequired = para "The property may not be present."
1509− describeIssue Forward UnexpectedProperty = para "The property has been removed."
1510− describeIssue Backward UnexpectedProperty = para "The property has been added."
1511−
1512−instance Behavable 'SchemaLevel 'TypedSchemaLevel where
1513− data Behave 'SchemaLevel 'TypedSchemaLevel
1514− = OfType JsonType
1515− deriving stock (Eq, Ord, Show)
1516−
1517− describeBehaviour (OfType t) = describeJSONType t
1518−
1519−describeJSONType :: IsString s => JsonType -> s
1520−describeJSONType = \case
1521− Null -> "Null"
1522− Boolean -> "Boolean"
1523− Number -> "Number"
1524− String -> "String"
1525− Array -> "Array"
1526− Object -> "Object"
1527−
1528−instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
1529− data Behave 'TypedSchemaLevel 'TypedSchemaLevel
1530− = InPartition (PartitionLocation, PartitionChoice)
1531− deriving stock (Eq, Ord, Show)
1532−
1533− describeBehaviour (InPartition partition) = showPartition partition
1534−
1535−showPartition :: (PartitionLocation, PartitionChoice) -> Inlines
1536−showPartition = \case
1537− (partition, CByEnumValue (S.toList -> [v])) ->
1538− "In cases where " <> renderPartitionLocation partition <> " is " <> showJSONValueInline v
1539− (partition, CByEnumValue (S.toList -> vs)) ->
1540− "In cases where " <> renderPartitionLocation partition <> " has values: "
1541− <> (fold . L.intersperse ", " . fmap showJSONValueInline $ vs)
1542− (partition, CByProperties (S.toList -> incl) (S.toList -> [])) ->
1543− "In cases where " <> renderPartitionLocation partition <> " contains the properties: " <> listCodes incl
1544− (partition, CByProperties (S.toList -> []) (S.toList -> excl)) ->
1545− "In cases where " <> renderPartitionLocation partition <> " does not contain the properties: " <> listCodes excl
1546− (partition, CByProperties (S.toList -> incl) (S.toList -> excl)) ->
1547− "In cases where " <> renderPartitionLocation partition
1548− <> " contains the properties "
1549− <> listCodes incl
1550− <> " and does not contain the properties "
1551− <> listCodes excl
1552− where
1553− listCodes :: [Text] -> Inlines
1554− listCodes = fold . L.intersperse ", " . fmap code
1555− renderPartitionLocation :: PartitionLocation -> Inlines
1556− renderPartitionLocation p = code $ "$" <> renderPartitionLocation' p
1557− where
1558− renderPartitionLocation' :: PartitionLocation -> Text
1559− renderPartitionLocation' PHere = mempty
1560− renderPartitionLocation' (PInProperty prop rest) = "." <> prop <> renderPartitionLocation' rest
1561−
1562−instance Behavable 'TypedSchemaLevel 'SchemaLevel where
1563− data Behave 'TypedSchemaLevel 'SchemaLevel
1564− = InItems
1565− | InProperty Text
1566− | InAdditionalProperty
1567− deriving stock (Eq, Ord, Show)
1568−
1569− describeBehaviour InItems = "Items"
1570− describeBehaviour (InProperty p) = "Property " <> code p
1571− describeBehaviour InAdditionalProperty = "Additional properties"
1572−
1573−instance Subtree Schema where
1574− type SubtreeLevel Schema = 'SchemaLevel
1575− type CheckEnv Schema = '[ProdCons (Traced (Definitions Schema))]
1576− checkStructuralCompatibility env pc = do
1577− structuralEq $ fmap _schemaRequired <$> pc
1578− structuralEq $ fmap _schemaNullable <$> pc
1579− structuralMaybeWith (structuralList env) $ tracedAllOf <$> pc
1580− structuralMaybeWith (structuralList env) $ tracedOneOf <$> pc
1581− structuralMaybe env $ sequence . stepTraced NotStep . fmap _schemaNot <$> pc
1582− structuralMaybeWith (structuralList env) $ tracedAnyOf <$> pc
1583− iohmStructural env $ stepTraced PropertiesStep . fmap _schemaProperties <$> pc
1584− structuralMaybeWith structuralAdditionalProperties $ tracedAdditionalProperties <$> pc
1585− structuralMaybeWith structuralDiscriminator $ tracedDiscriminator <$> pc
1586− structuralEq $ fmap _schemaReadOnly <$> pc
1587− structuralEq $ fmap _schemaWriteOnly <$> pc
1588− structuralEq $ fmap _schemaXml <$> pc
1589− structuralEq $ fmap _schemaMaxProperties <$> pc
1590− structuralEq $ fmap _schemaMinProperties <$> pc
1591− structuralEq $ fmap _schemaDefault <$> pc
1592− structuralEq $ fmap _schemaType <$> pc
1593− structuralEq $ fmap _schemaFormat <$> pc
1594− structuralMaybeWith structuralItems $ tracedItems <$> pc
1595− structuralEq $ fmap _schemaMaximum <$> pc
1596− structuralEq $ fmap _schemaExclusiveMaximum <$> pc
1597− structuralEq $ fmap _schemaMinimum <$> pc
1598− structuralEq $ fmap _schemaExclusiveMinimum <$> pc
1599− structuralEq $ fmap _schemaMaxLength <$> pc
1600− structuralEq $ fmap _schemaMinLength <$> pc
1601− structuralEq $ fmap _schemaPattern <$> pc
1602− structuralEq $ fmap _schemaMaxItems <$> pc
1603− structuralEq $ fmap _schemaMinItems <$> pc
1604− structuralEq $ fmap _schemaUniqueItems <$> pc
1605− structuralEq $ fmap _schemaEnum <$> pc
1606− structuralEq $ fmap _schemaMultipleOf <$> pc
1607− pure ()
1608− where
1609− structuralAdditionalProperties
1610− (ProdCons (Left x) (Left y)) = unless (x == y) structuralIssue
1611− structuralAdditionalProperties
1612− (ProdCons (Right x) (Right y)) =
1613− checkSubstructure env $ ProdCons x y
1614− structuralAdditionalProperties _ = structuralIssue
1615− structuralDiscriminator pc' = do
1616− structuralEq $ fmap _discriminatorPropertyName <$> pc'
1617− iohmStructural env $
1618− stepTraced DiscriminatorMapping . fmap (fmap parseDiscriminatorValue . _discriminatorMapping) <$> pc'
1619− pure ()
1620− structuralItems (ProdCons (Left a) (Left b)) =
1621− checkSubstructure env $ ProdCons a b
1622− structuralItems (ProdCons (Right a) (Right b)) =
1623− structuralList env $ ProdCons a b
1624− structuralItems _ = structuralIssue
1625− checkSemanticCompatibility env beh schs = do
1626− let defs = getH env
1627− checkFormulas env beh defs $ schemaToFormula <$> defs <*> schs