diff --git a/.github/workflows/springwolf-addons.yml b/.github/workflows/springwolf-addons.yml index 0c33e62a9..c78b26065 100644 --- a/.github/workflows/springwolf-addons.yml +++ b/.github/workflows/springwolf-addons.yml @@ -15,7 +15,7 @@ jobs: strategy: fail-fast: false matrix: - addon: [ "common-model-converters", "generic-binding" ] + addon: [ "common-model-converters", "generic-binding", "json-schema" ] timeout-minutes: 10 env: diff --git a/README.md b/README.md index de5b39816..9f00b1c1b 100644 --- a/README.md +++ b/README.md @@ -57,12 +57,13 @@ More details in the documentation. |-------------------------------------------------------------------------------------------------------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | [Core](https://github.com/springwolf/springwolf-core/tree/master/springwolf-core) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-core?color=green&label=springwolf-core&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-core?label=springwolf-core&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [AMQP](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-amqp-plugin) | [AMQP Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-amqp-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-amqp?color=green&label=springwolf-amqp&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-amqp?label=springwolf-amqp&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | -| [AWS SNS](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-sns-plugin) | [AWS SNS Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-sns-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-sns?color=green&label=springwolf-sqs&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-sns?label=springwolf-sns&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | +| [AWS SNS](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-sns-plugin) | [AWS SNS Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-sns-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-sns?color=green&label=springwolf-sqs&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-sns?label=springwolf-sns&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [AWS SQS](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-sqs-plugin) | [AWS SQS Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-sqs-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-sqs?color=green&label=springwolf-sqs&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-sqs?label=springwolf-sqs&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Cloud Stream](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-cloud-stream-plugin) | [Cloud Stream Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-cloud-stream-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-cloud-stream?color=green&label=springwolf-cloud-stream&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-cloud-stream?label=springwolf-cloud-stream&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Kafka](https://github.com/springwolf/springwolf-core/tree/master/springwolf-plugins/springwolf-kafka-plugin) | [Kafka Example](https://github.com/springwolf/springwolf-core/tree/master/springwolf-examples/springwolf-kafka-example) | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-kafka?color=green&label=springwolf-kafka&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-kafka?label=springwolf-kafka&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Common Model Converter](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-common-model-converters) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-common-model-converters?color=green&label=springwolf-common-model-converters&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-common-model-converters?label=springwolf-common-model-converters&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | | [Generic Binding](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-generic-binding) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-generic-binding?color=green&label=springwolf-generic-binding&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-generic-binding?label=springwolf-generic-binding&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | +| [Json Schema](https://github.com/springwolf/springwolf-core/tree/master/springwolf-add-ons/springwolf-json-schema) | | ![Maven Central](https://img.shields.io/maven-central/v/io.github.springwolf/springwolf-json-schema?color=green&label=springwolf-json-schema&style=plastic) | ![Sonatype Nexus (Snapshots)](https://img.shields.io/nexus/s/io.github.springwolf/springwolf-json-schema?label=springwolf-json-schema&server=https%3A%2F%2Fs01.oss.sonatype.org&style=plastic) | ### Development diff --git a/build.gradle b/build.gradle index 1a8ff5e57..7cab48b24 100644 --- a/build.gradle +++ b/build.gradle @@ -68,7 +68,9 @@ allprojects { useJUnitPlatform() testLogging { - events "passed", "skipped", "failed" + // showStandardStreams = true + + events "skipped", "failed" exceptionFormat = 'full' } } @@ -85,7 +87,9 @@ allprojects { excludePatterns = ['*IntegrationTest'] } testLogging { - events "passed", "skipped", "failed" + // showStandardStreams = true + + events "skipped", "failed" exceptionFormat = 'full' } } diff --git a/dependencies.gradle b/dependencies.gradle index d7fcec48e..f291dc966 100644 --- a/dependencies.gradle +++ b/dependencies.gradle @@ -43,6 +43,8 @@ ext { jacksonVersion = '2.15.3' jakartaAnnotationApiVersion = '2.1.1' + jsonSchemaValidator = '1.0.87' + mockitoCoreVersion = '5.7.0' mockitoJunitJupiterVersion = '5.7.0' diff --git a/settings.gradle b/settings.gradle index 88d272072..e1104c57e 100644 --- a/settings.gradle +++ b/settings.gradle @@ -14,7 +14,8 @@ include( 'springwolf-examples:springwolf-sqs-example', 'springwolf-ui', 'springwolf-add-ons:springwolf-common-model-converters', - 'springwolf-add-ons:springwolf-generic-binding' + 'springwolf-add-ons:springwolf-generic-binding', + 'springwolf-add-ons:springwolf-json-schema' ) project(':springwolf-plugins:springwolf-amqp-plugin').name = 'springwolf-amqp' diff --git a/springwolf-add-ons/springwolf-generic-binding/build.gradle b/springwolf-add-ons/springwolf-generic-binding/build.gradle index b403008f4..966e5b829 100644 --- a/springwolf-add-ons/springwolf-generic-binding/build.gradle +++ b/springwolf-add-ons/springwolf-generic-binding/build.gradle @@ -33,12 +33,6 @@ java { withSourcesJar() } -test { - dependsOn spotlessApply // Automatically fix code formatting if possible - - useJUnitPlatform() -} - publishing { publications { mavenJava(MavenPublication) { diff --git a/springwolf-add-ons/springwolf-json-schema/README.md b/springwolf-add-ons/springwolf-json-schema/README.md new file mode 100644 index 000000000..4b9649830 --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/README.md @@ -0,0 +1,57 @@ +# Springwolf Json Schema Add-on + +### Table Of Contents + +- [About](#about) +- [Usage](#usage) + - [Dependencies](#dependencies) + - [Result](#result) + +### About + +This module generates the [json-schema](https://json-schema.org) for all Springwolf detected schemas (payloads, headers, etc.). + +No configuration needed, only add the dependency. + +As Springwolf uses `swagger-parser` to create an `OpenApi` schema, this module maps the `OpenApi` schema to `json-schema`. + +### Usage + +Add the following dependency: + +#### Dependencies + +```groovy +dependencies { + runtimeOnly 'io.github.springwolf:springwolf-json-schema:' +} +``` + +#### Result + +The `x-json-schema` field is added for each `Schema`. + +Example: + +```json +{ + "MonetaryAmount-Header": { + "...": "", + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "MonetaryAmount-Header", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "javax.money.MonetaryAmount" + ], + "type": "string" + } + }, + "type": "object" + } + } +} + +``` diff --git a/springwolf-add-ons/springwolf-json-schema/build.gradle b/springwolf-add-ons/springwolf-json-schema/build.gradle new file mode 100644 index 000000000..42a61851e --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/build.gradle @@ -0,0 +1,55 @@ +plugins { + id 'java-library' + + id 'org.springframework.boot' + id 'io.spring.dependency-management' + id 'ca.cutterslade.analyze' +} + +dependencies { + api project(":springwolf-core") + + implementation "com.fasterxml.jackson.core:jackson-core:${jacksonVersion}" + implementation "com.fasterxml.jackson.core:jackson-databind:${jacksonVersion}" + + testImplementation "io.swagger.core.v3:swagger-core-jakarta:${swaggerVersion}" + implementation "io.swagger.core.v3:swagger-models-jakarta:${swaggerVersion}" + + implementation "org.apache.commons:commons-lang3:${commonsLang3Version}" + + implementation "org.slf4j:slf4j-api:${slf4jApiVersion}" + + implementation "org.springframework:spring-context" + + annotationProcessor "org.projectlombok:lombok:${lombokVersion}" + + testImplementation "org.mockito:mockito-core:${mockitoCoreVersion}" + testImplementation "org.assertj:assertj-core:${assertjCoreVersion}" + testImplementation "org.junit.jupiter:junit-jupiter-api:${junitJupiterVersion}" + testImplementation "org.junit.jupiter:junit-jupiter-params:${junitJupiterVersion}" + testRuntimeOnly "org.junit.jupiter:junit-jupiter:${junitJupiterVersion}" + + testImplementation "com.networknt:json-schema-validator:${jsonSchemaValidator}" +} + +jar { + enabled = true + archiveClassifier = '' +} +bootJar.enabled = false + +java { + withJavadocJar() + withSourcesJar() +} + +publishing { + publications { + mavenJava(MavenPublication) { + pom { + name = 'springwolf-json-schema' + description = 'Extends Springwolf schemas with json-schema' + } + } + } +} diff --git a/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaCustomizer.java b/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaCustomizer.java new file mode 100644 index 000000000..da8c61627 --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaCustomizer.java @@ -0,0 +1,39 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.addons.json_schema; + +import io.github.stavshamir.springwolf.asyncapi.AsyncApiCustomizer; +import io.github.stavshamir.springwolf.asyncapi.types.AsyncAPI; +import io.swagger.v3.oas.models.media.Schema; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +import java.util.HashMap; +import java.util.Map; + +@RequiredArgsConstructor +@Slf4j +public class JsonSchemaCustomizer implements AsyncApiCustomizer { + private static final String EXTENSION_JSON_SCHEMA = "x-json-schema"; + + private final JsonSchemaGenerator jsonSchemaGenerator; + + @Override + public void customize(AsyncAPI asyncAPI) { + Map schemas = asyncAPI.getComponents().getSchemas(); + for (Map.Entry entry : schemas.entrySet()) { + Schema schema = entry.getValue(); + if (schema.getExtensions() == null) { + schema.setExtensions(new HashMap<>()); + } + + try { + log.debug("Generate json-schema for %s".formatted(entry.getKey())); + + Object jsonSchema = jsonSchemaGenerator.fromSchema(schema, schemas); + schema.getExtensions().putIfAbsent(EXTENSION_JSON_SCHEMA, jsonSchema); + } catch (Exception ex) { + log.warn("Unable to create json-schema for %s".formatted(schema.getName()), ex); + } + } + } +} diff --git a/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaGenerator.java b/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaGenerator.java new file mode 100644 index 000000000..575323005 --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaGenerator.java @@ -0,0 +1,160 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.addons.json_schema; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.databind.node.ObjectNode; +import io.swagger.v3.oas.models.media.Schema; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; + +import java.util.HashSet; +import java.util.Map; +import java.util.Set; + +@RequiredArgsConstructor +public class JsonSchemaGenerator { + private final ObjectMapper objectMapper; + + public Object fromSchema(Schema schema, Map definitions) throws JsonProcessingException { + ObjectNode node = fromSchemaInternal(schema, definitions, new HashSet<>()); + node.put("$schema", "https://json-schema.org/draft-04/schema#"); + + return objectMapper.readValue(node.toString(), Object.class); + } + + private ObjectNode fromSchemaInternal(Schema schema, Map definitions, Set visited) { + if (schema != null && !visited.contains(schema)) { + visited.add(schema); + + return mapToJsonSchema(schema, definitions, visited); + } + return objectMapper.createObjectNode(); + } + + private ObjectNode mapToJsonSchema(Schema schema, Map definitions, Set visited) { + ObjectNode node = objectMapper.createObjectNode(); + + if (schema.getAnyOf() != null) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (Schema ofSchema : schema.getAnyOf()) { + arrayNode.add(fromSchemaInternal(ofSchema, definitions, visited)); + } + node.put("anyOf", arrayNode); + } + if (schema.getAllOf() != null) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (Schema ofSchema : schema.getAllOf()) { + arrayNode.add(fromSchemaInternal(ofSchema, definitions, visited)); + } + node.put("allOf", arrayNode); + } + if (schema.getConst() != null) { + node.put("const", schema.getConst().toString()); + } + if (schema.getDescription() != null) { + node.put("description", schema.getDescription()); + } + if (schema.getEnum() != null) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (Object property : schema.getEnum()) { + arrayNode.add(property.toString()); + } + if (schema.getNullable() != null && schema.getNullable()) { + arrayNode.add("null"); + } + node.set("enum", arrayNode); + } + if (schema.getFormat() != null) { + node.put("format", schema.getFormat()); + } + if (schema.getItems() != null) { + node.set("items", fromSchemaInternal(schema.getItems(), definitions, visited)); + } + if (schema.getMaximum() != null) { + node.put("maximum", schema.getMaximum()); + } + if (schema.getMinimum() != null) { + node.put("minimum", schema.getMinimum()); + } + if (schema.getMaxItems() != null) { + node.put("maxItems", schema.getMaxItems()); + } + if (schema.getMinItems() != null) { + node.put("minItems", schema.getMinItems()); + } + if (schema.getMaxLength() != null) { + node.put("maxLength", schema.getMaxLength()); + } + if (schema.getMinLength() != null) { + node.put("minLength", schema.getMinLength()); + } + if (schema.getMultipleOf() != null) { + node.put("multipleOf", schema.getMultipleOf()); + } + if (schema.getName() != null) { + node.put("name", schema.getName()); + } + if (schema.getNot() != null) { + node.put("not", fromSchemaInternal(schema.getNot(), definitions, visited)); + } + if (schema.getOneOf() != null) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (Schema ofSchema : schema.getOneOf()) { + arrayNode.add(fromSchemaInternal(ofSchema, definitions, visited)); + } + node.put("oneOf", arrayNode); + } + if (schema.getPattern() != null) { + node.put("pattern", schema.getPattern()); + } + if (schema.getProperties() != null && !schema.getProperties().isEmpty()) { + node.set("properties", buildProperties(schema, definitions, visited)); + } + if (schema.getRequired() != null) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + for (String property : schema.getRequired()) { + arrayNode.add(property); + } + node.set("required", arrayNode); + } + if (schema.getTitle() != null) { + node.put("title", schema.getTitle()); + } + if (schema.getType() != null) { + if (schema.getNullable() != null && schema.getNullable()) { + ArrayNode arrayNode = objectMapper.createArrayNode(); + arrayNode.add(schema.getType()); + arrayNode.add("null"); + node.set("type", arrayNode); + } else { + node.put("type", schema.getType()); + } + } + if (schema.getUniqueItems() != null) { + node.put("uniqueItems", schema.getUniqueItems()); + } + + return node; + } + + private JsonNode buildProperties(Schema schema, Map definitions, Set visited) { + ObjectNode node = objectMapper.createObjectNode(); + + for (Map.Entry propertySchemaSet : + schema.getProperties().entrySet()) { + Schema propertySchema = propertySchemaSet.getValue(); + + if (propertySchema != null && propertySchema.get$ref() != null) { + String schemaName = StringUtils.substringAfterLast(propertySchema.get$ref(), "/"); + propertySchema = definitions.get(schemaName); + } + + node.set(propertySchemaSet.getKey(), fromSchemaInternal(propertySchema, definitions, visited)); + } + + return node; + } +} diff --git a/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/configuration/SpringwolfJsonSchemaAutoConfiguration.java b/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/configuration/SpringwolfJsonSchemaAutoConfiguration.java new file mode 100644 index 000000000..43bd05a87 --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/src/main/java/io/github/stavshamir/springwolf/addons/json_schema/configuration/SpringwolfJsonSchemaAutoConfiguration.java @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.addons.json_schema.configuration; + +import com.fasterxml.jackson.databind.ObjectMapper; +import io.github.stavshamir.springwolf.addons.json_schema.JsonSchemaCustomizer; +import io.github.stavshamir.springwolf.addons.json_schema.JsonSchemaGenerator; +import io.github.stavshamir.springwolf.asyncapi.AsyncApiCustomizer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SpringwolfJsonSchemaAutoConfiguration { + + @Bean + public JsonSchemaGenerator jsonSchemaGenerator(ObjectMapper objectMapper) { + return new JsonSchemaGenerator(objectMapper); + } + + @Bean + public AsyncApiCustomizer jsonSchemaCustomizer(JsonSchemaGenerator jsonSchemaGenerator) { + return new JsonSchemaCustomizer(jsonSchemaGenerator); + } +} diff --git a/springwolf-add-ons/springwolf-json-schema/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/springwolf-add-ons/springwolf-json-schema/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 000000000..43fd6dc14 --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +io.github.stavshamir.springwolf.addons.json_schema.configuration.SpringwolfJsonSchemaAutoConfiguration diff --git a/springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaCustomizerTest.java b/springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaCustomizerTest.java new file mode 100644 index 000000000..cca130a4a --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaCustomizerTest.java @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.addons.json_schema; + +import com.fasterxml.jackson.core.JsonProcessingException; +import io.github.stavshamir.springwolf.asyncapi.types.AsyncAPI; +import io.github.stavshamir.springwolf.asyncapi.types.Components; +import io.swagger.v3.oas.models.media.ObjectSchema; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import java.util.Map; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +class JsonSchemaCustomizerTest { + + private JsonSchemaGenerator jsonSchemaGenerator; + private JsonSchemaCustomizer jsonSchemaCustomizer; + + @BeforeEach + void setUp() { + jsonSchemaGenerator = mock(JsonSchemaGenerator.class); + jsonSchemaCustomizer = new JsonSchemaCustomizer(jsonSchemaGenerator); + } + + @Test + public void handleEmptySchemaTest() { + // given + AsyncAPI asyncAPI = createAsyncApi(); + + // when + jsonSchemaCustomizer.customize(asyncAPI); + + // then + assertThat(asyncAPI).isEqualTo(createAsyncApi()); + } + + @Test + public void shouldAddJsonSchemaExtensionTest() throws JsonProcessingException { + // given + AsyncAPI asyncAPI = createAsyncApi(); + asyncAPI.getComponents().setSchemas(Map.of("schema", new ObjectSchema())); + + when(jsonSchemaGenerator.fromSchema(any(), any())).thenReturn("mock-string"); + + // when + jsonSchemaCustomizer.customize(asyncAPI); + + // then + assertThat(asyncAPI.getComponents().getSchemas().get("schema").getExtensions()) + .isEqualTo(Map.of("x-json-schema", "mock-string")); + } + + private static AsyncAPI createAsyncApi() { + AsyncAPI asyncAPI = new AsyncAPI(); + asyncAPI.setComponents(Components.builder().schemas(Map.of()).build()); + return asyncAPI; + } +} diff --git a/springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaGeneratorTest.java b/springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaGeneratorTest.java new file mode 100644 index 000000000..4b35f00e2 --- /dev/null +++ b/springwolf-add-ons/springwolf-json-schema/src/test/java/io/github/stavshamir/springwolf/addons/json_schema/JsonSchemaGeneratorTest.java @@ -0,0 +1,276 @@ +// SPDX-License-Identifier: Apache-2.0 +package io.github.stavshamir.springwolf.addons.json_schema; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersionDetector; +import io.swagger.v3.core.util.Json; +import io.swagger.v3.oas.models.media.ArraySchema; +import io.swagger.v3.oas.models.media.BooleanSchema; +import io.swagger.v3.oas.models.media.EmailSchema; +import io.swagger.v3.oas.models.media.MapSchema; +import io.swagger.v3.oas.models.media.NumberSchema; +import io.swagger.v3.oas.models.media.ObjectSchema; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.io.IOException; +import java.math.BigDecimal; +import java.util.List; +import java.util.Map; +import java.util.TreeMap; +import java.util.function.Supplier; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class JsonSchemaGeneratorTest { + private final ObjectMapper mapper = Json.mapper(); + private final JsonSchemaGenerator jsonSchemaGenerator = new JsonSchemaGenerator(mapper); + + @ParameterizedTest + @MethodSource + public void validateJsonSchemaTest(String expectedJsonSchema, Supplier> asyncApiSchema) + throws IOException { + // when + verifyValidJsonSchema(expectedJsonSchema); + + // ref cycle ping -> pingField -> pong -> pongField -> ping (repeat) + ObjectSchema pingSchema = new ObjectSchema(); + pingSchema.setName("PingSchema"); + ObjectSchema pingFieldSchema = new ObjectSchema(); + pingFieldSchema.setName("PingFieldSchema"); + pingFieldSchema.set$ref("PongSchema"); + pingSchema.setProperties(Map.of("pingfield", pingFieldSchema)); + ObjectSchema pongSchema = new ObjectSchema(); + pongSchema.setName("PongSchema"); + ObjectSchema pongFieldSchema = new ObjectSchema(); + pongFieldSchema.setName("PongFieldSchema"); + pongFieldSchema.set$ref("PingSchema"); + pongSchema.setProperties(Map.of("pongField", pongFieldSchema)); + + Map definitions = Map.of( + "StringRef", + new StringSchema(), + "PingSchema", + pingSchema, + "PingFieldSchema", + pingFieldSchema, + "PongSchema", + pongSchema, + "PongFieldSchema", + pongFieldSchema); + + // when + Object jsonSchema = jsonSchemaGenerator.fromSchema(asyncApiSchema.get(), definitions); + + // then + String jsonSchemaString = mapper.writeValueAsString(jsonSchema); + verifyValidJsonSchema(jsonSchemaString); + + assertThat(jsonSchemaString).isEqualToIgnoringWhitespace(expectedJsonSchema); + } + + public static Stream validateJsonSchemaTest() { + return Stream.of( + // types + Arguments.of( + "{\"type\":\"number\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) NumberSchema::new), + Arguments.of( + "{\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) StringSchema::new), + Arguments.of( + "{\"type\":\"array\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) ArraySchema::new), + Arguments.of( + "{\"type\":\"boolean\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) BooleanSchema::new), + Arguments.of( + "{\"format\":\"email\",\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) EmailSchema::new), + Arguments.of( + "{\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) MapSchema::new), + Arguments.of( + "{\"properties\": { \"id\": {\"type\": \"number\"}},\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setProperties(Map.of("id", new NumberSchema())); + return schema; + }), + // fields + Arguments.of( + "{\"anyOf\": [{\"type\": \"number\"}],\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setAnyOf(List.of(new NumberSchema())); + return schema; + }), + Arguments.of( + "{\"allOf\": [{\"type\": \"number\"}],\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setAllOf(List.of(new NumberSchema())); + return schema; + }), + Arguments.of( + "{\"const\": \"test\",\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setConst("test"); + return schema; + }), + Arguments.of( + "{\"description\": \"test\",\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setDescription("test"); + return schema; + }), + Arguments.of( + "{\"enum\": [\"test\", \"value2\"],\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setEnum(List.of("test", "value2")); + return schema; + }), + Arguments.of( + "{\"enum\": [\"test\", \"value2\", \"null\"],\"type\":[\"string\",\"null\"],\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setEnum(List.of("test", "value2")); + schema.setNullable(true); + return schema; + }), + Arguments.of( + "{\"format\": \"test\",\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setFormat("test"); + return schema; + }), + Arguments.of( + "{\"items\": {\"type\": \"number\"},\"type\":\"array\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ArraySchema schema = new ArraySchema(); + schema.setItems(new NumberSchema()); + return schema; + }), + Arguments.of( + "{\"maximum\": 10,\"minimum\": 1,\"type\":\"number\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + NumberSchema schema = new NumberSchema(); + schema.setMaximum(new BigDecimal(10)); + schema.setMinimum(new BigDecimal(1)); + return schema; + }), + Arguments.of( + "{\"maxItems\": 10,\"minItems\": 1,\"type\":\"array\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ArraySchema schema = new ArraySchema(); + schema.setMaxItems(10); + schema.setMinItems(1); + return schema; + }), + Arguments.of( + "{\"maxLength\": 10,\"minLength\": 1,\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setMaxLength(10); + schema.setMinLength(1); + return schema; + }), + Arguments.of( + "{\"multipleOf\": 10,\"type\":\"number\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + NumberSchema schema = new NumberSchema(); + schema.setMultipleOf(new BigDecimal(10)); + return schema; + }), + Arguments.of( + "{\"name\": \"test\",\"type\":\"string\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + StringSchema schema = new StringSchema(); + schema.setName("test"); + return schema; + }), + Arguments.of( + "{\"not\": {\"type\": \"number\"},\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setNot(new NumberSchema()); + return schema; + }), + Arguments.of( + "{\"oneOf\": [{\"type\": \"number\"}],\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setOneOf(List.of(new NumberSchema())); + return schema; + }), + Arguments.of( + "{\"pattern\": \"test\",\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setPattern("test"); + return schema; + }), + Arguments.of( + "{\"properties\": {\"field1\": {\"type\": \"number\"}, \"field2\": {\"type\": \"string\"}}, \"required\":[\"field1\"],\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setProperties( + new TreeMap(Map.of("field1", new NumberSchema(), "field2", new StringSchema()))); + schema.setRequired(List.of("field1")); + return schema; + }), + Arguments.of( + "{\"title\": \"test\",\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema schema = new ObjectSchema(); + schema.setTitle("test"); + return schema; + }), + Arguments.of( + "{\"type\":\"array\",\"uniqueItems\": true,\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ArraySchema schema = new ArraySchema(); + schema.setUniqueItems(true); + return schema; + }), + // ref + Arguments.of( + "{\"properties\":{\"field\": {\"type\":\"string\"}}, \"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema refField = new ObjectSchema(); + refField.set$ref("StringRef"); + + ObjectSchema schema = new ObjectSchema(); + schema.setProperties(Map.of("field", refField)); + return schema; + }), + Arguments.of( + "{\"properties\":{\"field\":{\"name\":\"PingSchema\",\"properties\":{\"pingfield\":{\"name\":\"PongSchema\",\"properties\":{\"pongField\":{}},\"type\":\"object\"}},\"type\":\"object\"}},\"type\":\"object\",\"$schema\":\"https://json-schema.org/draft-04/schema#\"}", + (Supplier) () -> { + ObjectSchema refField = new ObjectSchema(); + refField.set$ref("PingSchema"); + + ObjectSchema schema = new ObjectSchema(); + schema.setProperties(Map.of("field", refField)); + return schema; + }) + // + ); + } + + private void verifyValidJsonSchema(String content) throws JsonProcessingException { + JsonNode jsonNode = mapper.readTree(content); + JsonSchemaFactory.getInstance(SpecVersionDetector.detect(jsonNode)); + } +} diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java index eb948fee0..5881202dd 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/asyncapi/DefaultAsyncApiService.java @@ -48,8 +48,6 @@ public AsyncAPI getAsyncAPI() { /** * Does the 'heavy work' of building the AsyncAPI documents once. Stores the resulting * AsyncAPI document or alternativly a catched exception/error in the instance variable asyncAPIResult. - * - * @return */ protected synchronized void initAsyncAPI() { if (this.asyncAPIResult != null) { @@ -80,10 +78,15 @@ protected synchronized void initAsyncAPI() { .build(); for (AsyncApiCustomizer customizer : customizers) { + log.debug( + "Starting customizer %s".formatted(customizer.getClass().getName())); customizer.customize(asyncAPI); } this.asyncAPIResult = new AsyncAPIResult(asyncAPI, null); + + log.debug("AsyncAPI document was build"); } catch (Throwable t) { + log.debug("Failed to build AsyncAPI document", t); this.asyncAPIResult = new AsyncAPIResult(null, t); } } diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java index 38e44ee2a..78803866f 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java @@ -59,7 +59,7 @@ public Object fromSchema(Schema schema, Map definitions) { String exampleString = buildSchema(schema, definitions); return objectMapper.readValue(exampleString, Object.class); } catch (JsonProcessingException | ExampleGeneratingException ex) { - log.error("Failed to build json example for schema {}", schema.getName()); + log.warn("Failed to build json example for schema {}", schema.getName()); } return null; } diff --git a/springwolf-examples/springwolf-kafka-example/build.gradle b/springwolf-examples/springwolf-kafka-example/build.gradle index b148bdf9d..dbb57c2b1 100644 --- a/springwolf-examples/springwolf-kafka-example/build.gradle +++ b/springwolf-examples/springwolf-kafka-example/build.gradle @@ -14,6 +14,7 @@ dependencies { implementation project(":springwolf-plugins:springwolf-kafka") runtimeOnly project(":springwolf-add-ons:springwolf-common-model-converters") + runtimeOnly project(":springwolf-add-ons:springwolf-json-schema") runtimeOnly project(":springwolf-ui") annotationProcessor project(":springwolf-plugins:springwolf-kafka") 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 871168c0b..d5529f6bb 100644 --- a/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-kafka-example/src/test/resources/asyncapi.json @@ -347,12 +347,73 @@ "ce_time": "2015-07-20T15:49:04-07:00", "ce_type": "io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint", "content-type": "application/json" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "CloudEventHeadersForAnotherPayloadDtoEndpoint", + "properties": { + "ce_id": { + "description": "CloudEvent Id Header", + "enum": [ + "1234-1234-1234" + ], + "type": "string" + }, + "ce_source": { + "description": "CloudEvent Source Header", + "enum": [ + "springwolf-kafka-example/anotherPayloadDtoEndpoint" + ], + "type": "string" + }, + "ce_specversion": { + "description": "CloudEvent Spec Version Header", + "enum": [ + "1.0" + ], + "type": "string" + }, + "ce_subject": { + "description": "CloudEvent Subject Header", + "enum": [ + "Test Subject" + ], + "type": "string" + }, + "ce_time": { + "description": "CloudEvent Time Header", + "enum": [ + "2015-07-20T15:49:04-07:00" + ], + "type": "string" + }, + "ce_type": { + "description": "CloudEvent Payload Type Header", + "enum": [ + "io.github.stavshamir.springwolf.CloudEventHeadersForAnotherPayloadDtoEndpoint" + ], + "type": "string" + }, + "content-type": { + "description": "CloudEvent Content-Type Header", + "enum": [ + "application/json" + ], + "type": "string" + } + }, + "type": "object" } }, "HeadersNotDocumented": { "type": "object", "properties": { }, - "example": { } + "example": { }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "HeadersNotDocumented", + "type": "object" + } }, "SpringDefaultHeaderAndCloudEvent": { "type": "object", @@ -431,6 +492,69 @@ "ce_time": "2023-10-28 20:01:23+00:00", "ce_type": "NestedPayloadDto.v1", "content-type": "application/json" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "SpringDefaultHeaderAndCloudEvent", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "io.github.stavshamir.springwolf.example.kafka.dtos.NestedPayloadDto" + ], + "type": "string" + }, + "ce_id": { + "description": "CloudEvent Id Header", + "enum": [ + "2c60089e-6f39-459d-8ced-2d6df7e4c03a" + ], + "type": "string" + }, + "ce_source": { + "description": "CloudEvent Source Header", + "enum": [ + "http://localhost" + ], + "type": "string" + }, + "ce_specversion": { + "description": "CloudEvent Spec Version Header", + "enum": [ + "1.0" + ], + "type": "string" + }, + "ce_subject": { + "description": "CloudEvent Subject Header", + "enum": [ + "Springwolf example project - Kafka" + ], + "type": "string" + }, + "ce_time": { + "description": "CloudEvent Time Header", + "enum": [ + "2023-10-28 20:01:23+00:00" + ], + "type": "string" + }, + "ce_type": { + "description": "CloudEvent Payload Type Header", + "enum": [ + "NestedPayloadDto.v1" + ], + "type": "string" + }, + "content-type": { + "description": "CloudEvent Content-Type Header", + "enum": [ + "application/json" + ], + "type": "string" + } + }, + "type": "object" } }, "SpringKafkaDefaultHeaders-AnotherPayloadDto": { @@ -447,6 +571,20 @@ }, "example": { "__TypeId__": "io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "SpringKafkaDefaultHeaders-AnotherPayloadDto", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto" + ], + "type": "string" + } + }, + "type": "object" } }, "SpringKafkaDefaultHeaders-ExamplePayloadDto": { @@ -463,6 +601,20 @@ }, "example": { "__TypeId__": "io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "SpringKafkaDefaultHeaders-ExamplePayloadDto", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto" + ], + "type": "string" + } + }, + "type": "object" } }, "SpringKafkaDefaultHeaders-MonetaryAmount": { @@ -479,6 +631,20 @@ }, "example": { "__TypeId__": "javax.money.MonetaryAmount" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "SpringKafkaDefaultHeaders-MonetaryAmount", + "properties": { + "__TypeId__": { + "description": "Spring Type Id Header", + "enum": [ + "javax.money.MonetaryAmount" + ], + "type": "string" + } + }, + "type": "object" } }, "io.github.stavshamir.springwolf.addons.common_model_converters.converters.monetaryamount.MonetaryAmount": { @@ -496,6 +662,21 @@ "example": { "amount": 99.99, "currency": "USD" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "io.github.stavshamir.springwolf.addons.common_model_converters.converters.monetaryamount.MonetaryAmount", + "properties": { + "amount": { + "name": "amount", + "type": "number" + }, + "currency": { + "name": "currency", + "type": "string" + } + }, + "type": "object" } }, "io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto": { @@ -521,6 +702,54 @@ "someString": "some string value" }, "foo": "bar" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "description": "Another payload model", + "name": "io.github.stavshamir.springwolf.example.kafka.dtos.AnotherPayloadDto", + "properties": { + "example": { + "description": "Example payload model", + "name": "io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto", + "properties": { + "someEnum": { + "description": "Some enum field", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ], + "name": "someEnum", + "type": "string" + }, + "someLong": { + "description": "Some long field", + "format": "int64", + "name": "someLong", + "type": "integer" + }, + "someString": { + "description": "Some string field", + "name": "someString", + "type": "string" + } + }, + "required": [ + "someEnum", + "someString" + ], + "type": "object" + }, + "foo": { + "description": "Foo field", + "name": "foo", + "type": "string" + } + }, + "required": [ + "example" + ], + "type": "object" } }, "io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto": { @@ -557,6 +786,39 @@ "someEnum": "FOO2", "someLong": 5, "someString": "some string value" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "description": "Example payload model", + "name": "io.github.stavshamir.springwolf.example.kafka.dtos.ExamplePayloadDto", + "properties": { + "someEnum": { + "description": "Some enum field", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ], + "name": "someEnum", + "type": "string" + }, + "someLong": { + "description": "Some long field", + "format": "int64", + "name": "someLong", + "type": "integer" + }, + "someString": { + "description": "Some string field", + "name": "someString", + "type": "string" + } + }, + "required": [ + "someEnum", + "someString" + ], + "type": "object" } }, "io.github.stavshamir.springwolf.example.kafka.dtos.NestedPayloadDto": { @@ -590,6 +852,28 @@ "someStrings": [ "some string value" ] + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "description": "Payload model with nested complex types", + "name": "io.github.stavshamir.springwolf.example.kafka.dtos.NestedPayloadDto", + "properties": { + "examplePayloads": { + "items": { }, + "name": "examplePayloads", + "type": "array" + }, + "someStrings": { + "items": { + "description": "Some string field", + "type": "string" + }, + "name": "someStrings", + "type": "array", + "uniqueItems": true + } + }, + "type": "object" } } } diff --git a/springwolf-examples/springwolf-sns-example/.env b/springwolf-examples/springwolf-sns-example/.env deleted file mode 100644 index 3e6c13682..000000000 --- a/springwolf-examples/springwolf-sns-example/.env +++ /dev/null @@ -1 +0,0 @@ -SPRINGWOLF_VERSION=0.16.0-SNAPSHOT diff --git a/springwolf-examples/springwolf-sns-example/.env b/springwolf-examples/springwolf-sns-example/.env new file mode 120000 index 000000000..c7360fb82 --- /dev/null +++ b/springwolf-examples/springwolf-sns-example/.env @@ -0,0 +1 @@ +../../.env \ No newline at end of file diff --git a/springwolf-examples/springwolf-sns-example/build.gradle b/springwolf-examples/springwolf-sns-example/build.gradle index 3ad003610..9851c78f1 100644 --- a/springwolf-examples/springwolf-sns-example/build.gradle +++ b/springwolf-examples/springwolf-sns-example/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation project(":springwolf-plugins:springwolf-sns") annotationProcessor project(":springwolf-plugins:springwolf-sns") + runtimeOnly project(":springwolf-add-ons:springwolf-json-schema") runtimeOnly project(":springwolf-ui") runtimeOnly "org.springframework.boot:spring-boot-starter-web" @@ -48,6 +49,9 @@ dependencies { testImplementation "org.testcontainers:testcontainers:${testcontainersVersion}" testImplementation "org.testcontainers:junit-jupiter:${testcontainersVersion}" testImplementation "org.testcontainers:localstack:${testcontainersVersion}" + + testAnnotationProcessor "org.projectlombok:lombok:${lombokVersion}" + testCompileOnly "org.projectlombok:lombok:${lombokVersion}" } docker { diff --git a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationWithDockerIntegrationTest.java b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationWithDockerIntegrationTest.java index dc27a5426..749975f17 100644 --- a/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationWithDockerIntegrationTest.java +++ b/springwolf-examples/springwolf-sns-example/src/test/java/io/github/stavshamir/springwolf/example/sns/ApiIntegrationWithDockerIntegrationTest.java @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 package io.github.stavshamir.springwolf.example.sns; +import lombok.extern.slf4j.Slf4j; import org.json.JSONException; import org.junit.jupiter.api.Test; import org.springframework.web.client.RestTemplate; @@ -24,6 +25,7 @@ * the setup uses a full docker-compose context with a real sns instance. */ @Testcontainers +@Slf4j // @Ignore("Uncomment this line if you have issues running this test on your local machine.") public class ApiIntegrationWithDockerIntegrationTest { @@ -46,7 +48,8 @@ public class ApiIntegrationWithDockerIntegrationTest { @Container public DockerComposeContainer environment = new DockerComposeContainer<>(new File("docker-compose.yml")) .withExposedService(APP_NAME, APP_PORT) - .withEnv(ENV); + .withEnv(ENV) + .withLogConsumer(APP_NAME, l -> log.debug("APP: %s".formatted(l.getUtf8StringWithoutLineEnding()))); private String baseUrl() { String host = environment.getServiceHost(APP_NAME, APP_PORT); diff --git a/springwolf-examples/springwolf-sns-example/src/test/resources/asyncapi.json b/springwolf-examples/springwolf-sns-example/src/test/resources/asyncapi.json index dd8ce4c59..2842a7931 100644 --- a/springwolf-examples/springwolf-sns-example/src/test/resources/asyncapi.json +++ b/springwolf-examples/springwolf-sns-example/src/test/resources/asyncapi.json @@ -97,7 +97,12 @@ "HeadersNotDocumented": { "type": "object", "properties": { }, - "example": { } + "example": { }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "name": "HeadersNotDocumented", + "type": "object" + } }, "io.github.stavshamir.springwolf.example.sns.dtos.AnotherPayloadDto": { "required": [ @@ -122,6 +127,54 @@ "someString": "some string value" }, "foo": "bar" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "description": "Another payload model", + "name": "io.github.stavshamir.springwolf.example.sns.dtos.AnotherPayloadDto", + "properties": { + "example": { + "description": "Example payload model", + "name": "io.github.stavshamir.springwolf.example.sns.dtos.ExamplePayloadDto", + "properties": { + "someEnum": { + "description": "Some enum field", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ], + "name": "someEnum", + "type": "string" + }, + "someLong": { + "description": "Some long field", + "format": "int64", + "name": "someLong", + "type": "integer" + }, + "someString": { + "description": "Some string field", + "name": "someString", + "type": "string" + } + }, + "required": [ + "someEnum", + "someString" + ], + "type": "object" + }, + "foo": { + "description": "Foo field", + "name": "foo", + "type": "string" + } + }, + "required": [ + "example" + ], + "type": "object" } }, "io.github.stavshamir.springwolf.example.sns.dtos.ExamplePayloadDto": { @@ -158,6 +211,39 @@ "someEnum": "FOO2", "someLong": 5, "someString": "some string value" + }, + "x-json-schema": { + "$schema": "https://json-schema.org/draft-04/schema#", + "description": "Example payload model", + "name": "io.github.stavshamir.springwolf.example.sns.dtos.ExamplePayloadDto", + "properties": { + "someEnum": { + "description": "Some enum field", + "enum": [ + "FOO1", + "FOO2", + "FOO3" + ], + "name": "someEnum", + "type": "string" + }, + "someLong": { + "description": "Some long field", + "format": "int64", + "name": "someLong", + "type": "integer" + }, + "someString": { + "description": "Some string field", + "name": "someString", + "type": "string" + } + }, + "required": [ + "someEnum", + "someString" + ], + "type": "object" } } }