Skip to content

Commit d3b5029

Browse files
xeromankKieun
authored andcommitted
Feat : Schema reuse through subschema (ePages-de#246)
* feat : Input a name for the subschema * feat : Input a name for the subschema * feat : Make sub schema * fix: lint * fix: requested & Suggested (cherry picked from commit 437d7da)
1 parent 37f1964 commit d3b5029

File tree

5 files changed

+194
-1
lines changed

5 files changed

+194
-1
lines changed

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

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,11 +168,13 @@ class JsonSchemaFromFieldDescriptorsGenerator {
168168
.build()
169169
)
170170
} else {
171+
val schemaName = propertyField?.fieldDescriptor?.attributes?.schemaName
171172
builder.addPropertySchema(
172173
propertyName,
173174
traverse(
174175
traversedSegments, fields,
175176
ObjectSchema.builder()
177+
.title(schemaName)
176178
.description(propertyField?.fieldDescriptor?.description) as ObjectSchema.Builder
177179
)
178180
)

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

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,22 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
4040

4141
private var schemaString: String? = null
4242

43+
@Test
44+
@Throws(IOException::class)
45+
fun should_generate_reuse_schema() {
46+
givenFieldDescriptorsWithSchemaName()
47+
48+
whenSchemaGenerated()
49+
50+
then(schema).isInstanceOf(ObjectSchema::class.java)
51+
val objectSchema = schema as ObjectSchema?
52+
val postSchema = objectSchema?.propertySchemas?.get("post") as ObjectSchema
53+
val shippingAddressSchema = postSchema.propertySchemas["shippingAddress"] as ObjectSchema
54+
then(shippingAddressSchema.title).isEqualTo("Address")
55+
val billingAddressSchema = postSchema.propertySchemas["billingAddress"] as ObjectSchema
56+
then(billingAddressSchema.title).isEqualTo("Address")
57+
}
58+
4359
@Test
4460
@Throws(IOException::class)
4561
fun should_generate_complex_schema() {
@@ -789,6 +805,23 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
789805
)
790806
}
791807

808+
private fun givenFieldDescriptorsWithSchemaName() {
809+
810+
fieldDescriptors = listOf(
811+
FieldDescriptor(
812+
"post",
813+
"some",
814+
"OBJECT",
815+
),
816+
FieldDescriptor("post.shippingAddress", "some", "OBJECT", attributes = Attributes(schemaName = "Address")),
817+
FieldDescriptor("post.shippingAddress.firstName", "some", "STRING"),
818+
FieldDescriptor("post.shippingAddress.valid", "some", "BOOLEAN"),
819+
FieldDescriptor("post.billingAddress", "some", "OBJECT", attributes = Attributes(schemaName = "Address")),
820+
FieldDescriptor("post.billingAddress.firstName", "some", "STRING"),
821+
FieldDescriptor("post.billingAddress.valid", "some", "BOOLEAN"),
822+
)
823+
}
824+
792825
private fun thenSchemaValidatesJson(json: String) {
793826
schema!!.validate(if (json.startsWith("[")) JSONArray(json) else JSONObject(json))
794827
}

restdocs-api-spec-model/src/main/kotlin/com/epages/restdocs/apispec/model/ResourceModel.kt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,8 @@ open class FieldDescriptor(
9090
data class Attributes(
9191
val validationConstraints: List<Constraint> = emptyList(),
9292
val enumValues: List<Any> = emptyList(),
93-
val itemsType: String? = null
93+
val itemsType: String? = null,
94+
val schemaName: String? = null,
9495
)
9596

9697
data class Constraint(

restdocs-api-spec-openapi3-generator/src/main/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3Generator.kt

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ import com.epages.restdocs.apispec.model.SimpleType
1414
import com.epages.restdocs.apispec.model.groupByPath
1515
import com.epages.restdocs.apispec.openapi3.SecuritySchemeGenerator.addSecurityDefinitions
1616
import com.epages.restdocs.apispec.openapi3.SecuritySchemeGenerator.addSecurityItemFromSecurityRequirements
17+
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
1718
import com.fasterxml.jackson.module.kotlin.readValue
1819
import io.swagger.v3.core.util.Json
1920
import io.swagger.v3.oas.models.Components
@@ -76,11 +77,41 @@ object OpenApi3Generator {
7677
resources,
7778
oauth2SecuritySchemeDefinition
7879
)
80+
7981
extractDefinitions()
82+
makeSubSchema()
8083
addSecurityDefinitions(oauth2SecuritySchemeDefinition)
8184
}
8285
}
8386

87+
private fun OpenAPI.makeSubSchema() {
88+
val schemas = this.components.schemas
89+
val subSchemas = mutableMapOf<String, Schema<Any>>()
90+
schemas.forEach {
91+
val schema = it.value
92+
if (schema.properties != null) {
93+
makeSubSchema(subSchemas, schema.properties)
94+
}
95+
}
96+
97+
if (subSchemas.isNotEmpty()) {
98+
this.components.schemas.putAll(subSchemas)
99+
}
100+
}
101+
102+
private fun makeSubSchema(schemas: MutableMap<String, Schema<Any>>, properties: Map<String, Schema<Any>>) {
103+
properties.asSequence().filter { it.value.title != null }.forEach {
104+
val objectMapper = jacksonObjectMapper()
105+
val subSchema = it.value
106+
val strSubSchema = objectMapper.writeValueAsString(subSchema)
107+
val copySchema = objectMapper.readValue(strSubSchema, subSchema.javaClass)
108+
val schemaTitle = copySchema.title
109+
subSchema.`$ref`("#/components/schemas/$schemaTitle")
110+
schemas[schemaTitle] = copySchema
111+
makeSubSchema(schemas, copySchema.properties)
112+
}
113+
}
114+
84115
fun generateAndSerialize(
85116
resources: List<ResourceModel>,
86117
servers: List<Server>,
@@ -132,6 +163,8 @@ object OpenApi3Generator {
132163
schemasToKeys.getValue(it) to it
133164
}.toMap()
134165
}
166+
167+
this.components
135168
}
136169

137170
private fun List<MediaType>.extractSchemas(
@@ -454,24 +487,28 @@ object OpenApi3Generator {
454487
.map { it as Boolean }
455488
.forEach { this.addEnumItem(it) }
456489
}
490+
457491
SimpleType.STRING.name.toLowerCase() -> StringSchema().apply {
458492
this._default(parameterDescriptor.defaultValue?.let { it as String })
459493
parameterDescriptor.attributes.enumValues
460494
.map { it as String }
461495
.forEach { this.addEnumItem(it) }
462496
}
497+
463498
SimpleType.NUMBER.name.toLowerCase() -> NumberSchema().apply {
464499
this._default(parameterDescriptor.defaultValue?.asBigDecimal())
465500
parameterDescriptor.attributes.enumValues
466501
.map { it.asBigDecimal() }
467502
.forEach { this.addEnumItem(it) }
468503
}
504+
469505
SimpleType.INTEGER.name.toLowerCase() -> IntegerSchema().apply {
470506
this._default(parameterDescriptor.defaultValue?.asInt())
471507
parameterDescriptor.attributes.enumValues
472508
.map { it.asInt() }
473509
.forEach { this.addEnumItem(it) }
474510
}
511+
475512
else -> throw IllegalArgumentException("Unknown type '${parameterDescriptor.type}'")
476513
}
477514
}

restdocs-api-spec-openapi3-generator/src/test/kotlin/com/epages/restdocs/apispec/openapi3/OpenApi3GeneratorTest.kt

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,17 @@ class OpenApi3GeneratorTest {
3030
lateinit var openApiSpecJsonString: String
3131
lateinit var openApiJsonPathContext: DocumentContext
3232

33+
@Test
34+
fun `should convert multi level schema model to openapi`() {
35+
givenPutProductResourceModel()
36+
37+
whenOpenApiObjectGenerated()
38+
39+
val optionDTOPath = "components.schemas.OptionDTO"
40+
then(openApiJsonPathContext.read<LinkedHashMap<String, Any>>("$optionDTOPath.properties.name")).isNotNull()
41+
then(openApiJsonPathContext.read<LinkedHashMap<String, Any>>("$optionDTOPath.properties.id")).isNotNull()
42+
}
43+
3344
@Test
3445
fun `should convert single resource model to openapi`() {
3546
givenGetProductResourceModel()
@@ -928,6 +939,21 @@ class OpenApi3GeneratorTest {
928939
)
929940
}
930941

942+
private fun givenPutProductResourceModel() {
943+
resources = listOf(
944+
ResourceModel(
945+
operationId = "test",
946+
summary = "summary",
947+
description = "description",
948+
privateResource = false,
949+
deprecated = false,
950+
tags = setOf("tag1", "tag2"),
951+
request = getProductPutRequest(),
952+
response = getProductPutResponse(Schema("ProductPutResponse"))
953+
)
954+
)
955+
}
956+
931957
private fun givenGetProductResourceModel() {
932958
resources = listOf(
933959
ResourceModel(
@@ -1054,6 +1080,54 @@ class OpenApi3GeneratorTest {
10541080
)
10551081
}
10561082

1083+
private fun getProductPutResponse(schema: Schema? = null): ResponseModel {
1084+
return ResponseModel(
1085+
status = 200,
1086+
contentType = "application/json",
1087+
schema = schema,
1088+
headers = listOf(
1089+
HeaderDescriptor(
1090+
name = "SIGNATURE",
1091+
description = "This is some signature",
1092+
type = "STRING",
1093+
optional = false
1094+
)
1095+
),
1096+
responseFields = listOf(
1097+
FieldDescriptor(
1098+
path = "id",
1099+
description = "product id",
1100+
type = "STRING"
1101+
),
1102+
FieldDescriptor(
1103+
path = "option",
1104+
description = "option",
1105+
type = "OBJECT",
1106+
attributes = Attributes(schemaName = "OptionDTO")
1107+
),
1108+
FieldDescriptor(
1109+
path = "option.id",
1110+
description = "option id",
1111+
type = "STRING"
1112+
),
1113+
FieldDescriptor(
1114+
path = "option.name",
1115+
description = "option name",
1116+
type = "STRING"
1117+
),
1118+
),
1119+
example = """
1120+
{
1121+
"id": "pid12312",
1122+
"option": {
1123+
"id": "otid00001",
1124+
"name": "Option name"
1125+
}
1126+
}
1127+
""".trimIndent(),
1128+
)
1129+
}
1130+
10571131
private fun getProductHalResponse(schema: Schema? = null): ResponseModel {
10581132
return ResponseModel(
10591133
status = 200,
@@ -1149,6 +1223,52 @@ class OpenApi3GeneratorTest {
11491223
)
11501224
}
11511225

1226+
private fun getProductPutRequest(): RequestModel {
1227+
return RequestModel(
1228+
path = "/products/{id}",
1229+
method = HTTPMethod.PUT,
1230+
headers = listOf(),
1231+
pathParameters = listOf(),
1232+
queryParameters = listOf(),
1233+
formParameters = listOf(),
1234+
securityRequirements = null,
1235+
requestFields = listOf(
1236+
FieldDescriptor(
1237+
path = "id",
1238+
description = "product id",
1239+
type = "STRING"
1240+
),
1241+
FieldDescriptor(
1242+
path = "option",
1243+
description = "option",
1244+
type = "OBJECT",
1245+
attributes = Attributes(schemaName = "OptionDTO")
1246+
),
1247+
FieldDescriptor(
1248+
path = "option.id",
1249+
description = "option id",
1250+
type = "STRING"
1251+
),
1252+
FieldDescriptor(
1253+
path = "option.name",
1254+
description = "option name",
1255+
type = "STRING"
1256+
),
1257+
),
1258+
contentType = "application/json",
1259+
example = """
1260+
{
1261+
"id": "pid12312",
1262+
"option": {
1263+
"id": "otid00001",
1264+
"name": "Option name"
1265+
}
1266+
}
1267+
""".trimIndent(),
1268+
schema = Schema("ProductPutRequest")
1269+
)
1270+
}
1271+
11521272
private fun getProductRequestWithMultiplePathParameters(getSecurityRequirement: () -> SecurityRequirements = ::getOAuth2SecurityRequirement): RequestModel {
11531273
return RequestModel(
11541274
path = "/products/{id}-{subId}",

0 commit comments

Comments
 (0)