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