Skip to content

Commit

Permalink
Adds code-gen support for OpenAPI discriminators using Tagged Unions
Browse files Browse the repository at this point in the history
Although the OpenAPI spec allows for more complicated scenarios, I've
restricted this to discriminator unions with an explicit mapping using
only schema types that are not referenced elsewhere. This allows us
to generate nearly the exact same code as we would write by hand for
handling our own tagged union schemas.
  • Loading branch information
qxjit committed Apr 19, 2024
1 parent 9eb6d4b commit 88a4a53
Show file tree
Hide file tree
Showing 19 changed files with 678 additions and 87 deletions.
414 changes: 354 additions & 60 deletions json-fleece-codegen-util/src/Fleece/CodeGenUtil.hs

Large diffs are not rendered by default.

45 changes: 35 additions & 10 deletions json-fleece-codegen-util/src/Fleece/CodeGenUtil/HaskellCode.hs
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ module Fleece.CodeGenUtil.HaskellCode
, newline
, quote
, union
, unionTypeList
, taggedUnion
, intercalate
, lines
, indent
Expand All @@ -57,6 +57,7 @@ module Fleece.CodeGenUtil.HaskellCode
, enum
, sumType
, typeAnnotate
, typeApplication
, stringLiteral
, intLiteral
, caseMatch
Expand Down Expand Up @@ -263,6 +264,10 @@ typeAnnotate :: VarName -> TypeExpression -> HaskellCode
typeAnnotate item annotation =
varNameToCode Nothing item <> " :: " <> toCode annotation

typeApplication :: HaskellCode -> HaskellCode
typeApplication code =
String.fromString "@" <> code

indent :: Int -> HaskellCode -> HaskellCode
indent n code =
fromText (T.replicate n " ") <> code
Expand All @@ -280,8 +285,9 @@ toConstructorName typeName constructorName =
case T.unpack constructorName of
c : _
| Char.isNumber c ->
ConstructorName . fromText $
Manip.toPascal (typeNameText typeName)
ConstructorName
. fromText
$ Manip.toPascal (typeNameText typeName)
<> Manip.toPascal constructorName
_ -> ConstructorName . fromText $ Manip.toPascal constructorName

Expand Down Expand Up @@ -445,18 +451,37 @@ mapOf keyName itemName =
<> itemName
<> ")"

union :: TypeExpression
union =
typeNameToCodeDefaultQualification (shrubberyType "Union")

unionTypeList :: [TypeExpression] -> TypeExpression
unionTypeList members =
union :: [TypeExpression] -> TypeExpression
union members =
fromCode $
lines
( toCode union
( typeNameToCodeDefaultQualification (shrubberyType "Union")
: map (indent 2 . toCode) (delimitLines "'[ " " , " members <> [" ]"])
)

taggedUnion :: [(T.Text, TypeExpression)] -> TypeExpression
taggedUnion members =
let
mkMemberExpression (tag, typeExpr) =
stringLiteral tag
<> " "
<> shrubberyTagAssignment
<> " "
<> toCode typeExpr

memberExpressions =
fmap mkMemberExpression members
in
fromCode $
lines
( typeNameToCodeDefaultQualification (shrubberyType "TaggedUnion")
: map (indent 2 . toCode) (delimitLines "'[ " " , " memberExpressions <> [" ]"])
)

shrubberyTagAssignment :: HaskellCode
shrubberyTagAssignment =
addReferences [VarReference "Shrubbery" Nothing "type (@=)"] "@="

quote :: HaskellCode -> HaskellCode
quote code =
"\"" <> code <> "\""
Expand Down
21 changes: 21 additions & 0 deletions json-fleece-openapi3/examples/test-cases/TestCases/Types/Bar.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Bar
( Bar(..)
, barSchema
) where

import Fleece.Core ((#+), Object)
import qualified Fleece.Core as FC
import Prelude (Eq, Maybe, Show)
import qualified TestCases.Types.Bar.BarName as BarName

data Bar = Bar
{ barName :: Maybe BarName.BarName
}
deriving (Eq, Show)

barSchema :: FC.Fleece schema => Object schema Bar Bar
barSchema =
FC.constructor Bar
#+ FC.optional "barName" barName BarName.barNameSchema
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Bar.BarName
( BarName(..)
, barNameSchema
) where

import qualified Data.Text as T
import qualified Fleece.Core as FC
import Prelude (Eq, Show)

newtype BarName = BarName T.Text
deriving (Show, Eq)

barNameSchema :: FC.Fleece schema => schema BarName
barNameSchema =
FC.coerceSchema FC.text
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Bar.Type
( Type(..)
, typeSchema
) where

import qualified Data.Text as T
import qualified Fleece.Core as FC
import Prelude (Eq, Show)

newtype Type = Type T.Text
deriving (Show, Eq)

typeSchema :: FC.Fleece schema => schema Type
typeSchema =
FC.coerceSchema FC.text
21 changes: 21 additions & 0 deletions json-fleece-openapi3/examples/test-cases/TestCases/Types/Baz.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Baz
( Baz(..)
, bazSchema
) where

import Fleece.Core ((#+), Object)
import qualified Fleece.Core as FC
import Prelude (Eq, Maybe, Show)
import qualified TestCases.Types.Baz.BazName as BazName

data Baz = Baz
{ bazName :: Maybe BazName.BazName
}
deriving (Eq, Show)

bazSchema :: FC.Fleece schema => Object schema Baz Baz
bazSchema =
FC.constructor Baz
#+ FC.optional "bazName" bazName BazName.bazNameSchema
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Baz.BazName
( BazName(..)
, bazNameSchema
) where

import qualified Data.Text as T
import qualified Fleece.Core as FC
import Prelude (Eq, Show)

newtype BazName = BazName T.Text
deriving (Show, Eq)

bazNameSchema :: FC.Fleece schema => schema BazName
bazNameSchema =
FC.coerceSchema FC.text
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Baz.Type
( Type(..)
, typeSchema
) where

import qualified Data.Text as T
import qualified Fleece.Core as FC
import Prelude (Eq, Show)

newtype Type = Type T.Text
deriving (Show, Eq)

typeSchema :: FC.Fleece schema => schema Type
typeSchema =
FC.coerceSchema FC.text
17 changes: 17 additions & 0 deletions json-fleece-openapi3/examples/test-cases/TestCases/Types/Foo.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Foo
( Foo(..)
, fooSchema
) where

import Fleece.Core (Object)
import qualified Fleece.Core as FC
import Prelude (Eq, Show)

data Foo = Foo
deriving (Eq, Show)

fooSchema :: FC.Fleece schema => Object schema Foo Foo
fooSchema =
FC.constructor Foo
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{-# LANGUAGE NoImplicitPrelude #-}

module TestCases.Types.Foo.Type
( Type(..)
, typeSchema
) where

import qualified Data.Text as T
import qualified Fleece.Core as FC
import Prelude (Eq, Show)

newtype Type = Type T.Text
deriving (Show, Eq)

typeSchema :: FC.Fleece schema => schema Type
typeSchema =
FC.coerceSchema FC.text
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE TypeApplications #-}

module TestCases.Types.OneOfWithDiscriminator
( OneOfWithDiscriminator(..)
, oneOfWithDiscriminatorSchema
) where

import Fleece.Core ((#@))
import qualified Fleece.Core as FC
import Prelude (($), Eq, Show)
import Shrubbery (type (@=))
import qualified Shrubbery as Shrubbery
import qualified TestCases.Types.Bar as Bar
import qualified TestCases.Types.Baz as Baz
import qualified TestCases.Types.Foo as Foo

newtype OneOfWithDiscriminator = OneOfWithDiscriminator (Shrubbery.TaggedUnion
'[ "bar" @= Bar.Bar
, "baz" @= Baz.Baz
, "foo" @= Foo.Foo
])
deriving (Show, Eq)

oneOfWithDiscriminatorSchema :: FC.Fleece schema => schema OneOfWithDiscriminator
oneOfWithDiscriminatorSchema =
FC.coerceSchema $
FC.taggedUnionNamed (FC.qualifiedName "TestCases.Types.OneOfWithDiscriminator" "OneOfWithDiscriminator") "type" $
FC.taggedUnionMember @"bar" Bar.barSchema
#@ FC.taggedUnionMember @"baz" Baz.bazSchema
#@ FC.taggedUnionMember @"foo" Foo.fooSchema
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}

module TestCases.Types.OneOfWithNullable
( OneOfWithNullable(..)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}

module TestCases.Types.ReferenceOneOfInsideOneOf
( ReferenceOneOfInsideOneOf(..)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}

module TestCases.Types.TopLevelOneOf
( TopLevelOneOf(..)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NoImplicitPrelude #-}
{-# LANGUAGE DataKinds #-}

module TestCases.Types.TopLevelOneOfOneOption
( TopLevelOneOfOneOption(..)
Expand Down
9 changes: 9 additions & 0 deletions json-fleece-openapi3/examples/test-cases/test-cases.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,12 @@ library
TestCases.Operations.TestCases.QueryParams.StringParam
TestCases.Operations.TestCases.RequestBody
TestCases.Types.AStringType
TestCases.Types.Bar
TestCases.Types.Bar.BarName
TestCases.Types.Bar.Type
TestCases.Types.Baz
TestCases.Types.Baz.BazName
TestCases.Types.Baz.Type
TestCases.Types.CustomDateFormat
TestCases.Types.DateTimeFormats
TestCases.Types.DateTimeFormats.CustomLocalTimeField
Expand All @@ -104,6 +110,8 @@ library
TestCases.Types.FieldTestCases.OptionalNullableField
TestCases.Types.FieldTestCases.RequiredField
TestCases.Types.FieldTestCases.RequiredNullableField
TestCases.Types.Foo
TestCases.Types.Foo.Type
TestCases.Types.JustAdditionalPropertiesSchemaInline
TestCases.Types.JustAdditionalPropertiesSchemaInlineItem
TestCases.Types.JustAdditionalPropertiesSchemaRef
Expand Down Expand Up @@ -151,6 +159,7 @@ library
TestCases.Types.NameConflicts.Type
TestCases.Types.NameConflicts.Where
TestCases.Types.Num2SchemaStartingWithNumber
TestCases.Types.OneOfWithDiscriminator
TestCases.Types.OneOfWithNullable
TestCases.Types.ReferenceOneOf
TestCases.Types.ReferenceOneOfInsideOneOf
Expand Down
35 changes: 35 additions & 0 deletions json-fleece-openapi3/examples/test-cases/test-cases.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -580,6 +580,41 @@ components:
oneOf:
- type: string

OneOfWithDiscriminator:
oneOf:
- $ref: "#/components/schemas/Foo"
- $ref: "#/components/schemas/Bar"
- $ref: "#/components/schemas/Baz"

discriminator:
propertyName: "type"
mapping:
foo: "#/components/schemas/Foo"
bar: "#/components/schemas/Bar"
baz: "#/components/schemas/Baz"

Foo:
type: object
properties:
type:
type: string

Bar:
type: object
properties:
type:
type: string
barName:
type: string

Baz:
type: object
properties:
type:
type: string
bazName:
type: string

TopLevelArray:
type: array
description: A schema that defines a top level array
Expand Down
Loading

0 comments on commit 88a4a53

Please sign in to comment.