Skip to content

Commit 1a79b0d

Browse files
committed
Improves OpenAPI spec names and types and error reporting
This uses the new `json-fleece` API to improve the names and types given to OpenAPI specs. This includes a change that restricts the characters allowed in the name of an OpenAPI spec. This default is to allow alphanumeric characters, plus '.' and '_'. The list of allowed characters can be specified both via the various functions that build OpenAPI specs and the default command line generation command. This also improves the error reporting when schemas have conflicts or bad names to try to provide context of where the offending spec is contained within the spec definitions so that the developer can more easily locate the offending specs within the code.
1 parent da0e6ef commit 1a79b0d

20 files changed

+917
-297
lines changed

.helix/languages.toml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
[language-server.haskell-language-server]
2+
command = "docker"
3+
args = ["compose", "run", "--rm", "-T", "dev", "haskell-language-server-wrapper", "--lsp"]
4+
5+
[language-server.haskell-language-server.config]
6+
haskell.formattingProvider = "fourmolu"
7+
haskell.plugin.hlint.globalOn = false
8+

orb.cabal

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ cabal-version: 1.12
55
-- see: https://github.com/sol/hpack
66

77
name: orb
8-
version: 0.5.0.2
8+
version: 0.6.0.0
99
description: Please see the README on GitHub at <https://github.com/flipstone/orb#readme>
1010
homepage: https://github.com/flipstone/orb#readme
1111
bug-reports: https://github.com/flipstone/orb/issues
@@ -127,6 +127,7 @@ test-suite orb-test
127127
Fixtures.OpenApiSubset
128128
Fixtures.SimpleGet
129129
Fixtures.SimplePost
130+
Fixtures.TaggedUnion
130131
Fixtures.Union
131132
Handler
132133
OpenApi

package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: orb
2-
version: 0.5.0.2
2+
version: 0.6.0.0
33
github: "flipstone/orb"
44
license: MIT
55
author: "Flipstone Technology Partners, Inc"

src/Orb/Main.hs

Lines changed: 121 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,20 @@
11
module Orb.Main
22
( main
3+
, mainWithOptions
34
, mainParserInfo
45
, mainParser
56
, mainParserWithCommands
7+
, openApiOptionsParser
68
, openApiLabelArgument
79
, generateOpenApiCommand
810
, generateOpenApiMain
911
) where
1012

1113
import Data.Aeson.Encode.Pretty qualified as AesonPretty
1214
import Data.ByteString.Lazy qualified as LBS
15+
import Data.Foldable (traverse_)
1316
import Data.List qualified as List
17+
import Data.Set qualified as Set
1418
import Options.Applicative qualified as Opt
1519
import System.Exit qualified as Exit
1620
import System.IO qualified as IO
@@ -30,25 +34,64 @@ import Orb.OpenApi qualified as OpenApi
3034
of the labeled routes as JSON to stdout.
3135
-}
3236
main :: OpenApi.OpenApiRouter a -> IO () -> IO ()
33-
main routes appMain = do
34-
io <- Opt.customExecParser parserPrefs (mainParserInfo appMain routes)
37+
main =
38+
mainWithOptions OpenApi.defaultOpenApiOptions
39+
40+
{- |
41+
Constructs a main function that parses the command line arguments and
42+
provides two subcommands in the executable:
43+
44+
- @api@ - runs the IO action provided as the main. Presumably this
45+
invokes some form of 'Orb.Wai.runOrb' (or otherwise runs a way server)
46+
that serves an application handling the routes provided to this function.
47+
48+
- @generate-open-api@ - accepts a argument matching one of the labels
49+
provided to 'OpenApi.provideOpenApi' and prints an OpenApi description
50+
of the labeled routes as JSON to stdout.
51+
52+
53+
A version of 'main' takes an 'OpenApi.OpenApiOptions'
54+
value to use as the default options to the @generate-open-api@
55+
command. If you use this function, you probably also want
56+
to use 'Orb.SwaggerUI.swaggerUIRoutesWithOptions' instead of
57+
'Orb.SwaggerUI.swaggerUIRoutes' and specify the same options that
58+
you're passing to this function.
59+
-}
60+
mainWithOptions :: OpenApi.OpenApiOptions -> OpenApi.OpenApiRouter a -> IO () -> IO ()
61+
mainWithOptions defaultOptions routes appMain = do
62+
io <- Opt.customExecParser parserPrefs (mainParserInfo defaultOptions appMain routes)
3563
io
3664

3765
{- |
3866
Constructs a 'Opt.ParserInfo' that will execute as description in 'main', but
3967
can be used as an argument to 'Opt.command' to use it as a subcommand.
68+
69+
The options passed will be used as the default values for when no options
70+
are specified on the command line.
4071
-}
41-
mainParserInfo :: IO () -> OpenApi.OpenApiRouter a -> Opt.ParserInfo (IO ())
42-
mainParserInfo apiMain routes =
43-
Opt.info (mainParser apiMain routes) mempty
72+
mainParserInfo ::
73+
OpenApi.OpenApiOptions ->
74+
IO () ->
75+
OpenApi.OpenApiRouter a ->
76+
Opt.ParserInfo (IO ())
77+
mainParserInfo defaultOptions apiMain routes =
78+
Opt.info (mainParser defaultOptions apiMain routes) mempty
4479

4580
{- |
4681
Constructs a 'Opt.Parser' that will execute as description in 'main', but
4782
can be used directly in other option parsers.
83+
84+
The options passed will be used as the default values for when no options
85+
are specified on the command line.
4886
-}
49-
mainParser :: IO () -> OpenApi.OpenApiRouter a -> Opt.Parser (IO ())
50-
mainParser apiMain =
87+
mainParser ::
88+
OpenApi.OpenApiOptions ->
89+
IO () ->
90+
OpenApi.OpenApiRouter a ->
91+
Opt.Parser (IO ())
92+
mainParser defaultOptions apiMain =
5193
mainParserWithCommands
94+
defaultOptions
5295
[ Opt.command "api" (Opt.info (pure apiMain) mempty)
5396
]
5497

@@ -57,15 +100,19 @@ mainParser apiMain =
57100
the 'main' function along with the other commands passed. In this case no
58101
@api@ command is added by orb. It is up to the application to passed
59102
whatever set of other commands it desires.
103+
104+
The options passed will be used as the default values for when no options
105+
are specified on the command line.
60106
-}
61107
mainParserWithCommands ::
108+
OpenApi.OpenApiOptions ->
62109
[Opt.Mod Opt.CommandFields (IO ())] ->
63110
OpenApi.OpenApiRouter a ->
64111
Opt.Parser (IO ())
65-
mainParserWithCommands commands routes =
112+
mainParserWithCommands defaultOptions commands routes =
66113
Opt.hsubparser $
67114
mconcat
68-
( generateOpenApiCommand routes
115+
( generateOpenApiCommandWithOptions defaultOptions routes
69116
: commands
70117
)
71118

@@ -74,9 +121,67 @@ mainParserWithCommands commands routes =
74121
can be used along with 'Opt.hsubparser' include the command wherever the user
75122
chooses in their options parsing.
76123
-}
77-
generateOpenApiCommand :: OpenApi.OpenApiRouter a -> Opt.Mod Opt.CommandFields (IO ())
124+
generateOpenApiCommand ::
125+
OpenApi.OpenApiRouter a ->
126+
Opt.Mod Opt.CommandFields (IO ())
78127
generateOpenApiCommand routes =
79-
Opt.command "generate-open-api" (Opt.info (generateOpenApiMain routes <$> openApiLabelArgument routes) mempty)
128+
let
129+
parser =
130+
generateOpenApiMain
131+
<$> openApiOptionsParser OpenApi.defaultOpenApiOptions
132+
<*> pure routes
133+
<*> openApiLabelArgument routes
134+
in
135+
Opt.command "generate-open-api" (Opt.info parser mempty)
136+
137+
{- |
138+
Constructs an 'Opt.command' modifier for the @generate-open-api@ command that
139+
can be used along with 'Opt.hsubparser' include the command wherever the user
140+
chooses in their options parsing.
141+
142+
The options passed will be used as the default values for when no options
143+
are specified on the command line.
144+
145+
A version of 'generateOpenApiCommand' that takes the options argument.
146+
-}
147+
generateOpenApiCommandWithOptions ::
148+
OpenApi.OpenApiOptions ->
149+
OpenApi.OpenApiRouter a ->
150+
Opt.Mod Opt.CommandFields (IO ())
151+
generateOpenApiCommandWithOptions defaultOptions routes =
152+
let
153+
parser =
154+
generateOpenApiMain
155+
<$> openApiOptionsParser defaultOptions
156+
<*> pure routes
157+
<*> openApiLabelArgument routes
158+
in
159+
Opt.command "generate-open-api" (Opt.info parser mempty)
160+
161+
{- |
162+
Constructs a 'Opt.Parser' that will parse command line options to control
163+
how the OpenApi spec is generated.
164+
165+
The options passed will be used as the default values for when no options
166+
are specified on the command line.
167+
-}
168+
openApiOptionsParser :: OpenApi.OpenApiOptions -> Opt.Parser OpenApi.OpenApiOptions
169+
openApiOptionsParser defaultOptions =
170+
let
171+
mkOpts allowedChars =
172+
OpenApi.defaultOpenApiOptions
173+
{ OpenApi.openApiAllowedSchemaNameChars = Set.fromList allowedChars
174+
}
175+
in
176+
mkOpts
177+
<$> Opt.option
178+
Opt.str
179+
( Opt.long "allowed-chars"
180+
<> Opt.metavar "CHARS"
181+
<> Opt.help "e.g. abcdefg12345"
182+
<> Opt.value (Set.toList (OpenApi.openApiAllowedSchemaNameChars defaultOptions))
183+
<> Opt.showDefault
184+
)
80185

81186
{- |
82187
Constructs a 'Opt.Parser' than will parse an argument representing one
@@ -110,12 +215,12 @@ parserPrefs =
110215
the @generate-open-api@ command in their own main functions without
111216
using @optparse-applicative@.
112217
-}
113-
generateOpenApiMain :: OpenApi.OpenApiRouter a -> String -> IO ()
114-
generateOpenApiMain routes label =
115-
case OpenApi.mkOpenApi routes label of
116-
Left err -> do
218+
generateOpenApiMain :: OpenApi.OpenApiOptions -> OpenApi.OpenApiRouter a -> String -> IO ()
219+
generateOpenApiMain options routes label =
220+
case OpenApi.mkOpenApi options routes label of
221+
Left errs -> do
117222
IO.hPutStrLn IO.stderr ("Unable to generate OpenApi Spec for " <> label <> "!")
118-
IO.hPutStrLn IO.stderr err
223+
traverse_ (IO.hPutStrLn IO.stderr . OpenApi.renderOpenApiError) errs
119224
Exit.exitWith (Exit.ExitFailure 1)
120225
Right openApi ->
121226
LBS.putStr (AesonPretty.encodePretty openApi)

0 commit comments

Comments
 (0)