git.iko.soy/typeable/compaREST
on master help sign in
Splitting (#104)
2a7f02amniip
src/OpenAPI/Checker/Report.hs+11−6
@@ -192,10 +192,14 @@ unfoldFunctions initA fs g = unfoldFunctions' initA fs
192 let (m, a') = f a
192 let (m, a') = f a
193 in unfoldFunctions' a' ff <> m
193 in unfoldFunctions' a' ff <> m
194
194
195−jets :: [ReportJet' Behave Inlines]
195+jets :: [ReportJet' Behave (Maybe Inlines)]
196jets =
196jets =
197 unwrapReportJetResult
197 unwrapReportJetResult
198− <$> [ constructReportJet jsonPathJet
198+ <$> [ constructReportJet $
199+ curry $ \case
200+ (OfType Object, p@(InPartition _)) -> Just $ describeBehaviour p :: Maybe Inlines
201+ _ -> Nothing
202+ , constructReportJet jsonPathJet
199 , constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
203 , constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
200 strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
204 strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
201 , constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
205 , constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
@@ -239,7 +243,7 @@ jets =
239 <> showParts ys
243 <> showParts ys
240
244
241observeJetShowErrs
245observeJetShowErrs
242− :: ReportJet' Behave Inlines
246+ :: ReportJet' Behave (Maybe Inlines)
243 -> P.PathsPrefixTree Behave AnIssue a
247 -> P.PathsPrefixTree Behave AnIssue a
244 -> (Report, P.PathsPrefixTree Behave AnIssue a)
248 -> (Report, P.PathsPrefixTree Behave AnIssue a)
245observeJetShowErrs jet p = case observeJetShowErrs' jet p of
249observeJetShowErrs jet p = case observeJetShowErrs' jet p of
@@ -248,7 +252,7 @@ observeJetShowErrs jet p = case observeJetShowErrs' jet p of
248
252
249observeJetShowErrs'
253observeJetShowErrs'
250 :: forall a.
254 :: forall a.
251− ReportJet' Behave Inlines
255+ ReportJet' Behave (Maybe Inlines)
252 -> P.PathsPrefixTree Behave AnIssue a
256 -> P.PathsPrefixTree Behave AnIssue a
253 -> Maybe (Report, P.PathsPrefixTree Behave AnIssue a)
257 -> Maybe (Report, P.PathsPrefixTree Behave AnIssue a)
254observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
258observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
@@ -260,10 +264,11 @@ observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues)
260 & mapMaybe
264 & mapMaybe
261 (\case
265 (\case
262 Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
266 Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
263− Pure h ->
267+ Pure (Just h) ->
264 if P.null subErrs
268 if P.null subErrs
265 then Just mempty
269 then Just mempty
266− else Just (singletonHeader h (showErrs subErrs), mempty))
270+ else Just (singletonHeader h (showErrs subErrs), mempty)
271+ Pure Nothing -> Nothing)
267 in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
272 in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
268 if any isRight results
273 if any isRight results
269 then
274 then
src/OpenAPI/Checker/Report/Jet.hs+5−2
@@ -25,14 +25,17 @@ import Text.Pandoc.Builder
25--
25--
26-- The pattern fits well for simplifying 'Behaviour' tree paths.
26-- The pattern fits well for simplifying 'Behaviour' tree paths.
27class ConstructReportJet x f where
27class ConstructReportJet x f where
28− constructReportJet :: x -> ReportJetResult f Inlines
28+ constructReportJet :: x -> ReportJetResult f (Maybe Inlines)
29
29
30instance (ConstructReportJet b f, JetArg a) => ConstructReportJet (a -> b) f where
30instance (ConstructReportJet b f, JetArg a) => ConstructReportJet (a -> b) f where
31 constructReportJet f = Free (fmap f <$> consumeJetArg @a) >>= constructReportJet
31 constructReportJet f = Free (fmap f <$> consumeJetArg @a) >>= constructReportJet
32
32
33−instance ConstructReportJet Inlines f where
33+instance ConstructReportJet (Maybe Inlines) f where
34 constructReportJet x = Pure x
34 constructReportJet x = Pure x
35
35
36+instance ConstructReportJet Inlines f where
37+ constructReportJet x = Pure $ Just x
38+
36class JetArg a where
39class JetArg a where
37 consumeJetArg :: ReportJet' f a
40 consumeJetArg :: ReportJet' f a
38
41
src/OpenAPI/Checker/Validate/Schema.hs+405−52
@@ -16,16 +16,20 @@ module OpenAPI.Checker.Validate.Schema
16where
16where
17
17
18import Algebra.Lattice
18import Algebra.Lattice
19+import Algebra.Lattice.Lifted
19import Control.Applicative
20import Control.Applicative
20import Control.Arrow
21import Control.Arrow
21import Control.Comonad.Env hiding (env)
22import Control.Comonad.Env hiding (env)
22import Control.Monad.Reader hiding (ask)
23import Control.Monad.Reader hiding (ask)
23import qualified Control.Monad.Reader as R
24import qualified Control.Monad.Reader as R
24import Control.Monad.State
25import Control.Monad.State
26+import qualified Control.Monad.Trans.Reader as R (liftCatch)
27+import qualified Control.Monad.Trans.Writer as W (liftCatch)
25import Control.Monad.Writer
28import Control.Monad.Writer
26import qualified Data.Aeson as A
29import qualified Data.Aeson as A
27import qualified Data.ByteString.Lazy as BSL
30import qualified Data.ByteString.Lazy as BSL
28import Data.Coerce
31import Data.Coerce
32+import Data.Foldable
29import qualified Data.Foldable as F
33import qualified Data.Foldable as F
30import Data.Functor
34import Data.Functor
31import Data.Functor.Identity
35import Data.Functor.Identity
@@ -34,6 +38,8 @@ import qualified Data.HashMap.Strict as HM
34import qualified Data.HashMap.Strict.InsOrd as IOHM
38import qualified Data.HashMap.Strict.InsOrd as IOHM
35import Data.Int
39import Data.Int
36import Data.Kind
40import Data.Kind
41+import Data.List (sortBy)
42+import qualified Data.List as L
37import qualified Data.List.NonEmpty as NE
43import qualified Data.List.NonEmpty as NE
38import qualified Data.Map as M
44import qualified Data.Map as M
39import Data.Maybe
45import Data.Maybe
@@ -167,7 +173,7 @@ showCondition = \case
167 showForEachJsonFormula i =
173 showForEachJsonFormula i =
168 bulletList $
174 bulletList $
169 foldType
175 foldType
170− (\t f -> case f i of
176+ (\t f -> case getJsonFormula $ f i of
171 BottomFormula -> mempty
177 BottomFormula -> mempty
172 (DNF conds') ->
178 (DNF conds') ->
173 let conds = S.toList <$> S.toList conds'
179 let conds = S.toList <$> S.toList conds'
@@ -243,28 +249,25 @@ instance Ord SomeCondition where
243
249
244deriving stock instance Show SomeCondition
250deriving stock instance Show SomeCondition
245
251
246−-- | A boolean formula (without "not") of 'Condition's. Represented as a
252+-- | A boolean formula (without "not") represented as a Disjunctive Normal Form:
247−-- Disjunctive Normal Form: the formula is a disjunction of a set of conjuncts,
253+-- the formula is a disjunction of a set of conjuncts, each of which is a
248−-- each of which is a conjunction of a set of 'Condition's.
254+-- conjunction of a set of some elementary formulas.
249−newtype JsonFormula t
255+newtype DNF a
250− = DNF (S.Set (S.Set (Condition t)))
256+ = DNF (S.Set (S.Set a))
251 deriving stock (Eq, Ord, Show)
257 deriving stock (Eq, Ord, Show)
252
258
253−disjAdd
259+disjAdd :: Ord a => DNF a -> S.Set a -> DNF a
254− :: JsonFormula t
255− -> S.Set (Condition t)
256− -> JsonFormula t
257disjAdd (DNF yss) xs
260disjAdd (DNF yss) xs
258 | any (`S.isSubsetOf` xs) yss = DNF yss
261 | any (`S.isSubsetOf` xs) yss = DNF yss
259 | otherwise = DNF $ S.insert xs $ S.filter (not . S.isSubsetOf xs) yss
262 | otherwise = DNF $ S.insert xs $ S.filter (not . S.isSubsetOf xs) yss
260
263
261−instance Lattice (JsonFormula t) where
264+instance Ord a => Lattice (DNF a) where
262 xss \/ DNF yss = S.foldl' disjAdd xss yss
265 xss \/ DNF yss = S.foldl' disjAdd xss yss
263 DNF xss /\ DNF yss =
266 DNF xss /\ DNF yss =
264 F.foldl' disjAdd bottom $
267 F.foldl' disjAdd bottom $
265 liftA2 S.union (S.toList xss) (S.toList yss)
268 liftA2 S.union (S.toList xss) (S.toList yss)
266
269
267−pattern BottomFormula :: JsonFormula t
270+pattern BottomFormula :: DNF a
268pattern BottomFormula <-
271pattern BottomFormula <-
269 DNF (S.null -> True)
272 DNF (S.null -> True)
270 where
273 where
@@ -275,7 +278,7 @@ isSingleton s
275 | S.size s == 1 = S.lookupMin s
278 | S.size s == 1 = S.lookupMin s
276 | otherwise = Nothing
279 | otherwise = Nothing
277
280
278−pattern Conjunct :: [Condition t] -> S.Set (Condition t)
281+pattern Conjunct :: Ord a => [a] -> S.Set a
279pattern Conjunct xs <-
282pattern Conjunct xs <-
280 (S.toList -> xs)
283 (S.toList -> xs)
281 where
284 where
@@ -283,28 +286,28 @@ pattern Conjunct xs <-
283
286
284{-# COMPLETE Conjunct #-}
287{-# COMPLETE Conjunct #-}
285
288
286−pattern SingleConjunct :: [Condition t] -> JsonFormula t
289+pattern SingleConjunct :: Ord a => [a] -> DNF a
287pattern SingleConjunct xs <-
290pattern SingleConjunct xs <-
288 DNF (isSingleton -> Just (Conjunct xs))
291 DNF (isSingleton -> Just (Conjunct xs))
289 where
292 where
290 SingleConjunct xs = DNF $ S.singleton $ Conjunct xs
293 SingleConjunct xs = DNF $ S.singleton $ Conjunct xs
291
294
292−pattern TopFormula :: JsonFormula t
295+pattern TopFormula :: DNF a
293pattern TopFormula <-
296pattern TopFormula <-
294 DNF (isSingleton -> Just (S.null -> True))
297 DNF (isSingleton -> Just (S.null -> True))
295 where
298 where
296 TopFormula = DNF $ S.singleton S.empty
299 TopFormula = DNF $ S.singleton S.empty
297
300
298−instance BoundedJoinSemiLattice (JsonFormula t) where
301+instance Ord a => BoundedJoinSemiLattice (DNF a) where
299 bottom = BottomFormula
302 bottom = BottomFormula
300
303
301−instance BoundedMeetSemiLattice (JsonFormula t) where
304+instance Ord a => BoundedMeetSemiLattice (DNF a) where
302 top = TopFormula
305 top = TopFormula
303
306
304foldLattice
307foldLattice
305 :: BoundedLattice l
308 :: BoundedLattice l
306− => (Condition t -> l)
309+ => (a -> l)
307− -> JsonFormula t
310+ -> DNF a
308 -> l
311 -> l
309foldLattice f (DNF xss) =
312foldLattice f (DNF xss) =
310 S.foldl'
313 S.foldl'
@@ -313,8 +316,12 @@ foldLattice f (DNF xss) =
313 bottom
316 bottom
314 xss
317 xss
315
318
319+newtype JsonFormula t = JsonFormula {getJsonFormula :: DNF (Condition t)}
320+ deriving stock (Eq, Ord, Show)
321+ deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
322+
316satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
323satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
317−satisfiesFormula val = foldLattice (satisfiesTyped val)
324+satisfiesFormula val = foldLattice (satisfiesTyped val) . getJsonFormula
318
325
319data ForeachType (f :: JsonType -> Type) = ForeachType
326data ForeachType (f :: JsonType -> Type) = ForeachType
320 { forNull :: f 'Null
327 { forNull :: f 'Null
@@ -424,6 +431,14 @@ instance Steppable Schema (Referenced Schema) where
424 | NotStep
431 | NotStep
425 deriving stock (Eq, Ord, Show)
432 deriving stock (Eq, Ord, Show)
426
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+
427instance Steppable Schema (Definitions (Referenced Schema)) where
442instance Steppable Schema (Definitions (Referenced Schema)) where
428 data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
443 data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
429 deriving stock (Eq, Ord, Show)
444 deriving stock (Eq, Ord, Show)
@@ -535,7 +550,7 @@ processSchema
535 -> m (ForeachType JsonFormula)
550 -> m (ForeachType JsonFormula)
536processSchema sch@(extract -> Schema {..}) = do
551processSchema sch@(extract -> Schema {..}) = do
537 let singletonFormula :: Condition t -> JsonFormula t
552 let singletonFormula :: Condition t -> JsonFormula t
538− singletonFormula f = SingleConjunct [f]
553+ singletonFormula f = JsonFormula $ SingleConjunct [f]
539
554
540 allClauses <- case tracedAllOf sch of
555 allClauses <- case tracedAllOf sch of
541 Nothing -> pure []
556 Nothing -> pure []
@@ -730,11 +745,11 @@ processSchema sch@(extract -> Schema {..}) = do
730 pure (addProps, fromMaybe fakeSchema addPropSchema)
745 pure (addProps, fromMaybe fakeSchema addPropSchema)
731 pure (k, Property (k `elem` _schemaRequired) f psch)
746 pure (k, Property (k `elem` _schemaRequired) f psch)
732 let allBottom f = getAll $
747 let allBottom f = getAll $
733− foldType $ \_ ty -> case ty f of
748+ foldType $ \_ ty -> case getJsonFormula $ ty f of
734 BottomFormula -> All True
749 BottomFormula -> All True
735 _ -> All False
750 _ -> All False
736 allTop f = getAll $
751 allTop f = getAll $
737− foldType $ \_ ty -> case ty f of
752+ foldType $ \_ ty -> case getJsonFormula $ ty f of
738 TopFormula -> All True
753 TopFormula -> All True
739 _ -> All False
754 _ -> All False
740 -- remove optional fields whose schemata match that of additional props
755 -- remove optional fields whose schemata match that of additional props
@@ -810,9 +825,10 @@ checkFormulas
810 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
825 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
811 => HList xs
826 => HList xs
812 -> Behavior 'SchemaLevel
827 -> Behavior 'SchemaLevel
828+ -> ProdCons (Traced (Definitions Schema))
813 -> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
829 -> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
814 -> SemanticCompatFormula ()
830 -> SemanticCompatFormula ()
815−checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
831+checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
816 case P.toList ep ++ P.toList ec of
832 case P.toList ep ++ P.toList ec of
817 issues@(_ : _) -> F.for_ issues $ embedFormula beh . anItem
833 issues@(_ : _) -> F.for_ issues $ embedFormula beh . anItem
818 [] -> do
834 [] -> do
@@ -826,23 +842,47 @@ checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
826 -- 1) (A ⊂ ∅) <= 0
842 -- 1) (A ⊂ ∅) <= 0
827 -- 2) (X ∩ Y ⊂ B) <= (X ⊂ B) \/ (Y ⊂ B)
843 -- 2) (X ∩ Y ⊂ B) <= (X ⊂ B) \/ (Y ⊂ B)
828 -- 3) (⊤ ⊂ B) <= 0
844 -- 3) (⊤ ⊂ B) <= 0
829− -- Therefore we have the isomorphisms:
845+ -- Therefore we have the isomorphisms with (∃ and ∀ being the N-ary
846+ -- versions of \/ and /\ respectively):
830 -- (⋃_i ⋂_j A[i,j]) ⊂ (⋃_k ⋂_l B[k,l])
847 -- (⋃_i ⋂_j A[i,j]) ⊂ (⋃_k ⋂_l B[k,l])
831− -- = \/_k /\_l /\_i (⋂_j A[i,j]) ⊂ B[k,l]
848+ -- = ∃k ∀l ∀i, (⋂_j A[i,j]) ⊂ B[k,l]
832− -- = /\_i \/_k /\_l (⋂_j A[i,j]) ⊂ B[k,l]
849+ -- = ∀i ∃k ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
833 -- with the caveat that the the set over which k ranges is nonempty.
850 -- with the caveat that the the set over which k ranges is nonempty.
834 -- This is because 1) is not an isomorphism.
851 -- This is because 1) is not an isomorphism.
835 -- Our disjunction loses information, so it makes sense to nest it as
852 -- Our disjunction loses information, so it makes sense to nest it as
836 -- deeply as possible, hence we choose the latter representation.
853 -- deeply as possible, hence we choose the latter representation.
837 --
854 --
838 -- We delegate the verification of (⋂_j A[j]) ⊂ B to a separate heuristic
855 -- We delegate the verification of (⋂_j A[j]) ⊂ B to a separate heuristic
839− -- function, with the understanding that \/_j A[j] ⊂ B is a sufficient,
856+ -- function, with the understanding that ∃j, A[j] ⊂ B is a sufficient,
840 -- but not necessary condition (because of 2) and 3)).
857 -- but not necessary condition (because of 2) and 3)).
841 --
858 --
842 -- If k ranges over an empty set, we have the isomorphism:
859 -- If k ranges over an empty set, we have the isomorphism:
843− -- (⋃_i ⋂_j A[i,j]) ⊂ ∅ = /\_i (⋂_j A[i,j]) ⊂ ∅
860+ -- (⋃_i ⋂_j A[i,j]) ⊂ ∅ = ∀i, (⋂_j A[i,j]) ⊂ ∅
844 -- where we again delegate (⋂_j A[j]) ⊂ ∅ to a heuristic, though here the
861 -- where we again delegate (⋂_j A[j]) ⊂ ∅ to a heuristic, though here the
845− -- shortcut of \/_j A[j] ⊂ ∅ hardly helps.
862+ -- shortcut of ∃j, A[j] ⊂ ∅ hardly helps.
863+ --
864+ -- Disjunctions tend to erase informative error messages, so we may want
865+ -- to avoid them. This can be formally done as follows: if we can
866+ -- partition the universal set into a disjoint union of some parts:
867+ -- ⊤ = ⊔_α P[α]
868+ -- such that the conjuncts in our disjunctive normal form are subordinate
869+ -- to the partition:
870+ -- ∀i ∃α, (⋂_j A[i,j]) ⊂ P[α]
871+ -- ∀k ∃α, (⋂_l B[k,l]) ⊂ P[α]
872+ -- then we can partition the sets over which i and k range into partitions
873+ -- I[α] and K[α], and then in each "bucket" verify the inclusion in the
874+ -- aforementioned way:
875+ -- ∀α, (⋃_i∈I[α] ⋂_j A[i,j]) ⊂ (⋃_k∈K[α] ⋂_l B[k,l])
876+ -- = ∀α ∀i∈I[α] ∃k∈K[α] ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
877+ -- We already somewhat do this by partitioning JSON into types, but we can
878+ -- additionally partition e.g. "enum" fields or existence of particular
879+ -- properties. This works especially well if we manage to ensure K[α] are
880+ -- 1-element sets.
881+ --
882+ -- Since the set:
883+ -- (⋃_i∈I[α] ⋂_j A[i,j]) = (⋃_i ⋂_j A[i,j]) ∩ P[α]
884+ -- does not actually appear in the source schema, we need to construct it
885+ -- ourselves and come up with a name for it.
846 let typesRestricted = not (anyBottomTypes fp) && anyBottomTypes fc
886 let typesRestricted = not (anyBottomTypes fp) && anyBottomTypes fc
847 -- Specifically handle the case when a schema's type has been
887 -- Specifically handle the case when a schema's type has been
848 -- restricted from "all" to specific types: if all types were allowed
888 -- restricted from "all" to specific types: if all types were allowed
@@ -851,7 +891,7 @@ checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
851 when typesRestricted $ issueAt beh $ TypesRestricted $ nonBottomTypes fc
891 when typesRestricted $ issueAt beh $ TypesRestricted $ nonBottomTypes fc
852 forType_ $ \tyName ty -> do
892 forType_ $ \tyName ty -> do
853 let beh' = beh >>> step (OfType tyName)
893 let beh' = beh >>> step (OfType tyName)
854− case (ty fp, ty fc) of
894+ case (getJsonFormula $ ty fp, getJsonFormula $ ty fc) of
855 (DNF pss, BottomFormula) -> unless typesRestricted $ do
895 (DNF pss, BottomFormula) -> unless typesRestricted $ do
856 -- don't repeat the TypesRestricted issue
896 -- don't repeat the TypesRestricted issue
857 F.for_ pss $ \(Conjunct ps) -> checkContradiction beh' ps
897 F.for_ pss $ \(Conjunct ps) -> checkContradiction beh' ps
@@ -862,26 +902,293 @@ checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
862 -- In this case we want to show which restrictions were added. (instead
902 -- In this case we want to show which restrictions were added. (instead
863 -- of showing an empty list restrictions that couldn't be satisfied.)
903 -- of showing an empty list restrictions that couldn't be satisfied.)
864 F.for_ css $ \(Conjunct cs) -> F.for_ cs $ checkImplication env beh' []
904 F.for_ css $ \(Conjunct cs) -> F.for_ cs $ checkImplication env beh' []
865− (DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
905+ (pss', css') -> F.for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
906+ (mPart, ProdCons pf cf) -> do
907+ let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
908+ case (getJsonFormula pf, getJsonFormula cf) of
909+ (DNF pss, BottomFormula) -> F.for_ pss $ \(Conjunct ps) -> checkContradiction beh'' ps
910+ (DNF pss, SingleConjunct cs) -> F.for_ pss $ \(Conjunct ps) -> do
911+ F.for_ cs $ checkImplication env beh'' ps
912+ -- unlucky:
913+ (DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
866− anyOfAt
914+ anyOfAt
867− beh'
915+ beh'
868− (issueFromConjunct ps)
916+ (issueFromConjunct Nothing ps)
869− [F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
917+ [F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
870 pure ()
918 pure ()
871 where
919 where
872 anyBottomTypes f = getAny $
920 anyBottomTypes f = getAny $
873− foldType $ \_ ty -> case ty f of
921+ foldType $ \_ ty -> case getJsonFormula $ ty f of
874 BottomFormula -> Any True
922 BottomFormula -> Any True
875 _ -> mempty
923 _ -> mempty
876− nonBottomTypes f = foldType $ \tyName ty -> case ty f of
924+ nonBottomTypes f = foldType $ \tyName ty -> case getJsonFormula $ ty f of
877 BottomFormula -> mempty
925 BottomFormula -> mempty
878 _ -> [tyName]
926 _ -> [tyName]
879− issueFromConjunct :: Typeable t => [Condition t] -> Issue 'TypedSchemaLevel
927+ issueFromConjunct :: Typeable t => Partition -> [Condition t] -> Issue 'TypedSchemaLevel
880− issueFromConjunct ps
928+ issueFromConjunct _ ps
881 | Just e <- findExactly ps
929 | Just e <- findExactly ps
882 , all (satisfiesTyped e) ps =
930 , all (satisfiesTyped e) ps =
883− EnumDoesntSatisfy $ untypeValue e
931+ EnumDoesntSatisfy $ untypeValue e -- what does this look like when partitioned?
884− issueFromConjunct ps = NoMatchingCondition $ SomeCondition <$> ps
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]
885
1192
886checkContradiction
1193checkContradiction
887 :: Behavior 'TypedSchemaLevel
1194 :: Behavior 'TypedSchemaLevel
@@ -889,6 +1196,13 @@ checkContradiction
889 -> SemanticCompatFormula ()
1196 -> SemanticCompatFormula ()
890checkContradiction beh _ = issueAt beh NoContradiction -- TODO #70
1197checkContradiction beh _ = issueAt beh NoContradiction -- TODO #70
891
1198
1199+tracedConjunct :: NE.NonEmpty (Traced (Referenced Schema)) -> Traced (Referenced Schema)
1200+tracedConjunct refSchemas = case NE.sortWith ask refSchemas of
1201+ (rs NE.:| []) -> rs
1202+ (rs1 NE.:| rs2 : rss) ->
1203+ traced (ask rs1 >>> step (ConjunctedWith $ ask <$> rs2 NE.:| rss)) $
1204+ Inline mempty {_schemaAllOf = Just $ extract <$> rs1 : rs2 : rss}
1205+
892checkImplication
1206checkImplication
893 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
1207 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
894 => HList xs
1208 => HList xs
@@ -949,10 +1263,7 @@ checkImplication env beh prods cons = case findExactly prods of
949 then pure ()
1263 then pure ()
950 else issueAt beh (NoMatchingFormat f)
1264 else issueAt beh (NoMatchingFormat f)
951 Items _ cons' -> case findRelevant (<>) (\case Items _ rs -> Just (rs NE.:| []); _ -> Nothing) prods of
1265 Items _ cons' -> case findRelevant (<>) (\case Items _ rs -> Just (rs NE.:| []); _ -> Nothing) prods of
952− Just (rs NE.:| []) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
953− Just rs -> do
954− let sch = Inline mempty {_schemaAllOf = Just . NE.toList $ extract <$> rs}
955− checkCompatibility (beh >>> step InItems) env $ ProdCons (traced (ask $ NE.head rs) sch) cons' -- TODO: bad trace
1266+ Just (tracedConjunct -> rs) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
956 Nothing -> issueAt beh NoMatchingItems
1267 Nothing -> issueAt beh NoMatchingItems
957 MaxItems m -> case findRelevant min (\case MaxItems m' -> Just m'; _ -> Nothing) prods of
1268 MaxItems m -> case findRelevant min (\case MaxItems m' -> Just m'; _ -> Nothing) prods of
958 Just m' ->
1269 Just m' ->
@@ -1079,7 +1390,7 @@ instance Issuable 'TypedSchemaLevel where
1079 | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
1390 | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
1080 MatchingMinPropertiesWeak (ProdCons Integer)
1391 MatchingMinPropertiesWeak (ProdCons Integer)
1081 | -- | 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)
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)
1082− NoMatchingCondition [SomeCondition]
1393+ NoMatchingCondition Partition [SomeCondition]
1083 | -- | 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)
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)
1084 -- AKA consumer does not have the type
1395 -- AKA consumer does not have the type
1085 NoContradiction
1396 NoContradiction
@@ -1128,15 +1439,23 @@ instance Issuable 'TypedSchemaLevel where
1128 describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
1439 describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
1129 describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
1440 describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
1130 describeIssue _ (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
1441 describeIssue _ (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
1131− describeIssue _ (NoMatchingCondition conds) =
1442+ describeIssue _ (NoMatchingCondition mPart conds) =
1132− para "Expected the following conditions to hold, but they didn't (please file a bug if you see this):"
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):")
1133 <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
1449 <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
1134− describeIssue Forward NoContradiction = para "The type has been removed."
1450+ describeIssue Forward NoContradiction = para "The value has been removed."
1135− describeIssue Backward NoContradiction = para "The type has been added."
1451+ describeIssue Backward NoContradiction = para "The value has been added."
1136
1452
1137showJSONValue :: A.Value -> Blocks
1453showJSONValue :: A.Value -> Blocks
1138showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1454showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1139
1455
1456+showJSONValueInline :: A.Value -> Inlines
1457+showJSONValueInline v = code (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1458+
1140showBound :: Show a => Bound a -> Inlines
1459showBound :: Show a => Bound a -> Inlines
1141showBound (Inclusive x) = show' x <> " inclusive"
1460showBound (Inclusive x) = show' x <> " inclusive"
1142showBound (Exclusive x) = show' x <> " exclusive"
1461showBound (Exclusive x) = show' x <> " exclusive"
@@ -1206,6 +1525,40 @@ describeJSONType = \case
1206 Array -> "Array"
1525 Array -> "Array"
1207 Object -> "Object"
1526 Object -> "Object"
1208
1527
1528+instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
1529+ data Behave 'TypedSchemaLevel 'TypedSchemaLevel
1530+ = InPartition (PartitionLocation, PartitionChoice)
1531+ deriving stock (Eq, Ord, Show)
1532+
1533+ describeBehaviour (InPartition partition) = showPartition partition
1534+
1535+showPartition :: (PartitionLocation, PartitionChoice) -> Inlines
1536+showPartition = \case
1537+ (partition, CByEnumValue (S.toList -> [v])) ->
1538+ "In cases where " <> renderPartitionLocation partition <> " is " <> showJSONValueInline v
1539+ (partition, CByEnumValue (S.toList -> vs)) ->
1540+ "In cases where " <> renderPartitionLocation partition <> " has values: "
1541+ <> (fold . L.intersperse ", " . fmap showJSONValueInline $ vs)
1542+ (partition, CByProperties (S.toList -> incl) (S.toList -> [])) ->
1543+ "In cases where " <> renderPartitionLocation partition <> " contains the properties: " <> listCodes incl
1544+ (partition, CByProperties (S.toList -> []) (S.toList -> excl)) ->
1545+ "In cases where " <> renderPartitionLocation partition <> " does not contain the properties: " <> listCodes excl
1546+ (partition, CByProperties (S.toList -> incl) (S.toList -> excl)) ->
1547+ "In cases where " <> renderPartitionLocation partition
1548+ <> " contains the properties "
1549+ <> listCodes incl
1550+ <> " and does not contain the properties "
1551+ <> listCodes excl
1552+ where
1553+ listCodes :: [Text] -> Inlines
1554+ listCodes = fold . L.intersperse ", " . fmap code
1555+ renderPartitionLocation :: PartitionLocation -> Inlines
1556+ renderPartitionLocation p = code $ "$" <> renderPartitionLocation' p
1557+ where
1558+ renderPartitionLocation' :: PartitionLocation -> Text
1559+ renderPartitionLocation' PHere = mempty
1560+ renderPartitionLocation' (PInProperty prop rest) = "." <> prop <> renderPartitionLocation' rest
1561+
1209instance Behavable 'TypedSchemaLevel 'SchemaLevel where
1562instance Behavable 'TypedSchemaLevel 'SchemaLevel where
1210 data Behave 'TypedSchemaLevel 'SchemaLevel
1563 data Behave 'TypedSchemaLevel 'SchemaLevel
1211 = InItems
1564 = InItems
@@ -1271,4 +1624,4 @@ instance Subtree Schema where
1271 structuralItems _ = structuralIssue
1624 structuralItems _ = structuralIssue
1272 checkSemanticCompatibility env beh schs = do
1625 checkSemanticCompatibility env beh schs = do
1273 let defs = getH env
1626 let defs = getH env
1274− checkFormulas env beh $ schemaToFormula <$> defs <*> schs
1627+ checkFormulas env beh defs $ schemaToFormula <$> defs <*> schs
stack.yaml+1−1
@@ -1,4 +1,4 @@
1−resolver: lts-18.0
1+resolver: lts-18.2
2
2
3extra-deps:
3extra-deps:
4 - open-union-0.4.0.0
4 - open-union-0.4.0.0
stack.yaml.lock+4−4
@@ -20,7 +20,7 @@ packages:
20 hackage: type-fun-0.1.3
20 hackage: type-fun-0.1.3
21snapshots:
21snapshots:
22- completed:
22- completed:
23− size: 585393
23+ size: 585392
24− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/0.yaml
24+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/2.yaml
25− sha256: c632012da648385b9fa3c29f4e0afd56ead299f1c5528ee789058be410e883c0
25+ sha256: 7abb45c0cc5eb349448b66d8753655542d45d387ad26970419282eab3d860724
26− original: lts-18.0
26+ original: lts-18.2
test/golden/common/enum-added/a.yamlnew file+25
@@ -0,0 +1,25 @@
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+ enum: ["A", "B"]
test/golden/common/enum-added/b.yamlnew file+25
@@ -0,0 +1,25 @@
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+ enum: ["A", "B", "C"]
test/golden/common/enum-added/report.mdnew file+33
@@ -0,0 +1,33 @@
1+# Summary
2+
3+| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
4+|------------------------------------------|-------------------------------------------------|-------------------------------|
5+| 1 | 1 | 0 |
6+
7+# <span id="breaking-changes"></span>⚠️ Breaking changes
8+
9+## **POST** /test
10+
11+### 📱⬅️ JSON Response – 200
12+
13+#### `$(String)`
14+
15+The following enum value was added:
16+
17+``` json
18+"C"
19+```
20+
21+# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
22+
23+## **POST** /test
24+
25+### 📱➡️ JSON Request
26+
27+#### `$(String)`
28+
29+The following enum value was added:
30+
31+``` json
32+"C"
33+```
test/golden/common/enum-added/trace-tree.yamlnew file+14
@@ -0,0 +1,14 @@
1+breakingChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ WithStatusCode 200:
5+ ResponsePayload:
6+ PayloadSchema:
7+ OfType String: EnumDoesntSatisfy (String "C")
8+nonBreakingChanges:
9+ AtPath "/test":
10+ InOperation PostMethod:
11+ InRequest:
12+ InPayload:
13+ PayloadSchema:
14+ OfType String: EnumDoesntSatisfy (String "C")
test/golden/common/pathItem/operation/parameters/change/report.md+2−2
@@ -14,7 +14,7 @@
14
14
15##### `$(String)`
15##### `$(String)`
16
16
17−The type has been removed.
17+The value has been removed.
18
18
19# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
19# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
20
20
@@ -26,4 +26,4 @@ The type has been removed.
26
26
27##### `$(Number)`
27##### `$(Number)`
28
28
29−The type has been added.
29+The value has been added.
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+2−2
@@ -12,7 +12,7 @@
12
12
13#### `$(String)`
13#### `$(String)`
14
14
15−The type has been removed.
15+The value has been removed.
16
16
17# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
18
18
@@ -22,4 +22,4 @@ The type has been removed.
22
22
23#### `$(Number)`
23#### `$(Number)`
24
24
25−The type has been added.
25+The value has been added.
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+2−2
@@ -12,7 +12,7 @@
12
12
13#### `$(Number)`
13#### `$(Number)`
14
14
15−The type has been added.
15+The value has been added.
16
16
17# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
17# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
18
18
@@ -22,4 +22,4 @@ The type has been added.
22
22
23#### `$(String)`
23#### `$(String)`
24
24
25−The type has been removed.
25+The value has been removed.
test/golden/common/type-changing/report.md+4−4
@@ -32,7 +32,7 @@ Values are now limited to the following types:
32
32
33#### `$(Array)`
33#### `$(Array)`
34
34
35−The type has been removed.
35+The value has been removed.
36
36
37## **POST** /test4
37## **POST** /test4
38
38
@@ -40,7 +40,7 @@ The type has been removed.
40
40
41#### `$(Object)`
41#### `$(Object)`
42
42
43−The type has been removed.
43+The value has been removed.
44
44
45## **POST** /test5
45## **POST** /test5
46
46
@@ -82,7 +82,7 @@ Values are now limited to the following types:
82
82
83#### `$(Array)`
83#### `$(Array)`
84
84
85−The type has been removed.
85+The value has been removed.
86
86
87## **POST** /test4
87## **POST** /test4
88
88
@@ -90,7 +90,7 @@ The type has been removed.
90
90
91#### `$(Object)`
91#### `$(Object)`
92
92
93−The type has been removed.
93+The value has been removed.
94
94
95## **POST** /test5
95## **POST** /test5
96
96
test/golden/common/variant-added/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: ["B"]
38+ prop_B:
39+ type: number
test/golden/common/variant-added/b.yamlnew file+46
@@ -0,0 +1,46 @@
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: ["B"]
38+ prop_B:
39+ type: number
40+ - type: object
41+ required: ["tag", "prop_C"]
42+ properties:
43+ tag:
44+ enum: ["C"]
45+ prop_C:
46+ type: number
test/golden/common/variant-added/report.mdnew file+25
@@ -0,0 +1,25 @@
1+# Summary
2+
3+| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
4+|------------------------------------------|-------------------------------------------------|-------------------------------|
5+| 1 | 1 | 0 |
6+
7+# <span id="breaking-changes"></span>⚠️ Breaking changes
8+
9+## **POST** /test
10+
11+### 📱⬅️ JSON Response – 200
12+
13+#### In cases where `$.tag` is `"C"`
14+
15+The value has been added.
16+
17+# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
18+
19+## **POST** /test
20+
21+### 📱➡️ JSON Request
22+
23+#### In cases where `$.tag` is `"C"`
24+
25+The value has been added.
test/golden/common/variant-added/trace-tree.yamlnew file+16
@@ -0,0 +1,16 @@
1+breakingChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ WithStatusCode 200:
5+ ResponsePayload:
6+ PayloadSchema:
7+ OfType Object:
8+ InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
9+nonBreakingChanges:
10+ AtPath "/test":
11+ InOperation PostMethod:
12+ InRequest:
13+ InPayload:
14+ PayloadSchema:
15+ OfType Object:
16+ InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
test/golden/common/variant-changed/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: ["B"]
38+ prop_B:
39+ type: number
test/golden/common/variant-changed/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_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: ["B"]
38+ prop_B:
39+ type: string
test/golden/common/variant-changed/report.mdnew file+45
@@ -0,0 +1,45 @@
1+# Summary
2+
3+| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
4+|------------------------------------------|-------------------------------------------------|-------------------------------|
5+| 2 | 2 | 0 |
6+
7+# <span id="breaking-changes"></span>⚠️ Breaking changes
8+
9+## **POST** /test
10+
11+### 📱➡️ JSON Request
12+
13+#### In cases where `$.tag` is `"B"`
14+
15+##### `$.prop_B(Number)`
16+
17+The value has been removed.
18+
19+### 📱⬅️ JSON Response – 200
20+
21+#### In cases where `$.tag` is `"B"`
22+
23+##### `$.prop_B(String)`
24+
25+The value has been added.
26+
27+# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
28+
29+## **POST** /test
30+
31+### 📱➡️ JSON Request
32+
33+#### In cases where `$.tag` is `"B"`
34+
35+##### `$.prop_B(String)`
36+
37+The value has been added.
38+
39+### 📱⬅️ JSON Response – 200
40+
41+#### In cases where `$.tag` is `"B"`
42+
43+##### `$.prop_B(Number)`
44+
45+The value has been removed.
test/golden/common/variant-changed/trace-tree.yamlnew file+34
@@ -0,0 +1,34 @@
1+breakingChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema:
7+ OfType Object:
8+ InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
9+ InProperty "prop_B":
10+ OfType Number: NoContradiction
11+ WithStatusCode 200:
12+ ResponsePayload:
13+ PayloadSchema:
14+ OfType Object:
15+ InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
16+ InProperty "prop_B":
17+ OfType String: NoContradiction
18+nonBreakingChanges:
19+ AtPath "/test":
20+ InOperation PostMethod:
21+ InRequest:
22+ InPayload:
23+ PayloadSchema:
24+ OfType Object:
25+ InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
26+ InProperty "prop_B":
27+ OfType String: NoContradiction
28+ WithStatusCode 200:
29+ ResponsePayload:
30+ PayloadSchema:
31+ OfType Object:
32+ InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
33+ InProperty "prop_B":
34+ OfType Number: NoContradiction
test/golden/common/variant-deep-changed/a.yamlnew file+47
@@ -0,0 +1,47 @@
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: ["desc", "prop_A"]
28+ properties:
29+ desc:
30+ type: object
31+ required: ["name"]
32+ properties:
33+ name:
34+ enum: ["A"]
35+ prop_A:
36+ type: string
37+ - type: object
38+ required: ["desc", "prop_B"]
39+ properties:
40+ desc:
41+ type: object
42+ required: ["name"]
43+ properties:
44+ name:
45+ enum: ["B"]
46+ prop_B:
47+ type: number
test/golden/common/variant-deep-changed/b.yamlnew file+47
@@ -0,0 +1,47 @@
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: ["desc", "prop_A"]
28+ properties:
29+ desc:
30+ type: object
31+ required: ["name"]
32+ properties:
33+ name:
34+ enum: ["A"]
35+ prop_A:
36+ type: string
37+ - type: object
38+ required: ["desc", "prop_B"]
39+ properties:
40+ desc:
41+ type: object
42+ required: ["name"]
43+ properties:
44+ name:
45+ enum: ["B"]
46+ prop_B:
47+ type: string
test/golden/common/variant-deep-changed/report.mdnew file+45
@@ -0,0 +1,45 @@
1+# Summary
2+
3+| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
4+|------------------------------------------|-------------------------------------------------|-------------------------------|
5+| 2 | 2 | 0 |
6+
7+# <span id="breaking-changes"></span>⚠️ Breaking changes
8+
9+## **POST** /test
10+
11+### 📱➡️ JSON Request
12+
13+#### In cases where `$.desc.name` is `"B"`
14+
15+##### `$.prop_B(Number)`
16+
17+The value has been removed.
18+
19+### 📱⬅️ JSON Response – 200
20+
21+#### In cases where `$.desc.name` is `"B"`
22+
23+##### `$.prop_B(String)`
24+
25+The value has been added.
26+
27+# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
28+
29+## **POST** /test
30+
31+### 📱➡️ JSON Request
32+
33+#### In cases where `$.desc.name` is `"B"`
34+
35+##### `$.prop_B(String)`
36+
37+The value has been added.
38+
39+### 📱⬅️ JSON Response – 200
40+
41+#### In cases where `$.desc.name` is `"B"`
42+
43+##### `$.prop_B(Number)`
44+
45+The value has been removed.
test/golden/common/variant-deep-changed/trace-tree.yamlnew file+34
@@ -0,0 +1,34 @@
1+breakingChanges:
2+ AtPath "/test":
3+ InOperation PostMethod:
4+ InRequest:
5+ InPayload:
6+ PayloadSchema:
7+ OfType Object:
8+ InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
9+ InProperty "prop_B":
10+ OfType Number: NoContradiction
11+ WithStatusCode 200:
12+ ResponsePayload:
13+ PayloadSchema:
14+ OfType Object:
15+ InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
16+ InProperty "prop_B":
17+ OfType String: NoContradiction
18+nonBreakingChanges:
19+ AtPath "/test":
20+ InOperation PostMethod:
21+ InRequest:
22+ InPayload:
23+ PayloadSchema:
24+ OfType Object:
25+ InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
26+ InProperty "prop_B":
27+ OfType String: NoContradiction
28+ WithStatusCode 200:
29+ ResponsePayload:
30+ PayloadSchema:
31+ OfType Object:
32+ InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
33+ InProperty "prop_B":
34+ OfType Number: NoContradiction