Skip to content

handle size and required constraints on arrays #183

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

Merged
merged 2 commits into from
Aug 9, 2021
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
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ internal object ConstraintResolver {

private const val LENGTH_CONSTRAINT = "org.hibernate.validator.constraints.Length"

private const val SIZE_CONSTRAINT = "javax.validation.constraints.Size"

internal fun maybeMinSizeArray(fieldDescriptor: FieldDescriptor?) = fieldDescriptor?.maybeSizeConstraint()?.let { it.configuration["min"] as? Int }

internal fun maybeMaxSizeArray(fieldDescriptor: FieldDescriptor?) = fieldDescriptor?.maybeSizeConstraint()?.let { it.configuration["max"] as? Int }

private fun FieldDescriptor.maybeSizeConstraint() = findConstraints(this).firstOrNull { SIZE_CONSTRAINT == it.name }

internal fun minLengthString(fieldDescriptor: FieldDescriptor): Int? {
return findConstraints(fieldDescriptor)
.firstOrNull { constraint ->
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.epages.restdocs.apispec.jsonschema

import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.isRequired
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maxLengthString
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maybeMaxSizeArray
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maybeMinSizeArray
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.minLengthString
import com.epages.restdocs.apispec.model.Attributes
import com.epages.restdocs.apispec.model.FieldDescriptor
Expand Down Expand Up @@ -60,7 +62,8 @@ class JsonSchemaFromFieldDescriptorsGenerator {
if (schema is ObjectSchema) {
val groups = groupFieldsByFirstRemainingPathSegment(emptyList(), jsonFieldPaths)
if (groups.keys.size == 1 && groups.keys.contains("[]")) {
return ArraySchema.builder().allItemSchema(schema.propertySchemas["[]"]).title(schema.title).build()
val descriptor = jsonFieldPaths.find { it.fieldDescriptor.path == "[]" }?.fieldDescriptor
return ArraySchema.builder().allItemSchema(schema.propertySchemas["[]"]).applyConstraints(descriptor).title(schema.title).build()
}
}
return schema
Expand Down Expand Up @@ -135,6 +138,9 @@ class JsonSchemaFromFieldDescriptorsGenerator {
propertyField: JsonFieldPath? = null
) {
val remainingSegments = fields[0].remainingSegments(traversedSegments)
if (propertyField?.fieldDescriptor?.let { isRequired(it) } == true) {
builder.addRequiredProperty(propertyName)
}
if (remainingSegments.isNotEmpty() && JsonFieldPath.isArraySegment(
remainingSegments[0]
)
Expand All @@ -144,13 +150,11 @@ class JsonSchemaFromFieldDescriptorsGenerator {
propertyName,
ArraySchema.builder()
.allItemSchema(traverse(traversedSegments, fields, ObjectSchema.builder()))
.applyConstraints(propertyField?.fieldDescriptor)
.description(propertyField?.fieldDescriptor?.description)
.build()
)
} else {
if (propertyField?.fieldDescriptor?.let { isRequired(it) } == true) {
builder.addRequiredProperty(propertyName)
}
builder.addPropertySchema(
propertyName,
traverse(
Expand All @@ -173,7 +177,7 @@ class JsonSchemaFromFieldDescriptorsGenerator {
if (propertyName == "[]") {
builder.addPropertySchema(
propertyName,
createSchemaWithArrayContent(ObjectSchema.builder().build(), depthOfArrayPath(fieldDescriptor.path))
createSchemaWithArrayContent(ObjectSchema.builder().build(), depthOfArrayPath(fieldDescriptor.path), fieldDescriptor)
)
} else {
builder.addPropertySchema(propertyName, fieldDescriptor.jsonSchemaType())
Expand All @@ -187,13 +191,13 @@ class JsonSchemaFromFieldDescriptorsGenerator {
.size - 1
}

private fun createSchemaWithArrayContent(schema: Schema, level: Int): Schema {
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).build()
ArraySchema.builder().addItemSchema(schema).applyConstraints(fieldDescriptor).build()
} else {
createSchemaWithArrayContent(ArraySchema.builder().addItemSchema(schema).build(), level - 1)
createSchemaWithArrayContent(ArraySchema.builder().addItemSchema(schema).applyConstraints(fieldDescriptor).build(), level - 1, fieldDescriptor)
}
}

Expand Down Expand Up @@ -239,7 +243,7 @@ class JsonSchemaFromFieldDescriptorsGenerator {
"null" -> NullSchema.builder()
"empty" -> EmptySchema.builder()
"object" -> ObjectSchema.builder()
"array" -> ArraySchema.builder().allItemSchema(
"array" -> ArraySchema.builder().applyConstraints(this).allItemSchema(
CombinedSchema.oneOf(
listOf(
ObjectSchema.builder().build(),
Expand Down Expand Up @@ -286,3 +290,8 @@ class JsonSchemaFromFieldDescriptorsGenerator {
}
}
}

private fun ArraySchema.Builder.applyConstraints(fieldDescriptor: FieldDescriptor?) = apply {
minItems(maybeMinSizeArray(fieldDescriptor))
maxItems(maybeMaxSizeArray(fieldDescriptor))
}
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,15 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {

then(lineItemSchema.allItemSchema).isInstanceOf(ObjectSchema::class.java)

thenSchemaIsValid()
then(objectSchema.propertySchemas["lineItems"]).isInstanceOf(ArraySchema::class.java)

val paymentLineItem = objectSchema.propertySchemas["paymentLineItem"] as ObjectSchema

val lineItemsTaxesSchema = paymentLineItem.propertySchemas["lineItemTaxes"] as ArraySchema
then(lineItemsTaxesSchema.minItems).isEqualTo(1)
then(lineItemsTaxesSchema.maxItems).isEqualTo(255)
then(lineItemsTaxesSchema.requiresArray()).isTrue()

// language=JSON
thenSchemaValidatesJson(
"""{
Expand Down Expand Up @@ -168,6 +176,18 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
then(objSchema.requiredProperties).contains("obj")
}

@Test
fun should_generate_schema_for_required_array_in_object() {
givenFieldDescriptorWithRequiredArray()

whenSchemaGenerated()

then(schema).isInstanceOf(ObjectSchema::class.java)
thenSchemaIsValid()
val objSchema = schema!!.let { it as ObjectSchema }
then(objSchema.requiredProperties).contains("obj")
}

@Test
fun should_fail_on_unknown_field_type() {
givenFieldDescriptorWithInvalidType()
Expand Down Expand Up @@ -230,6 +250,45 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
thenSchemaValidatesJson("""{ some: "ENUM_VALUE_1" }""")
}

@Test
fun should_generate_schema_for_top_level_array_with_size_constraint() {
givenFieldDescriptorWithTopLevelArrayWithSizeConstraint()

whenSchemaGenerated()

then(schema).isInstanceOf(ArraySchema::class.java)
then((schema as ArraySchema).minItems).isEqualTo(1)
then((schema as ArraySchema).maxItems).isEqualTo(255)
thenSchemaIsValid()
}

@Test
fun should_generate_schema_for_top_level_array_of_arrays_with_size_constraint() {
givenFieldDescriptorWithTopLevelArrayOfArraysWithSizeConstraint()

whenSchemaGenerated()

then(schema).isInstanceOf(ArraySchema::class.java)
then((schema as ArraySchema).allItemSchema).isInstanceOf(ArraySchema::class.java)
then(((schema as ArraySchema).allItemSchema as ArraySchema).minItems).isEqualTo(1)
then(((schema as ArraySchema).allItemSchema as ArraySchema).maxItems).isEqualTo(255)
thenSchemaIsValid()
}

@Test
fun should_generate_schema_for_array_with_size_constraint() {
givenFieldDescriptorUnspecifiedArrayItemsWithSizeConstraint()

whenSchemaGenerated()

then(schema).isInstanceOf(ObjectSchema::class.java)
then((schema as ObjectSchema).definesProperty("some")).isTrue
then((schema as ObjectSchema).propertySchemas["some"]).isInstanceOf(ArraySchema::class.java)
then(((schema as ObjectSchema).propertySchemas["some"] as ArraySchema).minItems).isEqualTo(1)
then(((schema as ObjectSchema).propertySchemas["some"] as ArraySchema).maxItems).isEqualTo(255)
thenSchemaIsValid()
}

private fun thenSchemaIsValid() {

val report = JsonSchemaFactory.byDefault()
Expand All @@ -256,6 +315,14 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
)
}

private fun givenFieldDescriptorWithRequiredArray() {
val notNullConstraint = Attributes(listOf(Constraint(NotNull::class.java.name, emptyMap())))
fieldDescriptors = listOf(
FieldDescriptor("array", "someArray", "ARRAY", attributes = notNullConstraint),
FieldDescriptor("array[].field", "some", "STRING")
)
}

private fun givenFieldDescriptorWithTopLevelArray() {
fieldDescriptors = listOf(FieldDescriptor("[]['id']", "some", "STRING"))
}
Expand All @@ -272,6 +339,50 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
fieldDescriptors = listOf(FieldDescriptor("some[]", "some", "ARRAY"))
}

private fun givenFieldDescriptorWithTopLevelArrayWithSizeConstraint() {
fieldDescriptors = listOf(
FieldDescriptor(
"[]",
"some",
"ARRAY",
attributes = Attributes(
listOf(
Constraint(
"javax.validation.constraints.Size",
mapOf("min" to 1, "max" to 255)
)
)
)
)
)
}

private fun givenFieldDescriptorWithTopLevelArrayOfArraysWithSizeConstraint() {
fieldDescriptors = listOf(
FieldDescriptor(
"[][]",
"some",
"ARRAY",
attributes = Attributes(
listOf(Constraint("javax.validation.constraints.Size", mapOf("min" to 1, "max" to 255)))
)
)
)
}

private fun givenFieldDescriptorUnspecifiedArrayItemsWithSizeConstraint() {
fieldDescriptors = listOf(
FieldDescriptor(
"some[]",
"some",
"ARRAY",
attributes = Attributes(
listOf(Constraint("javax.validation.constraints.Size", mapOf("min" to 1, "max" to 255)))
)
)
)
}

private fun givenFieldDescriptorWithInvalidType() {
fieldDescriptors = listOf(FieldDescriptor("id", "some", "invalid-type"))
}
Expand Down Expand Up @@ -340,6 +451,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
"NUMBER",
attributes = constraintAttributeWithNotNull
),

FieldDescriptor("lineItems[*].quantity.unit", "some", "STRING"),
FieldDescriptor("shippingAddress", "some", "OBJECT"),
FieldDescriptor("billingAddress", "some", "OBJECT"),
Expand All @@ -355,7 +467,26 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
)
),
FieldDescriptor("billingAddress.valid", "some", "BOOLEAN"),
FieldDescriptor("paymentLineItem.lineItemTaxes", "some", "ARRAY")
FieldDescriptor(
"paymentLineItem.lineItemTaxes",
"some",
"ARRAY",
attributes = Attributes(
listOf(
Constraint(
"javax.validation.constraints.Size",
mapOf(
"min" to 1,
"max" to 255
)
),
Constraint(
NotNull::class.java.name,
emptyMap()
)
)
)
)
)
}

Expand Down