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
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
@@ -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 <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#multiFormatSchemaObject">Multi Format Schema</a>
* @see <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#schemaObject">Schema</a>
*/
@EqualsAndHashCode(callSuper = true)
public class XMLFormatSchema extends MultiFormatSchema<Object> {

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 <b>MUST</b> be one of:
* <ul>
* <li>{@link Reference}</li>
* </ul>
*
* @param schema XML Schema or Reference
*/
@Override
public void setSchema(@NotNull Object schema) {
super.setSchema(schema);
}

/**
* Schema:
* <ul>
* <li>{@link Reference}</li>
* </ul>
*/
@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;
}
}
Original file line number Diff line number Diff line change
@@ -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<Object> {

@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");
}

}
Original file line number Diff line number Diff line change
@@ -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("<xs:schema"))
}

private fun parse(specPath: String): AsyncAPI? {
return objectMapper.readValue(
ClasspathUtils.readAsString(specPath),
AsyncAPI::class.java
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
asyncapi: 3.0.0
info:
title: Account Service
version: 1.0.0
description: This service is in charge of processing user signups
channels:
userSignedup:
address: user/signedup
messages:
UserSignedUp:
$ref: '#/components/messages/UserSignedUp'
operations:
sendUserSignedup:
action: send
channel:
$ref: '#/channels/userSignedup'
messages:
- $ref: '#/channels/userSignedup/messages/UserSignedUp'
components:
messages:
UserSignedUp:
payload:
schemaFormat: application/xml
schema:
$ref: /path/to/schema.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
asyncapi: 3.0.0
info:
title: Account Service
version: 1.0.0
description: This service is in charge of processing user signups
channels:
userSignedup:
address: user/signedup
messages:
UserSignedUp:
$ref: '#/components/messages/UserSignedUp'
operations:
sendUserSignedup:
action: send
channel:
$ref: '#/channels/userSignedup'
messages:
- $ref: '#/channels/userSignedup/messages/UserSignedUp'
components:
messages:
UserSignedUp:
payload:
schemaFormat: application/xml
schema: |
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
<xs:element name="User">
<xs:complexType>
<xs:sequence>
<xs:element name="displayName" type="xs:string">
<xs:annotation>
<xs:documentation>Name of the user</xs:documentation>
</xs:annotation>
</xs:element>
<xs:element name="email" type="xs:string">
<xs:annotation>
<xs:documentation>Email of the user</xs:documentation>
</xs:annotation>
</xs:element>
</xs:sequence>
</xs:complexType>
</xs:element>
</xs:schema>


Loading