git.iko.soy/typeable/compaREST
on master help sign in
Added report generation (#73)

* first stab * Made things compile * Fixed headers * Implemented jets * Added method-path jet * Added yet more jets * Rerun tests * Updated formatting * Added some issue descriptions * resolved producer/consumer mixup in schema * Added UnguardedRecursion descriptions * Added condition formatting * Added jet docs * Added different format outputs

commit218336b11ebaded48b5c1f0b654d5f21dbbe8c3e authoriko <ilyakooo0@gmail.com> dateSat 5 Jun 2021, 19:15 (5 years ago) parentf39734f
76 files changed+884−268
app/FormatHeuristic.hs+69 app/Main.hs+59−3 openapi-diff.cabal+13−2 src/OpenAPI/Checker/Behavior.hs+6−4 src/OpenAPI/Checker/Paths.hs+5−1 src/OpenAPI/Checker/PathsPrefixTree.hs+20−4 src/OpenAPI/Checker/Report.hs+136 src/OpenAPI/Checker/Run.hs−40 src/OpenAPI/Checker/Subtree.hs+13−6 src/OpenAPI/Checker/Validate/Header.hs+5 src/OpenAPI/Checker/Validate/Link.hs+2 src/OpenAPI/Checker/Validate/MediaTypeObject.hs+19−6 src/OpenAPI/Checker/Validate/OAuth2Flows.hs+25 src/OpenAPI/Checker/Validate/OpenApi.hs+1−1 src/OpenAPI/Checker/Validate/Operation.hs+39−8 src/OpenAPI/Checker/Validate/Param.hs+11−1 src/OpenAPI/Checker/Validate/PathFragment.hs+1−1 src/OpenAPI/Checker/Validate/RequestBody.hs+8 src/OpenAPI/Checker/Validate/Responses.hs+11−1 src/OpenAPI/Checker/Validate/Schema.hs+212−107 src/OpenAPI/Checker/Validate/Server.hs+10 stack.yaml+1−1 stack.yaml.lock+4−4 test/Spec.hs+1−5 test/Spec/Golden/Extra.hs+11−37 test/Spec/Golden/TraceTree.hs+44−14 test/golden/common/enum-anyof/report.md+1 test/golden/common/id/report.md+1 test/golden/common/json/recursive/report.md+1 test/golden/common/maximum-lowered/report.md+7 test/golden/common/maximum-lowered/trace-tree.yaml+3−2 test/golden/common/pathItem/operation/parameters/allowEmptyValue/reset/report.md+5 test/golden/common/pathItem/operation/parameters/allowEmptyValue/reset/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/parameters/allowEmptyValue/set/report.md+1 test/golden/common/pathItem/operation/parameters/change/report.md+9 test/golden/common/pathItem/operation/parameters/change/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/parameters/required/false/add/report.md+1 test/golden/common/pathItem/operation/parameters/required/false/del/report.md+1 test/golden/common/pathItem/operation/parameters/required/reset/report.md+1 test/golden/common/pathItem/operation/parameters/required/set/report.md+5 test/golden/common/pathItem/operation/parameters/required/set/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/parameters/required/true/add/report.md+3 test/golden/common/pathItem/operation/parameters/required/true/add/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/parameters/required/true/del/report.md+1 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/add/report.md+1 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+7 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/del/report.md+6 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/del/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/requestBody/required/reset/report.md+1 test/golden/common/pathItem/operation/requestBody/required/set/report.md+5 test/golden/common/pathItem/operation/requestBody/required/set/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/responses/add/report.md+3 test/golden/common/pathItem/operation/responses/add/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/responses/change/headers/mandatory/add/report.md+1 test/golden/common/pathItem/operation/responses/change/headers/mandatory/del/report.md+5 test/golden/common/pathItem/operation/responses/change/headers/mandatory/del/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/responses/change/headers/optional/add/report.md+1 test/golden/common/pathItem/operation/responses/change/headers/optional/del/report.md+1 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/add/report.md+6 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/add/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+7 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/trace-tree.yaml+1−1 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/del/report.md+1 test/golden/common/pathItem/operation/responses/del/report.md+1 test/golden/common/property-removed/report.md+13 test/golden/common/property-removed/trace-tree.yaml+1−1 test/golden/common/property-required/report.md+7 test/golden/common/property-required/trace-tree.yaml+1−1 test/golden/common/recursive/report.md+1 test/golden/common/security-scheme/report.md+15 test/golden/common/security-scheme/trace-tree.yaml+5−5 test/golden/common/servers/report.md+11 test/golden/common/servers/trace-tree.yaml+1−1 test/golden/common/unguarded-recursive/report.md+5 test/golden/common/unguarded-recursive/trace-tree.yaml+1−1
app/FormatHeuristic.hsnew file+69
@@ -0,0 +1,69 @@
1+-- |
2+-- Originally based on:
3+-- https://github.com/jgm/pandoc/blob/master/src/Text/Pandoc/App/FormatHeuristics.hs
4+module FormatHeuristic
5+ ( formatFromFilePath
6+ )
7+where
8+
9+import Data.Char (toLower)
10+import System.FilePath (takeExtension)
11+import Text.Pandoc
12+
13+formatFromFilePath :: PandocMonad m => FilePath -> Maybe (Writer m)
14+formatFromFilePath x =
15+ case takeExtension (map toLower x) of
16+ ".adoc" -> f "asciidoc"
17+ ".asciidoc" -> f "asciidoc"
18+ ".context" -> f "context"
19+ ".ctx" -> f "context"
20+ ".db" -> f "docbook"
21+ ".doc" -> f "doc" -- so we get an "unknown reader" error
22+ ".docx" -> f "docx"
23+ ".dokuwiki" -> f "dokuwiki"
24+ ".epub" -> f "epub"
25+ ".fb2" -> f "fb2"
26+ ".htm" -> f "html"
27+ ".html" -> f "html"
28+ ".icml" -> f "icml"
29+ ".json" -> f "json"
30+ ".latex" -> f "latex"
31+ ".lhs" -> f "markdown+lhs"
32+ ".ltx" -> f "latex"
33+ ".markdown" -> f "markdown"
34+ ".mkdn" -> f "markdown"
35+ ".mkd" -> f "markdown"
36+ ".mdwn" -> f "markdown"
37+ ".mdown" -> f "markdown"
38+ ".Rmd" -> f "markdown"
39+ ".md" -> f "markdown"
40+ ".ms" -> f "ms"
41+ ".muse" -> f "muse"
42+ ".native" -> f "native"
43+ ".odt" -> f "odt"
44+ ".opml" -> f "opml"
45+ ".org" -> f "org"
46+ ".pdf" -> f "pdf" -- so we get an "unknown reader" error
47+ ".pptx" -> f "pptx"
48+ ".roff" -> f "ms"
49+ ".rst" -> f "rst"
50+ ".rtf" -> f "rtf"
51+ ".s5" -> f "s5"
52+ ".t2t" -> f "t2t"
53+ ".tei" -> f "tei"
54+ ".tei.xml" -> f "tei"
55+ ".tex" -> f "latex"
56+ ".texi" -> f "texinfo"
57+ ".texinfo" -> f "texinfo"
58+ ".text" -> f "markdown"
59+ ".textile" -> f "textile"
60+ ".txt" -> f "markdown"
61+ ".wiki" -> f "mediawiki"
62+ ".xhtml" -> f "html"
63+ ".ipynb" -> f "ipynb"
64+ ".csv" -> f "csv"
65+ ".bib" -> f "biblatex"
66+ ['.', y] | y `elem` ['1' .. '9'] -> f "man"
67+ _ -> Nothing
68+ where
69+ f k = lookup k writers
app/Main.hs+59−3
@@ -1,7 +1,63 @@
11module Main (main) where
22
3−import OpenAPI.Checker.Run (runChecker)
4−
3+import Control.Monad
4+import Control.Monad.Except
5+import Data.Aeson
6+import qualified Data.ByteString.Lazy as BSL
7+import Data.Default
8+import Data.HList
9+import qualified Data.Text.IO as T
10+import qualified Data.Yaml as Yaml
11+import FormatHeuristic
12+import OpenAPI.Checker.Behavior
13+import OpenAPI.Checker.Options
14+import OpenAPI.Checker.Paths
15+import OpenAPI.Checker.PathsPrefixTree
16+import OpenAPI.Checker.Report
17+import OpenAPI.Checker.Subtree
18+import System.Exit
19+import System.IO
20+import Text.Pandoc
521
622main :: IO ()
7−main = runChecker
23+main = do
24+ opts <- execParser optionsParserInfo
25+ let parseSchema path =
26+ eitherDecodeFileStrict path >>= \case
27+ Left jsonErr -> do
28+ Yaml.decodeFileEither path >>= \case
29+ Left yamlErr -> do
30+ putStrLn "Could not parse as json or yaml"
31+ print jsonErr
32+ print yamlErr
33+ fail "Exiting"
34+ Right s -> pure s
35+ Right s -> pure s
36+ a <- traced (step ClientSchema) <$> parseSchema (clientFile opts)
37+ b <- traced (step ServerSchema) <$> parseSchema (serverFile opts)
38+ let result = runCompatFormula $ checkCompatibility HNil Root (ProdCons a b)
39+ runPandocIO :: PandocIO a -> ExceptT Errors IO a
40+ runPandocIO x = lift (runIO x) >>= either (throwError . DocumentError) pure
41+ output :: Either (PathsPrefixTree Behave AnIssue 'APILevel) () -> ExceptT Errors IO ()
42+ output = case outputMode opts of
43+ StdoutMode -> lift . T.putStrLn <=< runPandocIO . writeMarkdown def . generateReport
44+ FileMode f -> case formatFromFilePath f of
45+ Nothing -> \_ -> throwError UnknownOutputFormat
46+ Just (TextWriter writer) -> lift . T.writeFile f <=< runPandocIO . writer def . generateReport
47+ Just (ByteStringWriter writer) -> lift . BSL.writeFile f <=< runPandocIO . writer def . generateReport
48+ either handler pure <=< runExceptT $ output result
49+ case result of
50+ Right () -> exitSuccess
51+ Left _ -> exitWith $ ExitFailure 1
52+
53+data Errors
54+ = DocumentError PandocError
55+ | UnknownOutputFormat
56+
57+handler :: Errors -> IO a
58+handler (DocumentError err) = do
59+ T.hPutStrLn stderr (renderError err)
60+ exitWith $ ExitFailure 100
61+handler UnknownOutputFormat = do
62+ T.hPutStrLn stderr "Could not determine output format from file extension."
63+ exitWith $ ExitFailure 101
openapi-diff.cabal+13−2
@@ -63,6 +63,7 @@ common common-options
6363 , vector
6464 , yaml
6565 , hashable
66+ , pandoc-types
6667
6768 default-extensions: ApplicativeDo
6869 , BangPatterns
@@ -117,7 +118,6 @@ library
117118 , OpenAPI.Checker.Paths
118119 , OpenAPI.Checker.PathsPrefixTree
119120 , OpenAPI.Checker.References
120− , OpenAPI.Checker.Run
121121 , OpenAPI.Checker.Subtree
122122 , OpenAPI.Checker.Validate.MediaTypeObject
123123 , OpenAPI.Checker.Validate.OpenApi
@@ -136,15 +136,24 @@ library
136136 , OpenAPI.Checker.Common
137137 , OpenAPI.Checker.Validate.SecurityScheme
138138 , OpenAPI.Checker.Validate.OAuth2Flows
139+ , OpenAPI.Checker.Report
139140
140141executable openapi-diff
141142 import: common-options
142143 hs-source-dirs: app
143144 main-is: Main.hs
144− build-depends: openapi-diff
145+ build-depends:
146+ openapi-diff
147+ , pandoc
148+ , data-default
149+ , bytestring
150+ , yaml
151+ , filepath
145152 ghc-options: -threaded
146153 -rtsopts
147154 -with-rtsopts=-N
155+ other-modules:
156+ FormatHeuristic
148157
149158test-suite openapi-diff-test
150159 import: common-options
@@ -163,6 +172,8 @@ test-suite openapi-diff-test
163172 , yaml
164173 , directory
165174 , filepath
175+ , pandoc
176+ , data-default
166177 ghc-options: -threaded
167178 -rtsopts
168179 -with-rtsopts=-N
src/OpenAPI/Checker/Behavior.hs+6−4
@@ -9,9 +9,9 @@ where
99
1010import Data.Aeson
1111import Data.Kind
12−import Data.Text as T
1312import Data.Typeable
1413import OpenAPI.Checker.Paths
14+import Text.Pandoc.Builder
1515
1616-- | Kind
1717data BehaviorLevel
@@ -37,18 +37,20 @@ class
3737 Behavable (a :: BehaviorLevel) (b :: BehaviorLevel)
3838 where
3939 data Behave a b
40+ describeBehaviour :: Behave a b -> Inlines
41+
42+type instance AdditionalQuiverConstraints Behave a b = Behavable a b
4043
4144class (Typeable l, Ord (Issue l), Show (Issue l)) => Issuable (l :: BehaviorLevel) where
4245 data Issue l :: Type
43− describeIssue :: Issue l -> Text
44− describeIssue = T.pack . show -- TODO: remove this default
46+ describeIssue :: Issue l -> Blocks
4547 issueIsUnsupported :: Issue l -> Bool
4648
4749-- | A set of interactions having common unifying features
4850type Behavior = Paths Behave 'APILevel
4951
5052instance Issuable l => ToJSON (Issue l) where
51− toJSON = toJSON . describeIssue
53+ toJSON = toJSON . show
5254
5355data AnIssue (l :: BehaviorLevel) where
5456 AnIssue :: Issuable l => Issue l -> AnIssue l
src/OpenAPI/Checker/Paths.hs+5−1
@@ -3,6 +3,7 @@
33-- structure.
44module OpenAPI.Checker.Paths
55 ( NiceQuiver
6+ , AdditionalQuiverConstraints
67 , Paths (..)
78 , DiffPaths (..)
89 , catDiffPaths
@@ -24,7 +25,10 @@ import Data.Type.Equality
2425import Type.Reflection
2526import Prelude hiding ((.))
2627
27−type NiceQuiver (q :: k -> j -> Type) (a :: k) (b :: j) = (Typeable q, Typeable a, Typeable b, Ord (q a b), Show (q a b))
28+type NiceQuiver (q :: k -> j -> Type) (a :: k) (b :: j) =
29+ (Typeable q, Typeable a, Typeable b, Ord (q a b), Show (q a b), AdditionalQuiverConstraints q a b)
30+
31+type family AdditionalQuiverConstraints (q :: k -> j -> Type) (a :: k) (b :: j) :: Constraint
2832
2933-- | All the possible ways to navigate between nodes in a heterogeneous tree
3034-- form a quiver. The hom-sets of the free category constructed from this quiver
src/OpenAPI/Checker/PathsPrefixTree.hs+20−4
@@ -1,7 +1,8 @@
11{-# LANGUAGE QuantifiedConstraints #-}
22
33module OpenAPI.Checker.PathsPrefixTree
4− ( PathsPrefixTree
4+ ( PathsPrefixTree (PathsPrefixNode)
5+ , AStep (..)
56 , empty
67 , singleton
78 , fromList
@@ -24,6 +25,7 @@ import qualified Data.Text as T
2425import Data.Type.Equality
2526import qualified Data.TypeRepMap as TRM
2627import qualified Data.Vector as V
28+import qualified GHC.Exts as Exts
2729import qualified GHC.Exts as TRM
2830import OpenAPI.Checker.Paths
2931import Type.Reflection
@@ -35,6 +37,15 @@ data PathsPrefixTree (q :: k -> k -> Type) (f :: k -> Type) (r :: k) = PathsPref
3537 , snocItems :: !(TRM.TypeRepMap (AStep q f r))
3638 }
3739
40+pattern PathsPrefixNode :: Ord (f r) => S.Set (f r) -> [TRM.WrapTypeable (AStep q f r)] -> PathsPrefixTree q f r
41+pattern PathsPrefixNode s steps <-
42+ (\(PathsPrefixTree aset m) -> (toSet aset, Exts.toList m) -> (s, steps))
43+ where
44+ PathsPrefixNode s steps | S.null s = PathsPrefixTree AnEmptySet (Exts.fromList steps)
45+ PathsPrefixNode s steps = PathsPrefixTree (ASet s) (Exts.fromList steps)
46+
47+{-# COMPLETE PathsPrefixNode #-}
48+
3849instance (forall a. ToJSON (f a)) => ToJSON (PathsPrefixTree q f r) where
3950 toJSON =
4051 Object . getMergableObject
@@ -78,6 +89,10 @@ data ASet (a :: Type) where
7889 AnEmptySet :: ASet a
7990 ASet :: Ord a => S.Set a -> ASet a
8091
92+toSet :: ASet a -> S.Set a
93+toSet AnEmptySet = S.empty
94+toSet (ASet s) = s
95+
8196instance ToJSON a => ToJSON (ASet a) where
8297 toJSON =
8398 toJSON . \case
@@ -123,6 +138,7 @@ instance Semigroup (PathsPrefixTree q f r) where
123138 PathsPrefixTree r1 s1 <> PathsPrefixTree r2 s2 =
124139 PathsPrefixTree (r1 <> r2) (TRM.unionWith joinSteps s1 s2)
125140 where
141+ joinSteps :: AStep q f r a -> AStep q f r a -> AStep q f r a
126142 joinSteps (AStep m1) (AStep m2) = AStep $ M.unionWith (<>) m1 m2
127143
128144instance Monoid (PathsPrefixTree q f r) where
@@ -135,7 +151,7 @@ fromList :: [AnItem q f r] -> PathsPrefixTree q f r
135151fromList = foldMap singleton
136152
137153null :: PathsPrefixTree q f r -> Bool
138−null (PathsPrefixTree AnEmptySet s) = TRM.size s == 0
154+null (PathsPrefixTree AnEmptySet s) = all (\(TRM.WrapTypeable (AStep x)) -> all null x) (Exts.toList s)
139155null _ = False
140156
141157foldWith
@@ -161,11 +177,11 @@ toList :: PathsPrefixTree q f r -> [AnItem q f r]
161177toList t = appEndo (foldWith (\xs f -> Endo (AnItem xs f :)) t) []
162178
163179-- | Select a subtree by prefix
164−filter :: Paths q r a -> PathsPrefixTree q f r -> PathsPrefixTree q f a
180+filter :: forall q f r a. Paths q r a -> PathsPrefixTree q f r -> PathsPrefixTree q f a
165181filter Root t = t
166182filter (Snoc xs x) t =
167183 foldMap (\(AStep m) -> fold $ M.lookup x m) $
168− TRM.lookup $ snocItems $ filter xs t
184+ TRM.lookup @a $ snocItems $ filter xs t
169185
170186-- | Embed a subtree in a larger tree with given prefix
171187embed :: Paths q r a -> PathsPrefixTree q f a -> PathsPrefixTree q f r
src/OpenAPI/Checker/Report.hsnew file+136
@@ -0,0 +1,136 @@
1+module OpenAPI.Checker.Report
2+ ( generateReport
3+ )
4+where
5+
6+import Control.Monad.Reader
7+import Control.Monad.Writer
8+import Data.Foldable
9+import Data.Functor
10+import qualified Data.Map as M
11+import Data.Maybe
12+import qualified Data.Text as T
13+import Data.Traversable
14+import Data.TypeRepMap hiding (empty)
15+import Data.Typeable
16+import OpenAPI.Checker.Behavior
17+import OpenAPI.Checker.Paths
18+import OpenAPI.Checker.PathsPrefixTree hiding (empty)
19+import qualified OpenAPI.Checker.PathsPrefixTree as P hiding (empty)
20+import OpenAPI.Checker.Validate.OpenApi
21+import Text.Pandoc.Builder
22+
23+generateReport :: Either (P.PathsPrefixTree Behave AnIssue 'APILevel) () -> Pandoc
24+generateReport (Right ()) = doc $ header 1 "No breaking changes found ✨"
25+generateReport (Left errs) = doc $ runReportMonad jets $ showErrs errs
26+
27+data ReportState = ReportState
28+ { sourceJets :: [SomeReportJet Behave]
29+ , headerLevel :: Int
30+ }
31+
32+type ReportMonad = ReaderT ReportState (Writer Blocks)
33+
34+runReportMonad :: [SomeReportJet Behave] -> ReportMonad () -> Blocks
35+runReportMonad jts =
36+ execWriter
37+ . flip
38+ runReaderT
39+ ReportState
40+ { sourceJets = jts
41+ , headerLevel = 1
42+ }
43+
44+smartHeader :: Inlines -> ReportMonad ()
45+smartHeader i = do
46+ h <- asks headerLevel
47+ tell $ header h i
48+
49+showErrs :: Typeable a => P.PathsPrefixTree Behave AnIssue a -> ReportMonad ()
50+showErrs x@(P.PathsPrefixNode currentIssues _) = do
51+ jts <- asks sourceJets
52+ for_ currentIssues $ \(AnIssue i) -> tell . describeIssue $ i
53+ unfoldM x (observeSomeJetShowErrs <$> jts) $ \(P.PathsPrefixNode _ subIssues) -> do
54+ for_ subIssues $ \(WrapTypeable (AStep m)) ->
55+ for_ (M.toList m) $ \(bhv, subErrors) -> do
56+ unless (P.null subErrors) $ do
57+ smartHeader $ describeBehaviour bhv
58+ incrementHeaders $ showErrs subErrors
59+
60+unfoldM :: Monad m => a -> [a -> m a] -> (a -> m ()) -> m ()
61+unfoldM a [] g = g a
62+unfoldM a (f : ff) g = do
63+ a' <- f a
64+ unfoldM a' ff g
65+
66+observeSomeJetShowErrs
67+ :: forall a.
68+ Typeable a
69+ => SomeReportJet Behave
70+ -> P.PathsPrefixTree Behave AnIssue a
71+ -> ReportMonad (P.PathsPrefixTree Behave AnIssue a)
72+observeSomeJetShowErrs (SomeReportJet (Proxy :: Proxy a') f) x
73+ | Just Refl <- eqT @a @a' = observeJetShowErrs f x
74+observeSomeJetShowErrs _ x = pure x
75+
76+observeJetShowErrs :: ReportJet Behave a -> P.PathsPrefixTree Behave AnIssue a -> ReportMonad (P.PathsPrefixTree Behave AnIssue a)
77+observeJetShowErrs jet (P.PathsPrefixNode currentIssues subIssues) = do
78+ rest <- fmap (fold . join) $
79+ for subIssues $ \(WrapTypeable (AStep m)) -> fmap catMaybes $
80+ for (M.toList m) $ \(bhv, subErrs) ->
81+ case applyReportJet jet bhv of
82+ Just (Left h) -> do
83+ smartHeader h
84+ incrementHeaders $ showErrs subErrs
85+ return Nothing
86+ Just (Right jet') -> do
87+ rest <- observeJetShowErrs jet' subErrs
88+ return $ Just $ embed (step bhv) rest
89+ Nothing -> return $ Just $ embed (step bhv) subErrs
90+ return $ PathsPrefixNode currentIssues mempty <> rest
91+
92+-- | A "jet" is a way of simplifying expressions from "outside". The "jetted"
93+-- expressions should still be completely valid and correct without the jets.
94+-- Jets just make the expression more "optimized" by identifying patterns and
95+-- replacing the expressions with "better" ones that have the same sematics.
96+--
97+-- The tem "jet" in this context was introduced in the Urbit project:
98+-- https://urbit.org/docs/vere/jetting/
99+--
100+-- The pattern fits well for simplifying 'Behaviour' tree paths.
101+class ConstructReportJet f a b c where
102+ constructReportJet :: (f a b -> c) -> ReportJet f a
103+
104+instance (ConstructReportJet f b c d, Typeable b) => ConstructReportJet f a b (f b c -> d) where
105+ constructReportJet f = ReportJet Proxy $ \x -> constructReportJet $ f x
106+
107+instance Typeable b => ConstructReportJet f a b Inlines where
108+ constructReportJet f = TerminalJet Proxy f
109+
110+constructSomeReportJet :: (ConstructReportJet f a b c, Typeable a) => (f a b -> c) -> SomeReportJet f
111+constructSomeReportJet = SomeReportJet Proxy . constructReportJet
112+
113+data ReportJet f a where
114+ ReportJet :: Typeable b => Proxy b -> (f a b -> ReportJet f b) -> ReportJet f a
115+ TerminalJet :: Typeable b => Proxy b -> (f a b -> Inlines) -> ReportJet f a
116+
117+data SomeReportJet f where
118+ SomeReportJet :: Typeable a => Proxy a -> ReportJet f a -> SomeReportJet f
119+
120+applyReportJet :: forall f a b. Typeable b => ReportJet f a -> f a b -> Maybe (Either Inlines (ReportJet f b))
121+applyReportJet (TerminalJet (Proxy :: Proxy b') f) x = eqT @b @b' <&> \Refl -> Left $ f x
122+applyReportJet (ReportJet (Proxy :: Proxy b') f) x = eqT @b @b' <&> \Refl -> Right $ f x
123+
124+incrementHeaders :: ReportMonad x -> ReportMonad x
125+incrementHeaders m = do
126+ l <- asks headerLevel
127+ local (\x -> x {headerLevel = l + 1}) m
128+
129+jets :: [SomeReportJet Behave]
130+jets =
131+ [ constructSomeReportJet $ \p@(AtPath _) op@(InOperation _) ->
132+ strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
133+ , constructSomeReportJet $ \InRequest InPayload PayloadSchema -> "JSON Request" :: Inlines
134+ , constructSomeReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
135+ "JSON Response – " <> str (T.pack . show $ c) :: Inlines
136+ ]
src/OpenAPI/Checker/Run.hsdeleted−40
@@ -1,40 +0,0 @@
1−module OpenAPI.Checker.Run (runChecker) where
2−
3−import Data.Aeson
4−import qualified Data.ByteString.Char8 as BSC
5−import Data.HList
6−import qualified Data.Yaml as Yaml
7−import OpenAPI.Checker.Options
8−import OpenAPI.Checker.Paths
9−import OpenAPI.Checker.Subtree
10−import OpenAPI.Checker.Validate.OpenApi ()
11−import System.Exit
12−import Prelude hiding (id, (.))
13−
14−runChecker :: IO ()
15−runChecker = do
16− opts <- execParser optionsParserInfo
17− let parseSchema path =
18− eitherDecodeFileStrict path >>= \case
19− Left jsonErr -> do
20− Yaml.decodeFileEither path >>= \case
21− Left yamlErr -> do
22− putStrLn "Could not parse as json or yaml"
23− print jsonErr
24− print yamlErr
25− fail "Exiting"
26− Right s -> pure s
27− Right s -> pure s
28− a <- traced (step ClientSchema) <$> parseSchema (clientFile opts)
29− b <- traced (step ServerSchema) <$> parseSchema (serverFile opts)
30− let report = runCompatFormula $ checkCompatibility HNil Root (ProdCons a b)
31− output = case outputMode opts of
32− StdoutMode -> BSC.putStrLn
33− FileMode f -> BSC.writeFile f
34− case report of
35− Right () -> do
36− output "No breaking changes found"
37− exitSuccess
38− Left e -> do
39− output $ Yaml.encode e
40− exitFailure
src/OpenAPI/Checker/Subtree.hs+13−6
@@ -64,7 +64,7 @@ import OpenAPI.Checker.Paths
6464import qualified OpenAPI.Checker.PathsPrefixTree as P
6565
6666class
67− NiceQuiver Step a b =>
67+ (Typeable Step, Typeable a, Typeable b, Ord (Step a b), Show (Step a b)) =>
6868 Steppable (a :: Type) (b :: Type)
6969 where
7070 -- | How to get from an @a@ node to a @b@ node
@@ -80,6 +80,8 @@ instance Steppable TraceRoot OpenApi where
8080
8181type Trace = Paths Step TraceRoot
8282
83+type instance AdditionalQuiverConstraints Step a b = ()
84+
8385type Traced' a b = Env (Trace a) b
8486
8587type Traced a = Traced' a a
@@ -128,10 +130,12 @@ type SemanticCompatFormula = CompatFormula' Behave AnIssue 'APILevel
128130
129131type StructuralCompatFormula = CompatFormula' VoidQuiver Proxy ()
130132
131−data VoidQuiver a b where
133+data VoidQuiver a b
132134
133135deriving stock instance Eq (VoidQuiver a b)
134136
137+type instance AdditionalQuiverConstraints VoidQuiver a b = ()
138+
135139deriving stock instance Ord (VoidQuiver a b)
136140
137141deriving stock instance Show (VoidQuiver a b)
@@ -297,10 +301,13 @@ memo
297301 -> (ProdCons (Traced a) -> CompatFormula' q f r ())
298302 -> (ProdCons (Traced a) -> CompatFormula' q f r ())
299303memo bhv k f pc = Compose $ do
300− formula' <- memoWithKnot fixpointKnot (do
301− formula <- getCompose $ f pc
302− pure $ mapErrors (P.filter bhv) formula
303− ) (k, ask <$> pc)
304+ formula' <-
305+ memoWithKnot
306+ fixpointKnot
307+ (do
308+ formula <- getCompose $ f pc
309+ pure $ mapErrors (P.filter bhv) formula)
310+ (k, ask <$> pc)
304311 pure $ mapErrors (P.embed bhv) formula'
305312
306313data MemoKey = SemanticMemoKey | StructuralMemoKey
src/OpenAPI/Checker/Validate/Header.hs+5
@@ -13,6 +13,7 @@ import OpenAPI.Checker.Behavior
1313import OpenAPI.Checker.References ()
1414import OpenAPI.Checker.Subtree
1515import OpenAPI.Checker.Validate.Schema ()
16+import Text.Pandoc.Builder
1617
1718instance Subtree Header where
1819 type SubtreeLevel Header = 'HeaderLevel
@@ -50,8 +51,12 @@ instance Issuable 'HeaderLevel where
5051 | HeaderSchemaRequired
5152 deriving stock (Eq, Ord, Show)
5253 issueIsUnsupported _ = False
54+ describeIssue RequiredHeaderMissing = para "Header expected, but missing."
55+ describeIssue NonEmptyHeaderRequired = para "Empty header not allowed."
56+ describeIssue HeaderSchemaRequired = para "Expected header schema, but it is not present."
5357
5458instance Behavable 'HeaderLevel 'SchemaLevel where
5559 data Behave 'HeaderLevel 'SchemaLevel
5660 = InSchema
5761 deriving stock (Eq, Ord, Show)
62+ describeBehaviour InSchema = "JSON Schema"
src/OpenAPI/Checker/Validate/Link.hs+2
@@ -5,6 +5,7 @@ module OpenAPI.Checker.Validate.Link () where
55import Data.OpenApi
66import OpenAPI.Checker.Behavior
77import OpenAPI.Checker.Subtree
8+import Text.Pandoc.Builder
89
910instance Subtree Link where
1011 type SubtreeLevel Link = 'LinkLevel
@@ -18,3 +19,4 @@ instance Issuable 'LinkLevel where
1819 deriving (Eq, Ord, Show)
1920 issueIsUnsupported = \case
2021 LinksUnsupported -> True
22+ describeIssue LinksUnsupported = para "OpenApi Diff does not currently support Link Objects."
src/OpenAPI/Checker/Validate/MediaTypeObject.hs+19−6
@@ -6,19 +6,22 @@ module OpenAPI.Checker.Validate.MediaTypeObject
66 )
77where
88
9−import Control.Lens
9+import Control.Lens hiding (para)
1010import Data.Foldable as F
1111import Data.HList
1212import Data.HashMap.Strict.InsOrd as IOHM
1313import Data.Map.Strict as M
1414import Data.OpenApi
15+import Data.String
1516import Data.Text (Text)
17+import qualified Data.Text as T
1618import Network.HTTP.Media (MediaType, mainType, subType)
1719import OpenAPI.Checker.Behavior
1820import OpenAPI.Checker.Subtree
1921import OpenAPI.Checker.Validate.Header ()
2022import OpenAPI.Checker.Validate.Products
2123import OpenAPI.Checker.Validate.Schema ()
24+import Text.Pandoc.Builder
2225
2326tracedSchema :: Traced MediaTypeObject -> Maybe (Traced (Referenced Schema))
2427tracedSchema mto = _mediaTypeObjectSchema (extract mto) <&> traced (ask mto >>> step MediaTypeSchema)
@@ -31,9 +34,7 @@ tracedEncoding mto =
3134
3235instance Issuable 'PayloadLevel where
3336 data Issue 'PayloadLevel
34− = PayloadMediaTypeNotFound
35− | MediaEncodingIncompat
36− | MediaTypeSchemaRequired
37+ = MediaTypeSchemaRequired
3738 | MediaEncodingMissing Text
3839 | EncodingNotSupported
3940 deriving (Eq, Ord, Show)
@@ -41,10 +42,15 @@ instance Issuable 'PayloadLevel where
4142 EncodingNotSupported -> True
4243 _ -> False
4344
45+ describeIssue MediaTypeSchemaRequired = para "Media type expected, but was not specified."
46+ describeIssue (MediaEncodingMissing enc) = para $ "Media encoding " <> str enc <> " expected, but was not specified."
47+ describeIssue EncodingNotSupported = para "OpenApi Diff does not currently support media encodings other than JSON."
48+
4449instance Behavable 'PayloadLevel 'SchemaLevel where
4550 data Behave 'PayloadLevel 'SchemaLevel
4651 = PayloadSchema
4752 deriving (Eq, Ord, Show)
53+ describeBehaviour PayloadSchema = "JSON Schema"
4854
4955instance Subtree MediaTypeObject where
5056 type SubtreeLevel MediaTypeObject = 'PayloadLevel
@@ -136,15 +142,22 @@ instance Behavable 'OperationLevel 'ResponseLevel where
136142 data Behave 'OperationLevel 'ResponseLevel
137143 = WithStatusCode HttpStatusCode
138144 deriving stock (Eq, Ord, Show)
145+ describeBehaviour (WithStatusCode c) = "Response code " <> (fromString . show $ c)
139146
140147instance Issuable 'OperationLevel where
141148 data Issue 'OperationLevel
142149 = ResponseCodeNotFound HttpStatusCode
143− | CallbacksNotSupported
144150 | ParamNotMatched Text
145151 | PathFragmentNotMatched Int
146152 | NoRequestBody
147153 deriving stock (Eq, Ord, Show)
148154 issueIsUnsupported = \case
149− CallbacksNotSupported -> True
150155 _ -> False
156+ describeIssue (ResponseCodeNotFound c) =
157+ para $ "Reponse code " <> (str . T.pack . show $ c) <> " is not supported."
158+ describeIssue (ParamNotMatched param) =
159+ para $ "Parameter " <> str param <> " is not supported."
160+ describeIssue (PathFragmentNotMatched i) =
161+ -- TODO: Indices are meaningless in this context. Replace with a better error.
162+ para $ "Path fragment " <> (str . T.pack . show $ i) <> " not matched."
163+ describeIssue NoRequestBody = para "Request body not specified."
src/OpenAPI/Checker/Validate/OAuth2Flows.hs+25
@@ -9,6 +9,7 @@ where
99
1010import Control.Monad
1111import Data.Function
12+import Data.Functor
1213import qualified Data.HashMap.Strict.InsOrd as IOHM
1314import Data.OpenApi
1415import Data.Proxy
@@ -18,6 +19,7 @@ import Data.Text (Text)
1819import OpenAPI.Checker.Behavior
1920import OpenAPI.Checker.Orphans ()
2021import OpenAPI.Checker.Subtree
22+import Text.Pandoc.Builder
2123
2224instance Subtree OAuth2Flows where
2325 type CheckEnv OAuth2Flows = '[]
@@ -102,6 +104,8 @@ instance Issuable 'SecurityRequirementLevel where
102104 | UndefinedSecurityScheme Text
103105 deriving stock (Eq, Ord, Show)
104106 issueIsUnsupported _ = False
107+ describeIssue SecurityRequirementNotMet = para "Security scheme was not met."
108+ describeIssue (UndefinedSecurityScheme k) = para $ "Security scheme " <> code k <> " is not defined."
105109
106110instance Issuable 'SecuritySchemeLevel where
107111 data Issue 'SecuritySchemeLevel
@@ -125,8 +129,29 @@ instance Issuable 'SecuritySchemeLevel where
125129 | ScopeNotDefined Text
126130 deriving stock (Eq, Ord, Show)
127131 issueIsUnsupported _ = False
132+ describeIssue RefreshUrlsDontMatch = para "Refresh URLs differ."
133+ describeIssue (HttpSchemeTypesDontMatch _ _) = para "HTTP scheme types don't match."
134+ describeIssue (ApiKeyParamsDontMatch _ _) = para "API Key parameters don't match."
135+ describeIssue (OpenIdConnectUrlsDontMatch _ _) = para "OpenaId Connect URLs don't match."
136+ describeIssue (CustomHttpSchemesDontMatch e a) =
137+ para $ "Expected HTTP scheme " <> code e <> " but got " <> code a <> "."
138+ describeIssue ConsumerDoesNotSupportImplicitFlow = para "Implicit flow not supported."
139+ describeIssue ConsumerDoesNotSupportPasswordFlow = para "Password flow not supported."
140+ describeIssue ConsumerDoesNotSupportClientCridentialsFlow = para "Client Cridentials flow not supported."
141+ describeIssue ConsumerDoesNotSupportAuthorizationCodeFlow = para "Authorization Code flow not supported."
142+ describeIssue SecuritySchemeNotMatched = para "Security scheme not met."
143+ describeIssue OAuth2ImplicitFlowNotEqual = para "Implicit Flows don't match."
144+ describeIssue OAuth2PasswordFlowNotEqual = para "Password Flows don't match."
145+ describeIssue OAuth2ClientCredentialsFlowNotEqual = para "Client Cridentials Flows don't match."
146+ describeIssue OAuth2AuthorizationCodeFlowNotEqual = para "Authorization Code Flows don't match."
147+ describeIssue (ScopesMissing ss) =
148+ para "Scopes missing:" <> bulletList (S.toList ss <&> codeBlock)
149+ describeIssue DifferentSecuritySchemes = para "Completely different security scheme types."
150+ describeIssue CanNotHaveScopes = para "The specified security scheme can not have scopes."
151+ describeIssue (ScopeNotDefined k) = para $ "Scope with key " <> code k <> " is not defined."
128152
129153instance Behavable 'SecurityRequirementLevel 'SecuritySchemeLevel where
130154 data Behave 'SecurityRequirementLevel 'SecuritySchemeLevel
131155 = SecuritySchemeStep Text
132156 deriving stock (Eq, Ord, Show)
157+ describeBehaviour (SecuritySchemeStep s) = text s
src/OpenAPI/Checker/Validate/OpenApi.hs+1−1
@@ -3,7 +3,7 @@
33{-# OPTIONS_GHC -fconstraint-solver-iterations=0 #-}
44
55module OpenAPI.Checker.Validate.OpenApi
6− (
6+ ( Behave (..)
77 )
88where
99
src/OpenAPI/Checker/Validate/Operation.hs+39−8
@@ -13,6 +13,7 @@ module OpenAPI.Checker.Validate.Operation
1313 , ProcessedPathItems (..)
1414 , processPathItems
1515 , Step (..)
16+ , Behave (..)
1617 )
1718where
1819
@@ -27,6 +28,7 @@ import qualified Data.List as L
2728import Data.Map.Strict as M
2829import Data.Maybe
2930import Data.OpenApi
31+import Data.String
3032import Data.Text (Text)
3133import qualified Data.Text as T
3234import OpenAPI.Checker.Behavior
@@ -36,11 +38,12 @@ import OpenAPI.Checker.Validate.MediaTypeObject
3638import OpenAPI.Checker.Validate.OAuth2Flows
3739import OpenAPI.Checker.Validate.PathFragment
3840import OpenAPI.Checker.Validate.Products
39−import OpenAPI.Checker.Validate.RequestBody ()
40−import OpenAPI.Checker.Validate.Responses ()
41+import OpenAPI.Checker.Validate.RequestBody
42+import OpenAPI.Checker.Validate.Responses
4143import OpenAPI.Checker.Validate.SecurityRequirement ()
4244import OpenAPI.Checker.Validate.Server ()
4345import OpenAPI.Checker.Validate.Sums
46+import Text.Pandoc.Builder
4447
4548data MatchedOperation = MatchedOperation
4649 { operation :: !Operation
@@ -95,18 +98,24 @@ getServers env oper =
9598instance Behavable 'OperationLevel 'PathFragmentLevel where
9699 data Behave 'OperationLevel 'PathFragmentLevel
97100 = InParam Text
98− | InFragment Int
101+ | InFragment (PathFragment Text)
99102 deriving stock (Eq, Ord, Show)
103+ describeBehaviour (InParam p) = "Parameter " <> text p
104+ describeBehaviour (InFragment (StaticPath p)) = "Static fragment " <> code p
105+ describeBehaviour (InFragment (DynamicPath p)) = "Dynamic fragment " <> code p
100106
101107instance Behavable 'OperationLevel 'RequestLevel where
102108 data Behave 'OperationLevel 'RequestLevel
103109 = InRequest
104110 deriving stock (Eq, Ord, Show)
111+ describeBehaviour InRequest = "Request"
105112
106113instance Behavable 'OperationLevel 'SecurityRequirementLevel where
107114 data Behave 'OperationLevel 'SecurityRequirementLevel
108115 = SecurityRequirementStep Int
109116 deriving stock (Eq, Ord, Show)
117+ describeBehaviour (SecurityRequirementStep i) =
118+ "Security requirement " <> (text . T.pack . show $ i)
110119
111120instance Subtree MatchedOperation where
112121 type SubtreeLevel MatchedOperation = 'OperationLevel
@@ -202,7 +211,8 @@ instance Subtree MatchedOperation where
202211 fragments = getFragments <$> pathParams <*> prodCons
203212 getFragments params mop = getPathFragments (extract mop) params
204213 -- Feed path parameters to the fragments getter
205− check idx frags = checkCompatibility @PathFragmentParam env (beh >>> step (InFragment idx)) frags
214+ check _ frags@(ProdCons (Traced _ p) _) =
215+ checkCompatibility @PathFragmentParam env (beh >>> step (InFragment $ _paramName . extract <$> p)) frags
206216 elements =
207217 fragments <&> \frags -> M.fromList $
208218 zip [0 :: Int ..] $ do
@@ -348,12 +358,16 @@ instance Issuable 'APILevel where
348358 -- When several paths match given but all checks failed
349359 deriving stock (Eq, Ord, Show)
350360 issueIsUnsupported _ = False
361+ describeIssue (NoPathsMatched p) = para $ "The path " <> (code . T.pack) p <> " did not match anything."
362+ describeIssue (AllPathsFailed p) = para $ "The path " <> (code . T.pack) p <> " did not match anything."
351363
352364instance Behavable 'APILevel 'PathLevel where
353365 data Behave 'APILevel 'PathLevel
354− = AtPath (ProdCons FilePath) -- TODO: why are there two?
366+ = AtPath FilePath
355367 deriving stock (Eq, Ord, Show)
356368
369+ describeBehaviour (AtPath p) = str (T.pack p)
370+
357371instance Subtree ProcessedPathItems where
358372 type SubtreeLevel ProcessedPathItems = 'APILevel
359373 type
@@ -376,15 +390,16 @@ instance Subtree ProcessedPathItems where
376390 -- one way
377391 for_ (unProcessedPathItems . extract $ p) $ \prodItem -> do
378392 let prodPath = path prodItem
393+ beh' = beh >>> step (AtPath prodPath)
379394 matchedItems = do
380395 consItem <- unProcessedPathItems . extract $ c
381396 F.toList $ matchingPathItems $ ProdCons prodItem consItem
382397 case matchedItems of
383398 [] -> issueAt beh $ NoPathsMatched prodPath
384− [match] -> checkCompatibility env (beh >>> step (AtPath $ matchedPath <$> match)) (retraced <$> pc <*> match)
399+ [match] -> checkCompatibility env beh' (retraced <$> pc <*> match)
385400 matches -> anyOfAt beh (AllPathsFailed prodPath) $ do
386401 match <- matches
387− pure $ checkCompatibility env (beh >>> step (AtPath $ matchedPath <$> match)) (retraced <$> pc <*> match)
402+ pure $ checkCompatibility env beh' (retraced <$> pc <*> match)
388403 where
389404 retraced pc mpi = traced (ask pc >>> step (MatchedPathStep $ matchedPath mpi)) mpi
390405
@@ -429,7 +444,6 @@ tracedMatchedPathItemParameters mpi =
429444 | (i, x) <- L.zip [0 ..] $ _pathItemParameters . pathItem $ extract mpi
430445 ]
431446
432−-- TODO: simplify?
433447tracedFragments :: Traced MatchedPathItem -> [Env (Trace PathFragmentParam) (PathFragment Text)]
434448tracedFragments mpi =
435449 [ env (ask mpi >>> step (PathFragmentStep i)) x
@@ -447,12 +461,26 @@ instance Issuable 'PathLevel where
447461 = OperationMissing OperationMethod
448462 deriving stock (Eq, Ord, Show)
449463 issueIsUnsupported _ = False
464+ describeIssue (OperationMissing op) = para $ "Method " <> strong (showMethod op) <> " is not defined."
450465
451466instance Behavable 'PathLevel 'OperationLevel where
452467 data Behave 'PathLevel 'OperationLevel
453468 = InOperation OperationMethod
454469 deriving (Eq, Ord, Show)
455470
471+ describeBehaviour (InOperation method) = showMethod method
472+
473+showMethod :: IsString s => OperationMethod -> s
474+showMethod = \case
475+ GetMethod -> "GET"
476+ PutMethod -> "PUT"
477+ PostMethod -> "POST"
478+ DeleteMethod -> "DELETE"
479+ OptionsMethod -> "OPTIONS"
480+ HeadMethod -> "HEAD"
481+ PatchMethod -> "PATCH"
482+ TraceMethod -> "TRACE"
483+
456484instance Subtree MatchedPathItem where
457485 type SubtreeLevel MatchedPathItem = 'PathLevel
458486 type
@@ -550,6 +578,7 @@ instance Issuable 'CallbackLevel where
550578 deriving (Eq, Ord, Show)
551579 issueIsUnsupported = \case
552580 CallbacksUnsupported -> True
581+ describeIssue CallbacksUnsupported = para "OpenApi Diff does not currently support callbacks."
553582
554583tracedCallbackPathItems :: Traced Callback -> Traced ProcessedPathItems
555584tracedCallbackPathItems (Traced t (Callback x)) =
@@ -562,3 +591,5 @@ instance Steppable Callback ProcessedPathItems where
562591instance Behavable 'OperationLevel 'CallbackLevel where
563592 data Behave 'OperationLevel 'CallbackLevel = OperationCallback Text
564593 deriving stock (Eq, Ord, Show)
594+
595+ describeBehaviour (OperationCallback key) = "Operation " <> code key
src/OpenAPI/Checker/Validate/Param.hs+11−1
@@ -6,7 +6,7 @@ module OpenAPI.Checker.Validate.Param
66 , Issue (..)
77 ) where
88
9−import Control.Lens
9+import Control.Lens hiding (para)
1010import Control.Monad
1111import Data.Maybe
1212import Data.OpenApi
@@ -15,6 +15,7 @@ import OpenAPI.Checker.Behavior
1515import OpenAPI.Checker.Orphans ()
1616import OpenAPI.Checker.Subtree
1717import OpenAPI.Checker.Validate.Schema ()
18+import Text.Pandoc.Builder
1819
1920-- | The type is normalized encoding style of the parameter. If two encoding
2021-- styles are equal then parameters are compatible with their encoding style
@@ -62,11 +63,20 @@ instance Issuable 'PathFragmentLevel where
6263 | PathFragmentsDontMatch Text Text
6364 deriving (Eq, Ord, Show)
6465 issueIsUnsupported _ = False
66+ describeIssue ParamNameMismatch = para "The path fragments don't match."
67+ describeIssue ParamEmptinessIncompatible = para "Expected that an empty parameter is allowed, but it isn't."
68+ describeIssue ParamRequired = para "Expected the parameter to be optional, but it is required."
69+ describeIssue ParamPlaceIncompatible = para "Parameters in incompatible locations."
70+ describeIssue ParamStyleMismatch = para "Different parameter styles (encodings)."
71+ describeIssue ParamSchemaMismatch = para "Expected a schema, but didn't find one."
72+ describeIssue (PathFragmentsDontMatch e a) = para $ "Expected " <> code e <> " but got " <> code a <> "."
6573
6674instance Behavable 'PathFragmentLevel 'SchemaLevel where
6775 data Behave 'PathFragmentLevel 'SchemaLevel
6876 = InParamSchema
6977 deriving (Eq, Ord, Show)
78+
79+ describeBehaviour InParamSchema = "JSON Schema"
7080
7181instance Subtree Param where
7282 type SubtreeLevel Param = 'PathFragmentLevel
src/OpenAPI/Checker/Validate/PathFragment.hs+1−1
@@ -32,7 +32,7 @@ parsePath = fmap partition . T.splitOn "/" . T.pack
3232data PathFragment param
3333 = StaticPath Text
3434 | DynamicPath param
35− deriving stock (Eq, Ord)
35+ deriving stock (Eq, Ord, Show, Functor)
3636
3737type PathFragmentParam = PathFragment (Traced Param)
3838
src/OpenAPI/Checker/Validate/RequestBody.hs+8
@@ -2,6 +2,7 @@
22
33module OpenAPI.Checker.Validate.RequestBody
44 ( Issue (..)
5+ , Behave (..)
56 )
67where
78
@@ -10,11 +11,13 @@ import Data.HashMap.Strict.InsOrd as IOHM
1011import Data.Map.Strict as M
1112import Data.Maybe
1213import Data.OpenApi
14+import qualified Data.Text as T
1315import Network.HTTP.Media (MediaType)
1416import OpenAPI.Checker.Behavior
1517import OpenAPI.Checker.Subtree
1618import OpenAPI.Checker.Validate.MediaTypeObject
1719import OpenAPI.Checker.Validate.Sums
20+import Text.Pandoc.Builder
1821
1922-- TODO: Use RequestMediaTypeObjectMapping
2023tracedContent :: Traced RequestBody -> IOHM.InsOrdHashMap MediaType (Traced MediaTypeObject)
@@ -28,11 +31,16 @@ instance Issuable 'RequestLevel where
2831 | RequestMediaTypeNotFound MediaType
2932 deriving stock (Eq, Ord, Show)
3033 issueIsUnsupported _ = False
34+ describeIssue RequestBodyRequired =
35+ para "Expected the request body to be optional, but found it to be required."
36+ describeIssue (RequestMediaTypeNotFound t) =
37+ para $ "Couldn't find a request body for media type " <> (code . T.pack . show $ t) <> "."
3138
3239instance Behavable 'RequestLevel 'PayloadLevel where
3340 data Behave 'RequestLevel 'PayloadLevel
3441 = InPayload
3542 deriving stock (Eq, Ord, Show)
43+ describeBehaviour InPayload = "Payload"
3644
3745instance Subtree RequestBody where
3846 type SubtreeLevel RequestBody = 'RequestLevel
src/OpenAPI/Checker/Validate/Responses.hs+11−1
@@ -2,7 +2,7 @@
22{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
33
44module OpenAPI.Checker.Validate.Responses
5− (
5+ ( Behave (..)
66 )
77where
88
@@ -11,6 +11,7 @@ import Data.HashMap.Strict.InsOrd as IOHM
1111import Data.Map.Strict as M
1212import Data.Maybe
1313import Data.OpenApi
14+import qualified Data.Text as T
1415import Network.HTTP.Media (MediaType)
1516import OpenAPI.Checker.Behavior
1617import OpenAPI.Checker.References
@@ -21,6 +22,7 @@ import OpenAPI.Checker.Validate.MediaTypeObject
2122import OpenAPI.Checker.Validate.Products
2223import OpenAPI.Checker.Validate.Schema ()
2324import OpenAPI.Checker.Validate.Sums
25+import Text.Pandoc.Builder
2426
2527tracedResponses :: Traced Responses -> IOHM.InsOrdHashMap HttpStatusCode (Traced (Referenced Response))
2628tracedResponses resp =
@@ -70,17 +72,25 @@ instance Issuable 'ResponseLevel where
7072 | ResponseHeaderMissing HeaderName
7173 deriving stock (Eq, Ord, Show)
7274 issueIsUnsupported _ = False
75+ describeIssue (ResponseMediaTypeMissing t) =
76+ para $ "Couldn't find reponse for media type " <> (code . T.pack . show $ t) <> "."
77+ describeIssue (ResponseHeaderMissing h) =
78+ para $ "Couldn't find header " <> code h <> "."
7379
7480instance Behavable 'ResponseLevel 'PayloadLevel where
7581 data Behave 'ResponseLevel 'PayloadLevel
7682 = ResponsePayload
7783 deriving stock (Eq, Ord, Show)
7884
85+ describeBehaviour ResponsePayload = "Payload"
86+
7987instance Behavable 'ResponseLevel 'HeaderLevel where
8088 data Behave 'ResponseLevel 'HeaderLevel
8189 = InHeader HeaderName
8290 deriving stock (Eq, Ord, Show)
8391
92+ describeBehaviour (InHeader name) = "Header " <> code name
93+
8494instance Subtree Response where
8595 type SubtreeLevel Response = 'ResponseLevel
8696 type
src/OpenAPI/Checker/Validate/Schema.hs+212−107
@@ -17,12 +17,13 @@ import Algebra.Lattice
1717import Control.Applicative
1818import Control.Arrow
1919import Control.Comonad.Env hiding (env)
20−import Control.Lens hiding (cons)
20+import Control.Lens hiding (cons, para)
2121import Control.Monad.Reader hiding (ask)
2222import qualified Control.Monad.Reader as R
2323import Control.Monad.State
2424import Control.Monad.Writer
2525import qualified Data.Aeson as A
26+import qualified Data.ByteString.Lazy as BSL
2627import Data.Coerce
2728import qualified Data.Foldable as F
2829import Data.HList
@@ -40,8 +41,8 @@ import Data.Scientific
4041import qualified Data.Set as S
4142import Data.Text (Text)
4243import qualified Data.Text as T hiding (singleton)
44+import qualified Data.Text.Encoding as T
4345import Data.Typeable
44−import Text.Regex.Pcre2
4546import OpenAPI.Checker.Behavior
4647import OpenAPI.Checker.Memo
4748import OpenAPI.Checker.Orphans ()
@@ -49,6 +50,8 @@ import OpenAPI.Checker.Paths
4950import qualified OpenAPI.Checker.PathsPrefixTree as P
5051import OpenAPI.Checker.References
5152import OpenAPI.Checker.Subtree
53+import Text.Pandoc.Builder hiding (Format, Null)
54+import Text.Regex.Pcre2
5255
5356-- | Type of a JSON value
5457data JsonType
@@ -84,7 +87,7 @@ untypeValue (TArray a) = A.Array a
8487untypeValue (TObject o) = A.Object o
8588
8689data Bound a = Exclusive !a | Inclusive !a
87− deriving (Eq, Show)
90+ deriving (Eq, Show, Functor)
8891
8992-- | The order is lexicographical on @a * Bool@.
9093instance Ord a => Ord (Bound a) where
@@ -128,6 +131,45 @@ data Condition :: JsonType -> Type where
128131 MaxProperties :: !Integer -> Condition 'Object
129132 MinProperties :: !Integer -> Condition 'Object
130133
134+showCondition :: Condition a -> Blocks
135+showCondition = \case
136+ (Exactly v) -> para "The value should be:" <> showJSONValue (untypeValue v)
137+ (Maximum b) -> para $ "The value should be less than " <> showBound b <> "."
138+ (Minimum (Down b)) -> para $ "The value should be more than " <> showBound (getDown <$> b) <> "."
139+ (MultipleOf n) -> para $ "The value should be a multiple of " <> show' n <> "."
140+ (NumberFormat p) -> para $ "The number should have the following format:" <> code p <> "."
141+ (Pattern p) -> para "The value should satisfy the following pattern (regular expression):" <> codeBlock p
142+ (StringFormat p) -> para $ "The string should have the following format:" <> code p <> "."
143+ (MaxLength p) -> para $ "The length of the string should be less than or equal to " <> show' p <> "."
144+ (MinLength p) -> para $ "The length of the string should be more than or equal to " <> show' p <> "."
145+ (Items i _) -> para "The items of the array should satisfy:" <> showForEachJsonFormula i
146+ (MaxItems n) -> para $ "The length of the array should be less than or equal to " <> show' n <> "."
147+ (MinItems n) -> para $ "The length of the array should be more than or equal to " <> show' n <> "."
148+ UniqueItems -> para "The elements in the array should be unique."
149+ (Properties props additional _) ->
150+ bulletList $
151+ (M.toList props
152+ <&> (\(k, p) ->
153+ para (code k)
154+ <> para (strong $ if propRequired p then "Required" else "Optional")
155+ <> showForEachJsonFormula (propFormula p)))
156+ <> [ para (emph "Additional properties")
157+ <> showForEachJsonFormula additional
158+ ]
159+ (MaxProperties n) -> para $ "The maximum number of fields should be " <> show' n <> "."
160+ (MinProperties n) -> para $ "The minimum number of fields should be " <> show' n <> "."
161+ where
162+ showForEachJsonFormula :: ForeachType JsonFormula -> Blocks
163+ showForEachJsonFormula i =
164+ bulletList $
165+ foldType
166+ (\t f ->
167+ let (DNF conds') = f i
168+ conds = S.toList <$> S.toList conds'
169+ in [ para (describeJSONType t)
170+ <> bulletList (conds <&> \cond -> bulletList (showCondition <$> cond))
171+ ])
172+
131173satisfiesTyped :: TypedValue t -> Condition t -> Bool
132174satisfiesTyped e (Exactly e') = e == e'
133175satisfiesTyped (TNumber n) (Maximum (Exclusive m)) = n < m
@@ -363,10 +405,6 @@ instance
363405 , forObject = top
364406 }
365407
366−instance Behavable 'SchemaLevel 'SchemaLevel where
367− data Behave 'SchemaLevel 'SchemaLevel
368− deriving (Eq, Ord, Show)
369−
370408instance Steppable Schema (Referenced Schema) where
371409 data Step Schema (Referenced Schema)
372410 = AllOfStep Int
@@ -396,7 +434,7 @@ parseDiscriminatorValue v = case A.fromJSON @(Referenced Schema) $ A.object ["$r
396434 A.Error _ -> Ref $ Reference v
397435
398436-- | A fake writer monad that doesn't actually record anything and allows lazy recursion.
399−newtype Silent q f r a = Silent { runSilent :: a }
437+newtype Silent q f r a = Silent {runSilent :: a}
400438 deriving stock (Functor)
401439 deriving (Applicative, Monad) via Identity
402440
@@ -427,11 +465,12 @@ silently m = do
427465 pure . runSilent $ runReaderT m defs
428466
429467warnKnot :: MonadProcess m => KnotTier (ForeachType JsonFormula) () m
430−warnKnot = KnotTier
431− { onKnotFound = warn UnguardedRecursion
432− , onKnotUsed = \_ -> pure bottom
433− , tieKnot = \_ -> pure
434− }
468+warnKnot =
469+ KnotTier
470+ { onKnotFound = warn UnguardedRecursion
471+ , onKnotUsed = \_ -> pure bottom
472+ , tieKnot = \_ -> pure
473+ }
435474
436475processRefSchema
437476 :: MonadProcess m
@@ -835,19 +874,19 @@ checkImplication env beh prods cons = case findExactly prods of
835874 Just m' ->
836875 if m' <= m
837876 then pure ()
838− else issueAt beh (MatchingMaximumWeak m m')
877+ else issueAt beh (MatchingMaximumWeak $ ProdCons {producer = m', consumer = m})
839878 Nothing -> issueAt beh (NoMatchingMaximum m)
840879 Minimum m -> case findRelevant max (\case Minimum m' -> Just m'; _ -> Nothing) prods of
841880 Just m' ->
842881 if m' >= m
843882 then pure ()
844− else issueAt beh (MatchingMinimumWeak (coerce m) (coerce m'))
883+ else issueAt beh (MatchingMinimumWeak ProdCons {producer = coerce m', consumer = coerce m})
845884 Nothing -> issueAt beh (NoMatchingMinimum (coerce m))
846885 MultipleOf m -> case findRelevant lcmScientific (\case MultipleOf m' -> Just m'; _ -> Nothing) prods of
847886 Just m' ->
848887 if lcmScientific m m' == m'
849888 then pure ()
850− else issueAt beh (MatchingMultipleOfWeak m m')
889+ else issueAt beh (MatchingMultipleOfWeak $ ProdCons {producer = m', consumer = m})
851890 Nothing -> issueAt beh (NoMatchingMultipleOf m)
852891 NumberFormat f ->
853892 if any (\case NumberFormat f' -> f == f'; _ -> False) prods
@@ -857,13 +896,13 @@ checkImplication env beh prods cons = case findExactly prods of
857896 Just m' ->
858897 if m' <= m
859898 then pure ()
860− else issueAt beh (MatchingMaxLengthWeak m m')
899+ else issueAt beh (MatchingMaxLengthWeak $ ProdCons {producer = m', consumer = m})
861900 Nothing -> issueAt beh (NoMatchingMaxLength m)
862901 MinLength m -> case findRelevant max (\case MinLength m' -> Just m'; _ -> Nothing) prods of
863902 Just m' ->
864903 if m' >= m
865904 then pure ()
866− else issueAt beh (MatchingMinLengthWeak m m')
905+ else issueAt beh (MatchingMinLengthWeak $ ProdCons {producer = m', consumer = m})
867906 Nothing -> issueAt beh (NoMatchingMinLength m)
868907 Pattern p ->
869908 if any (\case Pattern p' -> p == p'; _ -> False) prods
@@ -883,50 +922,52 @@ checkImplication env beh prods cons = case findExactly prods of
883922 Just m' ->
884923 if m' <= m
885924 then pure ()
886− else issueAt beh (MatchingMaxItemsWeak m m')
925+ else issueAt beh (MatchingMaxItemsWeak ProdCons {producer = m', consumer = m})
887926 Nothing -> issueAt beh (NoMatchingMaxItems m)
888927 MinItems m -> case findRelevant max (\case MinItems m' -> Just m'; _ -> Nothing) prods of
889928 Just m' ->
890929 if m' >= m
891930 then pure ()
892− else issueAt beh (MatchingMinItemsWeak m m')
931+ else issueAt beh (MatchingMinItemsWeak ProdCons {producer = m', consumer = m})
893932 Nothing -> issueAt beh (NoMatchingMinItems m)
894933 UniqueItems ->
895934 if any (== UniqueItems) $ prods
896935 then pure ()
897936 else issueAt beh NoMatchingUniqueItems
898937 Properties props _ madd -> case findRelevant (<>) (\case Properties props' _ madd' -> Just $ (props', madd') NE.:| []; _ -> Nothing) prods of
899− Just pm -> anyOfAt beh NoMatchingProperties $ NE.toList pm <&> \(props', madd') -> do
900− F.for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
901− let go sch sch' = checkCompatibility env (beh >>> step (InProperty k)) (ProdCons sch sch')
902− case (M.lookup k props', madd', M.lookup k props, madd) of
903− (Nothing, Nothing, _, _) -> pure () -- vacuously
904− (_, _, Nothing, Nothing) -> issueAt beh (UnexpectedProperty k)
905− (Just p', _, Just p, _) -> go (propRefSchema p') (propRefSchema p)
906− (Nothing, Just add', Just p, _) -> go add' (propRefSchema p)
907− (Just p', _, Nothing, Just add) -> go (propRefSchema p') add
908− (Nothing, Just _, Nothing, Just _) -> pure ()
909− case (maybe False propRequired $ M.lookup k props', maybe False propRequired $ M.lookup k props) of
910− (False, True) -> issueAt beh (PropertyNowRequired k)
911− _ -> pure ()
912− pure ()
913− case (madd', madd) of
914− (Nothing, _) -> pure () -- vacuously
915− (_, Nothing) -> issueAt beh NoAdditionalProperties
916− (Just add', Just add) -> checkCompatibility env (beh >>> step InAdditionalProperty) (ProdCons add' add)
917− pure ()
938+ Just pm ->
939+ anyOfAt beh NoMatchingProperties $
940+ NE.toList pm <&> \(props', madd') -> do
941+ F.for_ (S.fromList $ M.keys props <> M.keys props') $ \k -> do
942+ let go sch sch' = checkCompatibility env (beh >>> step (InProperty k)) (ProdCons sch sch')
943+ case (M.lookup k props', madd', M.lookup k props, madd) of
944+ (Nothing, Nothing, _, _) -> pure () -- vacuously
945+ (_, _, Nothing, Nothing) -> issueAt beh (UnexpectedProperty k)
946+ (Just p', _, Just p, _) -> go (propRefSchema p') (propRefSchema p)
947+ (Nothing, Just add', Just p, _) -> go add' (propRefSchema p)
948+ (Just p', _, Nothing, Just add) -> go (propRefSchema p') add
949+ (Nothing, Just _, Nothing, Just _) -> pure ()
950+ case (maybe False propRequired $ M.lookup k props', maybe False propRequired $ M.lookup k props) of
951+ (False, True) -> issueAt beh (PropertyNowRequired k)
952+ _ -> pure ()
953+ pure ()
954+ case (madd', madd) of
955+ (Nothing, _) -> pure () -- vacuously
956+ (_, Nothing) -> issueAt beh NoAdditionalProperties
957+ (Just add', Just add) -> checkCompatibility env (beh >>> step InAdditionalProperty) (ProdCons add' add)
958+ pure ()
918959 Nothing -> issueAt beh NoMatchingProperties
919960 MaxProperties m -> case findRelevant min (\case MaxProperties m' -> Just m'; _ -> Nothing) prods of
920961 Just m' ->
921962 if m' <= m
922963 then pure ()
923− else issueAt beh (MatchingMaxPropertiesWeak m m')
964+ else issueAt beh (MatchingMaxPropertiesWeak ProdCons {producer = m', consumer = m})
924965 Nothing -> issueAt beh (NoMatchingMaxProperties m)
925966 MinProperties m -> case findRelevant max (\case MinProperties m' -> Just m'; _ -> Nothing) prods of
926967 Just m' ->
927968 if m' >= m
928969 then pure ()
929− else issueAt beh (MatchingMinPropertiesWeak m m')
970+ else issueAt beh (MatchingMinPropertiesWeak ProdCons {producer = m', consumer = m})
930971 Nothing -> issueAt beh (NoMatchingMinProperties m)
931972 where
932973 findExactly (Exactly x : _) = Just x
@@ -939,85 +980,145 @@ checkImplication env beh prods cons = case findExactly prods of
939980
940981instance Issuable 'TypedSchemaLevel where
941982 data Issue 'TypedSchemaLevel
942− = EnumDoesntSatisfy A.Value
943− -- ^ producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
944− | NoMatchingEnum A.Value
945− -- ^ producer produces a specific value ($1) which is not listed in the consumer's list of specific values
946− | NoMatchingMaximum (Bound Scientific)
947− -- ^ producer declares a maximum numeric value ($1), consumer doesnt
948− | MatchingMaximumWeak (Bound Scientific) (Bound Scientific)
949− -- ^ producer declares a maximum numeric value ($1), consumer declares a weaker (higher) limit ($2)
950− | NoMatchingMinimum (Bound Scientific)
951− -- ^ producer declares a minimum numeric value, consumer doesnt
952− | MatchingMinimumWeak (Bound Scientific) (Bound Scientific)
953− -- ^ producer declares a minimum numeric value ($1), consumer declares a weaker (lower) limit ($2)
954− | NoMatchingMultipleOf Scientific
955− -- ^ producer declares that the numeric value must be a multiple of $1, consumer doesn't
956− | MatchingMultipleOfWeak Scientific Scientific
957− -- ^ producer declares that the numeric value must be a multiple of $1, consumer declares a weaker condition (multiple of $2)
958− | NoMatchingFormat Format
959− -- ^ producer declares a string/number format, consumer declares none or a different format (TODO: improve via regex #32)
960− | NoMatchingMaxLength Integer
961− -- ^ producer declares a maximum length of the string ($1), consumer doesn't.
962− | MatchingMaxLengthWeak Integer Integer
963− -- ^ producer declares a maximum length of the string ($1), consumer declares a weaker (higher) limit ($2)
964− | NoMatchingMinLength Integer
965− -- ^ producer declares a minimum length of the string ($1), consumer doesn't.
966− | MatchingMinLengthWeak Integer Integer
967− -- ^ producer declares a minimum length of the string ($1), consumer declares a weaker (lower) limit ($2)
968− | NoMatchingPattern Pattern
969− -- ^ producer declares the string value must matrix a regex ($1), consumer doesn't declare or declares different regex (TODO: #32)
970− | NoMatchingItems
971− -- ^ producer declares the items of an array must satisfy some condition, consumer doesn't
972− | NoMatchingMaxItems Integer
973− -- ^ producer declares a maximum length of the array ($1), consumer doesn't.
974− | MatchingMaxItemsWeak Integer Integer
975− -- ^ producer declares a maximum length of the array ($1), consumer declares a weaker (higher) limit ($2)
976− | NoMatchingMinItems Integer
977− -- ^ producer declares a minimum length of the array ($1), consumer doesn't.
978− | MatchingMinItemsWeak Integer Integer
979− -- ^ producer declares a minimum length of the array ($1), consumer declares a weaker (lower) limit ($2)
980− | NoMatchingUniqueItems
981− -- ^ producer declares that items must be unique, consumer doesn't
982− | NoMatchingProperties
983− -- ^ producer declares the properties of an object must satisfy some condition, consumer doesn't
984− | UnexpectedProperty Text
985− -- ^ producer allows a property that is not allowed in the consumer
986− | PropertyNowRequired Text
987− -- ^ consumer requires a property that is not required/allowed in the consumer
988− | NoAdditionalProperties
989− -- ^ producer allows additional properties, consumer doesn't
990− | NoMatchingMaxProperties Integer
991− -- ^ producer declares a maximum number of properties in the object ($1), consumer doesn't.
992− | MatchingMaxPropertiesWeak Integer Integer
993− -- ^ producer declares a maximum number of properties in the object ($1), consumer declares a weaker (higher) limit ($2)
994− | NoMatchingMinProperties Integer
995− -- ^ producer declares a minimum number of properties in the object ($1), consumer doesn't.
996− | MatchingMinPropertiesWeak Integer Integer
997− -- ^ producer declares a minimum number of properties in the object ($1), consumer declares a weaker (lower) limit ($2)
998− | NoMatchingCondition [SomeCondition]
999− -- ^ consumer declares that the value must satisfy a disjunction of some conditions, but producer's requirements couldn't be matched against any single one of them (TODO: split heuristic #71)
1000− | NoContradiction
1001− -- ^ 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)
983+ = -- | producer produces a specific value ($1), consumer has a condition that is not satisfied by said value
984+ EnumDoesntSatisfy A.Value
985+ | -- | consumer only expects a specific value which the producer does not produce.
986+ NoMatchingEnum A.Value
987+ | -- | consumer declares a maximum numeric value ($1), producer doesn't
988+ NoMatchingMaximum (Bound Scientific)
989+ | -- | consumer declares a maximum numeric value ($1), producer declares a weaker (higher) limit ($2)
990+ MatchingMaximumWeak (ProdCons (Bound Scientific))
991+ | -- | consumer declares a minimum numeric value, producer doesn't
992+ NoMatchingMinimum (Bound Scientific)
993+ | -- | consumer declares a minimum numeric value ($1), producer declares a weaker (lower) limit ($2)
994+ MatchingMinimumWeak (ProdCons (Bound Scientific))
995+ | -- | consumer declares that the numeric value must be a multiple of $1, producer doesn't
996+ NoMatchingMultipleOf Scientific
997+ | -- | consumer declares that the numeric value must be a multiple of $1, producer declares a weaker condition (multiple of $2)
998+ MatchingMultipleOfWeak (ProdCons Scientific)
999+ | -- | consumer declares a string/number format, producer declares none or a different format (TODO: improve via regex #32)
1000+ NoMatchingFormat Format
1001+ | -- | consumer declares a maximum length of the string ($1), producer doesn't.
1002+ NoMatchingMaxLength Integer
1003+ | -- | consumer declares a maximum length of the string ($1), producer declares a weaker (higher) limit ($2)
1004+ MatchingMaxLengthWeak (ProdCons Integer)
1005+ | -- | consumer declares a minimum length of the string ($1), producer doesn't.
1006+ NoMatchingMinLength Integer
1007+ | -- | consumer declares a minimum length of the string ($1), producer declares a weaker (lower) limit ($2)
1008+ MatchingMinLengthWeak (ProdCons Integer)
1009+ | -- | consumer declares the string value must matrix a regex ($1), producer doesn't declare or declares different regex (TODO: #32)
1010+ NoMatchingPattern Pattern
1011+ | -- | consumer declares the items of an array must satisfy some condition, producer doesn't
1012+ NoMatchingItems
1013+ | -- | consumer declares a maximum length of the array ($1), producer doesn't.
1014+ NoMatchingMaxItems Integer
1015+ | -- | consumer declares a maximum length of the array ($1), producer declares a weaker (higher) limit ($2)
1016+ MatchingMaxItemsWeak (ProdCons Integer)
1017+ | -- | consumer declares a minimum length of the array ($1), producer doesn't.
1018+ NoMatchingMinItems Integer
1019+ | -- | consumer declares a minimum length of the array ($1), producer declares a weaker (lower) limit ($2)
1020+ MatchingMinItemsWeak (ProdCons Integer)
1021+ | -- | consumer declares that items must be unique, producer doesn't
1022+ NoMatchingUniqueItems
1023+ | -- | consumer declares the properties of an object must satisfy some condition, producer doesn't
1024+ NoMatchingProperties
1025+ | -- | producer allows a property that is not allowed in the consumer
1026+ UnexpectedProperty Text
1027+ | -- | consumer requires a property that is not required/allowed in the producer
1028+ PropertyNowRequired Text
1029+ | -- | producer allows additional properties, consumer doesn't
1030+ NoAdditionalProperties
1031+ | -- | consumer declares a maximum number of properties in the object ($1), producer doesn't.
1032+ NoMatchingMaxProperties Integer
1033+ | -- | consumer declares a maximum number of properties in the object ($1), producer declares a weaker (higher) limit ($2)
1034+ MatchingMaxPropertiesWeak (ProdCons Integer)
1035+ | -- | consumer declares a minimum number of properties in the object ($1), producer doesn't.
1036+ NoMatchingMinProperties Integer
1037+ | -- | consumer declares a minimum number of properties in the object ($1), producer declares a weaker (lower) limit ($2)
1038+ MatchingMinPropertiesWeak (ProdCons Integer)
1039+ | -- | consumer declares that the value must satisfy a disjunction of some conditions, but producer's requirements couldn't be matched against any single one of them (TODO: split heuristic #71)
1040+ NoMatchingCondition [SomeCondition]
1041+ | -- | consumer indicates that values of this type are now allowed, but the producer does not do so (currently we only check immediate contradictions, c.f. #70)
1042+ NoContradiction
10021043 deriving stock (Eq, Ord, Show)
10031044 issueIsUnsupported _ = False
1045+ describeIssue (EnumDoesntSatisfy v) = para "The following enum value will yield an error:" <> showJSONValue v
1046+ describeIssue (NoMatchingEnum v) = para "The following enum value is not supported:" <> showJSONValue v
1047+ describeIssue (NoMatchingMaximum b) = para $ "Unexpected upper bound " <> showBound b <> "."
1048+ describeIssue (MatchingMaximumWeak (ProdCons p c)) = para $ "Expected upper bound " <> showBound p <> " but but found " <> showBound c <> "."
1049+ describeIssue (NoMatchingMinimum b) = para $ "Unexpected lower bound " <> showBound b <> "."
1050+ describeIssue (MatchingMinimumWeak (ProdCons p c)) = para $ "Expected lower bound " <> showBound p <> " but but found " <> showBound c <> "."
1051+ describeIssue (NoMatchingMultipleOf n) = para $ "Didn't expect the value to be a multiple of " <> show' n <> " but it was."
1052+ describeIssue (MatchingMultipleOfWeak (ProdCons p c)) = para $ "Expected the value to be a multiple of " <> show' p <> " but found a multiple of " <> show' c <> "."
1053+ describeIssue (NoMatchingFormat f) = para $ "Unexpected format: " <> code f <> "."
1054+ describeIssue (NoMatchingMaxLength n) = para $ "Unexpected maximum length of the string " <> show' n <> "."
1055+ describeIssue (MatchingMaxLengthWeak (ProdCons p c)) = para $ "Expected the maximum length of the string to be " <> show' p <> "but it was " <> show' c <> "."
1056+ describeIssue (NoMatchingMinLength n) = para $ "Unexpected minimum length of the string " <> show' n <> "."
1057+ describeIssue (MatchingMinLengthWeak (ProdCons p c)) = para $ "Expected the minimum length of the string to be " <> show' p <> "but it was " <> show' c <> "."
1058+ describeIssue (NoMatchingPattern p) = para "Unexpected pattern (regular expression): " <> codeBlock p
1059+ describeIssue NoMatchingItems = para "Couldn't find any matching items."
1060+ describeIssue (NoMatchingMaxItems n) = para $ "Unexpected maximum length of the array " <> show' n <> "."
1061+ describeIssue (MatchingMaxItemsWeak (ProdCons p c)) = para $ "Expected the maximum length of the array to be " <> show' p <> "but it was " <> show' c <> "."
1062+ describeIssue (NoMatchingMinItems n) = para $ "Unexpected minimum length of the array " <> show' n <> "."
1063+ describeIssue (MatchingMinItemsWeak (ProdCons p c)) = para $ "Expected the minimum length of the array to be " <> show' p <> "but it was " <> show' c <> "."
1064+ describeIssue NoMatchingUniqueItems = para "Didn't expect the items to be unique, but they were."
1065+ describeIssue NoMatchingProperties = para "Couldn't find matching properties."
1066+ describeIssue (UnexpectedProperty p) = para $ "Expected the property " <> code p <> " to be allowed, but it wasn't."
1067+ describeIssue (PropertyNowRequired p) = para $ "Don't have a required property " <> code p <> "."
1068+ describeIssue NoAdditionalProperties = para "Expected additional properties to be allowed, but they weren't."
1069+ describeIssue (NoMatchingMaxProperties n) = para $ "Unexpected maximum number of properties " <> show' n <> "."
1070+ describeIssue (MatchingMaxPropertiesWeak (ProdCons p c)) = para $ "Expected the maximum number of properties to be " <> show' p <> "but it was " <> show' c <> "."
1071+ describeIssue (NoMatchingMinProperties n) = para $ "Unexpected minimum number of properties " <> show' n <> "."
1072+ describeIssue (MatchingMinPropertiesWeak (ProdCons p c)) = para $ "Expected the minimum number of properties to be " <> show' p <> "but it was " <> show' c <> "."
1073+ describeIssue (NoMatchingCondition conds) =
1074+ para "Expected the following conditions to hold, but they didn't:"
1075+ <> bulletList ((\(SomeCondition c) -> showCondition c) <$> conds)
1076+ describeIssue NoContradiction = para "Expected the type to be allowed, but it wasn't."
1077+
1078+showJSONValue :: A.Value -> Blocks
1079+showJSONValue v = codeBlockWith ("", ["json"], mempty) (T.decodeUtf8 . BSL.toStrict . A.encode $ v)
1080+
1081+showBound :: Show a => Bound a -> Inlines
1082+showBound (Inclusive x) = show' x <> " inclusive"
1083+showBound (Exclusive x) = show' x <> " exclusive"
1084+
1085+show' :: Show x => x -> Inlines
1086+show' = str . T.pack . show
10041087
10051088instance Issuable 'SchemaLevel where
10061089 data Issue 'SchemaLevel
1007− = NotSupported Text
1008− -- ^ Some (openapi-supported) feature that we do not support was encountered in the schema
1009− | InvalidSchema Text
1010− -- ^ The schema is actually invalid
1011− | UnguardedRecursion
1012− -- ^ The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
1090+ = -- | Some (openapi-supported) feature that we do not support was encountered in the schema
1091+ NotSupported Text
1092+ | -- | The schema is actually invalid
1093+ InvalidSchema Text
1094+ | -- | The schema contains a reference loop along "anyOf"/"allOf"/"oneOf".
1095+ UnguardedRecursion
10131096 deriving stock (Eq, Ord, Show)
10141097 issueIsUnsupported _ = True
10151098
1099+ describeIssue (NotSupported i) =
1100+ para (emph "Encountered a feature that OpenApi Diff does not support: " <> text i <> ".")
1101+ describeIssue (InvalidSchema i) =
1102+ para (emph "The schema is invalid: " <> text i <> ".")
1103+ describeIssue UnguardedRecursion =
1104+ para "Encountered recursion that is too complex for OpenApi Diff to untangle."
1105+
10161106instance Behavable 'SchemaLevel 'TypedSchemaLevel where
10171107 data Behave 'SchemaLevel 'TypedSchemaLevel
10181108 = OfType JsonType
10191109 deriving stock (Eq, Ord, Show)
10201110
1111+ describeBehaviour (OfType t) = describeJSONType t
1112+
1113+describeJSONType :: JsonType -> Inlines
1114+describeJSONType = \case
1115+ Null -> "Null"
1116+ Boolean -> "Boolean"
1117+ Number -> "Number"
1118+ String -> "String"
1119+ Array -> "Array"
1120+ Object -> "Object"
1121+
10211122instance Behavable 'TypedSchemaLevel 'SchemaLevel where
10221123 data Behave 'TypedSchemaLevel 'SchemaLevel
10231124 = InItems
@@ -1025,6 +1126,10 @@ instance Behavable 'TypedSchemaLevel 'SchemaLevel where
10251126 | InAdditionalProperty
10261127 deriving stock (Eq, Ord, Show)
10271128
1129+ describeBehaviour InItems = "Items"
1130+ describeBehaviour (InProperty p) = "Property " <> code p
1131+ describeBehaviour InAdditionalProperty = "Additional properties"
1132+
10281133instance Subtree Schema where
10291134 type SubtreeLevel Schema = 'SchemaLevel
10301135 type CheckEnv Schema = '[ProdCons (Traced (Definitions Schema))]
src/OpenAPI/Checker/Validate/Server.hs+10
@@ -28,6 +28,7 @@ import OpenAPI.Checker.Common
2828import OpenAPI.Checker.Paths
2929import OpenAPI.Checker.Subtree
3030import OpenAPI.Checker.Validate.MediaTypeObject
31+import Text.Pandoc.Builder
3132import Prelude as P
3233
3334tracedParsedServerUrlParts
@@ -45,6 +46,8 @@ instance Behavable 'OperationLevel 'ServerLevel where
4546 = InServer Text
4647 deriving stock (Eq, Ord, Show)
4748
49+ describeBehaviour (InServer n) = "Server " <> code n
50+
4851instance Subtree [Server] where
4952 type SubtreeLevel [Server] = 'OperationLevel
5053 type CheckEnv [Server] = '[]
@@ -131,6 +134,13 @@ instance Issuable 'ServerLevel where
131134 | ServerNotMatched
132135 deriving stock (Eq, Ord, Show)
133136 issueIsUnsupported _ = False
137+ describeIssue (EnumValueNotConsumed _ v) =
138+ para $ "Enum value " <> code v <> " is not supported."
139+ describeIssue (ConsumerNotOpen _) =
140+ para $ "Expected a variable to be open (any value), but it wasn't."
141+ describeIssue (ServerVariableNotDefined k) =
142+ para $ "Variable " <> code k <> " is not defined."
143+ describeIssue ServerNotMatched = para $ "Couldn't find a matching server."
134144
135145instance Subtree ProcessedServer where
136146 type SubtreeLevel ProcessedServer = 'ServerLevel
stack.yaml+1−1
@@ -1 +1 @@
1−resolver: lts-17.12
1+resolver: nightly-2021-06-01
stack.yaml.lock+4−4
@@ -6,7 +6,7 @@
66packages: []
77snapshots:
88- completed:
9− size: 567669
10− url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/lts/17/12.yaml
11− sha256: facf6cac73b22a83ca955b580a98a7a09ed71f8f974c7a55d28e608c23e689a9
12− original: lts-17.12
9+ size: 587963
10+ url: https://raw.githubusercontent.com/commercialhaskell/stackage-snapshots/master/nightly/2021/6/1.yaml
11+ sha256: 6f13285c82266d1d06f7f68366a5190dcbc6758ae8806813005cf56daa6bb9be
12+ original: nightly-2021-06-01
test/Spec.hs+1−5
@@ -9,8 +9,4 @@ main = defaultMain =<< tests
99tests :: IO TestTree
1010tests = do
1111 goldenReportTree <- Spec.Golden.TraceTree.tests
12− return . localOption (mkTimeout 1000000) $
13− testGroup
14− "Golden tests"
15− [ goldenReportTree
16− ]
12+ return . localOption (mkTimeout 1000000) $ goldenReportTree
test/Spec/Golden/Extra.hs+11−37
@@ -8,15 +8,11 @@ where
88
99import Control.Lens
1010import Control.Monad
11−import Data.Aeson hiding (Result)
1211import qualified Data.ByteString.Lazy as BSL
13−import qualified Data.Yaml as Yaml
14−import OpenAPI.Checker.Subtree
1512import System.Directory
1613import System.FilePath
1714import Test.Tasty
1815import Test.Tasty.Golden
19−import Test.Tasty.Providers
2016
2117data TestInput t
2218 = TestInputNode TestName [TestInput t]
@@ -54,62 +50,40 @@ getGoldenInputsUniform
5450getGoldenInputsUniform name f filepath inp = getGoldenInputs name filepath $ inp & each %~ (,f)
5551
5652goldenInputsTree
57− :: (Each s t (FilePath, FilePath -> IO a) a, ToJSON x, HasUnsupportedFeature x)
53+ :: (Each s t (FilePath, FilePath -> IO a) a)
5854 => TestName
5955 -> FilePath -- ^ Root path
6056 -> FilePath -- ^ Name of golden file
6157 -> s
62− -> (t -> x)
58+ -> (t -> IO BSL.ByteString)
6359 -> IO TestTree
6460goldenInputsTree name filepath golden inp f = do
6561 runTestInputTree golden f <$> getGoldenInputs name filepath inp
6662
6763runTestInputTree
68− :: (ToJSON x, HasUnsupportedFeature x)
69− => FilePath
70− -> (t -> x)
64+ :: FilePath
65+ -> (t -> IO BSL.ByteString)
7166 -> TestInput t
7267 -> TestTree
7368runTestInputTree golden f (TestInputNode name rest) =
7469 testGroup name (runTestInputTree golden f <$> rest)
75−runTestInputTree golden f (TestInputLeaf name t path)
76− | testSupported name =
77− goldenVsStringDiff
78− name
79− (\ref new -> ["diff", "-u", ref, new])
80− (path </> golden)
81− (pure . BSL.fromStrict . Yaml.encode . f $ t)
82−runTestInputTree _ f (TestInputLeaf name t _) =
83− reportResult name $
84− if hasUnsupportedFeature (f t)
85− then testPassed "Feature unsupported"
86− else testFailed "Unexpected feature support"
87−
88−reportResult :: TestName -> Result -> TestTree
89−reportResult name result = singleTest name $ SimpleTestReporter result
90−
91−newtype SimpleTestReporter = SimpleTestReporter Result
92−
93−instance IsTest SimpleTestReporter where
94− run _ (SimpleTestReporter result) _ = return result
95− testOptions = mempty
96−
97−testSupported :: TestName -> Bool
98−testSupported ('x' : ' ' : _) = False
99−testSupported _ = True
70+runTestInputTree golden f (TestInputLeaf name t path) =
71+ goldenVsStringDiff
72+ name
73+ (\ref new -> ["diff", "-u", ref, new])
74+ (path </> golden)
75+ (f t)
10076
10177goldenInputsTreeUniform
10278 :: ( Each t h (FilePath, FilePath -> IO a) a
103− , ToJSON x
10479 , Each s t FilePath (FilePath, FilePath -> IO a)
105− , HasUnsupportedFeature x
10680 )
10781 => String
10882 -> FilePath -- ^ Root path
10983 -> FilePath -- ^ Name of golden file
11084 -> s
11185 -> (FilePath -> IO a)
112− -> (h -> x)
86+ -> (h -> IO BSL.ByteString)
11387 -> IO TestTree
11488goldenInputsTreeUniform name filepath golden inp h =
11589 goldenInputsTree name filepath golden (inp & each %~ (,h))
test/Spec/Golden/TraceTree.hs+44−14
@@ -4,26 +4,56 @@ module Spec.Golden.TraceTree
44where
55
66import Control.Category
7+import Control.Exception
8+import qualified Data.ByteString.Lazy as BSL
9+import Data.Default
710import Data.HList
11+import Data.OpenApi
12+import Data.Text (Text)
13+import qualified Data.Text.Encoding as T
814import qualified Data.Yaml as Yaml
9−import OpenAPI.Checker.Subtree
15+import OpenAPI.Checker.Behavior
1016import OpenAPI.Checker.Paths
17+import OpenAPI.Checker.PathsPrefixTree
18+import OpenAPI.Checker.Report
19+import OpenAPI.Checker.Subtree
1120import OpenAPI.Checker.Validate.OpenApi ()
1221import Spec.Golden.Extra
13−import Test.Tasty (TestTree)
22+import Test.Tasty (TestTree, testGroup)
23+import Text.Pandoc.Class
24+import Text.Pandoc.Writers
1425import Prelude hiding (id, (.))
1526
1627tests :: IO TestTree
17−tests =
18− goldenInputsTreeUniform
19− "Golden TraceTree"
20− "test/golden/common"
21− "trace-tree.yaml"
22− ("a.yaml", "b.yaml")
23− Yaml.decodeFileThrow
24− (runCompatFormula . checkCompatibility HNil Root . toPC)
25− where
26− toPC (client, server) = ProdCons
27− { producer = traced (step ClientSchema) client
28− , consumer = traced (step ServerSchema) server
28+tests = do
29+ traceTreeTests <-
30+ goldenInputsTreeUniform
31+ "TraceTree"
32+ "test/golden/common"
33+ "trace-tree.yaml"
34+ ("a.yaml", "b.yaml")
35+ Yaml.decodeFileThrow
36+ (pure . BSL.fromStrict . Yaml.encode . runChecker)
37+
38+ reportTests <-
39+ goldenInputsTreeUniform
40+ "Report"
41+ "test/golden/common"
42+ "report.md"
43+ ("a.yaml", "b.yaml")
44+ Yaml.decodeFileThrow
45+ (runPandoc . writeMarkdown def . generateReport . runChecker)
46+
47+ return $ testGroup "Golden tests" [traceTreeTests, reportTests]
48+
49+runPandoc :: PandocPure Text -> IO BSL.ByteString
50+runPandoc = either throwIO (pure . BSL.fromStrict . T.encodeUtf8) . runPure
51+
52+runChecker :: (OpenApi, OpenApi) -> Either (PathsPrefixTree Behave AnIssue 'APILevel) ()
53+runChecker = runCompatFormula . checkCompatibility HNil Root . toPC
54+ where
55+ toPC (c, s) =
56+ ProdCons
57+ { producer = traced (step ClientSchema) c
58+ , consumer = traced (step ServerSchema) s
2959 }
test/golden/common/enum-anyof/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/id/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/json/recursive/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/maximum-lowered/report.mdnew file+7
@@ -0,0 +1,7 @@
1+# **POST** /test
2+
3+## JSON Request
4+
5+### Number
6+
7+Expected upper bound 3.0 inclusive but but found 2.0 inclusive.
test/golden/common/maximum-lowered/trace-tree.yaml+3−2
@@ -1,7 +1,8 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InRequest:
55 InPayload:
66 PayloadSchema:
7− OfType Number: MatchingMaximumWeak (Inclusive 2.0) (Inclusive 3.0)
7+ OfType Number: MatchingMaximumWeak (ProdCons {producer = Inclusive 3.0,
8+ consumer = Inclusive 2.0})
test/golden/common/pathItem/operation/parameters/allowEmptyValue/reset/report.mdnew file+5
@@ -0,0 +1,5 @@
1+# **POST** /test
2+
3+## Parameter test1
4+
5+Expected that an empty parameter is allowed, but it isn't.
test/golden/common/pathItem/operation/parameters/allowEmptyValue/reset/trace-tree.yaml+1−1
@@ -1,4 +1,4 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InParam "test1": ParamEmptinessIncompatible
test/golden/common/pathItem/operation/parameters/allowEmptyValue/set/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/parameters/change/report.mdnew file+9
@@ -0,0 +1,9 @@
1+# **POST** /test
2+
3+## Parameter test
4+
5+### JSON Schema
6+
7+#### String
8+
9+Expected the type to be allowed, but it wasn't.
test/golden/common/pathItem/operation/parameters/change/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InParam "test":
55 InParamSchema:
test/golden/common/pathItem/operation/parameters/required/false/add/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/parameters/required/false/del/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/parameters/required/reset/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/parameters/required/set/report.mdnew file+5
@@ -0,0 +1,5 @@
1+# **POST** /test
2+
3+## Parameter test1
4+
5+Expected the parameter to be optional, but it is required.
test/golden/common/pathItem/operation/parameters/required/set/trace-tree.yaml+1−1
@@ -1,4 +1,4 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InParam "test1": ParamRequired
test/golden/common/pathItem/operation/parameters/required/true/add/report.mdnew file+3
@@ -0,0 +1,3 @@
1+# **POST** /test
2+
3+Parameter test2 is not supported.
test/golden/common/pathItem/operation/parameters/required/true/add/trace-tree.yaml+1−1
@@ -1,3 +1,3 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod: ParamNotMatched "test2"
test/golden/common/pathItem/operation/parameters/required/true/del/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/add/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.mdnew file+7
@@ -0,0 +1,7 @@
1+# **POST** /test
2+
3+## JSON Request
4+
5+### String
6+
7+Expected the type to be allowed, but it wasn't.
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InRequest:
55 InPayload:
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/del/report.mdnew file+6
@@ -0,0 +1,6 @@
1+# **POST** /test
2+
3+## Request
4+
5+Couldn't find a request body for media type
6+`application/x-www-form-urlencoded`.
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/del/trace-tree.yaml+1−1
@@ -1,4 +1,4 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InRequest: RequestMediaTypeNotFound application/x-www-form-urlencoded
test/golden/common/pathItem/operation/requestBody/required/reset/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/requestBody/required/set/report.mdnew file+5
@@ -0,0 +1,5 @@
1+# **POST** /test
2+
3+## Request
4+
5+Expected the request body to be optional, but found it to be required.
test/golden/common/pathItem/operation/requestBody/required/set/trace-tree.yaml+1−1
@@ -1,4 +1,4 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InRequest: RequestBodyRequired
test/golden/common/pathItem/operation/responses/add/report.mdnew file+3
@@ -0,0 +1,3 @@
1+# **POST** /test
2+
3+Reponse code 500 is not supported.
test/golden/common/pathItem/operation/responses/add/trace-tree.yaml+1−1
@@ -1,3 +1,3 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod: ResponseCodeNotFound 500
test/golden/common/pathItem/operation/responses/change/headers/mandatory/add/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/responses/change/headers/mandatory/del/report.mdnew file+5
@@ -0,0 +1,5 @@
1+# **POST** /test
2+
3+## Response code 200
4+
5+Couldn't find header `Test2`.
test/golden/common/pathItem/operation/responses/change/headers/mandatory/del/trace-tree.yaml+1−1
@@ -1,4 +1,4 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 WithStatusCode 200: ResponseHeaderMissing "Test2"
test/golden/common/pathItem/operation/responses/change/headers/optional/add/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/responses/change/headers/optional/del/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/add/report.mdnew file+6
@@ -0,0 +1,6 @@
1+# **POST** /test
2+
3+## Response code 200
4+
5+Couldn't find reponse for media type
6+`application/x-www-form-urlencoded`.
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/add/trace-tree.yaml+1−1
@@ -1,4 +1,4 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 WithStatusCode 200: ResponseMediaTypeMissing application/x-www-form-urlencoded
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.mdnew file+7
@@ -0,0 +1,7 @@
1+# **POST** /test
2+
3+## JSON Response – 200
4+
5+### Number
6+
7+Expected the type to be allowed, but it wasn't.
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 WithStatusCode 200:
55 ResponsePayload:
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/del/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/pathItem/operation/responses/del/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/property-removed/report.mdnew file+13
@@ -0,0 +1,13 @@
1+# **POST** /test
2+
3+## JSON Request
4+
5+### Object
6+
7+Expected the property `property2` to be allowed, but it wasn't.
8+
9+## JSON Response – 200
10+
11+### Object
12+
13+Don't have a required property `property2`.
test/golden/common/property-removed/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InRequest:
55 InPayload:
test/golden/common/property-required/report.mdnew file+7
@@ -0,0 +1,7 @@
1+# **POST** /test
2+
3+## JSON Request
4+
5+### Object
6+
7+Don't have a required property `property2`.
test/golden/common/property-required/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/test", consumer = "/test"}):
2+ AtPath "/test":
33 InOperation PostMethod:
44 InRequest:
55 InPayload:
test/golden/common/recursive/report.mdnew file+1
@@ -0,0 +1 @@
1+# No breaking changes found ✨
test/golden/common/security-scheme/report.mdnew file+15
@@ -0,0 +1,15 @@
1+# **POST** /oauth/check
2+
3+## Security requirement 0
4+
5+### oauth
6+
7+Security scheme not met.
8+
9+# **GET** /oauth/sign\_out
10+
11+## Security requirement 1
12+
13+### oauth
14+
15+Security scheme not met.
test/golden/common/security-scheme/trace-tree.yaml+5−5
@@ -1,9 +1,9 @@
11Left:
2− AtPath (ProdCons {producer = "/oauth/check", consumer = "/oauth/check"}):
3− InOperation PostMethod:
4− SecurityRequirementStep 0:
5− SecuritySchemeStep "oauth": SecuritySchemeNotMatched
6− AtPath (ProdCons {producer = "/oauth/sign_out", consumer = "/oauth/sign_out"}):
2+ AtPath "/oauth/sign_out":
73 InOperation GetMethod:
84 SecurityRequirementStep 1:
95 SecuritySchemeStep "oauth": SecuritySchemeNotMatched
6+ AtPath "/oauth/check":
7+ InOperation PostMethod:
8+ SecurityRequirementStep 0:
9+ SecuritySchemeStep "oauth": SecuritySchemeNotMatched
test/golden/common/servers/report.mdnew file+11
@@ -0,0 +1,11 @@
1+# **GET** /pets
2+
3+## Server `http://missing.url`
4+
5+Couldn't find a matching server.
6+
7+## Server `http://{x}variable.path/{y}/{openVariable1}/{openVariable2}`
8+
9+Enum value `a` is not supported.
10+
11+Expected a variable to be open (any value), but it wasn't.
test/golden/common/servers/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/pets", consumer = "/pets"}):
2+ AtPath "/pets":
33 InOperation GetMethod:
44 InServer "http://{x}variable.path/{y}/{openVariable1}/{openVariable2}":
55 - EnumValueNotConsumed 1 "a"
test/golden/common/unguarded-recursive/report.mdnew file+5
@@ -0,0 +1,5 @@
1+# **GET** /api/foo
2+
3+## JSON Response – 200
4+
5+Encountered recursion that is too complex for OpenApi Diff to untangle.
test/golden/common/unguarded-recursive/trace-tree.yaml+1−1
@@ -1,5 +1,5 @@
11Left:
2− AtPath (ProdCons {producer = "/api/foo", consumer = "/api/foo"}):
2+ AtPath "/api/foo":
33 InOperation GetMethod:
44 WithStatusCode 200:
55 ResponsePayload: