diff --git a/annotations/src/main/kotlin/io/github/tabilzad/ktor/annotations/Annotations.kt b/annotations/src/main/kotlin/io/github/tabilzad/ktor/annotations/Annotations.kt index 2fb39b6..e48e55e 100644 --- a/annotations/src/main/kotlin/io/github/tabilzad/ktor/annotations/Annotations.kt +++ b/annotations/src/main/kotlin/io/github/tabilzad/ktor/annotations/Annotations.kt @@ -47,3 +47,10 @@ annotation class KtorFieldDescription( val description: String = "", val required: Boolean = false ) + +@Retention(AnnotationRetention.SOURCE) +@Target(AnnotationTarget.FIELD) +annotation class OpenApiProperty( + val type : String = "", + val format: String = "" +) diff --git a/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/k2/visitors/ClassDescriptorVisitorK2.kt b/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/k2/visitors/ClassDescriptorVisitorK2.kt index 4bc7591..17dfb73 100644 --- a/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/k2/visitors/ClassDescriptorVisitorK2.kt +++ b/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/k2/visitors/ClassDescriptorVisitorK2.kt @@ -3,6 +3,7 @@ package io.github.tabilzad.ktor.k2.visitors import io.github.tabilzad.ktor.PluginConfiguration import io.github.tabilzad.ktor.annotations.KtorDescription import io.github.tabilzad.ktor.annotations.KtorFieldDescription +import io.github.tabilzad.ktor.annotations.OpenApiProperty import io.github.tabilzad.ktor.getKDocComments import io.github.tabilzad.ktor.k1.visitors.KtorDescriptionBag import io.github.tabilzad.ktor.k1.visitors.toSwaggerType @@ -44,7 +45,7 @@ internal class ClassDescriptorVisitorK2( ) : FirDefaultVisitor() { - @OptIn(SealedClassInheritorsProviderInternals::class, SymbolInternals::class) + @OptIn(SealedClassInheritorsProviderInternals::class, SymbolInternals::class, PrivateForInline::class) override fun visitProperty(property: FirProperty, data: ObjectType): ObjectType { val coneTypeOrNull = property.returnTypeRef.coneTypeOrNull!! val type = if (coneTypeOrNull is ConeTypeParameterType && genericParameters.isNotEmpty()) { @@ -74,6 +75,21 @@ internal class ClassDescriptorVisitorK2( data } + property.findAnnotation(OpenApiProperty::class.simpleName) != null -> { + val formatAnnotation = property.findAnnotation(OpenApiProperty::class.simpleName) + val resolved = formatAnnotation?.let { FirExpressionEvaluator.evaluateAnnotationArguments(it, session) } + val dataType = resolved?.entries?.find { it.key.asString() == "type" }?.value?.result + val format = resolved?.entries?.find { it.key.asString() == "format" }?.value?.result + data.addProperty( + property, + objectType = ObjectType( + type = dataType?.accept(StringResolutionVisitor(), ""), + format = format?.accept(StringResolutionVisitor(), "") + ), session + ) + data + } + else -> { val fqClassName = type.fqNameStr() @@ -105,6 +121,12 @@ internal class ClassDescriptorVisitorK2( ) } + valueClassType.isMap -> { + acc.type = "object" + acc.additionalProperties = valueClassType.resolveItems() + } + + valueClassType.isAny -> { acc.type = "object" } @@ -298,6 +320,11 @@ internal class ClassDescriptorVisitorK2( ObjectType("string").apply { enum = typeSymbol?.resolveEnumEntries() } + } else if (type.isMap() && this.typeArguments.firstOrNull()?.type?.isString == true) { + ObjectType( + "object", + additionalProperties = this.typeArguments.lastOrNull()?.type?.resolveItems() + ) } else { if (!classNames.names.contains(jetTypeFqName)) { diff --git a/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/output/OpenApiSpec.kt b/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/output/OpenApiSpec.kt index 3479463..22c2345 100644 --- a/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/output/OpenApiSpec.kt +++ b/create-plugin/src/main/kotlin/io/github/tabilzad/ktor/output/OpenApiSpec.kt @@ -45,6 +45,7 @@ data class OpenApiSpec( var type: String?, var properties: MutableMap? = null, var items: ObjectType? = null, + var format: String? = null, var enum: List? = null, @JsonIgnore override var fqName: String? = null, diff --git a/create-plugin/src/test/resources/expected/RequestBody-expected.json b/create-plugin/src/test/resources/expected/RequestBody-expected.json index 3da2af6..f41ea76 100644 --- a/create-plugin/src/test/resources/expected/RequestBody-expected.json +++ b/create-plugin/src/test/resources/expected/RequestBody-expected.json @@ -75,6 +75,20 @@ } } } + }, + "/v2/postInstantRequest" : { + "post" : { + "requestBody" : { + "required" : true, + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/sources.requests.InstantRequest" + } + } + } + } + } } }, "components" : { @@ -128,6 +142,16 @@ } } }, + "complexMapValueMap" : { + "type" : "object", + "properties" : { }, + "additionalProperties" : { + "type" : "object", + "additionalProperties" : { + "$ref" : "#/components/schemas/sources.requests.ComplexMapValue" + } + } + }, "complexNestedList" : { "type" : "array", "items" : { @@ -213,7 +237,21 @@ } } }, - "required" : [ "list", "nestedList", "nestedMutableList", "complexList", "complexNestedList", "complexListStringMap", "complexListMap", "complexNestedListMap", "stringMap", "intValueMap", "complexValueMap", "enumValueMap", "complexEnumValueMap" ] + "required" : [ "list", "nestedList", "nestedMutableList", "complexList", "complexNestedList", "complexListStringMap", "complexListMap", "complexNestedListMap", "stringMap", "intValueMap", "complexValueMap", "enumValueMap", "complexEnumValueMap", "complexMapValueMap" ] + }, + "sources.requests.InstantRequest" : { + "type" : "object", + "properties" : { + "date" : { + "type" : "string", + "format" : "iso 8601" + }, + "formattedInstant" : { + "type" : "string", + "format" : "date-time" + } + }, + "required" : [ "date", "formattedInstant" ] }, "sources.requests.NestedRequest" : { "type" : "object", diff --git a/create-plugin/src/test/resources/sources/RequestBody.kt b/create-plugin/src/test/resources/sources/RequestBody.kt index 57192cb..abeaf7d 100644 --- a/create-plugin/src/test/resources/sources/RequestBody.kt +++ b/create-plugin/src/test/resources/sources/RequestBody.kt @@ -5,6 +5,7 @@ import io.ktor.server.application.* import io.ktor.server.request.* import io.ktor.server.routing.* import sources.requests.ComplexRequest +import sources.requests.InstantRequest import sources.requests.NestedRequest import sources.requests.SimpleRequest @@ -33,6 +34,10 @@ fun Application.requestBodyTest() { post("/postBodyComplexRequest") { call.receive() } + + post("/postInstantRequest") { + call.receive() + } } } } diff --git a/create-plugin/src/test/resources/sources/requests/RequestDataClasses.kt b/create-plugin/src/test/resources/sources/requests/RequestDataClasses.kt index f86b97a..673a33b 100644 --- a/create-plugin/src/test/resources/sources/requests/RequestDataClasses.kt +++ b/create-plugin/src/test/resources/sources/requests/RequestDataClasses.kt @@ -1,5 +1,8 @@ package sources.requests +import io.github.tabilzad.ktor.annotations.OpenApiProperty +import java.time.Instant + data class SimpleRequest( val string: String, val integer: Int, @@ -32,6 +35,7 @@ data class ComplexRequest( val complexValueMap: Map, val enumValueMap: Map, val complexEnumValueMap: Map>, + val complexMapValueMap: Map> ) data class ComplexMapValue( @@ -42,6 +46,8 @@ data class ComplexMapKey( val something: Int ) +data class InstantRequest(@OpenApiProperty(type = "string", format = "iso 8601") val date: Instant, @OpenApiProperty(type = "string", format = "date-time") val formattedInstant: Instant) + enum class MyEnum { ONE, TWO,