Skip to content

Commit bb4463b

Browse files
ChetanBhasinChetan BhasinBijan Chokoufe Nejad
authored
Support Default Values For Enum Types & Bug Fixes (#1)
* Support Default Values For Enum Types & Bug Fixes * Extend tests for defaults * Translated AST Default Ref Codegen Test Co-authored-by: Chetan Bhasin <[email protected]> Co-authored-by: Bijan Chokoufe Nejad <[email protected]>
1 parent ab8dcbb commit bb4463b

19 files changed

+577
-367
lines changed

.github/workflows/deploy.yml

Lines changed: 0 additions & 43 deletions
This file was deleted.

build.sbt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,8 @@ import Dependencies._
22
import ScalaOptions._
33

44
organization in ThisBuild := "com.enfore"
5-
version in ThisBuild := "1.3.2"
6-
//version in ThisBuild := "unstable-SNAPSHOT"
5+
ThisBuild / crossScalaVersions := supportedVersions
6+
version in ThisBuild := "unstable-SNAPSHOT"
77
fork in Test in ThisBuild := true
88

99
lazy val http4s = Seq(http4sCore, http4sDsl, http4sCirce, http4sServer)
@@ -45,7 +45,6 @@ lazy val root = (project in file("."))
4545
lazy val `openapi-scala` = (project in file("openapi-scala"))
4646
.settings(
4747
name := "openapi-scala",
48-
// publishMavenStyle := true,
4948
libraryDependencies ++= Seq(
5049
circeYaml % "test",
5150
scalameta % "test"
@@ -73,7 +72,6 @@ lazy val `sbt-openapi` = (project in file("sbt-openapi"))
7372
.settings(
7473
name := "sbt-openapi",
7574
sbtPlugin := true,
76-
publishMavenStyle := true,
7775
libraryDependencies ++= Seq(
7876
Dependencies.scalafmt,
7977
swaggerCore,
@@ -88,7 +86,7 @@ lazy val publishSettings = Seq(
8886
crossPaths := false,
8987
autoAPIMappings := true,
9088
publishTo := Some(
91-
Opts.resolver.sonatypeStaging
89+
Opts.resolver.sonatypeSnapshots
9290
),
9391
useGpg := false,
9492
usePgpKeyHex("1EAA6358E4812E9E"),

openapi-scala/src/main/scala/com/enfore/apis/ast/ASTTranslationFunctions.scala

Lines changed: 62 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -30,10 +30,9 @@ object ASTTranslationFunctions {
3030
private def extractRefOfMediaTypeObject(schemaObject: MediaTypeObject): Option[Ref] =
3131
schemaObject.schema
3232
.flatMap {
33-
case _: SchemaObject => None
34-
case ReferenceObject(ref) => Some(ref)
33+
case _: SchemaObject => None
34+
case ReferenceObject(ref, _) => Some(Ref(ref, ref.split("/").last, None))
3535
}
36-
.map(x => Ref(x, x.split("/").last))
3736

3837
private def getBodyEncodings(media: RequestBodyObject)(implicit packageName: PackageName): List[TypeRepr] =
3938
media.content.values.toList.map { extractMediaTypeObject }.sequence.toList.flatten
@@ -80,15 +79,15 @@ object ASTTranslationFunctions {
8079
buildPrimitiveFromSchemaObjectType(NonEmptyList.fromList(refinements), a.items)(packageName)(
8180
a.`type`.get
8281
).get
83-
if (!isRequired) {
84-
PrimitiveOption(dataType = value, defaultValue = a.default)
82+
if (!isRequired && a.default.isEmpty) {
83+
PrimitiveOption(dataType = value)
8584
} else {
8685
value
8786
}
8887
case referenceObject: ReferenceObject =>
8988
val value = loadSingleProperty(referenceObject).get
90-
if (!isRequired) {
91-
PrimitiveOption(dataType = value, defaultValue = None)
89+
if (!isRequired && referenceObject.default.isEmpty) {
90+
PrimitiveOption(dataType = value)
9291
} else {
9392
value
9493
}
@@ -216,10 +215,10 @@ object ASTTranslationFunctions {
216215
refinements: Option[NonEmptyList[TypeRepr.RefinedTags]],
217216
items: Option[SchemaOrReferenceObject] = None
218217
)(implicit packageName: PackageName): SchemaObjectType => Option[Primitive] = {
219-
case SchemaObjectType.string => Some(PrimitiveString(refinements))
220-
case SchemaObjectType.boolean => Some(PrimitiveBoolean(refinements))
221-
case SchemaObjectType.number => Some(PrimitiveNumber(refinements))
222-
case SchemaObjectType.`integer` => Some(PrimitiveInt(refinements))
218+
case SchemaObjectType.string => Some(PrimitiveString(refinements, None))
219+
case SchemaObjectType.boolean => Some(PrimitiveBoolean(refinements, None))
220+
case SchemaObjectType.number => Some(PrimitiveNumber(refinements, None))
221+
case SchemaObjectType.`integer` => Some(PrimitiveInt(refinements, None))
223222
case SchemaObjectType.`array` =>
224223
val loadedType = items.flatMap {
225224
case so: SchemaObject =>
@@ -248,10 +247,10 @@ object ASTTranslationFunctions {
248247
so.`type`
249248
.flatMap(buildPrimitiveFromSchemaObjectTypeForComponents(so.items, refinements))
250249
}(loadSingleProperty(_).map(PrimitiveDict(_, None)))
251-
case ReferenceObject(ref) =>
250+
case ReferenceObject(ref, _) =>
252251
val name: String = ref.split("/").last
253252
val path: String = ref.split("/").dropRight(1).mkString(".")
254-
(Ref(path.replace("#.components.schemas", packageName.name), name): TypeRepr).some
253+
(Ref(path.replace("#.components.schemas", packageName.name), name, None): TypeRepr).some
255254
}
256255

257256
private def makeSymbolFromTypeRepr(name: String, repr: TypeRepr): Symbol = repr match {
@@ -265,61 +264,67 @@ object ASTTranslationFunctions {
265264
properties: Map[String, SchemaOrReferenceObject],
266265
required: List[String],
267266
summary: Option[String],
268-
description: Option[String]
267+
description: Option[String],
268+
allSchemas: Map[String, SchemaObject]
269269
)(
270270
implicit packageName: PackageName
271271
): Option[NewType] = {
272272
val mapped: immutable.Iterable[Option[Symbol]] = properties map {
273273
case (name: String, repr: SchemaOrReferenceObject) =>
274-
val loaded = getTypeRepr(required, name, repr)
274+
val loaded: Option[TypeRepr] = getTypeRepr(required, name, repr, allSchemas)
275275
assert(loaded.isDefined, s"$name in $typeName could not be parsed.")
276276
loaded map (makeSymbolFromTypeRepr(name, _))
277277
}
278278
mapped.toList.sequence.map(PrimitiveProduct(packageName.name, typeName, _, summary, description))
279279
}
280280

281-
private def typedDefaultMapping(value: TypeRepr, default: Option[PrimitiveValue]): PrimitiveOption = {
282-
val mismatchErr = s"${value.typeName} has wrong default value type"
283-
val mappedDefault = default map {
284-
case PrimitiveNumberValue(n) =>
285-
value match {
286-
case _: PrimitiveNumber => PrimitiveNumberValue(n)
287-
case _: PrimitiveInt =>
288-
assert(n.isValidInt, "Implicit double to integer type coercion")
289-
PrimitiveIntValue(n.toInt)
290-
case _ => throw new AssertionError(mismatchErr)
291-
}
292-
case i: PrimitiveIntValue =>
293-
assert(value.isInstanceOf[PrimitiveInt], mismatchErr)
294-
i
295-
case s: PrimitiveStringValue =>
296-
assert(value.isInstanceOf[PrimitiveString], mismatchErr)
297-
s
298-
case self => self
299-
}
300-
PrimitiveOption(value, mappedDefault)
301-
}
302-
303-
private def getTypeRepr(required: List[String], name: String, repr: SchemaOrReferenceObject)(
281+
private def getTypeRepr(
282+
required: List[String],
283+
name: String,
284+
repr: SchemaOrReferenceObject,
285+
allProps: Map[String, SchemaObject]
286+
)(
304287
implicit packageName: PackageName
305288
): Option[TypeRepr] =
306289
repr match {
290+
case o: SchemaObject if o.oneOf.nonEmpty =>
291+
throw new AssertionError("Discriminated Unions (OpenAPI: oneOf) are only supported as top-level types for now.")
307292
case r: ReferenceObject =>
308-
val loadedTypeRepr = loadSingleProperty(r)
309-
if (required.contains(name)) loadedTypeRepr else loadedTypeRepr.map(PrimitiveOption(_, None))
310-
case o: SchemaObject if o.oneOf.isEmpty =>
311-
val loadedTypeRepr: Option[TypeRepr] = loadSingleProperty(o)
312-
if (required.contains(name)) loadedTypeRepr else loadedTypeRepr.map(typedDefaultMapping(_, o.default))
293+
val typeRepr = loadSingleProperty(repr)
294+
val referenceTo = r.$ref.split("/").last.stripSuffix("Request")
295+
allProps
296+
.getOrElse(
297+
referenceTo,
298+
throw new Exception(
299+
s"You're referencing ${r.$ref} which does not exist. Available components are ${allProps.keys.mkString(", ")}"
300+
)
301+
)
302+
.default
303+
.fold(if (required contains name) typeRepr else typeRepr.map(PrimitiveOption))(
304+
dv => typeRepr.map(_.packDefault(dv))
305+
)
313306
case _ =>
314-
throw new AssertionError("Discriminated Unions (OpenAPI: oneOf) are only supported as top-level types for now.")
307+
val typeRepr: Option[TypeRepr] = loadSingleProperty(repr)
308+
repr.default.fold(if (required contains name) typeRepr else typeRepr.map(PrimitiveOption))(
309+
dv => typeRepr.map(_.packDefault(dv))
310+
)
315311
}
316312

317-
private def loadEnum(typeName: String, values: List[String], summary: Option[String], description: Option[String])(
313+
private def loadEnum(
314+
typeName: String,
315+
values: List[String],
316+
summary: Option[String],
317+
description: Option[String]
318+
)(
318319
implicit packageName: PackageName
319320
): NewType =
320321
PrimitiveEnum(packageName.name, typeName, values.toSet, summary, description)
321322

322-
private def handleSchemaObjectProductType(name: String, schemaObject: SchemaObject)(
323+
private def handleSchemaObjectProductType(
324+
name: String,
325+
schemaObject: SchemaObject,
326+
allSchemas: Map[String, SchemaObject]
327+
)(
323328
implicit packageName: PackageName
324329
): Option[Symbol] = {
325330
val required = schemaObject.required.getOrElse(List.empty[String])
@@ -330,7 +335,8 @@ object ASTTranslationFunctions {
330335
schemaObject.properties.getOrElse(Map.empty),
331336
required,
332337
schemaObject.summary,
333-
schemaObject.description
338+
schemaObject.description,
339+
allSchemas
334340
)
335341
case SchemaObjectType.`string` =>
336342
schemaObject.enum.map(loadEnum(name, _, schemaObject.summary, schemaObject.description))
@@ -362,10 +368,10 @@ object ASTTranslationFunctions {
362368
implicit packageName: PackageName
363369
): Option[Symbol] = {
364370
val references: Set[Ref] = unionMembers.map {
365-
case ReferenceObject(ref) =>
371+
case ReferenceObject(ref, _) =>
366372
val name: String = ref.split("/").last
367373
val path: String = ref.split("/").dropRight(1).mkString(".")
368-
Ref(path, name)
374+
Ref(path, name, None)
369375
}.toSet
370376
val newType: NewType =
371377
PrimitiveUnion(
@@ -385,10 +391,10 @@ object ASTTranslationFunctions {
385391
NewTypeSymbol(name, newType).some
386392
}
387393

388-
private def evalSchema(name: String, schemaObject: SchemaObject)(
394+
private def evalSchema(name: String, schemaObject: SchemaObject, allSchemas: Map[String, SchemaObject])(
389395
implicit packageName: PackageName
390396
): Option[Symbol] =
391-
schemaObject.oneOf.fold(handleSchemaObjectProductType(name, schemaObject))(
397+
schemaObject.oneOf.fold(handleSchemaObjectProductType(name, schemaObject, allSchemas))(
392398
handleSchemaObjectUnionType(name, schemaObject.discriminator, _, schemaObject.summary, schemaObject.description)
393399
)
394400

@@ -399,13 +405,13 @@ object ASTTranslationFunctions {
399405
so.readOnly.getOrElse(
400406
so.properties.exists { _.values.exists { schemaObjectHasReadOnlyComponent(components) } }
401407
) || so.oneOf.fold(false)(_.map(schemaObjectHasReadOnlyComponent(components)(_)).reduce(_ || _))
402-
case ReferenceObject(ref) =>
408+
case ReferenceObject(ref, _) =>
403409
schemaObjectHasReadOnlyComponent(components)(findRefInComponents(components, ref.split("/").last))
404410
}
405411

406412
private def hasReadOnlyBase: SchemaOrReferenceObject => Boolean = {
407-
case so: SchemaObject => so.readOnly.getOrElse(false)
408-
case ReferenceObject(_) => false
413+
case so: SchemaObject => so.readOnly.getOrElse(false)
414+
case ReferenceObject(_, _) => false
409415
}
410416

411417
private def enrichSublevelPropsWithRequest(
@@ -415,7 +421,7 @@ object ASTTranslationFunctions {
415421
.map {
416422
case (k, v: ReferenceObject) =>
417423
if (schemaObjectHasReadOnlyComponent(components)(v)) {
418-
(k, ReferenceObject(v.$ref + "Request"))
424+
(k, ReferenceObject(v.$ref + "Request", v.default))
419425
} else {
420426
(k, v)
421427
}
@@ -451,7 +457,7 @@ object ASTTranslationFunctions {
451457
splitReadOnlyComponents(ast.components.schemas)
452458
.flatMap {
453459
case (name: String, schemaObject: SchemaObject) =>
454-
evalSchema(name, schemaObject)
460+
evalSchema(name, schemaObject, ast.components.schemas)
455461
.map { cleanFilename(name) -> _ }
456462
}
457463
.toList

openapi-scala/src/main/scala/com/enfore/apis/ast/SwaggerAST.scala

Lines changed: 15 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ object SwaggerAST {
3939
* definition relies on optional value to represent the nature of type (i.e., sum, product, primitive, etc.),
4040
* we clean them up at load time.
4141
*/
42-
sealed trait SchemaOrReferenceObject
42+
sealed trait SchemaOrReferenceObject {
43+
val default: Option[PrimitiveValue]
44+
}
4345

4446
/*
4547
* Marker trait for pointing to a type definition. This is to identify between different types of type definitions
@@ -50,27 +52,27 @@ object SwaggerAST {
5052
final case class Discriminator(propertyName: String)
5153

5254
final case class SchemaObject(
53-
summary: Option[String] = None, // Optional field (short docstring)
55+
summary: Option[String] = None, // Optional field (short docstring)
5456
description: Option[String] = None, // Optional field (docstring)
5557
`type`: Option[SchemaObjectType] = None, // Only present when property is not a reference
5658
// CAVEAT: We allow a reference here, which according to the spec is not allowed but the way all OpenAPI tools work
5759
properties: Option[Map[String, SchemaOrReferenceObject]] = None, // Only present when schema object is an object
5860
additionalProperties: Option[SchemaOrReferenceObject] = None,
5961
// CAVEAT: We allow a reference here, which according to the spec is not allowed but the way all OpenAPI tools work
6062
items: Option[SchemaOrReferenceObject] = None, // Only present when schema object is an array
61-
oneOf: Option[List[ReferenceObject]] = None, // Used for sum types and nothing else
62-
discriminator: Option[Discriminator] = None, // Used for sum types and nothing else
63+
oneOf: Option[List[ReferenceObject]] = None, // Used for sum types and nothing else
64+
discriminator: Option[Discriminator] = None, // Used for sum types and nothing else
6365
enum: Option[List[String]] = None,
6466
required: Option[List[String]] = None,
65-
readOnly: Option[Boolean] = None, // Points out whether a property is readOnly (defaults to false)
66-
minLength: Option[Int] = None, // Optional refinement
67-
maxLength: Option[Int] = None, // Optional refinement
68-
maxItems: Option[Int] = None, // Optional refinement
69-
minItems: Option[Int] = None, // Optional refinement
67+
readOnly: Option[Boolean] = None, // Points out whether a property is readOnly (defaults to false)
68+
minLength: Option[Int] = None, // Optional refinement
69+
maxLength: Option[Int] = None, // Optional refinement
70+
maxItems: Option[Int] = None, // Optional refinement
71+
minItems: Option[Int] = None, // Optional refinement
7072
maxProperties: Option[Int] = None, // Optional refinement
7173
minProperties: Option[Int] = None, // Optional refinement
72-
maximum: Option[Int] = None, // Optional refinement
73-
minimum: Option[Int] = None, // Optional refinement
74+
maximum: Option[Int] = None, // Optional refinement
75+
minimum: Option[Int] = None, // Optional refinement
7476
default: Option[PrimitiveValue] = None
7577
) extends SchemaOrReferenceObject
7678
with TypeDef
@@ -79,9 +81,10 @@ object SwaggerAST {
7981
* Example : {{{
8082
* schema:
8183
* $ref: '...'
84+
* default: NONE // An optional field (used only for Enum references)
8285
* }}}
8386
*/
84-
final case class ReferenceObject($ref: String) extends SchemaOrReferenceObject
87+
final case class ReferenceObject($ref: String, default: Option[PrimitiveValue]) extends SchemaOrReferenceObject
8588

8689
// --- Types for Routes ---
8790

openapi-scala/src/main/scala/com/enfore/apis/generator/PathInterfaceGenerator.scala

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@ object PathInterfaceGenerator {
3939
queries.map {
4040
case (name: String, primType: TypeRepr) =>
4141
primType match {
42-
case PrimitiveInt(_) => s""""$name" -> "int""""
43-
case PrimitiveNumber(_) => s""""$name" -> "double""""
44-
case PrimitiveString(_) => s""""$name" -> "string""""
45-
case _ => s""""$name" -> "string""""
42+
case PrimitiveInt(_, _) => s""""$name" -> "int""""
43+
case PrimitiveNumber(_, _) => s""""$name" -> "double""""
44+
case PrimitiveString(_, _) => s""""$name" -> "string""""
45+
case _ => s""""$name" -> "string""""
4646
}
4747
}.toList
4848

@@ -57,8 +57,8 @@ object PathInterfaceGenerator {
5757
val pathParamsSyntax: Option[String] =
5858
NonEmptyList.fromList(pathParams.map(param => s"$param: String")).map(_.toList.mkString(", "))
5959
val reqSyntax: Option[String] = reqType.map {
60-
case request @ Ref(_, _) => s"request: ${resolveRef(request)(p)}"
61-
case x => s"request: ${x.typeName}"
60+
case request @ Ref(_, _, _) => s"request: ${resolveRef(request)(p)}"
61+
case x => s"request: ${x.typeName}"
6262
}
6363
List(querySyntax, pathParamsSyntax, reqSyntax).flatten
6464
.mkString(", ")

0 commit comments

Comments
 (0)