Skip to content

Commit

Permalink
GH-134: fix: Handle NPE in ExampleJsonGenerator (#316)
Browse files Browse the repository at this point in the history
* GH-134: fix: Handle NPE in ExampleJsonGenerator

i.e. A MapSchema is an object, but doesn't have properties
Also catch recursions

* fix: Handle unknown schema

* feat: Provide example value for uuid and email
  • Loading branch information
timonback authored Aug 18, 2023
1 parent 271c1df commit f4333b8
Show file tree
Hide file tree
Showing 5 changed files with 281 additions and 15 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
bin/
build/
springwolf-add-ons/build/
springwolf-core/build/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,11 @@
import org.springframework.stereotype.Component;

import java.math.BigDecimal;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;

import static io.github.stavshamir.springwolf.configuration.properties.SpringwolfConfigConstants.SPRINGWOLF_SCHEMA_EXAMPLE_GENERATOR;
Expand All @@ -32,6 +35,16 @@ public class ExampleJsonGenerator implements ExampleGenerator {
private static final String DEFAULT_BINARY_EXAMPLE =
"\"0111010001100101011100110111010000101101011000100110100101101110011000010110010001111001\"";
private static final String DEFAULT_STRING_EXAMPLE = "\"string\"";
public static final String DEFAULT_EMAIL_EXAMPLE = "\"[email protected]\"";
public static final String DEFAULT_UUID_EXAMPLE = "\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"";

private static String DEFAULT_UNKNOWN_SCHEMA_EXAMPLE(String type) {
return "\"unknown schema type: " + type + "\"";
}

private static String DEFAULT_UNKNOWN_SCHEMA_STRING_EXAMPLE(String format) {
return "\"unknown string schema format: " + format + "\"";
}

@Override
public Object fromSchema(Schema schema, Map<String, Schema> definitions) {
Expand All @@ -45,30 +58,34 @@ public Object fromSchema(Schema schema, Map<String, Schema> definitions) {
}

static String buildSchema(Schema schema, Map<String, Schema> definitions) {
return buildSchemaInternal(schema, definitions, new HashSet<>());
}

private static String buildSchemaInternal(Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
String exampleValue = ExampleJsonGenerator.getExampleValue(schema);
if (exampleValue != null) {
return exampleValue;
}

String type = schema.getType();
if (type == null) {
String schemaName = StringUtils.substringAfterLast(schema.get$ref(), "/");
String ref = schema.get$ref();
if (ref != null) {
String schemaName = StringUtils.substringAfterLast(ref, "/");
Schema resolvedSchema = definitions.get(schemaName);
if (resolvedSchema == null) {
throw new ExampleGeneratingException("Missing schema during example json generation: " + schemaName);
}
return buildSchema(resolvedSchema, definitions);
return buildSchemaInternal(resolvedSchema, definitions, visited);
}

String type = schema.getType();
return switch (type) {
case "array" -> ExampleJsonGenerator.handleArraySchema(schema, definitions);
case "array" -> ExampleJsonGenerator.handleArraySchema(schema, definitions, visited);
case "boolean" -> DEFAULT_BOOLEAN_EXAMPLE;
case "integer" -> DEFAULT_INTEGER_EXAMPLE;
case "number" -> DEFAULT_NUMBER_EXAMPLE;
case "object" -> ExampleJsonGenerator.handleObject(schema, definitions);
case "object" -> ExampleJsonGenerator.handleObject(schema, definitions, visited);
case "string" -> ExampleJsonGenerator.handleStringSchema(schema);
default -> "unknown schema type: " + type;
default -> DEFAULT_UNKNOWN_SCHEMA_EXAMPLE(type);
};
}

Expand All @@ -86,10 +103,10 @@ private static String getExampleValue(Schema schema) {
return null;
}

private static String handleArraySchema(Schema schema, Map<String, Schema> definitions) {
private static String handleArraySchema(Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
StringBuilder sb = new StringBuilder();
sb.append("[");
sb.append(buildSchema(schema.getItems(), definitions));
sb.append(buildSchemaInternal(schema.getItems(), definitions, visited));
sb.append("]");
return sb.toString();
}
Expand All @@ -109,35 +126,54 @@ private static String handleStringSchema(Schema schema) {
return switch (format) {
case "date" -> DEFUALT_DATE_EXAMPLE;
case "date-time" -> DEFAULT_DATE_TIME_EXAMPLE;
case "email" -> DEFAULT_EMAIL_EXAMPLE;
case "password" -> DEFAULT_PASSWORD_EXAMPLE;
case "byte" -> DEFAULT_BYTE_EXAMPLE;
case "binary" -> DEFAULT_BINARY_EXAMPLE;
default -> "unknown type format: " + format;
case "uuid" -> DEFAULT_UUID_EXAMPLE;
default -> DEFAULT_UNKNOWN_SCHEMA_STRING_EXAMPLE(format);
};
}

private static String getFirstEnumValue(Schema schema) {
if (schema.getEnum() != null) {
Optional<String> firstEnumEntry = schema.getEnum().stream().findFirst();
List<String> enums = schema.getEnum();
if (enums != null) {
Optional<String> firstEnumEntry = enums.stream().findFirst();
if (firstEnumEntry.isPresent()) {
return firstEnumEntry.get();
}
}
return null;
}

private static String handleObject(Schema schema, Map<String, Schema> definitions) {
private static String handleObject(Schema schema, Map<String, Schema> definitions, Set<Schema> visited) {
Map<String, Schema> properties = schema.getProperties();
if (properties != null) {

if (!visited.contains(schema)) {
visited.add(schema);
String example = handleObjectProperties(properties, definitions, visited);
visited.remove(schema);

return example;
}
}
// i.e. A MapSchema is type=object, but has properties=null
return "{}";
}

private static String handleObjectProperties(
Map<String, Schema> properties, Map<String, Schema> definitions, Set<Schema> visited) {
StringBuilder sb = new StringBuilder();
sb.append("{");

Map<String, Schema> properties = schema.getProperties();
String data = properties.entrySet().stream()
.map(entry -> {
StringBuilder propertyStringBuilder = new StringBuilder();
propertyStringBuilder.append("\"");
propertyStringBuilder.append(entry.getKey());
propertyStringBuilder.append("\": ");
propertyStringBuilder.append(buildSchema(entry.getValue(), definitions));
propertyStringBuilder.append(buildSchemaInternal(entry.getValue(), definitions, visited));
return propertyStringBuilder.toString();
})
.sorted()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,15 +8,20 @@
import io.github.stavshamir.springwolf.schemas.example.ExampleGenerator;
import io.github.stavshamir.springwolf.schemas.example.ExampleJsonGenerator;
import io.swagger.v3.core.util.Json;
import jakarta.annotation.Nullable;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.junit.jupiter.api.Test;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.OffsetDateTime;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.UUID;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -65,6 +70,17 @@ void getArrayDefinitions() throws IOException {
assertEquals(expected, actualDefinitions);
}

@Test
void getComplexDefinitions() throws IOException {
schemasService.register(ComplexFoo.class);

String actualDefinitions = objectMapper.writer(printer).writeValueAsString(schemasService.getDefinitions());
String expected = jsonResource("/schemas/complex-definitions.json");

System.out.println("Got: " + actualDefinitions);
assertEquals(expected, actualDefinitions);
}

@Test
void classWithSchemaAnnotation() {
String modelName = schemasService.register(ClassWithSchemaAnnotation.class);
Expand Down Expand Up @@ -127,4 +143,42 @@ private static class ClassWithSchemaAnnotation {
private String s;
private boolean b;
}

@Data
@NoArgsConstructor
private static class ComplexFoo {
private String s;
private Boolean b;
private Integer i;
private Float f;
private Double d;
private OffsetDateTime dt;
private Nested n;

@Data
@NoArgsConstructor
private static class Nested {
private String ns;
private List<Integer> nli;
private Set<MyClass> nsm;
private Map<Float, MyClass> nmfm;
private UUID nu;
private byte[] nba;
private Cyclic nc;

@Data
@NoArgsConstructor
private static class Cyclic {

@Nullable
private Cyclic cyclic;
}

@Data
@NoArgsConstructor
private static class MyClass {
private String s;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,14 @@
import io.swagger.v3.oas.models.media.BooleanSchema;
import io.swagger.v3.oas.models.media.DateSchema;
import io.swagger.v3.oas.models.media.DateTimeSchema;
import io.swagger.v3.oas.models.media.EmailSchema;
import io.swagger.v3.oas.models.media.IntegerSchema;
import io.swagger.v3.oas.models.media.NumberSchema;
import io.swagger.v3.oas.models.media.ObjectSchema;
import io.swagger.v3.oas.models.media.PasswordSchema;
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 org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;

Expand Down Expand Up @@ -197,6 +199,15 @@ void type_string_format_datetime() {
assertThat(actual).isEqualTo("\"2015-07-20T15:49:04-07:00\"");
}

@Test
void type_string_format_email() {
EmailSchema schema = new EmailSchema();

String actual = ExampleJsonGenerator.buildSchema(schema, emptyMap());

assertThat(actual).isEqualTo("\"[email protected]\"");
}

@Test
void type_string_format_password() {
PasswordSchema schema = new PasswordSchema();
Expand All @@ -206,6 +217,40 @@ void type_string_format_password() {
assertThat(actual).isEqualTo("\"string-password\"");
}

@Test
void type_string_format_uuid() {
UUIDSchema schema = new UUIDSchema();

String actual = ExampleJsonGenerator.buildSchema(schema, emptyMap());

assertThat(actual).isEqualTo("\"3fa85f64-5717-4562-b3fc-2c963f66afa6\"");
}

@Test
void type_string_format_unknown() {
StringSchema schema = new StringSchema();
schema.setFormat("unknown");

String actual = ExampleJsonGenerator.buildSchema(schema, emptyMap());

assertThat(actual).isEqualTo("\"unknown string schema format: unknown\"");
}

@Test
void type_unknown_schema() {
class TestSchema extends Schema<StringSchema> {
TestSchema() {
super("test-schema", (String) null);
}
}

TestSchema schema = new TestSchema();

String actual = ExampleJsonGenerator.buildSchema(schema, emptyMap());

assertThat(actual).isEqualTo("\"unknown schema type: test-schema\"");
}

@Test
void type_primitive_array() {
ArraySchema schema = new ArraySchema();
Expand Down
Loading

0 comments on commit f4333b8

Please sign in to comment.