Skip to content

Commit 0eb15be

Browse files
authored
Single fielded oneof (#118)
* Detect oneof branches with single fields. When specified, name oneof variants after those fields instead of with numbers. Fix up some utilities for consistency, and add a Field type instead of recomputing property names. * Add a test * Make it generate the same thing it was generating before * Make it the default behavior * Bump version * Re-add property suffix * Regenerate testing files * Make shortening single-field objects configurable because it doesn't work 100% of the time. * Use property type suffix the way it was before
1 parent db004b9 commit 0eb15be

File tree

18 files changed

+209
-97
lines changed

18 files changed

+209
-97
lines changed

example/generatedCode/src/OpenAPI/Common.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ createBaseRequest config method path queryParams =
255255
else basePath
256256
-- filters all maybe
257257
query = BF.second pure <$> serializeQueryParams queryParams
258-
userAgent = configApplicationName config <> " openapi3-code-generator/0.1.0.7 (https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator)"
258+
userAgent = configApplicationName config <> " openapi3-code-generator/0.2.0.0 (https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator)"
259259
addUserAgent =
260260
if configIncludeUserAgent config
261261
then HS.addRequestHeader HT.hUserAgent $ textToByte userAgent

openapi3-code-generator/default.nix

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
}:
99
mkDerivation {
1010
pname = "openapi3-code-generator";
11-
version = "0.1.0.7";
11+
version = "0.2.0.0";
1212
src = ./.;
1313
isLibrary = true;
1414
isExecutable = true;

openapi3-code-generator/openapi3-code-generator.cabal

Lines changed: 1 addition & 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: openapi3-code-generator
8-
version: 0.1.0.7
8+
version: 0.2.0.0
99
synopsis: OpenAPI3 Haskell Client Code Generator
1010
description: Please see the README on GitHub at <https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator#readme>
1111
category: Code-Generator

openapi3-code-generator/package.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
name: openapi3-code-generator
2-
version: 0.1.0.7
2+
version: 0.2.0.0
33
github: "Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator"
44
author: "Remo Dörig & Joel Fisch"
55
synopsis: OpenAPI3 Haskell Client Code Generator

openapi3-code-generator/src/OpenAPI/Generate/Internal/Util.hs

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ haskellifyText ::
4949
-- | The identifier to transform
5050
Text ->
5151
-- | The resulting identifier
52-
String
52+
Text
5353
haskellifyText convertToCamelCase startWithUppercase name =
5454
let casefn = if startWithUppercase then Char.toUpper else Char.toLower
5555
replaceChar '.' = '\''
@@ -94,18 +94,19 @@ haskellifyText convertToCamelCase startWithUppercase name =
9494
replacePlus ('+' : rest) = "Plus" <> replacePlus rest
9595
replacePlus (x : xs) = x : replacePlus xs
9696
replacePlus a = a
97-
in replaceReservedWord $
98-
caseFirstCharCorrectly $
99-
generateNameForEmptyIdentifier name $
100-
removeIllegalLeadingCharacters $
101-
(if convertToCamelCase then toCamelCase else id) $
102-
nameWithoutSpecialChars $
103-
replacePlus $
104-
T.unpack name
97+
in T.pack $
98+
replaceReservedWord $
99+
caseFirstCharCorrectly $
100+
generateNameForEmptyIdentifier name $
101+
removeIllegalLeadingCharacters $
102+
(if convertToCamelCase then toCamelCase else id) $
103+
nameWithoutSpecialChars $
104+
replacePlus $
105+
T.unpack name
105106

106107
-- | The same as 'haskellifyText' but transform the result to a 'Name'
107108
haskellifyName :: Bool -> Bool -> Text -> Name
108-
haskellifyName convertToCamelCase startWithUppercase name = mkName $ haskellifyText convertToCamelCase startWithUppercase name
109+
haskellifyName convertToCamelCase startWithUppercase name = mkName . T.unpack $ haskellifyText convertToCamelCase startWithUppercase name
109110

110111
-- | 'OAM.Generator' version of 'haskellifyName'
111112
haskellifyNameM :: Bool -> Text -> OAM.Generator Name

openapi3-code-generator/src/OpenAPI/Generate/Model.hs

Lines changed: 108 additions & 68 deletions
Large diffs are not rendered by default.

openapi3-code-generator/src/OpenAPI/Generate/Operation.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -87,7 +87,7 @@ defineModuleForOperation mainModuleName requestPath method operation = OAM.neste
8787
convertToCamelCase <- OAM.getSetting OAO.settingConvertToCamelCase
8888
let operationIdAsText = T.pack $ show operationIdName
8989
appendToOperationName = ((T.pack $ nameBase operationIdName) <>)
90-
moduleName = haskellifyText convertToCamelCase True operationIdAsText
90+
moduleName = T.unpack $ haskellifyText convertToCamelCase True operationIdAsText
9191
OAM.logInfo $ "Generating operation with name '" <> operationIdAsText <> "'"
9292
(bodySchema, bodyPath) <- getBodySchemaFromOperation operation
9393
(responseTypeName, responseTransformerExp, responseBodyDefinitions, responseBodyDependencies) <- OAR.getResponseDefinitions operation appendToOperationName

openapi3-code-generator/src/OpenAPI/Generate/OptParse.hs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,10 @@ data Settings = Settings
110110
-- in the 'FromJSON' instance.
111111
-- This setting allows to change this behavior by including all fixed value
112112
-- fields instead ("include" strategy), i.e. just not trying to do anything smart.
113-
settingFixedValueStrategy :: !FixedValueStrategy
113+
settingFixedValueStrategy :: !FixedValueStrategy,
114+
-- | When encountering an object with a single field, shorten the field of that object to be the
115+
-- schema name. Respects property type suffix.
116+
settingShortenSingleFieldObjects :: !Bool
114117
}
115118
deriving (Show, Eq)
116119

@@ -157,6 +160,7 @@ combineToSettings Flags {..} mConf configurationFilePath = do
157160
settingWhiteListedSchemas = fromMaybe [] $ flagWhiteListedSchemas <|> mc configWhiteListedSchemas
158161
settingOutputAllSchemas = fromMaybe False $ flagOutputAllSchemas <|> mc configOutputAllSchemas
159162
settingFixedValueStrategy = fromMaybe FixedValueStrategyExclude $ flagFixedValueStrategy <|> mc configFixedValueStrategy
163+
settingShortenSingleFieldObjects = fromMaybe False $ flagShortenSingleFieldObjects <|> mc configShortenSingleFieldObjects
160164

161165
pure Settings {..}
162166
where

openapi3-code-generator/src/OpenAPI/Generate/OptParse/Configuration.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,8 @@ data Configuration = Configuration
4949
configOpaqueSchemas :: !(Maybe [Text]),
5050
configWhiteListedSchemas :: !(Maybe [Text]),
5151
configOutputAllSchemas :: !(Maybe Bool),
52-
configFixedValueStrategy :: !(Maybe FixedValueStrategy)
52+
configFixedValueStrategy :: !(Maybe FixedValueStrategy),
53+
configShortenSingleFieldObjects :: !(Maybe Bool)
5354
}
5455
deriving stock (Show, Eq)
5556
deriving (FromJSON, ToJSON) via (Autodocodec Configuration)
@@ -91,6 +92,7 @@ instance HasCodec Configuration where
9192
<*> optionalField "whiteListedSchemas" "A list of schema names (exactly as they are named in the components.schemas section of the corresponding OpenAPI 3 specification) which need to be generated. For all other schemas only a type alias to 'Aeson.Value' is created." .= configWhiteListedSchemas
9293
<*> optionalField "outputAllSchemas" "Output all component schemas" .= configOutputAllSchemas
9394
<*> optionalField "fixedValueStrategy" "In OpenAPI 3, fixed values can be defined as an enum with only one allowed value. If such a constant value is encountered as a required property of an object, the generator excludes this property by default ('exclude' strategy) and adds the value in the 'ToJSON' instance and expects the value to be there in the 'FromJSON' instance. This setting allows to change this behavior by including all fixed value fields instead ('include' strategy), i.e. just not trying to do anything smart." .= configFixedValueStrategy
95+
<*> optionalField "shortenSingleFieldObjects" "When encountering an object with a single field, shorten the field of that object to be the schema name. Respects property type suffix." .= configShortenSingleFieldObjects
9496

9597
getConfiguration :: Text -> IO (Maybe Configuration)
9698
getConfiguration path = resolveFile' (T.unpack path) >>= readYamlConfigFile

openapi3-code-generator/src/OpenAPI/Generate/OptParse/Flags.hs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,8 @@ data Flags = Flags
4646
flagOpaqueSchemas :: !(Maybe [Text]),
4747
flagWhiteListedSchemas :: !(Maybe [Text]),
4848
flagOutputAllSchemas :: !(Maybe Bool),
49-
flagFixedValueStrategy :: !(Maybe FixedValueStrategy)
49+
flagFixedValueStrategy :: !(Maybe FixedValueStrategy),
50+
flagShortenSingleFieldObjects :: !(Maybe Bool)
5051
}
5152
deriving (Show, Eq)
5253

@@ -87,6 +88,7 @@ parseFlags =
8788
<*> parseFlagWhiteListedSchemas
8889
<*> parseFlagOutputAllSchemas
8990
<*> parseFlagFixedValueStrategy
91+
<*> parseFlagShortenSingleFieldObjects
9092

9193
parseFlagConfiguration :: Parser (Maybe Text)
9294
parseFlagConfiguration =
@@ -382,3 +384,7 @@ parseFlagFixedValueStrategy =
382384
help "In OpenAPI 3, fixed values can be defined as an enum with only one allowed value. If such a constant value is encountered as a required property of an object, the generator excludes this property by default ('exclude' strategy) and adds the value in the 'ToJSON' instance and expects the value to be there in the 'FromJSON' instance. This setting allows to change this behavior by including all fixed value fields instead ('include' strategy), i.e. just not trying to do anything smart (default: 'exclude').",
383385
long "fixed-value-strategy"
384386
]
387+
388+
parseFlagShortenSingleFieldObjects :: Parser (Maybe Bool)
389+
parseFlagShortenSingleFieldObjects =
390+
booleanFlag "When encountering an object with a single field, shorten the field of that object to be the schema name. Respects property type suffix." "shorten-single-field-objects" Nothing

openapi3-code-generator/src/OpenAPI/Generate/Response.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,7 @@ errorSuffix = "Error"
114114

115115
-- | Create the name as 'Text' of the response type / data constructor based on a suffix
116116
createResponseNameAsText :: Bool -> (Text -> Text) -> Text -> Text
117-
createResponseNameAsText convertToCamelCase appendToOperationName = T.pack . haskellifyText convertToCamelCase True . appendToOperationName
117+
createResponseNameAsText convertToCamelCase appendToOperationName = haskellifyText convertToCamelCase True . appendToOperationName
118118

119119
-- | Create the name as 'Name' of the response type / data constructor based on a suffix
120120
createResponseName :: Bool -> (Text -> Text) -> Text -> Name

openapi3-code-generator/test/OpenAPI/Generate/Internal/UtilSpec.hs

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,18 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
13
module OpenAPI.Generate.Internal.UtilSpec where
24

35
import qualified Data.Char as Char
46
import Data.GenValidity.Text ()
7+
import Data.Text (Text)
58
import qualified Data.Text as T
69
import Data.Validity.Text ()
710
import OpenAPI.Generate.Internal.Util
811
import Test.Hspec
912
import Test.Validity
1013

1114
-- See https://www.haskell.org/onlinereport/lexemes.html § 2.4
12-
isValidVarId :: String -> Bool
13-
isValidVarId "" = False
15+
isValidVarId :: Text -> Bool
1416
isValidVarId "case" = False
1517
isValidVarId "class" = False
1618
isValidVarId "data" = False
@@ -32,11 +34,14 @@ isValidVarId "of" = False
3234
isValidVarId "then" = False
3335
isValidVarId "type" = False
3436
isValidVarId "where" = False
35-
isValidVarId (x : xs) = isValidSmall x && isValidSuffix xs
37+
isValidVarId other = case T.unpack other of
38+
[] -> False
39+
(x : xs) -> isValidSmall x && isValidSuffix xs
3640

37-
isValidConId :: String -> Bool
38-
isValidConId "" = False
39-
isValidConId (x : xs) = isValidLarge x && isValidSuffix xs
41+
isValidConId :: Text -> Bool
42+
isValidConId other = case T.unpack other of
43+
[] -> False
44+
(x : xs) -> isValidLarge x && isValidSuffix xs
4045

4146
isValidSmall :: Char -> Bool
4247
isValidSmall x = x == '_' || Char.isLower x
@@ -65,4 +70,4 @@ spec = do
6570
describe "transformToModuleName" $
6671
it "should be valid module name" $
6772
forAllValid $
68-
isValidConId . T.unpack . transformToModuleName
73+
isValidConId . transformToModuleName

specifications/z_complex_self_made_example.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -202,6 +202,12 @@ components:
202202
type: string
203203
hunts:
204204
type: boolean
205+
- type: object
206+
required:
207+
- another_cat
208+
properties:
209+
another_cat:
210+
$ref: '#/components/schemas/Cat'
205211
relative:
206212
anyOf:
207213
- $ref: '#/components/schemas/Cat'

testing/golden-output/src/OpenAPI/Common.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -255,7 +255,7 @@ createBaseRequest config method path queryParams =
255255
else basePath
256256
-- filters all maybe
257257
query = BF.second pure <$> serializeQueryParams queryParams
258-
userAgent = configApplicationName config <> " openapi3-code-generator/0.1.0.7 (https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator)"
258+
userAgent = configApplicationName config <> " openapi3-code-generator/0.2.0.0 (https://github.com/Haskell-OpenAPI-Code-Generator/Haskell-OpenAPI-Client-Code-Generator)"
259259
addUserAgent =
260260
if configIncludeUserAgent config
261261
then HS.addRequestHeader HT.hUserAgent $ textToByte userAgent

testing/golden-output/src/OpenAPI/Types/Mischling.hs

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,6 +155,23 @@ instance Data.Aeson.Types.FromJSON.FromJSON MischlingAnother_relativeOneOf5
155155
mkMischlingAnother_relativeOneOf5 :: MischlingAnother_relativeOneOf5
156156
mkMischlingAnother_relativeOneOf5 = MischlingAnother_relativeOneOf5{mischlingAnother_relativeOneOf5Hunts = GHC.Maybe.Nothing,
157157
mischlingAnother_relativeOneOf5Pet_type = GHC.Maybe.Nothing}
158+
-- | Defines the object schema located at @components.schemas.Mischling.allOf.properties.another_relative.oneOf@ in the specification.
159+
--
160+
--
161+
data MischlingAnother_relativeAnother_cat = MischlingAnother_relativeAnother_cat {
162+
-- | another_cat
163+
mischlingAnother_relativeAnother_catAnother_cat :: Cat
164+
} deriving (GHC.Show.Show
165+
, GHC.Classes.Eq)
166+
instance Data.Aeson.Types.ToJSON.ToJSON MischlingAnother_relativeAnother_cat
167+
where {toJSON obj = Data.Aeson.Types.Internal.object (Data.Foldable.concat (["another_cat" Data.Aeson.Types.ToJSON..= mischlingAnother_relativeAnother_catAnother_cat obj] : GHC.Base.mempty));
168+
toEncoding obj = Data.Aeson.Encoding.Internal.pairs (GHC.Base.mconcat (Data.Foldable.concat (["another_cat" Data.Aeson.Types.ToJSON..= mischlingAnother_relativeAnother_catAnother_cat obj] : GHC.Base.mempty)))}
169+
instance Data.Aeson.Types.FromJSON.FromJSON MischlingAnother_relativeAnother_cat
170+
where {parseJSON = Data.Aeson.Types.FromJSON.withObject "MischlingAnother_relativeAnother_cat" (\obj -> GHC.Base.pure MischlingAnother_relativeAnother_cat GHC.Base.<*> (obj Data.Aeson.Types.FromJSON..: "another_cat"))}
171+
-- | Create a new 'MischlingAnother_relativeAnother_cat' with all required fields.
172+
mkMischlingAnother_relativeAnother_cat :: Cat -- ^ 'mischlingAnother_relativeAnother_catAnother_cat'
173+
-> MischlingAnother_relativeAnother_cat
174+
mkMischlingAnother_relativeAnother_cat mischlingAnother_relativeAnother_catAnother_cat = MischlingAnother_relativeAnother_cat{mischlingAnother_relativeAnother_catAnother_cat = mischlingAnother_relativeAnother_catAnother_cat}
158175
-- | Defines the oneOf schema located at @components.schemas.Mischling.allOf.properties.another_relative.oneOf@ in the specification.
159176
--
160177
--
@@ -166,19 +183,21 @@ data MischlingAnother_relativeVariants =
166183
| MischlingAnother_relativeText Data.Text.Internal.Text
167184
| MischlingAnother_relativeListTText [Data.Text.Internal.Text]
168185
| MischlingAnother_relativeMischlingAnother_relativeOneOf5 MischlingAnother_relativeOneOf5
186+
| MischlingAnother_relativeMischlingAnother_relativeAnother_cat MischlingAnother_relativeAnother_cat
169187
deriving (GHC.Show.Show, GHC.Classes.Eq)
170188
instance Data.Aeson.Types.ToJSON.ToJSON MischlingAnother_relativeVariants
171189
where {toJSON (MischlingAnother_relativeCat a) = Data.Aeson.Types.ToJSON.toJSON a;
172190
toJSON (MischlingAnother_relativePetByType a) = Data.Aeson.Types.ToJSON.toJSON a;
173191
toJSON (MischlingAnother_relativeText a) = Data.Aeson.Types.ToJSON.toJSON a;
174192
toJSON (MischlingAnother_relativeListTText a) = Data.Aeson.Types.ToJSON.toJSON a;
175193
toJSON (MischlingAnother_relativeMischlingAnother_relativeOneOf5 a) = Data.Aeson.Types.ToJSON.toJSON a;
194+
toJSON (MischlingAnother_relativeMischlingAnother_relativeAnother_cat a) = Data.Aeson.Types.ToJSON.toJSON a;
176195
toJSON (MischlingAnother_relativeEmptyString) = "";
177196
toJSON (MischlingAnother_relativeTest) = "test"}
178197
instance Data.Aeson.Types.FromJSON.FromJSON MischlingAnother_relativeVariants
179198
where {parseJSON val = if | val GHC.Classes.== "" -> GHC.Base.pure MischlingAnother_relativeEmptyString
180199
| val GHC.Classes.== "test" -> GHC.Base.pure MischlingAnother_relativeTest
181-
| GHC.Base.otherwise -> case (MischlingAnother_relativeCat Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativePetByType Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeText Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeListTText Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeMischlingAnother_relativeOneOf5 Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> Data.Aeson.Types.Internal.Error "No variant matched")))) of
200+
| GHC.Base.otherwise -> case (MischlingAnother_relativeCat Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativePetByType Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeText Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeListTText Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeMischlingAnother_relativeOneOf5 Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> ((MischlingAnother_relativeMischlingAnother_relativeAnother_cat Data.Functor.<$> Data.Aeson.Types.FromJSON.fromJSON val) GHC.Base.<|> Data.Aeson.Types.Internal.Error "No variant matched"))))) of
182201
{Data.Aeson.Types.Internal.Success a -> GHC.Base.pure a;
183202
Data.Aeson.Types.Internal.Error a -> Control.Monad.Fail.fail a}}
184203
-- | Defines the enum schema located at @components.schemas.Mischling.allOf.properties.breed@ in the specification.

testing/golden-output/src/OpenAPI/Types/Mischling.hs-boot

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,11 @@ instance Show MischlingAnother_relativeOneOf5
1111
instance Eq MischlingAnother_relativeOneOf5
1212
instance Data.Aeson.FromJSON MischlingAnother_relativeOneOf5
1313
instance Data.Aeson.ToJSON MischlingAnother_relativeOneOf5
14+
data MischlingAnother_relativeAnother_cat
15+
instance Show MischlingAnother_relativeAnother_cat
16+
instance Eq MischlingAnother_relativeAnother_cat
17+
instance Data.Aeson.FromJSON MischlingAnother_relativeAnother_cat
18+
instance Data.Aeson.ToJSON MischlingAnother_relativeAnother_cat
1419
data MischlingAnother_relativeVariants
1520
instance Show MischlingAnother_relativeVariants
1621
instance Eq MischlingAnother_relativeVariants

0 commit comments

Comments
 (0)