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

GH-134: fix: Handle NPE in ExampleJsonGenerator #316

Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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