From e46a02fc90057ab87251e81a21a69038016c69c3 Mon Sep 17 00:00:00 2001 From: tstevelinck Date: Fri, 20 Aug 2021 11:30:42 +0200 Subject: [PATCH 1/2] feat: identify array of single type --- ...JsonSchemaFromFieldDescriptorsGenerator.kt | 59 +++------- ...SchemaFromFieldDescriptorsGeneratorTest.kt | 108 ++++++++++++++++++ .../restdocs/apispec/model/ResourceModel.kt | 3 +- 3 files changed, 129 insertions(+), 41 deletions(-) diff --git a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt index 2f46a096..d7744d34 100644 --- a/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt +++ b/restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt @@ -62,8 +62,8 @@ class JsonSchemaFromFieldDescriptorsGenerator { if (schema is ObjectSchema) { val groups = groupFieldsByFirstRemainingPathSegment(emptyList(), jsonFieldPaths) if (groups.keys.size == 1 && groups.keys.contains("[]")) { - val descriptor = jsonFieldPaths.find { it.fieldDescriptor.path == "[]" }?.fieldDescriptor - return ArraySchema.builder().allItemSchema(schema.propertySchemas["[]"]).applyConstraints(descriptor).title(schema.title).build() + return jsonFieldPaths.find { it.fieldDescriptor.path == "[]" }?.fieldDescriptor?.jsonSchemaType() + ?: ArraySchema.builder().allItemSchema(schema.propertySchemas["[]"]).title(schema.title).build() } } return schema @@ -148,6 +148,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { traversedSegments.add(remainingSegments[0]) builder.addPropertySchema( propertyName, + ArraySchema.builder() .allItemSchema(traverse(traversedSegments, fields, ObjectSchema.builder())) .applyConstraints(propertyField?.fieldDescriptor) @@ -167,37 +168,11 @@ class JsonSchemaFromFieldDescriptorsGenerator { } private fun handleEndOfPath(builder: ObjectSchema.Builder, propertyName: String, fieldDescriptor: FieldDescriptorWithSchemaType) { - - if (fieldDescriptor.ignored) { - // We don't need to render anything - } else { + if (!fieldDescriptor.ignored) { if (isRequired(fieldDescriptor)) { builder.addRequiredProperty(propertyName) } - if (propertyName == "[]") { - builder.addPropertySchema( - propertyName, - createSchemaWithArrayContent(ObjectSchema.builder().build(), depthOfArrayPath(fieldDescriptor.path), fieldDescriptor) - ) - } else { - builder.addPropertySchema(propertyName, fieldDescriptor.jsonSchemaType()) - } - } - } - - private fun depthOfArrayPath(path: String): Int { - return path.split("]") - .filter { it.isNotEmpty() } - .size - 1 - } - - private fun createSchemaWithArrayContent(schema: Schema, level: Int, fieldDescriptor: FieldDescriptorWithSchemaType): Schema { - return if (schema is ObjectSchema && level < 1) { - schema - } else if (level <= 1) { - ArraySchema.builder().addItemSchema(schema).applyConstraints(fieldDescriptor).build() - } else { - createSchemaWithArrayContent(ArraySchema.builder().addItemSchema(schema).applyConstraints(fieldDescriptor).build(), level - 1, fieldDescriptor) + builder.addPropertySchema(propertyName, fieldDescriptor.jsonSchemaType()) } } @@ -243,16 +218,7 @@ class JsonSchemaFromFieldDescriptorsGenerator { "null" -> NullSchema.builder() "empty" -> EmptySchema.builder() "object" -> ObjectSchema.builder() - "array" -> ArraySchema.builder().applyConstraints(this).allItemSchema( - CombinedSchema.oneOf( - listOf( - ObjectSchema.builder().build(), - BooleanSchema.builder().build(), - StringSchema.builder().build(), - NumberSchema.builder().build() - ) - ).build() - ) + "array" -> ArraySchema.builder().applyConstraints(this).allItemSchema(arrayItemsSchema()) "boolean" -> BooleanSchema.builder() "number" -> NumberSchema.builder() "string" -> StringSchema.builder() @@ -267,6 +233,19 @@ class JsonSchemaFromFieldDescriptorsGenerator { else -> throw IllegalArgumentException("unknown field type $type") } + private fun arrayItemsSchema(): Schema { + return attributes.itemsType + ?.let { typeToSchema(it.toLowerCase()).build() } + ?: CombinedSchema.oneOf( + listOf( + ObjectSchema.builder().build(), + BooleanSchema.builder().build(), + StringSchema.builder().build(), + NumberSchema.builder().build() + ) + ).build() + } + fun equalsOnPathAndType(f: FieldDescriptorWithSchemaType): Boolean = ( this.path == f.path && diff --git a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt index 399256cf..75570357 100644 --- a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt +++ b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt @@ -9,8 +9,11 @@ import com.jayway.jsonpath.JsonPath import org.assertj.core.api.BDDAssertions.then import org.assertj.core.api.BDDAssertions.thenThrownBy import org.everit.json.schema.ArraySchema +import org.everit.json.schema.BooleanSchema import org.everit.json.schema.CombinedSchema +import org.everit.json.schema.CombinedSchema.ONE_CRITERION import org.everit.json.schema.EnumSchema +import org.everit.json.schema.NumberSchema import org.everit.json.schema.ObjectSchema import org.everit.json.schema.Schema import org.everit.json.schema.StringSchema @@ -289,6 +292,89 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { thenSchemaIsValid() } + @Test + fun should_specify_generic_items_type_in_array_when_descriptor_doesnt_contain_itemsType_in_additionalParameters() { + givenFieldDescriptorWithTopLevelArrayOfAny() + + whenSchemaGenerated() + + then(schema).isInstanceOf(ArraySchema::class.java) + then((schema as ArraySchema).allItemSchema).isInstanceOf(CombinedSchema::class.java) + val combinedSchema = ((schema as ArraySchema).allItemSchema) as CombinedSchema + then(combinedSchema.criterion).isEqualTo(ONE_CRITERION) + then(combinedSchema.subschemas).extracting("class").containsExactlyInAnyOrder( + ObjectSchema::class.java, + BooleanSchema::class.java, + StringSchema::class.java, + NumberSchema::class.java + ) + thenSchemaIsValid() + } + + @Test + fun should_specify_accurate_items_type_in_array_when_descriptor_contains_itemsType_in_additionalParameters() { + givenFieldDescriptorWithArrayOfSingleType() + + whenSchemaGenerated() + + then(schema).isInstanceOf(ArraySchema::class.java) + then((schema as ArraySchema).allItemSchema).isInstanceOf(StringSchema::class.java) + thenSchemaIsValid() + } + + @Test + fun should_specify_generic_items_type_in_array_of_array_when_descriptor_doesnt_contain_itemsType_in_additionalParameters() { + givenFieldDescriptorWithTopLevelArrayOfArrayOfAny() + + whenSchemaGenerated() + + then(schema).isInstanceOf(ArraySchema::class.java) + then((schema as ArraySchema).allItemSchema).isInstanceOf(ArraySchema::class.java) + val arrayOfArraySchema = ((schema as ArraySchema).allItemSchema) as ArraySchema + then(arrayOfArraySchema.allItemSchema).isInstanceOf(CombinedSchema::class.java) + val combinedSchema = arrayOfArraySchema.allItemSchema as CombinedSchema + then(combinedSchema.criterion).isEqualTo(ONE_CRITERION) + then(combinedSchema.subschemas).extracting("class").containsExactlyInAnyOrder( + ObjectSchema::class.java, + BooleanSchema::class.java, + StringSchema::class.java, + NumberSchema::class.java + ) + thenSchemaIsValid() + } + + @Test + fun should_specify_accurate_items_type_in_array_of_array_when_descriptor_contains_itemsType_in_additionalParameters() { + givenFieldDescriptorWithTopLevelArrayOfArrayOfSingleType() + + whenSchemaGenerated() + + then(schema).isInstanceOf(ArraySchema::class.java) + then((schema as ArraySchema).allItemSchema).isInstanceOf(ArraySchema::class.java) + val arrayOfArraySchema = (schema as ArraySchema).allItemSchema as ArraySchema + then(arrayOfArraySchema.allItemSchema).isInstanceOf(StringSchema::class.java) + thenSchemaIsValid() + } + + @Test + fun should_not_erased_array_content() { + fieldDescriptors = listOf( + FieldDescriptor("thisIsAnArray", "I'm an array", "ARRAY"), + FieldDescriptor("thisIsAnArray[].numberItem", "I'm a number", "NUMBER"), + FieldDescriptor("thisIsAnArray[].objectItem", "I'm an object", "OBJECT") + ) + whenSchemaGenerated() + + then(schema).isInstanceOf(ObjectSchema::class.java) + then((schema as ObjectSchema).definesProperty("thisIsAnArray")).isTrue + then((schema as ObjectSchema).propertySchemas["thisIsAnArray"]).isInstanceOf(ArraySchema::class.java) + val objectInArray = ((schema as ObjectSchema).propertySchemas["thisIsAnArray"] as ArraySchema).allItemSchema as ObjectSchema + then(objectInArray.definesProperty("numberItem")).isTrue + then(objectInArray.propertySchemas["numberItem"]).isInstanceOf(NumberSchema::class.java) + then(objectInArray.definesProperty("objectItem")).isTrue + then(objectInArray.propertySchemas["objectItem"]).isInstanceOf(ObjectSchema::class.java) + } + private fun thenSchemaIsValid() { val report = JsonSchemaFactory.byDefault() @@ -331,10 +417,32 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { fieldDescriptors = listOf(FieldDescriptor("[]", "some", "ARRAY")) } + private fun givenFieldDescriptorWithArrayOfSingleType() { + fieldDescriptors = listOf( + FieldDescriptor( + "[]", + "some", + "ARRAY", + attributes = Attributes(itemsType = "string") + ) + ) + } + private fun givenFieldDescriptorWithTopLevelArrayOfArrayOfAny() { fieldDescriptors = listOf(FieldDescriptor("[][]", "some", "ARRAY")) } + private fun givenFieldDescriptorWithTopLevelArrayOfArrayOfSingleType() { + fieldDescriptors = listOf( + FieldDescriptor( + "[][]", + "some", + "ARRAY", + attributes = Attributes(itemsType = "string") + ) + ) + } + private fun givenFieldDescriptorUnspecifiedArrayItems() { fieldDescriptors = listOf(FieldDescriptor("some[]", "some", "ARRAY")) } diff --git a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt index bcdcfe9c..a1a0adec 100644 --- a/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt +++ b/restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt @@ -86,7 +86,8 @@ open class FieldDescriptor( data class Attributes( val validationConstraints: List = emptyList(), - val enumValues: List = emptyList() + val enumValues: List = emptyList(), + val itemsType: String? = null ) data class Constraint( From 4ddbe394a92694c970a3b8c5e0da6d1fd0893288 Mon Sep 17 00:00:00 2001 From: tstevelinck Date: Wed, 25 Aug 2021 10:36:15 +0200 Subject: [PATCH 2/2] apply reviews --- ...SchemaFromFieldDescriptorsGeneratorTest.kt | 64 +++++++------------ 1 file changed, 23 insertions(+), 41 deletions(-) diff --git a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt index 75570357..9fdb947e 100644 --- a/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt +++ b/restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt @@ -141,6 +141,15 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { whenSchemaGenerated() then(schema).isInstanceOf(ArraySchema::class.java) + then((schema as ArraySchema).allItemSchema).isInstanceOf(CombinedSchema::class.java) + val combinedSchema = ((schema as ArraySchema).allItemSchema) as CombinedSchema + then(combinedSchema.criterion).isEqualTo(ONE_CRITERION) + then(combinedSchema.subschemas).extracting("class").containsExactlyInAnyOrder( + ObjectSchema::class.java, + BooleanSchema::class.java, + StringSchema::class.java, + NumberSchema::class.java + ) thenSchemaIsValid() thenSchemaValidatesJson("""[{"id": "some"}]""") } @@ -152,6 +161,17 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { whenSchemaGenerated() then(schema).isInstanceOf(ArraySchema::class.java) + then((schema as ArraySchema).allItemSchema).isInstanceOf(ArraySchema::class.java) + val arrayOfArraySchema = ((schema as ArraySchema).allItemSchema) as ArraySchema + then(arrayOfArraySchema.allItemSchema).isInstanceOf(CombinedSchema::class.java) + val combinedSchema = arrayOfArraySchema.allItemSchema as CombinedSchema + then(combinedSchema.criterion).isEqualTo(ONE_CRITERION) + then(combinedSchema.subschemas).extracting("class").containsExactlyInAnyOrder( + ObjectSchema::class.java, + BooleanSchema::class.java, + StringSchema::class.java, + NumberSchema::class.java + ) thenSchemaIsValid() thenSchemaValidatesJson("""[[{"id": "some"}]]""") } @@ -292,25 +312,6 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { thenSchemaIsValid() } - @Test - fun should_specify_generic_items_type_in_array_when_descriptor_doesnt_contain_itemsType_in_additionalParameters() { - givenFieldDescriptorWithTopLevelArrayOfAny() - - whenSchemaGenerated() - - then(schema).isInstanceOf(ArraySchema::class.java) - then((schema as ArraySchema).allItemSchema).isInstanceOf(CombinedSchema::class.java) - val combinedSchema = ((schema as ArraySchema).allItemSchema) as CombinedSchema - then(combinedSchema.criterion).isEqualTo(ONE_CRITERION) - then(combinedSchema.subschemas).extracting("class").containsExactlyInAnyOrder( - ObjectSchema::class.java, - BooleanSchema::class.java, - StringSchema::class.java, - NumberSchema::class.java - ) - thenSchemaIsValid() - } - @Test fun should_specify_accurate_items_type_in_array_when_descriptor_contains_itemsType_in_additionalParameters() { givenFieldDescriptorWithArrayOfSingleType() @@ -322,27 +323,6 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { thenSchemaIsValid() } - @Test - fun should_specify_generic_items_type_in_array_of_array_when_descriptor_doesnt_contain_itemsType_in_additionalParameters() { - givenFieldDescriptorWithTopLevelArrayOfArrayOfAny() - - whenSchemaGenerated() - - then(schema).isInstanceOf(ArraySchema::class.java) - then((schema as ArraySchema).allItemSchema).isInstanceOf(ArraySchema::class.java) - val arrayOfArraySchema = ((schema as ArraySchema).allItemSchema) as ArraySchema - then(arrayOfArraySchema.allItemSchema).isInstanceOf(CombinedSchema::class.java) - val combinedSchema = arrayOfArraySchema.allItemSchema as CombinedSchema - then(combinedSchema.criterion).isEqualTo(ONE_CRITERION) - then(combinedSchema.subschemas).extracting("class").containsExactlyInAnyOrder( - ObjectSchema::class.java, - BooleanSchema::class.java, - StringSchema::class.java, - NumberSchema::class.java - ) - thenSchemaIsValid() - } - @Test fun should_specify_accurate_items_type_in_array_of_array_when_descriptor_contains_itemsType_in_additionalParameters() { givenFieldDescriptorWithTopLevelArrayOfArrayOfSingleType() @@ -357,12 +337,13 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { } @Test - fun should_not_erased_array_content() { + fun should_create_objectSchema_in_arraySchema_when_items_of_array_are_object() { fieldDescriptors = listOf( FieldDescriptor("thisIsAnArray", "I'm an array", "ARRAY"), FieldDescriptor("thisIsAnArray[].numberItem", "I'm a number", "NUMBER"), FieldDescriptor("thisIsAnArray[].objectItem", "I'm an object", "OBJECT") ) + whenSchemaGenerated() then(schema).isInstanceOf(ObjectSchema::class.java) @@ -373,6 +354,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest { then(objectInArray.propertySchemas["numberItem"]).isInstanceOf(NumberSchema::class.java) then(objectInArray.definesProperty("objectItem")).isTrue then(objectInArray.propertySchemas["objectItem"]).isInstanceOf(ObjectSchema::class.java) + thenSchemaIsValid() } private fun thenSchemaIsValid() {