Skip to content
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

Feat : Schema reuse through subschema #246

Merged
merged 5 commits into from
Sep 25, 2023
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 @@ -168,11 +168,13 @@ class JsonSchemaFromFieldDescriptorsGenerator {
.build()
)
} else {
val schemaName = propertyField?.fieldDescriptor?.attributes?.schemaName
builder.addPropertySchema(
propertyName,
traverse(
traversedSegments, fields,
ObjectSchema.builder()
.title(schemaName)
.description(propertyField?.fieldDescriptor?.description) as ObjectSchema.Builder
)
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,22 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {

private var schemaString: String? = null

@Test
@Throws(IOException::class)
fun should_generate_reuse_schema() {
givenFieldDescriptorsWithSchemaName()

whenSchemaGenerated()

then(schema).isInstanceOf(ObjectSchema::class.java)
val objectSchema = schema as ObjectSchema?
val postSchema = objectSchema?.propertySchemas?.get("post") as ObjectSchema
val shippingAddressSchema = postSchema.propertySchemas["shippingAddress"] as ObjectSchema
then(shippingAddressSchema.title).isEqualTo("Address")
val billingAddressSchema = postSchema.propertySchemas["billingAddress"] as ObjectSchema
then(billingAddressSchema.title).isEqualTo("Address")
}

@Test
@Throws(IOException::class)
fun should_generate_complex_schema() {
Expand Down Expand Up @@ -789,6 +805,23 @@ class JsonSchemaFromFieldDescriptorsGeneratorTest {
)
}

private fun givenFieldDescriptorsWithSchemaName() {

fieldDescriptors = listOf(
FieldDescriptor(
"post",
"some",
"OBJECT",
),
FieldDescriptor("post.shippingAddress", "some", "OBJECT", attributes = Attributes(schemaName = "Address")),
FieldDescriptor("post.shippingAddress.firstName", "some", "STRING"),
FieldDescriptor("post.shippingAddress.valid", "some", "BOOLEAN"),
FieldDescriptor("post.billingAddress", "some", "OBJECT", attributes = Attributes(schemaName = "Address")),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Marking this for future reference on how to use the feature.

FieldDescriptor("post.billingAddress.firstName", "some", "STRING"),
FieldDescriptor("post.billingAddress.valid", "some", "BOOLEAN"),
)
}

private fun thenSchemaValidatesJson(json: String) {
schema!!.validate(if (json.startsWith("[")) JSONArray(json) else JSONObject(json))
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,8 @@ open class FieldDescriptor(
data class Attributes(
val validationConstraints: List<Constraint> = emptyList(),
val enumValues: List<Any> = emptyList(),
val itemsType: String? = null
val itemsType: String? = null,
val schemaName: String? = null,
)

data class Constraint(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import com.epages.restdocs.apispec.model.SimpleType
import com.epages.restdocs.apispec.model.groupByPath
import com.epages.restdocs.apispec.openapi3.SecuritySchemeGenerator.addSecurityDefinitions
import com.epages.restdocs.apispec.openapi3.SecuritySchemeGenerator.addSecurityItemFromSecurityRequirements
import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper
import com.fasterxml.jackson.module.kotlin.readValue
import io.swagger.v3.core.util.Json
import io.swagger.v3.oas.models.Components
Expand Down Expand Up @@ -76,11 +77,41 @@ object OpenApi3Generator {
resources,
oauth2SecuritySchemeDefinition
)

extractDefinitions()
makeSubSchema()
addSecurityDefinitions(oauth2SecuritySchemeDefinition)
}
}

private fun OpenAPI.makeSubSchema() {
val schemas = this.components.schemas
val subSchemas = mutableMapOf<String, Schema<Any>>()
schemas.forEach {
val schema = it.value
if (schema.properties != null) {
makeSubSchema(subSchemas, schema.properties)
}
}

if (subSchemas.isNotEmpty()) {
this.components.schemas.putAll(subSchemas)
}
}

private fun makeSubSchema(schemas: MutableMap<String, Schema<Any>>, properties: Map<String, Schema<Any>>) {
properties.asSequence().filter { it.value.title != null }.forEach {
val objectMapper = jacksonObjectMapper()
val subSchema = it.value
val strSubSchema = objectMapper.writeValueAsString(subSchema)
val copySchema = objectMapper.readValue(strSubSchema, subSchema.javaClass)
val schemaTitle = copySchema.title
subSchema.`$ref`("#/components/schemas/$schemaTitle")
schemas[schemaTitle] = copySchema
makeSubSchema(schemas, copySchema.properties)
}
}

fun generateAndSerialize(
resources: List<ResourceModel>,
servers: List<Server>,
Expand Down Expand Up @@ -132,6 +163,8 @@ object OpenApi3Generator {
schemasToKeys.getValue(it) to it
}.toMap()
}

this.components
}

private fun List<MediaType>.extractSchemas(
Expand Down Expand Up @@ -453,24 +486,28 @@ object OpenApi3Generator {
.map { it as Boolean }
.forEach { this.addEnumItem(it) }
}

SimpleType.STRING.name.toLowerCase() -> StringSchema().apply {
this._default(parameterDescriptor.defaultValue?.let { it as String })
parameterDescriptor.attributes.enumValues
.map { it as String }
.forEach { this.addEnumItem(it) }
}

SimpleType.NUMBER.name.toLowerCase() -> NumberSchema().apply {
this._default(parameterDescriptor.defaultValue?.asBigDecimal())
parameterDescriptor.attributes.enumValues
.map { it.asBigDecimal() }
.forEach { this.addEnumItem(it) }
}

SimpleType.INTEGER.name.toLowerCase() -> IntegerSchema().apply {
this._default(parameterDescriptor.defaultValue?.asInt())
parameterDescriptor.attributes.enumValues
.map { it.asInt() }
.forEach { this.addEnumItem(it) }
}

else -> throw IllegalArgumentException("Unknown type '${parameterDescriptor.type}'")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,17 @@ class OpenApi3GeneratorTest {
lateinit var openApiSpecJsonString: String
lateinit var openApiJsonPathContext: DocumentContext

@Test
fun `should convert multi level schema model to openapi`() {
givenPutProductResourceModel()

whenOpenApiObjectGenerated()
ozscheyge marked this conversation as resolved.
Show resolved Hide resolved

val optionDTOPath = "components.schemas.OptionDTO"
then(openApiJsonPathContext.read<LinkedHashMap<String, Any>>("$optionDTOPath.properties.name")).isNotNull()
then(openApiJsonPathContext.read<LinkedHashMap<String, Any>>("$optionDTOPath.properties.id")).isNotNull()
}

@Test
fun `should convert single resource model to openapi`() {
givenGetProductResourceModel()
Expand Down Expand Up @@ -929,6 +940,21 @@ class OpenApi3GeneratorTest {
)
}

private fun givenPutProductResourceModel() {
resources = listOf(
ResourceModel(
operationId = "test",
summary = "summary",
description = "description",
privateResource = false,
deprecated = false,
tags = setOf("tag1", "tag2"),
request = getProductPutRequest(),
response = getProductPutResponse(Schema("ProductPutResponse"))
)
)
}

private fun givenGetProductResourceModel() {
resources = listOf(
ResourceModel(
Expand Down Expand Up @@ -1055,6 +1081,54 @@ class OpenApi3GeneratorTest {
)
}

private fun getProductPutResponse(schema: Schema? = null): ResponseModel {
return ResponseModel(
status = 200,
contentType = "application/json",
schema = schema,
headers = listOf(
HeaderDescriptor(
name = "SIGNATURE",
description = "This is some signature",
type = "STRING",
optional = false
)
),
responseFields = listOf(
FieldDescriptor(
path = "id",
description = "product id",
type = "STRING"
),
FieldDescriptor(
path = "option",
description = "option",
type = "OBJECT",
attributes = Attributes(schemaName = "OptionDTO")
),
FieldDescriptor(
path = "option.id",
description = "option id",
type = "STRING"
),
FieldDescriptor(
path = "option.name",
description = "option name",
type = "STRING"
),
),
example = """
{
"id": "pid12312",
"option": {
"id": "otid00001",
"name": "Option name"
}
}
""".trimIndent(),
)
}

private fun getProductHalResponse(schema: Schema? = null): ResponseModel {
return ResponseModel(
status = 200,
Expand Down Expand Up @@ -1152,6 +1226,52 @@ class OpenApi3GeneratorTest {
)
}

private fun getProductPutRequest(): RequestModel {
return RequestModel(
path = "/products/{id}",
method = HTTPMethod.PUT,
headers = listOf(),
pathParameters = listOf(),
queryParameters = listOf(),
formParameters = listOf(),
securityRequirements = null,
requestFields = listOf(
FieldDescriptor(
path = "id",
description = "product id",
type = "STRING"
),
FieldDescriptor(
path = "option",
description = "option",
type = "OBJECT",
attributes = Attributes(schemaName = "OptionDTO")
),
FieldDescriptor(
path = "option.id",
description = "option id",
type = "STRING"
),
FieldDescriptor(
path = "option.name",
description = "option name",
type = "STRING"
),
),
contentType = "application/json",
example = """
{
"id": "pid12312",
"option": {
"id": "otid00001",
"name": "Option name"
}
}
""".trimIndent(),
schema = Schema("ProductPutRequest")
)
}

private fun getProductRequestWithMultiplePathParameters(getSecurityRequirement: () -> SecurityRequirements = ::getOAuth2SecurityRequirement): RequestModel {
return RequestModel(
path = "/products/{id}-{subId}",
Expand Down