git.iko.soy/typeable/compaREST
on master help sign in
Added JSON Path jets (#75)

* Updated ConstructReportJet variables * changed ConstructReportJet yet more * Added support for Union in jets * Extracted jet args * observeJetShowErrs' * Complex Arguments * Added branching * Added JSON path jets * Simplified things * Better type rendering in report

commit5fd5a1cd78e830c8e81c173576714e7897303093 authoriko <ilyakooo0@gmail.com> dateTue 8 Jun 2021, 17:57 (5 years ago) parent218336b
13 files changed+205−61
.gitignore+1 openapi-diff.cabal+4 src/Data/OpenUnion/Extra.hs+43 src/OpenAPI/Checker/Report.hs+127−52 src/OpenAPI/Checker/Validate/Schema.hs+4−1 stack.yaml+4 stack.yaml.lock+15−1 test/golden/common/maximum-lowered/report.md+1−1 test/golden/common/pathItem/operation/parameters/change/report.md+1−1 test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+1−1 test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+1−1 test/golden/common/property-removed/report.md+2−2 test/golden/common/property-required/report.md+1−1
.gitignore+1
@@ -2,3 +2,4 @@
22TAGS
33.vscode
44.dir-locals.el
5+test/golden/common/prod
openapi-diff.cabal+4
@@ -64,6 +64,9 @@ common common-options
6464 , yaml
6565 , hashable
6666 , pandoc-types
67+ , open-union
68+ , type-fun
69+ , free
6770
6871 default-extensions: ApplicativeDo
6972 , BangPatterns
@@ -137,6 +140,7 @@ library
137140 , OpenAPI.Checker.Validate.SecurityScheme
138141 , OpenAPI.Checker.Validate.OAuth2Flows
139142 , OpenAPI.Checker.Report
143+ , Data.OpenUnion.Extra
140144
141145executable openapi-diff
142146 import: common-options
src/Data/OpenUnion/Extra.hsnew file+43
@@ -0,0 +1,43 @@
1+module Data.OpenUnion.Extra
2+ ( (@@>)
3+ , TryLiftUnion (..)
4+ , pattern SingletonUnion
5+ )
6+where
7+
8+import Control.Applicative
9+import Data.Dynamic
10+import Data.OpenUnion.Internal
11+import Data.Typeable
12+import TypeFun.Data.List hiding (Union)
13+
14+class TryLiftUnion xs where
15+ tryLiftUnion :: (Alternative m, Typeable x) => x -> m (Union xs)
16+
17+instance TryLiftUnion '[] where
18+ tryLiftUnion _ = empty
19+
20+instance
21+ (Typeable y, SubList ys (y : ys), TryLiftUnion ys)
22+ => TryLiftUnion (y ': ys)
23+ where
24+ tryLiftUnion (x :: x) = case eqT @x @y of
25+ Nothing -> reUnion <$> tryLiftUnion @ys x
26+ Just Refl -> pure $ liftUnion x
27+
28+-- | Like '@>', but enforces a specific type list order.
29+-- (Useful for deconstruction-directed type inference.)
30+(@@>) :: Typeable a => (a -> b) -> (Union xs -> b) -> Union (a ': xs) -> b
31+r @@> l = either l r . restrict'
32+ where
33+ restrict' :: Typeable a => Union (a ': aa) -> Either (Union aa) a
34+ restrict' (Union d) = maybe (Left $ Union d) Right $ fromDynamic d
35+{-# INLINE (@@>) #-}
36+
37+infixr 2 @@>
38+
39+pattern SingletonUnion :: (Typeable a, Elem a s) => a -> Union s
40+pattern SingletonUnion x <-
41+ ((\(Union y) -> fromDynamic y) -> Just x)
42+ where
43+ SingletonUnion x = liftUnion x
src/OpenAPI/Checker/Report.hs+127−52
@@ -3,14 +3,22 @@ module OpenAPI.Checker.Report
33 )
44where
55
6+import Control.Applicative
7+import Control.Monad.Free hiding (unfoldM)
68import Control.Monad.Reader
79import Control.Monad.Writer
10+import Data.Either
811import Data.Foldable
12+import Data.Function
913import Data.Functor
14+import Data.List.NonEmpty
15+import qualified Data.List.NonEmpty as NE
1016import qualified Data.Map as M
1117import Data.Maybe
18+import Data.OpenUnion
19+import Data.OpenUnion.Extra
20+import Data.Text (Text)
1221import qualified Data.Text as T
13−import Data.Traversable
1422import Data.TypeRepMap hiding (empty)
1523import Data.Typeable
1624import OpenAPI.Checker.Behavior
@@ -18,6 +26,7 @@ import OpenAPI.Checker.Paths
1826import OpenAPI.Checker.PathsPrefixTree hiding (empty)
1927import qualified OpenAPI.Checker.PathsPrefixTree as P hiding (empty)
2028import OpenAPI.Checker.Validate.OpenApi
29+import OpenAPI.Checker.Validate.Schema
2130import Text.Pandoc.Builder
2231
2332generateReport :: Either (P.PathsPrefixTree Behave AnIssue 'APILevel) () -> Pandoc
@@ -25,13 +34,13 @@ generateReport (Right ()) = doc $ header 1 "No breaking changes found ✨"
2534generateReport (Left errs) = doc $ runReportMonad jets $ showErrs errs
2635
2736data ReportState = ReportState
28− { sourceJets :: [SomeReportJet Behave]
37+ { sourceJets :: [ReportJet' Behave Inlines]
2938 , headerLevel :: Int
3039 }
3140
3241type ReportMonad = ReaderT ReportState (Writer Blocks)
3342
34−runReportMonad :: [SomeReportJet Behave] -> ReportMonad () -> Blocks
43+runReportMonad :: [ReportJet' Behave Inlines] -> ReportMonad () -> Blocks
3544runReportMonad jts =
3645 execWriter
3746 . flip
@@ -46,11 +55,11 @@ smartHeader i = do
4655 h <- asks headerLevel
4756 tell $ header h i
4857
49−showErrs :: Typeable a => P.PathsPrefixTree Behave AnIssue a -> ReportMonad ()
58+showErrs :: P.PathsPrefixTree Behave AnIssue a -> ReportMonad ()
5059showErrs x@(P.PathsPrefixNode currentIssues _) = do
5160 jts <- asks sourceJets
5261 for_ currentIssues $ \(AnIssue i) -> tell . describeIssue $ i
53− unfoldM x (observeSomeJetShowErrs <$> jts) $ \(P.PathsPrefixNode _ subIssues) -> do
62+ unfoldM x (observeJetShowErrs <$> jts) $ \(P.PathsPrefixNode _ subIssues) -> do
5463 for_ subIssues $ \(WrapTypeable (AStep m)) ->
5564 for_ (M.toList m) $ \(bhv, subErrors) -> do
5665 unless (P.null subErrors) $ do
@@ -63,74 +72,140 @@ unfoldM a (f : ff) g = do
6372 a' <- f a
6473 unfoldM a' ff g
6574
66−observeSomeJetShowErrs
67− :: forall a.
68− Typeable a
69− => SomeReportJet Behave
75+observeJetShowErrs
76+ :: ReportJet' Behave Inlines
7077 -> P.PathsPrefixTree Behave AnIssue a
7178 -> 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
79+observeJetShowErrs jet p = case observeJetShowErrs' jet p of
80+ Just m -> m
81+ Nothing -> pure p
82+
83+observeJetShowErrs'
84+ :: forall a.
85+ ReportJet' Behave Inlines
86+ -> P.PathsPrefixTree Behave AnIssue a
87+ -> Maybe (ReportMonad (P.PathsPrefixTree Behave AnIssue a))
88+observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
89+ let results =
90+ subIssues >>= \(WrapTypeable (AStep m)) ->
91+ M.toList m <&> \(bhv, subErrs) ->
92+ maybe (Left $ embed (step bhv) subErrs) Right . listToMaybe $
93+ jet @_ @_ @[] bhv
94+ & mapMaybe
95+ (\case
96+ Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
97+ Pure h -> Just $ do
98+ smartHeader h
99+ incrementHeaders $ showErrs subErrs
100+ return mempty)
101+ in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
102+ if any isRight results
103+ then
104+ Just $
105+ catMapM
106+ (\case
107+ Left e -> pure e
108+ Right m -> m)
109+ results
110+ else Nothing
111+
112+catMapM :: (Monad m, Monoid b) => (a -> m b) -> [a] -> m b
113+catMapM f xs = mconcat <$> mapM f xs
91114
92115-- | A "jet" is a way of simplifying expressions from "outside". The "jetted"
93116-- expressions should still be completely valid and correct without the jets.
94117-- Jets just make the expression more "optimized" by identifying patterns and
95118-- replacing the expressions with "better" ones that have the same sematics.
96119--
97−-- The tem "jet" in this context was introduced in the Urbit project:
120+-- The term "jet" in this context was introduced in the Urbit project:
98121-- https://urbit.org/docs/vere/jetting/
99122--
100123-- 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
124+class ConstructReportJet x f where
125+ constructReportJet :: x -> ReportJetResult f Inlines
126+
127+instance (ConstructReportJet b f, JetArg a) => ConstructReportJet (a -> b) f where
128+ constructReportJet f = Free (fmap f <$> consumeJetArg @a) >>= constructReportJet
129+
130+instance ConstructReportJet Inlines f where
131+ constructReportJet x = Pure x
132+
133+class JetArg a where
134+ consumeJetArg :: ReportJet' f a
103135
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
136+instance Typeable (f a b) => JetArg (f a b) where
137+ consumeJetArg =
138+ ReportJet $ \(x :: x) ->
139+ case eqT @(f a b) @x of
140+ Nothing -> empty
141+ Just Refl -> pure $ Pure x
106142
107−instance Typeable b => ConstructReportJet f a b Inlines where
108− constructReportJet f = TerminalJet Proxy f
143+instance TryLiftUnion xs => JetArg (Union xs) where
144+ consumeJetArg = ReportJet $ fmap Pure . tryLiftUnion
109145
110−constructSomeReportJet :: (ConstructReportJet f a b c, Typeable a) => (f a b -> c) -> SomeReportJet f
111−constructSomeReportJet = SomeReportJet Proxy . constructReportJet
146+instance JetArg x => JetArg (NonEmpty x) where
147+ consumeJetArg =
148+ let (ReportJet f) = (consumeJetArg @x)
149+ in ReportJet $ \a -> do
150+ u <- f a
151+ pure (u >>= \y -> Free $ fmap (NE.cons y) <$> consumeJetArg)
152+ <|> pure (pure <$> u)
112153
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
154+type ReportJetResult f = Free (ReportJet f)
116155
117−data SomeReportJet f where
118− SomeReportJet :: Typeable a => Proxy a -> ReportJet f a -> SomeReportJet f
156+-- Not a true 'Applicative'
157+newtype ReportJet f x = ReportJet (forall a b m. (Typeable (f a b), Alternative m, Monad m) => f a b -> m x)
158+ deriving stock (Functor)
119159
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
160+type ReportJet' f a = ReportJet f (Free (ReportJet f) a)
123161
124162incrementHeaders :: ReportMonad x -> ReportMonad x
125163incrementHeaders m = do
126164 l <- asks headerLevel
127165 local (\x -> x {headerLevel = l + 1}) m
128166
129−jets :: [SomeReportJet Behave]
167+jets :: [ReportJet' Behave Inlines]
130168jets =
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− ]
169+ unwrapReportJetResult
170+ <$> [ constructReportJet jsonPathJet
171+ , constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
172+ strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
173+ , constructReportJet $ \InRequest InPayload PayloadSchema -> "JSON Request" :: Inlines
174+ , constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
175+ "JSON Response – " <> str (T.pack . show $ c) :: Inlines
176+ ]
177+ where
178+ unwrapReportJetResult :: ReportJetResult Behave x -> ReportJet' Behave x
179+ unwrapReportJetResult (Pure _) = error "There really shouldn't be any results here."
180+ unwrapReportJetResult (Free f) = f
181+
182+ jsonPathJet
183+ :: NonEmpty
184+ ( Union
185+ '[ Behave 'SchemaLevel 'TypedSchemaLevel
186+ , Behave 'TypedSchemaLevel 'SchemaLevel
187+ ]
188+ )
189+ -> Inlines
190+ jsonPathJet x = code $ "$" <> showParts (NE.toList x)
191+ where
192+ showParts
193+ :: [ Union
194+ '[ Behave 'SchemaLevel 'TypedSchemaLevel
195+ , Behave 'TypedSchemaLevel 'SchemaLevel
196+ ]
197+ ]
198+ -> Text
199+ showParts [] = mempty
200+ showParts (SingletonUnion (OfType Object) : xs@((SingletonUnion (InProperty _)) : _)) = showParts xs
201+ showParts (SingletonUnion (OfType Object) : xs@((SingletonUnion InAdditionalProperty) : _)) = showParts xs
202+ showParts (SingletonUnion (OfType Array) : xs@(SingletonUnion InItems : _)) = showParts xs
203+ showParts (y : ys) =
204+ ((\(OfType t) -> "(" <> describeJSONType t <> ")")
205+ @@> (\case
206+ InItems -> "[*]"
207+ InProperty p -> "." <> p
208+ InAdditionalProperty -> ".*")
209+ @@> typesExhausted)
210+ y
211+ <> showParts ys
src/OpenAPI/Checker/Validate/Schema.hs+4−1
@@ -10,6 +10,8 @@ module OpenAPI.Checker.Validate.Schema
1010 , Bound (..)
1111 , schemaToFormula
1212 , foldLattice
13+ , Behave (..)
14+ , describeJSONType
1315 )
1416where
1517
@@ -39,6 +41,7 @@ import Data.Ord
3941import Data.Ratio
4042import Data.Scientific
4143import qualified Data.Set as S
44+import Data.String
4245import Data.Text (Text)
4346import qualified Data.Text as T hiding (singleton)
4447import qualified Data.Text.Encoding as T
@@ -1110,7 +1113,7 @@ instance Behavable 'SchemaLevel 'TypedSchemaLevel where
11101113
11111114 describeBehaviour (OfType t) = describeJSONType t
11121115
1113−describeJSONType :: JsonType -> Inlines
1116+describeJSONType :: IsString s => JsonType -> s
11141117describeJSONType = \case
11151118 Null -> "Null"
11161119 Boolean -> "Boolean"
stack.yaml+4
@@ -1 +1,5 @@
11resolver: nightly-2021-06-01
2+
3+extra-deps:
4+ - open-union-0.4.0.0
5+ - type-fun-0.1.3
stack.yaml.lock+15−1
@@ -3,7 +3,21 @@
33# For more information, please see the documentation at:
44# https://docs.haskellstack.org/en/stable/lock_files
55
6−packages: []
6+packages:
7+- completed:
8+ hackage: open-union-0.4.0.0@sha256:1c8f8090054b0974e95e44bed88e74fff956ba2120aade6e8deea92d65ef5e49,3503
9+ pantry-tree:
10+ size: 329
11+ sha256: e01838ec41f7aa2a97aa2c3586c8188c4ad093ba0b2be41229ab7b8dbd33869e
12+ original:
13+ hackage: open-union-0.4.0.0
14+- completed:
15+ hackage: type-fun-0.1.3@sha256:336b851757792f201078043210aec180021ac052f0955c71fa330a5fe11b0604,1765
16+ pantry-tree:
17+ size: 583
18+ sha256: b977d42525f0b4223959918d18e7905085283e937493c395077b608cd19b42c1
19+ original:
20+ hackage: type-fun-0.1.3
721snapshots:
822- completed:
923 size: 587963
test/golden/common/maximum-lowered/report.md+1−1
@@ -2,6 +2,6 @@
22
33## JSON Request
44
5−### Number
5+### `$(Number)`
66
77Expected upper bound 3.0 inclusive but but found 2.0 inclusive.
test/golden/common/pathItem/operation/parameters/change/report.md+1−1
@@ -4,6 +4,6 @@
44
55### JSON Schema
66
7−#### String
7+#### `$(String)`
88
99Expected the type to be allowed, but it wasn't.
test/golden/common/pathItem/operation/requestBody/mediaTypeObject/change/report.md+1−1
@@ -2,6 +2,6 @@
22
33## JSON Request
44
5−### String
5+### `$(String)`
66
77Expected the type to be allowed, but it wasn't.
test/golden/common/pathItem/operation/responses/change/mediaTypeObject/change/report.md+1−1
@@ -2,6 +2,6 @@
22
33## JSON Response – 200
44
5−### Number
5+### `$(Number)`
66
77Expected the type to be allowed, but it wasn't.
test/golden/common/property-removed/report.md+2−2
@@ -2,12 +2,12 @@
22
33## JSON Request
44
5−### Object
5+### `$(Object)`
66
77Expected the property `property2` to be allowed, but it wasn't.
88
99## JSON Response – 200
1010
11−### Object
11+### `$(Object)`
1212
1313Don't have a required property `property2`.
test/golden/common/property-required/report.md+1−1
@@ -2,6 +2,6 @@
22
33## JSON Request
44
5−### Object
5+### `$(Object)`
66
77Don't have a required property `property2`.