From 907da67ba5d66d1f6c4f82b1b8854e7e1218f889 Mon Sep 17 00:00:00 2001 From: Andrew Rouse Date: Mon, 15 Apr 2024 11:12:36 +0100 Subject: [PATCH 1/3] Scanner tests: Include output in failure message Include the full JSON output from the scanner when a test fails, to avoid needing to search the logs and enable debug logging. --- .../openapi/runtime/scanner/IndexScannerTestBase.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java index 09e2bd7c3..5e7cc1f71 100644 --- a/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/IndexScannerTestBase.java @@ -186,7 +186,13 @@ public static void assertJsonEquals(String expectedResource, OpenAPI actual) thr } public static void assertJsonEquals(URL expectedResourceUrl, OpenAPI actual) throws JSONException, IOException { - JSONAssert.assertEquals(loadResource(expectedResourceUrl), toJSON(actual), true); + String json = toJSON(actual); + try { + JSONAssert.assertEquals(loadResource(expectedResourceUrl), json, true); + } catch (AssertionError e) { + // If the JSON did not match, we want to add the serialized version to the end + throw new AssertionError(e.getMessage() + "\nFull result:\n" + json, e); + } } public static OpenAPI scan(Class... classes) { From 8d98138e5d7c7d0ab483c138c6334663d36becbf Mon Sep 17 00:00:00 2001 From: Andrew Rouse Date: Mon, 15 Apr 2024 11:13:40 +0100 Subject: [PATCH 2/3] Add tests for subschemas Tests for: - if/then/else - allOf/anyOf/oneOf/not - dependentSchemas - prefixItems - contains/maxContains/minContains - propertyNames - patternProperties - contentEncoding - contentMediaType - contentSchema --- .../scanner/SubschemaApplicationTest.java | 271 ++++++++++++++++++ ...chemas.subschema-application-property.json | 83 ++++++ ...ponents.schemas.subschema-application.json | 55 ++++ ...chemas.subschema-collections-property.json | 83 ++++++ ...ponents.schemas.subschema-collections.json | 83 ++++++ ...subschema-dependent-required-property.json | 23 ++ ....schemas.subschema-dependent-required.json | 30 ++ ....subschema-dependent-schemas-property.json | 30 ++ ...s.schemas.subschema-dependent-schemas.json | 31 ++ ...schemas.subschema-ifthenelse-property.json | 35 +++ ...mponents.schemas.subschema-ifthenelse.json | 31 ++ 11 files changed, 755 insertions(+) create mode 100644 core/src/test/java/io/smallrye/openapi/runtime/scanner/SubschemaApplicationTest.java create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application-property.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections-property.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required-property.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas-property.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse-property.json create mode 100644 core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse.json diff --git a/core/src/test/java/io/smallrye/openapi/runtime/scanner/SubschemaApplicationTest.java b/core/src/test/java/io/smallrye/openapi/runtime/scanner/SubschemaApplicationTest.java new file mode 100644 index 000000000..74f82263a --- /dev/null +++ b/core/src/test/java/io/smallrye/openapi/runtime/scanner/SubschemaApplicationTest.java @@ -0,0 +1,271 @@ +package io.smallrye.openapi.runtime.scanner; + +import static org.eclipse.microprofile.openapi.annotations.enums.SchemaType.OBJECT; + +import java.util.List; +import java.util.Map; + +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.media.DependentRequired; +import org.eclipse.microprofile.openapi.annotations.media.DependentSchema; +import org.eclipse.microprofile.openapi.annotations.media.PatternProperty; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.media.SchemaProperty; +import org.eclipse.microprofile.openapi.models.OpenAPI; +import org.jboss.jandex.Index; +import org.junit.jupiter.api.Test; + +public class SubschemaApplicationTest extends IndexScannerTestBase { + + @Test + public void testSubschemaApplication() throws Exception { + Index index = indexOf(A.class, B.class, C.class, TestOneOf.class, TestAnyOf.class, TestAllOf.class, TestNot.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-application.json", result); + } + + @Test + public void testSubschemaApplicationProperty() throws Exception { + Index index = indexOf(A.class, B.class, C.class, TestOneOfProperty.class, TestAnyOfProperty.class, + TestAllOfProperty.class, TestNotProperty.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-application-property.json", result); + } + + @Test + public void testSubschemaIfThenElse() throws Exception { + Index index = indexOf(A.class, B.class, C.class, TestIfThenElse.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-ifthenelse.json", result); + } + + @Test + public void testSubschemaIfThenElseProperty() throws Exception { + Index index = indexOf(A.class, B.class, C.class, TestIfThenElseProperty.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-ifthenelse-property.json", result); + } + + @Test + public void testDependentSchemas() throws Exception { + Index index = indexOf(A.class, B.class, TestDepedentSchemas.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-dependent-schemas.json", result); + } + + @Test + public void testDependentSchemasProperty() throws Exception { + Index index = indexOf(A.class, B.class, TestDepedentSchemasProperty.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-dependent-schemas-property.json", result); + } + + @Test + public void testDependentRequired() throws Exception { + Index index = indexOf(TestDependentRequired.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-dependent-required.json", result); + } + + @Test + public void testDependentRequiredProperty() throws Exception { + Index index = indexOf(TestDependentRequiredProperty.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-dependent-required-property.json", result); + } + + @Test + public void testSubschemaCollections() throws Exception { + Index index = indexOf(A.class, JavaTypeString.class, TestCollections.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-collections.json", result); + } + + @Test + public void testSubschemaCollectionsProperty() throws Exception { + Index index = indexOf(A.class, JavaTypeString.class, TestCollectionsProperty.class); + OpenApiAnnotationScanner scanner = new OpenApiAnnotationScanner(emptyConfig(), index); + + OpenAPI result = scanner.scan(); + + printToConsole(result); + assertJsonEquals("components.schemas.subschema-collections-property.json", result); + } + + @Schema(oneOf = { A.class, B.class, C.class }) + public static class TestOneOf { + } + + @Schema(anyOf = { A.class, B.class, C.class }) + public static class TestAnyOf { + } + + @Schema(allOf = { A.class, B.class, C.class }) + public static class TestAllOf { + } + + @Schema(not = A.class) + public static class TestNot { + } + + @Schema(properties = { + @SchemaProperty(name = "prop", oneOf = { A.class, B.class, C.class }) + }) + public static class TestOneOfProperty { + } + + @Schema(properties = { + @SchemaProperty(name = "prop", anyOf = { A.class, B.class, C.class }) + }) + public static class TestAnyOfProperty { + } + + @Schema(properties = { + @SchemaProperty(name = "prop", allOf = { A.class, B.class, C.class }) + }) + public static class TestAllOfProperty { + } + + @Schema(properties = @SchemaProperty(name = "prop", not = A.class)) + public static class TestNotProperty { + } + + @Schema(ifSchema = A.class, thenSchema = B.class, elseSchema = C.class) + public static class TestIfThenElse { + } + + @Schema(properties = @SchemaProperty(name = "prop", ifSchema = A.class, thenSchema = B.class, elseSchema = C.class)) + public static class TestIfThenElseProperty { + } + + @Schema(dependentSchemas = { + @DependentSchema(name = "field1", schema = A.class), + @DependentSchema(name = "field2", schema = B.class) + }) + public static class TestDepedentSchemas { + public String field1; + } + + @Schema(properties = @SchemaProperty(name = "prop", dependentSchemas = { + @DependentSchema(name = "field1", schema = A.class), + @DependentSchema(name = "field2", schema = B.class) + })) + public static class TestDepedentSchemasProperty { + } + + @Schema(dependentRequired = { + @DependentRequired(name = "field1", requires = { + "field2", + "field3" + }), + @DependentRequired(name = "field4", requires = "field5") + }) + public static class TestDependentRequired { + public String field1; + public String field2; + public String field3; + } + + @Schema(properties = @SchemaProperty(name = "prop", dependentRequired = { + @DependentRequired(name = "field1", requires = { + "field2", + "field3" + }), + @DependentRequired(name = "field4", requires = "field5") + })) + public static class TestDependentRequiredProperty { + } + + @Schema + public static class TestCollections { + @Schema(prefixItems = { JavaTypeString.class, JavaTypeString.class }) + public List mustStartWithTwoTypeNames; + + @Schema(contains = JavaTypeString.class) + public List mustContainATypeName; + + @Schema(contains = JavaTypeString.class, minContains = 3, maxContains = 5) + public List mustContain3To5TypeNames; + + @Schema(propertyNames = JavaTypeString.class) + public Map keysMustBeTypeNames; + + @Schema(patternProperties = { + @PatternProperty(regex = "^str", schema = String.class), + @PatternProperty(regex = "^int", schema = Integer.class) + }) + public Map keysNamedByType; + + @Schema(contentEncoding = "base64", contentMediaType = "application/json", contentSchema = A.class) + public String encodedJson; + } + + @Schema(properties = { + @SchemaProperty(name = "mustStartWithTwoTypeNames", type = SchemaType.ARRAY, implementation = String.class, prefixItems = { + JavaTypeString.class, + JavaTypeString.class + }), + @SchemaProperty(name = "mustContainATypeName", type = SchemaType.ARRAY, implementation = String.class, contains = JavaTypeString.class), + @SchemaProperty(name = "mustContain3To5TypeNames", type = SchemaType.ARRAY, implementation = String.class, contains = JavaTypeString.class, minContains = 3, maxContains = 5), + @SchemaProperty(name = "keysMustBeTypeNames", type = OBJECT, propertyNames = JavaTypeString.class, additionalProperties = String.class), + @SchemaProperty(name = "keysNamedByType", type = OBJECT, implementation = Object.class, patternProperties = { + @PatternProperty(regex = "^str", schema = String.class), + @PatternProperty(regex = "^int", schema = Integer.class) + }, additionalProperties = Object.class), + @SchemaProperty(name = "encodedJson", implementation = String.class, contentEncoding = "base64", contentMediaType = "application/json", contentSchema = A.class) + }) + public static class TestCollectionsProperty { + } + + @Schema(type = SchemaType.STRING, pattern = "^[A-Z][a-zA-Z0-9]*$") + public static class JavaTypeString { + } + + @Schema(type = OBJECT, description = "A") + public static class A { + } + + @Schema(description = "B") + public static class B { + } + + @Schema(description = "C") + public static class C { + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application-property.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application-property.json new file mode 100644 index 000000000..a6d3d8673 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application-property.json @@ -0,0 +1,83 @@ +{ + "openapi": "3.0.3", + "components": { + "schemas": { + "A": { + "description": "A", + "type": "object" + }, + "B": { + "description": "B", + "type": "object" + }, + "C": { + "description": "C", + "type": "object" + }, + "TestAnyOfProperty": { + "properties": { + "prop": { + "anyOf": [ + { + "$ref": "#/components/schemas/A" + }, + { + "$ref": "#/components/schemas/B" + }, + { + "$ref": "#/components/schemas/C" + } + ] + } + }, + "type": "object" + }, + "TestAllOfProperty": { + "properties": { + "prop": { + "allOf": [ + { + "$ref": "#/components/schemas/A" + }, + { + "$ref": "#/components/schemas/B" + }, + { + "$ref": "#/components/schemas/C" + } + ] + } + }, + "type": "object" + }, + "TestNotProperty": { + "properties": { + "prop": { + "not": { + "$ref": "#/components/schemas/A" + } + } + }, + "type": "object" + }, + "TestOneOfProperty": { + "properties": { + "prop": { + "oneOf": [ + { + "$ref": "#/components/schemas/A" + }, + { + "$ref": "#/components/schemas/B" + }, + { + "$ref": "#/components/schemas/C" + } + ] + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application.json new file mode 100644 index 000000000..c866058f2 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-application.json @@ -0,0 +1,55 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "A" : { + "description" : "A", + "type" : "object" + }, + "B" : { + "description" : "B", + "type" : "object" + }, + "C" : { + "description" : "C", + "type" : "object" + }, + "TestAnyOf" : { + "anyOf" : [ { + "$ref" : "#/components/schemas/A" + }, { + "$ref" : "#/components/schemas/B" + }, { + "$ref" : "#/components/schemas/C" + } ], + "type" : "object" + }, + "TestAllOf" : { + "allOf" : [ { + "$ref" : "#/components/schemas/A" + }, { + "$ref" : "#/components/schemas/B" + }, { + "$ref" : "#/components/schemas/C" + } ], + "type" : "object" + }, + "TestNot" : { + "not" : { + "$ref" : "#/components/schemas/A" + }, + "type" : "object" + }, + "TestOneOf" : { + "oneOf" : [ { + "$ref" : "#/components/schemas/A" + }, { + "$ref" : "#/components/schemas/B" + }, { + "$ref" : "#/components/schemas/C" + } ], + "type" : "object" + } + } + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections-property.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections-property.json new file mode 100644 index 000000000..20dbe1fa5 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections-property.json @@ -0,0 +1,83 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "JavaTypeString" : { + "type" : "string", + "pattern" : "^[A-Z][a-zA-Z0-9]*$" + }, + "A" : { + "description" : "A", + "type" : "object" + }, + "TestCollectionsProperty" : { + "properties" : { + "mustStartWithTwoTypeNames" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "prefixItems" : [ + { + "$ref" : "#/components/schemas/JavaTypeString" + }, { + "$ref" : "#/components/schemas/JavaTypeString" + } + ] + }, + "mustContainATypeName" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "contains" : { + "$ref" : "#/components/schemas/JavaTypeString" + } + }, + "mustContain3To5TypeNames" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "contains" : { + "$ref" : "#/components/schemas/JavaTypeString" + }, + "minContains" : 3, + "maxContains" : 5 + }, + "keysMustBeTypeNames" : { + "type" : "object", + "propertyNames" : { + "$ref" : "#/components/schemas/JavaTypeString" + }, + "additionalProperties" : { + "type" : "string" + } + }, + "keysNamedByType" : { + "type" : "object", + "patternProperties" : { + "^str" : { + "type" : "string" + }, + "^int" : { + "type" : "integer", + "format" : "int32" + } + }, + "additionalProperties" : {} + }, + "encodedJson" : { + "type" : "string", + "contentEncoding" : "base64", + "contentMediaType" : "application/json", + "contentSchema" : { + "$ref" : "#/components/schemas/A" + } + } + }, + "type" : "object" + } + } + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections.json new file mode 100644 index 000000000..f26e32238 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-collections.json @@ -0,0 +1,83 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "JavaTypeString" : { + "type" : "string", + "pattern" : "^[A-Z][a-zA-Z0-9]*$" + }, + "A" : { + "description" : "A", + "type" : "object" + }, + "TestCollections" : { + "properties" : { + "mustStartWithTwoTypeNames" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "prefixItems" : [ + { + "$ref" : "#/components/schemas/JavaTypeString" + }, { + "$ref" : "#/components/schemas/JavaTypeString" + } + ] + }, + "mustContainATypeName" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "contains" : { + "$ref" : "#/components/schemas/JavaTypeString" + } + }, + "mustContain3To5TypeNames" : { + "type" : "array", + "items" : { + "type" : "string" + }, + "contains" : { + "$ref" : "#/components/schemas/JavaTypeString" + }, + "minContains" : 3, + "maxContains" : 5 + }, + "keysMustBeTypeNames" : { + "type" : "object", + "propertyNames" : { + "$ref" : "#/components/schemas/JavaTypeString" + }, + "additionalProperties" : { + "type" : "string" + } + }, + "keysNamedByType" : { + "type" : "object", + "patternProperties" : { + "^str" : { + "type" : "string" + }, + "^int" : { + "type" : "integer", + "format" : "int32" + } + }, + "additionalProperties" : {} + }, + "encodedJson" : { + "type" : "string", + "contentEncoding" : "base64", + "contentMediaType" : "application/json", + "contentSchema" : { + "$ref" : "#/components/schemas/A" + } + } + }, + "type" : "object" + } + } + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required-property.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required-property.json new file mode 100644 index 000000000..c0c1d939a --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required-property.json @@ -0,0 +1,23 @@ +{ + "openapi": "3.0.3", + "components": { + "schemas": { + "TestDependentRequiredProperty": { + "properties": { + "prop": { + "dependentRequired": { + "field1": [ + "field2", + "field3" + ], + "field4": [ + "field5" + ] + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required.json new file mode 100644 index 000000000..fbc345fd2 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-required.json @@ -0,0 +1,30 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "TestDependentRequired" : { + "properties" : { + "field1" : { + "type" : "string" + }, + "field2" : { + "type" : "string" + }, + "field3" : { + "type" : "string" + } + }, + "dependentRequired": { + "field1" : [ + "field2", + "field3" + ], + "field4" : [ + "field5" + ] + }, + "type" : "object" + } + } + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas-property.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas-property.json new file mode 100644 index 000000000..3a145c1a2 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas-property.json @@ -0,0 +1,30 @@ +{ + "openapi": "3.0.3", + "components": { + "schemas": { + "A": { + "description": "A", + "type": "object" + }, + "B": { + "description": "B", + "type": "object" + }, + "TestDepedentSchemasProperty": { + "properties": { + "prop": { + "dependentSchemas": { + "field1": { + "$ref": "#/components/schemas/A" + }, + "field2": { + "$ref": "#/components/schemas/B" + } + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas.json new file mode 100644 index 000000000..e9e4c630e --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-dependent-schemas.json @@ -0,0 +1,31 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "A" : { + "description" : "A", + "type" : "object" + }, + "B" : { + "description" : "B", + "type" : "object" + }, + "TestDepedentSchemas" : { + "properties" : { + "field1" : { + "type" : "string" + } + }, + "dependentSchemas": { + "field1" : { + "$ref" : "#/components/schemas/A" + }, + "field2" : { + "$ref" : "#/components/schemas/B" + } + }, + "type" : "object" + } + } + } +} diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse-property.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse-property.json new file mode 100644 index 000000000..cbe0b7b6a --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse-property.json @@ -0,0 +1,35 @@ +{ + "openapi": "3.0.3", + "components": { + "schemas": { + "A": { + "description": "A", + "type": "object" + }, + "B": { + "description": "B", + "type": "object" + }, + "C": { + "description": "C", + "type": "object" + }, + "TestIfThenElseProperty": { + "properties": { + "prop": { + "if": { + "$ref": "#/components/schemas/A" + }, + "then": { + "$ref": "#/components/schemas/B" + }, + "else": { + "$ref": "#/components/schemas/C" + } + } + }, + "type": "object" + } + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse.json b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse.json new file mode 100644 index 000000000..31d6aca52 --- /dev/null +++ b/core/src/test/resources/io/smallrye/openapi/runtime/scanner/components.schemas.subschema-ifthenelse.json @@ -0,0 +1,31 @@ +{ + "openapi" : "3.0.3", + "components" : { + "schemas" : { + "A" : { + "description" : "A", + "type" : "object" + }, + "B" : { + "description" : "B", + "type" : "object" + }, + "C" : { + "description" : "C", + "type" : "object" + }, + "TestIfThenElse" : { + "if" : { + "$ref" : "#/components/schemas/A" + }, + "then" : { + "$ref" : "#/components/schemas/B" + }, + "else" :{ + "$ref" : "#/components/schemas/C" + }, + "type" : "object" + } + } + } +} From 617b83171a2d95ac8bd5119b22725f6901bbe0a5 Mon Sep 17 00:00:00 2001 From: Andrew Rouse Date: Mon, 15 Apr 2024 17:36:17 +0100 Subject: [PATCH 3/3] Implement new Schema annotation fields - examples - comment - if/then/elseSchema - dependentSchemas - prefixItems - contains/minContains/maxContains - patternProperties - propertyNames - constValue - dependentRequired - contentEncoding/contentMediaType/contentSchema --- .../runtime/io/schema/SchemaConstant.java | 7 + .../runtime/io/schema/SchemaFactory.java | 124 ++++++++++++++++-- 2 files changed, 119 insertions(+), 12 deletions(-) diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaConstant.java b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaConstant.java index bf86b9580..3be863bb0 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaConstant.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaConstant.java @@ -107,11 +107,18 @@ public class SchemaConstant { public static final String PROP_PREFIX_ITEMS = "prefixItems"; public static final String PROP_CONTAINS = "contains"; public static final String PROP_PATTERN_PROPERTIES = "patternProperties"; + public static final String PROP_IF_SCHEMA = "ifSchema"; + public static final String PROP_THEN_SCHEMA = "thenSchema"; + public static final String PROP_ELSE_SCHEMA = "elseSchema"; + public static final String PROP_CONST_VALUE = "constValue"; + public static final String PROP_COMMENT_FIELD = "comment"; // Only in SchemaFactory ? public static final String PROP_REQUIRED_PROPERTIES = "requiredProperties"; public static final String PROP_PROPERTIES = "properties"; public static final String PROP_NOT = "not"; + public static final String PROP_REGEX = "regex"; + public static final String PROP_REQUIRES = "requires"; public static final String DIALECT_OAS31 = "https://spec.openapis.org/oas/3.1/dialect/base"; public static final String DIALECT_JSON_2020_12 = "https://json-schema.org/draft/2020-12/schema"; diff --git a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java index 250d3d783..1b7279fd9 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/io/schema/SchemaFactory.java @@ -1,5 +1,7 @@ package io.smallrye.openapi.runtime.io.schema; +import static java.util.Arrays.asList; + import java.math.BigDecimal; import java.util.ArrayList; import java.util.Arrays; @@ -154,6 +156,12 @@ public static Schema readSchema(final AnnotationScannerContext context, types -> readClassSchemas(context, types, false), defaults)); schema.setAllOf(SchemaFactory.> readAttr(context, annotation, SchemaConstant.PROP_ALL_OF, types -> readClassSchemas(context, types, true), defaults)); + schema.setIfSchema(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_IF_SCHEMA, + types -> readClassSchema(context, types, true), defaults)); + schema.setThenSchema(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_THEN_SCHEMA, + types -> readClassSchema(context, types, true), defaults)); + schema.setElseSchema(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_ELSE_SCHEMA, + types -> readClassSchema(context, types, true), defaults)); schema.setTitle(readAttr(context, annotation, SchemaConstant.PROP_TITLE, defaults)); schema.setMultipleOf(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_MULTIPLE_OF, BigDecimal::valueOf, defaults)); @@ -191,7 +199,23 @@ public static Schema readSchema(final AnnotationScannerContext context, final SchemaType type = readSchemaType(context, annotation, schema, defaults); SchemaImpl.setType(schema, type); - schema.setExamples(wrapInList(parseSchemaAttr(context, annotation, SchemaConstant.PROP_EXAMPLE, defaults, type))); + + Object example = parseSchemaAttr(context, annotation, SchemaConstant.PROP_EXAMPLE, defaults, type); + List examples = SchemaFactory.> readAttr(context, annotation, + SchemaConstant.PROP_EXAMPLES, + egs -> Arrays.stream(egs) + .map(e -> parseValue(context, e, type)) + .collect(Collectors.toCollection(ArrayList::new)), + defaults); + if (examples != null) { + if (example != null) { + examples.add(example); + } + schema.setExamples(examples); + } else { + schema.setExamples(wrapInList(example)); + } + schema.setDefaultValue( parseSchemaAttr(context, annotation, SchemaConstant.PROP_DEFAULT_VALUE, defaults, type)); schema.setDiscriminator(context.io().discriminatorIO().read(annotation)); @@ -199,6 +223,8 @@ public static Schema readSchema(final AnnotationScannerContext context, schema.setMinItems(readAttr(context, annotation, SchemaConstant.PROP_MIN_ITEMS, defaults)); schema.setUniqueItems(readAttr(context, annotation, SchemaConstant.PROP_UNIQUE_ITEMS, defaults)); schema.setExtensions(context.io().extensionIO().readExtensible(annotation)); + schema.setComment(readAttr(context, annotation, SchemaConstant.PROP_COMMENT_FIELD, defaults)); + schema.setConstValue(parseSchemaAttr(context, annotation, SchemaConstant.PROP_CONST_VALUE, defaults, type)); schema.setProperties(SchemaFactory.> readAttr(context, annotation, SchemaConstant.PROP_PROPERTIES, properties -> { @@ -215,17 +241,35 @@ public static Schema readSchema(final AnnotationScannerContext context, return propertySchemas; }, defaults)); - Type additionalProperties = readAttr(context, annotation, "additionalProperties", defaults); + schema.setAdditionalPropertiesSchema( + SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_ADDITIONAL_PROPERTIES, + types -> readClassSchema(context, types, true), defaults)); - if (additionalProperties != null) { - if (additionalProperties.name().equals(SchemaConstant.DOTNAME_TRUE_SCHEMA)) { - schema.setAdditionalPropertiesSchema(new SchemaImpl().booleanSchema(Boolean.TRUE)); - } else if (additionalProperties.name().equals(SchemaConstant.DOTNAME_FALSE_SCHEMA)) { - schema.setAdditionalPropertiesSchema(new SchemaImpl().booleanSchema(Boolean.FALSE)); - } else { - schema.setAdditionalPropertiesSchema(readClassSchema(context, additionalProperties, true)); - } - } + schema.setDependentRequired(SchemaFactory.>> readAttr(context, + annotation, SchemaConstant.PROP_DEPENDENT_REQUIRED, + annos -> readDependentRequired(context, annos), defaults)); + + schema.setDependentSchemas(SchemaFactory.> readAttr(context, annotation, + SchemaConstant.PROP_DEPENDENT_SCHEMAS, + annos -> readDependentSchemas(context, annos), defaults)); + + schema.setContentEncoding(readAttr(context, annotation, SchemaConstant.PROP_CONTENT_ENCODING, defaults)); + schema.setContentMediaType(readAttr(context, annotation, SchemaConstant.PROP_CONTENT_MEDIA_TYPE, defaults)); + schema.setContentSchema(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_CONTENT_SCHEMA, + types -> readClassSchema(context, types, true), defaults)); + + schema.setContains(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_CONTAINS, + types -> readClassSchema(context, types, true), defaults)); + schema.setMaxContains(readAttr(context, annotation, SchemaConstant.PROP_MAX_CONTAINS, defaults)); + schema.setMinContains(readAttr(context, annotation, SchemaConstant.PROP_MIN_CONTAINS, defaults)); + schema.setPrefixItems( + SchemaFactory.> readAttr(context, annotation, SchemaConstant.PROP_PREFIX_ITEMS, + types -> readClassSchemas(context, types, true), defaults)); + schema.setPropertyNames(SchemaFactory. readAttr(context, annotation, SchemaConstant.PROP_PROPERTY_NAMES, + types -> readClassSchema(context, types, true), defaults)); + schema.setPatternProperties(SchemaFactory.> readAttr(context, annotation, + SchemaConstant.PROP_PATTERN_PROPERTIES, + annos -> readPatternProperties(context, annos), defaults)); List enumeration = readAttr(context, annotation, SchemaConstant.PROP_ENUMERATION, (Object[] values) -> { List parsed = new ArrayList<>(values.length); @@ -467,7 +511,11 @@ static Schema readClassSchema(final AnnotationScannerContext context, Type type, return null; } Schema schema; - if (type.kind() == Type.Kind.ARRAY) { + if (type.name().equals(SchemaConstant.DOTNAME_TRUE_SCHEMA)) { + schema = new SchemaImpl().booleanSchema(true); + } else if (type.name().equals(SchemaConstant.DOTNAME_FALSE_SCHEMA)) { + schema = new SchemaImpl().booleanSchema(false); + } else if (type.kind() == Type.Kind.ARRAY) { schema = new SchemaImpl().addType(SchemaType.ARRAY); ArrayType array = type.asArrayType(); int dimensions = array.dimensions(); @@ -760,4 +808,56 @@ private static BigDecimal tolerantParseBigDecimal(String value) { return null; } } + + private static Map> readDependentRequired(AnnotationScannerContext context, + AnnotationInstance[] requireds) { + if (requireds == null || requireds.length == 0) { + return null; + } + + Map> result = new LinkedHashMap<>(); + for (AnnotationInstance required : requireds) { + String name = context.annotations().value(required, SchemaConstant.PROP_NAME); + String[] requires = context.annotations().value(required, SchemaConstant.PROP_REQUIRES); + result.put(name, new ArrayList<>(asList(requires))); + } + + return result; + } + + private static Map readDependentSchemas(AnnotationScannerContext context, + AnnotationInstance[] dependentSchemas) { + if (dependentSchemas == null || dependentSchemas.length == 0) { + return null; + } + + Map result = new LinkedHashMap<>(); + for (AnnotationInstance dependentSchema : dependentSchemas) { + String name = context.annotations().value(dependentSchema, SchemaConstant.PROP_NAME); + Type schemaClass = context.annotations().value(dependentSchema, SchemaConstant.PROP_SCHEMA); + Schema schema = readClassSchema(context, schemaClass, true); + if (schema != null) { + result.put(name, schema); + } + } + return result; + } + + private static Map readPatternProperties(AnnotationScannerContext context, + AnnotationInstance[] patternProperties) { + if (patternProperties == null || patternProperties.length == 0) { + return null; + } + + Map result = new LinkedHashMap<>(); + for (AnnotationInstance patternProperty : patternProperties) { + String regex = context.annotations().value(patternProperty, SchemaConstant.PROP_REGEX); + Type schemaClass = context.annotations().value(patternProperty, SchemaConstant.PROP_SCHEMA); + Schema schema = readClassSchema(context, schemaClass, true); + if (schema != null) { + result.put(regex, schema); + } + } + return result; + } }