Skip to content

Commit

Permalink
GH-134: fix: Handle NPE in ExampleJsonGenerator
Browse files Browse the repository at this point in the history
i.e. A MapSchema is an object, but doesn't have properties
  • Loading branch information
timonback committed Aug 7, 2023
1 parent 7b9c709 commit 8484972
Show file tree
Hide file tree
Showing 4 changed files with 200 additions and 10 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 Down Expand Up @@ -45,7 +48,10 @@ 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;
Expand All @@ -58,15 +64,15 @@ static String buildSchema(Schema schema, Map<String, Schema> definitions) {
if (resolvedSchema == null) {
throw new ExampleGeneratingException("Missing schema during example json generation: " + schemaName);
}
return buildSchema(resolvedSchema, definitions);
return buildSchemaInternal(resolvedSchema, definitions, visited);
}

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;
};
Expand All @@ -86,10 +92,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 Down Expand Up @@ -117,27 +123,44 @@ private static String handleStringSchema(Schema schema) {
}

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,19 @@
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 static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;
Expand Down Expand Up @@ -65,6 +69,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 +142,40 @@ 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 Cyclic nc;

@Data
@NoArgsConstructor
private static class Cyclic {

@Nullable
private Cyclic cyclic;
}

@Data
@NoArgsConstructor
private static class MyClass {
private String s;
}
}
}
}
115 changes: 115 additions & 0 deletions springwolf-core/src/test/resources/schemas/complex-definitions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
{
"ComplexFoo" : {
"type" : "object",
"properties" : {
"b" : {
"type" : "boolean"
},
"d" : {
"type" : "number",
"format" : "double"
},
"dt" : {
"type" : "string",
"format" : "date-time"
},
"f" : {
"type" : "number",
"format" : "float"
},
"i" : {
"type" : "integer",
"format" : "int32"
},
"n" : {
"$ref" : "#/components/schemas/Nested"
},
"s" : {
"type" : "string"
}
},
"example" : {
"b" : true,
"d" : 1.1,
"dt" : "2015-07-20T15:49:04-07:00",
"f" : 1.1,
"i" : 0,
"n" : {
"nc" : {
"cyclic" : { }
},
"nli" : [ 0 ],
"nmfm" : { },
"ns" : "string",
"nsm" : [ {
"s" : "string"
} ]
},
"s" : "string"
}
},
"Cyclic" : {
"type" : "object",
"properties" : {
"cyclic" : {
"$ref" : "#/components/schemas/Cyclic"
}
},
"example" : {
"cyclic" : { }
}
},
"MyClass" : {
"type" : "object",
"properties" : {
"s" : {
"type" : "string"
}
},
"example" : {
"s" : "string"
}
},
"Nested" : {
"type" : "object",
"properties" : {
"nc" : {
"$ref" : "#/components/schemas/Cyclic"
},
"nli" : {
"type" : "array",
"items" : {
"type" : "integer",
"format" : "int32"
}
},
"nmfm" : {
"type" : "object",
"additionalProperties" : {
"$ref" : "#/components/schemas/MyClass"
}
},
"ns" : {
"type" : "string"
},
"nsm" : {
"uniqueItems" : true,
"type" : "array",
"items" : {
"$ref" : "#/components/schemas/MyClass"
}
}
},
"example" : {
"nc" : {
"cyclic" : { }
},
"nli" : [ 0 ],
"nmfm" : { },
"ns" : "string",
"nsm" : [ {
"s" : "string"
} ]
}
}
}

0 comments on commit 8484972

Please sign in to comment.