git.iko.soy/typeable/compaREST
on master help sign in
Add disjointness heuristic with the help of partitioning (#107)

* Try to detect disjointness in oneOf via partitioning * Split OpenAPI.Checker.Validate.Schema into modules * Refactor schema code * Tweak error messages for NoContradiction * Refined reports Co-authored-by: iko <ilyakooo0@gmail.com>

commit7029d2439bfb72e80a6749fa53473d1cc36da3e8 authormniip <mniip@users.noreply.github.com> dateFri 16 Jul 2021, 15:39 (5 years ago) parent2a7f02a
28 files changed+1774−1450
openapi-diff.cabal+7 src/OpenAPI/Checker/Report.hs+2−1 src/OpenAPI/Checker/Validate/Schema.hs+139−1405 src/OpenAPI/Checker/Validate/Schema/DNF.hs+101 src/OpenAPI/Checker/Validate/Schema/Issues.hs+222 src/OpenAPI/Checker/Validate/Schema/JsonFormula.hs+224 src/OpenAPI/Checker/Validate/Schema/Partition.hs+316 src/OpenAPI/Checker/Validate/Schema/Process.hs+372 src/OpenAPI/Checker/Validate/Schema/Traced.hs+114 src/OpenAPI/Checker/Validate/Schema/TypedJson.hs+118 test/golden/common/bad-oneof-unchanged/a.yaml+39 test/golden/common/bad-oneof-unchanged/b.yaml+39 test/golden/common/bad-oneof-unchanged/report.md+19 test/golden/common/bad-oneof-unchanged/trace-tree.yaml+18 test/golden/common/pathItem/operation/parameters/change/report.md+2−2 test/golden/common/pathItem/operation/parameters/change/trace-tree.yaml+2−2 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+2−2 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/trace-tree.yaml+2−2 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+2−2 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/trace-tree.yaml+2−2 test/golden/common/type-changing/report.md+4−4 test/golden/common/type-changing/trace-tree.yaml+4−4 test/golden/common/variant-added/report.md+4−4 test/golden/common/variant-added/trace-tree.yaml+4−4 test/golden/common/variant-changed/report.md+4−4 test/golden/common/variant-changed/trace-tree.yaml+4−4 test/golden/common/variant-deep-changed/report.md+4−4 test/golden/common/variant-deep-changed/trace-tree.yaml+4−4
openapi-diff.cabal+7
@@ -130,6 +130,13 @@ library
130130 , OpenAPI.Checker.Validate.RequestBody
131131 , OpenAPI.Checker.Validate.Responses
132132 , OpenAPI.Checker.Validate.Schema
133+ , OpenAPI.Checker.Validate.Schema.TypedJson
134+ , OpenAPI.Checker.Validate.Schema.DNF
135+ , OpenAPI.Checker.Validate.Schema.Issues
136+ , OpenAPI.Checker.Validate.Schema.JsonFormula
137+ , OpenAPI.Checker.Validate.Schema.Partition
138+ , OpenAPI.Checker.Validate.Schema.Process
139+ , OpenAPI.Checker.Validate.Schema.Traced
133140 , OpenAPI.Checker.Validate.SecurityRequirement
134141 , OpenAPI.Checker.Validate.Server
135142 , OpenAPI.Checker.Validate.Sums
src/OpenAPI/Checker/Report.hs+2−1
@@ -37,7 +37,8 @@ import qualified OpenAPI.Checker.PathsPrefixTree as P hiding (empty)
3737import OpenAPI.Checker.Report.Jet
3838import OpenAPI.Checker.Subtree (invertIssueOrientationP)
3939import OpenAPI.Checker.Validate.OpenApi
40−import OpenAPI.Checker.Validate.Schema
40+import OpenAPI.Checker.Validate.Schema.TypedJson
41+import OpenAPI.Checker.Validate.Schema.Issues
4142import Text.Pandoc.Builder
4243
4344type Changes = P.PathsPrefixTree Behave AnIssue 'APILevel
src/OpenAPI/Checker/Validate/Schema.hs+139−1405
@@ -1,825 +1,34 @@
1−{-# OPTIONS_GHC -fno-warn-orphans #-}
2−
1+{-# OPTIONS_GHC -Wno-orphans #-}
32module 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
3+ (
154 )
165where
176
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)
287import Control.Monad.Writer
298import qualified Data.Aeson as A
30−import qualified Data.ByteString.Lazy as BSL
319import Data.Coerce
32−import Data.Foldable
33−import qualified Data.Foldable as F
10+import Data.Foldable (for_)
3411import Data.Functor
35−import Data.Functor.Identity
3612import 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
4313import qualified Data.List.NonEmpty as NE
4414import qualified Data.Map as M
4515import Data.Maybe
46−import Data.OpenApi hiding (get)
16+import Data.OpenApi
4717import Data.Ord
4818import Data.Ratio
49−import Data.Scientific
5019import qualified Data.Set as S
51−import Data.String
5220import Data.Text (Text)
53−import qualified Data.Text as T hiding (singleton)
54−import qualified Data.Text.Encoding as T
55−import Data.Typeable
5621import OpenAPI.Checker.Behavior
57−import OpenAPI.Checker.Memo
58−import OpenAPI.Checker.Orphans ()
5922import OpenAPI.Checker.Paths
6023import qualified OpenAPI.Checker.PathsPrefixTree as P
61−import OpenAPI.Checker.References
6224import 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
25+import OpenAPI.Checker.Validate.Schema.DNF
26+import OpenAPI.Checker.Validate.Schema.Issues
27+import OpenAPI.Checker.Validate.Schema.JsonFormula
28+import OpenAPI.Checker.Validate.Schema.Traced
29+import OpenAPI.Checker.Validate.Schema.TypedJson
30+import OpenAPI.Checker.Validate.Schema.Partition
31+import OpenAPI.Checker.Validate.Schema.Process
82332
82433checkFormulas
82534 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
@@ -830,7 +39,7 @@ checkFormulas
83039 -> SemanticCompatFormula ()
83140checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
83241 case P.toList ep ++ P.toList ec of
833− issues@(_ : _) -> F.for_ issues $ embedFormula beh . anItem
42+ issues@(_ : _) -> for_ issues $ embedFormula beh . anItem
83443 [] -> do
83544 -- We have the following isomorphisms:
83645 -- (A ⊂ X ∪ Y) = (A ⊂ X) \/ (A ⊂ Y)
@@ -892,322 +101,57 @@ checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
892101 forType_ $ \tyName ty -> do
893102 let beh' = beh >>> step (OfType tyName)
894103 case (getJsonFormula $ ty fp, getJsonFormula $ ty fc) of
895− (DNF pss, BottomFormula) -> unless typesRestricted $ do
104+ (DNF pss, BottomDNF) -> unless typesRestricted $ do
896105 -- 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) ->
106+ for_ pss $ \(Disjunct ps) -> checkContradiction beh' Nothing ps
107+ (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
108+ for_ cs $ checkImplication env beh' ps -- avoid disjunction if there's only one conjunct
109+ (TopDNF, DNF css) ->
901110 -- producer is "open" (allows any value), but consumer has restrictions.
902111 -- In this case we want to show which restrictions were added. (instead
903112 -- 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
113+ for_ css $ \(Disjunct cs) -> for_ cs $ checkImplication env beh' S.empty
114+ (pss', css') -> for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
906115 (mPart, ProdCons pf cf) -> do
907116 let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
908117 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
118+ (DNF pss, BottomDNF) -> for_ pss $ \(Disjunct ps) -> checkContradiction beh' mPart ps
119+ (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
120+ for_ cs $ checkImplication env beh'' ps
912121 -- unlucky:
913− (DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
122+ (DNF pss, DNF css) -> for_ pss $ \(Disjunct ps) -> do
914123 anyOfAt
915124 beh'
916− (issueFromConjunct Nothing ps)
917− [F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
125+ (issueFromDisjunct Nothing ps)
126+ [for_ cs $ checkImplication env beh' ps | Disjunct cs <- S.toList css]
918127 pure ()
919128 where
920129 anyBottomTypes f = getAny $
921130 foldType $ \_ ty -> case getJsonFormula $ ty f of
922− BottomFormula -> Any True
131+ BottomDNF -> Any True
923132 _ -> mempty
924133 nonBottomTypes f = foldType $ \tyName ty -> case getJsonFormula $ ty f of
925− BottomFormula -> mempty
134+ BottomDNF -> mempty
926135 _ -> [tyName]
927− issueFromConjunct :: Typeable t => Partition -> [Condition t] -> Issue 'TypedSchemaLevel
928− issueFromConjunct _ ps
136+ issueFromDisjunct :: Typeable t => Maybe Partition -> S.Set (Condition t) -> Issue 'TypedSchemaLevel
137+ issueFromDisjunct _ ps
929138 | Just e <- findExactly ps
930139 , all (satisfiesTyped e) ps =
931140 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]
141+ issueFromDisjunct mPart ps = NoMatchingCondition mPart $ SomeCondition <$> S.toList ps
1192142
1193143checkContradiction
1194144 :: Behavior 'TypedSchemaLevel
1195− -> [Condition t]
145+ -> Maybe Partition
146+ -> S.Set (Condition t)
1196147 -> 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}
148+checkContradiction beh mPart _ = issueAt beh $ maybe TypeBecomesEmpty PartitionBecomesEmpty mPart -- TODO #70
1205149
1206150checkImplication
1207151 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
1208152 => HList xs
1209153 -> Behavior 'TypedSchemaLevel
1210− -> [Condition t]
154+ -> S.Set (Condition t)
1211155 -> Condition t
1212156 -> SemanticCompatFormula ()
1213157checkImplication env beh prods cons = case findExactly prods of
@@ -1217,75 +161,83 @@ checkImplication env beh prods cons = case findExactly prods of
1217161 then pure ()
1218162 else issueAt beh (EnumDoesntSatisfy $ untypeValue e)
1219163 | otherwise -> pure () -- vacuously true
164+
1220165 Nothing -> case cons of
1221166 -- the above code didn't catch it, so there's no Exactly condition on the lhs
1222167 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
168+
169+ Maximum m -> foldCheck min m NoMatchingMaximum MatchingMaximumWeak $ \case
170+ Maximum m' -> Just m'
171+ _ -> Nothing
172+
173+ Minimum m -> foldCheck max m (NoMatchingMinimum . coerce) (MatchingMinimumWeak . coerce) $ \case
174+ Minimum m' -> Just m'
175+ _ -> Nothing
176+
177+ MultipleOf m -> foldCheck lcmScientific m NoMatchingMultipleOf MatchingMultipleOfWeak $ \case
178+ MultipleOf m' -> Just m'
179+ _ -> Nothing
180+
181+ NumberFormat f -> case flip any prods $ \case
182+ NumberFormat f' -> f == f'
183+ _ -> False
184+ of
185+ True -> pure ()
186+ False -> issueAt beh (NoMatchingFormat f)
187+
188+ MaxLength m -> foldCheck min m NoMatchingMaxLength MatchingMaxLengthWeak $ \case
189+ MaxLength m' -> Just m'
190+ _ -> Nothing
191+
192+ MinLength m -> foldCheck max m NoMatchingMinLength MatchingMinLengthWeak $ \case
193+ MinLength m' -> Just m'
194+ _ -> Nothing
195+
196+ Pattern p -> case flip any prods $ \case
197+ Pattern p' -> p == p'
198+ _ -> False
199+ of
200+ True -> pure ()
201+ False -> issueAt beh (NoMatchingPattern p) -- TODO: regex comparison #32
202+
203+ StringFormat f -> case flip any prods $ \case
204+ StringFormat f' -> f == f'
205+ _ -> False
206+ of
207+ True -> pure ()
208+ False -> issueAt beh (NoMatchingFormat f)
209+
210+ Items _ cons' -> case foldSome (<>) prods $ \case
211+ Items _ rs -> Just (rs NE.:| [])
212+ _ -> Nothing
213+ of
1266214 Just (tracedConjunct -> rs) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
1267215 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
216+
217+ MaxItems m -> foldCheck min m NoMatchingMaxItems MatchingMaxItemsWeak $ \case
218+ MaxItems m' -> Just m'
219+ _ -> Nothing
220+
221+ MinItems m -> foldCheck max m NoMatchingMinItems MatchingMinItemsWeak $ \case
222+ MinItems m' -> Just m'
223+ _ -> Nothing -- TODO: tuple items
224+
225+ UniqueItems -> case flip any prods $ \case
226+ UniqueItems -> True
227+ MaxItems 1 -> True
228+ _ -> False -- TODO: tuple items
229+ of
230+ True -> pure ()
231+ False -> issueAt beh NoMatchingUniqueItems
232+
233+ Properties props _ madd -> case foldSome (<>) prods $ \case
234+ Properties props' _ madd' -> Just $ (props', madd') NE.:| []
235+ _ -> Nothing
236+ of
1285237 Just pm ->
1286− anyOfAt beh NoMatchingProperties $
238+ anyOfAt beh NoMatchingProperties $ -- TODO: could first "concat" the lists
1287239 NE.toList pm <&> \(props', madd') -> do
1288− F.for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
240+ for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
1289241 let beh' = beh >>> step (InProperty k)
1290242 go sch sch' = checkCompatibility beh' env (ProdCons sch sch')
1291243 case (maybe False propRequired $ M.lookup k props', maybe False propRequired $ M.lookup k props) of
@@ -1312,263 +264,40 @@ checkImplication env beh prods cons = case findExactly prods of
1312264 (Just add', Just add) -> checkCompatibility (beh >>> step InAdditionalProperty) env (ProdCons add' add)
1313265 pure ()
1314266 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)
1516267
1517− describeBehaviour (OfType t) = describeJSONType t
268+ MaxProperties m -> foldCheck min m NoMatchingMaxProperties MatchingMaxPropertiesWeak $ \case
269+ MaxProperties m' -> Just m'
270+ _ -> Nothing
1518271
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
272+ MinProperties m -> foldCheck max m NoMatchingMinProperties MatchingMinPropertiesWeak $ \case
273+ MinProperties m' -> Just m'
274+ _ -> Nothing
1552275 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)
276+ lcmScientific (toRational -> a) (toRational -> b) =
277+ fromRational $ lcm (numerator a) (numerator b) % gcd (denominator a) (denominator b)
1568278
1569− describeBehaviour InItems = "Items"
1570− describeBehaviour (InProperty p) = "Property " <> code p
1571− describeBehaviour InAdditionalProperty = "Additional properties"
279+ foldCheck
280+ :: Eq a
281+ => (a -> a -> a)
282+ -> a
283+ -> (a -> Issue 'TypedSchemaLevel)
284+ -> (ProdCons a -> Issue 'TypedSchemaLevel)
285+ -> (forall t. Condition t -> Maybe a)
286+ -> SemanticCompatFormula ()
287+ foldCheck f m missing weak extr = case foldSome f prods extr of
288+ Just m'
289+ | f m' m == m' -> pure ()
290+ | otherwise -> issueAt beh (weak ProdCons {producer = m', consumer = m})
291+ Nothing -> issueAt beh (missing m)
292+
293+foldSome :: (b -> b -> b) -> S.Set a -> (a -> Maybe b) -> Maybe b
294+foldSome combine xs extr =
295+ fmap (foldr1 combine) . NE.nonEmpty . mapMaybe extr . S.toList $ xs
296+
297+findExactly :: S.Set (Condition t) -> Maybe (TypedValue t)
298+findExactly xs = foldSome const xs $ \case
299+ Exactly x -> Just x
300+ _ -> Nothing
1572301
1573302instance Subtree Schema where
1574303 type SubtreeLevel Schema = 'SchemaLevel
@@ -1625,3 +354,8 @@ instance Subtree Schema where
1625354 checkSemanticCompatibility env beh schs = do
1626355 let defs = getH env
1627356 checkFormulas env beh defs $ schemaToFormula <$> defs <*> schs
357+
358+parseDiscriminatorValue :: Text -> Referenced Schema
359+parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$ref" A..= v] of
360+ A.Success x -> x
361+ A.Error _ -> Ref $ Reference v
src/OpenAPI/Checker/Validate/Schema/DNF.hsnew file+101
@@ -0,0 +1,101 @@
1+module OpenAPI.Checker.Validate.Schema.DNF
2+ ( DNF (..)
3+ , Disjunct (..)
4+ , pattern SingleDisjunct
5+ , pattern TopDNF
6+ , pattern BottomDNF
7+ , pattern LiteralDNF
8+ , foldDNF
9+ , forDNF
10+ )
11+where
12+
13+import Algebra.Lattice
14+import Control.Applicative
15+import Data.Foldable
16+import qualified Data.Set as S
17+
18+-- | A boolean formula (without "not") represented as a Disjunctive Normal Form:
19+-- the formula is a disjunction of a set of clauses, each of which is a
20+-- conjunction of a set of some elementary formulas.
21+-- Invariant: no two disjuncts imply eachother
22+newtype DNF a = DNF (S.Set (Disjunct a))
23+ deriving stock (Eq, Ord, Show)
24+
25+-- A disjunct is a thing that is to be disjuncted. Itself it is a conjunction of some literals.
26+newtype Disjunct a = Disjunct (S.Set a)
27+ deriving stock (Eq, Ord, Show)
28+
29+disjImplies :: Ord a => Disjunct a -> Disjunct a -> Bool
30+disjImplies (Disjunct xs) (Disjunct ys) = ys `S.isSubsetOf` xs
31+
32+disjConjunction :: Ord a => Disjunct a -> Disjunct a -> Disjunct a
33+disjConjunction (Disjunct xs) (Disjunct ys) = Disjunct $ xs `S.union` ys
34+
35+disjAdd :: Ord a => DNF a -> Disjunct a -> DNF a
36+disjAdd (DNF yss) xs
37+ | any (xs `disjImplies`) yss = DNF yss
38+ | otherwise = DNF $ S.insert xs $ S.filter (not . (`disjImplies` xs)) yss
39+
40+instance Ord a => Lattice (DNF a) where
41+ xss \/ DNF yss = S.foldl' disjAdd xss yss
42+ DNF xss /\ DNF yss =
43+ foldl' disjAdd bottom $
44+ liftA2 disjConjunction (S.toList xss) (S.toList yss)
45+
46+pattern BottomDNF :: DNF a
47+pattern BottomDNF <-
48+ DNF (S.null -> True)
49+ where
50+ BottomDNF = DNF S.empty
51+
52+isSingleton :: S.Set a -> Maybe a
53+isSingleton s
54+ | S.size s == 1 = S.lookupMin s
55+ | otherwise = Nothing
56+
57+pattern SingleDisjunct :: Ord a => Disjunct a -> DNF a
58+pattern SingleDisjunct xs <-
59+ DNF (isSingleton -> Just xs)
60+ where
61+ SingleDisjunct xs = DNF $ S.singleton xs
62+
63+pattern TopDNF :: DNF a
64+pattern TopDNF <-
65+ DNF (isSingleton -> Just (Disjunct (S.null -> True)))
66+ where
67+ TopDNF = DNF $ S.singleton $ Disjunct S.empty
68+
69+pattern LiteralDNF :: Ord a => a -> DNF a
70+pattern LiteralDNF x <-
71+ SingleDisjunct (Disjunct (isSingleton -> Just x))
72+ where
73+ LiteralDNF x = SingleDisjunct $ Disjunct $ S.singleton x
74+
75+instance Ord a => BoundedJoinSemiLattice (DNF a) where
76+ bottom = BottomDNF
77+
78+instance Ord a => BoundedMeetSemiLattice (DNF a) where
79+ top = TopDNF
80+
81+foldDisjunct :: BoundedMeetSemiLattice l => (a -> l) -> Disjunct a -> l
82+foldDisjunct f (Disjunct xs) = S.foldl' (\y l -> y /\ f l) top xs
83+
84+foldDNF :: BoundedLattice l => (a -> l) -> DNF a -> l
85+foldDNF f (DNF xss) = S.foldl' (\y xs -> y \/ foldDisjunct f xs) bottom xss
86+
87+newtype LiftA f a = LiftA {getLiftA :: f a}
88+ deriving newtype (Functor, Applicative)
89+
90+instance (Lattice a, Applicative f) => Lattice (LiftA f a) where
91+ (/\) = liftA2 (/\)
92+ (\/) = liftA2 (\/)
93+
94+instance (BoundedJoinSemiLattice a, Applicative f) => BoundedJoinSemiLattice (LiftA f a) where
95+ bottom = pure bottom
96+
97+instance (BoundedMeetSemiLattice a, Applicative f) => BoundedMeetSemiLattice (LiftA f a) where
98+ top = pure top
99+
100+forDNF :: (BoundedLattice l, Applicative f) => (a -> f l) -> DNF a -> f l
101+forDNF f = getLiftA . foldDNF (LiftA . f)
src/OpenAPI/Checker/Validate/Schema/Issues.hsnew file+222
@@ -0,0 +1,222 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.Schema.Issues
4+ ( Issue (..)
5+ , Behave (..)
6+ )
7+where
8+
9+import qualified Data.Aeson as A
10+import Data.OpenApi
11+import Data.Scientific
12+import Data.Text (Text)
13+import qualified Data.Text as T
14+import OpenAPI.Checker.Behavior
15+import OpenAPI.Checker.Subtree
16+import OpenAPI.Checker.Validate.Schema.JsonFormula
17+import OpenAPI.Checker.Validate.Schema.Partition
18+import OpenAPI.Checker.Validate.Schema.TypedJson
19+import Text.Pandoc.Builder hiding (Format, Null)
20+
21+instance Issuable 'TypedSchemaLevel where
22+ data Issue 'TypedSchemaLevel
23+ = -- | producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
24+ EnumDoesntSatisfy A.Value
25+ | -- | consumer only expects a specific value which the producer does not produce.
26+ NoMatchingEnum A.Value
27+ | -- | consumer declares a maximum numeric value ($1), producer doesn't
28+ NoMatchingMaximum (Bound Scientific)
29+ | -- | consumer declares a maximum numeric value ($1), producer declares a weaker (higher) limit ($2)
30+ MatchingMaximumWeak (ProdCons (Bound Scientific))
31+ | -- | consumer declares a minimum numeric value, producer doesn't
32+ NoMatchingMinimum (Bound Scientific)
33+ | -- | consumer declares a minimum numeric value ($1), producer declares a weaker (lower) limit ($2)
34+ MatchingMinimumWeak (ProdCons (Bound Scientific))
35+ | -- | consumer declares that the numeric value must be a multiple of $1, producer doesn't
36+ NoMatchingMultipleOf Scientific
37+ | -- | consumer declares that the numeric value must be a multiple of $1, producer declares a weaker condition (multiple of $2)
38+ MatchingMultipleOfWeak (ProdCons Scientific)
39+ | -- | consumer declares a string/number format, producer declares none or a different format (TODO: improve via regex #32)
40+ NoMatchingFormat Format
41+ | -- | consumer declares a maximum length of the string ($1), producer doesn't.
42+ NoMatchingMaxLength Integer
43+ | -- | consumer declares a maximum length of the string ($1), producer declares a weaker (higher) limit ($2)
44+ MatchingMaxLengthWeak (ProdCons Integer)
45+ | -- | consumer declares a minimum length of the string ($1), producer doesn't.
46+ NoMatchingMinLength Integer
47+ | -- | consumer declares a minimum length of the string ($1), producer declares a weaker (lower) limit ($2)
48+ MatchingMinLengthWeak (ProdCons Integer)
49+ | -- | consumer declares the string value must match a regex ($1), producer doesn't declare or declares different regex (TODO: #32)
50+ NoMatchingPattern Pattern
51+ | -- | consumer declares the items of an array must satisfy some condition, producer doesn't
52+ NoMatchingItems
53+ | -- | consumer declares a maximum length of the array ($1), producer doesn't.
54+ NoMatchingMaxItems Integer
55+ | -- | consumer declares a maximum length of the array ($1), producer declares a weaker (higher) limit ($2)
56+ MatchingMaxItemsWeak (ProdCons Integer)
57+ | -- | consumer declares a minimum length of the array ($1), producer doesn't.
58+ NoMatchingMinItems Integer
59+ | -- | consumer declares a minimum length of the array ($1), producer declares a weaker (lower) limit ($2)
60+ MatchingMinItemsWeak (ProdCons Integer)
61+ | -- | consumer declares that items must be unique, producer doesn't
62+ NoMatchingUniqueItems
63+ | -- | consumer declares the properties of an object must satisfy some condition, producer doesn't
64+ NoMatchingProperties
65+ | -- | producer allows additional properties, consumer doesn't
66+ NoAdditionalProperties
67+ | -- | consumer declares a maximum number of properties in the object ($1), producer doesn't.
68+ NoMatchingMaxProperties Integer
69+ | -- | consumer declares a maximum number of properties in the object ($1), producer declares a weaker (higher) limit ($2)
70+ MatchingMaxPropertiesWeak (ProdCons Integer)
71+ | -- | consumer declares a minimum number of properties in the object ($1), producer doesn't.
72+ NoMatchingMinProperties Integer
73+ | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
74+ MatchingMinPropertiesWeak (ProdCons Integer)
75+ | -- | 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)
76+ NoMatchingCondition (Maybe Partition) [SomeCondition]
77+ | -- | consumer indicates that no values of this type are allowed, but we weren't able to conclude that in the producer (currently only immediate contradictions are checked, c.f. #70)
78+ TypeBecomesEmpty
79+ | -- | consumer indicates that no values in a particular partition are allowed, but we weren't able to conclude this in the producer
80+ PartitionBecomesEmpty Partition
81+ deriving stock (Eq, Ord, Show)
82+ issueIsUnsupported _ = False
83+ describeIssue Forward (EnumDoesntSatisfy v) = para "The following enum value was removed:" <> showJSONValue v
84+ describeIssue Backward (EnumDoesntSatisfy v) = para "The following enum value was added:" <> showJSONValue v
85+ describeIssue Forward (NoMatchingEnum v) = para "The following enum value has been added:" <> showJSONValue v
86+ describeIssue Backward (NoMatchingEnum v) = para "The following enum value has been removed:" <> showJSONValue v
87+ describeIssue Forward (NoMatchingMaximum b) = para $ "Upper bound has been added:" <> showBound b <> "."
88+ describeIssue Backward (NoMatchingMaximum b) = para $ "Upper bound has been removed:" <> showBound b <> "."
89+ describeIssue _ (MatchingMaximumWeak (ProdCons p c)) = para $ "Upper bound changed from " <> showBound p <> " to " <> showBound c <> "."
90+ describeIssue Forward (NoMatchingMinimum b) = para $ "Lower bound has been added: " <> showBound b <> "."
91+ describeIssue Backward (NoMatchingMinimum b) = para $ "Lower bound has been removed: " <> showBound b <> "."
92+ describeIssue _ (MatchingMinimumWeak (ProdCons p c)) = para $ "Lower bound changed from " <> showBound p <> " to " <> showBound c <> "."
93+ describeIssue Forward (NoMatchingMultipleOf n) = para $ "Value is now a multiple of " <> show' n <> "."
94+ describeIssue Backward (NoMatchingMultipleOf n) = para $ "Value is no longer a multiple of " <> show' n <> "."
95+ describeIssue _ (MatchingMultipleOfWeak (ProdCons p c)) = para $ "Value changed from being a multiple of " <> show' p <> " to being a multiple of " <> show' c <> "."
96+ describeIssue Forward (NoMatchingFormat f) = para $ "Format added: " <> code f <> "."
97+ describeIssue Backward (NoMatchingFormat f) = para $ "Format removed: " <> code f <> "."
98+ describeIssue Forward (NoMatchingMaxLength n) = para $ "Maximum length added: " <> show' n <> "."
99+ describeIssue Backward (NoMatchingMaxLength n) = para $ "Maximum length removed: " <> show' n <> "."
100+ describeIssue _ (MatchingMaxLengthWeak (ProdCons p c)) = para $ "Maximum length of the string changed from " <> show' p <> " to " <> show' c <> "."
101+ describeIssue Forward (NoMatchingMinLength n) = para $ "Minimum length of the string added: " <> show' n <> "."
102+ describeIssue Backward (NoMatchingMinLength n) = para $ "Minimum length of the string removed: " <> show' n <> "."
103+ describeIssue _ (MatchingMinLengthWeak (ProdCons p c)) = para $ "Minimum length of the string changed from " <> show' p <> " to " <> show' c <> "."
104+ describeIssue Forward (NoMatchingPattern p) = para "Pattern (regular expression) added: " <> codeBlock p
105+ describeIssue Backward (NoMatchingPattern p) = para "Pattern (regular expression) removed: " <> codeBlock p
106+ describeIssue Forward NoMatchingItems = para "Array item schema has been added."
107+ describeIssue Backward NoMatchingItems = para "Array item schema has been removed."
108+ describeIssue Forward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been added " <> show' n <> "."
109+ describeIssue Backward (NoMatchingMaxItems n) = para $ "Maximum length of the array has been removed " <> show' n <> "."
110+ describeIssue _ (MatchingMaxItemsWeak (ProdCons p c)) = para $ "Maximum length of the array changed from " <> show' p <> " to " <> show' c <> "."
111+ describeIssue Forward (NoMatchingMinItems n) = para $ "Minimum length of the array added: " <> show' n <> "."
112+ describeIssue Backward (NoMatchingMinItems n) = para $ "Minimum length of the array removed: " <> show' n <> "."
113+ describeIssue _ (MatchingMinItemsWeak (ProdCons p c)) = para $ "Minimum length of the array changed from " <> show' p <> " to " <> show' c <> "."
114+ describeIssue Forward NoMatchingUniqueItems = para "Items are now required to be unique."
115+ describeIssue Backward NoMatchingUniqueItems = para "Items are no longer required to be unique."
116+ describeIssue Forward NoMatchingProperties = para "Property added."
117+ describeIssue Backward NoMatchingProperties = para "Property removed."
118+ describeIssue Forward NoAdditionalProperties = para "Additional properties have been removed."
119+ describeIssue Backward NoAdditionalProperties = para "Additional properties have been added."
120+ describeIssue Forward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been added: " <> show' n <> "."
121+ describeIssue Backward (NoMatchingMaxProperties n) = para $ "Maximum number of properties has been removed: " <> show' n <> "."
122+ describeIssue _ (MatchingMaxPropertiesWeak (ProdCons p c)) = para $ "Maximum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
123+ describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
124+ describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
125+ describeIssue _ (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
126+ describeIssue _ (NoMatchingCondition mPart conds) =
127+ para
128+ (case mPart of
129+ Nothing -> "Could not verify that the following conditions hold (please file a bug if you see this):"
130+ Just locPart ->
131+ "In cases where " <> showPartition locPart
132+ <> " – could not verify that the following conditions hold (please file a bug if you see this):")
133+ <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
134+ describeIssue Forward TypeBecomesEmpty = para "The type has been removed."
135+ describeIssue Backward TypeBecomesEmpty = para "The type has been added."
136+ describeIssue Forward (PartitionBecomesEmpty part) = para $ "The case where " <> showPartition part <> " – has been removed."
137+ describeIssue Backward (PartitionBecomesEmpty part) = para $ "The case where " <> showPartition part <> " – has been added."
138+
139+show' :: Show x => x -> Inlines
140+show' = str . T.pack . show
141+
142+instance Issuable 'SchemaLevel where
143+ data Issue 'SchemaLevel
144+ = -- | Some (openapi-supported) feature that we do not support was encountered in the schema
145+ NotSupported Text
146+ | -- | We couldn't prove that the branches of a oneOf are disjoint, and we will treat it as an anyOf, meaning we don't check whether the overlaps are excluded in a compatible way
147+ OneOfNotDisjoint
148+ | -- | The schema is actually invalid
149+ InvalidSchema Text
150+ | -- | The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
151+ UnguardedRecursion
152+ | -- | Producer doesn't place any restrictions on the types, but the consumer does. List what types remain available in the consumer.
153+ TypesRestricted [JsonType]
154+ | -- | 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
155+ AdditionalToProperty
156+ | -- | 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
157+ PropertyToAdditional
158+ | -- | consumer requires a property that is not required/allowed in the producer
159+ PropertyNowRequired
160+ | -- | producer allows a property that is not allowed in the consumer
161+ UnexpectedProperty
162+ deriving stock (Eq, Ord, Show)
163+ issueIsUnsupported = \case
164+ NotSupported _ -> True
165+ OneOfNotDisjoint -> True
166+ InvalidSchema _ -> True
167+ UnguardedRecursion -> True
168+ _ -> False
169+
170+ describeIssue _ (NotSupported i) =
171+ para (emph "Encountered a feature that OpenApi Diff does not support: " <> text i <> ".")
172+ describeIssue _ OneOfNotDisjoint =
173+ para $
174+ "Could not deduce that " <> code "oneOf"
175+ <> " cases don't overlap. Treating the "
176+ <> code "oneOf"
177+ <> " as an "
178+ <> code "anyOf"
179+ <> ". Reported errors might not be accurate."
180+ describeIssue _ (InvalidSchema i) =
181+ para (emph "The schema is invalid: " <> text i <> ".")
182+ describeIssue _ UnguardedRecursion =
183+ para "Encountered recursion that is too complex for OpenApi Diff to untangle."
184+ describeIssue Forward (TypesRestricted tys) = case tys of
185+ [] -> para "No longer has any valid values." -- weird
186+ _ -> para "Values are now limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
187+ describeIssue Backward (TypesRestricted tys) = case tys of
188+ [] -> para "Any value of any type is now allowed." -- weird
189+ _ -> para "Values are no longer limited to the following types: " <> bulletList (para . describeJSONType <$> tys)
190+ describeIssue Forward AdditionalToProperty = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
191+ describeIssue Backward AdditionalToProperty = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
192+ describeIssue Forward PropertyToAdditional = para "The property was previously explicitly defined. It is now implicitly described by the catch-all \"additional properties\" case."
193+ describeIssue Backward PropertyToAdditional = para "The property was previously implicitly described by the catch-all \"additional properties\" case. It is now explicitly defined."
194+ describeIssue Forward PropertyNowRequired = para "The property has become required."
195+ describeIssue Backward PropertyNowRequired = para "The property may not be present."
196+ describeIssue Forward UnexpectedProperty = para "The property has been removed."
197+ describeIssue Backward UnexpectedProperty = para "The property has been added."
198+
199+instance Behavable 'SchemaLevel 'TypedSchemaLevel where
200+ data Behave 'SchemaLevel 'TypedSchemaLevel
201+ = OfType JsonType
202+ deriving stock (Eq, Ord, Show)
203+
204+ describeBehaviour (OfType t) = describeJSONType t
205+
206+instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
207+ data Behave 'TypedSchemaLevel 'TypedSchemaLevel
208+ = InPartition Partition
209+ deriving stock (Eq, Ord, Show)
210+
211+ describeBehaviour (InPartition partition) = "In cases where " <> showPartition partition
212+
213+instance Behavable 'TypedSchemaLevel 'SchemaLevel where
214+ data Behave 'TypedSchemaLevel 'SchemaLevel
215+ = InItems
216+ | InProperty Text
217+ | InAdditionalProperty
218+ deriving stock (Eq, Ord, Show)
219+
220+ describeBehaviour InItems = "Items"
221+ describeBehaviour (InProperty p) = "Property " <> code p
222+ describeBehaviour InAdditionalProperty = "Additional properties"
src/OpenAPI/Checker/Validate/Schema/JsonFormula.hsnew file+224
@@ -0,0 +1,224 @@
1+module OpenAPI.Checker.Validate.Schema.JsonFormula
2+ ( Bound (..)
3+ , showBound
4+ , Property (..)
5+ , Condition (..)
6+ , showCondition
7+ , satisfiesTyped
8+ , checkStringFormat
9+ , checkNumberFormat
10+ , SomeCondition (..)
11+ , JsonFormula (..)
12+ , satisfiesFormula
13+ , satisfies
14+ , showJSONValue
15+ , showJSONValueInline
16+ )
17+where
18+
19+import Algebra.Lattice
20+import qualified Data.Aeson as A
21+import qualified Data.ByteString.Lazy as BSL
22+import qualified Data.Foldable as F
23+import qualified Data.HashMap.Strict as HM
24+import Data.Functor
25+import Data.Int
26+import Data.Kind
27+import qualified Data.Map as M
28+import Data.Maybe
29+import Data.OpenApi
30+import Data.Ord
31+import Data.Ratio
32+import Data.Scientific
33+import qualified Data.Set as S
34+import Data.Text (Text)
35+import qualified Data.Text as T
36+import qualified Data.Text.Encoding as T
37+import Data.Typeable
38+import OpenAPI.Checker.Orphans ()
39+import OpenAPI.Checker.Subtree
40+import OpenAPI.Checker.Validate.Schema.DNF
41+import OpenAPI.Checker.Validate.Schema.TypedJson
42+import Text.Pandoc.Builder hiding (Format, Null)
43+import Text.Regex.Pcre2
44+
45+data Bound a = Exclusive !a | Inclusive !a
46+ deriving stock (Eq, Show, Functor)
47+
48+-- | The order is lexicographical on @a * Bool@.
49+instance Ord a => Ord (Bound a) where
50+ Exclusive a `compare` Exclusive b = compare a b
51+ Exclusive a `compare` Inclusive b = if a <= b then LT else GT
52+ Inclusive a `compare` Exclusive b = if a < b then LT else GT
53+ Inclusive a `compare` Inclusive b = compare a b
54+
55+showBound :: Show a => Bound a -> Inlines
56+showBound (Inclusive x) = show' x <> " inclusive"
57+showBound (Exclusive x) = show' x <> " exclusive"
58+
59+data Property = Property
60+ { propRequired :: Bool
61+ , propFormula :: ForeachType JsonFormula
62+ , propRefSchema :: Traced (Referenced Schema)
63+ }
64+ deriving stock (Eq, Ord, Show)
65+
66+-- | A primitive structural condition for the "top level" of a JSON value (of a specific type)
67+data Condition :: JsonType -> Type where
68+ Exactly :: TypedValue t -> Condition t
69+ Maximum :: !(Bound Scientific) -> Condition 'Number
70+ Minimum
71+ :: !(Down (Bound (Down Scientific)))
72+ -> Condition 'Number -- ^ this has the right Ord
73+ MultipleOf :: !Scientific -> Condition 'Number
74+ NumberFormat :: !Format -> Condition 'Number
75+ MaxLength :: !Integer -> Condition 'String
76+ MinLength :: !Integer -> Condition 'String
77+ Pattern :: !Pattern -> Condition 'String
78+ StringFormat :: !Format -> Condition 'String
79+ Items
80+ :: !(ForeachType JsonFormula)
81+ -> !(Traced (Referenced Schema))
82+ -> Condition 'Array
83+ MaxItems :: !Integer -> Condition 'Array
84+ MinItems :: !Integer -> Condition 'Array
85+ UniqueItems :: Condition 'Array
86+ Properties
87+ :: !(M.Map Text Property)
88+ -> !(ForeachType JsonFormula) -- ^ formula for additional properties
89+ -> !(Maybe (Traced (Referenced Schema))) -- ^ schema for additional properties, Nothing means bottom
90+ -> Condition 'Object
91+ MaxProperties :: !Integer -> Condition 'Object
92+ MinProperties :: !Integer -> Condition 'Object
93+
94+deriving stock instance Eq (Condition t)
95+
96+deriving stock instance Ord (Condition t)
97+
98+deriving stock instance Show (Condition t)
99+
100+showCondition :: Condition a -> Blocks
101+showCondition = \case
102+ (Exactly v) -> para "The value should be:" <> showJSONValue (untypeValue v)
103+ (Maximum b) -> para $ "The value should be less than " <> showBound b <> "."
104+ (Minimum (Down b)) -> para $ "The value should be more than " <> showBound (getDown <$> b) <> "."
105+ (MultipleOf n) -> para $ "The value should be a multiple of " <> show' n <> "."
106+ (NumberFormat p) -> para $ "The number should have the following format:" <> code p <> "."
107+ (Pattern p) -> para "The value should satisfy the following pattern (regular expression):" <> codeBlock p
108+ (StringFormat p) -> para $ "The string should have the following format:" <> code p <> "."
109+ (MaxLength p) -> para $ "The length of the string should be less than or equal to " <> show' p <> "."
110+ (MinLength p) -> para $ "The length of the string should be more than or equal to " <> show' p <> "."
111+ (Items i _) -> para "The items of the array should satisfy:" <> showForEachJsonFormula i
112+ (MaxItems n) -> para $ "The length of the array should be less than or equal to " <> show' n <> "."
113+ (MinItems n) -> para $ "The length of the array should be more than or equal to " <> show' n <> "."
114+ UniqueItems -> para "The elements in the array should be unique."
115+ (Properties props additional _) ->
116+ bulletList $
117+ (M.toList props
118+ <&> (\(k, p) ->
119+ para (code k)
120+ <> para (strong $ if propRequired p then "Required" else "Optional")
121+ <> showForEachJsonFormula (propFormula p)))
122+ <> [ para (emph "Additional properties")
123+ <> showForEachJsonFormula additional
124+ ]
125+ (MaxProperties n) -> para $ "The maximum number of fields should be " <> show' n <> "."
126+ (MinProperties n) -> para $ "The minimum number of fields should be " <> show' n <> "."
127+ where
128+ showForEachJsonFormula :: ForeachType JsonFormula -> Blocks
129+ showForEachJsonFormula i =
130+ bulletList $
131+ foldType
132+ (\t f -> case getJsonFormula $ f i of
133+ BottomDNF -> mempty
134+ (DNF conds) ->
135+ [ para (describeJSONType t)
136+ <> bulletList
137+ (S.toList conds <&> \case
138+ Disjunct (S.toList -> []) -> para "Empty"
139+ Disjunct (S.toList -> cond) -> bulletList (showCondition <$> cond))
140+ ])
141+
142+showJSONValue :: A.Value -> Blocks
143+showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
144+
145+showJSONValueInline :: A.Value -> Inlines
146+showJSONValueInline v = code (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
147+
148+show' :: Show x => x -> Inlines
149+show' = str . T.pack . show
150+
151+satisfiesTyped :: TypedValue t -> Condition t -> Bool
152+satisfiesTyped e (Exactly e') = e == e'
153+satisfiesTyped (TNumber n) (Maximum (Exclusive m)) = n < m
154+satisfiesTyped (TNumber n) (Maximum (Inclusive m)) = n <= m
155+satisfiesTyped (TNumber n) (Minimum (Down (Exclusive (Down m)))) = n > m
156+satisfiesTyped (TNumber n) (Minimum (Down (Inclusive (Down m)))) = n >= m
157+satisfiesTyped (TNumber n) (MultipleOf m) = denominator (toRational n / toRational m) == 1 -- TODO: could be better #36
158+satisfiesTyped (TNumber n) (NumberFormat f) = checkNumberFormat f n
159+satisfiesTyped (TString s) (MaxLength m) = fromIntegral (T.length s) <= m
160+satisfiesTyped (TString s) (MinLength m) = fromIntegral (T.length s) >= m
161+satisfiesTyped (TString s) (Pattern p) = isJust $ match p s -- TODO: regex stuff #32
162+satisfiesTyped (TString s) (StringFormat f) = checkStringFormat f s
163+satisfiesTyped (TArray a) (Items f _) = all (`satisfies` f) a
164+satisfiesTyped (TArray a) (MaxItems m) = fromIntegral (F.length a) <= m
165+satisfiesTyped (TArray a) (MinItems m) = fromIntegral (F.length a) >= m
166+satisfiesTyped (TArray a) UniqueItems = S.size (S.fromList $ F.toList a) == F.length a -- TODO: could be better #36
167+satisfiesTyped (TObject o) (Properties props additional _) =
168+ all (`HM.member` o) (M.keys (M.filter propRequired props))
169+ && all (\(k, v) -> satisfies v $ maybe additional propFormula $ M.lookup k props) (HM.toList o)
170+satisfiesTyped (TObject o) (MaxProperties m) = fromIntegral (HM.size o) <= m
171+satisfiesTyped (TObject o) (MinProperties m) = fromIntegral (HM.size o) >= m
172+
173+checkNumberFormat :: Format -> Scientific -> Bool
174+checkNumberFormat "int32" (toRational -> n) =
175+ denominator n == 1
176+ && n >= toRational (minBound :: Int32)
177+ && n <= toRational (maxBound :: Int32)
178+checkNumberFormat "int64" (toRational -> n) =
179+ denominator n == 1
180+ && n >= toRational (minBound :: Int64)
181+ && n <= toRational (maxBound :: Int64)
182+checkNumberFormat "float" _n = True
183+checkNumberFormat "double" _n = True
184+checkNumberFormat f _n = error $ "Invalid number format: " <> T.unpack f
185+
186+checkStringFormat :: Format -> Text -> Bool
187+checkStringFormat "byte" _s = True -- TODO: regex stuff #32
188+checkStringFormat "binary" _s = True
189+checkStringFormat "date" _s = True
190+checkStringFormat "date-time" _s = True
191+checkStringFormat "password" _s = True
192+checkStringFormat "uuid" _s = True
193+checkStringFormat f _s = error $ "Invalid string format: " <> T.unpack f
194+
195+data SomeCondition where
196+ SomeCondition :: Typeable t => Condition t -> SomeCondition
197+
198+instance Eq SomeCondition where
199+ SomeCondition x == SomeCondition y = case cast x of
200+ Just x' -> x' == y
201+ Nothing -> False
202+
203+instance Ord SomeCondition where
204+ compare (SomeCondition x) (SomeCondition y) = case cast x of
205+ Just x' -> compare x' y
206+ Nothing -> compare (typeRep x) (typeRep y)
207+
208+deriving stock instance Show SomeCondition
209+
210+newtype JsonFormula t = JsonFormula {getJsonFormula :: DNF (Condition t)}
211+ deriving stock (Eq, Ord, Show)
212+ deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
213+
214+satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
215+satisfiesFormula val = foldDNF (satisfiesTyped val) . getJsonFormula
216+
217+satisfies :: A.Value -> ForeachType JsonFormula -> Bool
218+satisfies val p = case val of
219+ A.Null -> satisfiesFormula TNull $ forNull p
220+ A.Bool b -> satisfiesFormula (TBool b) $ forBoolean p
221+ A.Number n -> satisfiesFormula (TNumber n) $ forNumber p
222+ A.String s -> satisfiesFormula (TString s) $ forString p
223+ A.Array a -> satisfiesFormula (TArray a) $ forArray p
224+ A.Object o -> satisfiesFormula (TObject o) $ forObject p
src/OpenAPI/Checker/Validate/Schema/Partition.hsnew file+316
@@ -0,0 +1,316 @@
1+module OpenAPI.Checker.Validate.Schema.Partition
2+ ( partitionSchema
3+ , partitionRefSchema
4+ , selectPartition
5+ , runPartitionM
6+ , tryPartition
7+ , showPartition
8+ , intersectSchema
9+ , intersectRefSchema
10+ , IntersectionResult (..)
11+ , runIntersectionM
12+ , Partition
13+ )
14+where
15+
16+import Algebra.Lattice
17+import Algebra.Lattice.Lifted
18+import Control.Applicative
19+import Control.Monad.Reader hiding (ask)
20+import qualified Control.Monad.Reader as R
21+import Control.Monad.State
22+import qualified Control.Monad.Trans.Reader as R (liftCatch)
23+import qualified Control.Monad.Trans.Writer as W (liftCatch)
24+import Control.Monad.Writer
25+import qualified Data.Aeson as A
26+import Data.Foldable
27+import Data.Functor.Identity
28+import qualified Data.HashMap.Strict.InsOrd as IOHM
29+import Data.List (sortBy)
30+import qualified Data.List as L
31+import qualified Data.List.NonEmpty as NE
32+import qualified Data.Map as M
33+import Data.Maybe
34+import Data.OpenApi
35+import Data.Ord
36+import qualified Data.Set as S
37+import Data.Text (Text)
38+import qualified Data.Text as T
39+import OpenAPI.Checker.Memo
40+import OpenAPI.Checker.References
41+import OpenAPI.Checker.Subtree
42+import OpenAPI.Checker.Validate.Schema.DNF
43+import OpenAPI.Checker.Validate.Schema.JsonFormula
44+import OpenAPI.Checker.Validate.Schema.Traced
45+import OpenAPI.Checker.Validate.Schema.TypedJson
46+import Text.Pandoc.Builder hiding (Format, Null)
47+
48+data PartitionData
49+ = DByEnumValue (DNF (S.Set A.Value))
50+ | DByProperties (DNF (S.Set Text, S.Set Text)) -- optional, required
51+ deriving stock (Eq, Ord, Show)
52+
53+conjPart :: PartitionData -> PartitionData -> Maybe PartitionData
54+conjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss /\ yss
55+conjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss /\ yss
56+conjPart _ _ = Nothing
57+
58+disjPart :: PartitionData -> PartitionData -> Maybe PartitionData
59+disjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss \/ yss
60+disjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss \/ yss
61+disjPart _ _ = Nothing
62+
63+newtype Partitions = Partitions (M.Map PartitionLocation (S.Set PartitionData))
64+ deriving stock (Eq, Ord, Show)
65+
66+instance Lattice Partitions where
67+ Partitions xss /\ Partitions yss = Partitions $ M.unionWith conj xss yss
68+ where
69+ conj xs ys = S.fromList . catMaybes $ liftA2 conjPart (S.toList xs) (S.toList ys)
70+ Partitions xss \/ Partitions yss = Partitions $ M.intersectionWith disj xss yss
71+ where
72+ disj xs ys = S.fromList . catMaybes $ liftA2 disjPart (S.toList xs) (S.toList ys)
73+
74+instance BoundedMeetSemiLattice Partitions where
75+ top = Partitions M.empty
76+
77+-- The lattice has no bottom, but we use 'Lifted' to adjoin a free bottom element
78+
79+type PartitionM = ReaderT (Traced (Definitions Schema)) (State (MemoState ()))
80+
81+ignoreKnot :: KnotTier (Lifted Partitions) () PartitionM
82+ignoreKnot =
83+ KnotTier
84+ { onKnotFound = pure ()
85+ , onKnotUsed = \_ -> pure bottom
86+ , tieKnot = \_ -> pure
87+ }
88+
89+singletonPart :: PartitionData -> Lifted Partitions
90+singletonPart = Lift . Partitions . M.singleton PHere . S.singleton
91+
92+partitionSchema :: Traced Schema -> PartitionM (Lifted Partitions)
93+partitionSchema sch = do
94+ allClauses <- case tracedAllOf sch of
95+ Nothing -> pure []
96+ Just xs -> mapM partitionRefSchema xs
97+
98+ anyClause <- case tracedAnyOf sch of
99+ Nothing -> pure top
100+ Just xs -> joins <$> mapM partitionRefSchema xs
101+
102+ oneClause <- case tracedOneOf sch of
103+ Nothing -> pure top
104+ Just xs -> joins <$> mapM partitionRefSchema xs
105+
106+ byEnumClause <- case _schemaEnum $ extract sch of
107+ Nothing -> pure top
108+ Just xs ->
109+ pure . singletonPart $
110+ DByEnumValue $ LiteralDNF (S.fromList xs)
111+
112+ -- We can only partition by presence of a property if additional properties
113+ -- are disallowed, and the property is not optional
114+ let reqd = S.fromList $ _schemaRequired $ extract sch
115+ byPropertiesClause <- case _schemaAdditionalProperties $ extract sch of
116+ Just (AdditionalPropertiesAllowed False) -> do
117+ let props = S.fromList . IOHM.keys . _schemaProperties $ extract sch
118+ pure . singletonPart $
119+ DByProperties $ LiteralDNF (props S.\\ reqd, props `S.intersection` reqd)
120+ _ -> pure top
121+
122+ -- We can partition on something nested in a property only if the property is
123+ -- required
124+ let reqdProps = IOHM.filterWithKey (\k _ -> k `S.member` reqd) $ tracedProperties sch
125+ inPropertiesClauses <- forM (IOHM.toList reqdProps) $ \(k, rs) -> do
126+ f <- partitionRefSchema rs
127+ pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
128+
129+ pure $ meets $ allClauses <> [anyClause, oneClause, byEnumClause, byPropertiesClause] <> inPropertiesClauses
130+
131+partitionRefSchema :: Traced (Referenced Schema) -> PartitionM (Lifted Partitions)
132+partitionRefSchema x = do
133+ defs <- R.ask
134+ memoWithKnot ignoreKnot (partitionSchema $ dereference defs x) (ask x)
135+
136+partitionCondition :: Condition t -> PartitionM (Lifted Partitions)
137+partitionCondition = \case
138+ Exactly x ->
139+ pure . singletonPart $
140+ DByEnumValue $ LiteralDNF (S.singleton $ untypeValue x)
141+ Properties props _ madd -> do
142+ let byProps = case madd of
143+ Just _ -> top
144+ Nothing ->
145+ singletonPart $
146+ DByProperties $ LiteralDNF
147+ ( M.keysSet $ M.filter (not . propRequired) props
148+ , M.keysSet $ M.filter propRequired props
149+ )
150+ inProps <- forM (M.toList $ M.filter propRequired props) $ \(k, prop) -> do
151+ f <- partitionRefSchema $ propRefSchema prop
152+ pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
153+ pure $ byProps /\ meets inProps
154+ _ -> pure top
155+
156+runPartitionM :: Traced (Definitions Schema) -> PartitionM a -> a
157+runPartitionM defs = runIdentity . runMemo () . (`runReaderT` defs)
158+
159+partitionJsonFormulas
160+ :: ProdCons (Traced (Definitions Schema))
161+ -> ProdCons (JsonFormula t)
162+ -> Lifted Partitions
163+partitionJsonFormulas defs pc = producer pcPart \/ consumer pcPart
164+ where
165+ pcPart = partitionFormula <$> defs <*> pc
166+ partitionFormula def (JsonFormula xss) = runPartitionM def $ forDNF partitionCondition xss
167+
168+selectPartition :: Lifted Partitions -> Maybe (PartitionLocation, S.Set PartitionChoice)
169+selectPartition Bottom = Nothing
170+selectPartition (Lift (Partitions m)) =
171+ go [(loc, part) | (loc, parts) <- sortBy (comparing $ locLength . fst) $ M.toList m, part <- S.toList parts]
172+ where
173+ locLength :: PartitionLocation -> Int
174+ locLength = walk 0
175+ where
176+ walk !n PHere = n
177+ walk !n (PInProperty _ l) = walk (n + 1) l
178+ go [] = Nothing
179+ -- Skip partitioning by property for now
180+ go ((_, DByProperties _) : ps) = go ps
181+ -- Don't partition by enum value at the root (this reports removed enum values as contradictions in their respective partitions)
182+ go ((PHere, DByEnumValue _) : ps) = go ps
183+ go ((loc, DByEnumValue (DNF xss)) : ps)
184+ -- Check that no disjunction branches are unresticted
185+ | Just enums <- traverse (\(Disjunct xs) -> fmap (foldr1 S.intersection) . NE.nonEmpty . S.toList $ xs) . S.toList $ xss =
186+ -- TODO: improve
187+ Just (loc, S.map (CByEnumValue . S.singleton) $ S.unions enums)
188+ | otherwise = go ps
189+
190+-- This essentially has 3 cases:
191+-- Nothing -- we have produced a bottom schema
192+-- Just (False, _) -- there's been no change to the schema
193+-- Just (True, x) -- x is a new schema
194+type IntersectionM = ReaderT (Traced (Definitions Schema)) (WriterT Any Maybe)
195+
196+mBottom :: IntersectionM a
197+mBottom = lift . lift $ Nothing
198+
199+catchBottom :: IntersectionM a -> IntersectionM a -> IntersectionM a
200+catchBottom act handler = R.liftCatch (W.liftCatch (\a h -> a <|> h ())) act (\_ -> handler)
201+
202+mChange :: IntersectionM ()
203+mChange = tell $ Any True
204+
205+data IntersectionResult a = Disjoint | Same a | New a
206+ deriving stock (Eq, Ord, Show)
207+
208+runIntersectionM :: Traced (Definitions Schema) -> IntersectionM a -> IntersectionResult a
209+runIntersectionM defs act = case runWriterT $ runReaderT act defs of
210+ Nothing -> Disjoint
211+ Just (x, Any False) -> Same x
212+ Just (x, Any True) -> New x
213+
214+intersectSchema
215+ :: PartitionLocation
216+ -> PartitionChoice
217+ -> Traced Schema
218+ -> IntersectionM Schema
219+intersectSchema loc part sch = do
220+ allOf' <- forM (tracedAllOf sch) $ \rss ->
221+ -- Assuming i ranges over a nonempty set (checked in processSchema)
222+ -- (⋂_i A[i]) ∩ X = ⋂_i (A[i] ∩ X)
223+ -- If any intersections are empty, the result is empty. If any intersections are a change, the result is a change.
224+ traverse (intersectRefSchema loc part) rss
225+ anyOf' <- forM (tracedAnyOf sch) $ \rss -> do
226+ -- (⋃_i A[i]) ∩ X = ⋃_i (A[i] ∩ X)
227+ -- Collect only the nonempty A[i] ∩ X, unless there are none, in which case the result is empty.
228+ -- If any schema is empty, we remove it from the list which constitutes a change.
229+ mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
230+ case catMaybes mSchemas of
231+ [] -> mBottom
232+ schs -> pure schs
233+ oneOf' <- forM (tracedOneOf sch) $ \rss -> do
234+ -- Same as anyOf'. By intersecting we're only making them more disjoint if anything.
235+ mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
236+ case catMaybes mSchemas of
237+ [] -> mBottom
238+ schs -> pure schs
239+ let sch' = (extract sch) {_schemaAllOf = allOf', _schemaAnyOf = anyOf', _schemaOneOf = oneOf'}
240+ -- Now the local changes:
241+ case loc of
242+ PInProperty k loc' -> case IOHM.lookup k $ tracedProperties sch of
243+ Nothing -> error $ "Partitioning via absent property: " <> T.unpack k
244+ Just prop -> do
245+ prop' <- intersectRefSchema loc' part prop
246+ pure $ sch' {_schemaProperties = IOHM.adjust (const prop') k $ _schemaProperties sch'}
247+ PHere -> case part of
248+ CByEnumValue vals -> do
249+ enum' <- case _schemaEnum sch' of
250+ Nothing -> do
251+ mChange
252+ pure $ S.toList vals
253+ Just xs -> do
254+ when (any (`S.notMember` vals) xs) mChange
255+ case filter (`S.member` vals) xs of
256+ [] -> mBottom
257+ xs' -> pure xs'
258+ pure $ sch' {_schemaEnum = Just enum'}
259+ CByProperties {} -> error "CByProperties not implemented"
260+
261+intersectRefSchema
262+ :: PartitionLocation
263+ -> PartitionChoice
264+ -> Traced (Referenced Schema)
265+ -> IntersectionM (Referenced Schema)
266+intersectRefSchema loc part rs = do
267+ defs <- R.ask
268+ Inline <$> intersectSchema loc part (dereference defs rs)
269+
270+intersectCondition :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> Condition t -> DNF (Condition t)
271+intersectCondition _defs PHere (CByEnumValue values) cond@(Exactly x) =
272+ if untypeValue x `S.member` values then LiteralDNF cond else bottom
273+intersectCondition defs (PInProperty k loc) part cond@(Properties props add madd) = case M.lookup k props of
274+ Nothing -> LiteralDNF cond -- shouldn't happen
275+ Just prop -> case runIntersectionM defs $ intersectRefSchema loc part $ propRefSchema prop of
276+ New rs' ->
277+ let trs' = traced (ask (propRefSchema prop) >>> step (Partitioned (loc, part))) rs'
278+ in LiteralDNF $ Properties (M.insert k prop {propRefSchema = trs'} props) add madd
279+ Same _ -> LiteralDNF cond
280+ Disjoint -> bottom
281+intersectCondition _defs _loc _part cond = LiteralDNF cond
282+
283+intersectFormula :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> JsonFormula t -> JsonFormula t
284+intersectFormula defs loc part = JsonFormula . foldDNF (intersectCondition defs loc part) . getJsonFormula
285+
286+tryPartition :: ProdCons (Traced (Definitions Schema)) -> ProdCons (JsonFormula t) -> [(Maybe Partition, ProdCons (JsonFormula t))]
287+tryPartition defs pc = case selectPartition $ partitionJsonFormulas defs pc of
288+ Nothing -> [(Nothing, pc)]
289+ Just (loc, parts) -> [(Just (loc, part), intersectFormula <$> defs <*> pure loc <*> pure part <*> pc) | part <- S.toList parts]
290+
291+showPartition :: Partition -> Inlines
292+showPartition = \case
293+ (partition, CByEnumValue (S.toList -> [v])) ->
294+ renderPartitionLocation partition <> " is " <> showJSONValueInline v
295+ (partition, CByEnumValue (S.toList -> vs)) ->
296+ renderPartitionLocation partition <> " has values: "
297+ <> (fold . L.intersperse ", " . fmap showJSONValueInline $ vs)
298+ (partition, CByProperties (S.toList -> incl) (S.toList -> [])) ->
299+ renderPartitionLocation partition <> " contains the properties: " <> listCodes incl
300+ (partition, CByProperties (S.toList -> []) (S.toList -> excl)) ->
301+ renderPartitionLocation partition <> " does not contain the properties: " <> listCodes excl
302+ (partition, CByProperties (S.toList -> incl) (S.toList -> excl)) ->
303+ renderPartitionLocation partition
304+ <> " contains the properties "
305+ <> listCodes incl
306+ <> " and does not contain the properties "
307+ <> listCodes excl
308+ where
309+ listCodes :: [Text] -> Inlines
310+ listCodes = fold . L.intersperse ", " . fmap code
311+ renderPartitionLocation :: PartitionLocation -> Inlines
312+ renderPartitionLocation p = code $ "$" <> renderPartitionLocation' p
313+ where
314+ renderPartitionLocation' :: PartitionLocation -> Text
315+ renderPartitionLocation' PHere = mempty
316+ renderPartitionLocation' (PInProperty prop rest) = "." <> prop <> renderPartitionLocation' rest
src/OpenAPI/Checker/Validate/Schema/Process.hsnew file+372
@@ -0,0 +1,372 @@
1+module OpenAPI.Checker.Validate.Schema.Process
2+ ( schemaToFormula
3+ )
4+where
5+
6+import Algebra.Lattice
7+import qualified Control.Monad.Reader as R
8+import Control.Monad.Reader hiding (ask)
9+import Control.Monad.State
10+import Control.Monad.Writer
11+import qualified Data.Aeson as A
12+import Data.Functor.Identity
13+import qualified Data.HashMap.Strict.InsOrd as IOHM
14+import qualified Data.Map as M
15+import Data.Maybe
16+import Data.OpenApi hiding (get)
17+import Data.Ord
18+import qualified Data.Set as S
19+import OpenAPI.Checker.Behavior
20+import OpenAPI.Checker.Memo
21+import OpenAPI.Checker.Paths
22+import qualified OpenAPI.Checker.PathsPrefixTree as P
23+import OpenAPI.Checker.References
24+import OpenAPI.Checker.Subtree
25+import OpenAPI.Checker.Validate.Schema.DNF
26+import OpenAPI.Checker.Validate.Schema.Issues
27+import OpenAPI.Checker.Validate.Schema.JsonFormula
28+import OpenAPI.Checker.Validate.Schema.Partition
29+import OpenAPI.Checker.Validate.Schema.Traced
30+import OpenAPI.Checker.Validate.Schema.TypedJson
31+
32+-- | A fake writer monad that doesn't actually record anything and allows lazy recursion.
33+newtype Silent w a = Silent {runSilent :: a}
34+ deriving stock (Functor)
35+ deriving (Applicative, Monad) via Identity
36+
37+instance Monoid w => MonadWriter w (Silent w) where
38+ tell _ = Silent ()
39+ listen (Silent x) = Silent (x, mempty)
40+ pass (Silent (x, _)) = Silent x
41+
42+instance MonadState (MemoState ()) (Silent w) where
43+ get = Silent $ runIdentity $ runMemo () get
44+ put _ = pure ()
45+
46+type ProcessM = StateT (MemoState ()) (ReaderT (Traced (Definitions Schema)) (Writer (P.PathsPrefixTree Behave AnIssue 'SchemaLevel)))
47+
48+type SilentM = ReaderT (Traced (Definitions Schema)) (Silent (P.PathsPrefixTree Behave AnIssue 'SchemaLevel))
49+
50+-- Either SilentM or ProcessM
51+type MonadProcess m =
52+ ( MonadReader (Traced (Definitions Schema)) m
53+ , MonadWriter (P.PathsPrefixTree Behave AnIssue 'SchemaLevel) m
54+ , MonadState (MemoState ()) m
55+ )
56+
57+warn :: MonadProcess m => Issue 'SchemaLevel -> m ()
58+warn issue = tell $ P.singleton $ AnItem Root $ anIssue issue
59+
60+-- | Ignore warnings but allow a recursive loop that lazily computes a recursive 'Condition'.
61+silently :: MonadProcess m => SilentM a -> m a
62+silently m = do
63+ defs <- R.ask
64+ pure . runSilent $ runReaderT m defs
65+
66+warnKnot :: MonadProcess m => KnotTier (ForeachType JsonFormula) () m
67+warnKnot =
68+ KnotTier
69+ { onKnotFound = warn UnguardedRecursion
70+ , onKnotUsed = \_ -> pure bottom
71+ , tieKnot = \_ -> pure
72+ }
73+
74+processRefSchema
75+ :: MonadProcess m
76+ => Traced (Referenced Schema)
77+ -> m (ForeachType JsonFormula)
78+processRefSchema x = do
79+ defs <- R.ask
80+ memoWithKnot warnKnot (processSchema $ dereference defs x) (ask x)
81+
82+-- | Turn a schema into a tuple of 'JsonFormula's that describes the condition
83+-- for every possible type of a JSON value. The conditions are independent, and
84+-- are thus checked independently.
85+processSchema
86+ :: MonadProcess m
87+ => Traced Schema
88+ -> m (ForeachType JsonFormula)
89+processSchema sch@(extract -> Schema {..}) = do
90+ let singletonFormula :: Condition t -> JsonFormula t
91+ singletonFormula = JsonFormula . LiteralDNF
92+
93+ allClauses <- case tracedAllOf sch of
94+ Nothing -> pure []
95+ Just [] -> [] <$ warn (InvalidSchema "no items in allOf")
96+ Just xs -> mapM processRefSchema xs
97+
98+ anyClause <- case tracedAnyOf sch of
99+ Nothing -> pure top
100+ Just [] -> bottom <$ warn (InvalidSchema "no items in anyOf")
101+ Just xs -> joins <$> mapM processRefSchema xs
102+
103+ oneClause <- case tracedOneOf sch of
104+ Nothing -> pure top
105+ Just [] -> bottom <$ warn (InvalidSchema "no items in oneOf")
106+ Just xs -> do
107+ checkOneOfDisjoint xs >>= \case
108+ True -> pure ()
109+ False -> warn OneOfNotDisjoint
110+ joins <$> mapM processRefSchema xs
111+
112+ case _schemaNot of
113+ Nothing -> pure ()
114+ Just _ -> warn (NotSupported "not clause is unsupported")
115+
116+ let typeClause = case _schemaType of
117+ Nothing -> top
118+ Just OpenApiNull ->
119+ bottom
120+ { forNull = top
121+ }
122+ Just OpenApiBoolean ->
123+ bottom
124+ { forBoolean = top
125+ }
126+ Just OpenApiNumber ->
127+ bottom
128+ { forNumber = top
129+ }
130+ Just OpenApiInteger ->
131+ bottom
132+ { forNumber = singletonFormula $ MultipleOf 1
133+ }
134+ Just OpenApiString ->
135+ bottom
136+ { forString = top
137+ }
138+ Just OpenApiArray ->
139+ bottom
140+ { forArray = top
141+ }
142+ Just OpenApiObject ->
143+ bottom
144+ { forObject = top
145+ }
146+
147+ let valueEnum A.Null =
148+ bottom
149+ { forNull = singletonFormula $ Exactly TNull
150+ }
151+ valueEnum (A.Bool b) =
152+ bottom
153+ { forBoolean = singletonFormula $ Exactly $ TBool b
154+ }
155+ valueEnum (A.Number n) =
156+ bottom
157+ { forNumber = singletonFormula $ Exactly $ TNumber n
158+ }
159+ valueEnum (A.String s) =
160+ bottom
161+ { forString = singletonFormula $ Exactly $ TString s
162+ }
163+ valueEnum (A.Array a) =
164+ bottom
165+ { forArray = singletonFormula $ Exactly $ TArray a
166+ }
167+ valueEnum (A.Object o) =
168+ bottom
169+ { forObject = singletonFormula $ Exactly $ TObject o
170+ }
171+ enumClause <- case _schemaEnum of
172+ Nothing -> pure top
173+ Just [] -> bottom <$ warn (InvalidSchema "no items in enum")
174+ Just xs -> pure $ joins (valueEnum <$> xs)
175+
176+ let maximumClause = case _schemaMaximum of
177+ Nothing -> top
178+ Just n ->
179+ top
180+ { forNumber = singletonFormula $
181+ Maximum $
182+ case _schemaExclusiveMaximum of
183+ Just True -> Exclusive n
184+ _ -> Inclusive n
185+ }
186+
187+ minimumClause = case _schemaMinimum of
188+ Nothing -> top
189+ Just n ->
190+ top
191+ { forNumber = singletonFormula $
192+ Minimum $
193+ Down $
194+ case _schemaExclusiveMinimum of
195+ Just True -> Exclusive $ Down n
196+ _ -> Inclusive $ Down n
197+ }
198+
199+ multipleOfClause = case _schemaMultipleOf of
200+ Nothing -> top
201+ Just n ->
202+ top
203+ { forNumber = singletonFormula $ MultipleOf n
204+ }
205+
206+ formatClause <- case _schemaFormat of
207+ Nothing -> pure top
208+ Just f
209+ | f `elem` ["int32", "int64", "float", "double"] ->
210+ pure
211+ top
212+ { forNumber = singletonFormula $ NumberFormat f
213+ }
214+ Just f
215+ | f `elem` ["byte", "binary", "date", "date-time", "password", "uuid"] ->
216+ pure
217+ top
218+ { forString = singletonFormula $ StringFormat f
219+ }
220+ Just f -> top <$ warn (NotSupported $ "Unknown format: " <> f)
221+
222+ let maxLengthClause = case _schemaMaxLength of
223+ Nothing -> top
224+ Just n ->
225+ top
226+ { forString = singletonFormula $ MaxLength n
227+ }
228+
229+ minLengthClause = case _schemaMinLength of
230+ Nothing -> top
231+ Just n ->
232+ top
233+ { forString = singletonFormula $ MinLength n
234+ }
235+
236+ patternClause = case _schemaPattern of
237+ Nothing -> top
238+ Just p ->
239+ top
240+ { forString = singletonFormula $ Pattern p
241+ }
242+
243+ itemsClause <- case tracedItems sch of
244+ Nothing -> pure top
245+ Just (Left rs) -> do
246+ f <- silently $ processRefSchema rs
247+ pure top {forArray = singletonFormula $ Items f rs}
248+ Just (Right _) -> top <$ warn (NotSupported "array in items is not supported")
249+
250+ let maxItemsClause = case _schemaMaxItems of
251+ Nothing -> top
252+ Just n ->
253+ top
254+ { forArray = singletonFormula $ MaxItems n
255+ }
256+
257+ minItemsClause = case _schemaMinItems of
258+ Nothing -> top
259+ Just n ->
260+ top
261+ { forArray = singletonFormula $ MinItems n
262+ }
263+
264+ uniqueItemsClause = case _schemaUniqueItems of
265+ Just True ->
266+ top
267+ { forArray = singletonFormula UniqueItems
268+ }
269+ _ -> top
270+
271+ (addProps, addPropSchema) <- case tracedAdditionalProperties sch of
272+ Just (Right rs) -> (,Just rs) <$> silently (processRefSchema rs)
273+ Just (Left False) -> pure (bottom, Nothing)
274+ _ -> pure (top, Just $ traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty)
275+ propList <- forM (S.toList . S.fromList $ IOHM.keys _schemaProperties <> _schemaRequired) $ \k -> do
276+ (f, psch) <- case IOHM.lookup k $ tracedProperties sch of
277+ Just rs -> (,rs) <$> silently (processRefSchema rs)
278+ Nothing ->
279+ let fakeSchema = traced (ask sch `Snoc` AdditionalPropertiesStep) $ Inline mempty
280+ in -- The mempty here is incorrect, but if addPropSchema was Nothing, then
281+ -- addProps is bottom, and k is in _schemaRequired. We handle this situation
282+ -- below and short-circuit the entire Properties condition to bottom
283+ pure (addProps, fromMaybe fakeSchema addPropSchema)
284+ pure (k, Property (k `elem` _schemaRequired) f psch)
285+ let allBottom f = getAll $
286+ foldType $ \_ ty -> case getJsonFormula $ ty f of
287+ BottomDNF -> All True
288+ _ -> All False
289+ allTop f = getAll $
290+ foldType $ \_ ty -> case getJsonFormula $ ty f of
291+ TopDNF -> All True
292+ _ -> All False
293+ -- remove optional fields whose schemata match that of additional props
294+ propMap = M.filter (\p -> propRequired p || propFormula p /= addProps) $ M.fromList propList
295+ propertiesClause
296+ | any (\p -> propRequired p && allBottom (propFormula p)) propMap =
297+ bottom -- if any required field has unsatisfiable schema
298+ | M.null propMap
299+ , allTop addProps =
300+ top -- if all fields are optional and have trivial schemata
301+ | otherwise =
302+ top
303+ { forObject = singletonFormula $ Properties propMap addProps addPropSchema
304+ }
305+
306+ maxPropertiesClause = case _schemaMaxProperties of
307+ Nothing -> top
308+ Just n ->
309+ top
310+ { forObject = singletonFormula $ MaxProperties n
311+ }
312+
313+ minPropertiesClause = case _schemaMinProperties of
314+ Nothing -> top
315+ Just n ->
316+ top
317+ { forObject = singletonFormula $ MinProperties n
318+ }
319+
320+ nullableClause
321+ | Just True <- _schemaNullable =
322+ bottom
323+ { forNull = singletonFormula $ Exactly TNull
324+ }
325+ | otherwise = bottom
326+
327+ pure $
328+ nullableClause
329+ \/ meets
330+ (allClauses
331+ <> [ anyClause
332+ , oneClause
333+ , typeClause
334+ , enumClause
335+ , maximumClause
336+ , minimumClause
337+ , multipleOfClause
338+ , formatClause
339+ , maxLengthClause
340+ , minLengthClause
341+ , patternClause
342+ , itemsClause
343+ , maxItemsClause
344+ , minItemsClause
345+ , uniqueItemsClause
346+ , propertiesClause
347+ , maxPropertiesClause
348+ , minPropertiesClause
349+ ])
350+
351+{- TODO: ReadOnly/WriteOnly #68 -}
352+
353+checkOneOfDisjoint :: MonadProcess m => [Traced (Referenced Schema)] -> m Bool
354+checkOneOfDisjoint schs = do
355+ defs <- R.ask
356+ pure $ case selectPartition $ joins $ runPartitionM defs $ traverse partitionRefSchema schs of
357+ Nothing -> False
358+ Just (loc, parts) ->
359+ let intersects part sch = case runIntersectionM defs $ intersectRefSchema loc part sch of
360+ Disjoint -> False
361+ _ -> True
362+ in all (\part -> 1 >= length (filter (intersects part) schs)) parts
363+ where
364+
365+runProcessM :: Traced (Definitions Schema) -> ProcessM a -> (a, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
366+runProcessM defs = runWriter . (`runReaderT` defs) . runMemo ()
367+
368+schemaToFormula
369+ :: Traced (Definitions Schema)
370+ -> Traced Schema
371+ -> (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
372+schemaToFormula defs rs = runProcessM defs $ processSchema rs
src/OpenAPI/Checker/Validate/Schema/Traced.hsnew file+114
@@ -0,0 +1,114 @@
1+{-# OPTIONS_GHC -Wno-orphans #-}
2+
3+module OpenAPI.Checker.Validate.Schema.Traced
4+ ( Step (..)
5+ , tracedAllOf
6+ , tracedAnyOf
7+ , tracedOneOf
8+ , tracedItems
9+ , tracedAdditionalProperties
10+ , tracedDiscriminator
11+ , tracedProperties
12+ , tracedConjunct
13+ , PartitionLocation (..)
14+ , PartitionChoice (..)
15+ , Partition
16+ )
17+where
18+
19+import qualified Data.Aeson as A
20+import Data.Functor
21+import qualified Data.HashMap.Strict.InsOrd as IOHM
22+import qualified Data.List.NonEmpty as NE
23+import Data.OpenApi
24+import qualified Data.Set as S
25+import Data.Text (Text)
26+import OpenAPI.Checker.Subtree
27+
28+data PartitionChoice
29+ = CByEnumValue (S.Set A.Value)
30+ | CByProperties (S.Set Text) (S.Set Text) -- included, excluded
31+ deriving stock (Eq, Ord, Show)
32+
33+data PartitionLocation
34+ = PHere
35+ | PInProperty Text PartitionLocation
36+ deriving stock (Eq, Ord, Show)
37+
38+type Partition = (PartitionLocation, PartitionChoice)
39+
40+instance Steppable Schema (Referenced Schema) where
41+ data Step Schema (Referenced Schema)
42+ = AllOfStep Int
43+ | OneOfStep Int
44+ | AnyOfStep Int
45+ | ItemsObjectStep
46+ | ItemsArrayStep Int
47+ | AdditionalPropertiesStep
48+ | NotStep
49+ deriving stock (Eq, Ord, Show)
50+
51+instance Steppable (Referenced Schema) (Referenced Schema) where
52+ data Step (Referenced Schema) (Referenced Schema)
53+ = -- | Invariant (for better memoization only): the "tail" of the trace is
54+ -- the "least" of the traces of the conjuncted schemata
55+ ConjunctedWith (NE.NonEmpty (Trace (Referenced Schema)))
56+ | Partitioned Partition
57+ deriving stock (Eq, Ord, Show)
58+
59+instance Steppable Schema (Definitions (Referenced Schema)) where
60+ data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
61+ deriving stock (Eq, Ord, Show)
62+
63+instance Steppable Schema Discriminator where
64+ data Step Schema Discriminator = DiscriminatorStep
65+ deriving stock (Eq, Ord, Show)
66+
67+instance Steppable Discriminator (Definitions (Referenced Schema)) where
68+ data Step Discriminator (Definitions (Referenced Schema)) = DiscriminatorMapping
69+ deriving stock (Eq, Ord, Show)
70+
71+tracedAllOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
72+tracedAllOf sch =
73+ _schemaAllOf (extract sch) <&> \xs ->
74+ [traced (ask sch >>> step (AllOfStep i)) x | (i, x) <- zip [0 ..] xs]
75+
76+tracedAnyOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
77+tracedAnyOf sch =
78+ _schemaAnyOf (extract sch) <&> \xs ->
79+ [traced (ask sch >>> step (AnyOfStep i)) x | (i, x) <- zip [0 ..] xs]
80+
81+tracedOneOf :: Traced Schema -> Maybe [Traced (Referenced Schema)]
82+tracedOneOf sch =
83+ _schemaOneOf (extract sch) <&> \xs ->
84+ [traced (ask sch >>> step (OneOfStep i)) x | (i, x) <- zip [0 ..] xs]
85+
86+tracedItems :: Traced Schema -> Maybe (Either (Traced (Referenced Schema)) [Traced (Referenced Schema)])
87+tracedItems sch =
88+ _schemaItems (extract sch) <&> \case
89+ OpenApiItemsObject x -> Left $ traced (ask sch >>> step ItemsObjectStep) x
90+ OpenApiItemsArray xs ->
91+ Right
92+ [traced (ask sch >>> step (ItemsArrayStep i)) x | (i, x) <- zip [0 ..] xs]
93+
94+tracedAdditionalProperties :: Traced Schema -> Maybe (Either Bool (Traced (Referenced Schema)))
95+tracedAdditionalProperties sch =
96+ _schemaAdditionalProperties (extract sch) <&> \case
97+ AdditionalPropertiesAllowed b -> Left b
98+ AdditionalPropertiesSchema x -> Right $ traced (ask sch >>> step AdditionalPropertiesStep) x
99+
100+tracedDiscriminator :: Traced Schema -> Maybe (Traced Discriminator)
101+tracedDiscriminator = sequence . stepTraced DiscriminatorStep . fmap _schemaDiscriminator
102+
103+tracedProperties :: Traced Schema -> IOHM.InsOrdHashMap Text (Traced (Referenced Schema))
104+tracedProperties sch =
105+ IOHM.mapWithKey
106+ (\k -> traced (ask sch >>> step PropertiesStep >>> step (InsOrdHashMapKeyStep k)))
107+ (_schemaProperties $ extract sch)
108+
109+tracedConjunct :: NE.NonEmpty (Traced (Referenced Schema)) -> Traced (Referenced Schema)
110+tracedConjunct refSchemas = case NE.sortWith ask refSchemas of
111+ (rs NE.:| []) -> rs
112+ (rs1 NE.:| rs2 : rss) ->
113+ traced (ask rs1 >>> step (ConjunctedWith $ ask <$> rs2 NE.:| rss)) $
114+ Inline mempty {_schemaAllOf = Just $ extract <$> rs1 : rs2 : rss}
src/OpenAPI/Checker/Validate/Schema/TypedJson.hsnew file+118
@@ -0,0 +1,118 @@
1+module OpenAPI.Checker.Validate.Schema.TypedJson
2+ ( JsonType (..)
3+ , describeJSONType
4+ , TypedValue (..)
5+ , untypeValue
6+ , ForeachType (..)
7+ , foldType
8+ , forType_
9+ )
10+where
11+
12+import Algebra.Lattice
13+import qualified Data.Aeson as A
14+import Data.Kind
15+import Data.Monoid
16+import Data.Scientific
17+import Data.String
18+import Data.Text (Text)
19+import Data.Typeable
20+
21+-- | Type of a JSON value
22+data JsonType
23+ = Null
24+ | Boolean
25+ | Number
26+ | String
27+ | Array
28+ | Object
29+ deriving stock (Eq, Ord, Show)
30+
31+describeJSONType :: IsString s => JsonType -> s
32+describeJSONType = \case
33+ Null -> "Null"
34+ Boolean -> "Boolean"
35+ Number -> "Number"
36+ String -> "String"
37+ Array -> "Array"
38+ Object -> "Object"
39+
40+-- | A 'A.Value' whose type we know
41+data TypedValue :: JsonType -> Type where
42+ TNull :: TypedValue 'Null
43+ TBool :: !Bool -> TypedValue 'Boolean
44+ TNumber :: !Scientific -> TypedValue 'Number
45+ TString :: !Text -> TypedValue 'String
46+ TArray :: !A.Array -> TypedValue 'Array
47+ TObject :: !A.Object -> TypedValue 'Object
48+
49+deriving stock instance Eq (TypedValue t)
50+
51+deriving stock instance Ord (TypedValue t)
52+
53+deriving stock instance Show (TypedValue t)
54+
55+untypeValue :: TypedValue t -> A.Value
56+untypeValue TNull = A.Null
57+untypeValue (TBool b) = A.Bool b
58+untypeValue (TNumber n) = A.Number n
59+untypeValue (TString s) = A.String s
60+untypeValue (TArray a) = A.Array a
61+untypeValue (TObject o) = A.Object o
62+
63+data ForeachType (f :: JsonType -> Type) = ForeachType
64+ { forNull :: f 'Null
65+ , forBoolean :: f 'Boolean
66+ , forNumber :: f 'Number
67+ , forString :: f 'String
68+ , forArray :: f 'Array
69+ , forObject :: f 'Object
70+ }
71+
72+deriving stock instance (forall x. Typeable x => Eq (f x)) => Eq (ForeachType f)
73+
74+deriving stock instance (forall x. Typeable x => Ord (f x)) => Ord (ForeachType f)
75+
76+deriving stock instance (forall x. Typeable x => Show (f x)) => Show (ForeachType f)
77+
78+foldType :: Monoid m => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m) -> m
79+foldType k =
80+ k Null forNull
81+ <> k Boolean forBoolean
82+ <> k Number forNumber
83+ <> k String forString
84+ <> k Array forArray
85+ <> k Object forObject
86+
87+forType_ :: Applicative m => (forall x. Typeable x => JsonType -> (ForeachType f -> f x) -> m ()) -> m ()
88+forType_ k = getAp $ foldType (\ty proj -> Ap $ k ty proj)
89+
90+broadcastType :: (forall x. Typeable x => f x) -> ForeachType f
91+broadcastType k = ForeachType
92+ { forNull = k
93+ , forBoolean = k
94+ , forNumber = k
95+ , forString = k
96+ , forArray = k
97+ , forObject = k
98+ }
99+
100+zipType :: (forall x. Typeable x => f x -> g x -> h x) -> ForeachType f -> ForeachType g -> ForeachType h
101+zipType k f1 f2 = ForeachType
102+ { forNull = k (forNull f1) (forNull f2)
103+ , forBoolean = k (forBoolean f1) (forBoolean f2)
104+ , forNumber = k (forNumber f1) (forNumber f2)
105+ , forString = k (forString f1) (forString f2)
106+ , forArray = k (forArray f1) (forArray f2)
107+ , forObject = k (forObject f1) (forObject f2)
108+ }
109+
110+instance (forall x. Lattice (f x)) => Lattice (ForeachType f) where
111+ (\/) = zipType (\/)
112+ (/\) = zipType (/\)
113+
114+instance (forall x. BoundedJoinSemiLattice (f x)) => BoundedJoinSemiLattice (ForeachType f) where
115+ bottom = broadcastType bottom
116+
117+instance (forall x. BoundedMeetSemiLattice (f x)) => BoundedMeetSemiLattice (ForeachType f) where
118+ top = broadcastType top
test/golden/common/bad-oneof-unchanged/a.yamlnew file+39
@@ -0,0 +1,39 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ oneOf:
26+ - type: object
27+ required: ["tag", "prop_A"]
28+ properties:
29+ tag:
30+ enum: ["A"]
31+ prop_A:
32+ type: string
33+ - type: object
34+ required: ["tag", "prop_B"]
35+ properties:
36+ tag:
37+ enum: ["A"]
38+ prop_B:
39+ type: number
test/golden/common/bad-oneof-unchanged/b.yamlnew file+39
@@ -0,0 +1,39 @@
1+openapi: "3.0.0"
2+info:
3+ version: 1.0.0
4+ title: Test
5+servers:
6+ - url: http://localhost/
7+paths:
8+ /test:
9+ post:
10+ requestBody:
11+ content:
12+ application/json:
13+ schema:
14+ $ref: "#/components/schemas/Test"
15+ responses:
16+ '200':
17+ description: test
18+ content:
19+ application/json:
20+ schema:
21+ $ref: "#/components/schemas/Test"
22+components:
23+ schemas:
24+ Test:
25+ oneOf:
26+ - type: object
27+ required: ["tag", "prop_B"]
28+ properties:
29+ tag:
30+ enum: ["A"]
31+ prop_B:
32+ type: number
33+ - type: object
34+ required: ["tag", "prop_A"]
35+ properties:
36+ tag:
37+ enum: ["A"]
38+ prop_A:
39+ type: string
test/golden/common/bad-oneof-unchanged/report.mdnew file+19
@@ -0,0 +1,19 @@
1+# Summary
2+
3+| ⚠️ Breaking changes | 🙆 Non-breaking changes | [🤷 Unsupported feature changes](#unsupported-changes) |
4+|---------------------|------------------------|-------------------------------------------------------|
5+| 0 | 0 | 2 |
6+
7+# <span id="unsupported-changes"></span>🤷 Unsupported feature changes
8+
9+## **POST** /test
10+
11+### 📱➡️ JSON Request
12+
13+Could not deduce that `oneOf` cases don't overlap. Treating the `oneOf`
14+as an `anyOf`. Reported errors might not be accurate.
15+
16+### 📱⬅️ JSON Response – 200
17+
18+Could not deduce that `oneOf` cases don't overlap. Treating the `oneOf`
19+as an `anyOf`. Reported errors might not be accurate.
test/golden/common/bad-oneof-unchanged/trace-tree.yamlnew file+18
@@ -0,0 +1,18 @@
1+breakingChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema: OneOfNotDisjoint
7+ WithStatusCode 200:
8+ ResponsePayload:
9+ PayloadSchema: OneOfNotDisjoint
10+nonBreakingChanges:
11+ AtPath "/test":
12+ InOperation PostMethod:
13+ InRequest:
14+ InPayload:
15+ PayloadSchema: OneOfNotDisjoint
16+ WithStatusCode 200:
17+ ResponsePayload:
18+ PayloadSchema: OneOfNotDisjoint
test/golden/common/pathItem/operation/parameters/change/report.md+2−2
@@ -14,7 +14,7 @@
1414
1515##### `$(String)`
1616
17−The value has been removed.
17+The type has been removed.
1818
1919# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
2020
@@ -26,4 +26,4 @@ The value has been removed.
2626
2727##### `$(Number)`
2828
29−The value has been added.
29+The type has been added.
test/golden/common/pathItem/operation/parameters/change/trace-tree.yaml+2−2
@@ -3,10 +3,10 @@ breakingChanges:
33 InOperation PostMethod:
44 InParam "test":
55 InParamSchema:
6− OfType String: NoContradiction
6+ OfType String: TypeBecomesEmpty
77nonBreakingChanges:
88 AtPath "/test":
99 InOperation PostMethod:
1010 InParam "test":
1111 InParamSchema:
12− OfType Number: NoContradiction
12+ OfType Number: TypeBecomesEmpty
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+2−2
@@ -12,7 +12,7 @@
1212
1313#### `$(String)`
1414
15−The value has been removed.
15+The type has been removed.
1616
1717# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
1818
@@ -22,4 +22,4 @@ The value has been removed.
2222
2323#### `$(Number)`
2424
25−The value has been added.
25+The type has been added.
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/trace-tree.yaml+2−2
@@ -4,11 +4,11 @@ breakingChanges:
44 InRequest:
55 InPayload:
66 PayloadSchema:
7− OfType String: NoContradiction
7+ OfType String: TypeBecomesEmpty
88nonBreakingChanges:
99 AtPath "/test":
1010 InOperation PostMethod:
1111 InRequest:
1212 InPayload:
1313 PayloadSchema:
14− OfType Number: NoContradiction
14+ OfType Number: TypeBecomesEmpty
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+2−2
@@ -12,7 +12,7 @@
1212
1313#### `$(Number)`
1414
15−The value has been added.
15+The type has been added.
1616
1717# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
1818
@@ -22,4 +22,4 @@ The value has been added.
2222
2323#### `$(String)`
2424
25−The value has been removed.
25+The type has been removed.
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/trace-tree.yaml+2−2
@@ -4,11 +4,11 @@ breakingChanges:
44 WithStatusCode 200:
55 ResponsePayload:
66 PayloadSchema:
7− OfType Number: NoContradiction
7+ OfType Number: TypeBecomesEmpty
88nonBreakingChanges:
99 AtPath "/test":
1010 InOperation PostMethod:
1111 WithStatusCode 200:
1212 ResponsePayload:
1313 PayloadSchema:
14− OfType String: NoContradiction
14+ OfType String: TypeBecomesEmpty
test/golden/common/type-changing/report.md+4−4
@@ -32,7 +32,7 @@ Values are now limited to the following types:
3232
3333#### `$(Array)`
3434
35−The value has been removed.
35+The type has been removed.
3636
3737## **POST** /test4
3838
@@ -40,7 +40,7 @@ The value has been removed.
4040
4141#### `$(Object)`
4242
43−The value has been removed.
43+The type has been removed.
4444
4545## **POST** /test5
4646
@@ -82,7 +82,7 @@ Values are now limited to the following types:
8282
8383#### `$(Array)`
8484
85−The value has been removed.
85+The type has been removed.
8686
8787## **POST** /test4
8888
@@ -90,7 +90,7 @@ The value has been removed.
9090
9191#### `$(Object)`
9292
93−The value has been removed.
93+The type has been removed.
9494
9595## **POST** /test5
9696
test/golden/common/type-changing/trace-tree.yaml+4−4
@@ -11,13 +11,13 @@ breakingChanges:
1111 InRequest:
1212 InPayload:
1313 PayloadSchema:
14− OfType Array: NoContradiction
14+ OfType Array: TypeBecomesEmpty
1515 AtPath "/test4":
1616 InOperation PostMethod:
1717 InRequest:
1818 InPayload:
1919 PayloadSchema:
20− OfType Object: NoContradiction
20+ OfType Object: TypeBecomesEmpty
2121 AtPath "/test1":
2222 InOperation PostMethod:
2323 InRequest:
@@ -41,13 +41,13 @@ nonBreakingChanges:
4141 WithStatusCode 200:
4242 ResponsePayload:
4343 PayloadSchema:
44− OfType Array: NoContradiction
44+ OfType Array: TypeBecomesEmpty
4545 AtPath "/test4":
4646 InOperation PostMethod:
4747 WithStatusCode 200:
4848 ResponsePayload:
4949 PayloadSchema:
50− OfType Object: NoContradiction
50+ OfType Object: TypeBecomesEmpty
5151 AtPath "/test1":
5252 InOperation PostMethod:
5353 WithStatusCode 200:
test/golden/common/variant-added/report.md+4−4
@@ -10,9 +10,9 @@
1010
1111### 📱⬅️ JSON Response – 200
1212
13−#### In cases where `$.tag` is `"C"`
13+#### `$(Object)`
1414
15−The value has been added.
15+The case where `$.tag` is `"C"` – has been added.
1616
1717# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
1818
@@ -20,6 +20,6 @@ The value has been added.
2020
2121### 📱➡️ JSON Request
2222
23−#### In cases where `$.tag` is `"C"`
23+#### `$(Object)`
2424
25−The value has been added.
25+The case where `$.tag` is `"C"` – has been added.
test/golden/common/variant-added/trace-tree.yaml+4−4
@@ -4,13 +4,13 @@ breakingChanges:
44 WithStatusCode 200:
55 ResponsePayload:
66 PayloadSchema:
7− OfType Object:
8− InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
7+ OfType Object: PartitionBecomesEmpty (PInProperty "tag" PHere,CByEnumValue
8+ (fromList [String "C"]))
99nonBreakingChanges:
1010 AtPath "/test":
1111 InOperation PostMethod:
1212 InRequest:
1313 InPayload:
1414 PayloadSchema:
15− OfType Object:
16− InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
15+ OfType Object: PartitionBecomesEmpty (PInProperty "tag" PHere,CByEnumValue
16+ (fromList [String "C"]))
test/golden/common/variant-changed/report.md+4−4
@@ -14,7 +14,7 @@
1414
1515##### `$.prop_B(Number)`
1616
17−The value has been removed.
17+The type has been removed.
1818
1919### 📱⬅️ JSON Response – 200
2020
@@ -22,7 +22,7 @@ The value has been removed.
2222
2323##### `$.prop_B(String)`
2424
25−The value has been added.
25+The type has been added.
2626
2727# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
2828
@@ -34,7 +34,7 @@ The value has been added.
3434
3535##### `$.prop_B(String)`
3636
37−The value has been added.
37+The type has been added.
3838
3939### 📱⬅️ JSON Response – 200
4040
@@ -42,4 +42,4 @@ The value has been added.
4242
4343##### `$.prop_B(Number)`
4444
45−The value has been removed.
45+The type has been removed.
test/golden/common/variant-changed/trace-tree.yaml+4−4
@@ -7,14 +7,14 @@ breakingChanges:
77 OfType Object:
88 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
99 InProperty "prop_B":
10− OfType Number: NoContradiction
10+ OfType Number: TypeBecomesEmpty
1111 WithStatusCode 200:
1212 ResponsePayload:
1313 PayloadSchema:
1414 OfType Object:
1515 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
1616 InProperty "prop_B":
17− OfType String: NoContradiction
17+ OfType String: TypeBecomesEmpty
1818nonBreakingChanges:
1919 AtPath "/test":
2020 InOperation PostMethod:
@@ -24,11 +24,11 @@ nonBreakingChanges:
2424 OfType Object:
2525 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
2626 InProperty "prop_B":
27− OfType String: NoContradiction
27+ OfType String: TypeBecomesEmpty
2828 WithStatusCode 200:
2929 ResponsePayload:
3030 PayloadSchema:
3131 OfType Object:
3232 InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
3333 InProperty "prop_B":
34− OfType Number: NoContradiction
34+ OfType Number: TypeBecomesEmpty
test/golden/common/variant-deep-changed/report.md+4−4
@@ -14,7 +14,7 @@
1414
1515##### `$.prop_B(Number)`
1616
17−The value has been removed.
17+The type has been removed.
1818
1919### 📱⬅️ JSON Response – 200
2020
@@ -22,7 +22,7 @@ The value has been removed.
2222
2323##### `$.prop_B(String)`
2424
25−The value has been added.
25+The type has been added.
2626
2727# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
2828
@@ -34,7 +34,7 @@ The value has been added.
3434
3535##### `$.prop_B(String)`
3636
37−The value has been added.
37+The type has been added.
3838
3939### 📱⬅️ JSON Response – 200
4040
@@ -42,4 +42,4 @@ The value has been added.
4242
4343##### `$.prop_B(Number)`
4444
45−The value has been removed.
45+The type has been removed.
test/golden/common/variant-deep-changed/trace-tree.yaml+4−4
@@ -7,14 +7,14 @@ breakingChanges:
77 OfType Object:
88 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
99 InProperty "prop_B":
10− OfType Number: NoContradiction
10+ OfType Number: TypeBecomesEmpty
1111 WithStatusCode 200:
1212 ResponsePayload:
1313 PayloadSchema:
1414 OfType Object:
1515 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
1616 InProperty "prop_B":
17− OfType String: NoContradiction
17+ OfType String: TypeBecomesEmpty
1818nonBreakingChanges:
1919 AtPath "/test":
2020 InOperation PostMethod:
@@ -24,11 +24,11 @@ nonBreakingChanges:
2424 OfType Object:
2525 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
2626 InProperty "prop_B":
27− OfType String: NoContradiction
27+ OfType String: TypeBecomesEmpty
2828 WithStatusCode 200:
2929 ResponsePayload:
3030 PayloadSchema:
3131 OfType Object:
3232 InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
3333 InProperty "prop_B":
34− OfType Number: NoContradiction
34+ OfType Number: TypeBecomesEmpty