Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .devops/config
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
apibuilder_organization=apicollective
3 changes: 3 additions & 0 deletions app/app/views/doc/apiJson.scala.html
Original file line number Diff line number Diff line change
Expand Up @@ -413,6 +413,7 @@ <h2>Union declaration</h2>
{
"name": {
"plural": <em>string (optional)</em>,
"fields": <em>JSON Array of <a href="#field">Field</a></em> (optional)</em>,
"discriminator": <em>string (optional)</em>,
"description": <em>string (optional)</em>,
"interfaces": <em>JSON Array of type string where each value indicates the name of a declared interface</em> (optional),
Expand All @@ -426,6 +427,8 @@ <h2>Union declaration</h2>
where:
<ul>
<li><em>name</em> 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.</li>
<li><em>fields</em> </li>
<li><em>fields</em> Optional JSON Array of 0 or more <a href="#field">Fields</a>. 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.</li>
<li><em>plural</em> 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.</li>
<li><em>discriminator</em> 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 <a href="@routes.DocController.playUnionTypes">Play Union Types</a> for more information and examples.</li>
@description("union")
Expand Down
21 changes: 20 additions & 1 deletion core/app/builder/api_json/ApiJsonServiceValidator.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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(_ =>
Expand Down
35 changes: 35 additions & 0 deletions core/app/builder/api_json/AutoCreateUnionTypeModels.scala
Original file line number Diff line number Diff line change
@@ -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
)
}
}
116 changes: 57 additions & 59 deletions core/app/builder/api_json/InternalApiJsonForm.scala
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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(
Expand Down Expand Up @@ -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"),
Expand Down Expand Up @@ -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],
Expand Down Expand Up @@ -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("")}]")
)
)
Expand Down Expand Up @@ -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
}
Expand Down
1 change: 1 addition & 0 deletions core/app/builder/api_json/ServiceBuilder.scala
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Loading
Loading