Skip to content

Commit 8e918e0

Browse files
Add basic support for XML schema format in messages
1 parent 8f4335f commit 8e918e0

File tree

6 files changed

+242
-0
lines changed

6 files changed

+242
-0
lines changed

asyncapi-core/src/main/java/com/asyncapi/schemas/asyncapi/multiformat/MultiFormatSchema.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,9 @@
9797
"application/vnd.apache.avro;version=1.11.1",
9898
"application/vnd.apache.avro+json;version=1.11.1",
9999
"application/vnd.apache.avro+yaml;version=1.11.1"
100+
}),
101+
@JsonSubTypes.Type(value = XMLFormatSchema.class, names = {
102+
"application/xml"
100103
})
101104
})
102105
@EqualsAndHashCode(callSuper = true)
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package com.asyncapi.schemas.asyncapi.multiformat;
2+
3+
import com.asyncapi.schemas.asyncapi.AsyncAPISchema;
4+
import com.asyncapi.schemas.asyncapi.Reference;
5+
import com.asyncapi.schemas.xml.XMLSchemaDeserializer;
6+
import com.fasterxml.jackson.annotation.JsonCreator;
7+
import com.fasterxml.jackson.annotation.JsonProperty;
8+
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
9+
import lombok.EqualsAndHashCode;
10+
import org.jetbrains.annotations.NotNull;
11+
import org.jetbrains.annotations.Nullable;
12+
13+
/**
14+
* The XML Format Schema Object represents a schema definition. It differs from the {@link AsyncAPISchema} in that it supports
15+
* multiple schema formats or languages (e.g., JSON Schema, Avro, XML, etc.).
16+
*
17+
* @see <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#multiFormatSchemaObject">Multi Format Schema</a>
18+
* @see <a href="https://www.asyncapi.com/docs/reference/specification/v3.0.0#schemaObject">Schema</a>
19+
*/
20+
@EqualsAndHashCode(callSuper = true)
21+
public class XMLFormatSchema extends MultiFormatSchema<Object> {
22+
23+
public XMLFormatSchema(@NotNull @JsonDeserialize(using = XMLSchemaDeserializer.class) Object schema) {
24+
super("application/xml", schema);
25+
}
26+
27+
@JsonCreator(mode = JsonCreator.Mode.PROPERTIES)
28+
public XMLFormatSchema(
29+
@JsonProperty("schemaFormat") @Nullable String schemaFormat,
30+
@JsonProperty("schema") @NotNull @JsonDeserialize(using = XMLSchemaDeserializer.class) Object schema
31+
) {
32+
super(schemaFormat(schemaFormat), schema);
33+
}
34+
35+
/**
36+
* Schema <b>MUST</b> be one of:
37+
* <ul>
38+
* <li>{@link Reference}</li>
39+
* </ul>
40+
*
41+
* @param schema XML Schema or Reference
42+
*/
43+
@Override
44+
public void setSchema(@NotNull Object schema) {
45+
super.setSchema(schema);
46+
}
47+
48+
/**
49+
* Schema:
50+
* <ul>
51+
* <li>{@link Reference}</li>
52+
* </ul>
53+
*/
54+
@NotNull
55+
public Object getSchema() {
56+
return super.getSchema();
57+
}
58+
59+
@NotNull
60+
private static String schemaFormat(@Nullable String schemaFormat) {
61+
if (schemaFormat == null || schemaFormat.isEmpty()) {
62+
return "application/xml";
63+
}
64+
return schemaFormat;
65+
}
66+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
package com.asyncapi.schemas.xml;
2+
3+
import com.asyncapi.schemas.asyncapi.Reference;
4+
import com.fasterxml.jackson.core.JsonParser;
5+
import com.fasterxml.jackson.core.JsonProcessingException;
6+
import com.fasterxml.jackson.core.ObjectCodec;
7+
import com.fasterxml.jackson.databind.DeserializationContext;
8+
import com.fasterxml.jackson.databind.JsonDeserializer;
9+
import com.fasterxml.jackson.databind.JsonNode;
10+
import com.fasterxml.jackson.databind.node.JsonNodeType;
11+
12+
import java.io.IOException;
13+
import java.util.Objects;
14+
15+
public class XMLSchemaDeserializer extends JsonDeserializer<Object> {
16+
17+
@Override
18+
public Object deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
19+
ObjectCodec objectCodec = p.getCodec();
20+
JsonNode node = objectCodec.readTree(p);
21+
22+
return chooseKnownPojo(node, objectCodec);
23+
}
24+
25+
private Object chooseKnownPojo(JsonNode jsonNode, final ObjectCodec objectCodec) throws IOException {
26+
try (JsonParser jsonParser = jsonNode.traverse(objectCodec)) {
27+
JsonNodeType nodeType = jsonNode.getNodeType();
28+
29+
if (Objects.requireNonNull(nodeType) == JsonNodeType.STRING) {
30+
return jsonParser.readValueAs(String.class);
31+
}
32+
33+
if(Objects.requireNonNull(nodeType) == JsonNodeType.OBJECT) {
34+
if (isRefNode(jsonNode)) {
35+
return jsonParser.readValueAs(Reference.class);
36+
}
37+
return null;
38+
}
39+
40+
return null;
41+
}
42+
}
43+
44+
private boolean isRefNode(JsonNode jsonNode) {
45+
return jsonNode.size() == 1 && jsonNode.has("$ref");
46+
}
47+
48+
}
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package com.asyncapi.schemas.multiformat.xml
2+
3+
import com.asyncapi.schemas.asyncapi.Reference
4+
import com.asyncapi.schemas.asyncapi.multiformat.XMLFormatSchema
5+
import com.asyncapi.v3.ClasspathUtils
6+
import com.asyncapi.v3._0_0.model.AsyncAPI
7+
import com.asyncapi.v3._0_0.model.channel.message.Message
8+
import com.fasterxml.jackson.annotation.JsonInclude
9+
import com.fasterxml.jackson.databind.ObjectMapper
10+
import com.fasterxml.jackson.dataformat.yaml.YAMLFactory
11+
import org.junit.jupiter.api.Test
12+
13+
class XmlFormatSchemaTest {
14+
15+
private val objectMapper = ObjectMapper(YAMLFactory())
16+
.setSerializationInclusion(JsonInclude.Include.NON_NULL)
17+
18+
@Test
19+
fun `should parse a spec where the schemaFormat is application xml and the schema is referenced from an external xml schema file`() {
20+
val specFilePath = "/examples/v3.0.0/xml/simple-referenced-schema.yaml"
21+
val asyncAPI = parse(specFilePath)
22+
23+
val payload = (asyncAPI?.components?.messages?.get("UserSignedUp") as? Message)?.payload
24+
25+
assert(payload is XMLFormatSchema)
26+
payload as XMLFormatSchema
27+
28+
assert(payload.schemaFormat == "application/xml")
29+
assert(payload.schema is Reference)
30+
31+
assert((payload.schema as Reference).ref == "/path/to/schema.xml")
32+
}
33+
34+
@Test
35+
fun `should parse a spec where the schemaFormat is application xml and the schema is specified as an inline string`() {
36+
val specFilePath = "/examples/v3.0.0/xml/simple-string-schema.yaml"
37+
val asyncAPI = parse(specFilePath)
38+
39+
val payload = (asyncAPI?.components?.messages?.get("UserSignedUp") as? Message)?.payload
40+
41+
assert(payload is XMLFormatSchema)
42+
payload as XMLFormatSchema
43+
44+
assert(payload.schemaFormat == "application/xml")
45+
assert(payload.schema is String)
46+
47+
assert((payload.schema as String).contains("<xs:schema"))
48+
}
49+
50+
private fun parse(specPath: String): AsyncAPI? {
51+
return objectMapper.readValue(
52+
ClasspathUtils.readAsString(specPath),
53+
AsyncAPI::class.java
54+
)
55+
}
56+
}
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
asyncapi: 3.0.0
2+
info:
3+
title: Account Service
4+
version: 1.0.0
5+
description: This service is in charge of processing user signups
6+
channels:
7+
userSignedup:
8+
address: user/signedup
9+
messages:
10+
UserSignedUp:
11+
$ref: '#/components/messages/UserSignedUp'
12+
operations:
13+
sendUserSignedup:
14+
action: send
15+
channel:
16+
$ref: '#/channels/userSignedup'
17+
messages:
18+
- $ref: '#/channels/userSignedup/messages/UserSignedUp'
19+
components:
20+
messages:
21+
UserSignedUp:
22+
payload:
23+
schemaFormat: application/xml
24+
schema:
25+
$ref: /path/to/schema.xml
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
asyncapi: 3.0.0
2+
info:
3+
title: Account Service
4+
version: 1.0.0
5+
description: This service is in charge of processing user signups
6+
channels:
7+
userSignedup:
8+
address: user/signedup
9+
messages:
10+
UserSignedUp:
11+
$ref: '#/components/messages/UserSignedUp'
12+
operations:
13+
sendUserSignedup:
14+
action: send
15+
channel:
16+
$ref: '#/channels/userSignedup'
17+
messages:
18+
- $ref: '#/channels/userSignedup/messages/UserSignedUp'
19+
components:
20+
messages:
21+
UserSignedUp:
22+
payload:
23+
schemaFormat: application/xml
24+
schema: |
25+
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema" elementFormDefault="qualified">
26+
<xs:element name="User">
27+
<xs:complexType>
28+
<xs:sequence>
29+
<xs:element name="displayName" type="xs:string">
30+
<xs:annotation>
31+
<xs:documentation>Name of the user</xs:documentation>
32+
</xs:annotation>
33+
</xs:element>
34+
<xs:element name="email" type="xs:string">
35+
<xs:annotation>
36+
<xs:documentation>Email of the user</xs:documentation>
37+
</xs:annotation>
38+
</xs:element>
39+
</xs:sequence>
40+
</xs:complexType>
41+
</xs:element>
42+
</xs:schema>
43+
44+

0 commit comments

Comments
 (0)