Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create json-schema Springwolf add-on #447

Merged
merged 11 commits into from
Nov 17, 2023
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