@@ -3,14 +3,22 @@ module OpenAPI.Checker.Report
6+import Control.Applicative
7+import Control.Monad.Free hiding (unfoldM)
6import Control.Monad.Reader
8import Control.Monad.Reader
7import Control.Monad.Writer
9import Control.Monad.Writer
8import Data.Foldable
11import Data.Foldable
9import Data.Functor
13import Data.Functor
14+import Data.List.NonEmpty
15+import qualified Data.List.NonEmpty as NE
10import qualified Data.Map as M
16import qualified Data.Map as M
11import Data.Maybe
17import Data.Maybe
19+import Data.OpenUnion.Extra
20+import Data.Text (Text)
12import qualified Data.Text as T
21import qualified Data.Text as T
13−import Data.Traversable
14import Data.TypeRepMap hiding (empty)
22import Data.TypeRepMap hiding (empty)
15import Data.Typeable
23import Data.Typeable
16import OpenAPI.Checker.Behavior
24import OpenAPI.Checker.Behavior
@@ -18,6 +26,7 @@ import OpenAPI.Checker.Paths
18import OpenAPI.Checker.PathsPrefixTree hiding (empty)
26import OpenAPI.Checker.PathsPrefixTree hiding (empty)
19import qualified OpenAPI.Checker.PathsPrefixTree as P hiding (empty)
27import qualified OpenAPI.Checker.PathsPrefixTree as P hiding (empty)
20import OpenAPI.Checker.Validate.OpenApi
28import OpenAPI.Checker.Validate.OpenApi
29+import OpenAPI.Checker.Validate.Schema
21import Text.Pandoc.Builder
30import Text.Pandoc.Builder
23generateReport :: Either (P.PathsPrefixTree Behave AnIssue 'APILevel) () -> Pandoc
32generateReport :: Either (P.PathsPrefixTree Behave AnIssue 'APILevel) () -> Pandoc
@@ -25,13 +34,13 @@ generateReport (Right ()) = doc $ header 1 "No breaking changes found ✨"
25generateReport (Left errs) = doc $ runReportMonad jets $ showErrs errs
34generateReport (Left errs) = doc $ runReportMonad jets $ showErrs errs
27data ReportState = ReportState
36data ReportState = ReportState
28− { sourceJets :: [SomeReportJet Behave]
37+ { sourceJets :: [ReportJet' Behave Inlines]
29 , headerLevel :: Int
38 , headerLevel :: Int
32type ReportMonad = ReaderT ReportState (Writer Blocks)
41type ReportMonad = ReaderT ReportState (Writer Blocks)
34−runReportMonad :: [SomeReportJet Behave] -> ReportMonad () -> Blocks
43+runReportMonad :: [ReportJet' Behave Inlines] -> ReportMonad () -> Blocks
35runReportMonad jts =
44runReportMonad jts =
36 execWriter
45 execWriter
@@ -46,11 +55,11 @@ smartHeader i = do
46 h <- asks headerLevel
55 h <- asks headerLevel
47 tell $ header h i
56 tell $ header h i
49−showErrs :: Typeable a => P.PathsPrefixTree Behave AnIssue a -> ReportMonad ()
58+showErrs :: P.PathsPrefixTree Behave AnIssue a -> ReportMonad ()
50showErrs x@(P.PathsPrefixNode currentIssues _) = do
59showErrs x@(P.PathsPrefixNode currentIssues _) = do
51 jts <- asks sourceJets
60 jts <- asks sourceJets
52 for_ currentIssues $ \(AnIssue i) -> tell . describeIssue $ i
61 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
54 for_ subIssues $ \(WrapTypeable (AStep m)) ->
63 for_ subIssues $ \(WrapTypeable (AStep m)) ->
55 for_ (M.toList m) $ \(bhv, subErrors) -> do
64 for_ (M.toList m) $ \(bhv, subErrors) -> do
56 unless (P.null subErrors) $ do
65 unless (P.null subErrors) $ do
@@ -63,74 +72,140 @@ unfoldM a (f : ff) g = do
64 unfoldM a' ff g
73 unfoldM a' ff g
66−observeSomeJetShowErrs
75+observeJetShowErrs
67− :: forall a.
76+ :: ReportJet' Behave Inlines
69− => SomeReportJet Behave
70 -> P.PathsPrefixTree Behave AnIssue a
77 -> P.PathsPrefixTree Behave AnIssue a
71 -> ReportMonad (P.PathsPrefixTree Behave AnIssue a)
78 -> ReportMonad (P.PathsPrefixTree Behave AnIssue a)
72−observeSomeJetShowErrs (SomeReportJet (Proxy :: Proxy a') f) x
79+observeJetShowErrs jet p = case observeJetShowErrs' jet p of
73− | Just Refl <- eqT @a @a' = observeJetShowErrs f x
80+ Just m -> m
74−observeSomeJetShowErrs _ x = pure x
81+ Nothing -> pure p
85+ ReportJet' Behave Inlines
86+ -> P.PathsPrefixTree Behave AnIssue a
76−observeJetShowErrs :: ReportJet Behave a -> P.PathsPrefixTree Behave AnIssue a -> ReportMonad (P.PathsPrefixTree Behave AnIssue a)
87+ -> Maybe (ReportMonad (P.PathsPrefixTree Behave AnIssue a))
77−observeJetShowErrs jet (P.PathsPrefixNode currentIssues subIssues) = do
88+observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues) =
78− rest <- fmap (fold . join) $
89+ let results =
79− for subIssues $ \(WrapTypeable (AStep m)) -> fmap catMaybes $
90+ subIssues >>= \(WrapTypeable (AStep m)) ->
80− for (M.toList m) $ \(bhv, subErrs) ->
91+ M.toList m <&> \(bhv, subErrs) ->
81− case applyReportJet jet bhv of
92+ maybe (Left $ embed (step bhv) subErrs) Right . listToMaybe $
82− Just (Left h) -> do
93+ jet @_ @_ @[] bhv
96+ Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
83− smartHeader h
98+ smartHeader h
84− incrementHeaders $ showErrs subErrs
99+ incrementHeaders $ showErrs subErrs
85− return Nothing
100+ return mempty)
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
101+ in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
102+ if any isRight results
112+catMapM :: (Monad m, Monoid b) => (a -> m b) -> [a] -> m b
113+catMapM f xs = mconcat <$> mapM f xs
92-- | A "jet" is a way of simplifying expressions from "outside". The "jetted"
115-- | A "jet" is a way of simplifying expressions from "outside". The "jetted"
93-- expressions should still be completely valid and correct without the jets.
116-- expressions should still be completely valid and correct without the jets.
94-- Jets just make the expression more "optimized" by identifying patterns and
117-- Jets just make the expression more "optimized" by identifying patterns and
95-- replacing the expressions with "better" ones that have the same sematics.
118-- replacing the expressions with "better" ones that have the same sematics.
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:
98-- https://urbit.org/docs/vere/jetting/
121-- https://urbit.org/docs/vere/jetting/
100-- The pattern fits well for simplifying 'Behaviour' tree paths.
123-- The pattern fits well for simplifying 'Behaviour' tree paths.
101−class ConstructReportJet f a b c where
124+class ConstructReportJet x f where
125+ constructReportJet :: x -> ReportJetResult f Inlines
127+instance (ConstructReportJet b f, JetArg a) => ConstructReportJet (a -> b) f where
102− constructReportJet :: (f a b -> c) -> ReportJet f a
128+ constructReportJet f = Free (fmap f <$> consumeJetArg @a) >>= constructReportJet
130+instance ConstructReportJet Inlines f where
131+ constructReportJet x = Pure x
134+ consumeJetArg :: ReportJet' f a
104−instance (ConstructReportJet f b c d, Typeable b) => ConstructReportJet f a b (f b c -> d) where
136+instance Typeable (f a b) => JetArg (f a b) where
105− constructReportJet f = ReportJet Proxy $ \x -> constructReportJet $ f x
137+ consumeJetArg =
138+ ReportJet $ \(x :: x) ->
139+ case eqT @(f a b) @x of
141+ Just Refl -> pure $ Pure x
107−instance Typeable b => ConstructReportJet f a b Inlines where
143+instance TryLiftUnion xs => JetArg (Union xs) where
108− constructReportJet f = TerminalJet Proxy f
144+ consumeJetArg = ReportJet $ fmap Pure . tryLiftUnion
110−constructSomeReportJet :: (ConstructReportJet f a b c, Typeable a) => (f a b -> c) -> SomeReportJet f
146+instance JetArg x => JetArg (NonEmpty x) where
111−constructSomeReportJet = SomeReportJet Proxy . constructReportJet
147+ consumeJetArg =
148+ let (ReportJet f) = (consumeJetArg @x)
149+ in ReportJet $ \a -> do
151+ pure (u >>= \y -> Free $ fmap (NE.cons y) <$> consumeJetArg)
152+ <|> pure (pure <$> u)
113−data ReportJet f a where
154+type ReportJetResult f = Free (ReportJet f)
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
117−data SomeReportJet f where
156+-- Not a true 'Applicative'
118− SomeReportJet :: Typeable a => Proxy a -> ReportJet f a -> SomeReportJet f
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)
120−applyReportJet :: forall f a b. Typeable b => ReportJet f a -> f a b -> Maybe (Either Inlines (ReportJet f b))
160+type ReportJet' f a = ReportJet f (Free (ReportJet f) a)
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
124incrementHeaders :: ReportMonad x -> ReportMonad x
162incrementHeaders :: ReportMonad x -> ReportMonad x
125incrementHeaders m = do
163incrementHeaders m = do
126 l <- asks headerLevel
164 l <- asks headerLevel
127 local (\x -> x {headerLevel = l + 1}) m
165 local (\x -> x {headerLevel = l + 1}) m
129−jets :: [SomeReportJet Behave]
167+jets :: [ReportJet' Behave Inlines]
169+ unwrapReportJetResult
170+ <$> [ constructReportJet jsonPathJet
131− [ constructSomeReportJet $ \p@(AtPath _) op@(InOperation _) ->
171+ , constructReportJet $ \p@(AtPath _) op@(InOperation _) ->
132− strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
172+ strong (describeBehaviour op) <> " " <> describeBehaviour p :: Inlines
133− , constructSomeReportJet $ \InRequest InPayload PayloadSchema -> "JSON Request" :: Inlines
173+ , constructReportJet $ \InRequest InPayload PayloadSchema -> "JSON Request" :: Inlines
134− , constructSomeReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
174+ , constructReportJet $ \(WithStatusCode c) ResponsePayload PayloadSchema ->
135− "JSON Response – " <> str (T.pack . show $ c) :: Inlines
175+ "JSON Response – " <> str (T.pack . show $ c) :: Inlines
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
185+ '[ Behave 'SchemaLevel 'TypedSchemaLevel
186+ , Behave 'TypedSchemaLevel 'SchemaLevel
190+ jsonPathJet x = code $ "$" <> showParts (NE.toList x)
194+ '[ Behave 'SchemaLevel 'TypedSchemaLevel
195+ , Behave 'TypedSchemaLevel 'SchemaLevel
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 <> ")")
207+ InProperty p -> "." <> p
208+ InAdditionalProperty -> ".*")