diff --git a/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/MultiFormatSchema.java b/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/MultiFormatSchema.java index 6a6e1cb9..1c09a6a7 100644 --- a/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/MultiFormatSchema.java +++ b/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/MultiFormatSchema.java @@ -97,6 +97,9 @@ "application/vnd.apache.avro;version=1.11.1", "application/vnd.apache.avro+json;version=1.11.1", "application/vnd.apache.avro+yaml;version=1.11.1" + }), + @JsonSubTypes.Type(value = XMLFormatSchema.class, names = { + "application/xml" }) }) @EqualsAndHashCode(callSuper = true) diff --git a/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/XMLFormatSchema.java b/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/XMLFormatSchema.java new file mode 100644 index 00000000..9c4b8ad7 --- /dev/null +++ b/asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/XMLFormatSchema.java @@ -0,0 +1,66 @@ +package com.asyncapi.schemas.asyncapi.multiformat; + +import com.asyncapi.schemas.asyncapi.AsyncAPISchema; +import com.asyncapi.schemas.asyncapi.Reference; +import com.asyncapi.schemas.xml.XMLSchemaDeserializer; +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import lombok.EqualsAndHashCode; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +/** + * The XML Format Schema Object represents a schema definition. It differs from the {@link AsyncAPISchema} in that it supports + * multiple schema formats or languages (e.g., JSON Schema, Avro, XML, etc.). + * + * @see Multi Format Schema + * @see Schema + */ +@EqualsAndHashCode(callSuper = true) +public class XMLFormatSchema extends MultiFormatSchema { + + public XMLFormatSchema(@NotNull @JsonDeserialize(using = XMLSchemaDeserializer.class) Object schema) { + super("application/xml", schema); + } + + @JsonCreator(mode = JsonCreator.Mode.PROPERTIES) + public XMLFormatSchema( + @JsonProperty("schemaFormat") @Nullable String schemaFormat, + @JsonProperty("schema") @NotNull @JsonDeserialize(using = XMLSchemaDeserializer.class) Object schema + ) { + super(schemaFormat(schemaFormat), schema); + } + + /** + * Schema MUST be one of: + * + * + * @param schema XML Schema or Reference + */ + @Override + public void setSchema(@NotNull Object schema) { + super.setSchema(schema); + } + + /** + * Schema: + * + */ + @NotNull + public Object getSchema() { + return super.getSchema(); + } + + @NotNull + private static String schemaFormat(@Nullable String schemaFormat) { + if (schemaFormat == null || schemaFormat.isEmpty()) { + return "application/xml"; + } + return schemaFormat; + } +} diff --git a/asyncapi-core/src/main/java/com/asyncapi/schemas/xml/XMLSchemaDeserializer.java b/asyncapi-core/src/main/java/com/asyncapi/schemas/xml/XMLSchemaDeserializer.java new file mode 100644 index 00000000..02742385 --- /dev/null +++ b/asyncapi-core/src/main/java/com/asyncapi/schemas/xml/XMLSchemaDeserializer.java @@ -0,0 +1,48 @@ +package com.asyncapi.schemas.xml; + +import com.asyncapi.schemas.asyncapi.Reference; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.ObjectCodec; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.node.JsonNodeType; + +import java.io.IOException; +import java.util.Objects; + +public class XMLSchemaDeserializer extends JsonDeserializer { + + @Override + public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException { + ObjectCodec objectCodec = p.getCodec(); + JsonNode node = objectCodec.readTree(p); + + return chooseKnownPojo(node, objectCodec); + } + + private Object chooseKnownPojo(JsonNode jsonNode, final ObjectCodec objectCodec) throws IOException { + try (JsonParser jsonParser = jsonNode.traverse(objectCodec)) { + JsonNodeType nodeType = jsonNode.getNodeType(); + + if (Objects.requireNonNull(nodeType) == JsonNodeType.STRING) { + return jsonParser.readValueAs(String.class); + } + + if(Objects.requireNonNull(nodeType) == JsonNodeType.OBJECT) { + if (isRefNode(jsonNode)) { + return jsonParser.readValueAs(Reference.class); + } + return null; + } + + return null; + } + } + + private boolean isRefNode(JsonNode jsonNode) { + return jsonNode.size() == 1 && jsonNode.has("$ref"); + } + +} diff --git a/asyncapi-core/src/test/kotlin/com/asyncapi/schemas/multiformat/xml/XmlFormatSchemaTest.kt b/asyncapi-core/src/test/kotlin/com/asyncapi/schemas/multiformat/xml/XmlFormatSchemaTest.kt new file mode 100644 index 00000000..fa165740 --- /dev/null +++ b/asyncapi-core/src/test/kotlin/com/asyncapi/schemas/multiformat/xml/XmlFormatSchemaTest.kt @@ -0,0 +1,56 @@ +package com.asyncapi.schemas.multiformat.xml + +import com.asyncapi.schemas.asyncapi.Reference +import com.asyncapi.schemas.asyncapi.multiformat.XMLFormatSchema +import com.asyncapi.v3.ClasspathUtils +import com.asyncapi.v3._0_0.model.AsyncAPI +import com.asyncapi.v3._0_0.model.channel.message.Message +import com.fasterxml.jackson.annotation.JsonInclude +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.dataformat.yaml.YAMLFactory +import org.junit.jupiter.api.Test + +class XmlFormatSchemaTest { + + private val objectMapper = ObjectMapper(YAMLFactory()) + .setSerializationInclusion(JsonInclude.Include.NON_NULL) + + @Test + fun `should parse a spec where the schemaFormat is application xml and the schema is referenced from an external xml schema file`() { + val specFilePath = "/examples/v3.0.0/xml/simple-referenced-schema.yaml" + val asyncAPI = parse(specFilePath) + + val payload = (asyncAPI?.components?.messages?.get("UserSignedUp") as? Message)?.payload + + assert(payload is XMLFormatSchema) + payload as XMLFormatSchema + + assert(payload.schemaFormat == "application/xml") + assert(payload.schema is Reference) + + assert((payload.schema as Reference).ref == "/path/to/schema.xml") + } + + @Test + fun `should parse a spec where the schemaFormat is application xml and the schema is specified as an inline string`() { + val specFilePath = "/examples/v3.0.0/xml/simple-string-schema.yaml" + val asyncAPI = parse(specFilePath) + + val payload = (asyncAPI?.components?.messages?.get("UserSignedUp") as? Message)?.payload + + assert(payload is XMLFormatSchema) + payload as XMLFormatSchema + + assert(payload.schemaFormat == "application/xml") + assert(payload.schema is String) + + assert((payload.schema as String).contains(" + + + + + + Name of the user + + + + + Email of the user + + + + + + + +