Skip to content

Commit

Permalink
Change how discriminated unions are translated (#173)
Browse files Browse the repository at this point in the history
* Change how discriminated unions are translated

* fix tests and inline union logic

* silence mima

---------

Co-authored-by: Jeff Lewis <[email protected]>
  • Loading branch information
Baccata and lewisjkl authored Jul 2, 2024
1 parent 1ac4339 commit 310edbb
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 123 deletions.
4 changes: 4 additions & 0 deletions build.sc
Original file line number Diff line number Diff line change
Expand Up @@ -167,6 +167,10 @@ trait OpenapiModule extends BaseCrossScalaModule {
)

object test extends ScalaTests with BaseMunitTests

override def mimaBinaryIssueFilters = super.mimaBinaryIssueFilters() ++ Seq(
ProblemFilter.exclude[MissingClassProblem]("alloy.openapi.DiscriminatedUnions")
)
}

object `protocol-tests` extends BaseJavaModule {
Expand Down
4 changes: 2 additions & 2 deletions modules/openapi/src/alloy/openapi/AlloyOpenApiExtension.scala
Original file line number Diff line number Diff line change
Expand Up @@ -40,12 +40,12 @@ final class AlloyOpenApiExtension() extends Smithy2OpenApiExtension {
new UnsupportedTraits(),
new RemoveEmptyComponents(),
new AddTags(),
new ExternalDocumentationMapperOpenApi()
new ExternalDocumentationMapperOpenApi(),
new DiscriminatedUnionMemberComponents()
).asJava

override def getJsonSchemaMappers(): ju.List[JsonSchemaMapper] = List(
new OpenApiJsonSchemaMapper(): JsonSchemaMapper,
new DiscriminatedUnions(),
new UntaggedUnions(),
new DataExamplesMapper(),
new ExternalDocumentationMapperJsonSchema(),
Expand Down
82 changes: 0 additions & 82 deletions modules/openapi/src/alloy/openapi/DiscriminatedUnion.scala

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
/* Copyright 2022 Disney Streaming
*
* Licensed under the Tomorrow Open Source Technology License, Version 1.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://disneystreaming.github.io/TOST-1.0.txt
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package alloy.openapi

import scala.jdk.CollectionConverters._
import software.amazon.smithy.openapi.fromsmithy.OpenApiMapper
import software.amazon.smithy.openapi.fromsmithy.Context
import software.amazon.smithy.openapi.model.OpenApi
import software.amazon.smithy.model.traits.Trait
import alloy.DiscriminatedUnionTrait
import software.amazon.smithy.jsonschema.Schema
import software.amazon.smithy.model.shapes.ShapeId
import software.amazon.smithy.model.shapes.Shape
import software.amazon.smithy.model.traits.JsonNameTrait
import software.amazon.smithy.model.node.ObjectNode
import software.amazon.smithy.jsonschema.Schema.Builder

/** Creates components for the discriminated union
*/
class DiscriminatedUnionMemberComponents() extends OpenApiMapper {

override def after(
context: Context[_ <: Trait],
openapi: OpenApi
): OpenApi = {
val unions = context
.getModel()
.getUnionShapesWithTrait(classOf[DiscriminatedUnionTrait])
val componentBuilder = openapi.getComponents().toBuilder()
val componentSchemas: Map[String, Schema] = openapi
.getComponents()
.getSchemas()
.asScala
.toMap
unions.asScala.foreach { union =>
val unionMixinName = union.getId().getName() + "Mixin"
val unionMixinId =
ShapeId.fromParts(union.getId().getNamespace(), unionMixinName)
val discriminatorField =
union.expectTrait(classOf[DiscriminatedUnionTrait]).getValue()

val unionMixinSchema = Schema
.builder()
.`type`("object")
.properties(
Map(
discriminatorField -> Schema
.builder()
.`type`("string")
.build()
).asJava
)
.required(List(discriminatorField).asJava)
.build()

val unionMixinRef = context.createRef(unionMixinId)

componentBuilder.putSchema(unionMixinName, unionMixinSchema)

union.members().asScala.foreach { memberShape =>
val syntheticMemberName =
union.getId().getName() + memberShape.getMemberName.capitalize
context.getPointer(union).split('/').last + memberShape
.getMemberName()
.capitalize
val targetRef = context.createRef(memberShape.getTarget())
val syntheticUnionMember =
Schema.builder().allOf(List(targetRef, unionMixinRef).asJava).build()
componentBuilder.putSchema(syntheticMemberName, syntheticUnionMember)
}

val existingSchemaBuilder = componentSchemas
.get(union.toShapeId.getName)
.map(_.toBuilder())
.getOrElse(Schema.builder())
componentBuilder.putSchema(
union.toShapeId.getName,
updateDiscriminatedUnion(
union,
existingSchemaBuilder,
discriminatorField
)
.build()
)

}
openapi.toBuilder.components(componentBuilder.build()).build()
}

private def updateDiscriminatedUnion(
shape: Shape,
schemaBuilder: Builder,
discriminatorField: String
): Builder = {
val alts = shape
.members()
.asScala
.map { member =>
val label = member
.getTrait(classOf[JsonNameTrait])
.asScala
.map(_.getValue())
.getOrElse(member.getMemberName())
val syntheticMemberId =
shape.getId().getName() + member.getMemberName().capitalize
val refString = s"#/components/schemas/$syntheticMemberId"
val refSchema =
Schema.builder.ref(refString).build
(label, refString, refSchema)
}
.toList
val schemas = alts.map(_._3).asJava
val mapping = ObjectNode.fromStringMap(
alts
.map { case (label, refString, _) => (label, refString) }
.toMap
.asJava
)
schemaBuilder
.oneOf(schemas)
.putExtension(
"discriminator",
ObjectNode
.builder()
.withMember("propertyName", discriminatorField)
.withMember("mapping", mapping)
.build()
)
}

}
77 changes: 38 additions & 39 deletions modules/openapi/test/resources/foo.json
Original file line number Diff line number Diff line change
Expand Up @@ -246,56 +246,55 @@
"CatOrDog": {
"oneOf": [
{
"allOf": [
{
"$ref": "#/components/schemas/Cat"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"cat"
]
}
},
"required": [
"type"
]
}
]
"$ref": "#/components/schemas/CatOrDogCat"
},
{
"allOf": [
{
"$ref": "#/components/schemas/Dog"
},
{
"type": "object",
"properties": {
"type": {
"type": "string",
"enum": [
"dog"
]
}
},
"required": [
"type"
]
}
]
"$ref": "#/components/schemas/CatOrDogDog"
}
],
"externalDocs": {
"description": "Homepage",
"url": "https://www.example.com/"
},
"discriminator": {
"propertyName": "type"
"propertyName": "type",
"mapping": {
"cat": "#/components/schemas/CatOrDogCat",
"dog": "#/components/schemas/CatOrDogDog"
}
}
},
"CatOrDogCat": {
"allOf": [
{
"$ref": "#/components/schemas/Cat"
},
{
"$ref": "#/components/schemas/CatOrDogMixin"
}
]
},
"CatOrDogDog": {
"allOf": [
{
"$ref": "#/components/schemas/Dog"
},
{
"$ref": "#/components/schemas/CatOrDogMixin"
}
]
},
"CatOrDogMixin": {
"type": "object",
"properties": {
"type": {
"type": "string"
}
},
"required": [
"type"
]
},
"Dog": {
"type": "object",
"properties": {
Expand Down

0 comments on commit 310edbb

Please sign in to comment.