@@ -7,15 +7,17 @@ where
7import Control.Monad.Writer
7import Control.Monad.Writer
8import qualified Data.Aeson as A
8import qualified Data.Aeson as A
9import Data.Coerce
9import Data.Coerce
10−import Data.Foldable (for_)
10+import Data.Foldable (for_, toList)
11import Data.Functor
11import Data.Functor
12import Data.HList
12import Data.HList
13import qualified Data.List.NonEmpty as NE
13import qualified Data.List.NonEmpty as NE
14import qualified Data.Map as M
14import qualified Data.Map as M
15+import Data.List (group, genericLength, genericIndex)
15import Data.Maybe
16import Data.Maybe
16import Data.OpenApi
17import Data.OpenApi
17import Data.Ord
18import Data.Ord
18import Data.Ratio
19import Data.Ratio
19import qualified Data.Set as S
21import qualified Data.Set as S
20import Data.Text (Text)
22import Data.Text (Text)
21import OpenAPI.Checker.Behavior
23import OpenAPI.Checker.Behavior
@@ -34,10 +36,11 @@ checkFormulas
34 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
36 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
35 => HList xs
37 => HList xs
36 -> Behavior 'SchemaLevel
38 -> Behavior 'SchemaLevel
39+ -> ProdCons (Trace Schema)
37 -> ProdCons (Traced (Definitions Schema))
40 -> ProdCons (Traced (Definitions Schema))
38 -> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
41 -> ProdCons (ForeachType JsonFormula, P.PathsPrefixTree Behave AnIssue 'SchemaLevel)
39 -> SemanticCompatFormula ()
42 -> SemanticCompatFormula ()
40−checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
43+checkFormulas env beh trs defs (ProdCons (fp, ep) (fc, ec)) =
41 case P.toList ep ++ P.toList ec of
44 case P.toList ep ++ P.toList ec of
42 issues@(_ : _) -> for_ issues $ embedFormula beh . anItem
45 issues@(_ : _) -> for_ issues $ embedFormula beh . anItem
@@ -105,25 +108,25 @@ checkFormulas env beh defs (ProdCons (fp, ep) (fc, ec)) =
105 -- don't repeat the TypesRestricted issue
108 -- don't repeat the TypesRestricted issue
106 for_ pss $ \(Disjunct ps) -> checkContradiction beh' Nothing ps
109 for_ pss $ \(Disjunct ps) -> checkContradiction beh' Nothing ps
107 (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
110 (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
108− for_ cs $ checkImplication env beh' ps -- avoid disjunction if there's only one conjunct
111+ for_ cs $ checkImplication env beh' trs ps -- avoid disjunction if there's only one conjunct
109 (TopDNF, DNF css) ->
112 (TopDNF, DNF css) ->
110 -- producer is "open" (allows any value), but consumer has restrictions.
113 -- producer is "open" (allows any value), but consumer has restrictions.
111 -- In this case we want to show which restrictions were added. (instead
114 -- In this case we want to show which restrictions were added. (instead
112 -- of showing an empty list restrictions that couldn't be satisfied.)
115 -- of showing an empty list restrictions that couldn't be satisfied.)
113− for_ css $ \(Disjunct cs) -> for_ cs $ checkImplication env beh' S.empty
116+ for_ css $ \(Disjunct cs) -> for_ cs $ checkImplication env beh' trs S.empty
114 (pss', css') -> for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
117 (pss', css') -> for_ (tryPartition defs $ ProdCons (JsonFormula pss') (JsonFormula css')) $ \case
115 (mPart, ProdCons pf cf) -> do
118 (mPart, ProdCons pf cf) -> do
116 let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
119 let beh'' = foldr ((<<<) . step . InPartition) beh' mPart
117 case (getJsonFormula pf, getJsonFormula cf) of
120 case (getJsonFormula pf, getJsonFormula cf) of
118 (DNF pss, BottomDNF) -> for_ pss $ \(Disjunct ps) -> checkContradiction beh' mPart ps
121 (DNF pss, BottomDNF) -> for_ pss $ \(Disjunct ps) -> checkContradiction beh' mPart ps
119 (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
122 (DNF pss, SingleDisjunct (Disjunct cs)) -> for_ pss $ \(Disjunct ps) -> do
120− for_ cs $ checkImplication env beh'' ps
123+ for_ cs $ checkImplication env beh'' trs ps
121 -- unlucky:
124 -- unlucky:
122 (DNF pss, DNF css) -> for_ pss $ \(Disjunct ps) -> do
125 (DNF pss, DNF css) -> for_ pss $ \(Disjunct ps) -> do
125 (issueFromDisjunct Nothing ps)
128 (issueFromDisjunct Nothing ps)
126− [for_ cs $ checkImplication env beh' ps | Disjunct cs <- S.toList css]
129+ [for_ cs $ checkImplication env beh' trs ps | Disjunct cs <- S.toList css]
129 anyBottomTypes f = getAny $
132 anyBottomTypes f = getAny $
@@ -151,10 +154,11 @@ checkImplication
151 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
154 :: (ReassembleHList xs (CheckEnv (Referenced Schema)))
152 => HList xs
155 => HList xs
153 -> Behavior 'TypedSchemaLevel
156 -> Behavior 'TypedSchemaLevel
157+ -> ProdCons (Trace Schema) -- the traces of the root schemas used in this comparison
154 -> S.Set (Condition t)
158 -> S.Set (Condition t)
155 -> Condition t
159 -> Condition t
156 -> SemanticCompatFormula ()
160 -> SemanticCompatFormula ()
157−checkImplication env beh prods cons = case findExactly prods of
161+checkImplication env beh trs prods cons = case findExactly prods of
159 | all (satisfiesTyped e) prods ->
163 | all (satisfiesTyped e) prods ->
160 if satisfiesTyped e cons
164 if satisfiesTyped e cons
@@ -208,24 +212,69 @@ checkImplication env beh prods cons = case findExactly prods of
208 False -> issueAt beh (NoMatchingFormat f)
212 False -> issueAt beh (NoMatchingFormat f)
210 Items _ cons' -> case foldSome (<>) prods $ \case
214 Items _ cons' -> case foldSome (<>) prods $ \case
211− Items _ rs -> Just (rs NE.:| [])
215+ Items _ rs -> Just (Just (rs NE.:| []), mempty)
216+ TupleItems (map snd -> fs) -> Just (mempty, Just (fs NE.:| []))
212 _ -> Nothing
217 _ -> Nothing
219+ Just (mItems, Just pfs)
220+ | not $ allSame (length <$> pfs) -> pure () -- vacuously
221+ | let plen = genericLength (NE.head pfs)
222+ -> clarifyIssue (AnItem beh (anIssue TupleToArray)) $ for_ [0 .. plen - 1] $ \i -> do
223+ let prod' = tracedConjunct $ case mItems of
224+ Just prods' -> ((`genericIndex` i) <$> pfs) <> prods'
225+ Nothing -> (`genericIndex` i) <$> pfs
226+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons prod' cons'
227+ Just (Just prods', Nothing) -> do
228+ let prod' = tracedConjunct prods'
214− Just (tracedConjunct -> rs) -> checkCompatibility (beh >>> step InItems) env $ ProdCons rs cons'
229+ checkCompatibility (beh >>> step InItems) env $ ProdCons prod' cons'
215− Nothing -> issueAt beh NoMatchingItems
230+ _ -> clarifyIssue (AnItem beh (anIssue NoMatchingItems)) $ do
231+ checkCompatibility (beh >>> step InItems) env $ ProdCons prodTopSchema cons'
233+ TupleItems (map snd -> fs) -> case foldSome (<>) prods $ \case
234+ TupleItems (map snd -> fs') -> Just (Just $ fs' NE.:| [], Just . Max $ genericLength fs', Just . Min $ genericLength fs', mempty)
235+ MinItems m' -> Just (mempty, Just . Max $ m', mempty, mempty)
236+ MaxItems m' -> Just (mempty, mempty, Just . Min $ m', mempty)
237+ Items _ rs -> Just (mempty, mempty, mempty, Just (rs NE.:| []))
240+ -- if the length constraints in the producer are contradictory:
241+ Just (_, Just (Max lowest), Just (Min highest), _) | lowest > highest -> pure ()
242+ -- We have an explicit tuple items clause...
243+ Just (Just pfs, Just (Max plen), _, _)
244+ | plen /= genericLength fs -- ...of wrong length
245+ -> issueAt beh (TupleItemsLengthChanged ProdCons {producer = plen, consumer = genericLength fs})
247+ -> for_ [0 .. plen - 1] $ \i -> do
248+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons (tracedConjunct $ (`genericIndex` i) <$> pfs) (fs `genericIndex` i)
249+ -- We have a fixed length array in the producer...
250+ Just (Nothing, Just (Max plen), Just (Min plen'), mProd)
252+ -> clarifyIssue (AnItem beh (anIssue ArrayToTuple)) $ case mProd of
253+ _ | plen /= genericLength fs -> -- ...of wrong length
254+ issueAt beh (TupleItemsLengthChanged ProdCons {producer = plen, consumer = genericLength fs})
255+ Just rs -> for_ [0 .. plen - 1] $ \i -> do
256+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons (tracedConjunct rs) (fs `genericIndex` i)
257+ -- ...and no "items" schema
258+ Nothing -> clarifyIssue (AnItem beh (anIssue NoMatchingTupleItems)) $ do
259+ for_ [0 .. plen - 1] $ \i -> do
260+ checkCompatibility (beh >>> step (InItem i)) env $ ProdCons prodTopSchema (fs `genericIndex` i)
261+ _ -> issueAt beh NoMatchingTupleItems
217 MaxItems m -> foldCheck min m NoMatchingMaxItems MatchingMaxItemsWeak $ \case
263 MaxItems m -> foldCheck min m NoMatchingMaxItems MatchingMaxItemsWeak $ \case
218 MaxItems m' -> Just m'
264 MaxItems m' -> Just m'
265+ TupleItems fs -> Just $ toInteger $ length fs
219 _ -> Nothing
266 _ -> Nothing
221 MinItems m -> foldCheck max m NoMatchingMinItems MatchingMinItemsWeak $ \case
268 MinItems m -> foldCheck max m NoMatchingMinItems MatchingMinItemsWeak $ \case
222 MinItems m' -> Just m'
269 MinItems m' -> Just m'
270+ TupleItems fs -> Just $ toInteger $ length fs
223− _ -> Nothing -- TODO: tuple items
271+ _ -> Nothing
225 UniqueItems -> case flip any prods $ \case
273 UniqueItems -> case flip any prods $ \case
226 UniqueItems -> True
274 UniqueItems -> True
227 MaxItems 1 -> True
275 MaxItems 1 -> True
276+ TupleItems fs | length fs == 1 -> True
228− _ -> False -- TODO: tuple items
277+ _ -> False
230 True -> pure ()
279 True -> pure ()
231 False -> issueAt beh NoMatchingUniqueItems
280 False -> issueAt beh NoMatchingUniqueItems
@@ -290,6 +339,14 @@ checkImplication env beh prods cons = case findExactly prods of
290 | otherwise -> issueAt beh (weak ProdCons {producer = m', consumer = m})
339 | otherwise -> issueAt beh (weak ProdCons {producer = m', consumer = m})
291 Nothing -> issueAt beh (missing m)
340 Nothing -> issueAt beh (missing m)
342+ prodTopSchema = traced (producer trs >>> step ImplicitTopSchema) $ Inline mempty
344+allSame :: (Foldable f, Eq a) => f a -> Bool
345+allSame xs = case group (toList xs) of
293foldSome :: (b -> b -> b) -> S.Set a -> (a -> Maybe b) -> Maybe b
350foldSome :: (b -> b -> b) -> S.Set a -> (a -> Maybe b) -> Maybe b
294foldSome combine xs extr =
351foldSome combine xs extr =
295 fmap (foldr1 combine) . NE.nonEmpty . mapMaybe extr . S.toList $ xs
352 fmap (foldr1 combine) . NE.nonEmpty . mapMaybe extr . S.toList $ xs
@@ -353,7 +410,7 @@ instance Subtree Schema where
353 structuralItems _ = structuralIssue
410 structuralItems _ = structuralIssue
354 checkSemanticCompatibility env beh schs = do
411 checkSemanticCompatibility env beh schs = do
355 let defs = getH env
412 let defs = getH env
356− checkFormulas env beh defs $ schemaToFormula <$> defs <*> schs
413+ checkFormulas env beh (ask <$> schs) defs $ schemaToFormula <$> defs <*> schs
358parseDiscriminatorValue :: Text -> Referenced Schema
415parseDiscriminatorValue :: Text -> Referenced Schema
359parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$ref" A..= v] of
416parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$ref" A..= v] of