Skip to content

Commit dc5fc79

Browse files
TiboStevtstevelinck
and
tstevelinck
authored
Handle size and required constraints on arrays (#183)
* handle size and required constraints on arrays Fixes #90, #180 * apply review comments Co-authored-by: tstevelinck <[email protected]>
1 parent 1df181c commit dc5fc79

File tree

3 files changed

+159
-11
lines changed

3 files changed

+159
-11
lines changed

restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/ConstraintResolver.kt

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,14 @@ internal object ConstraintResolver {
2222

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

25+
private const val SIZE_CONSTRAINT = "javax.validation.constraints.Size"
26+
27+
internal fun maybeMinSizeArray(fieldDescriptor: FieldDescriptor?) = fieldDescriptor?.maybeSizeConstraint()?.let { it.configuration["min"] as? Int }
28+
29+
internal fun maybeMaxSizeArray(fieldDescriptor: FieldDescriptor?) = fieldDescriptor?.maybeSizeConstraint()?.let { it.configuration["max"] as? Int }
30+
31+
private fun FieldDescriptor.maybeSizeConstraint() = findConstraints(this).firstOrNull { SIZE_CONSTRAINT == it.name }
32+
2533
internal fun minLengthString(fieldDescriptor: FieldDescriptor): Int? {
2634
return findConstraints(fieldDescriptor)
2735
.firstOrNull { constraint ->

restdocs-api-spec-jsonschema/src/main/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGenerator.kt

Lines changed: 18 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package com.epages.restdocs.apispec.jsonschema
22

33
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.isRequired
44
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maxLengthString
5+
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maybeMaxSizeArray
6+
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.maybeMinSizeArray
57
import com.epages.restdocs.apispec.jsonschema.ConstraintResolver.minLengthString
68
import com.epages.restdocs.apispec.model.Attributes
79
import com.epages.restdocs.apispec.model.FieldDescriptor
@@ -60,7 +62,8 @@ class JsonSchemaFromFieldDescriptorsGenerator {
6062
if (schema is ObjectSchema) {
6163
val groups = groupFieldsByFirstRemainingPathSegment(emptyList(), jsonFieldPaths)
6264
if (groups.keys.size == 1 && groups.keys.contains("[]")) {
63-
return ArraySchema.builder().allItemSchema(schema.propertySchemas["[]"]).title(schema.title).build()
65+
val descriptor = jsonFieldPaths.find { it.fieldDescriptor.path == "[]" }?.fieldDescriptor
66+
return ArraySchema.builder().allItemSchema(schema.propertySchemas["[]"]).applyConstraints(descriptor).title(schema.title).build()
6467
}
6568
}
6669
return schema
@@ -135,6 +138,9 @@ class JsonSchemaFromFieldDescriptorsGenerator {
135138
propertyField: JsonFieldPath? = null
136139
) {
137140
val remainingSegments = fields[0].remainingSegments(traversedSegments)
141+
if (propertyField?.fieldDescriptor?.let { isRequired(it) } == true) {
142+
builder.addRequiredProperty(propertyName)
143+
}
138144
if (remainingSegments.isNotEmpty() && JsonFieldPath.isArraySegment(
139145
remainingSegments[0]
140146
)
@@ -144,13 +150,11 @@ class JsonSchemaFromFieldDescriptorsGenerator {
144150
propertyName,
145151
ArraySchema.builder()
146152
.allItemSchema(traverse(traversedSegments, fields, ObjectSchema.builder()))
153+
.applyConstraints(propertyField?.fieldDescriptor)
147154
.description(propertyField?.fieldDescriptor?.description)
148155
.build()
149156
)
150157
} else {
151-
if (propertyField?.fieldDescriptor?.let { isRequired(it) } == true) {
152-
builder.addRequiredProperty(propertyName)
153-
}
154158
builder.addPropertySchema(
155159
propertyName,
156160
traverse(
@@ -173,7 +177,7 @@ class JsonSchemaFromFieldDescriptorsGenerator {
173177
if (propertyName == "[]") {
174178
builder.addPropertySchema(
175179
propertyName,
176-
createSchemaWithArrayContent(ObjectSchema.builder().build(), depthOfArrayPath(fieldDescriptor.path))
180+
createSchemaWithArrayContent(ObjectSchema.builder().build(), depthOfArrayPath(fieldDescriptor.path), fieldDescriptor)
177181
)
178182
} else {
179183
builder.addPropertySchema(propertyName, fieldDescriptor.jsonSchemaType())
@@ -187,13 +191,13 @@ class JsonSchemaFromFieldDescriptorsGenerator {
187191
.size - 1
188192
}
189193

190-
private fun createSchemaWithArrayContent(schema: Schema, level: Int): Schema {
194+
private fun createSchemaWithArrayContent(schema: Schema, level: Int, fieldDescriptor: FieldDescriptorWithSchemaType): Schema {
191195
return if (schema is ObjectSchema && level < 1) {
192196
schema
193197
} else if (level <= 1) {
194-
ArraySchema.builder().addItemSchema(schema).build()
198+
ArraySchema.builder().addItemSchema(schema).applyConstraints(fieldDescriptor).build()
195199
} else {
196-
createSchemaWithArrayContent(ArraySchema.builder().addItemSchema(schema).build(), level - 1)
200+
createSchemaWithArrayContent(ArraySchema.builder().addItemSchema(schema).applyConstraints(fieldDescriptor).build(), level - 1, fieldDescriptor)
197201
}
198202
}
199203

@@ -239,7 +243,7 @@ class JsonSchemaFromFieldDescriptorsGenerator {
239243
"null" -> NullSchema.builder()
240244
"empty" -> EmptySchema.builder()
241245
"object" -> ObjectSchema.builder()
242-
"array" -> ArraySchema.builder().allItemSchema(
246+
"array" -> ArraySchema.builder().applyConstraints(this).allItemSchema(
243247
CombinedSchema.oneOf(
244248
listOf(
245249
ObjectSchema.builder().build(),
@@ -286,3 +290,8 @@ class JsonSchemaFromFieldDescriptorsGenerator {
286290
}
287291
}
288292
}
293+
294+
private fun ArraySchema.Builder.applyConstraints(fieldDescriptor: FieldDescriptor?) = apply {
295+
minItems(maybeMinSizeArray(fieldDescriptor))
296+
maxItems(maybeMaxSizeArray(fieldDescriptor))
297+
}

restdocs-api-spec-jsonschema/src/test/kotlin/com/epages/restdocs/apispec/jsonschema/JsonSchemaFromFieldDescriptorsGeneratorTest.kt

Lines changed: 133 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,15 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
8080

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

83-
thenSchemaIsValid()
83+
then(objectSchema.propertySchemas["lineItems"]).isInstanceOf(ArraySchema::class.java)
84+
85+
val paymentLineItem = objectSchema.propertySchemas["paymentLineItem"] as ObjectSchema
86+
87+
val lineItemsTaxesSchema = paymentLineItem.propertySchemas["lineItemTaxes"] as ArraySchema
88+
then(lineItemsTaxesSchema.minItems).isEqualTo(1)
89+
then(lineItemsTaxesSchema.maxItems).isEqualTo(255)
90+
then(lineItemsTaxesSchema.requiresArray()).isTrue()
91+
8492
// language=JSON
8593
thenSchemaValidatesJson(
8694
"""{
@@ -168,6 +176,18 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
168176
then(objSchema.requiredProperties).contains("obj")
169177
}
170178

179+
@Test
180+
fun should_generate_schema_for_required_array_in_object() {
181+
givenFieldDescriptorWithRequiredArray()
182+
183+
whenSchemaGenerated()
184+
185+
then(schema).isInstanceOf(ObjectSchema::class.java)
186+
thenSchemaIsValid()
187+
val objSchema = schema!!.let { it as ObjectSchema }
188+
then(objSchema.requiredProperties).contains("obj")
189+
}
190+
171191
@Test
172192
fun should_fail_on_unknown_field_type() {
173193
givenFieldDescriptorWithInvalidType()
@@ -230,6 +250,45 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
230250
thenSchemaValidatesJson("""{ some: "ENUM_VALUE_1" }""")
231251
}
232252

253+
@Test
254+
fun should_generate_schema_for_top_level_array_with_size_constraint() {
255+
givenFieldDescriptorWithTopLevelArrayWithSizeConstraint()
256+
257+
whenSchemaGenerated()
258+
259+
then(schema).isInstanceOf(ArraySchema::class.java)
260+
then((schema as ArraySchema).minItems).isEqualTo(1)
261+
then((schema as ArraySchema).maxItems).isEqualTo(255)
262+
thenSchemaIsValid()
263+
}
264+
265+
@Test
266+
fun should_generate_schema_for_top_level_array_of_arrays_with_size_constraint() {
267+
givenFieldDescriptorWithTopLevelArrayOfArraysWithSizeConstraint()
268+
269+
whenSchemaGenerated()
270+
271+
then(schema).isInstanceOf(ArraySchema::class.java)
272+
then((schema as ArraySchema).allItemSchema).isInstanceOf(ArraySchema::class.java)
273+
then(((schema as ArraySchema).allItemSchema as ArraySchema).minItems).isEqualTo(1)
274+
then(((schema as ArraySchema).allItemSchema as ArraySchema).maxItems).isEqualTo(255)
275+
thenSchemaIsValid()
276+
}
277+
278+
@Test
279+
fun should_generate_schema_for_array_with_size_constraint() {
280+
givenFieldDescriptorUnspecifiedArrayItemsWithSizeConstraint()
281+
282+
whenSchemaGenerated()
283+
284+
then(schema).isInstanceOf(ObjectSchema::class.java)
285+
then((schema as ObjectSchema).definesProperty("some")).isTrue
286+
then((schema as ObjectSchema).propertySchemas["some"]).isInstanceOf(ArraySchema::class.java)
287+
then(((schema as ObjectSchema).propertySchemas["some"] as ArraySchema).minItems).isEqualTo(1)
288+
then(((schema as ObjectSchema).propertySchemas["some"] as ArraySchema).maxItems).isEqualTo(255)
289+
thenSchemaIsValid()
290+
}
291+
233292
private fun thenSchemaIsValid() {
234293

235294
val report = JsonSchemaFactory.byDefault()
@@ -256,6 +315,14 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
256315
)
257316
}
258317

318+
private fun givenFieldDescriptorWithRequiredArray() {
319+
val notNullConstraint = Attributes(listOf(Constraint(NotNull::class.java.name, emptyMap())))
320+
fieldDescriptors = listOf(
321+
FieldDescriptor("array", "someArray", "ARRAY", attributes = notNullConstraint),
322+
FieldDescriptor("array[].field", "some", "STRING")
323+
)
324+
}
325+
259326
private fun givenFieldDescriptorWithTopLevelArray() {
260327
fieldDescriptors = listOf(FieldDescriptor("[]['id']", "some", "STRING"))
261328
}
@@ -272,6 +339,50 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
272339
fieldDescriptors = listOf(FieldDescriptor("some[]", "some", "ARRAY"))
273340
}
274341

342+
private fun givenFieldDescriptorWithTopLevelArrayWithSizeConstraint() {
343+
fieldDescriptors = listOf(
344+
FieldDescriptor(
345+
"[]",
346+
"some",
347+
"ARRAY",
348+
attributes = Attributes(
349+
listOf(
350+
Constraint(
351+
"javax.validation.constraints.Size",
352+
mapOf("min" to 1, "max" to 255)
353+
)
354+
)
355+
)
356+
)
357+
)
358+
}
359+
360+
private fun givenFieldDescriptorWithTopLevelArrayOfArraysWithSizeConstraint() {
361+
fieldDescriptors = listOf(
362+
FieldDescriptor(
363+
"[][]",
364+
"some",
365+
"ARRAY",
366+
attributes = Attributes(
367+
listOf(Constraint("javax.validation.constraints.Size", mapOf("min" to 1, "max" to 255)))
368+
)
369+
)
370+
)
371+
}
372+
373+
private fun givenFieldDescriptorUnspecifiedArrayItemsWithSizeConstraint() {
374+
fieldDescriptors = listOf(
375+
FieldDescriptor(
376+
"some[]",
377+
"some",
378+
"ARRAY",
379+
attributes = Attributes(
380+
listOf(Constraint("javax.validation.constraints.Size", mapOf("min" to 1, "max" to 255)))
381+
)
382+
)
383+
)
384+
}
385+
275386
private fun givenFieldDescriptorWithInvalidType() {
276387
fieldDescriptors = listOf(FieldDescriptor("id", "some", "invalid-type"))
277388
}
@@ -340,6 +451,7 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
340451
"NUMBER",
341452
attributes = constraintAttributeWithNotNull
342453
),
454+
343455
FieldDescriptor("lineItems[*].quantity.unit", "some", "STRING"),
344456
FieldDescriptor("shippingAddress", "some", "OBJECT"),
345457
FieldDescriptor("billingAddress", "some", "OBJECT"),
@@ -355,7 +467,26 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
355467
)
356468
),
357469
FieldDescriptor("billingAddress.valid", "some", "BOOLEAN"),
358-
FieldDescriptor("paymentLineItem.lineItemTaxes", "some", "ARRAY")
470+
FieldDescriptor(
471+
"paymentLineItem.lineItemTaxes",
472+
"some",
473+
"ARRAY",
474+
attributes = Attributes(
475+
listOf(
476+
Constraint(
477+
"javax.validation.constraints.Size",
478+
mapOf(
479+
"min" to 1,
480+
"max" to 255
481+
)
482+
),
483+
Constraint(
484+
NotNull::class.java.name,
485+
emptyMap()
486+
)
487+
)
488+
)
489+
)
359490
)
360491
}
361492

0 commit comments

Comments
 (0)