From 84849725ef4c2913d4f30773304727e00a3ac2a5 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Mon, 7 Aug 2023 10:29:05 +0200 Subject: [PATCH 1/3] GH-134: fix: Handle NPE in ExampleJsonGenerator i.e. A MapSchema is an object, but doesn't have properties --- .gitignore | 1 + .../schemas/example/ExampleJsonGenerator.java | 43 +++++-- .../schemas/DefaultSchemasServiceTest.java | 51 ++++++++ .../schemas/complex-definitions.json | 115 ++++++++++++++++++ 4 files changed, 200 insertions(+), 10 deletions(-) create mode 100644 springwolf-core/src/test/resources/schemas/complex-definitions.json diff --git a/.gitignore b/.gitignore index 836739e77..f66a854ab 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ +bin/ build/ springwolf-add-ons/build/ springwolf-core/build/ diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java index 0be9360c3..33e4b2df0 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java @@ -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; @@ -45,7 +48,10 @@ public Object fromSchema(Schema schema, Map definitions) { } static String buildSchema(Schema schema, Map definitions) { + return buildSchemaInternal(schema, definitions, new HashSet<>()); + } + private static String buildSchemaInternal(Schema schema, Map definitions, Set visited) { String exampleValue = ExampleJsonGenerator.getExampleValue(schema); if (exampleValue != null) { return exampleValue; @@ -58,15 +64,15 @@ static String buildSchema(Schema schema, Map 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; }; @@ -86,10 +92,10 @@ private static String getExampleValue(Schema schema) { return null; } - private static String handleArraySchema(Schema schema, Map definitions) { + private static String handleArraySchema(Schema schema, Map definitions, Set 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(); } @@ -117,8 +123,9 @@ private static String handleStringSchema(Schema schema) { } private static String getFirstEnumValue(Schema schema) { - if (schema.getEnum() != null) { - Optional firstEnumEntry = schema.getEnum().stream().findFirst(); + List enums = schema.getEnum(); + if (enums != null) { + Optional firstEnumEntry = enums.stream().findFirst(); if (firstEnumEntry.isPresent()) { return firstEnumEntry.get(); } @@ -126,18 +133,34 @@ private static String getFirstEnumValue(Schema schema) { return null; } - private static String handleObject(Schema schema, Map definitions) { + private static String handleObject(Schema schema, Map definitions, Set visited) { + Map 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 properties, Map definitions, Set visited) { StringBuilder sb = new StringBuilder(); sb.append("{"); - Map 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() diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java index 2f1d89456..3464bf512 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java @@ -8,6 +8,7 @@ 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; @@ -15,8 +16,11 @@ 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; @@ -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); @@ -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 nli; + private Set nsm; + private Map nmfm; + private Cyclic nc; + + @Data + @NoArgsConstructor + private static class Cyclic { + + @Nullable + private Cyclic cyclic; + } + + @Data + @NoArgsConstructor + private static class MyClass { + private String s; + } + } + } } diff --git a/springwolf-core/src/test/resources/schemas/complex-definitions.json b/springwolf-core/src/test/resources/schemas/complex-definitions.json new file mode 100644 index 000000000..316effabc --- /dev/null +++ b/springwolf-core/src/test/resources/schemas/complex-definitions.json @@ -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" + } ] + } + } +} \ No newline at end of file From 579ef1d7d6994fd65a86eb7cb8cb1cb3100bee2b Mon Sep 17 00:00:00 2001 From: Timon Back Date: Mon, 14 Aug 2023 16:33:20 +0200 Subject: [PATCH 2/3] fix: Handle unknown schema --- .../schemas/example/ExampleJsonGenerator.java | 13 +++++++++---- .../schemas/example/ExampleJsonGeneratorTest.java | 15 +++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java index 33e4b2df0..7bb34dabc 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java @@ -36,6 +36,10 @@ public class ExampleJsonGenerator implements ExampleGenerator { "\"0111010001100101011100110111010000101101011000100110100101101110011000010110010001111001\""; private static final String DEFAULT_STRING_EXAMPLE = "\"string\""; + private static String DEFAULT_UNKNOWN_SCHAME_EXAMPLE(String type) { + return "\"unknown schema type: " + type + "\""; + } + @Override public Object fromSchema(Schema schema, Map definitions) { try { @@ -57,9 +61,9 @@ private static String buildSchemaInternal(Schema schema, Map def 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); @@ -67,6 +71,7 @@ private static String buildSchemaInternal(Schema schema, Map def return buildSchemaInternal(resolvedSchema, definitions, visited); } + String type = schema.getType(); return switch (type) { case "array" -> ExampleJsonGenerator.handleArraySchema(schema, definitions, visited); case "boolean" -> DEFAULT_BOOLEAN_EXAMPLE; @@ -74,7 +79,7 @@ private static String buildSchemaInternal(Schema schema, Map def case "number" -> DEFAULT_NUMBER_EXAMPLE; case "object" -> ExampleJsonGenerator.handleObject(schema, definitions, visited); case "string" -> ExampleJsonGenerator.handleStringSchema(schema); - default -> "unknown schema type: " + type; + default -> DEFAULT_UNKNOWN_SCHAME_EXAMPLE(type); }; } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java index 8ff3fb4a9..b07e91981 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java @@ -206,6 +206,21 @@ void type_string_format_password() { assertThat(actual).isEqualTo("\"string-password\""); } + @Test + void type_unknown_schema() { + class TestSchema extends Schema { + 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(); From 3f6687656d6d2c17bf8a649b26a45ffdee216515 Mon Sep 17 00:00:00 2001 From: Timon Back Date: Mon, 14 Aug 2023 17:00:26 +0200 Subject: [PATCH 3/3] feat: Provide example value for uuid and email --- .../schemas/example/ExampleJsonGenerator.java | 14 +++++++-- .../schemas/DefaultSchemasServiceTest.java | 3 ++ .../example/ExampleJsonGeneratorTest.java | 30 +++++++++++++++++++ .../schemas/complex-definitions.json | 19 ++++++++++-- 4 files changed, 61 insertions(+), 5 deletions(-) diff --git a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java index 7bb34dabc..c1190a090 100644 --- a/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java +++ b/springwolf-core/src/main/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGenerator.java @@ -35,11 +35,17 @@ 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 = "\"example@example.com\""; + public static final String DEFAULT_UUID_EXAMPLE = "\"3fa85f64-5717-4562-b3fc-2c963f66afa6\""; - private static String DEFAULT_UNKNOWN_SCHAME_EXAMPLE(String type) { + 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 definitions) { try { @@ -79,7 +85,7 @@ private static String buildSchemaInternal(Schema schema, Map def case "number" -> DEFAULT_NUMBER_EXAMPLE; case "object" -> ExampleJsonGenerator.handleObject(schema, definitions, visited); case "string" -> ExampleJsonGenerator.handleStringSchema(schema); - default -> DEFAULT_UNKNOWN_SCHAME_EXAMPLE(type); + default -> DEFAULT_UNKNOWN_SCHEMA_EXAMPLE(type); }; } @@ -120,10 +126,12 @@ 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); }; } diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java index 3464bf512..40eb02c43 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/DefaultSchemasServiceTest.java @@ -21,6 +21,7 @@ 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; @@ -161,6 +162,8 @@ private static class Nested { private List nli; private Set nsm; private Map nmfm; + private UUID nu; + private byte[] nba; private Cyclic nc; @Data diff --git a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java index b07e91981..8b2f9e72b 100644 --- a/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java +++ b/springwolf-core/src/test/java/io/github/stavshamir/springwolf/schemas/example/ExampleJsonGeneratorTest.java @@ -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; @@ -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("\"example@example.com\""); + } + @Test void type_string_format_password() { PasswordSchema schema = new PasswordSchema(); @@ -206,6 +217,25 @@ 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 { diff --git a/springwolf-core/src/test/resources/schemas/complex-definitions.json b/springwolf-core/src/test/resources/schemas/complex-definitions.json index 316effabc..c630042a7 100644 --- a/springwolf-core/src/test/resources/schemas/complex-definitions.json +++ b/springwolf-core/src/test/resources/schemas/complex-definitions.json @@ -35,6 +35,7 @@ "f" : 1.1, "i" : 0, "n" : { + "nba" : [ "YmFzZTY0LWV4YW1wbGU=" ], "nc" : { "cyclic" : { } }, @@ -43,7 +44,8 @@ "ns" : "string", "nsm" : [ { "s" : "string" - } ] + } ], + "nu" : "3fa85f64-5717-4562-b3fc-2c963f66afa6" }, "s" : "string" } @@ -73,6 +75,13 @@ "Nested" : { "type" : "object", "properties" : { + "nba" : { + "type" : "array", + "items" : { + "type" : "string", + "format" : "byte" + } + }, "nc" : { "$ref" : "#/components/schemas/Cyclic" }, @@ -98,9 +107,14 @@ "items" : { "$ref" : "#/components/schemas/MyClass" } + }, + "nu" : { + "type" : "string", + "format" : "uuid" } }, "example" : { + "nba" : [ "YmFzZTY0LWV4YW1wbGU=" ], "nc" : { "cyclic" : { } }, @@ -109,7 +123,8 @@ "ns" : "string", "nsm" : [ { "s" : "string" - } ] + } ], + "nu" : "3fa85f64-5717-4562-b3fc-2c963f66afa6" } } } \ No newline at end of file