From 1ec6463e8ae1c2205b449983497aa790f061a6e6 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Wed, 5 Feb 2025 23:02:50 +0100 Subject: [PATCH 1/2] misc schema generation config improvements --- .../smiley4/ktoropenapi/examples/Schemas.kt | 27 +- .../config/SchemaGenerationModule.kt | 103 ++++++++ .../ktoropenapi/config/SchemaGenerator.kt | 244 +++++++++++++++++- pending-changelog.txt | 27 +- 4 files changed, 369 insertions(+), 32 deletions(-) create mode 100644 ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt diff --git a/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Schemas.kt b/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Schemas.kt index 2261b72..d35a327 100644 --- a/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Schemas.kt +++ b/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/Schemas.kt @@ -2,6 +2,7 @@ package io.github.smiley4.ktoropenapi.examples import com.fasterxml.jackson.annotation.JsonSubTypes import io.github.smiley4.ktoropenapi.OpenApi +import io.github.smiley4.ktoropenapi.config.SchemaGenerator import io.github.smiley4.ktoropenapi.config.anyOf import io.github.smiley4.ktoropenapi.config.array import io.github.smiley4.ktoropenapi.config.ref @@ -9,13 +10,6 @@ import io.github.smiley4.ktoropenapi.get import io.github.smiley4.ktoropenapi.openApi import io.github.smiley4.ktorredoc.redoc import io.github.smiley4.ktorswaggerui.swaggerUI -import io.github.smiley4.schemakenerator.core.addMissingSupertypeSubtypeRelations -import io.github.smiley4.schemakenerator.jackson.collectJacksonSubTypes -import io.github.smiley4.schemakenerator.reflection.analyseTypeUsingReflection -import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot -import io.github.smiley4.schemakenerator.swagger.data.TitleType -import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema -import io.github.smiley4.schemakenerator.swagger.withTitle import io.ktor.server.application.Application import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer @@ -45,21 +39,10 @@ private fun Application.myModule() { // add a type to the component section of the api spec with the id "type-schema" schema("type-schema") - // overwrite 'LocalDateTime' with custom schema (root only) - overwrite(Schema().also { - it.title = "timestamp" - it.type = "integer" - }) - - // customized schema generation pipeline - generator = { type -> - type - .collectJacksonSubTypes(typeProcessing = { types -> types.analyseTypeUsingReflection() }) // include types from jackson subtype-annotation - .analyseTypeUsingReflection() - .addMissingSupertypeSubtypeRelations() - .generateSwaggerSchema() - .withTitle(TitleType.SIMPLE) - .compileReferencingRoot() + // customized schema generation + generator = SchemaGenerator.reflection { + // overwrite default schema generation with one specific for LocalDateTime that correctly handles "type" and "format" + overwrite(SchemaGenerator.TypeOverwrites.LocalDateTime()) } } diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt new file mode 100644 index 0000000..edbccfc --- /dev/null +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt @@ -0,0 +1,103 @@ +@file:OptIn(ExperimentalSerializationApi::class) + +package io.github.smiley4.ktoropenapi.config + +import io.github.smiley4.schemakenerator.core.data.TypeData +import io.github.smiley4.schemakenerator.core.data.TypeName +import io.github.smiley4.schemakenerator.core.data.WrappedTypeData +import io.github.smiley4.schemakenerator.reflection.analyzer.MinimalTypeData +import io.github.smiley4.schemakenerator.reflection.analyzer.ReflectionTypeAnalyzerModule +import io.github.smiley4.schemakenerator.serialization.analyzer.SerializationTypeAnalyzerModule +import io.github.smiley4.schemakenerator.serialization.analyzer.fullName +import io.github.smiley4.schemakenerator.swagger.data.SwaggerSchema +import io.github.smiley4.schemakenerator.swagger.generator.SwaggerSchemaGenerationModule +import io.swagger.v3.oas.models.media.Schema +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nonNullOriginal +import kotlin.reflect.KClass +import kotlin.reflect.KType + +open class BasicSchemaOverwriteModule( + val identifier: String, + val schema: () -> Schema<*> +) : ReflectionTypeAnalyzerModule, SerializationTypeAnalyzerModule, SwaggerSchemaGenerationModule { + + override fun applies(type: KType, clazz: KClass<*>): Boolean { + return (clazz.qualifiedName ?: clazz.java.name) == identifier + } + + override fun applies(descriptor: SerialDescriptor): Boolean { + return descriptor.nonNullOriginal.serialName == identifier + } + + override fun preAnalyze(context: ReflectionTypeAnalyzerModule.Context): MinimalTypeData { + return MinimalTypeData( + identifyingName = context.clazz.toTypeName(), + descriptiveName = context.clazz.toTypeName(), + typeParameters = mutableListOf() + ) + } + + override fun analyze(context: ReflectionTypeAnalyzerModule.Context, minimalTypeData: MinimalTypeData): WrappedTypeData { + return WrappedTypeData( + typeData = TypeData( + id = context.id, + identifyingName = context.clazz.toTypeName(), + descriptiveName = context.clazz.toTypeName(), + typeParameters = mutableListOf(), + annotations = mutableListOf(), + subtypes = mutableListOf(), + supertypes = mutableListOf(), + members = mutableListOf(), + isInlineValue = false, + enumData = null, + collectionData = null, + mapData = null, + ), + nullable = context.type.isMarkedNullable, + ) + } + + override fun analyze(context: SerializationTypeAnalyzerModule.Context): WrappedTypeData { + return WrappedTypeData( + typeData = TypeData( + id = context.id, + identifyingName = context.descriptor.toTypeName(), + descriptiveName = context.descriptor.toTypeName(), + typeParameters = mutableListOf(), + annotations = mutableListOf(), + subtypes = mutableListOf(), + supertypes = mutableListOf(), + members = mutableListOf(), + isInlineValue = false, + enumData = null, + collectionData = null, + mapData = null + ), + nullable = context.nullable || context.descriptor.isNullable, + ) + } + + override fun applies(typeData: TypeData): Boolean { + return typeData.identifyingName.full == identifier + } + + override fun generate(context: SwaggerSchemaGenerationModule.Context): SwaggerSchema { + return SwaggerSchema( + typeData = context.typeData, + swagger = schema() + ) + } + + private fun KClass<*>.toTypeName() = TypeName( + full = this.qualifiedName ?: this.java.name, + short = this.simpleName ?: this.java.name + ) + + private fun SerialDescriptor.toTypeName() = TypeName( + full = this.fullName(), + short = this.serialName.split(".").last().replace("?", "") + ) + +} \ No newline at end of file diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt index 54528aa..299446b 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt @@ -1,3 +1,5 @@ +@file:OptIn(ExperimentalUuidApi::class) + package io.github.smiley4.ktoropenapi.config import io.github.smiley4.schemakenerator.core.addDiscriminatorProperty @@ -5,26 +7,39 @@ import io.github.smiley4.schemakenerator.core.addMissingSupertypeSubtypeRelation import io.github.smiley4.schemakenerator.core.data.InputType import io.github.smiley4.schemakenerator.core.handleNameAnnotation import io.github.smiley4.schemakenerator.reflection.analyseTypeUsingReflection +import io.github.smiley4.schemakenerator.reflection.analyzer.ReflectionCustomProvider +import io.github.smiley4.schemakenerator.reflection.analyzer.ReflectionTypeAnalyzerModule +import io.github.smiley4.schemakenerator.reflection.analyzer.ReflectionTypeMatcher +import io.github.smiley4.schemakenerator.reflection.analyzer.SimpleTypeAnalyzerModule import io.github.smiley4.schemakenerator.reflection.analyzer.TypeCategoryAnalyzer.Companion.DEFAULT_PRIMITIVE_TYPES import io.github.smiley4.schemakenerator.reflection.collectSubTypes import io.github.smiley4.schemakenerator.reflection.data.EnumConstType import io.github.smiley4.schemakenerator.serialization.addJsonClassDiscriminatorProperty import io.github.smiley4.schemakenerator.serialization.analyzeTypeUsingKotlinxSerialization +import io.github.smiley4.schemakenerator.serialization.analyzer.KotlinxSerializationCustomProvider +import io.github.smiley4.schemakenerator.serialization.analyzer.KotlinxSerializationTypeMatcher +import io.github.smiley4.schemakenerator.serialization.analyzer.SerializationTypeAnalyzerModule +import io.github.smiley4.schemakenerator.serialization.analyzer.SimpleSerializationTypeAnalyzerModule +import io.github.smiley4.schemakenerator.serialization.analyzer.fullName import io.github.smiley4.schemakenerator.swagger.RequiredHandling import io.github.smiley4.schemakenerator.swagger.compileReferencingRoot import io.github.smiley4.schemakenerator.swagger.data.CompiledSwaggerSchema import io.github.smiley4.schemakenerator.swagger.data.RefType import io.github.smiley4.schemakenerator.swagger.data.TitleType import io.github.smiley4.schemakenerator.swagger.generateSwaggerSchema +import io.github.smiley4.schemakenerator.swagger.generator.SwaggerSchemaGenerationModule import io.github.smiley4.schemakenerator.swagger.handleCoreAnnotations import io.github.smiley4.schemakenerator.swagger.handleSchemaAnnotations import io.github.smiley4.schemakenerator.swagger.mergePropertyAttributesIntoType import io.github.smiley4.schemakenerator.swagger.withTitle +import io.swagger.v3.oas.models.media.Schema +import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.json.Json import kotlinx.serialization.modules.SerializersModule import kotlin.reflect.KClass import kotlin.reflect.KType import kotlin.reflect.typeOf +import kotlin.uuid.ExperimentalUuidApi /** * Function to generate swagger schemas for any given type @@ -49,6 +64,7 @@ object SchemaGenerator { includeStatic = configInstance.includeStatic primitiveTypes = configInstance.primitiveTypes enumConstType = configInstance.enumConstType + modules.addAll(configInstance.analyzerModules) } .addMissingSupertypeSubtypeRelations() .handleNameAnnotation() @@ -59,6 +75,7 @@ object SchemaGenerator { .generateSwaggerSchema { optionals = configInstance.optionals nullables = configInstance.nullables + customModules.addAll(configInstance.generationModules) } .handleCoreAnnotations() .handleSchemaAnnotations() @@ -81,31 +98,37 @@ object SchemaGenerator { */ var includeGetters: Boolean = false + /** * Whether to include weak getters as members of classes (see [io.github.smiley4.schemakenerator.core.data.MemberKind.WEAK_GETTER]). */ var includeWeakGetters: Boolean = false + /** * Whether to include functions as members of classes (see [io.github.smiley4.schemakenerator.core.data.MemberKind.FUNCTION]). */ var includeFunctions: Boolean = false + /** * Whether to include hidden (e.g. private) members */ var includeHidden: Boolean = false + /** * Whether to include static members */ var includeStatic: Boolean = false + /** * The list of types that are considered "primitive types" */ var primitiveTypes: MutableSet> = DEFAULT_PRIMITIVE_TYPES.toMutableSet() + /** * Whether to use "toString" for enum values or the declared "name" */ @@ -129,6 +152,7 @@ object SchemaGenerator { */ var nullables: RequiredHandling = RequiredHandling.NON_REQUIRED + /** * Whether to explicitly include "null" types for nullable properties. */ @@ -140,11 +164,80 @@ object SchemaGenerator { */ var title: TitleType? = TitleType.SIMPLE + /** * The format of the reference paths. */ var referencePath: RefType = RefType.OPENAPI_FULL + + /** + * List of additional/custom [ReflectionTypeAnalyzerModule] to use for analysis. + */ + var analyzerModules = mutableListOf() + + + /** + * Adds a new [ReflectionTypeAnalyzerModule]. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(module: ReflectionTypeAnalyzerModule) { + analyzerModules.add(module) + } + + + /** + * Add a new custom type for types matched by the given matcher. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(matcher: ReflectionTypeMatcher, provider: ReflectionCustomProvider) { + analyzerModules.add(SimpleTypeAnalyzerModule(matcher, provider)) + } + + + /** + * Add a custom type overwriting the given type. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(clazz: KClass<*>, provider: ReflectionCustomProvider) { + customAnalyzer( + { _: KType, c: KClass<*> -> c == clazz }, + provider + ) + } + + + /** + * Add a custom type overwriting the given type. + * Modules overwrite previous modules when matching the same type. + */ + inline fun customAnalyzer(noinline provider: ReflectionCustomProvider) { + customAnalyzer(typeOf().classifier!! as KClass<*>, provider) + } + + + /** + * List of additional/custom [ReflectionTypeAnalyzerModule] to use for schema generation. + */ + val generationModules = mutableListOf() + + + /** + * Add a custom schema generation module. + */ + fun customGenerator(module: SwaggerSchemaGenerationModule) { + generationModules.add(module) + } + + + /** + * Specify the schema for the matching type. Overwrites default schema generation + */ + fun overwrite(module: BasicSchemaOverwriteModule) { + analyzerModules.add(module) + generationModules.add(module) + } + } @@ -153,19 +246,21 @@ object SchemaGenerator { */ fun kotlinx(json: Json? = null, config: KotlinxSerializationConfig.() -> Unit = {}): GenericSchemaGenerator { val configInstance = KotlinxSerializationConfig() - .apply { if(json != null) useKotlinxConfig(json) } + .apply { if (json != null) useKotlinxConfig(json) } .apply(config) return { type -> type .analyzeTypeUsingKotlinxSerialization { serializersModule = configInstance.serializersModule knownNotParameterized = configInstance.knownNotParameterized + customModules.addAll(configInstance.analyzerModules) } .addJsonClassDiscriminatorProperty() .handleNameAnnotation() .generateSwaggerSchema { optionals = configInstance.optionals nullables = configInstance.nullables + customModules.addAll(configInstance.generationModules) } .handleCoreAnnotations() .handleSchemaAnnotations() @@ -195,6 +290,7 @@ object SchemaGenerator { */ var knownNotParameterized = mutableSetOf() + /** * Mark the type with the given full/qualified name as "not parameterized", i.e. as not having any generic type parameters. * This helps the type processing step to determine whether two types are truly the same and may fix issues encountered with types. @@ -203,6 +299,7 @@ object SchemaGenerator { knownNotParameterized.add(name) } + /** * Mark the given type as "not parameterized", i.e as not having any generic type parameters. * This helps the type processing step to determine whether two types are truly the same and may fix issues encountered with types. @@ -212,6 +309,7 @@ object SchemaGenerator { markNotParameterized(clazz.qualifiedName ?: clazz.java.name) } + /** * Mark the given type as "not parameterized", i.e as not having any generic type parameters. * This helps the type processing step to determine whether two types are truly the same. @@ -221,42 +319,182 @@ object SchemaGenerator { markNotParameterized(clazz.qualifiedName ?: clazz.java.name) } + /** * Whether optional properties are treated as "required". An optional parameter is one that has a default value specified. */ var optionals: RequiredHandling = RequiredHandling.REQUIRED + /** * Whether nullable properties are treated as "required" */ var nullables: RequiredHandling = RequiredHandling.NON_REQUIRED + /** * Whether to explicitly include "null" types for nullable properties. */ var explicitNullTypes: Boolean = true + /** * The format of the titles. Set `null` to not include titles in the schemas. */ var title: TitleType? = TitleType.SIMPLE + /** * The format of the reference paths. */ var referencePath: RefType = RefType.OPENAPI_FULL + /** + * List of additional/custom [SerializationTypeAnalyzerModule] to use for analysis. + */ + var analyzerModules = mutableListOf() + + + /** + * Adds a new [SerializationTypeAnalyzerModule]. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(module: SerializationTypeAnalyzerModule) { + analyzerModules.add(module) + } + + + /** + * Add a new custom type for types matched by the given matcher. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(matcher: KotlinxSerializationTypeMatcher, provider: KotlinxSerializationCustomProvider) { + customAnalyzer(SimpleSerializationTypeAnalyzerModule(matcher, provider)) + } + + + /** + * Add a new custom type for types matched by the given serial name. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(serializerName: String, provider: KotlinxSerializationCustomProvider) { + customAnalyzer( + { descriptor: SerialDescriptor -> descriptor.fullName() == serializerName }, + provider + ) + } + + + /** + * Add a custom type overwriting the given type. + * Modules overwrite previous modules when matching the same type. + */ + fun customAnalyzer(type: KClass<*>, provider: KotlinxSerializationCustomProvider) { + customAnalyzer( + { descriptor: SerialDescriptor -> descriptor.fullName() == (type.qualifiedName ?: type.java.name) }, + provider + ) + } + + + /** + * Add a custom type overwriting the given type. + * Modules overwrite previous modules when matching the same type. + */ + inline fun customAnalyzer(noinline provider: KotlinxSerializationCustomProvider) { + customAnalyzer(typeOf().classifier!! as KClass<*>, provider) + } + + + /** + * List of additional/custom [ReflectionTypeAnalyzerModule] to use for schema generation. + */ + val generationModules = mutableListOf() + + + /** + * Add a custom schema generation module. + */ + fun customGenerator(module: SwaggerSchemaGenerationModule) { + generationModules.add(module) + } + + /** * Initialize this schema generator config using the given kotlinx json serializer and match its behavior as close as possible. * @param json the kotlinx json serializer */ fun useKotlinxConfig(json: Json) { serializersModule = json.serializersModule - optionals = if(json.configuration.encodeDefaults) RequiredHandling.REQUIRED else RequiredHandling.NON_REQUIRED - nullables = if(json.configuration.explicitNulls) RequiredHandling.REQUIRED else RequiredHandling.NON_REQUIRED + optionals = if (json.configuration.encodeDefaults) RequiredHandling.REQUIRED else RequiredHandling.NON_REQUIRED + nullables = if (json.configuration.explicitNulls) RequiredHandling.REQUIRED else RequiredHandling.NON_REQUIRED } } + object TypeOverwrites { + + class JavaUuid : BasicSchemaOverwriteModule( + identifier = java.util.UUID::class.qualifiedName!!, + schema = { + Schema().also { + it.types = setOf("string") + it.format = "uuid" + } + }, + ) + + class KotlinUuid : BasicSchemaOverwriteModule( + identifier = kotlin.uuid.Uuid::class.qualifiedName!!, + schema = { + Schema().also { + it.types = setOf("string") + it.format = "uuid" + } + }, + ) + + class File : BasicSchemaOverwriteModule( + identifier = java.io.File::class.qualifiedName!!, + schema = { + Schema().also { + it.types = setOf("string") + it.format = "binary" + } + }, + ) + + class Instant : BasicSchemaOverwriteModule( + identifier = java.time.Instant::class.qualifiedName!!, + schema = { + Schema().also { + it.types = setOf("string") + it.format = "date-time" + } + }, + ) + + class LocalDateTime : BasicSchemaOverwriteModule( + identifier = java.time.LocalDateTime::class.qualifiedName!!, + schema = { + Schema().also { + it.types = setOf("string") + it.format = "date-time" + } + }, + ) + + class LocalDate : BasicSchemaOverwriteModule( + identifier = java.time.LocalDate::class.qualifiedName!!, + schema = { + Schema().also { + it.types = setOf("string") + it.format = "date" + } + }, + ) + + } + } diff --git a/pending-changelog.txt b/pending-changelog.txt index 851228f..10ef387 100644 --- a/pending-changelog.txt +++ b/pending-changelog.txt @@ -9,10 +9,23 @@ - improved support for TypesafeRouting plugin - automatically detect path and query parameters -- renamed typalias `ExampleEncoder` to `GenericExampleEncoder` -- moved default example encoder to `ExampleEncoder.internal` -- moved `kotlinxExampleEncoder` to ExampleEncoder.kotlinx -- added typealias `GenericSchemaGenerator` for schema generation function -- create pre-defined configurable example encoders for internal swagger encoder and kotlinx-serialization -- create pre-defined configurable schema generators for reflection and kotlinx-serialization -- kotlinx-serialization schema generator and example encoder can be easily configured using the same kotlinx Json object (also used for serializing real ktor requests and responses) +- schema generation + - added typealias `GenericSchemaGenerator` for schema generation function + - create pre-defined configurable schema generators for reflection and kotlinx-serialization + - kotlinx-serialization schema generator can be configured using the kotlinx "Json" object (also used for serializing real ktor requests and responses) + - added pre-defined custom analysis and schema generation modules (SchemaGenerator.TypeOverwrites.XYZ) for common types + + +- example encoding + - renamed typalias `ExampleEncoder` to `GenericExampleEncoder` + - moved default example encoder to `ExampleEncoder.internal` + - moved `kotlinxExampleEncoder` to ExampleEncoder.kotlinx + - create pre-defined configurable example encoders for internal swagger encoder and kotlinx-serialization + - kotlinx-serialization example encoder can be configured using the kotlinx "Json" object (also used for serializing real ktor requests and responses) + + +TODO +- schema generation + - simplify config + - more/simpler support for custom code +- remove type overwrites from plugin -> no longer needed with customizable \ No newline at end of file From a91a551913fba1022d19648b9555993e328849c6 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Wed, 5 Feb 2025 23:05:47 +0100 Subject: [PATCH 2/2] drop old type overwrites from plugin config --- .../ktoropenapi/examples/CompleteConfig.kt | 4 -- .../ktoropenapi/examples/FileUpload.kt | 9 ++- .../builder/schema/SchemaContextImpl.kt | 7 +- .../ktoropenapi/config/SchemaConfig.kt | 45 ------------ .../ktoropenapi/config/SchemaGenerator.kt | 69 ++++++++++++++++--- ...tionModule.kt => SchemaOverwriteModule.kt} | 4 +- .../ktoropenapi/data/SchemaConfigData.kt | 2 - pending-changelog.txt | 5 +- 8 files changed, 70 insertions(+), 75 deletions(-) rename ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/{SchemaGenerationModule.kt => SchemaOverwriteModule.kt} (99%) diff --git a/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/CompleteConfig.kt b/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/CompleteConfig.kt index 22ee12e..358a773 100644 --- a/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/CompleteConfig.kt +++ b/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/CompleteConfig.kt @@ -113,10 +113,6 @@ private fun Application.myModule() { .withTitle(TitleType.SIMPLE) .compileReferencingRoot() } - overwrite(Schema().also { - it.type = "string" - it.format = "binary" - }) } examples { example("Id 1") { diff --git a/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/FileUpload.kt b/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/FileUpload.kt index c8eba35..4568940 100644 --- a/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/FileUpload.kt +++ b/examples/src/main/kotlin/io/github/smiley4/ktoropenapi/examples/FileUpload.kt @@ -1,6 +1,7 @@ package io.github.smiley4.ktoropenapi.examples import io.github.smiley4.ktoropenapi.OpenApi +import io.github.smiley4.ktoropenapi.config.SchemaGenerator import io.github.smiley4.ktoropenapi.post import io.github.smiley4.ktoropenapi.openApi import io.github.smiley4.ktorredoc.redoc @@ -14,7 +15,6 @@ import io.ktor.server.netty.Netty import io.ktor.server.response.respond import io.ktor.server.routing.route import io.ktor.server.routing.routing -import io.swagger.v3.oas.models.media.Schema import java.io.File fun main() { @@ -27,10 +27,9 @@ private fun Application.myModule() { install(OpenApi) { schemas { // overwrite type "File" with custom schema for binary data - overwrite(Schema().also { - it.type = "string" - it.format = "binary" - }) + generator = SchemaGenerator.reflection { + overwrite(SchemaGenerator.TypeOverwrites.File()) + } } } diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/schema/SchemaContextImpl.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/schema/SchemaContextImpl.kt index d5fe452..03b9efa 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/schema/SchemaContextImpl.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/builder/schema/SchemaContextImpl.kt @@ -57,14 +57,9 @@ internal class SchemaContextImpl(private val schemaConfig: SchemaConfigData) : S private fun generateSchema(typeDescriptor: TypeDescriptor): CompiledSwaggerSchema { return when (typeDescriptor) { is KTypeDescriptor -> { - if (schemaConfig.overwrite.containsKey(typeDescriptor.type)) { - generateSchema(schemaConfig.overwrite[typeDescriptor.type]!!) - } else { - generateSchema(typeDescriptor.type) - } + generateSchema(typeDescriptor.type) } is SerialTypeDescriptor -> { - // todo: support schemaConfig.overwrite generateSchema(typeDescriptor.descriptor) } is SwaggerTypeDescriptor -> { diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaConfig.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaConfig.kt index 19249f1..2b4659e 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaConfig.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaConfig.kt @@ -1,9 +1,6 @@ package io.github.smiley4.ktoropenapi.config import io.github.smiley4.ktoropenapi.data.* -import io.github.smiley4.schemakenerator.core.data.AnnotationData -import io.github.smiley4.schemakenerator.core.data.InputType -import io.github.smiley4.schemakenerator.swagger.data.CompiledSwaggerSchema import io.swagger.v3.oas.models.media.Schema import kotlin.reflect.KType import kotlin.reflect.typeOf @@ -21,47 +18,6 @@ class SchemaConfig { private val schemas = mutableMapOf() - private val overwrite = mutableMapOf() - - - /** - * Overwrite the given [type] with the given [replacement]. - * When the type is specified as the type of a schema, the replacement is used instead. - * This only works for "root"-types and not types of e.g. nested fields. - */ - fun overwrite(type: KType, replacement: TypeDescriptor) { - overwrite[type] = replacement - } - - /** - * Overwrite the given type [T] with the given [replacement]. - * When the type is specified as the type of a schema, the replacement is used instead. - * This only works for "root"-types and not types of e.g. nested fields. - */ - inline fun overwrite(replacement: TypeDescriptor) = overwrite(typeOf(), replacement) - - /** - * Overwrite the given type [T] with the given [replacement]. - * When the type is specified as the type of a schema, the replacement is used instead. - * This only works for "root"-types and not types of e.g. nested fields. - */ - inline fun overwrite(replacement: Schema<*>) = overwrite(typeOf(), SwaggerTypeDescriptor(replacement)) - - /** - * Overwrite the given type [T] with the given [replacement]. - * When the type is specified as the type of a schema, the replacement is used instead. - * This only works for "root"-types and not types of e.g. nested fields. - */ - inline fun overwrite(replacement: KType) = overwrite(typeOf(), KTypeDescriptor(replacement)) - - /** - * Overwrite the given type [T] with the given replacement [R]. - * When the type is specified as the type of a schema, the replacement is used instead. - * This only works for "root"-types and not types of e.g. nested fields. - */ - inline fun overwrite() = overwrite(typeOf(), KTypeDescriptor(typeOf())) - - /** * Add a shared schema that can be referenced by all routes by the given id. */ @@ -91,7 +47,6 @@ class SchemaConfig { internal fun build(securityConfig: SecurityData) = SchemaConfigData( generator = generator, schemas = schemas, - overwrite = overwrite, securitySchemas = securityConfig.defaultUnauthorizedResponse?.body?.let { body -> when (body) { is SimpleBodyData -> listOf(body.type) diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt index 299446b..4ea655a 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerator.kt @@ -50,6 +50,7 @@ object SchemaGenerator { /** * A pre-built [GenericSchemaGenerator] using reflection to analyze types and generate the schemas + * @param config the configuration of the schema generation */ fun reflection(config: ReflectionConfig.() -> Unit = {}): GenericSchemaGenerator { val configInstance = ReflectionConfig().apply(config) @@ -91,6 +92,10 @@ object SchemaGenerator { } } + + /** + * The configuration for a pre-built schema generator using reflection for type analysis. + */ class ReflectionConfig { /** @@ -187,7 +192,7 @@ object SchemaGenerator { /** - * Add a new custom type for types matched by the given matcher. + * Add a new custom analyzer for types matched by the given matcher. * Modules overwrite previous modules when matching the same type. */ fun customAnalyzer(matcher: ReflectionTypeMatcher, provider: ReflectionCustomProvider) { @@ -233,7 +238,7 @@ object SchemaGenerator { /** * Specify the schema for the matching type. Overwrites default schema generation */ - fun overwrite(module: BasicSchemaOverwriteModule) { + fun overwrite(module: SchemaOverwriteModule) { analyzerModules.add(module) generationModules.add(module) } @@ -276,6 +281,10 @@ object SchemaGenerator { } } + + /** + * The configuration for a pre-built schema generator using kotlinx-serialization for type analysis. + */ class KotlinxSerializationConfig { /** @@ -421,6 +430,15 @@ object SchemaGenerator { } + /** + * Specify the schema for the matching type. Overwrites default schema generation + */ + fun overwrite(module: SchemaOverwriteModule) { + analyzerModules.add(module) + generationModules.add(module) + } + + /** * Initialize this schema generator config using the given kotlinx json serializer and match its behavior as close as possible. * @param json the kotlinx json serializer @@ -435,7 +453,12 @@ object SchemaGenerator { object TypeOverwrites { - class JavaUuid : BasicSchemaOverwriteModule( + /** + * Custom analysis and schema generation module for handling [java.util.UUID]. + * Generates a swagger schema with type = "string" and format = "uuid". + * Can be registered in the config for [SchemaGenerator.reflection] or [SchemaGenerator.kotlinx] + */ + class JavaUuid : SchemaOverwriteModule( identifier = java.util.UUID::class.qualifiedName!!, schema = { Schema().also { @@ -445,7 +468,13 @@ object SchemaGenerator { }, ) - class KotlinUuid : BasicSchemaOverwriteModule( + + /** + * Custom analysis and schema generation module for handling [kotlin.uuid.Uuid]. + * Generates a swagger schema with type = "string" and format = "uuid". + * Can be registered in the config for [SchemaGenerator.reflection] or [SchemaGenerator.kotlinx] + */ + class KotlinUuid : SchemaOverwriteModule( identifier = kotlin.uuid.Uuid::class.qualifiedName!!, schema = { Schema().also { @@ -455,7 +484,13 @@ object SchemaGenerator { }, ) - class File : BasicSchemaOverwriteModule( + + /** + * Custom analysis and schema generation module for handling [java.io.File]. + * Generates a swagger schema with type = "string" and format = "binary". + * Can be registered in the config for [SchemaGenerator.reflection] or [SchemaGenerator.kotlinx] + */ + class File : SchemaOverwriteModule( identifier = java.io.File::class.qualifiedName!!, schema = { Schema().also { @@ -465,7 +500,13 @@ object SchemaGenerator { }, ) - class Instant : BasicSchemaOverwriteModule( + + /** + * Custom analysis and schema generation module for handling [java.time.Instant]. + * Generates a swagger schema with type = "string" and format = "date-time". + * Can be registered in the config for [SchemaGenerator.reflection] or [SchemaGenerator.kotlinx] + */ + class Instant : SchemaOverwriteModule( identifier = java.time.Instant::class.qualifiedName!!, schema = { Schema().also { @@ -475,7 +516,13 @@ object SchemaGenerator { }, ) - class LocalDateTime : BasicSchemaOverwriteModule( + + /** + * Custom analysis and schema generation module for handling [java.time.LocalDateTime]. + * Generates a swagger schema with type = "string" and format = "date-time". + * Can be registered in the config for [SchemaGenerator.reflection] or [SchemaGenerator.kotlinx] + */ + class LocalDateTime : SchemaOverwriteModule( identifier = java.time.LocalDateTime::class.qualifiedName!!, schema = { Schema().also { @@ -485,7 +532,13 @@ object SchemaGenerator { }, ) - class LocalDate : BasicSchemaOverwriteModule( + + /** + * Custom analysis and schema generation module for handling [java.time.LocalDate]. + * Generates a swagger schema with type = "string" and format = "date". + * Can be registered in the config for [SchemaGenerator.reflection] or [SchemaGenerator.kotlinx] + */ + class LocalDate : SchemaOverwriteModule( identifier = java.time.LocalDate::class.qualifiedName!!, schema = { Schema().also { diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaOverwriteModule.kt similarity index 99% rename from ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt rename to ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaOverwriteModule.kt index edbccfc..30ce980 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaGenerationModule.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/config/SchemaOverwriteModule.kt @@ -18,7 +18,7 @@ import kotlinx.serialization.descriptors.nonNullOriginal import kotlin.reflect.KClass import kotlin.reflect.KType -open class BasicSchemaOverwriteModule( +open class SchemaOverwriteModule( val identifier: String, val schema: () -> Schema<*> ) : ReflectionTypeAnalyzerModule, SerializationTypeAnalyzerModule, SwaggerSchemaGenerationModule { @@ -100,4 +100,4 @@ open class BasicSchemaOverwriteModule( short = this.serialName.split(".").last().replace("?", "") ) -} \ No newline at end of file +} diff --git a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SchemaConfigData.kt b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SchemaConfigData.kt index c74ac9e..563f8aa 100644 --- a/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SchemaConfigData.kt +++ b/ktor-openapi/src/main/kotlin/io/github/smiley4/ktoropenapi/data/SchemaConfigData.kt @@ -11,14 +11,12 @@ import kotlin.reflect.KType internal data class SchemaConfigData( val schemas: Map, val generator: GenericSchemaGenerator, - val overwrite: Map, val securitySchemas: List ) { companion object { val DEFAULT = SchemaConfigData( schemas = emptyMap(), generator = SchemaGenerator.reflection(), - overwrite = emptyMap(), securitySchemas = emptyList() ) } diff --git a/pending-changelog.txt b/pending-changelog.txt index 10ef387..e91b238 100644 --- a/pending-changelog.txt +++ b/pending-changelog.txt @@ -14,7 +14,7 @@ - create pre-defined configurable schema generators for reflection and kotlinx-serialization - kotlinx-serialization schema generator can be configured using the kotlinx "Json" object (also used for serializing real ktor requests and responses) - added pre-defined custom analysis and schema generation modules (SchemaGenerator.TypeOverwrites.XYZ) for common types - + - remove old "type overwrites" (no longer necessary due to more powerful and flexible schema generator) - example encoding - renamed typalias `ExampleEncoder` to `GenericExampleEncoder` @@ -27,5 +27,4 @@ TODO - schema generation - simplify config - - more/simpler support for custom code -- remove type overwrites from plugin -> no longer needed with customizable \ No newline at end of file + - more/simpler support for custom code \ No newline at end of file