diff --git a/dependencies.gradle b/dependencies.gradle index 800e28fb7..6f37a18db 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -50,6 +50,8 @@ ext { jakartaAnnotationApiVersion = '3.0.0' jakartaValidationApiVersion = '3.0.2' + jakartaXmlBindApiVersion = '4.0.2' + jsonSchemaValidator = '1.3.3' mockitoCoreVersion = '5.11.0' diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalker.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalker.java index 2ba66f07a..cbdac1448 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalker.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalker.java @@ -19,6 +19,22 @@ @RequiredArgsConstructor public class DefaultSchemaWalker implements SchemaWalker { + Boolean DEFAULT_BOOLEAN_EXAMPLE = true; + + String DEFAULT_STRING_EXAMPLE = "string"; + Integer DEFAULT_INTEGER_EXAMPLE = 0; + Double DEFAULT_NUMBER_EXAMPLE = 1.1; + + String DEFAULT_DATE_EXAMPLE = "2015-07-20"; + String DEFAULT_DATE_TIME_EXAMPLE = "2015-07-20T15:49:04-07:00"; + String DEFAULT_PASSWORD_EXAMPLE = "string-password"; + String DEFAULT_BYTE_EXAMPLE = "YmFzZTY0LWV4YW1wbGU="; + String DEFAULT_BINARY_EXAMPLE = + "0111010001100101011100110111010000101101011000100110100101101110011000010110010001111001"; + + String DEFAULT_EMAIL_EXAMPLE = "example@example.com"; + String DEFAULT_UUID_EXAMPLE = "3fa85f64-5717-4562-b3fc-2c963f66afa6"; + private final ExampleValueGenerator exampleValueGenerator; @Override @@ -32,7 +48,8 @@ public R fromSchema(Schema schema, Map definitions) { String schemaName = exampleValueGenerator.lookupSchemaName(schema); try { - T generatedExample = buildExample(schemaName, schema, definitions, new HashSet<>()); + T generatedExample = buildExample(schemaName, schema, definitions, new HashSet<>()) + .orElseThrow(() -> new ExampleGeneratingException("Something went wrong")); return exampleValueGenerator.prepareForSerialization(schema, generatedExample); } catch (ExampleGeneratingException ex) { @@ -41,9 +58,9 @@ public R fromSchema(Schema schema, Map definitions) { return null; } - private T buildExample(String name, Schema schema, Map definitions, Set visited) { - T exampleValue = getExampleValueFromSchemaAnnotation(schema); - if (exampleValue != null) { + private Optional buildExample(String name, Schema schema, Map definitions, Set visited) { + Optional exampleValue = getExampleValueFromSchemaAnnotation(schema); + if (exampleValue.isPresent()) { return exampleValue; } @@ -55,27 +72,27 @@ private T buildExample(String name, Schema schema, Map definitio String type = schema.getType(); return switch (type) { case "array" -> buildArrayExample(schema, definitions, visited); - case "boolean" -> exampleValueGenerator.createBooleanExample(); - case "integer" -> exampleValueGenerator.createIntegerExample(); - case "number" -> exampleValueGenerator.createDoubleExample(); + case "boolean" -> exampleValueGenerator.createBooleanExample(DEFAULT_BOOLEAN_EXAMPLE, schema); + case "integer" -> exampleValueGenerator.createIntegerExample(DEFAULT_INTEGER_EXAMPLE, schema); + case "number" -> exampleValueGenerator.createDoubleExample(DEFAULT_NUMBER_EXAMPLE, schema); case "object" -> buildFromObjectSchema(name, schema, definitions, visited); case "string" -> buildFromStringSchema(schema); default -> exampleValueGenerator.createUnknownSchemaStringTypeExample(type); }; } - private T getExampleValueFromSchemaAnnotation(Schema schema) { + private Optional getExampleValueFromSchemaAnnotation(Schema schema) { Object exampleValue = schema.getExample(); // schema is a map of properties from a nested object, whose example cannot be inferred if (exampleValue == null) { - return null; + return Optional.empty(); } // Return directly, when we have processed this before T processedExample = exampleValueGenerator.getExampleOrNull(schema, exampleValue); if (processedExample != null) { - return processedExample; + return Optional.of(processedExample); } // Handle special types (i.e. map) with custom @Schema annotation and specified example value @@ -83,32 +100,32 @@ private T getExampleValueFromSchemaAnnotation(Schema schema) { if (additionalProperties instanceof StringSchema additionalPropertiesSchema) { Object exampleValueString = additionalPropertiesSchema.getExample(); if (exampleValueString != null) { - return exampleValueGenerator.createRaw(exampleValueString); + return Optional.ofNullable(exampleValueGenerator.createRaw(exampleValueString)); } } // schema is a map of properties from a nested object, whose example cannot be inferred if (exampleValue instanceof Map) { - return null; + return Optional.empty(); } // value is represented in their native type if (exampleValue instanceof Boolean) { - return exampleValueGenerator.createBooleanExample((Boolean) exampleValue); + return exampleValueGenerator.createBooleanExample((Boolean) exampleValue, schema); } else if (exampleValue instanceof Number) { double doubleValue = ((Number) exampleValue).doubleValue(); // Check if it's an integer (whole number) if (doubleValue == (int) doubleValue) { - return exampleValueGenerator.createIntegerExample((int) doubleValue); + return exampleValueGenerator.createIntegerExample((int) doubleValue, schema); } - return exampleValueGenerator.createDoubleExample(doubleValue); + return exampleValueGenerator.createDoubleExample(doubleValue, schema); } try { // value (i.e. OffsetDateTime) is represented as string - return exampleValueGenerator.createStringExample(exampleValue.toString()); + return exampleValueGenerator.createStringExample(exampleValue.toString(), schema); } catch (IllegalArgumentException ex) { log.debug("Unable to convert example to JSON: %s".formatted(exampleValue.toString()), ex); } @@ -116,35 +133,37 @@ private T getExampleValueFromSchemaAnnotation(Schema schema) { return exampleValueGenerator.createEmptyObjectExample(); } - private T buildArrayExample(Schema schema, Map definitions, Set visited) { + private Optional buildArrayExample(Schema schema, Map definitions, Set visited) { Schema arrayItemSchema = resolveSchemaFromRef(schema.getItems(), definitions).orElse(schema.getItems()); String arrayItemName = exampleValueGenerator.lookupSchemaName(arrayItemSchema); - T arrayItem = buildExample(arrayItemName, arrayItemSchema, definitions, visited); + + Optional arrayItem = buildExample(arrayItemName, arrayItemSchema, definitions, visited); String arrayName = exampleValueGenerator.lookupSchemaName(schema); - return exampleValueGenerator.createArrayExample(arrayName, arrayItem); + + return arrayItem.map(array -> exampleValueGenerator.createArrayExample(arrayName, array)); } - private T buildFromStringSchema(Schema schema) { + private Optional buildFromStringSchema(Schema schema) { String firstEnumValue = getFirstEnumValue(schema); if (firstEnumValue != null) { - return exampleValueGenerator.createEnumExample(firstEnumValue); + return exampleValueGenerator.createEnumExample(firstEnumValue, schema); } String format = schema.getFormat(); if (format == null) { - return exampleValueGenerator.createStringExample(); + return exampleValueGenerator.createStringExample(DEFAULT_STRING_EXAMPLE, schema); } return switch (format) { - case "date" -> exampleValueGenerator.createDateExample(); - case "date-time" -> exampleValueGenerator.createDateTimeExample(); - case "email" -> exampleValueGenerator.createEmailExample(); - case "password" -> exampleValueGenerator.createPasswordExample(); - case "byte" -> exampleValueGenerator.createByteExample(); - case "binary" -> exampleValueGenerator.createBinaryExample(); - case "uuid" -> exampleValueGenerator.createUuidExample(); + case "date" -> exampleValueGenerator.createStringExample(DEFAULT_DATE_EXAMPLE, schema); + case "date-time" -> exampleValueGenerator.createStringExample(DEFAULT_DATE_TIME_EXAMPLE, schema); + case "email" -> exampleValueGenerator.createStringExample(DEFAULT_EMAIL_EXAMPLE, schema); + case "password" -> exampleValueGenerator.createStringExample(DEFAULT_PASSWORD_EXAMPLE, schema); + case "byte" -> exampleValueGenerator.createStringExample(DEFAULT_BYTE_EXAMPLE, schema); + case "binary" -> exampleValueGenerator.createStringExample(DEFAULT_BINARY_EXAMPLE, schema); + case "uuid" -> exampleValueGenerator.createStringExample(DEFAULT_UUID_EXAMPLE, schema); default -> exampleValueGenerator.createUnknownSchemaStringFormatExample(format); }; } @@ -160,27 +179,37 @@ private String getFirstEnumValue(Schema schema) { return null; } - private T buildFromObjectSchema(String name, Schema schema, Map definitions, Set visited) { + private Optional buildFromObjectSchema( + String name, Schema schema, Map definitions, Set visited) { Map properties = schema.getProperties(); - if (properties != null) { - if (!visited.contains(schema)) { - visited.add(schema); + if (properties != null && !visited.contains(schema)) { - List> propertyList = - buildPropertyExampleListFromSchema(properties, definitions, visited); - T example = exampleValueGenerator.createObjectExample(name, propertyList); + visited.add(schema); - visited.remove(schema); - return example; - } + T object = exampleValueGenerator.startObject(name); + + List> propertyList = + buildPropertyExampleListFromSchema(properties, definitions, visited); + exampleValueGenerator.addPropertyExamples(object, propertyList); + + exampleValueGenerator.endObject(); + + visited.remove(schema); + return Optional.of(object); } if (schema.getAllOf() != null && !schema.getAllOf().isEmpty()) { List schemas = schema.getAllOf(); + T object = exampleValueGenerator.startObject(name); + List> mergedProperties = buildPropertyExampleListFromSchemas(schemas, definitions, visited); - return exampleValueGenerator.createObjectExample(name, mergedProperties); + exampleValueGenerator.addPropertyExamples(object, mergedProperties); + + exampleValueGenerator.endObject(); + + return Optional.of(object); } if (schema.getAnyOf() != null && !schema.getAnyOf().isEmpty()) { List schemas = schema.getAnyOf(); @@ -200,11 +229,16 @@ private T buildFromObjectSchema(String name, Schema schema, Map private List> buildPropertyExampleListFromSchema( Map properties, Map definitions, Set visited) { return properties.entrySet().stream() - .map(entry -> { - String propertyKey = entry.getKey(); - T propertyValue = buildExample(propertyKey, entry.getValue(), definitions, visited); - return new PropertyExample<>(propertyKey, propertyValue); + .map(propertySchema -> { + String propertyKey = propertySchema.getKey(); + Optional propertyValue = + buildExample(propertyKey, propertySchema.getValue(), definitions, visited); + + return propertyValue + .map(optionalElem -> new PropertyExample<>(propertyKey, optionalElem)) + .orElse(null); }) + .filter(Objects::nonNull) .sorted(Comparator.comparing(PropertyExample::name)) .toList(); } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/ExampleValueGenerator.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/ExampleValueGenerator.java index 645c7904e..138035e10 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/ExampleValueGenerator.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/ExampleValueGenerator.java @@ -4,6 +4,7 @@ import io.swagger.v3.oas.models.media.Schema; import java.util.List; +import java.util.Optional; /** * Provides the building blocks to generate an example @@ -13,28 +14,12 @@ */ public interface ExampleValueGenerator { - Boolean DEFAULT_BOOLEAN_EXAMPLE = true; - - String DEFAULT_STRING_EXAMPLE = "string"; - Integer DEFAULT_INTEGER_EXAMPLE = 0; - Double DEFAULT_NUMBER_EXAMPLE = 1.1; - - String DEFAULT_DATE_EXAMPLE = "2015-07-20"; - String DEFAULT_DATE_TIME_EXAMPLE = "2015-07-20T15:49:04-07:00"; - String DEFAULT_PASSWORD_EXAMPLE = "string-password"; - String DEFAULT_BYTE_EXAMPLE = "YmFzZTY0LWV4YW1wbGU="; - String DEFAULT_BINARY_EXAMPLE = - "0111010001100101011100110111010000101101011000100110100101101110011000010110010001111001"; - - String DEFAULT_EMAIL_EXAMPLE = "example@example.com"; - String DEFAULT_UUID_EXAMPLE = "3fa85f64-5717-4562-b3fc-2c963f66afa6"; - boolean canHandle(String contentType); /** * Some internal representation need to be initialized per Schema */ - void initialize(); + default void initialize() {} String lookupSchemaName(Schema schema); @@ -43,45 +28,27 @@ public interface ExampleValueGenerator { */ R prepareForSerialization(Schema name, T exampleObject); - T createIntegerExample(Integer value); - - T createDoubleExample(Double value); - - T createBooleanExample(); - - T createBooleanExample(Boolean value); - - T createIntegerExample(); - - T createObjectExample(String name, List> properties); - - T createEmptyObjectExample(); - - T createDoubleExample(); - - T createDateExample(); - - T createDateTimeExample(); + Optional createIntegerExample(Integer value, Schema schema); - T createEmailExample(); + Optional createDoubleExample(Double value, Schema schema); - T createPasswordExample(); + Optional createBooleanExample(Boolean value, Schema schema); - T createByteExample(); + Optional createEmptyObjectExample(); - T createBinaryExample(); + Optional createStringExample(String value, Schema schema); - T createUuidExample(); + Optional createEnumExample(String anEnumValue, Schema schema); - T createStringExample(); + Optional createUnknownSchemaStringTypeExample(String schemaType); - T createStringExample(String value); + Optional createUnknownSchemaStringFormatExample(String schemaFormat); - T createEnumExample(String anEnumValue); + T startObject(String name); - T createUnknownSchemaStringTypeExample(String schemaType); + default void endObject() {} - T createUnknownSchemaStringFormatExample(String schemaFormat); + void addPropertyExamples(T object, List> properties); T createArrayExample(String name, T arrayItem); diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/json/ExampleJsonValueGenerator.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/json/ExampleJsonValueGenerator.java index 58852e7f7..9cca5f2cb 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/json/ExampleJsonValueGenerator.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/json/ExampleJsonValueGenerator.java @@ -18,14 +18,14 @@ import lombok.extern.slf4j.Slf4j; import java.util.List; +import java.util.Optional; import java.util.Set; @Slf4j public class ExampleJsonValueGenerator implements ExampleValueGenerator { private static final Set SUPPORTED_CONTENT_TYPES = Set.of("application/json"); - private static final BooleanNode DEFAULT_BOOLEAN_EXAMPLE = - BooleanNode.valueOf(ExampleValueGenerator.DEFAULT_BOOLEAN_EXAMPLE); + private static final ObjectMapper objectMapper = Json.mapper(); @Override @@ -33,11 +33,6 @@ public boolean canHandle(String contentType) { return SUPPORTED_CONTENT_TYPES.contains(contentType); } - @Override - public void initialize() { - // Nothing to do - } - @Override public String lookupSchemaName(Schema schema) { return schema.getName(); @@ -45,94 +40,38 @@ public String lookupSchemaName(Schema schema) { @NotNull @Override - public JsonNode createBooleanExample() { - return DEFAULT_BOOLEAN_EXAMPLE; - } - - @NotNull - @Override - public JsonNode createBooleanExample(Boolean value) { - return BooleanNode.valueOf(value); - } - - @Override - public JsonNode createIntegerExample() { - return new IntNode(DEFAULT_INTEGER_EXAMPLE); - } - - @Override - public JsonNode createDoubleExample() { - return new DoubleNode(DEFAULT_NUMBER_EXAMPLE); - } - - @Override - public JsonNode createIntegerExample(Integer value) { - return new IntNode(value); - } - - @Override - public JsonNode createDoubleExample(Double value) { - return new DoubleNode(value); - } - - @Override - public JsonNode createStringExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_STRING_EXAMPLE); + public Optional createBooleanExample(Boolean value, Schema schema) { + return Optional.of(BooleanNode.valueOf(value)); } @Override - public JsonNode createStringExample(String value) { - return JsonNodeFactory.instance.textNode(value); + public Optional createIntegerExample(Integer value, Schema schema) { + return Optional.of(new IntNode(value)); } @Override - public JsonNode createEnumExample(String anEnumValue) { - return JsonNodeFactory.instance.textNode(anEnumValue); + public Optional createDoubleExample(Double value, Schema schema) { + return Optional.of(new DoubleNode(value)); } @Override - public JsonNode createDateExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_DATE_EXAMPLE); + public Optional createStringExample(String value, Schema schema) { + return Optional.of(JsonNodeFactory.instance.textNode(value)); } @Override - public JsonNode createDateTimeExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_DATE_TIME_EXAMPLE); + public Optional createEnumExample(String anEnumValue, Schema schema) { + return Optional.of(JsonNodeFactory.instance.textNode(anEnumValue)); } @Override - public JsonNode createEmailExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_EMAIL_EXAMPLE); + public Optional createUnknownSchemaStringTypeExample(String type) { + return Optional.of(JsonNodeFactory.instance.textNode("unknown schema type: " + type)); } @Override - public JsonNode createPasswordExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_PASSWORD_EXAMPLE); - } - - @Override - public JsonNode createByteExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_BYTE_EXAMPLE); - } - - @Override - public JsonNode createBinaryExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_BINARY_EXAMPLE); - } - - @Override - public JsonNode createUuidExample() { - return JsonNodeFactory.instance.textNode(DEFAULT_UUID_EXAMPLE); - } - - @Override - public JsonNode createUnknownSchemaStringTypeExample(String type) { - return JsonNodeFactory.instance.textNode("unknown schema type: " + type); - } - - @Override - public JsonNode createUnknownSchemaStringFormatExample(String schemaFormat) { - return JsonNodeFactory.instance.textNode("unknown string schema format: " + schemaFormat); + public Optional createUnknownSchemaStringFormatExample(String schemaFormat) { + return Optional.of(JsonNodeFactory.instance.textNode("unknown string schema format: " + schemaFormat)); } @Override @@ -167,14 +106,25 @@ public JsonNode getExampleOrNull(Schema schema, Object example) { } @Override - public JsonNode createObjectExample(String name, List> properties) { - ObjectNode objectNode = objectMapper.createObjectNode(); - properties.forEach(property -> objectNode.set(property.name(), property.example())); - return objectNode; + public JsonNode startObject(String name) { + return objectMapper.createObjectNode(); } @Override - public JsonNode createEmptyObjectExample() { - return objectMapper.createObjectNode(); + public void addPropertyExamples(JsonNode object, List> properties) { + if (object == null) { + throw new IllegalArgumentException("JsonNode to add properties must not be empty"); + } + + if (object instanceof ObjectNode objectNode) { + properties.forEach(property -> objectNode.set(property.name(), property.example())); + } else { + throw new IllegalArgumentException("JsonNode to add properties must be of type ObjectNode"); + } + } + + @Override + public Optional createEmptyObjectExample() { + return Optional.of(objectMapper.createObjectNode()); } } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGenerator.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGenerator.java index 6b19f8610..a29938df7 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGenerator.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGenerator.java @@ -22,7 +22,9 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; +import java.util.Stack; @Slf4j public class ExampleXmlValueGenerator implements ExampleValueGenerator { @@ -37,6 +39,8 @@ public class ExampleXmlValueGenerator implements ExampleValueGenerator exampleCache = new HashMap<>(); + private final Stack nodeStack = new Stack<>(); + public ExampleXmlValueGenerator(ExampleXmlValueSerializer exampleXmlValueSerializer) { this.exampleXmlValueSerializer = exampleXmlValueSerializer; } @@ -49,6 +53,7 @@ public boolean canHandle(String contentType) { @Override public void initialize() { try { + nodeStack.clear(); document = createDocument(); } catch (ParserConfigurationException e) { throw new RuntimeException(e); @@ -57,50 +62,52 @@ public void initialize() { @Override public String lookupSchemaName(Schema schema) { - if (schema.getXml() != null) { + if (schema.getXml() != null && schema.getXml().getName() != null) { return schema.getXml().getName(); } return schema.getName(); } @Override - public Node createIntegerExample(Integer value) { - return document.createTextNode(value.toString()); + public Optional createIntegerExample(Integer value, Schema schema) { + return createNodeOrAddAttribute(value.toString(), schema); } @Override - public Node createDoubleExample(Double value) { - return document.createTextNode(value.toString()); + public Optional createDoubleExample(Double value, Schema schema) { + return createNodeOrAddAttribute(value.toString(), schema); } @Override - public Node createBooleanExample() { - return createBooleanExample(DEFAULT_BOOLEAN_EXAMPLE); + public Optional createBooleanExample(Boolean value, Schema schema) { + return createNodeOrAddAttribute(value.toString(), schema); } @Override - public Node createBooleanExample(Boolean value) { - return document.createTextNode(value.toString()); + public Element startObject(String name) { + if (name == null) { + throw new IllegalArgumentException("Object name must not be empty"); + } + + return nodeStack.push(document.createElement(name)); } @Override - public Node createIntegerExample() { - return createIntegerExample(DEFAULT_INTEGER_EXAMPLE); + public void endObject() { + nodeStack.pop(); } @Override - public Node createObjectExample(String name, List> properties) { - if (name == null) { - throw new IllegalArgumentException("Object name must not be empty"); + public void addPropertyExamples(Node object, List> properties) { + if (object == null) { + throw new IllegalArgumentException("Element to add properties must not be empty"); } try { - Element rootElement = document.createElement(name); for (PropertyExample propertyExample : properties) { - rootElement.appendChild(handlePropertyExample(propertyExample)); + object.appendChild(handlePropertyExample(propertyExample)); } - return rootElement; } catch (ParserConfigurationException e) { throw new RuntimeException(e); } @@ -125,68 +132,23 @@ private Element handlePropertyExample(PropertyExample propertyExample) thr } @Override - public Node createDoubleExample() { - return createDoubleExample(DEFAULT_NUMBER_EXAMPLE); - } - - @Override - public Node createDateExample() { - return document.createTextNode(DEFAULT_DATE_EXAMPLE); - } - - @Override - public Node createDateTimeExample() { - return document.createTextNode(DEFAULT_DATE_TIME_EXAMPLE); - } - - @Override - public Node createEmailExample() { - return document.createTextNode(DEFAULT_EMAIL_EXAMPLE); - } - - @Override - public Node createPasswordExample() { - return document.createTextNode(DEFAULT_PASSWORD_EXAMPLE); - } - - @Override - public Node createByteExample() { - return document.createTextNode(DEFAULT_BYTE_EXAMPLE); - } - - @Override - public Node createBinaryExample() { - return document.createTextNode(DEFAULT_BINARY_EXAMPLE); - } - - @Override - public Node createUuidExample() { - return document.createTextNode(DEFAULT_UUID_EXAMPLE); - } - - @Override - public Node createStringExample() { - return createStringExample(DEFAULT_STRING_EXAMPLE); + public Optional createStringExample(String value, Schema schema) { + return createNodeOrAddAttribute(value, schema); } @Override - public Node createStringExample(String value) { - return document.createTextNode(value); + public Optional createEnumExample(String anEnumValue, Schema schema) { + return createStringExample(anEnumValue, schema); } @Override - public Node createEnumExample(String anEnumValue) { - return createStringExample(anEnumValue); + public Optional createUnknownSchemaStringTypeExample(String schemaType) { + return Optional.of(document.createTextNode("unknown schema type: " + schemaType)); } @Override - public Node createUnknownSchemaStringTypeExample(String schemaType) { - return document.createTextNode("unknown schema type: " + schemaType); - } - - @Override - public Node createUnknownSchemaStringFormatExample(String schemaFormat) { - return document.createTextNode("unknown string schema format: " + schemaFormat); + public Optional createUnknownSchemaStringFormatExample(String schemaFormat) { + return Optional.of(document.createTextNode("unknown string schema format: " + schemaFormat)); } @Override @@ -241,8 +203,8 @@ public Node getExampleOrNull(Schema schema, Object example) { } @Override - public Node createEmptyObjectExample() { - return document.createTextNode(""); + public Optional createEmptyObjectExample() { + return Optional.of(document.createTextNode("")); } private String getCacheKey(Schema schema) { @@ -263,4 +225,18 @@ private Node readXmlString(String xmlString) { } return null; } + + private Optional createNodeOrAddAttribute(String value, Schema schema) { + if (!nodeStack.isEmpty() && isAttribute(schema)) { + Element currentParent = nodeStack.peek(); + currentParent.setAttribute(lookupSchemaName(schema), value); + return Optional.empty(); + } else { + return Optional.of(document.createTextNode(value)); + } + } + + private boolean isAttribute(Schema schema) { + return schema.getXml() != null && schema.getXml().getAttribute(); + } } diff --git a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/yaml/ExampleYamlValueGenerator.java b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/yaml/ExampleYamlValueGenerator.java index 70e4b7127..d74dfb815 100644 --- a/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/yaml/ExampleYamlValueGenerator.java +++ b/springwolf-core/src/main/java/io/github/springwolf/core/asyncapi/components/examples/walkers/yaml/ExampleYamlValueGenerator.java @@ -12,6 +12,7 @@ import lombok.extern.slf4j.Slf4j; import java.util.List; +import java.util.Optional; import java.util.Set; @Slf4j @@ -29,9 +30,6 @@ public boolean canHandle(String contentType) { return SUPPORTED_CONTENT_TYPES.contains(contentType); } - @Override - public void initialize() {} - @Override public String lookupSchemaName(Schema schema) { return exampleJsonValueGenerator.lookupSchemaName(schema); @@ -53,102 +51,52 @@ public String prepareForSerialization(Schema schema, JsonNode exampleObject) { } @Override - public JsonNode createIntegerExample(Integer value) { - return this.exampleJsonValueGenerator.createIntegerExample(value); - } - - @Override - public JsonNode createDoubleExample(Double value) { - return this.exampleJsonValueGenerator.createDoubleExample(value); + public Optional createIntegerExample(Integer value, Schema schema) { + return this.exampleJsonValueGenerator.createIntegerExample(value, schema); } @Override - public JsonNode createBooleanExample() { - return this.exampleJsonValueGenerator.createBooleanExample(); + public Optional createDoubleExample(Double value, Schema schema) { + return this.exampleJsonValueGenerator.createDoubleExample(value, schema); } @Override - public JsonNode createBooleanExample(Boolean value) { - return this.exampleJsonValueGenerator.createBooleanExample(value); + public Optional createBooleanExample(Boolean value, Schema schema) { + return this.exampleJsonValueGenerator.createBooleanExample(value, schema); } @Override - public JsonNode createIntegerExample() { - return this.exampleJsonValueGenerator.createIntegerExample(); + public JsonNode startObject(String name) { + return this.exampleJsonValueGenerator.startObject(name); } @Override - public JsonNode createObjectExample(String name, List> properties) { - return this.exampleJsonValueGenerator.createObjectExample(name, properties); + public void addPropertyExamples(JsonNode object, List> properties) { + this.exampleJsonValueGenerator.addPropertyExamples(object, properties); } @Override - public JsonNode createEmptyObjectExample() { + public Optional createEmptyObjectExample() { return this.exampleJsonValueGenerator.createEmptyObjectExample(); } @Override - public JsonNode createDoubleExample() { - return this.exampleJsonValueGenerator.createDoubleExample(); - } - - @Override - public JsonNode createDateExample() { - return this.exampleJsonValueGenerator.createDateExample(); - } - - @Override - public JsonNode createDateTimeExample() { - return this.exampleJsonValueGenerator.createDateTimeExample(); - } - - @Override - public JsonNode createEmailExample() { - return this.exampleJsonValueGenerator.createEmailExample(); - } - - @Override - public JsonNode createPasswordExample() { - return this.exampleJsonValueGenerator.createPasswordExample(); - } - - @Override - public JsonNode createByteExample() { - return this.exampleJsonValueGenerator.createByteExample(); - } - - @Override - public JsonNode createBinaryExample() { - return this.exampleJsonValueGenerator.createBinaryExample(); - } - - @Override - public JsonNode createUuidExample() { - return this.exampleJsonValueGenerator.createUuidExample(); - } - - @Override - public JsonNode createStringExample() { - return this.exampleJsonValueGenerator.createStringExample(); - } - - @Override - public JsonNode createStringExample(String value) { - return this.exampleJsonValueGenerator.createStringExample(value); + public Optional createStringExample(String value, Schema schema) { + return this.exampleJsonValueGenerator.createStringExample(value, schema); } @Override - public JsonNode createEnumExample(String anEnumValue) { - return this.exampleJsonValueGenerator.createEnumExample(anEnumValue); + public Optional createEnumExample(String anEnumValue, Schema schema) { + return this.exampleJsonValueGenerator.createEnumExample(anEnumValue, schema); } @Override - public JsonNode createUnknownSchemaStringTypeExample(String schemaType) { + public Optional createUnknownSchemaStringTypeExample(String schemaType) { return this.exampleJsonValueGenerator.createUnknownSchemaStringTypeExample(schemaType); } @Override - public JsonNode createUnknownSchemaStringFormatExample(String schemaFormat) { + public Optional createUnknownSchemaStringFormatExample(String schemaFormat) { return this.exampleJsonValueGenerator.createUnknownSchemaStringFormatExample(schemaFormat); } diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultXmlComponentsServiceTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultXmlComponentsServiceTest.java index 76db41ed3..74e38fec9 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultXmlComponentsServiceTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/DefaultXmlComponentsServiceTest.java @@ -16,6 +16,7 @@ import io.swagger.v3.core.util.Json; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.annotation.Nullable; +import jakarta.xml.bind.annotation.XmlAttribute; import jakarta.xml.bind.annotation.XmlRootElement; import lombok.Data; import lombok.NoArgsConstructor; @@ -43,8 +44,10 @@ class DefaultXmlComponentsServiceTest { new DefaultSchemaWalker<>(new ExampleXmlValueGenerator(new DefaultExampleXmlValueSerializer())))))), new SwaggerSchemaUtil(), new SpringwolfConfigProperties()); + private static final ObjectMapper objectMapper = Json.mapper().enable(SerializationFeature.ORDER_MAP_ENTRIES_BY_KEYS); + private static final PrettyPrinter printer = new DefaultPrettyPrinter().withObjectIndenter(new DefaultIndenter(" ", DefaultIndenter.SYS_LF)); @@ -93,6 +96,17 @@ void getComplexDefinitions() throws IOException { assertEquals(expected, actualDefinitions); } + @Test + void getComplexDefinitionsWithAttributes() throws IOException { + componentsService.registerSchema(ComplexAttributesFoo.class, "text/xml"); + + String actualDefinitions = objectMapper.writer(printer).writeValueAsString(componentsService.getSchemas()); + String expected = jsonResource("/schemas/xml/complex-definitions-with-attributes-xml.json"); + + System.out.println("Got: " + actualDefinitions); + assertEquals(expected, actualDefinitions); + } + @Test void getListWrapperDefinitions() throws IOException { componentsService.registerSchema(ListWrapper.class, "text/xml"); @@ -272,6 +286,53 @@ public class ImplementationTwo { } } + @Data + @NoArgsConstructor + @XmlRootElement(name = "ComplexAttributesFoo") + private static class ComplexAttributesFoo { + @XmlAttribute(name = "s") + private String s; + + @XmlAttribute(name = "b") + private Boolean b; + + @XmlAttribute(name = "i") + private Integer i; + + @XmlAttribute(name = "f") + private Float f; + + @XmlAttribute(name = "d") + private Double d; + + @XmlAttribute(name = "dt") + private OffsetDateTime dt; + + private Nested n; + + @Data + @NoArgsConstructor + @XmlRootElement(name = "NestedWithAttribute") + private static class Nested { + @XmlAttribute(name = "ns") + private String ns; + + private List nli; + private Set nsm; + private Map nmfm; + + @Data + @NoArgsConstructor + @XmlRootElement(name = "MyClassWithAttribute") + private static class MyClassWithAttribute { + private String s_elem; + + @XmlAttribute(name = "s_attribute") + private String s_attribute; + } + } + } + @Nested class AsyncApiPayloadTest { @Test diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalkerXmlIntegrationTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalkerXmlIntegrationTest.java index 1170421d2..19fe83af1 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalkerXmlIntegrationTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/DefaultSchemaWalkerXmlIntegrationTest.java @@ -17,6 +17,7 @@ import io.swagger.v3.oas.models.media.Schema; import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.media.UUIDSchema; +import io.swagger.v3.oas.models.media.XML; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import org.w3c.dom.Node; @@ -32,7 +33,8 @@ class DefaultSchemaWalkerXmlIntegrationTest { private final ExampleXmlValueGenerator exampleXmlValueGenerator = new ExampleXmlValueGenerator(new DefaultExampleXmlValueSerializer()); - private final DefaultSchemaWalker xmlSchemaWalker = new DefaultSchemaWalker(exampleXmlValueGenerator); + private final DefaultSchemaWalker xmlSchemaWalker = + new DefaultSchemaWalker<>(exampleXmlValueGenerator); @Nested class CanHandle { @@ -82,6 +84,7 @@ void build() { @Test void failWhenMissingDefinition() { ObjectSchema compositeSchema = new ObjectSchema(); + compositeSchema.setName("composite-schema"); Schema referenceSchema = new Schema(); referenceSchema.set$ref("#/components/schemas/Nested"); @@ -524,4 +527,31 @@ public String toString() { } } } + + @Nested + class TestXmlAttributes { + @Test + void composite_object_without_ref() { + ObjectSchema nestedSchema = new ObjectSchema(); + nestedSchema.setName("Nested"); + nestedSchema.addProperty("s_1", new StringSchema().name("s_1").xml(new XML().attribute(true))); + nestedSchema.addProperty("b", new BooleanSchema().name("b")); + + ObjectSchema compositeSchema = new ObjectSchema(); + compositeSchema.setName("composite_object_with_references_root"); + compositeSchema.addProperty("s_2", new StringSchema().name("s_2").xml(new XML().attribute(true))); + compositeSchema.addProperty("f", nestedSchema); + + String actual = xmlSchemaWalker + .fromSchema(compositeSchema, emptyMap()) + .trim() + .stripIndent(); + + assertThat(actual) + .isEqualTo( + "true" + .trim() + .stripIndent()); + } + } } diff --git a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGeneratorTest.java b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGeneratorTest.java index abe33f622..5bb3256ea 100644 --- a/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGeneratorTest.java +++ b/springwolf-core/src/test/java/io/github/springwolf/core/asyncapi/components/examples/walkers/xml/ExampleXmlValueGeneratorTest.java @@ -24,7 +24,10 @@ void cacheShouldResolveBySchemaName() { schema2.setXml(new XML().name("schema1")); generator.initialize(); - Node example1 = generator.createStringExample(); + + Node example1 = generator + .createStringExample("does-not-matter-for-test-1", schema1) + .get(); generator.prepareForSerialization(schema1, example1); Node cachedExample1 = generator.getExampleOrNull(schema1, "does-not-matter-for-test-1"); @@ -51,7 +54,8 @@ void cacheShouldStoreExampleBySchemaName() { schema2.setName("schema1"); generator.initialize(); - Node example1 = generator.createStringExample("example1"); + + Node example1 = generator.createStringExample("example1", schema1).get(); generator.prepareForSerialization(schema1, example1); generator.initialize(); diff --git a/springwolf-core/src/test/resources/schemas/xml/complex-definitions-with-attributes-xml.json b/springwolf-core/src/test/resources/schemas/xml/complex-definitions-with-attributes-xml.json new file mode 100644 index 000000000..148888e28 --- /dev/null +++ b/springwolf-core/src/test/resources/schemas/xml/complex-definitions-with-attributes-xml.json @@ -0,0 +1,74 @@ +{ + "io.github.springwolf.core.asyncapi.components.DefaultXmlComponentsServiceTest$ComplexAttributesFoo" : { + "type" : "string", + "properties" : { + "b" : { + "type" : "boolean" + }, + "d" : { + "type" : "number", + "format" : "double" + }, + "dt" : { + "type" : "string", + "format" : "date-time" + }, + "f" : { + "type" : "number", + "format" : "float" + }, + "i" : { + "type" : "integer", + "format" : "int32" + }, + "n" : { + "$ref" : "#/components/schemas/io.github.springwolf.core.asyncapi.components.DefaultXmlComponentsServiceTest$ComplexAttributesFoo$Nested" + }, + "s" : { + "type" : "string" + } + }, + "examples" : [ "0string" ] + }, + "io.github.springwolf.core.asyncapi.components.DefaultXmlComponentsServiceTest$ComplexAttributesFoo$Nested" : { + "type" : "string", + "properties" : { + "nli" : { + "type" : "array", + "items" : { + "type" : "integer", + "format" : "int32" + } + }, + "nmfm" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/io.github.springwolf.core.asyncapi.components.DefaultXmlComponentsServiceTest$ComplexAttributesFoo$Nested$MyClassWithAttribute" + } + }, + "ns" : { + "type" : "string" + }, + "nsm" : { + "type" : "array", + "items" : { + "$ref" : "#/components/schemas/io.github.springwolf.core.asyncapi.components.DefaultXmlComponentsServiceTest$ComplexAttributesFoo$Nested$MyClassWithAttribute" + }, + "uniqueItems" : true + } + }, + "examples" : [ "0string" ] + }, + "io.github.springwolf.core.asyncapi.components.DefaultXmlComponentsServiceTest$ComplexAttributesFoo$Nested$MyClassWithAttribute" : { + "type" : "string", + "properties" : { + "s_attribute" : { + "type" : "string" + }, + "s_elem" : { + "type" : "string" + } + }, + "examples" : [ "string" ] + } +} diff --git a/springwolf-examples/springwolf-kafka-example/build.gradle b/springwolf-examples/springwolf-kafka-example/build.gradle index 930df5834..619dcd79a 100644 --- a/springwolf-examples/springwolf-kafka-example/build.gradle +++ b/springwolf-examples/springwolf-kafka-example/build.gradle @@ -51,6 +51,8 @@ dependencies { implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + implementation "jakarta.xml.bind:jakarta.xml.bind-api:${jakartaXmlBindApiVersion}" + implementation "io.swagger.core.v3:swagger-annotations:${swaggerVersion}" implementation "io.swagger.core.v3:swagger-core-jakarta:${swaggerVersion}" diff --git a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/dtos/XmlPayloadDto.java b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/dtos/XmlPayloadDto.java index d130b5bb2..9f9b10f4f 100644 --- a/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/dtos/XmlPayloadDto.java +++ b/springwolf-examples/springwolf-kafka-example/src/main/java/io/github/springwolf/examples/kafka/dtos/XmlPayloadDto.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.springwolf.examples.kafka.dtos; +import jakarta.xml.bind.annotation.XmlAttribute; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; @@ -10,6 +11,9 @@ @NoArgsConstructor public class XmlPayloadDto { + @XmlAttribute(name = "someAttribute") + private String someAttribute; + private String someString; private long someLong; diff --git a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json index 787d71ca5..7847279e5 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -991,6 +991,9 @@ "io.github.springwolf.examples.kafka.dtos.XmlPayloadDto": { "type": "string", "properties": { + "someAttribute": { + "type": "string" + }, "someEnum": { "type": "string", "enum": [ @@ -1008,11 +1011,12 @@ } }, "examples": [ - "FOO10string" + "FOO10string" ], "x-json-schema": { "$schema": "https://json-schema.org/draft-04/schema#", "properties": { + "someAttribute": { }, "someEnum": { "enum": [ "FOO1", @@ -1484,4 +1488,4 @@ ] } } -} \ No newline at end of file +}