Splitting (#104)

* Fix traces in conjuncted "items" clauses

* Add a couple tests

* Add an initial framework for partitioning (#71) and allow partitioning by an enum

* Try to preserve the logic about enum removal

* Add some tests

* Report partitions in the behavior somehow

* Bumped resolver

* Refined report

* Removed trailing period

Co-authored-by: iko <ilyakooo0@gmail.com>
This commit is contained in:
mniip 2021-07-15 18:03:19 +03:00 committed by GitHub
parent cc07ccf254
commit 2a7f02ae1e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
25 changed files with 989 additions and 75 deletions

View File

@ -192,10 +192,14 @@ unfoldFunctions initA fs g = unfoldFunctions' initA fs
let (m, a') = f a
in unfoldFunctions' a' ff <> m
jets :: [ReportJet' Behave Inlines]
jets :: [ReportJet' Behave (Maybe Inlines)]
jets =
unwrapReportJetResult
<$> [ constructReportJet jsonPathJet
<$> [ constructReportJet $
curry $ \case
(OfType Object, p@(InPartition _)) -> Just $ describeBehaviour p :: Maybe Inlines
_ -> Nothing
, constructReportJet jsonPathJet
, constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
, constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
@ -239,7 +243,7 @@ jets =
<> showParts ys
observeJetShowErrs
:: ReportJet' Behave Inlines
:: ReportJet' Behave (Maybe Inlines)
-> P.PathsPrefixTree Behave AnIssue a
-> (Report, P.PathsPrefixTree Behave AnIssue a)
observeJetShowErrs jet p = case observeJetShowErrs' jet p of
@ -248,7 +252,7 @@ observeJetShowErrs jet p = case observeJetShowErrs' jet p of
observeJetShowErrs'
:: forall a.
ReportJet' Behave Inlines
ReportJet' Behave (Maybe Inlines)
-> P.PathsPrefixTree Behave AnIssue a
-> Maybe (Report, P.PathsPrefixTree Behave AnIssue a)
observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
@ -260,10 +264,11 @@ observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues)
& mapMaybe
(\case
Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
Pure h ->
Pure (Just h) ->
if P.null subErrs
then Just mempty
else Just (singletonHeader h (showErrs subErrs), mempty))
else Just (singletonHeader h (showErrs subErrs), mempty)
Pure Nothing -> Nothing)
in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
if any isRight results
then

View File

@ -25,14 +25,17 @@ import Text.Pandoc.Builder
--
-- The pattern fits well for simplifying 'Behaviour' tree paths.
class ConstructReportJet x f where
constructReportJet :: x -> ReportJetResult f Inlines
constructReportJet :: x -> ReportJetResult f (Maybe Inlines)
instance (ConstructReportJet b f, JetArg a) => ConstructReportJet (a -> b) f where
constructReportJet f = Free (fmap f <$> consumeJetArg @a) >>= constructReportJet
instance ConstructReportJet Inlines f where
instance ConstructReportJet (Maybe Inlines) f where
constructReportJet x = Pure x
instance ConstructReportJet Inlines f where
constructReportJet x = Pure $ Just x
class JetArg a where
consumeJetArg :: ReportJet' f a

View File

@ -16,16 +16,20 @@ module OpenAPI.Checker.Validate.Schema
where
import Algebra.Lattice
import Algebra.Lattice.Lifted
import Control.Applicative
import Control.Arrow
import Control.Comonad.Env hiding (env)
import Control.Monad.Reader hiding (ask)
import qualified Control.Monad.Reader as R
import Control.Monad.State
import qualified Control.Monad.Trans.Reader as R (liftCatch)
import qualified Control.Monad.Trans.Writer as W (liftCatch)
import Control.Monad.Writer
import qualified Data.Aeson as A
import qualified Data.ByteString.Lazy as BSL
import Data.Coerce
import Data.Foldable
import qualified Data.Foldable as F
import Data.Functor
import Data.Functor.Identity
@ -34,6 +38,8 @@ import qualified Data.HashMap.Strict as HM
import qualified Data.HashMap.Strict.InsOrd as IOHM
import Data.Int
import Data.Kind
import Data.List (sortBy)
import qualified Data.List as L
import qualified Data.List.NonEmpty as NE
import qualified Data.Map as M
import Data.Maybe
@ -167,7 +173,7 @@ showCondition = \case
showForEachJsonFormula i =
bulletList $
foldType
(\t f -> case f i of
(\t f -> case getJsonFormula $ f i of
BottomFormula -> mempty
(DNF conds') ->
let conds = S.toList <$> S.toList conds'
@ -243,28 +249,25 @@ instance Ord SomeCondition where
deriving stock instance Show SomeCondition
-- | A boolean formula (without "not") of 'Condition's. Represented as a
-- Disjunctive Normal Form: the formula is a disjunction of a set of conjuncts,
-- each of which is a conjunction of a set of 'Condition's.
newtype JsonFormula t
= DNF (S.Set (S.Set (Condition t)))
-- | A boolean formula (without "not") represented as a Disjunctive Normal Form:
-- the formula is a disjunction of a set of conjuncts, each of which is a
-- conjunction of a set of some elementary formulas.
newtype DNF a
= DNF (S.Set (S.Set a))
deriving stock (Eq, Ord, Show)
disjAdd
:: JsonFormula t
-> S.Set (Condition t)
-> JsonFormula t
disjAdd :: Ord a => DNF a -> S.Set a -> DNF a
disjAdd (DNF yss) xs
| any (`S.isSubsetOf` xs) yss = DNF yss
| otherwise = DNF $ S.insert xs $ S.filter (not . S.isSubsetOf xs) yss
instance Lattice (JsonFormula t) where
instance Ord a => Lattice (DNF a) where
xss \/ DNF yss = S.foldl' disjAdd xss yss
DNF xss /\ DNF yss =
F.foldl' disjAdd bottom $
liftA2 S.union (S.toList xss) (S.toList yss)
pattern BottomFormula :: JsonFormula t
pattern BottomFormula :: DNF a
pattern BottomFormula <-
DNF (S.null -> True)
where
@ -275,7 +278,7 @@ isSingleton s
| S.size s == 1 = S.lookupMin s
| otherwise = Nothing
pattern Conjunct :: [Condition t] -> S.Set (Condition t)
pattern Conjunct :: Ord a => [a] -> S.Set a
pattern Conjunct xs <-
(S.toList -> xs)
where
@ -283,28 +286,28 @@ pattern Conjunct xs <-
{-# COMPLETE Conjunct #-}
pattern SingleConjunct :: [Condition t] -> JsonFormula t
pattern SingleConjunct :: Ord a => [a] -> DNF a
pattern SingleConjunct xs <-
DNF (isSingleton -> Just (Conjunct xs))
where
SingleConjunct xs = DNF $ S.singleton $ Conjunct xs
pattern TopFormula :: JsonFormula t
pattern TopFormula :: DNF a
pattern TopFormula <-
DNF (isSingleton -> Just (S.null -> True))
where
TopFormula = DNF $ S.singleton S.empty
instance BoundedJoinSemiLattice (JsonFormula t) where
instance Ord a => BoundedJoinSemiLattice (DNF a) where
bottom = BottomFormula
instance BoundedMeetSemiLattice (JsonFormula t) where
instance Ord a => BoundedMeetSemiLattice (DNF a) where
top = TopFormula
foldLattice
:: BoundedLattice l
=> (Condition t -> l)
-> JsonFormula t
=> (a -> l)
-> DNF a
-> l
foldLattice f (DNF xss) =
S.foldl'
@ -313,8 +316,12 @@ foldLattice f (DNF xss) =
bottom
xss
newtype JsonFormula t = JsonFormula {getJsonFormula :: DNF (Condition t)}
deriving stock (Eq, Ord, Show)
deriving newtype (Lattice, BoundedJoinSemiLattice, BoundedMeetSemiLattice)
satisfiesFormula :: TypedValue t -> JsonFormula t -> Bool
satisfiesFormula val = foldLattice (satisfiesTyped val)
satisfiesFormula val = foldLattice (satisfiesTyped val) . getJsonFormula
data ForeachType (f :: JsonType -> Type) = ForeachType
{ forNull :: f 'Null
@ -424,6 +431,14 @@ instance Steppable Schema (Referenced Schema) where
| NotStep
deriving stock (Eq, Ord, Show)
instance Steppable (Referenced Schema) (Referenced Schema) where
data Step (Referenced Schema) (Referenced Schema)
= -- | Invariant (for better memoization only): the "tail" of the trace is
-- the "least" of the traces of the conjuncted schemata
ConjunctedWith (NE.NonEmpty (Trace (Referenced Schema)))
| Partitioned PartitionLocation PartitionChoice
deriving stock (Eq, Ord, Show)
instance Steppable Schema (Definitions (Referenced Schema)) where
data Step Schema (Definitions (Referenced Schema)) = PropertiesStep
deriving stock (Eq, Ord, Show)
@ -535,7 +550,7 @@ processSchema
-> m (ForeachType JsonFormula)
processSchema sch@(extract -> Schema {..}) = do
let singletonFormula :: Condition t -> JsonFormula t
singletonFormula f = SingleConjunct [f]
singletonFormula f = JsonFormula $ SingleConjunct [f]
allClauses <- case tracedAllOf sch of
Nothing -> pure []
@ -730,11 +745,11 @@ processSchema sch@(extract -> Schema {..}) = do
pure (addProps, fromMaybe fakeSchema addPropSchema)
pure (k, Property (k `elem` _schemaRequired) f psch)
let allBottom f = getAll $
foldType $ \_ ty -> case ty f of
foldType $ \_ ty -> case getJsonFormula $ ty f of
BottomFormula -> All True
_ -> All False
allTop f = getAll $
foldType $ \_ ty -> case ty f of
foldType $ \_ ty -> case getJsonFormula $ ty f of
TopFormula -> All True
_ -> All False
-- remove optional fields whose schemata match that of additional props
@ -810,9 +825,10 @@ checkFormulas
:: (ReassembleHList xs (CheckEnv (Referenced Schema)))
=> HList xs
-> Behavior 'SchemaLevel
-> ProdCons (Traced (Definitions Schema))
-> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
-> SemanticCompatFormula ()
checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
case P.toList ep ++ P.toList ec of
issues@(_ : _) -> F.for_ issues $ embedFormula beh . anItem
[] -> do
@ -826,23 +842,47 @@ checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
-- 1) (A ⊂ ∅) <= 0
-- 2) (X ∩ Y ⊂ B) <= (X ⊂ B) \/ (Y ⊂ B)
-- 3) ( ⊂ B) <= 0
-- Therefore we have the isomorphisms:
-- Therefore we have the isomorphisms with (∃ and ∀ being the N-ary
-- versions of \/ and /\ respectively):
-- (_i ⋂_j A[i,j]) ⊂ (_k ⋂_l B[k,l])
-- = \/_k /\_l /\_i (⋂_j A[i,j]) ⊂ B[k,l]
-- = /\_i \/_k /\_l (⋂_j A[i,j]) ⊂ B[k,l]
-- = ∃k ∀l ∀i, (⋂_j A[i,j]) ⊂ B[k,l]
-- = ∀i ∃k ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
-- with the caveat that the the set over which k ranges is nonempty.
-- This is because 1) is not an isomorphism.
-- Our disjunction loses information, so it makes sense to nest it as
-- deeply as possible, hence we choose the latter representation.
--
-- We delegate the verification of (⋂_j A[j]) ⊂ B to a separate heuristic
-- function, with the understanding that \/_j A[j] ⊂ B is a sufficient,
-- function, with the understanding that ∃j, A[j] ⊂ B is a sufficient,
-- but not necessary condition (because of 2) and 3)).
--
-- If k ranges over an empty set, we have the isomorphism:
-- (_i ⋂_j A[i,j]) ⊂ ∅ = /\_i (⋂_j A[i,j]) ⊂ ∅
-- (_i ⋂_j A[i,j]) ⊂ ∅ = ∀i, (⋂_j A[i,j]) ⊂ ∅
-- where we again delegate (⋂_j A[j]) ⊂ ∅ to a heuristic, though here the
-- shortcut of \/_j A[j] ⊂ ∅ hardly helps.
-- shortcut of ∃j, A[j] ⊂ ∅ hardly helps.
--
-- Disjunctions tend to erase informative error messages, so we may want
-- to avoid them. This can be formally done as follows: if we can
-- partition the universal set into a disjoint union of some parts:
-- = ⊔_α P[α]
-- such that the conjuncts in our disjunctive normal form are subordinate
-- to the partition:
-- ∀i ∃α, (⋂_j A[i,j]) ⊂ P[α]
-- ∀k ∃α, (⋂_l B[k,l]) ⊂ P[α]
-- then we can partition the sets over which i and k range into partitions
-- I[α] and K[α], and then in each "bucket" verify the inclusion in the
-- aforementioned way:
-- ∀α, (_i∈I[α] ⋂_j A[i,j]) ⊂ (_k∈K[α] ⋂_l B[k,l])
-- = ∀α ∀i∈I[α] ∃k∈K[α] ∀l, (⋂_j A[i,j]) ⊂ B[k,l]
-- We already somewhat do this by partitioning JSON into types, but we can
-- additionally partition e.g. "enum" fields or existence of particular
-- properties. This works especially well if we manage to ensure K[α] are
-- 1-element sets.
--
-- Since the set:
-- (_i∈I[α] ⋂_j A[i,j]) = (_i ⋂_j A[i,j]) ∩ P[α]
-- does not actually appear in the source schema, we need to construct it
-- ourselves and come up with a name for it.
let typesRestricted = not (anyBottomTypes fp) && anyBottomTypes fc
-- Specifically handle the case when a schema's type has been
-- restricted from "all" to specific types: if all types were allowed
@ -851,7 +891,7 @@ checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
when typesRestricted $ issueAt beh $ TypesRestricted $ nonBottomTypes fc
forType_ $ \tyName ty -> do
let beh' = beh >>> step (OfType tyName)
case (ty fp, ty fc) of
case (getJsonFormula $ ty fp, getJsonFormula $ ty fc) of
(DNF pss, BottomFormula) -> unless typesRestricted $ do
-- don't repeat the TypesRestricted issue
F.for_ pss $ \(Conjunct ps) -> checkContradiction beh' ps
@ -862,26 +902,293 @@ checkFormulas env beh (ProdCons (fp, ep) (fc, ec)) =
-- In this case we want to show which restrictions were added. (instead
-- of showing an empty list restrictions that couldn't be satisfied.)
F.for_ css $ \(Conjunct cs) -> F.for_ cs $ checkImplication env beh' []
(DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
anyOfAt
beh'
(issueFromConjunct ps)
[F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
(pss', css') -> F.for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
(mPart, ProdCons pf cf) -> do
let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
case (getJsonFormula pf, getJsonFormula cf) of
(DNF pss, BottomFormula) -> F.for_ pss $ \(Conjunct ps) -> checkContradiction beh'' ps
(DNF pss, SingleConjunct cs) -> F.for_ pss $ \(Conjunct ps) -> do
F.for_ cs $ checkImplication env beh'' ps
-- unlucky:
(DNF pss, DNF css) -> F.for_ pss $ \(Conjunct ps) -> do
anyOfAt
beh'
(issueFromConjunct Nothing ps)
[F.for_ cs $ checkImplication env beh' ps | Conjunct cs <- S.toList css]
pure ()
where
anyBottomTypes f = getAny $
foldType $ \_ ty -> case ty f of
foldType $ \_ ty -> case getJsonFormula $ ty f of
BottomFormula -> Any True
_ -> mempty
nonBottomTypes f = foldType $ \tyName ty -> case ty f of
nonBottomTypes f = foldType $ \tyName ty -> case getJsonFormula $ ty f of
BottomFormula -> mempty
_ -> [tyName]
issueFromConjunct :: Typeable t => [Condition t] -> Issue 'TypedSchemaLevel
issueFromConjunct ps
issueFromConjunct :: Typeable t => Partition -> [Condition t] -> Issue 'TypedSchemaLevel
issueFromConjunct _ ps
| Just e <- findExactly ps
, all (satisfiesTyped e) ps =
EnumDoesntSatisfy $ untypeValue e
issueFromConjunct ps = NoMatchingCondition $ SomeCondition <$> ps
EnumDoesntSatisfy $ untypeValue e -- what does this look like when partitioned?
issueFromConjunct mPart ps = NoMatchingCondition mPart $ SomeCondition <$> ps
data PartitionData
= DByEnumValue (DNF (S.Set A.Value))
| DByProperties (DNF (S.Set Text, S.Set Text)) -- optional, required
deriving stock (Eq, Ord, Show)
data PartitionChoice
= CByEnumValue (S.Set A.Value)
| CByProperties (S.Set Text) (S.Set Text) -- included, excluded
deriving stock (Eq, Ord, Show)
conjPart :: PartitionData -> PartitionData -> Maybe PartitionData
conjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss /\ yss
conjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss /\ yss
conjPart _ _ = Nothing
disjPart :: PartitionData -> PartitionData -> Maybe PartitionData
disjPart (DByEnumValue xss) (DByEnumValue yss) = Just . DByEnumValue $ xss \/ yss
disjPart (DByProperties xss) (DByProperties yss) = Just . DByProperties $ xss \/ yss
disjPart _ _ = Nothing
data PartitionLocation
= PHere
| PInProperty Text PartitionLocation
deriving stock (Eq, Ord, Show)
newtype Partitions = Partitions (M.Map PartitionLocation (S.Set PartitionData))
deriving stock (Eq, Ord, Show)
instance Lattice Partitions where
Partitions xss /\ Partitions yss = Partitions $ M.unionWith conj xss yss
where
conj xs ys = S.fromList . catMaybes $ liftA2 conjPart (S.toList xs) (S.toList ys)
Partitions xss \/ Partitions yss = Partitions $ M.intersectionWith disj xss yss
where
disj xs ys = S.fromList . catMaybes $ liftA2 disjPart (S.toList xs) (S.toList ys)
instance BoundedMeetSemiLattice Partitions where
top = Partitions M.empty
-- The lattice has no bottom, but we use 'Lifted' to adjoin a free bottom element
type PartitionM = ReaderT (Traced (Definitions Schema)) (State (MemoState ()))
ignoreKnot :: KnotTier (Lifted Partitions) () PartitionM
ignoreKnot =
KnotTier
{ onKnotFound = pure ()
, onKnotUsed = \_ -> pure bottom
, tieKnot = \_ -> pure
}
singletonPart :: PartitionData -> Lifted Partitions
singletonPart = Lift . Partitions . M.singleton PHere . S.singleton
partitionSchema :: Traced Schema -> PartitionM (Lifted Partitions)
partitionSchema sch = do
allClauses <- case tracedAllOf sch of
Nothing -> pure []
Just xs -> mapM partitionRefSchema xs
anyClause <- case tracedAnyOf sch of
Nothing -> pure top
Just xs -> joins <$> mapM partitionRefSchema xs
oneClause <- case tracedOneOf sch of
Nothing -> pure top
Just xs -> joins <$> mapM partitionRefSchema xs
byEnumClause <- case _schemaEnum $ extract sch of
Nothing -> pure top
Just xs ->
pure . singletonPart $
DByEnumValue $ SingleConjunct [S.fromList xs]
-- We can only partition by presence of a property if additional properties
-- are disallowed, and the property is not optional
let reqd = S.fromList $ _schemaRequired $ extract sch
byPropertiesClause <- case _schemaAdditionalProperties $ extract sch of
Just (AdditionalPropertiesAllowed False) -> do
let props = S.fromList . IOHM.keys . _schemaProperties $ extract sch
pure . singletonPart $
DByProperties $ SingleConjunct [(props S.\\ reqd, props `S.intersection` reqd)]
_ -> pure top
-- We can partition on something nested in a property only if the property is
-- required
let reqdProps = IOHM.filterWithKey (\k _ -> k `S.member` reqd) $ tracedProperties sch
inPropertiesClauses <- forM (IOHM.toList reqdProps) $ \(k, rs) -> do
f <- partitionRefSchema rs
pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
pure $ meets $ allClauses <> [anyClause, oneClause, byEnumClause, byPropertiesClause] <> inPropertiesClauses
partitionRefSchema :: Traced (Referenced Schema) -> PartitionM (Lifted Partitions)
partitionRefSchema x = do
defs <- R.ask
memoWithKnot ignoreKnot (partitionSchema $ dereference defs x) (ask x)
newtype LiftA f a = LiftA {getLiftA :: f a}
deriving newtype (Functor, Applicative)
instance (Lattice a, Applicative f) => Lattice (LiftA f a) where
(/\) = liftA2 (/\)
(\/) = liftA2 (\/)
instance (BoundedJoinSemiLattice a, Applicative f) => BoundedJoinSemiLattice (LiftA f a) where
bottom = pure bottom
instance (BoundedMeetSemiLattice a, Applicative f) => BoundedMeetSemiLattice (LiftA f a) where
top = pure top
partitionCondition :: Condition t -> PartitionM (Lifted Partitions)
partitionCondition = \case
Exactly x ->
pure . singletonPart $
DByEnumValue $ SingleConjunct [S.singleton $ untypeValue x]
Properties props _ madd -> do
let byProps = case madd of
Just _ -> top
Nothing ->
singletonPart $
DByProperties $
SingleConjunct
[ ( M.keysSet $ M.filter (not . propRequired) props
, M.keysSet $ M.filter propRequired props
)
]
inProps <- forM (M.toList $ M.filter propRequired props) $ \(k, prop) -> do
f <- partitionRefSchema $ propRefSchema prop
pure $ fmap (\(Partitions m) -> Partitions $ M.mapKeysMonotonic (PInProperty k) m) f
pure $ byProps /\ meets inProps
_ -> pure top
partitionJsonFormulas
:: ProdCons (Traced (Definitions Schema))
-> ProdCons (JsonFormula t)
-> Lifted Partitions
partitionJsonFormulas defs pc = producer pcPart \/ consumer pcPart
where
pcPart = partitionFormula <$> defs <*> pc
partitionFormula def (JsonFormula xss) = runIdentity . runMemo () . (`runReaderT` def) $ do
getLiftA . foldLattice (LiftA . partitionCondition) $ xss
selectPartition :: Lifted Partitions -> Maybe (PartitionLocation, S.Set PartitionChoice)
selectPartition Bottom = Nothing
selectPartition (Lift (Partitions m)) =
go [(loc, part) | (loc, parts) <- sortBy (comparing $ locLength . fst) $ M.toList m, part <- S.toList parts]
where
locLength :: PartitionLocation -> Int
locLength = walk 0
where
walk !n PHere = n
walk !n (PInProperty _ l) = walk (n + 1) l
go [] = Nothing
-- Skip partitioning by property for now
go ((_, DByProperties _) : ps) = go ps
-- Don't partition by enum value at the root (this reports removed enum values as contradictions in their respective partitions)
go ((PHere, DByEnumValue _) : ps) = go ps
go ((loc, DByEnumValue (DNF xss)) : ps)
-- Check that no disjunction branches are unresticted
| Just enums <- traverse (fmap (foldr1 S.intersection) . NE.nonEmpty . S.toList) . S.toList $ xss =
-- TODO: improve
Just (loc, S.map (CByEnumValue . S.singleton) $ S.unions enums)
| otherwise = go ps
-- This essentially has 3 cases:
-- Nothing -- we have produced a bottom schema
-- Just (False, _) -- there's been no change to the schema
-- Just (True, x) -- x is a new schema
type IntersectionM = ReaderT (Traced (Definitions Schema)) (WriterT Any Maybe)
mBottom :: IntersectionM a
mBottom = lift . lift $ Nothing
catchBottom :: IntersectionM a -> IntersectionM a -> IntersectionM a
catchBottom act handler = R.liftCatch (W.liftCatch (\a h -> a <|> h ())) act (\_ -> handler)
mChange :: IntersectionM ()
mChange = tell $ Any True
intersectSchema
:: PartitionLocation
-> PartitionChoice
-> Traced Schema
-> IntersectionM Schema
intersectSchema loc part sch = do
allOf' <- forM (tracedAllOf sch) $ \rss ->
-- Assuming i ranges over a nonempty set (checked in processSchema)
-- (⋂_i A[i]) ∩ X = ⋂_i (A[i] ∩ X)
-- If any intersections are empty, the result is empty. If any intersections are a change, the result is a change.
traverse (intersectRefSchema loc part) rss
anyOf' <- forM (tracedAnyOf sch) $ \rss -> do
-- (_i A[i]) ∩ X = _i (A[i] ∩ X)
-- Collect only the nonempty A[i] ∩ X, unless there are none, in which case the result is empty.
-- If any schema is empty, we remove it from the list which constitutes a change.
mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
case catMaybes mSchemas of
[] -> mBottom
schs -> pure schs
oneOf' <- forM (tracedOneOf sch) $ \rss -> do
-- Same as anyOf'. By intersecting we're only making them more disjoint if anything.
mSchemas <- forM rss $ \rs -> catchBottom (Just <$> intersectRefSchema loc part rs) (mChange >> pure Nothing)
case catMaybes mSchemas of
[] -> mBottom
schs -> pure schs
let sch' = (extract sch) {_schemaAllOf = allOf', _schemaAnyOf = anyOf', _schemaOneOf = oneOf'}
-- Now the local changes:
case loc of
PInProperty k loc' -> case IOHM.lookup k $ tracedProperties sch of
Nothing -> error $ "Partitioning via absent property: " <> T.unpack k
Just prop -> do
prop' <- intersectRefSchema loc' part prop
pure $ sch' {_schemaProperties = IOHM.adjust (const prop') k $ _schemaProperties sch'}
PHere -> case part of
CByEnumValue vals -> do
enum' <- case _schemaEnum sch' of
Nothing -> do
mChange
pure $ S.toList vals
Just xs -> do
when (any (`S.notMember` vals) xs) mChange
case filter (`S.member` vals) xs of
[] -> mBottom
xs' -> pure xs'
pure $ sch' {_schemaEnum = Just enum'}
CByProperties {} -> error "CByProperties not implemented"
intersectRefSchema
:: PartitionLocation
-> PartitionChoice
-> Traced (Referenced Schema)
-> IntersectionM (Referenced Schema)
intersectRefSchema loc part rs = do
defs <- R.ask
Inline <$> intersectSchema loc part (dereference defs rs)
intersectCondition :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> Condition t -> DNF (Condition t)
intersectCondition _defs PHere (CByEnumValue values) cond@(Exactly x) =
if untypeValue x `S.member` values then SingleConjunct [cond] else bottom
intersectCondition defs (PInProperty k loc) part cond@(Properties props add madd) = case M.lookup k props of
Nothing -> SingleConjunct [cond] -- shouldn't happen
Just prop -> case runWriterT . (`runReaderT` defs) $ intersectRefSchema loc part $ propRefSchema prop of
Just (rs', Any True) ->
let trs' = traced (ask (propRefSchema prop) >>> step (Partitioned loc part)) rs'
in SingleConjunct [Properties (M.insert k prop {propRefSchema = trs'} props) add madd]
Just (_, Any False) -> SingleConjunct [cond]
Nothing -> bottom
intersectCondition _defs _loc _part cond = SingleConjunct [cond]
intersectFormula :: Traced (Definitions Schema) -> PartitionLocation -> PartitionChoice -> JsonFormula t -> JsonFormula t
intersectFormula defs loc part = JsonFormula . foldLattice (intersectCondition defs loc part) . getJsonFormula
type Partition = Maybe (PartitionLocation, PartitionChoice)
tryPartition :: ProdCons (Traced (Definitions Schema)) -> ProdCons (JsonFormula t) -> [(Partition, ProdCons (JsonFormula t))]
tryPartition defs pc = case selectPartition $ partitionJsonFormulas defs pc of
Nothing -> [(Nothing, pc)]
Just (loc, parts) -> [(Just (loc, part), intersectFormula <$> defs <*> pure loc <*> pure part <*> pc) | part <- S.toList parts]
checkContradiction
:: Behavior 'TypedSchemaLevel
@ -889,6 +1196,13 @@ checkContradiction
-> SemanticCompatFormula ()
checkContradiction beh _ = issueAt beh NoContradiction -- TODO #70
tracedConjunct :: NE.NonEmpty (Traced (Referenced Schema)) -> Traced (Referenced Schema)
tracedConjunct refSchemas = case NE.sortWith ask refSchemas of
(rs NE.:| []) -> rs
(rs1 NE.:| rs2 : rss) ->
traced (ask rs1 >>> step (ConjunctedWith $ ask <$> rs2 NE.:| rss)) $
Inline mempty {_schemaAllOf = Just $ extract <$> rs1 : rs2 : rss}
checkImplication
:: (ReassembleHList xs (CheckEnv (Referenced Schema)))
=> HList xs
@ -949,10 +1263,7 @@ checkImplication env beh prods cons = case findExactly prods of
then pure ()
else issueAt beh (NoMatchingFormat f)
Items _ cons' -> case findRelevant (<>) (\case Items _ rs -> Just (rs NE.:| []); _ -> Nothing) prods of
Just (rs NE.:| []) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
Just rs -> do
let sch = Inline mempty {_schemaAllOf = Just . NE.toList $ extract <$> rs}
checkCompatibility (beh >>> step InItems) env $ ProdCons (traced (ask $ NE.head rs) sch) cons' -- TODO: bad trace
Just (tracedConjunct -> rs) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
Nothing -> issueAt beh NoMatchingItems
MaxItems m -> case findRelevant min (\case MaxItems m' -> Just m'; _ -> Nothing) prods of
Just m' ->
@ -1079,7 +1390,7 @@ instance Issuable 'TypedSchemaLevel where
| -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
MatchingMinPropertiesWeak (ProdCons Integer)
| -- | 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)
NoMatchingCondition [SomeCondition]
NoMatchingCondition Partition [SomeCondition]
| -- | 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)
-- AKA consumer does not have the type
NoContradiction
@ -1128,15 +1439,23 @@ instance Issuable 'TypedSchemaLevel where
describeIssue Forward (NoMatchingMinProperties n) = para $ "Minimum number of properties added: " <> show' n <> "."
describeIssue Backward (NoMatchingMinProperties n) = para $ "Minimum number of properties removed: " <> show' n <> "."
describeIssue _ (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Minimum number of properties has changed from " <> show' p <> " to " <> show' c <> "."
describeIssue _ (NoMatchingCondition conds) =
para "Expected the following conditions to hold, but they didn't (please file a bug if you see this):"
describeIssue _ (NoMatchingCondition mPart conds) =
para
(case mPart of
Nothing -> "Could not verify that the following conditions hold (please file a bug if you see this)"
Just locPart ->
showPartition locPart
<> " could not verify that the following conditions hold (please file a bug if you see this):")
<> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
describeIssue Forward NoContradiction = para "The type has been removed."
describeIssue Backward NoContradiction = para "The type has been added."
describeIssue Forward NoContradiction = para "The value has been removed."
describeIssue Backward NoContradiction = para "The value has been added."
showJSONValue :: A.Value -> Blocks
showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
showJSONValueInline :: A.Value -> Inlines
showJSONValueInline v = code (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
showBound :: Show a => Bound a -> Inlines
showBound (Inclusive x) = show' x <> " inclusive"
showBound (Exclusive x) = show' x <> " exclusive"
@ -1206,6 +1525,40 @@ describeJSONType = \case
Array -> "Array"
Object -> "Object"
instance Behavable 'TypedSchemaLevel 'TypedSchemaLevel where
data Behave 'TypedSchemaLevel 'TypedSchemaLevel
= InPartition (PartitionLocation, PartitionChoice)
deriving stock (Eq, Ord, Show)
describeBehaviour (InPartition partition) = showPartition partition
showPartition :: (PartitionLocation, PartitionChoice) -> Inlines
showPartition = \case
(partition, CByEnumValue (S.toList -> [v])) ->
"In cases where " <> renderPartitionLocation partition <> " is " <> showJSONValueInline v
(partition, CByEnumValue (S.toList -> vs)) ->
"In cases where " <> renderPartitionLocation partition <> " has values: "
<> (fold . L.intersperse ", " . fmap showJSONValueInline $ vs)
(partition, CByProperties (S.toList -> incl) (S.toList -> [])) ->
"In cases where " <> renderPartitionLocation partition <> " contains the properties: " <> listCodes incl
(partition, CByProperties (S.toList -> []) (S.toList -> excl)) ->
"In cases where " <> renderPartitionLocation partition <> " does not contain the properties: " <> listCodes excl
(partition, CByProperties (S.toList -> incl) (S.toList -> excl)) ->
"In cases where " <> renderPartitionLocation partition
<> " contains the properties "
<> listCodes incl
<> " and does not contain the properties "
<> listCodes excl
where
listCodes :: [Text] -> Inlines
listCodes = fold . L.intersperse ", " . fmap code
renderPartitionLocation :: PartitionLocation -> Inlines
renderPartitionLocation p = code $ "$" <> renderPartitionLocation' p
where
renderPartitionLocation' :: PartitionLocation -> Text
renderPartitionLocation' PHere = mempty
renderPartitionLocation' (PInProperty prop rest) = "." <> prop <> renderPartitionLocation' rest
instance Behavable 'TypedSchemaLevel 'SchemaLevel where
data Behave 'TypedSchemaLevel 'SchemaLevel
= InItems
@ -1271,4 +1624,4 @@ instance Subtree Schema where
structuralItems _ = structuralIssue
checkSemanticCompatibility env beh schs = do
let defs = getH env
checkFormulas env beh $ schemaToFormula <$> defs <*> schs
checkFormulas env beh defs $ schemaToFormula <$> defs <*> schs

View File

@ -1,4 +1,4 @@
resolver: lts-18.0
resolver: lts-18.2
extra-deps:
- open-union-0.4.0.0

View File

@ -20,7 +20,7 @@ packages:
hackage: type-fun-0.1.3
snapshots:
- completed:
size: 585393
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/0.yaml
sha256: c632012da648385b9fa3c29f4e0afd56ead299f1c5528ee789058be410e883c0
original: lts-18.0
size: 585392
url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/18/2.yaml
sha256: 7abb45c0cc5eb349448b66d8753655542d45d387ad26970419282eab3d860724
original: lts-18.2

View File

@ -0,0 +1,25 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
enum: ["A", "B"]

View File

@ -0,0 +1,25 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
enum: ["A", "B", "C"]

View File

@ -0,0 +1,33 @@
# Summary
| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
|------------------------------------------|-------------------------------------------------|-------------------------------|
| 1 | 1 | 0 |
# <span id="breaking-changes"></span>⚠️ Breaking changes
## **POST** /test
### 📱⬅️ JSON Response 200
#### `$(String)`
The following enum value was added:
``` json
"C"
```
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
## **POST** /test
### 📱➡️ JSON Request
#### `$(String)`
The following enum value was added:
``` json
"C"
```

View File

@ -0,0 +1,14 @@
breakingChanges:
AtPath "/test":
InOperation PostMethod:
WithStatusCode 200:
ResponsePayload:
PayloadSchema:
OfType String: EnumDoesntSatisfy (String "C")
nonBreakingChanges:
AtPath "/test":
InOperation PostMethod:
InRequest:
InPayload:
PayloadSchema:
OfType String: EnumDoesntSatisfy (String "C")

View File

@ -14,7 +14,7 @@
##### `$(String)`
The type has been removed.
The value has been removed.
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
@ -26,4 +26,4 @@ The type has been removed.
##### `$(Number)`
The type has been added.
The value has been added.

View File

@ -12,7 +12,7 @@
#### `$(String)`
The type has been removed.
The value has been removed.
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
@ -22,4 +22,4 @@ The type has been removed.
#### `$(Number)`
The type has been added.
The value has been added.

View File

@ -12,7 +12,7 @@
#### `$(Number)`
The type has been added.
The value has been added.
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
@ -22,4 +22,4 @@ The type has been added.
#### `$(String)`
The type has been removed.
The value has been removed.

View File

@ -32,7 +32,7 @@ Values are now limited to the following types:
#### `$(Array)`
The type has been removed.
The value has been removed.
## **POST** /test4
@ -40,7 +40,7 @@ The type has been removed.
#### `$(Object)`
The type has been removed.
The value has been removed.
## **POST** /test5
@ -82,7 +82,7 @@ Values are now limited to the following types:
#### `$(Array)`
The type has been removed.
The value has been removed.
## **POST** /test4
@ -90,7 +90,7 @@ The type has been removed.
#### `$(Object)`
The type has been removed.
The value has been removed.
## **POST** /test5

View File

@ -0,0 +1,39 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
oneOf:
- type: object
required: ["tag", "prop_A"]
properties:
tag:
enum: ["A"]
prop_A:
type: string
- type: object
required: ["tag", "prop_B"]
properties:
tag:
enum: ["B"]
prop_B:
type: number

View File

@ -0,0 +1,46 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
oneOf:
- type: object
required: ["tag", "prop_A"]
properties:
tag:
enum: ["A"]
prop_A:
type: string
- type: object
required: ["tag", "prop_B"]
properties:
tag:
enum: ["B"]
prop_B:
type: number
- type: object
required: ["tag", "prop_C"]
properties:
tag:
enum: ["C"]
prop_C:
type: number

View File

@ -0,0 +1,25 @@
# Summary
| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
|------------------------------------------|-------------------------------------------------|-------------------------------|
| 1 | 1 | 0 |
# <span id="breaking-changes"></span>⚠️ Breaking changes
## **POST** /test
### 📱⬅️ JSON Response 200
#### In cases where `$.tag` is `"C"`
The value has been added.
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
## **POST** /test
### 📱➡️ JSON Request
#### In cases where `$.tag` is `"C"`
The value has been added.

View File

@ -0,0 +1,16 @@
breakingChanges:
AtPath "/test":
InOperation PostMethod:
WithStatusCode 200:
ResponsePayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction
nonBreakingChanges:
AtPath "/test":
InOperation PostMethod:
InRequest:
InPayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "C"])): NoContradiction

View File

@ -0,0 +1,39 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
oneOf:
- type: object
required: ["tag", "prop_A"]
properties:
tag:
enum: ["A"]
prop_A:
type: string
- type: object
required: ["tag", "prop_B"]
properties:
tag:
enum: ["B"]
prop_B:
type: number

View File

@ -0,0 +1,39 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
oneOf:
- type: object
required: ["tag", "prop_A"]
properties:
tag:
enum: ["A"]
prop_A:
type: string
- type: object
required: ["tag", "prop_B"]
properties:
tag:
enum: ["B"]
prop_B:
type: string

View File

@ -0,0 +1,45 @@
# Summary
| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
|------------------------------------------|-------------------------------------------------|-------------------------------|
| 2 | 2 | 0 |
# <span id="breaking-changes"></span>⚠️ Breaking changes
## **POST** /test
### 📱➡️ JSON Request
#### In cases where `$.tag` is `"B"`
##### `$.prop_B(Number)`
The value has been removed.
### 📱⬅️ JSON Response 200
#### In cases where `$.tag` is `"B"`
##### `$.prop_B(String)`
The value has been added.
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
## **POST** /test
### 📱➡️ JSON Request
#### In cases where `$.tag` is `"B"`
##### `$.prop_B(String)`
The value has been added.
### 📱⬅️ JSON Response 200
#### In cases where `$.tag` is `"B"`
##### `$.prop_B(Number)`
The value has been removed.

View File

@ -0,0 +1,34 @@
breakingChanges:
AtPath "/test":
InOperation PostMethod:
InRequest:
InPayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType Number: NoContradiction
WithStatusCode 200:
ResponsePayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType String: NoContradiction
nonBreakingChanges:
AtPath "/test":
InOperation PostMethod:
InRequest:
InPayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType String: NoContradiction
WithStatusCode 200:
ResponsePayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "tag" PHere,CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType Number: NoContradiction

View File

@ -0,0 +1,47 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
oneOf:
- type: object
required: ["desc", "prop_A"]
properties:
desc:
type: object
required: ["name"]
properties:
name:
enum: ["A"]
prop_A:
type: string
- type: object
required: ["desc", "prop_B"]
properties:
desc:
type: object
required: ["name"]
properties:
name:
enum: ["B"]
prop_B:
type: number

View File

@ -0,0 +1,47 @@
openapi: "3.0.0"
info:
version: 1.0.0
title: Test
servers:
- url: http://localhost/
paths:
/test:
post:
requestBody:
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
responses:
'200':
description: test
content:
application/json:
schema:
$ref: "#/components/schemas/Test"
components:
schemas:
Test:
oneOf:
- type: object
required: ["desc", "prop_A"]
properties:
desc:
type: object
required: ["name"]
properties:
name:
enum: ["A"]
prop_A:
type: string
- type: object
required: ["desc", "prop_B"]
properties:
desc:
type: object
required: ["name"]
properties:
name:
enum: ["B"]
prop_B:
type: string

View File

@ -0,0 +1,45 @@
# Summary
| [⚠️ Breaking changes](#breaking-changes) | [🙆 Non-breaking changes](#non-breaking-changes) | 🤷 Unsupported feature changes |
|------------------------------------------|-------------------------------------------------|-------------------------------|
| 2 | 2 | 0 |
# <span id="breaking-changes"></span>⚠️ Breaking changes
## **POST** /test
### 📱➡️ JSON Request
#### In cases where `$.desc.name` is `"B"`
##### `$.prop_B(Number)`
The value has been removed.
### 📱⬅️ JSON Response 200
#### In cases where `$.desc.name` is `"B"`
##### `$.prop_B(String)`
The value has been added.
# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
## **POST** /test
### 📱➡️ JSON Request
#### In cases where `$.desc.name` is `"B"`
##### `$.prop_B(String)`
The value has been added.
### 📱⬅️ JSON Response 200
#### In cases where `$.desc.name` is `"B"`
##### `$.prop_B(Number)`
The value has been removed.

View File

@ -0,0 +1,34 @@
breakingChanges:
AtPath "/test":
InOperation PostMethod:
InRequest:
InPayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType Number: NoContradiction
WithStatusCode 200:
ResponsePayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType String: NoContradiction
nonBreakingChanges:
AtPath "/test":
InOperation PostMethod:
InRequest:
InPayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType String: NoContradiction
WithStatusCode 200:
ResponsePayload:
PayloadSchema:
OfType Object:
InPartition (PInProperty "desc" (PInProperty "name" PHere),CByEnumValue (fromList [String "B"])):
InProperty "prop_B":
OfType Number: NoContradiction