Skip to content

Commit

Permalink
Merge pull request #2031 from aml-org/publish-5.5.4
Browse files Browse the repository at this point in the history
W-16224755: Publish 5.5.4
  • Loading branch information
damianpedra authored Jul 30, 2024
2 parents 66e2d74 + 427e24a commit 65089b3
Show file tree
Hide file tree
Showing 123 changed files with 14,239 additions and 389 deletions.
59 changes: 59 additions & 0 deletions adrs/0014-avro-parsing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# 13. AWS OAS parsing

Date: 2024-07-02


## Status

Accepted


## Context

Async 2.x supports AVRO Schemas and we currently don't.
We want to add support AVRO Schemas inside Async APIs and as a standalone documents.

We need to decide:
- how are we going to map AVRO Schemas to the AMF Model
- how are we going to validate AVRO Schemas

an AVRO Schema has the following properties:
- It's defined in plain JSON, they MAY be also be defined as a `.avsc` file
- It doesn't have a special key that indicates it's an AVRO Schema, nor it's version (like JSON Schema does with it's `$schema`)


## Decision

Implement AVRO Schema parsing as a new specification, following the [AVRO Schema 1.9.0 specification](https://avro.apache.org/docs/1.9.0/spec.html#schemas).

An AVRO Schema may be a:
- Map
- Array
- Record (with fields, each one being any of the possible types)
- Enum
- Fixed Type
- Primitive Type ("null", "boolean", "int", "long", "float", "double", "bytes", "string")

We've parsed each AVRO Type to the following AMF Shape:
- Map --> NodeShape with `AdditionalProperties` field for the values shape
- Array --> ArrayShape with `Items` field for the items shape
- Record --> NodeShape with `Properties` with a PropertyShape that contains each field shape
- Enum --> ScalarShape with `Values` field for it's symbols
- Fixed Type --> ScalarShape with `Datatype` field for its type and `Size` for its size
- Primitive Type --> ScalarShape with `Datatype` field, or NilShape if its type 'null'

Given that in this mapping, several AVRO Types correspond to a ScalarShape or a NodeShape, **we've added the `avro-schema` annotation** with an `avroType` that contains the avro type declared before parsing.
This way, we can know the exact type for rendering or other purposes, for example having a NodeShape and knowing if it's an avro record or a map (both are parsed as NodeShapes).

We've also added 3 AVRO-specific fields to the `AnyShape` Model via the `AvroFields` trait, adding the following fields:
- AvroNamespace
- Aliases
- Size


For now only parsing is done


## Consequences

None so far.
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,12 @@ import amf.apicontract.internal.convert.ApiRegister
import amf.apicontract.internal.entities.{APIEntities, FragmentEntities}
import amf.apicontract.internal.plugins.ApiContractFallbackPlugin
import amf.apicontract.internal.spec.async.{Async20ElementRenderPlugin, Async20ParsePlugin, Async20RenderPlugin}
import amf.apicontract.internal.spec.avro.AvroParsePlugin
import amf.apicontract.internal.spec.avro.transformation.{
AvroSchemaCachePipeline,
AvroSchemaEditingPipeline,
AvroSchemaTransformationPipeline
}
import amf.apicontract.internal.spec.avro.{AvroParsePlugin, AvroRenderPlugin}
import amf.apicontract.internal.spec.oas._
import amf.apicontract.internal.spec.raml._
import amf.apicontract.internal.transformation._
Expand Down Expand Up @@ -159,8 +164,17 @@ object RAMLConfiguration extends APIConfigurationBuilder {

// AVRO is in alpha support mode
object AvroConfiguration extends APIConfigurationBuilder {
def Avro(): AMFConfiguration =
common().withPlugins(List(AvroParsePlugin)) // TODO: add validation profiles and serialization
def Avro(): AMFConfiguration = {
common()
.withPlugins(List(AvroParsePlugin, AvroRenderPlugin)) // TODO: add validation profiles
.withTransformationPipelines(
List(
AvroSchemaTransformationPipeline(),
AvroSchemaEditingPipeline(),
AvroSchemaCachePipeline()
)
)
}
}

/** [[APIConfigurationBuilder.common common()]] configuration with all configurations needed for OAS like:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package amf.apicontract.internal.spec.async
import amf.apicontract.internal.spec.async.AsyncHeader._
import amf.apicontract.internal.spec.async.parser.context.{Async2WebApiContext, AsyncWebApiContext}
import amf.apicontract.internal.spec.async.parser.document
import amf.apicontract.internal.spec.avro.AvroParsePlugin
import amf.apicontract.internal.spec.common.AsyncWebApiDeclarations
import amf.apicontract.internal.spec.oas.OasLikeParsePlugin
import amf.apicontract.internal.spec.raml.Raml10ParsePlugin
Expand All @@ -20,7 +21,7 @@ object Async20ParsePlugin extends OasLikeParsePlugin {
override def applies(element: Root): Boolean = AsyncHeader(element).isDefined

override def validSpecsToReference: Seq[Spec] =
super.validSpecsToReference :+ Raml10ParsePlugin.spec
super.validSpecsToReference ++ Seq(Raml10ParsePlugin.spec, AvroParsePlugin.spec)

override def mediaTypes: Seq[String] = Seq(Mimes.`application/yaml`, Mimes.`application/json`)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,38 +59,42 @@ case class AsyncApiTypeParser(entry: YMapEntry, adopt: Shape => Unit, version: S
) {

def parse(): Option[Shape] = version match {
case RAML10SchemaVersion => CustomRamlReferenceParser(YMapEntryLike(entry), adopt).parse()
case AVROSchema(_) =>
new AvroShapeParser(YMapEntryLike(entry).asMap)(new AvroSchemaContext(ctx, AvroSettings)).parse()
case _ => OasTypeParser(entry, adopt, version).parse()
case RAML10SchemaVersion => CustomReferenceParser(YMapEntryLike(entry), parseRamlType, adopt).parse()
case AVROSchema(_) => CustomReferenceParser(YMapEntryLike(entry), parseAvroSchema, adopt).parse()
case _ => OasTypeParser(entry, adopt, version).parse()
}

private def parseRamlType(entry: YMapEntryLike): Option[Shape] = {
val context = toRaml(ctx)
context.declarations.shapes = Map.empty
val result =
Raml10TypeParser(entry, "schema", adopt, TypeInfo(), AnyDefaultType)(context).parse()
context.futureDeclarations.resolve()
result
}

private def parseAvroSchema(entry: YMapEntryLike): Option[Shape] = {
new AvroShapeParser(entry.asMap)(new AvroSchemaContext(ctx, AvroSettings)).parse()
}
}

case class CustomRamlReferenceParser(entry: YMapEntryLike, adopt: Shape => Unit)(implicit
val ctx: OasLikeWebApiContext
case class CustomReferenceParser(entry: YMapEntryLike, parser: YMapEntryLike => Option[Shape], adopt: Shape => Unit)(
implicit val ctx: OasLikeWebApiContext
) {

def parse(): Option[Shape] = {
val shape = ctx.link(entry.value) match {
case Left(refValue) => handleRef(refValue)
case Right(_) => parseRamlType(entry)
case Right(_) => parser(entry)
}
shape.foreach(_.annotations += DefinedBySpec(Raml10))
shape
}

private def parseRamlType(entry: YMapEntryLike): Option[Shape] = {
val context = toRaml(ctx)
context.declarations.shapes = Map.empty
val result =
Raml10TypeParser(entry, "schema", adopt, TypeInfo(), AnyDefaultType)(context).parse()
context.futureDeclarations.resolve()
result
}

private def handleRef(refValue: String): Option[Shape] = {
val link = dataTypeFragmentRef(refValue)
.orElse(typeDefinedInLibraryRef(refValue))
.orElse(avroSchemaDocRef(refValue))
.orElse(externalFragmentRef(refValue))

if (link.isEmpty)
Expand All @@ -109,6 +113,11 @@ case class CustomRamlReferenceParser(entry: YMapEntryLike, adopt: Shape => Unit)
result
}

private def avroSchemaDocRef(refValue: String): Option[Shape] = {
val result = ctx.declarations.findEncodedTypeInDocFragment(refValue)
result.map(linkAndAdopt(_, refValue))
}

private def typeDefinedInLibraryRef(refValue: String): Option[Shape] = {
val values = refValue.split("#/types/").toList
values match {
Expand All @@ -122,7 +131,7 @@ case class CustomRamlReferenceParser(entry: YMapEntryLike, adopt: Shape => Unit)

private def externalFragmentRef(refValue: String): Option[Shape] = {
ctx.obtainRemoteYNode(refValue).flatMap { node =>
parseRamlType(YMapEntryLike(node))
parser(YMapEntryLike(node))
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ class Async20ConcreteOperationParser(entry: YMapEntry, adopt: Operation => Opera
override protected def parseMessages(map: YMap, operation: Operation): Unit = map.key(
"message",
messageEntry =>
AsyncHelper.messageType(entry.key.value.toString) foreach { msgType =>
AsyncHelper.messageType(entry.key.as[YScalar].text) foreach { msgType =>
val messages = AsyncMultipleMessageParser(messageEntry.value.as[YMap], operation.id, msgType).parse()
operation.fields
.setWithoutId(msgType.field, AmfArray(messages, Annotations(messageEntry.value)), Annotations(messageEntry))
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
package amf.apicontract.internal.spec.avro

import amf.core.client.common.{NormalPriority, PluginPriority}
import amf.core.client.scala.errorhandling.AMFErrorHandler
import amf.core.client.scala.model.document.BaseUnit
import amf.core.internal.plugins.render.{RenderConfiguration, RenderInfo, SYAMLBasedRenderPlugin}
import amf.core.internal.remote.Mimes._
import amf.core.internal.remote.Spec
import amf.apicontract.internal.spec.avro.emitters.context.AvroShapeEmitterContext
import amf.apicontract.internal.spec.avro.emitters.document.AvroSchemaDocumentEmitter
import amf.shapes.client.scala.model.document.AvroSchemaDocument
import org.yaml.model.YDocument

object AvroRenderPlugin extends SYAMLBasedRenderPlugin {

override val id: String = Spec.AVRO_SCHEMA.id

override def priority: PluginPriority = NormalPriority

override def defaultSyntax(): String = `application/json`

override def mediaTypes: Seq[String] = Seq(`application/json`)

override def applies(element: RenderInfo): Boolean = element.unit match {
case _: AvroSchemaDocument => true
case _ => false
}

override protected def unparseAsYDocument(
unit: BaseUnit,
renderConfig: RenderConfiguration,
errorHandler: AMFErrorHandler
): Option[YDocument] = {
unit match {
case document: AvroSchemaDocument => Some(emitDocument(renderConfig, errorHandler, document))
case _ => None
}
}

private def emitDocument(
renderConfig: RenderConfiguration,
errorHandler: AMFErrorHandler,
document: AvroSchemaDocument
): YDocument = {
new AvroSchemaDocumentEmitter(document)(
specContext(renderConfig, errorHandler)
).emit()
}

private def specContext(
renderConfig: RenderConfiguration,
errorHandler: AMFErrorHandler
): AvroShapeEmitterContext = {
new AvroShapeEmitterContext(errorHandler, renderConfig)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package amf.apicontract.internal.spec.avro.emitters.context

import amf.core.client.scala.errorhandling.AMFErrorHandler
import amf.core.client.scala.model.domain.Shape
import amf.core.internal.plugins.render.RenderConfiguration
import amf.shapes.internal.annotations.AVROSchemaType

class AvroShapeEmitterContext(
eh: AMFErrorHandler,
config: RenderConfiguration
) extends AvroSpecEmitterContext(eh, AvroRefEmitter, config) {

override val factory: AvroSpecEmitterFactory = new AvroSpecEmitterFactory()(this)

def getAvroType(shape: Shape): Option[String] = {
shape.annotations.find(classOf[AVROSchemaType]).map(_.avroType)
}

def isPrimitive(avroType: String): Boolean =
Seq("null", "boolean", "int", "long", "float", "double", "bytes", "string").contains(avroType)

def isComplex(avroType: String): Boolean =
Seq("record", "enum", "array", "map", "union", "fixed").contains(avroType)

def isComplex(shape: Shape): Boolean = getAvroType(shape).exists(isComplex)

def isPrimitive(shape: Shape): Boolean = getAvroType(shape).exists(isPrimitive)
}

object AvroShapeEmitterContext {
implicit def fromSpecEmitterContext(implicit spec: AvroSpecEmitterContext): AvroShapeEmitterContext =
new AvroShapeEmitterContext(spec.eh, spec.config)

def apply(eh: AMFErrorHandler, config: RenderConfiguration): AvroShapeEmitterContext = {
new AvroShapeEmitterContext(eh, config)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package amf.apicontract.internal.spec.avro.emitters.context

import amf.apicontract.client.scala.model.domain.Parameter
import amf.apicontract.client.scala.model.domain.security.{ParametrizedSecurityScheme, SecurityRequirement}
import amf.apicontract.internal.spec.avro.emitters.document.AvroDeclaredTypesEmitters
import amf.apicontract.internal.spec.avro.emitters.domain.AvroShapeEmitter
import amf.apicontract.internal.spec.common.emitter.{
AbstractSecurityRequirementEmitter,
AnnotationTypeEmitter,
ParametrizedSecuritySchemeEmitter
}
import amf.apicontract.internal.spec.oas.emitter.context.{OasLikeSpecEmitterContext, OasLikeSpecEmitterFactory}
import amf.core.client.scala.errorhandling.AMFErrorHandler
import amf.core.client.scala.model.document.BaseUnit
import amf.core.client.scala.model.domain.extensions.{CustomDomainProperty, ShapeExtension}
import amf.core.client.scala.model.domain.{DomainElement, RecursiveShape, Shape}
import amf.core.internal.metamodel.Field
import amf.core.internal.parser.domain.FieldEntry
import amf.core.internal.plugins.render.RenderConfiguration
import amf.core.internal.remote.{AvroSchema, Spec}
import amf.core.internal.render.BaseEmitters.MapEntryEmitter
import amf.core.internal.render.SpecOrdering
import amf.core.internal.render.emitters.{Emitter, EntryEmitter}
import amf.shapes.internal.spec.common.SchemaVersion
import amf.shapes.internal.spec.common.emitter.annotations.FacetsInstanceEmitter
import amf.shapes.internal.spec.common.emitter.{CustomFacetsEmitter, RefEmitter, TagToReferenceEmitter}
import org.yaml.model.YDocument.PartBuilder

import scala.util.matching.Regex

class AvroSpecEmitterContext(
eh: AMFErrorHandler,
refEmitter: RefEmitter = AvroRefEmitter,
val config: RenderConfiguration
) extends OasLikeSpecEmitterContext(eh, refEmitter, config) {

override val factory: AvroSpecEmitterFactory = new AvroSpecEmitterFactory()(this)
val spec: Spec = AvroSchema
def schemasDeclarationsPath: String = "/definitions/"
override def schemaVersion: SchemaVersion = ???
override def nameRegex: Regex = ???
}

class AvroSpecEmitterFactory(implicit override val spec: AvroSpecEmitterContext) extends OasLikeSpecEmitterFactory {

override def declaredTypesEmitter: (Seq[Shape], Seq[BaseUnit], SpecOrdering) => EntryEmitter =
(types, references, ordering) =>
AvroDeclaredTypesEmitters.obtainEmitter(types, references, ordering, spec.renderConfig)

def typeEmitters(
shape: Shape,
ordering: SpecOrdering,
ignored: Seq[Field] = Nil,
references: Seq[BaseUnit],
pointer: Seq[String] = Nil,
schemaPath: Seq[(String, String)] = Nil
): Seq[Emitter] = {
implicit val shapeContext: AvroShapeEmitterContext = AvroShapeEmitterContext.fromSpecEmitterContext(spec)
AvroShapeEmitter(shape, ordering)(shapeContext).entries()
}

override def recursiveShapeEmitter(
shape: RecursiveShape,
ordering: SpecOrdering,
schemaPath: Seq[(String, String)]
): EntryEmitter = ???

override def tagToReferenceEmitter: (DomainElement, Seq[BaseUnit]) => TagToReferenceEmitter = ???

override def customFacetsEmitter: (FieldEntry, SpecOrdering, Seq[BaseUnit]) => CustomFacetsEmitter = ???

override def facetsInstanceEmitter: (ShapeExtension, SpecOrdering) => FacetsInstanceEmitter = ???

override def parametrizedSecurityEmitter
: (ParametrizedSecurityScheme, SpecOrdering) => ParametrizedSecuritySchemeEmitter = ???

override def securityRequirementEmitter: (SecurityRequirement, SpecOrdering) => AbstractSecurityRequirementEmitter =
???

override def annotationTypeEmitter: (CustomDomainProperty, SpecOrdering) => AnnotationTypeEmitter = ???

override def headerEmitter: (Parameter, SpecOrdering, Seq[BaseUnit]) => EntryEmitter = ???
}

object AvroRefEmitter extends RefEmitter {
override def ref(url: String, b: PartBuilder): Unit = b.obj(MapEntryEmitter("$ref", url).emit(_))
}
Loading

0 comments on commit 65089b3

Please sign in to comment.