Skip to content

Commit

Permalink
GH-213 Support examples in @OpenApiResponse (Resolve #213, resolve #212)
Browse files Browse the repository at this point in the history
  • Loading branch information
dzikoysk committed Feb 3, 2024
1 parent c86e9dc commit 9eb54b0
Show file tree
Hide file tree
Showing 5 changed files with 61 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -166,18 +166,21 @@ public static void main(String[] args) {
requestBody = @OpenApiRequestBody(
description = "Supports multiple request bodies",
content = {
@OpenApiContent(from = String.class), // simple type
@OpenApiContent(from = LombokEntity.class), // lombok
@OpenApiContent(from = KotlinEntity.class), // kotlin
@OpenApiContent(from = String.class, example = "value"), // simple type
@OpenApiContent(from = KotlinEntity.class, mimeType = "app/barbie", exampleObjects = {
@OpenApiExampleProperty(name = "name", value = "Margot Robbie")
}), // kotlin
@OpenApiContent(from = LombokEntity.class, mimeType = "app/lombok"), // lombok
@OpenApiContent(from = EntityWithGenericType.class), // generics
@OpenApiContent(from = RecordEntity.class), // record class
@OpenApiContent(from = DtoWithFields.class), // map only fields
@OpenApiContent(from = EnumEntity.class), // enum,
@OpenApiContent(from = CustomNameEntity.class) // custom name
@OpenApiContent(from = RecordEntity.class, mimeType = "app/record"), // record class
@OpenApiContent(from = DtoWithFields.class, mimeType = "app/dto"), // map only fields
@OpenApiContent(from = EnumEntity.class, mimeType = "app/enum"), // enum,
@OpenApiContent(from = CustomNameEntity.class, mimeType = "app/custom-name-entity") // custom name
}
),
responses = {
@OpenApiResponse(status = "200", description = "Status of the executed command", content = {
@OpenApiContent(from = String.class, example = "Value"),
@OpenApiContent(from = EntityDto[].class)
}),
@OpenApiResponse(
Expand All @@ -189,7 +192,9 @@ public static void main(String[] args) {
}
),
@OpenApiResponse(status = "401", description = "Error message related to the unauthorized access", content = {
@OpenApiContent(from = EntityDto[].class)
@OpenApiContent(from = EntityDto[].class, exampleObjects = {
@OpenApiExampleProperty(name = "error", value = "ERROR-CODE-401"),
})
}),
@OpenApiResponse(status = "500") // fill description with HttpStatus message
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import io.javalin.openapi.OpenApiResponse
import io.javalin.openapi.OpenApis
import io.javalin.openapi.experimental.ClassDefinition
import io.javalin.openapi.experimental.StructureType.ARRAY
import io.javalin.openapi.experimental.processor.generators.ExampleGenerator
import io.javalin.openapi.experimental.processor.generators.ExampleGenerator.toExampleProperty
import io.javalin.openapi.experimental.processor.shared.addIfNotEmpty
import io.javalin.openapi.experimental.processor.shared.addString
import io.javalin.openapi.experimental.processor.shared.computeIfAbsent
Expand Down Expand Up @@ -353,8 +355,11 @@ internal class OpenApiGenerator {
.map { pathPart ->
if (pathPart.startsWith('{') or pathPart.startsWith('<')) {
/* Case this is a path parameter */
val pathParam = pathPart.drop(1).dropLast(1)
.split('-').joinToString(separator = ""){it.capitalise()}
val pathParam = pathPart
.drop(1)
.dropLast(1)
.split('-')
.joinToString(separator = "") { it.capitalise() }
pathParamPrefix + pathParam
} else {
/* Case this is a regular part of the path */
Expand Down Expand Up @@ -386,6 +391,8 @@ internal class OpenApiGenerator {
val properties = contentAnnotation.properties
var type = contentAnnotation.type.takeIf { it != NULL_STRING }
var mimeType = contentAnnotation.mimeType.takeIf { it != AUTODETECT }
val example = contentAnnotation.example.takeIf { it != NULL_STRING }
val exampleObjects = contentAnnotation.exampleObjects.map { it.toExampleProperty() }

if (mimeType == null) {
when (NULL_CLASS::class.qualifiedName) {
Expand Down Expand Up @@ -466,6 +473,19 @@ internal class OpenApiGenerator {
mediaType.add("schema", schema)
}

if (example != null) {
mediaType.addProperty("example", example)
}

if (exampleObjects.isNotEmpty()) {
val generatorResult = ExampleGenerator.generateFromExamples(exampleObjects)

when {
generatorResult.simpleValue != null -> mediaType.addProperty("example", generatorResult.simpleValue)
generatorResult.jsonElement != null -> mediaType.add("example", generatorResult.jsonElement)
}
}

requestBodySchemes[mimeType] = mediaType
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,9 @@ annotation class OpenApiContent(
val mimeType: String = ContentType.AUTODETECT,
val type: String = NULL_STRING,
val format: String = NULL_STRING,
val properties: Array<OpenApiContentProperty> = []
val properties: Array<OpenApiContentProperty> = [],
val example: String = NULL_STRING,
val exampleObjects: Array<OpenApiExampleProperty> = [],
)

@Target()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,18 +8,25 @@ import io.javalin.openapi.OpenApiExampleProperty

object ExampleGenerator {

data class ExampleProperty(
val name: String?,
val value: String?,
val objects: List<ExampleProperty>?
)

fun OpenApiExampleProperty.toExampleProperty(): ExampleProperty =
ExampleProperty(this.name, this.value, this.objects.map { it.toExampleProperty() })

data class GeneratorResult(val simpleValue: String?, val jsonElement: JsonElement?) {
init {
if (simpleValue != null && jsonElement != null) {
throw IllegalArgumentException("rawList and jsonElement cannot be both non-null")
}
if (simpleValue == null && jsonElement == null) {
throw IllegalArgumentException("rawList and jsonElement cannot be both null")
when {
simpleValue != null && jsonElement != null -> throw IllegalArgumentException("rawList and jsonElement cannot be both non-null")
simpleValue == null && jsonElement == null -> throw IllegalArgumentException("rawList and jsonElement cannot be both null")
}
}
}

fun generateFromExamples(examples: Array<OpenApiExampleProperty>): GeneratorResult {
fun generateFromExamples(examples: List<ExampleProperty>): GeneratorResult {
if (examples.isRawList()) {
val jsonArray = JsonArray()
examples.forEach { jsonArray.add(it.value) }
Expand All @@ -35,14 +42,14 @@ object ExampleGenerator {
return GeneratorResult(null, examples.toJsonObject())
}

private fun OpenApiExampleProperty.toSimpleExampleValue(): GeneratorResult =
private fun ExampleProperty.toSimpleExampleValue(): GeneratorResult =
when {
this.value != NULL_STRING -> GeneratorResult(this.value, null)
this.objects.isNotEmpty() -> GeneratorResult(null, objects.toJsonObject())
this.objects?.isNotEmpty() == true-> GeneratorResult(null, objects.toJsonObject())
else -> throw IllegalArgumentException("Example object must have either value or objects ($this)")
}

private fun Array<OpenApiExampleProperty>.toJsonObject(): JsonObject {
private fun List<ExampleProperty>.toJsonObject(): JsonObject {
val jsonObject = JsonObject()
this.forEach {
val result = it.toSimpleExampleValue()
Expand All @@ -57,10 +64,10 @@ object ExampleGenerator {
return jsonObject
}

private fun Array<OpenApiExampleProperty>.isObjectList(): Boolean =
this.isNotEmpty() && this.all { it.name == NULL_STRING && it.value == NULL_STRING && it.objects.isNotEmpty() }
private fun List<ExampleProperty>.isObjectList(): Boolean =
this.isNotEmpty() && this.all { it.name == NULL_STRING && it.value == NULL_STRING && it.objects?.isNotEmpty() ?: false }

private fun Array<OpenApiExampleProperty>.isRawList(): Boolean =
this.isNotEmpty() && this.all { it.name == NULL_STRING && it.value != NULL_STRING && it.objects.isEmpty() }
private fun List<ExampleProperty>.isRawList(): Boolean =
this.isNotEmpty() && this.all { it.name == NULL_STRING && it.value != NULL_STRING && it.objects?.isEmpty() ?: true }

}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ import io.javalin.openapi.experimental.AnnotationProcessorContext
import io.javalin.openapi.experimental.ClassDefinition
import io.javalin.openapi.experimental.CustomProperty
import io.javalin.openapi.experimental.EmbeddedTypeProcessorContext
import io.javalin.openapi.experimental.processor.generators.ExampleGenerator.ExampleProperty
import io.javalin.openapi.experimental.processor.generators.ExampleGenerator.toExampleProperty
import io.javalin.openapi.experimental.processor.shared.MessagerWriter
import io.javalin.openapi.experimental.processor.shared.getTypeMirror
import io.javalin.openapi.experimental.processor.shared.hasAnnotation
Expand Down Expand Up @@ -311,7 +313,7 @@ private fun Element.findExtra(context: AnnotationProcessorContext): Map<String,
extra["example"] = example.value
}
example.objects.isNotEmpty() -> {
val result = ExampleGenerator.generateFromExamples(example.objects)
val result = ExampleGenerator.generateFromExamples(example.objects.map { it.toExampleProperty() })
extra["example"] = result.jsonElement ?: result.simpleValue
}
}
Expand Down

0 comments on commit 9eb54b0

Please sign in to comment.