Skip to content

Commit

Permalink
Create json-schema Springwolf add-on (#447)
Browse files Browse the repository at this point in the history
* feat(json-schema): create json-schema add-on

* feat(json-schema): add json-schema to kafka and sns example

* feat(json-schema): update README.md

* test(json-schema): add logging docker container logging to ApiIntegrationWithDockerIntegrationTest

* test(json-schema): enable gradle test logging

* feat(core): customizer logging

Co-authored-by: sam0r040 <[email protected]>
  • Loading branch information
timonback and sam0r040 authored Nov 17, 2023
1 parent 06c0524 commit 545e4a2
Show file tree
Hide file tree
Showing 22 changed files with 1,074 additions and 18 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/springwolf-addons.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
8 changes: 6 additions & 2 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ allprojects {

useJUnitPlatform()
testLogging {
events "passed", "skipped", "failed"
// showStandardStreams = true

events "skipped", "failed"
exceptionFormat = 'full'
}
}
Expand All @@ -85,7 +87,9 @@ allprojects {
excludePatterns = ['*IntegrationTest']
}
testLogging {
events "passed", "skipped", "failed"
// showStandardStreams = true

events "skipped", "failed"
exceptionFormat = 'full'
}
}
Expand Down
2 changes: 2 additions & 0 deletions dependencies.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'

Expand Down
3 changes: 2 additions & 1 deletion settings.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
6 changes: 0 additions & 6 deletions springwolf-add-ons/springwolf-generic-binding/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,6 @@ java {
withSourcesJar()
}

test {
dependsOn spotlessApply // Automatically fix code formatting if possible

useJUnitPlatform()
}

publishing {
publications {
mavenJava(MavenPublication) {
Expand Down
57 changes: 57 additions & 0 deletions springwolf-add-ons/springwolf-json-schema/README.md
Original file line number Diff line number Diff line change
@@ -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:<springwolf-version>'
}
```

#### 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"
}
}
}

```
55 changes: 55 additions & 0 deletions springwolf-add-ons/springwolf-json-schema/build.gradle
Original file line number Diff line number Diff line change
@@ -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'
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Schema> schemas = asyncAPI.getComponents().getSchemas();
for (Map.Entry<String, Schema> 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);
}
}
}
}
Original file line number Diff line number Diff line change
@@ -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<String, Schema> 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<String, Schema> definitions, Set<Schema> visited) {
if (schema != null && !visited.contains(schema)) {
visited.add(schema);

return mapToJsonSchema(schema, definitions, visited);
}
return objectMapper.createObjectNode();
}

private ObjectNode mapToJsonSchema(Schema<?> schema, Map<String, Schema> definitions, Set<Schema> 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<String, Schema> definitions, Set<Schema> visited) {
ObjectNode node = objectMapper.createObjectNode();

for (Map.Entry<String, Schema> 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;
}
}
Loading

0 comments on commit 545e4a2

Please sign in to comment.