git.iko.soy/typeable/compaREST
on master help sign in
Added foldable tree report (#91)

* export raw html * wip * Added tree style options * render issues as list

commit62a321a537e65a7688103c49c2cc0b5190edd3e3 authoriko <ilyakooo0@gmail.com> dateMon 28 Jun 2021, 19:44 (5 years ago) parent649261f
8 files changed+156−95
app/FormatHeuristic.hs+56−53 app/Main.hs+2−2 app/OpenAPI/Checker/Options.hs+25−6 openapi-diff.cabal+1 src/OpenAPI/Checker/Report.hs+67−29 src/OpenAPI/Checker/Run.hs+2−2 test/Spec/Golden/TraceTree.hs+1−1 test/golden/common/servers/report.md+2−2
app/FormatHeuristic.hs+56−53
@@ -18,26 +18,26 @@ import System.FilePath (takeExtension)
1818import Text.DocTemplates.Internal
1919import Text.Pandoc
2020
21−formatFromFilePath :: forall m. PandocMonad m => FilePath -> Maybe (Pandoc -> m ByteString)
21+formatFromFilePath :: forall m. PandocMonad m => FilePath -> Maybe (Pandoc -> m ByteString, FilePath)
2222formatFromFilePath x =
2323 case takeExtension (map toLower x) of
24− ".adoc" -> f "asciidoc" def
25− ".asciidoc" -> f "asciidoc" def
26− ".context" -> f "context" def
27− ".ctx" -> f "context" def
28− ".db" -> f "docbook" def
29− ".doc" -> f "doc" def -- so we get an "unknown reader" error
30− ".docx" -> f "docx" def
31− ".dokuwiki" -> f "dokuwiki" def
32− ".epub" -> f "epub" def
33− ".fb2" -> f "fb2" def
34− ".htm" -> html
35− ".html" -> html
36− ".icml" -> f "icml" def
37− ".json" -> f "json" def
38− ".latex" -> f "latex" def
39− ".lhs" -> f "markdown+lhs" def
40− ".ltx" -> f "latex" def
24+ ".adoc" -> fWithDefaultFile "asciidoc" def
25+ ".asciidoc" -> fWithDefaultFile "asciidoc" def
26+ ".context" -> fWithDefaultFile "context" def
27+ ".ctx" -> fWithDefaultFile "context" def
28+ ".db" -> fWithDefaultFile "docbook" def
29+ ".doc" -> fWithDefaultFile "doc" def -- so we get an "unknown reader" error
30+ ".docx" -> fWithDefaultFile "docx" def
31+ ".dokuwiki" -> fWithDefaultFile "dokuwiki" def
32+ ".epub" -> fWithDefaultFile "epub" def
33+ ".fb2" -> fWithDefaultFile "fb2" def
34+ ".htm" -> fWithDefaultFile "html" def
35+ ".html" -> fWithDefaultFile "html" def
36+ ".icml" -> fWithDefaultFile "icml" def
37+ ".json" -> fWithDefaultFile "json" def
38+ ".latex" -> fWithDefaultFile "latex" def
39+ ".lhs" -> fWithDefaultFile "markdown+lhs" def
40+ ".ltx" -> fWithDefaultFile "latex" def
4141 ".markdown" -> markdown
4242 ".mkdn" -> markdown
4343 ".mkd" -> markdown
@@ -45,47 +45,50 @@ formatFromFilePath x =
4545 ".mdown" -> markdown
4646 ".Rmd" -> markdown
4747 ".md" -> markdown
48− ".ms" -> f "ms" def
49− ".muse" -> f "muse" def
50− ".native" -> f "native" def
51− ".odt" -> f "odt" def
52− ".opml" -> f "opml" def
53− ".org" -> f "org" def
48+ ".ms" -> fWithDefaultFile "ms" def
49+ ".muse" -> fWithDefaultFile "muse" def
50+ ".native" -> fWithDefaultFile "native" def
51+ ".odt" -> fWithDefaultFile "odt" def
52+ ".opml" -> fWithDefaultFile "opml" def
53+ ".org" -> fWithDefaultFile "org" def
5454 -- so we get an "unknown reader" error
55− ".pdf" -> f "pdf" def
56− ".pptx" -> f "pptx" def
57− ".roff" -> f "ms" def
58− ".rst" -> f "rst" def
59− ".rtf" -> f "rtf" def
60− ".s5" -> f "s5" def
61− ".t2t" -> f "t2t" def
62− ".tei" -> f "tei" def
63− ".tei.xml" -> f "tei" def
64− ".tex" -> f "latex" def
65− ".texi" -> f "texinfo" def
66− ".texinfo" -> f "texinfo" def
55+ ".pdf" -> fWithDefaultFile "pdf" def
56+ ".pptx" -> fWithDefaultFile "pptx" def
57+ ".roff" -> fWithDefaultFile "ms" def
58+ ".rst" -> fWithDefaultFile "rst" def
59+ ".rtf" -> fWithDefaultFile "rtf" def
60+ ".s5" -> fWithDefaultFile "s5" def
61+ ".t2t" -> fWithDefaultFile "t2t" def
62+ ".tei" -> fWithDefaultFile "tei" def
63+ ".tei.xml" -> fWithDefaultFile "tei" def
64+ ".tex" -> fWithDefaultFile "latex" def
65+ ".texi" -> fWithDefaultFile "texinfo" def
66+ ".texinfo" -> fWithDefaultFile "texinfo" def
6767 ".text" -> markdown
68− ".textile" -> f "textile" def
68+ ".textile" -> fWithDefaultFile "textile" def
6969 ".txt" -> markdown
70− ".wiki" -> f "mediawiki" def
71− ".xhtml" -> f "html" def
72− ".ipynb" -> f "ipynb" def
73− ".csv" -> f "csv" def
74− ".bib" -> f "biblatex" def
75− ['.', y] | y `elem` ['1' .. '9'] -> f "man" def
70+ ".wiki" -> fWithDefaultFile "mediawiki" def
71+ ".xhtml" -> fWithDefaultFile "html" def
72+ ".ipynb" -> fWithDefaultFile "ipynb" def
73+ ".csv" -> fWithDefaultFile "csv" def
74+ ".bib" -> fWithDefaultFile "biblatex" def
75+ ['.', y] | y `elem` ['1' .. '9'] -> fWithDefaultFile "man" def
76+ "" ->
77+ (,x <> ".html")
78+ <$> f
79+ "html"
80+ def
81+ { writerTemplate = Just Html.template
82+ , -- Not actually used. Needed to silence warning.
83+ writerVariables = Context $ M.fromList [("pagetitle", toVal ("OpenApi Diff" :: Text))]
84+ }
7685 _ -> Nothing
7786 where
78− markdown, html :: Maybe (Pandoc -> m ByteString)
79− markdown = f "markdown" markdownOpt
80− html =
81− f
82− "html"
83− def
84− { writerTemplate = Just Html.template
85− , -- Not actually used. Needed to silence warning.
86− writerVariables = Context $ M.fromList [("pagetitle", toVal ("OpenApi Diff" :: Text))]
87− }
87+ markdown :: Maybe (Pandoc -> m ByteString, FilePath)
88+ markdown = fWithDefaultFile "markdown" markdownOpt
89+
8890 markdownOpt = def {writerExtensions = githubMarkdownExtensions}
91+ fWithDefaultFile k opt = (,x) <$> f k opt
8992 f k opt =
9093 lookup k writers <&> \case
9194 TextWriter g -> fmap (TL.encodeUtf8 . TL.fromStrict) . g opt
app/Main.hs+2−2
@@ -38,8 +38,8 @@ main = do
3838 StdoutMode -> lift . T.putStrLn <=< runPandocIO . writeMarkdown options
3939 FileMode f -> case formatFromFilePath f of
4040 Nothing -> \_ -> throwError UnknownOutputFormat
41− Just writer -> lift . BSL.writeFile f <=< runPandocIO . writer
42− (report, status) = runReport (a, b)
41+ Just (writer, f') -> lift . BSL.writeFile f' <=< runPandocIO . writer
42+ (report, status) = runReport (reportConfig opts) (a, b)
4343 either handler pure <=< runExceptT $ write report
4444 case status of
4545 NoBreakingChanges -> exitSuccess
app/OpenAPI/Checker/Options.hs+25−6
@@ -8,6 +8,7 @@ module OpenAPI.Checker.Options
88where
99
1010import GHC.Generics (Generic)
11+import OpenAPI.Checker.Report
1112import Options.Applicative
1213
1314data Options = Options
@@ -15,6 +16,7 @@ data Options = Options
1516 , serverFile :: FilePath
1617 , mode :: Mode
1718 , outputMode :: OutputMode
19+ , reportConfig :: ReportConfig
1820 }
1921 deriving stock (Generic)
2022
@@ -56,10 +58,27 @@ optionsParser =
5658 "Report both incompatible and compatible changes. \
5759 \Compatible changes will not trigger a failure exit code.")
5860 <|> pure All)
59− <*> (pure StdoutMode
60− <|> (FileMode
61− <$> strOption
62− (short 'o' <> long "output"
61+ <*> ((FileMode
62+ <$> strOption
63+ (short 'o' <> long "output"
64+ <> help
65+ "The file path where the output should be writtrn. \
66+ \Leave blank to output result to stdout."))
67+ <|> pure StdoutMode)
68+ <*> (ReportConfig
69+ <$> (flag'
70+ FoldingBlockquotesTreeStyle
71+ (long "folding-block-quotes-style"
72+ <> help
73+ "The report tree is structured using \
74+ \summary/detail HTML elements and indented using \
75+ \block quotes. This style renders well on GitHub.\
76+ \Intended for HTML output format. Markdown has rendering \
77+ \bugs on GitHub.")
78+ <|> flag'
79+ HeadersTreeStyle
80+ (long "header-style"
6381 <> help
64− "The file path where the output should be writtrn. \
65− \Leave blank to output result to stdout.")))
82+ "The report tree is structured using \
83+ \increasing levels of headers.")
84+ <|> pure HeadersTreeStyle))
openapi-diff.cabal+1
@@ -108,6 +108,7 @@ library
108108 , generic-data
109109 , doctemplates
110110 , file-embed
111+ , data-default
111112 hs-source-dirs: src
112113 exposed-modules: Data.HList
113114 , OpenAPI.Checker.Behavior
src/OpenAPI/Checker/Report.hs+67−29
@@ -3,6 +3,8 @@ module OpenAPI.Checker.Report
33 , ReportInput (..)
44 , ReportStatus (..)
55 , Pandoc
6+ , ReportConfig (..)
7+ , ReportTreeStyle (..)
68 )
79where
810
@@ -10,6 +12,7 @@ import Control.Monad.Free hiding (unfoldM)
1012import Control.Monad.Reader
1113import Control.Monad.Writer
1214import Data.Aeson (ToJSON)
15+import Data.Default
1316import Data.Either
1417import Data.Foldable
1518import Data.Function
@@ -54,8 +57,20 @@ data ReportStatus
5457 -- there actually are any breaking changes.
5558 OnlyUnsupportedChanges
5659
57−generateReport :: ReportInput -> (Pandoc, ReportStatus)
58−generateReport inp =
60+data ReportConfig = ReportConfig
61+ { treeStyle :: ReportTreeStyle
62+ }
63+
64+instance Default ReportConfig where
65+ def =
66+ ReportConfig
67+ { treeStyle = HeadersTreeStyle
68+ }
69+
70+data ReportTreeStyle = HeadersTreeStyle | FoldingBlockquotesTreeStyle
71+
72+generateReport :: ReportConfig -> ReportInput -> (Pandoc, ReportStatus)
73+generateReport cfg inp =
5974 let partitionUnsupported = P.partition (\(AnIssue _ i) -> issueIsUnsupported i)
6075 (bUnsupported, breaking) = partitionUnsupported $ breakingChanges inp
6176 (nbUnsupported, invertIssueOrientationP -> nonBreaking) = partitionUnsupported $ nonBreakingChanges inp
@@ -64,8 +79,8 @@ generateReport inp =
6479 nonBreakingChangesPresent = not $ P.null nonBreaking
6580 unsupportedChangesPresent = not $ P.null unsupported
6681 report = doc $
67− runReportMonad jets $ do
68− smartHeader "Summary"
82+ runReportMonad cfg jets $ do
83+ tell $ header 1 "Summary"
6984 tell $
7085 simpleTable
7186 (para
@@ -75,13 +90,13 @@ generateReport inp =
7590 ])
7691 [para . show' <$> [P.size breaking, P.size nonBreaking, P.size unsupported]]
7792 when breakingChangesPresent $ do
78− smartHeader $ anchor breakingChangesId <> "⚠️ Breaking changes"
93+ tell $ header 1 $ anchor breakingChangesId <> "⚠️ Breaking changes"
7994 incrementHeaders $ showErrs breaking
8095 when nonBreakingChangesPresent $ do
81− smartHeader $ anchor nonBreakingChangesId <> "🙆 Non-breaking changes"
96+ tell $ header 1 $ anchor nonBreakingChangesId <> "🙆 Non-breaking changes"
8297 incrementHeaders $ showErrs nonBreaking
8398 when unsupportedChangesPresent $ do
84− smartHeader $ anchor unsupportedChangesId <> "🤷 Unsupported feature changes"
99+ tell $ header 1 $ anchor unsupportedChangesId <> "🤷 Unsupported feature changes"
85100 incrementHeaders $ showErrs unsupported
86101 status =
87102 if
@@ -105,24 +120,45 @@ generateReport inp =
105120data ReportState = ReportState
106121 { sourceJets :: [ReportJet' Behave Inlines]
107122 , headerLevel :: Int
123+ , headerHandle :: Inlines -> ReportMonad () -> ReportMonad ()
124+ , incrementHeadersHandle :: ReportState -> ReportState
108125 }
109126
110127type ReportMonad = ReaderT ReportState (Writer Blocks)
111128
112−runReportMonad :: [ReportJet' Behave Inlines] -> ReportMonad () -> Blocks
113−runReportMonad jts =
129+runReportMonad :: ReportConfig -> [ReportJet' Behave Inlines] -> ReportMonad () -> Blocks
130+runReportMonad cfg jts =
114131 execWriter
115132 . flip
116133 runReaderT
117134 ReportState
118135 { sourceJets = jts
119136 , headerLevel = 1
137+ , headerHandle =
138+ case treeStyle cfg of
139+ HeadersTreeStyle -> \i body -> do
140+ h <- asks headerLevel
141+ tell $ header h i
142+ body
143+ FoldingBlockquotesTreeStyle -> \i body -> do
144+ tell $
145+ rawHtml "<details>"
146+ <> rawHtml "<summary>"
147+ <> plain i
148+ <> rawHtml "</summary>"
149+ (mapReaderT . mapWriterT . fmap . fmap) blockQuote $ body
150+ tell $ rawHtml "</details>"
151+ where
152+ rawHtml = rawBlock "html"
153+ , incrementHeadersHandle = case treeStyle cfg of
154+ HeadersTreeStyle -> \l -> l {headerLevel = headerLevel l + 1}
155+ FoldingBlockquotesTreeStyle -> id
120156 }
121157
122−smartHeader :: Inlines -> ReportMonad ()
123−smartHeader i = do
124− h <- asks headerLevel
125− tell $ header h i
158+smartHeader :: Inlines -> ReportMonad () -> ReportMonad ()
159+smartHeader i body = do
160+ f <- asks headerHandle
161+ f i body
126162
127163showErrs :: forall a. Typeable a => P.PathsPrefixTree Behave AnIssue a -> ReportMonad ()
128164showErrs x@(P.PathsPrefixNode currentIssues _) = do
@@ -141,24 +177,27 @@ showErrs x@(P.PathsPrefixNode currentIssues _) = do
141177 in (Just (ori, p'), o)
142178 _ -> (Nothing, currentIssues)
143179 jts <- asks sourceJets
144− for_ otherIssues $ \(AnIssue ori i) -> tell . describeIssue ori $ i
180+ tell $ case S.toList otherIssues of
181+ [AnIssue ori i] -> describeIssue ori i
182+ ii -> orderedList $ ii <&> (\(AnIssue ori i) -> describeIssue ori i)
145183 case removedPaths of
146184 Just (ori, paths) -> do
147− smartHeader $ case ori of
148− Forward -> "Removed paths"
149− Backward -> "Added paths"
150− tell $
151− bulletList $
152− paths <&> \case
153− (NoPathsMatched p) -> para . code $ T.pack p
154− (AllPathsFailed p) -> para . code $ T.pack p
185+ smartHeader
186+ (case ori of
187+ Forward -> "Removed paths"
188+ Backward -> "Added paths")
189+ $ tell $
190+ bulletList $
191+ paths <&> \case
192+ (NoPathsMatched p) -> para . code $ T.pack p
193+ (AllPathsFailed p) -> para . code $ T.pack p
155194 Nothing -> pure ()
156195 unfoldM x (observeJetShowErrs <$> jts) $ \(P.PathsPrefixNode _ subIssues) -> do
157196 for_ subIssues $ \(WrapTypeable (AStep m)) ->
158197 for_ (M.toList m) $ \(bhv, subErrors) -> do
159− unless (P.null subErrors) $ do
160− smartHeader $ describeBehaviour bhv
161− incrementHeaders $ showErrs subErrors
198+ unless (P.null subErrors) $
199+ smartHeader (describeBehaviour bhv) $
200+ incrementHeaders $ showErrs subErrors
162201
163202unfoldM :: Monad m => a -> [a -> m a] -> (a -> m ()) -> m ()
164203unfoldM a [] g = g a
@@ -168,8 +207,8 @@ unfoldM a (f : ff) g = do
168207
169208incrementHeaders :: ReportMonad x -> ReportMonad x
170209incrementHeaders m = do
171− l <- asks headerLevel
172− local (\x -> x {headerLevel = l + 1}) m
210+ f <- asks incrementHeadersHandle
211+ local f m
173212
174213jets :: [ReportJet' Behave Inlines]
175214jets =
@@ -241,8 +280,7 @@ observeJetShowErrs' (ReportJet jet) (P.PathsPrefixNode currentIssues subIssues)
241280 Free jet' -> fmap (embed $ step bhv) <$> observeJetShowErrs' jet' subErrs
242281 Pure h -> Just $ do
243282 unless (P.null subErrs) $ do
244− smartHeader h
245− incrementHeaders $ showErrs subErrs
283+ smartHeader h $ incrementHeaders $ showErrs subErrs
246284 return mempty)
247285 in (fmap . fmap) (PathsPrefixNode currentIssues mempty <>) $
248286 if any isRight results
src/OpenAPI/Checker/Run.hs+2−2
@@ -26,5 +26,5 @@ runChecker (client, server) =
2626 }
2727 run p c = either id mempty . runCompatFormula . checkCompatibility Root HNil $ toPC p c
2828
29−runReport :: (OpenApi, OpenApi) -> (Pandoc, ReportStatus)
30−runReport = generateReport . runChecker
29+runReport :: ReportConfig -> (OpenApi, OpenApi) -> (Pandoc, ReportStatus)
30+runReport cfg = generateReport cfg . runChecker
test/Spec/Golden/TraceTree.hs+1−1
@@ -37,7 +37,7 @@ tests = do
3737 "report.md"
3838 ("a.yaml", "b.yaml")
3939 Yaml.decodeFileThrow
40− (runPandoc . writeMarkdown def {writerExtensions = githubMarkdownExtensions} . fst . runReport)
40+ (runPandoc . writeMarkdown def {writerExtensions = githubMarkdownExtensions} . fst . runReport def)
4141
4242 return $ testGroup "Golden tests" [traceTreeTests, reportTests]
4343
test/golden/common/servers/report.md+2−2
@@ -14,9 +14,9 @@ The server was removed.
1414
1515### Server `http://{x}variable.path/{y}/{openVariable1}/{openVariable2}`
1616
17−Enum value `a` has been removed.
17+1. Enum value `a` has been removed.
1818
19−A variable has been changed from being open to being closed.
19+2. A variable has been changed from being open to being closed.
2020
2121# <span id="non-breaking-changes"></span>🙆 Non-breaking changes
2222