diff --git a/.devops/config b/.devops/config
new file mode 100644
index 000000000..6f8bfa6af
--- /dev/null
+++ b/.devops/config
@@ -0,0 +1 @@
+apibuilder_organization=apicollective
\ No newline at end of file
diff --git a/app/app/views/doc/apiJson.scala.html b/app/app/views/doc/apiJson.scala.html
index c133bc67c..3678aed72 100644
--- a/app/app/views/doc/apiJson.scala.html
+++ b/app/app/views/doc/apiJson.scala.html
@@ -413,6 +413,7 @@
Union declaration
{
"name": {
"plural": string (optional),
+ "fields": JSON Array of Field (optional),
"discriminator": string (optional),
"description": string (optional),
"interfaces": JSON Array of type string where each value indicates the name of a declared interface (optional),
@@ -426,6 +427,8 @@ Union declaration
where:
- name specifies the name of the interface. Names must be alphanumeric and start with a letter. Valid characters are a-z, A-Z, 0-9 and _ characters. The name must be unique in the set of names assigned to enums, unions, or models. Note you may define an interface and a union of the same name, but in this case it is required to list that interface in the interfaces field.
+ - fields
+ - fields Optional JSON Array of 0 or more Fields. If specified, API Builder will create a model named after the type with these specified fields. This is syntactic sugar for creating the model yourself and then referencing here as the type.
- plural specifies the optional, plural form of the name. By default, we will pluralize the name using a basic set of english heuristics. The plural is used as a default in cases where it is more natural to specify web services. For example, the default path for a resource will be the plural.
- discriminator specifies an optional, but recommended, name for a type discriminator field which can then be used in serialization / deserialization to identify the type of object. For example, if not specified, a code generator may serialize the union type into a JSON structure of { "type" => object }. If a discriminator is provided, the same code generator can flatten the JSON representation to, for example: { "discriminator" => "xxx", "field1" => "yyy" }. If provided, the name of the discriminator field must be unique across all of the fields across all of the types of this union. See Play Union Types for more information and examples.
@description("union")
diff --git a/core/app/builder/api_json/ApiJsonServiceValidator.scala b/core/app/builder/api_json/ApiJsonServiceValidator.scala
index ca84cdd72..9791cac5e 100644
--- a/core/app/builder/api_json/ApiJsonServiceValidator.scala
+++ b/core/app/builder/api_json/ApiJsonServiceValidator.scala
@@ -139,12 +139,31 @@ case class ApiJsonServiceValidator(
union.types.map(_.warnings) ++ union.types.map { typ =>
typ.datatype match {
case Invalid(errors) => (s"Union[${union.name}] type[] " + errors.toNonEmptyList.toList.mkString(", ")).invalidNec
- case Valid(dt) => validateAttributes(s"Union[${union.name}] type[${dt.name}]", typ.attributes)
+ case Valid(dt) => sequenceUnique(Seq(
+ validateAttributes(s"Union[${union.name}] type[${dt.name}]", typ.attributes),
+ validateUnionTypeFields(union.name, dt, typ.fields),
+ ))
}
}
)
}
+ private def validateUnionTypeFields(unionName: String, dt: InternalDatatype, fields: Option[Seq[InternalFieldForm]]): ValidatedNec[String, Unit] = {
+ fields match {
+ case None => ().validNec
+ case Some(_) =>
+ dt match {
+ case _: InternalDatatype.List =>
+ s"Union[$unionName] type[${dt.name}] fields cannot be specified for list types".invalidNec
+ case _: InternalDatatype.Map =>
+ s"Union[$unionName] type[${dt.name}] fields cannot be specified for map types".invalidNec
+ case _: InternalDatatype.Singleton if lib.Primitives(dt.name).isDefined =>
+ s"Union[$unionName] type[${dt.name}] fields cannot be specified for primitive types".invalidNec
+ case _ => ().validNec
+ }
+ }
+ }
+
private def validateAnnotations(annotations: Seq[InternalAnnotationForm]): ValidatedNec[String, Unit] = {
sequenceUnique(
annotations.map(_.warnings) ++ annotations.filter(_.name.isEmpty).map(_ =>
diff --git a/core/app/builder/api_json/AutoCreateUnionTypeModels.scala b/core/app/builder/api_json/AutoCreateUnionTypeModels.scala
new file mode 100644
index 000000000..2f499fd99
--- /dev/null
+++ b/core/app/builder/api_json/AutoCreateUnionTypeModels.scala
@@ -0,0 +1,35 @@
+package builder.api_json
+
+import cats.implicits.*
+import lib.{DatatypeResolver, Text}
+
+/**
+ * Create default models for types in a union type if the user has not already created that
+ * model. Works as syntactic sugar for defining a model as a type in a union.
+ */
+object AutoCreateUnionTypeModels {
+
+ def createModelsForUnionTypes(resolver: DatatypeResolver, unions: Seq[InternalUnionForm]): Seq[InternalModelForm] = {
+ unions.flatMap { u =>
+ u.types.filter(_.fields.nonEmpty).flatMap { t =>
+ t.datatype.toOption.map(_.name).map { typ =>
+ createModelForUnionType(typ, t.fields.getOrElse(Nil))
+ }
+ }
+ }.distinctBy(_.name)
+ }
+
+ private def createModelForUnionType(typ: String, fields: Seq[InternalFieldForm]): InternalModelForm = {
+ InternalModelForm(
+ name = typ,
+ plural = Text.pluralize(typ),
+ description = None,
+ deprecation = None,
+ fields = fields,
+ attributes = Nil,
+ interfaces = Nil,
+ templates = Nil,
+ warnings = ().validNec
+ )
+ }
+}
diff --git a/core/app/builder/api_json/InternalApiJsonForm.scala b/core/app/builder/api_json/InternalApiJsonForm.scala
index 8cf9ca4f6..a0cda9a33 100644
--- a/core/app/builder/api_json/InternalApiJsonForm.scala
+++ b/core/app/builder/api_json/InternalApiJsonForm.scala
@@ -1,13 +1,13 @@
package builder.api_json
-import cats.implicits._
+import cats.implicits.*
import cats.data.ValidatedNec
import builder.JsonUtil
import builder.api_json.upgrades.AllUpgrades
import cats.data.Validated.{Invalid, Valid}
import core.ServiceFetcher
-import lib.{Text, ValidatedHelpers}
-import play.api.libs.json._
+import lib.{DatatypeResolver, Text, ValidatedHelpers}
+import play.api.libs.json.*
/**
* Just parses json with minimal validation - build to provide a way to
@@ -42,10 +42,8 @@ private[api_json] case class InternalApiJsonForm(
private lazy val declaredUnions: Seq[InternalUnionForm] = {
(json \ "unions").asOpt[JsValue] match {
case Some(unions: JsObject) => {
- unions.fields.flatMap { v =>
- v match {
- case(k, value) => value.asOpt[JsObject].map(InternalUnionForm(internalDatatypeBuilder, k, _))
- }
+ unions.fields.flatMap {
+ case (k, value) => value.asOpt[JsObject].map(InternalUnionForm(internalDatatypeBuilder, k, _))
}
}.toSeq
case _ => Seq.empty
@@ -57,10 +55,8 @@ private[api_json] case class InternalApiJsonForm(
private def parseModels(js: JsValue, prefix: Option[String]): Seq[InternalModelForm] = {
(js \ "models").asOpt[JsObject] match {
case Some(models) => {
- models.fields.flatMap { v =>
- v match {
- case (k, value) => value.asOpt[JsObject].map(InternalModelForm(internalDatatypeBuilder, k, _, prefix = prefix))
- }
+ models.fields.flatMap {
+ case (k, value) => value.asOpt[JsObject].map(InternalModelForm(internalDatatypeBuilder, k, _, prefix = prefix))
}
}.toSeq
case _ => Seq.empty
@@ -86,25 +82,33 @@ private[api_json] case class InternalApiJsonForm(
def interfaces: Seq[InternalInterfaceForm] = {
(json \ "interfaces").asOpt[JsValue] match {
case Some(interfaces: JsObject) => {
- interfaces.fields.flatMap { v =>
- v match {
- case(k, value) => value.asOpt[JsObject].map(InternalInterfaceForm(internalDatatypeBuilder, k, _))
- }
+ interfaces.fields.flatMap {
+ case (k, value) => value.asOpt[JsObject].map(InternalInterfaceForm(internalDatatypeBuilder, k, _))
}
}.toSeq
case _ => Seq.empty
}
} ++ internalDatatypeBuilder.interfaceForms
- def models: Seq[InternalModelForm] = declaredModels ++ internalDatatypeBuilder.modelForms
+ lazy val models: Seq[InternalModelForm] = {
+ val knownModels = declaredModels ++ internalDatatypeBuilder.modelForms
+
+ val datatypeResolver: DatatypeResolver = DatatypeResolver(
+ enumNames = enums.map(_.name),
+ interfaceNames = interfaces.map(_.name),
+ unionNames = unions.map(_.name),
+ modelNames = knownModels.map(_.name)
+ )
+
+ knownModels ++ AutoCreateUnionTypeModels.createModelsForUnionTypes(datatypeResolver, unions)
+ }
+
private lazy val declaredEnums: Seq[InternalEnumForm] = {
(json \ "enums").asOpt[JsValue] match {
case Some(enums: JsObject) => {
- enums.fields.flatMap { v =>
- v match {
- case(k, value) => value.asOpt[JsObject].map(InternalEnumForm(k, _))
- }
+ enums.fields.flatMap {
+ case (k, value) => value.asOpt[JsObject].map(InternalEnumForm(k, _))
}
}.toSeq
case _ => Seq.empty
@@ -131,11 +135,9 @@ private[api_json] case class InternalApiJsonForm(
case None => Seq.empty
case Some(resources: JsObject) => {
- resources.fields.flatMap { v =>
- v match {
- case (typeName, value) => {
- value.asOpt[JsObject].map(InternalResourceForm(internalDatatypeBuilder, typeName, declaredModels, declaredEnums, unions, _))
- }
+ resources.fields.flatMap {
+ case (typeName, value) => {
+ value.asOpt[JsObject].map(InternalResourceForm(internalDatatypeBuilder, typeName, declaredModels, declaredEnums, unions, _))
}
}
}.toSeq
@@ -148,10 +150,6 @@ private[api_json] case class InternalApiJsonForm(
lazy val attributes: Seq[InternalAttributeForm] = InternalAttributeForm.fromJson((json \ "attributes").asOpt[JsArray])
- lazy val typeResolver: TypeResolver = TypeResolver(
- defaultNamespace = namespace,
- RecursiveTypesProvider(this)
- )
}
case class InternalImportForm(
@@ -203,7 +201,7 @@ case class InternalTemplateDeclarationForm(
warnings: ValidatedNec[String, Unit]
)
-object InternalTemplateDeclarationForm {
+private object InternalTemplateDeclarationForm {
def apply(json: JsObject): InternalTemplateDeclarationForm = {
InternalTemplateDeclarationForm(
name = JsonUtil.asOptString(json \ "name"),
@@ -269,6 +267,7 @@ case class InternalUnionForm(
case class InternalUnionTypeForm(
datatype: ValidatedNec[String, InternalDatatype],
+ fields: Option[Seq[InternalFieldForm]],
description: Option[String],
deprecation: Option[InternalDeprecationForm],
attributes: Seq[InternalAttributeForm],
@@ -450,18 +449,19 @@ object InternalUnionForm {
InternalUnionTypeForm(
datatype = internalDatatype,
+ fields = (json \ "fields").asOpt[JsArray].map(_ => InternalFieldForm.parse(internalDatatypeBuilder, json)),
description = JsonUtil.asOptString(json \ "description"),
deprecation = InternalDeprecationForm.fromJsValue(json),
default = JsonUtil.asOptBoolean(json \ "default"),
discriminatorValue = JsonUtil.asOptString(json \ "discriminator_value"),
- attributes = InternalAttributeForm.fromJson((value \ "attributes").asOpt[JsArray]),
+ attributes = InternalAttributeForm.fromJson((json \ "attributes").asOpt[JsArray]),
warnings = JsonUtil.validate(
json,
anys = Seq("type"),
optionalStrings = Seq("description", "discriminator_value"),
optionalBooleans = Seq("default"),
optionalObjects = Seq("deprecation"),
- optionalArraysOfObjects = Seq("attributes"),
+ optionalArraysOfObjects = Seq("fields", "attributes"),
prefix = Some(s"Union[$name] type[${datatypeName.getOrElse("")}]")
)
)
@@ -614,36 +614,34 @@ object InternalEnumForm {
object InternalHeaderForm {
def apply(internalDatatypeBuilder: InternalDatatypeBuilder, json: JsValue): Seq[InternalHeaderForm] = {
- (json \ "headers").asOpt[JsArray].map(_.value).getOrElse(Seq.empty).flatMap { el =>
- el match {
- case o: JsObject => {
- val datatype = internalDatatypeBuilder.parseTypeFromObject(o)
-
- val headerName = JsonUtil.asOptString(o \ "name")
- Some(
- InternalHeaderForm(
- name = headerName,
- datatype = datatype,
- required = InternalDatatype.isRequired(datatype),
- description = JsonUtil.asOptString(o \ "description"),
- deprecation = InternalDeprecationForm.fromJsValue(o),
- default = JsonUtil.asOptString(o \ "default"),
- attributes = InternalAttributeForm.fromJson((o \ "attributes").asOpt[JsArray]),
- warnings = JsonUtil.validate(
- o,
- strings = Seq("name"),
- anys = Seq("type"),
- optionalBooleans = Seq("required"),
- optionalObjects = Seq("deprecation"),
- optionalStrings = Seq("default", "description"),
- optionalArraysOfObjects = Seq("attributes"),
- prefix = Some(s"Header[${headerName.getOrElse("")}]".trim)
- )
+ (json \ "headers").asOpt[JsArray].map(_.value).getOrElse(Seq.empty).flatMap {
+ case o: JsObject => {
+ val datatype = internalDatatypeBuilder.parseTypeFromObject(o)
+
+ val headerName = JsonUtil.asOptString(o \ "name")
+ Some(
+ InternalHeaderForm(
+ name = headerName,
+ datatype = datatype,
+ required = InternalDatatype.isRequired(datatype),
+ description = JsonUtil.asOptString(o \ "description"),
+ deprecation = InternalDeprecationForm.fromJsValue(o),
+ default = JsonUtil.asOptString(o \ "default"),
+ attributes = InternalAttributeForm.fromJson((o \ "attributes").asOpt[JsArray]),
+ warnings = JsonUtil.validate(
+ o,
+ strings = Seq("name"),
+ anys = Seq("type"),
+ optionalBooleans = Seq("required"),
+ optionalObjects = Seq("deprecation"),
+ optionalStrings = Seq("default", "description"),
+ optionalArraysOfObjects = Seq("attributes"),
+ prefix = Some(s"Header[${headerName.getOrElse("")}]".trim)
)
)
- }
- case _ => None
+ )
}
+ case _ => None
}
}.toSeq
}
diff --git a/core/app/builder/api_json/ServiceBuilder.scala b/core/app/builder/api_json/ServiceBuilder.scala
index 5177e0adc..df33e8939 100644
--- a/core/app/builder/api_json/ServiceBuilder.scala
+++ b/core/app/builder/api_json/ServiceBuilder.scala
@@ -374,6 +374,7 @@ case class ServiceBuilder(
`type` = typ.label,
description = it.description,
deprecation = it.deprecation.map(DeprecationBuilder(_)),
+ fields = it.fields.map(_.map(FieldBuilder(_))),
default = it.default,
discriminatorValue = Some(
it.discriminatorValue.getOrElse(typ.name)
diff --git a/core/test/core/UnionTypeFieldsSpec.scala b/core/test/core/UnionTypeFieldsSpec.scala
new file mode 100644
index 000000000..8752badef
--- /dev/null
+++ b/core/test/core/UnionTypeFieldsSpec.scala
@@ -0,0 +1,292 @@
+package core
+
+import helpers.ApiJsonHelpers
+import org.scalatest.funspec.AnyFunSpec
+import org.scalatest.matchers.should.Matchers
+
+class UnionTypeFieldsSpec extends AnyFunSpec with Matchers with ApiJsonHelpers {
+
+ it("creates model from union type with fields") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "unions": {
+ "user": {
+ "discriminator": "discriminator",
+ "types": [
+ {
+ "type": "guest",
+ "fields": [
+ { "name": "id", "type": "uuid" },
+ { "name": "nickname", "type": "string" }
+ ]
+ },
+ {
+ "type": "registered",
+ "fields": [
+ { "name": "id", "type": "uuid" },
+ { "name": "email", "type": "string" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ val service = setupValidApiJson(json)
+ val guestModel = service.models.find(_.name == "guest").getOrElse {
+ sys.error("No guest model created")
+ }
+ guestModel.fields.map(_.name) should equal(Seq("id", "nickname"))
+
+ val registeredModel = service.models.find(_.name == "registered").getOrElse {
+ sys.error("No registered model created")
+ }
+ registeredModel.fields.map(_.name) should equal(Seq("id", "email"))
+
+ service.unions.head.types.map(_.`type`) should equal(Seq("guest", "registered"))
+ }
+
+ it("creates empty model when fields is empty array") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "unions": {
+ "event": {
+ "discriminator": "discriminator",
+ "types": [
+ {
+ "type": "started",
+ "fields": []
+ },
+ {
+ "type": "completed",
+ "fields": [
+ { "name": "result", "type": "string" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ val service = setupValidApiJson(json)
+ val startedModel = service.models.find(_.name == "started").getOrElse {
+ sys.error("No started model created")
+ }
+ startedModel.fields should be(empty)
+
+ val completedModel = service.models.find(_.name == "completed").getOrElse {
+ sys.error("No completed model created")
+ }
+ completedModel.fields.map(_.name) should equal(Seq("result"))
+ }
+
+ it("rejects unknown type when fields is absent") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "unions": {
+ "user": {
+ "types": [
+ { "type": "unknown_type" }
+ ]
+ }
+ }
+ }
+ """
+
+ TestHelper.expectSingleError(json) should be(
+ "Union[user] type[unknown_type] not found"
+ )
+ }
+
+ it("rejects fields on primitive types") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "unions": {
+ "content": {
+ "types": [
+ {
+ "type": "string",
+ "fields": [
+ { "name": "value", "type": "string" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ TestHelper.expectSingleError(json) should be(
+ "Union[content] type[string] fields cannot be specified for primitive types"
+ )
+ }
+
+ it("rejects fields on list types") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "models": {
+ "item": {
+ "fields": [
+ { "name": "id", "type": "uuid" }
+ ]
+ }
+ },
+
+ "unions": {
+ "content": {
+ "types": [
+ {
+ "type": "[item]",
+ "fields": [
+ { "name": "value", "type": "string" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ TestHelper.expectSingleError(json) should be(
+ "Union[content] type[item] fields cannot be specified for list types"
+ )
+ }
+
+ it("rejects fields on map types") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "models": {
+ "item": {
+ "fields": [
+ { "name": "id", "type": "uuid" }
+ ]
+ }
+ },
+
+ "unions": {
+ "content": {
+ "types": [
+ {
+ "type": "map[item]",
+ "fields": [
+ { "name": "value", "type": "string" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ TestHelper.expectSingleError(json) should be(
+ "Union[content] type[item] fields cannot be specified for map types"
+ )
+ }
+
+ it("allows fields alongside existing model") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "models": {
+ "registered": {
+ "fields": [
+ { "name": "id", "type": "uuid" }
+ ]
+ }
+ },
+
+ "unions": {
+ "user": {
+ "discriminator": "discriminator",
+ "types": [
+ { "type": "registered" },
+ {
+ "type": "guest",
+ "fields": [
+ { "name": "id", "type": "uuid" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ val service = setupValidApiJson(json)
+ service.models.map(_.name).sorted should equal(Seq("guest", "registered"))
+ service.unions.head.types.map(_.`type`) should equal(Seq("registered", "guest"))
+ }
+
+ it("passes fields through to service spec union type") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "unions": {
+ "user": {
+ "discriminator": "discriminator",
+ "types": [
+ {
+ "type": "guest",
+ "fields": [
+ { "name": "id", "type": "uuid" }
+ ]
+ }
+ ]
+ }
+ }
+ }
+ """
+
+ val service = setupValidApiJson(json)
+ val unionType = service.unions.head.types.head
+ unionType.`type` should equal("guest")
+ val fields = unionType.fields.getOrElse(sys.error("Expected fields"))
+ fields.map(_.name) should equal(Seq("id"))
+ fields.head.`type` should equal("uuid")
+ }
+
+ it("union type fields is None when not specified") {
+ val json = """
+ {
+ "name": "Union Type Fields Test",
+
+ "models": {
+ "guest": {
+ "fields": [
+ { "name": "id", "type": "uuid" }
+ ]
+ }
+ },
+
+ "unions": {
+ "user": {
+ "discriminator": "discriminator",
+ "types": [
+ { "type": "guest" }
+ ]
+ }
+ }
+ }
+ """
+
+ val service = setupValidApiJson(json)
+ service.unions.head.types.head.fields should be(None)
+ }
+
+}
diff --git a/generated/app/ApicollectiveApibuilderSpecV0Client.scala b/generated/app/ApicollectiveApibuilderSpecV0Client.scala
index 0b7997e39..80d853787 100644
--- a/generated/app/ApicollectiveApibuilderSpecV0Client.scala
+++ b/generated/app/ApicollectiveApibuilderSpecV0Client.scala
@@ -1,6 +1,6 @@
/**
* Generated by API Builder - https://www.apibuilder.io
- * Service version: 0.16.53
+ * Service version: 0.16.74
* User agent: apibuilder app.apibuilder.io/apicollective/apibuilder-spec/latest/play_2_9_scala_3_client
*/
package io.apibuilder.spec.v0.models {
@@ -320,6 +320,7 @@ package io.apibuilder.spec.v0.models {
`type`: String,
description: _root_.scala.Option[String] = None,
deprecation: _root_.scala.Option[io.apibuilder.spec.v0.models.Deprecation] = None,
+ fields: _root_.scala.Option[Seq[io.apibuilder.spec.v0.models.Field]] = None,
attributes: Seq[io.apibuilder.spec.v0.models.Attribute] = Nil,
default: _root_.scala.Option[Boolean] = None,
discriminatorValue: _root_.scala.Option[String] = None
@@ -1351,10 +1352,11 @@ package io.apibuilder.spec.v0.models {
`type` <- (__ \ "type").read[String]
description <- (__ \ "description").readNullable[String]
deprecation <- (__ \ "deprecation").readNullable[io.apibuilder.spec.v0.models.Deprecation]
+ fields <- (__ \ "fields").readNullable[Seq[io.apibuilder.spec.v0.models.Field]]
attributes <- (__ \ "attributes").read[Seq[io.apibuilder.spec.v0.models.Attribute]]
default <- (__ \ "default").readNullable[Boolean]
discriminatorValue <- (__ \ "discriminator_value").readNullable[String]
- } yield UnionType(`type`, description, deprecation, attributes, default, discriminatorValue)
+ } yield UnionType(`type`, description, deprecation, fields, attributes, default, discriminatorValue)
}
def jsObjectUnionType(obj: io.apibuilder.spec.v0.models.UnionType): play.api.libs.json.JsObject = {
@@ -1369,6 +1371,10 @@ package io.apibuilder.spec.v0.models {
case None => play.api.libs.json.Json.obj()
case Some(x) => play.api.libs.json.Json.obj("deprecation" -> io.apibuilder.spec.v0.models.json.jsObjectDeprecation(x))
}) ++
+ (obj.fields match {
+ case None => play.api.libs.json.Json.obj()
+ case Some(x) => play.api.libs.json.Json.obj("fields" -> play.api.libs.json.Json.toJson(x))
+ }) ++
(obj.default match {
case None => play.api.libs.json.Json.obj()
case Some(x) => play.api.libs.json.Json.obj("default" -> play.api.libs.json.JsBoolean(x))
@@ -1575,7 +1581,7 @@ package io.apibuilder.spec.v0 {
val Namespace = "io.apibuilder.spec.v0"
val UserAgent = "apibuilder app.apibuilder.io/apicollective/apibuilder-spec/latest/play_2_9_scala_3_client"
- val Version = "0.16.53"
+ val Version = "0.16.74"
val VersionMajor = 0
}
diff --git a/generated/app/ApicollectiveApibuilderSpecV0MockClient.scala b/generated/app/ApicollectiveApibuilderSpecV0MockClient.scala
index 464e0983e..3dc4d9ffe 100644
--- a/generated/app/ApicollectiveApibuilderSpecV0MockClient.scala
+++ b/generated/app/ApicollectiveApibuilderSpecV0MockClient.scala
@@ -1,6 +1,6 @@
/**
* Generated by API Builder - https://www.apibuilder.io
- * Service version: 0.16.50
+ * Service version: 0.16.74
* User agent: apibuilder app.apibuilder.io/apicollective/apibuilder-spec/latest/play_2_8_mock_client
*/
package io.apibuilder.spec.v0.mock {
@@ -222,6 +222,7 @@ package io.apibuilder.spec.v0.mock {
`type` = Factories.randomString(24),
description = None,
deprecation = None,
+ fields = None,
attributes = Nil,
default = None,
discriminatorValue = None
diff --git a/lib/src/main/scala/generated/ApicollectiveApibuilderSpecV0Models.scala b/lib/src/main/scala/generated/ApicollectiveApibuilderSpecV0Models.scala
index c6f32fb6a..098227e5e 100644
--- a/lib/src/main/scala/generated/ApicollectiveApibuilderSpecV0Models.scala
+++ b/lib/src/main/scala/generated/ApicollectiveApibuilderSpecV0Models.scala
@@ -1,7 +1,7 @@
/**
* Generated by API Builder - https://www.apibuilder.io
- * Service version: 0.16.53
- * User agent: apibuilder localhost 9000/apicollective/apibuilder-spec/latest/play_2_x_standalone_json
+ * Service version: 0.16.74
+ * User agent: apibuilder app.apibuilder.io/apicollective/apibuilder-spec/latest/play_2_x_standalone_json
*/
package io.apibuilder.spec.v0.models {
@@ -320,6 +320,7 @@ package io.apibuilder.spec.v0.models {
`type`: String,
description: _root_.scala.Option[String] = None,
deprecation: _root_.scala.Option[io.apibuilder.spec.v0.models.Deprecation] = None,
+ fields: _root_.scala.Option[Seq[io.apibuilder.spec.v0.models.Field]] = None,
attributes: Seq[io.apibuilder.spec.v0.models.Attribute] = Nil,
default: _root_.scala.Option[Boolean] = None,
discriminatorValue: _root_.scala.Option[String] = None
@@ -1351,10 +1352,11 @@ package io.apibuilder.spec.v0.models {
`type` <- (__ \ "type").read[String]
description <- (__ \ "description").readNullable[String]
deprecation <- (__ \ "deprecation").readNullable[io.apibuilder.spec.v0.models.Deprecation]
+ fields <- (__ \ "fields").readNullable[Seq[io.apibuilder.spec.v0.models.Field]]
attributes <- (__ \ "attributes").read[Seq[io.apibuilder.spec.v0.models.Attribute]]
default <- (__ \ "default").readNullable[Boolean]
discriminatorValue <- (__ \ "discriminator_value").readNullable[String]
- } yield UnionType(`type`, description, deprecation, attributes, default, discriminatorValue)
+ } yield UnionType(`type`, description, deprecation, fields, attributes, default, discriminatorValue)
}
def jsObjectUnionType(obj: io.apibuilder.spec.v0.models.UnionType): play.api.libs.json.JsObject = {
@@ -1369,6 +1371,10 @@ package io.apibuilder.spec.v0.models {
case None => play.api.libs.json.Json.obj()
case Some(x) => play.api.libs.json.Json.obj("deprecation" -> io.apibuilder.spec.v0.models.json.jsObjectDeprecation(x))
}) ++
+ (obj.fields match {
+ case None => play.api.libs.json.Json.obj()
+ case Some(x) => play.api.libs.json.Json.obj("fields" -> play.api.libs.json.Json.toJson(x))
+ }) ++
(obj.default match {
case None => play.api.libs.json.Json.obj()
case Some(x) => play.api.libs.json.Json.obj("default" -> play.api.libs.json.JsBoolean(x))
diff --git a/spec/apibuilder-spec.json b/spec/apibuilder-spec.json
index a3f7d4bda..1e0979082 100644
--- a/spec/apibuilder-spec.json
+++ b/spec/apibuilder-spec.json
@@ -113,7 +113,7 @@
{ "name": "description", "type": "string", "required": false },
{ "name": "deprecation", "type": "deprecation", "required": false },
{ "name": "attributes", "type": "[attribute]", "default": "[]" },
- { "name": "value", "type": "string", "required": false, "description": "The actual string representation of this value. If not specified, defaults to 'name'" }
+ { "name": "value", "type": "string", "required": false, "description": "The actual string representation of this value. If not specified, defaults to 'name'" }
]
},
@@ -147,6 +147,7 @@
{ "name": "type", "type": "string", "description": "The name of a type (a primitive, model name, or enum name) that makes up this union type" },
{ "name": "description", "type": "string", "required": false },
{ "name": "deprecation", "type": "deprecation", "required": false },
+ { "name": "fields", "type": "[field]", "required": false },
{ "name": "attributes", "type": "[attribute]", "default": "[]" },
{ "name": "default", "type": "boolean", "required": false, "description": "If true, indicates that this type should be used as the default when deserializing union types. This field is only used by union types that require a discriminator and sets the default value for that discriminator during deserialization." },
{ "name": "discriminator_value", "type": "string", "required": false, "description": "The discriminator value defines the string to use in the discriminator field to identify this type. If not specified, the discriminator value will default to the name of the type itself." }
@@ -311,7 +312,7 @@
{ "name": "deprecation", "type": "deprecation", "required": false }
]
},
-
+
"annotation": {
"description": "Used to indicate an API concern for a field that is specific to the field's usage but not necessarily its data type. For example, you might use annotations to mark that certain fields contain PII or PCI data and thus should not be stored once processing is complete. Annotations communicate meaning to consumers of an API and may also be used within an implementation or tooling; for example, using static analysis tools to detect logging of sensitive data.",
"fields": [