From fe91bd409de52d95e65df9bc67b373c84cdc63cd Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Mon, 17 Apr 2023 22:10:44 +0200 Subject: [PATCH 01/27] bump version to 1.7.0 --- build.gradle.kts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/build.gradle.kts b/build.gradle.kts index d3ceee7..f0807e7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -6,7 +6,7 @@ plugins { } group = "io.github.smiley4" -version = "1.6.0" +version = "1.7.0" repositories { mavenCentral() From e95f06124bb56f898474af36b5d2838a0ee5cd8e Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Thu, 13 Apr 2023 00:35:03 +0200 Subject: [PATCH 02/27] experimental schema generator --- .../experimental/SchemaBuilder.kt | 96 ++++++++++++ .../tests/JsonSchemToOpenApiSchema.kt | 141 ++++++++++++++++++ 2 files changed, 237 insertions(+) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt new file mode 100644 index 0000000..c032247 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt @@ -0,0 +1,96 @@ +package io.github.smiley4.ktorswaggerui.experimental + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import com.github.victools.jsonschema.generator.Option +import com.github.victools.jsonschema.generator.OptionPreset +import com.github.victools.jsonschema.generator.SchemaGenerator +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder +import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.module.jackson.JacksonModule +import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.swagger.v3.oas.models.media.Schema +import java.lang.reflect.Type + + +class SchemaBuilder { + + companion object { + + private data class JsonSchema( + val rootSchema: String, + val schemas: Map + ) + + } + + private val generator = SchemaGenerator( + SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.INLINE_ALL_SCHEMAS) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.DEFINITION_FOR_MAIN_SCHEMA) + .without(Option.INLINE_ALL_SCHEMAS) + + .build() + ) + + fun build(type: Type): Schema<*> { + return type + .let { buildJsonSchema(it) } + .let { processJsonSchema(it) } + .let { buildOpenApiSchema(it) } + } + + private fun buildJsonSchema(type: Type): JsonNode { + return generator.generateSchema(type) + } + + private fun processJsonSchema(json: JsonNode): JsonSchema { + if (json is ObjectNode && json.get("\$defs") != null) { + val mainDefinition = json.get("\$ref").asText().replace("#/\$defs/", "") + val definitions = json.get("\$defs").fields().asSequence().map { it.key to it.value }.toList() + definitions.forEach { cleanupRefPaths(it.second) } + return JsonSchema( + rootSchema = mainDefinition, + schemas = definitions.associate { it } + ) + } else { + return JsonSchema( + rootSchema = "root", + schemas = mapOf("root" to json) + ) + } + } + + private fun cleanupRefPaths(node: JsonNode) { + when (node) { + is ObjectNode -> { + node.get("\$ref")?.also { + println(it) + node.set("\$ref", TextNode(it.asText().replace("#/\$defs/", ""))) + } + node.elements().asSequence().forEach { cleanupRefPaths(it) } + } + is ArrayNode -> { + node.elements().asSequence().forEach { cleanupRefPaths(it) } + } + } + } + + private fun buildOpenApiSchema(json: JsonSchema): Schema<*> { + // TODO: handle multiple schema-definitions + return ObjectMapper().readValue(json.schemas[json.rootSchema].toString(), Schema::class.java) + } + + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt new file mode 100644 index 0000000..68cd858 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt @@ -0,0 +1,141 @@ +package io.github.smiley4.ktorswaggerui.tests + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.fasterxml.jackson.core.type.TypeReference +import com.fasterxml.jackson.databind.ObjectMapper +import com.github.victools.jsonschema.generator.Option +import com.github.victools.jsonschema.generator.OptionPreset +import com.github.victools.jsonschema.generator.SchemaGenerator +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder +import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.module.jackson.JacksonModule +import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.github.smiley4.ktorswaggerui.experimental.SchemaBuilder +import io.kotest.core.spec.style.StringSpec +import io.swagger.v3.oas.models.media.Schema +import java.lang.reflect.Type + +class JsonSchemToOpenApiSchema : StringSpec({ + + "test" { + val jsonSchema = generateJsonSchema() + val oapiSchema = generateOpenApiSchema() + println(jsonSchema) + println(oapiSchema) + } + + "test 2" { + val type: Type = object : TypeReference() {}.type + val schema = SchemaBuilder().build(type) + println(schema) + } + +}) { + + companion object { + + private val generator = SchemaGenerator( + SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.INLINE_ALL_SCHEMAS) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.DEFINITION_FOR_MAIN_SCHEMA) + .without(Option.INLINE_ALL_SCHEMAS) + + .build() + ) + + private inline fun generateJsonSchema(): String { + val type: Type = object : TypeReference() {}.type + return generator.generateSchema(type).toPrettyString() + } + + private inline fun generateOpenApiSchema(): Schema<*> { + return ObjectMapper().readValue(generateJsonSchema(), Schema::class.java) + } + + data class GenericObject( + val flag: Boolean, + val data: T + ) + + data class SpecificObject( + val text: String, + val number: Long + ) + + data class Y(val a: String) + + data class X(val y: Y) + + enum class SimpleEnum { + RED, GREEN, BLUE + } + + data class SimpleDataClass( + val text: String, + val value: Float + ) + + data class DataClassWithMaps( + val mapStringValues: Map, + val mapLongValues: Map + ) + + data class AnotherDataClass( + val primitiveValue: Int, + val primitiveList: List, + private val privateValue: String, + val nestedClass: SimpleDataClass, + val nestedClassList: List + ) + + @JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "_type", + ) + @JsonSubTypes( + JsonSubTypes.Type(value = SubClassA::class), + JsonSubTypes.Type(value = SubClassB::class), + ) + abstract class Superclass( + val superField: String, + ) + + class SubClassA( + superField: String, + val subFieldA: Int + ) : Superclass(superField) + + class SubClassB( + superField: String, + val subFieldB: Boolean + ) : Superclass(superField) + + + data class ClassWithNestedAbstractClass( + val nestedClass: Superclass, + val someField: String + ) + + class ClassWithGenerics( + val genericField: T, + val genericList: List + ) + + class WrapperForClassWithGenerics( + val genericClass: ClassWithGenerics + ) + + } + + +} \ No newline at end of file From 14f4e97e88485926f60383a832b8db1a8de420df Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Thu, 13 Apr 2023 22:33:00 +0200 Subject: [PATCH 03/27] wip: working schema-builder prototype --- .../experimental/SchemaBuilder.kt | 20 ++++++++++++++----- .../tests/JsonSchemToOpenApiSchema.kt | 12 +++++------ 2 files changed, 21 insertions(+), 11 deletions(-) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt index c032247..ff87b5f 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt @@ -20,11 +20,16 @@ class SchemaBuilder { companion object { - private data class JsonSchema( + data class JsonSchema( val rootSchema: String, val schemas: Map ) + data class OpenApiSchema( + val rootSchema: String, + val schemas: Map> + ) + } private val generator = SchemaGenerator( @@ -44,7 +49,7 @@ class SchemaBuilder { .build() ) - fun build(type: Type): Schema<*> { + fun build(type: Type): OpenApiSchema { return type .let { buildJsonSchema(it) } .let { processJsonSchema(it) } @@ -87,10 +92,15 @@ class SchemaBuilder { } } - private fun buildOpenApiSchema(json: JsonSchema): Schema<*> { - // TODO: handle multiple schema-definitions - return ObjectMapper().readValue(json.schemas[json.rootSchema].toString(), Schema::class.java) + private fun buildOpenApiSchema(json: JsonSchema): OpenApiSchema { + return OpenApiSchema( + rootSchema = json.rootSchema, + schemas = json.schemas.mapValues { (_, schema) -> buildOpenApiSchema(schema) } + ) } + private fun buildOpenApiSchema(json: JsonNode): Schema<*> { + return ObjectMapper().readValue(json.toString(), Schema::class.java) + } } \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt index 68cd858..20f4b7f 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt @@ -18,15 +18,15 @@ import java.lang.reflect.Type class JsonSchemToOpenApiSchema : StringSpec({ - "test" { - val jsonSchema = generateJsonSchema() - val oapiSchema = generateOpenApiSchema() - println(jsonSchema) - println(oapiSchema) + + "test 1" { + val type: Type = object : TypeReference() {}.type + val schema = SchemaBuilder().build(type) + println(schema) } "test 2" { - val type: Type = object : TypeReference() {}.type + val type: Type = object : TypeReference() {}.type val schema = SchemaBuilder().build(type) println(schema) } From 9808b7d90ae27780a4e985c8dafb859958d12eef Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Mon, 17 Apr 2023 21:58:38 +0200 Subject: [PATCH 04/27] wip --- .../spec/openapi/ContactBuilder.kt | 15 +++ .../spec/openapi/ContentBuilder.kt | 26 +++++ .../openapi/ExternalDocumentationBuilder.kt | 14 +++ .../spec/openapi/HeaderBuilder.kt | 18 ++++ .../ktorswaggerui/spec/openapi/InfoBuilder.kt | 24 +++++ .../spec/openapi/LicenseBuilder.kt | 14 +++ .../spec/openapi/OpenApiBuilder.kt | 24 +++++ .../spec/openapi/OperationBuilder.kt | 35 +++++++ .../spec/openapi/OperationTagsBuilder.kt | 17 ++++ .../spec/openapi/ParameterBuilder.kt | 29 ++++++ .../ktorswaggerui/spec/openapi/PathBuilder.kt | 25 +++++ .../spec/openapi/PathsBuilder.kt | 39 ++++++++ .../spec/openapi/RequestBodyBuilder.kt | 17 ++++ .../spec/openapi/ResponseBuilder.kt | 20 ++++ .../spec/openapi/ResponsesBuilder.kt | 32 ++++++ .../spec/openapi/SchemaBuilder.kt | 12 +++ .../openapi/SecurityRequirementsBuilder.kt | 27 +++++ .../spec/openapi/ServerBuilder.kt | 14 +++ .../ktorswaggerui/spec/openapi/TagBuilder.kt | 19 ++++ .../spec/route/RouteCollector.kt | 98 +++++++++++++++++++ .../spec/route/RouteDocumentationMerger.kt | 37 +++++++ .../ktorswaggerui/spec/route/RouteMeta.kt | 14 +++ .../tests/JsonSchemaGenerationTests.kt | 1 - .../ktorswaggerui/tests/PathsObjectTest.kt | 1 - .../PrimitiveArraysSchemaGenerationTests.kt | 1 - .../tests/PrimitiveSchemaGenerationTests.kt | 1 - .../tests/SecuritySchemeObjectTest.kt | 1 - .../ktorswaggerui/tests/ServersObjectTest.kt | 1 - 28 files changed, 570 insertions(+), 6 deletions(-) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt new file mode 100644 index 0000000..02538a6 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContactBuilder.kt @@ -0,0 +1,15 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiContact +import io.swagger.v3.oas.models.info.Contact + +class ContactBuilder { + + fun build(contact: OpenApiContact): Contact = + Contact().also { + it.name = contact.name + it.email = contact.email + it.url = contact.url + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt new file mode 100644 index 0000000..8834e76 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt @@ -0,0 +1,26 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +import io.swagger.v3.oas.models.media.Content + +class ContentBuilder { + + fun build(body: OpenApiBaseBody): Content = + when (body) { + is OpenApiSimpleBody -> buildSimpleBody(body) + is OpenApiMultipartBody -> buildMultipartBody(body) + } + + private fun buildSimpleBody(body: OpenApiSimpleBody): Content = + Content().also { + // TODO + } + + private fun buildMultipartBody(body: OpenApiMultipartBody): Content = + Content().also { + // TODO + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt new file mode 100644 index 0000000..d16fc1c --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExternalDocumentationBuilder.kt @@ -0,0 +1,14 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.swagger.v3.oas.models.ExternalDocumentation + + +class ExternalDocumentationBuilder { + + fun build(url: String, description: String): ExternalDocumentation = + ExternalDocumentation().also { + it.url = url + it.description = description + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt new file mode 100644 index 0000000..a1023c4 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt @@ -0,0 +1,18 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiHeader +import io.swagger.v3.oas.models.headers.Header + +class HeaderBuilder( + private val schemaBuilder: SchemaBuilder +) { + + fun build(header: OpenApiHeader): Header = + Header().also { + it.description = header.description + it.required = header.required + it.deprecated = header.deprecated + it.schema = header.type?.let { t -> schemaBuilder.build(t) } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt new file mode 100644 index 0000000..4054527 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt @@ -0,0 +1,24 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo +import io.swagger.v3.oas.models.info.Info + +class InfoBuilder( + private val contactBuilder: ContactBuilder, + private val licenseBuilder: LicenseBuilder +) { + + fun build(info: OpenApiInfo): Info = + Info().also { + it.title = info.title + it.version = info.version + it.description = info.description + info.getContact()?.also { contact -> + it.contact = contactBuilder.build(contact) + } + info.getLicense()?.also { license -> + it.license = licenseBuilder.build(license) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt new file mode 100644 index 0000000..3ef7f1c --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/LicenseBuilder.kt @@ -0,0 +1,14 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiLicense +import io.swagger.v3.oas.models.info.License + +class LicenseBuilder { + + fun build(license: OpenApiLicense): License = + License().also { + it.name = license.name + it.url = license.url + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt new file mode 100644 index 0000000..d811729 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt @@ -0,0 +1,24 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.swagger.v3.oas.models.OpenAPI + +class OpenApiBuilder( + private val config: SwaggerUIPluginConfig, + private val infoBuilder: InfoBuilder, + private val serverBuilder: ServerBuilder, + private val tagBuilder: TagBuilder, + private val pathsBuilder: PathsBuilder +) { + + fun build(routes: Collection): OpenAPI = + OpenAPI().also { + it.info = infoBuilder.build(config.getInfo()) + it.servers = config.getServers().map { server -> serverBuilder.build(server) } + it.tags = config.getTags().map { tag -> tagBuilder.build(tag) } + it.paths = pathsBuilder.build(routes) + it.components = TODO() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt new file mode 100644 index 0000000..ed05595 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt @@ -0,0 +1,35 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.swagger.v3.oas.models.Operation + +class OperationBuilder( + private val operationTagsBuilder: OperationTagsBuilder, + private val parameterBuilder: ParameterBuilder, + private val requestBodyBuilder: RequestBodyBuilder, + private val responsesBuilder: ResponsesBuilder, + private val securityRequirementsBuilder: SecurityRequirementsBuilder +) { + + fun build(route: RouteMeta): Operation = + Operation().also { + it.summary = route.documentation.summary + it.description = route.documentation.description + it.operationId = route.documentation.operationId + it.deprecated = route.documentation.deprecated + it.tags = operationTagsBuilder.build(route) + it.parameters = route.documentation.getRequest().getParameters().map { param -> parameterBuilder.build(param) } + route.documentation.getRequest().getBody()?.let { body -> + it.requestBody = requestBodyBuilder.build(body) + } + it.responses = responsesBuilder.build(route.documentation.getResponses(), route.protected) + if (route.protected) { + securityRequirementsBuilder.build(route).also { securityRequirements -> + if (securityRequirements.isNotEmpty()) { + it.security = securityRequirements + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt new file mode 100644 index 0000000..b25733a --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt @@ -0,0 +1,17 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta + +class OperationTagsBuilder( + private val config: SwaggerUIPluginConfig +) { + + fun build(route: RouteMeta): List { + val generatedTags = config.automaticTagGenerator?.let { + it(route.path.split("/").filter { it.isNotEmpty() }) + } + return (route.documentation.tags + generatedTags).filterNotNull() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt new file mode 100644 index 0000000..f32b893 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt @@ -0,0 +1,29 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter +import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext +import io.swagger.v3.oas.models.parameters.Parameter + +class ParameterBuilder( + private val schemaBuilder: SchemaBuilder +) { + + fun build(parameter: OpenApiRequestParameter): Parameter = + Parameter().also { + it.`in` = when (parameter.location) { + OpenApiRequestParameter.Location.QUERY -> "query" + OpenApiRequestParameter.Location.HEADER -> "header" + OpenApiRequestParameter.Location.PATH -> "path" + } + it.name = parameter.name + it.description = parameter.description + it.required = parameter.required + it.deprecated = parameter.deprecated + it.allowEmptyValue = parameter.allowEmptyValue + it.explode = parameter.explode + it.example = parameter.example + it.allowReserved = parameter.allowReserved + it.schema = schemaBuilder.build(parameter.type) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt new file mode 100644 index 0000000..f114dcd --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt @@ -0,0 +1,25 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.ktor.http.HttpMethod +import io.swagger.v3.oas.models.PathItem + +class PathBuilder( + private val operationBuilder: OperationBuilder +) { + + fun build(route: RouteMeta): PathItem = + PathItem().also { + val operation = operationBuilder.build(route) + when (route.method) { + HttpMethod.Get -> it.get = operation + HttpMethod.Post -> it.post = operation + HttpMethod.Put -> it.put = operation + HttpMethod.Patch -> it.patch = operation + HttpMethod.Delete -> it.delete = operation + HttpMethod.Head -> it.head = operation + HttpMethod.Options -> it.options = operation + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt new file mode 100644 index 0000000..3f2ebc9 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt @@ -0,0 +1,39 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.swagger.v3.oas.models.PathItem +import io.swagger.v3.oas.models.Paths + +class PathsBuilder( + private val pathBuilder: PathBuilder +) { + + fun build(routes: Collection): Paths = + Paths().also { + routes.forEach { route -> + val existingPath = it[route.path] + if (existingPath != null) { + addToExistingPath(existingPath, route) + } else { + addAsNewPath(it, route) + } + } + } + + private fun addAsNewPath(paths: Paths, route: RouteMeta) { + paths.addPathItem(route.path, pathBuilder.build(route)) + } + + private fun addToExistingPath(existing: PathItem, route: RouteMeta) { + val path = pathBuilder.build(route) + existing.get = path.get ?: existing.get + existing.put = path.put ?: existing.put + existing.post = path.post ?: existing.post + existing.delete = path.delete ?: existing.delete + existing.options = path.options ?: existing.options + existing.head = path.head ?: existing.head + existing.patch = path.patch ?: existing.patch + existing.trace = path.trace ?: existing.trace + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt new file mode 100644 index 0000000..1490f52 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/RequestBodyBuilder.kt @@ -0,0 +1,17 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody +import io.swagger.v3.oas.models.parameters.RequestBody + +class RequestBodyBuilder( + private val contentBuilder: ContentBuilder +) { + + fun build(body: OpenApiBaseBody): RequestBody = + RequestBody().also { + it.description = body.description + it.required = body.required + it.content = contentBuilder.build(body) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt new file mode 100644 index 0000000..b95f9de --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt @@ -0,0 +1,20 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse +import io.swagger.v3.oas.models.responses.ApiResponse + +class ResponseBuilder( + private val headerBuilder: HeaderBuilder, + private val contentBuilder: ContentBuilder +) { + + fun build(response: OpenApiResponse): Pair = + "" to ApiResponse().also { + it.description = response.description + it.headers = response.getHeaders().mapValues { header -> headerBuilder.build(header.value) } + response.getBody()?.let { body -> + it.content = contentBuilder.build(body) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt new file mode 100644 index 0000000..1a827b6 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponsesBuilder.kt @@ -0,0 +1,32 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponses +import io.ktor.http.HttpStatusCode +import io.swagger.v3.oas.models.responses.ApiResponses + +class ResponsesBuilder( + private val responseBuilder: ResponseBuilder, + private val config: SwaggerUIPluginConfig +) { + + fun build(responses: OpenApiResponses, isProtected: Boolean): ApiResponses = + ApiResponses().also { + responses.getResponses() + .map { response -> responseBuilder.build(response) } + .forEach { (name, response) -> it.addApiResponse(name, response) } + if (shouldAddUnauthorized(responses, isProtected)) { + config.getDefaultUnauthorizedResponse() + ?.let { response -> responseBuilder.build(response) } + ?.also { (name, response) -> it.addApiResponse(name, response) } + } + } + + private fun shouldAddUnauthorized(responses: OpenApiResponses, isProtected: Boolean): Boolean { + val unauthorizedCode = HttpStatusCode.Unauthorized.value.toString(); + return config.getDefaultUnauthorizedResponse() != null + && isProtected + && responses.getResponses().count { it.statusCode == unauthorizedCode } == 0 + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt new file mode 100644 index 0000000..175e469 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt @@ -0,0 +1,12 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.swagger.v3.oas.models.media.Schema +import java.lang.reflect.Type + +class SchemaBuilder { + + fun build(type: Type): Schema<*> { + TODO() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt new file mode 100644 index 0000000..56730f5 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt @@ -0,0 +1,27 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.swagger.v3.oas.models.security.SecurityRequirement + +class SecurityRequirementsBuilder( + private val config: SwaggerUIPluginConfig +) { + + fun build(route: RouteMeta): List { + val securitySchemes = mutableSetOf().also { schemes -> + route.documentation.securitySchemeName?.also { schemes.add(it) } + route.documentation.securitySchemeNames?.also { schemes.addAll(it) } + } + if (securitySchemes.isEmpty()) { + config.defaultSecuritySchemeName?.also { securitySchemes.add(it) } + config.defaultSecuritySchemeNames?.also { securitySchemes.addAll(it) } + } + return securitySchemes.map { + SecurityRequirement().apply { + addList(it, emptyList()) + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt new file mode 100644 index 0000000..ff10f58 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ServerBuilder.kt @@ -0,0 +1,14 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer +import io.swagger.v3.oas.models.servers.Server + +class ServerBuilder { + + fun build(server: OpenApiServer): Server = + Server().also { + it.url = server.url + it.description = server.description + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt new file mode 100644 index 0000000..48ded64 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/TagBuilder.kt @@ -0,0 +1,19 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag +import io.swagger.v3.oas.models.tags.Tag + +class TagBuilder( + private val externalDocumentationBuilder: ExternalDocumentationBuilder +) { + + fun build(tag: OpenApiTag): Tag = + Tag().also { + it.name = tag.name + it.description = tag.description + if(tag.externalDocUrl != null && tag.externalDocDescription != null) { + it.externalDocs = externalDocumentationBuilder.build(tag.externalDocUrl!!, tag.externalDocDescription!!) + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt new file mode 100644 index 0000000..f844201 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt @@ -0,0 +1,98 @@ +package io.github.smiley4.ktorswaggerui.spec.route + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.DocumentedRouteSelector +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.ktor.http.HttpMethod +import io.ktor.server.application.Application +import io.ktor.server.application.plugin +import io.ktor.server.auth.AuthenticationRouteSelector +import io.ktor.server.routing.HttpMethodRouteSelector +import io.ktor.server.routing.RootRouteSelector +import io.ktor.server.routing.Route +import io.ktor.server.routing.RouteSelector +import io.ktor.server.routing.Routing +import io.ktor.server.routing.TrailingSlashRouteSelector +import kotlin.reflect.full.isSubclassOf + +class RouteCollector( + private val routeDocumentationMerger: RouteDocumentationMerger +) { + + /** + * Collect all routes from the given application + */ + fun collectRoutes(application: Application, config: SwaggerUIPluginConfig): Sequence { + return allRoutes(application.plugin(Routing)) + .asSequence() + .map { route -> + RouteMeta( + method = getMethod(route), + path = getPath(route, config), + documentation = getDocumentation(route, OpenApiRoute()), + protected = isProtected(route) + ) + } + } + + private fun getDocumentation(route: Route, base: OpenApiRoute): OpenApiRoute { + var documentation = base + if (route.selector is DocumentedRouteSelector) { + documentation = routeDocumentationMerger.merge(documentation, (route.selector as DocumentedRouteSelector).documentation) + } + return if (route.parent != null) { + getDocumentation(route.parent!!, documentation) + } else { + documentation + } + } + + private fun getMethod(route: Route): HttpMethod { + return (route.selector as HttpMethodRouteSelector).method + } + + private fun getPath(route: Route, config: SwaggerUIPluginConfig): String { + val selector = route.selector + return if (isIgnoredSelector(selector, config)) { + route.parent?.let { getPath(it, config) } ?: "" + } else { + when (route.selector) { + is TrailingSlashRouteSelector -> "/" + is RootRouteSelector -> "" + is DocumentedRouteSelector -> route.parent?.let { getPath(it, config) } ?: "" + is HttpMethodRouteSelector -> route.parent?.let { getPath(it, config) } ?: "" + is AuthenticationRouteSelector -> route.parent?.let { getPath(it, config) } ?: "" + else -> (route.parent?.let { getPath(it, config) } ?: "") + "/" + route.selector.toString() + } + } + } + + private fun isIgnoredSelector(selector: RouteSelector, config: SwaggerUIPluginConfig): Boolean { + return when (selector) { + is TrailingSlashRouteSelector -> false + is RootRouteSelector -> false + is DocumentedRouteSelector -> true + is HttpMethodRouteSelector -> true + is AuthenticationRouteSelector -> true + else -> config.ignoredRouteSelectors.any { selector::class.isSubclassOf(it) } + } + } + + private fun isProtected(route: Route): Boolean { + return when (route.selector) { + is AuthenticationRouteSelector -> true + is TrailingSlashRouteSelector -> false + is RootRouteSelector -> false + is DocumentedRouteSelector -> route.parent?.let { isProtected(it) } ?: false + is HttpMethodRouteSelector -> route.parent?.let { isProtected(it) } ?: false + else -> route.parent?.let { isProtected(it) } ?: false + } + } + + private fun allRoutes(root: Route): List { + return (listOf(root) + root.children.flatMap { allRoutes(it) }) + .filter { it.selector is HttpMethodRouteSelector } + } + + +} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt new file mode 100644 index 0000000..f269221 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteDocumentationMerger.kt @@ -0,0 +1,37 @@ +package io.github.smiley4.ktorswaggerui.spec.route + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute + +class RouteDocumentationMerger { + + fun merge(a: OpenApiRoute, b: OpenApiRoute): OpenApiRoute { + return OpenApiRoute().apply { + tags = mutableListOf().also { + it.addAll(a.tags) + it.addAll(b.tags) + } + summary = a.summary ?: b.summary + description = a.description ?: b.description + operationId = a.operationId ?: b.operationId + securitySchemeName = a.securitySchemeName ?: b.securitySchemeName + securitySchemeNames = mutableSetOf().also { merged -> + a.securitySchemeNames?.let { merged.addAll(it) } + b.securitySchemeNames?.let { merged.addAll(it) } + } + deprecated = a.deprecated || b.deprecated + hidden = a.hidden || b.hidden + request { + (getParameters() as MutableList).also { + it.addAll(a.getRequest().getParameters()) + it.addAll(b.getRequest().getParameters()) + } + setBody(a.getRequest().getBody() ?: b.getRequest().getBody()) + } + response { + b.getResponses().getResponses().forEach { response -> addResponse(response) } + a.getResponses().getResponses().forEach { response -> addResponse(response) } + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt new file mode 100644 index 0000000..e797598 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteMeta.kt @@ -0,0 +1,14 @@ +package io.github.smiley4.ktorswaggerui.spec.route + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.ktor.http.HttpMethod + +/** + * Information about a route + */ +data class RouteMeta( + val path: String, + val method: HttpMethod, + val documentation: OpenApiRoute, + val protected: Boolean +) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt index 3ddc606..131fddc 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt @@ -3,7 +3,6 @@ package io.github.smiley4.ktorswaggerui.tests import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.core.type.TypeReference -import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext import io.kotest.core.spec.style.StringSpec diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt index 254f027..2599b48 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt @@ -2,7 +2,6 @@ package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.github.smiley4.ktorswaggerui.specbuilder.OApiPathsBuilder import io.github.smiley4.ktorswaggerui.specbuilder.RouteCollector import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt index ebb1976..811c607 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt @@ -2,7 +2,6 @@ package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.github.smiley4.ktorswaggerui.specbuilder.OApiSchemaBuilder import io.kotest.core.spec.style.StringSpec import io.swagger.v3.oas.models.media.Schema import java.math.BigDecimal diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt index a86632e..4bfc213 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt @@ -2,7 +2,6 @@ package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.github.smiley4.ktorswaggerui.specbuilder.OApiSchemaBuilder import io.kotest.core.spec.style.StringSpec import java.math.BigDecimal diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt index a623c4d..938eb3b 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt @@ -4,7 +4,6 @@ import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation import io.github.smiley4.ktorswaggerui.dsl.AuthScheme import io.github.smiley4.ktorswaggerui.dsl.AuthType import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme -import io.github.smiley4.ktorswaggerui.specbuilder.OApiSecuritySchemesBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldContainKey diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt index d9ea631..23c3b39 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt @@ -1,7 +1,6 @@ package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer -import io.github.smiley4.ktorswaggerui.specbuilder.OApiServersBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldHaveSize import io.swagger.v3.oas.models.servers.Server From fe7b7e800de0ef0ac10051c1c5451b9193af1766 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20R=C3=BCgner?= Date: Mon, 15 May 2023 16:00:40 +0200 Subject: [PATCH 05/27] wip --- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 100 +++++++++- .../spec/openapi/ComponentsBuilder.kt | 21 +++ .../spec/openapi/ContentBuilder.kt | 109 ++++++++++- .../spec/openapi/ExampleBuilder.kt | 15 ++ .../spec/openapi/HeaderBuilder.kt | 5 +- .../spec/openapi/OAuthFlowsBuilder.kt | 33 ++++ .../spec/openapi/OpenApiBuilder.kt | 14 +- .../spec/openapi/OperationBuilder.kt | 2 +- .../spec/openapi/OperationTagsBuilder.kt | 2 +- .../spec/openapi/ParameterBuilder.kt | 6 +- .../ktorswaggerui/spec/openapi/PathBuilder.kt | 2 +- .../spec/openapi/PathsBuilder.kt | 2 +- .../spec/openapi/SchemaBuilder.kt | 12 -- .../openapi/SecurityRequirementsBuilder.kt | 2 +- .../spec/openapi/SecuritySchemesBuilder.kt | 53 ++++++ .../schema/JsonSchemaBuilder.kt} | 36 ++-- .../spec/schema/SchemaContext.kt | 176 ++++++++++++++++++ .../tests/JsonSchemToOpenApiSchema.kt | 6 +- 18 files changed, 530 insertions(+), 66 deletions(-) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt rename src/main/kotlin/io/github/smiley4/ktorswaggerui/{experimental/SchemaBuilder.kt => spec/schema/JsonSchemaBuilder.kt} (77%) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index d39cf5c..71a1007 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -1,12 +1,14 @@ package io.github.smiley4.ktorswaggerui -import io.github.smiley4.ktorswaggerui.specbuilder.ApiSpecBuilder -import io.ktor.server.application.ApplicationStarted -import io.ktor.server.application.createApplicationPlugin -import io.ktor.server.application.hooks.MonitoringEvent -import io.ktor.server.application.install -import io.ktor.server.application.pluginOrNull -import io.ktor.server.webjars.Webjars +import io.github.smiley4.ktorswaggerui.spec.openapi.* +import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector +import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.ktor.server.application.* +import io.ktor.server.application.hooks.* +import io.ktor.server.webjars.* +import io.swagger.v3.core.util.Json /** * This version must match the version of the gradle dependency @@ -21,7 +23,10 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration if (application.pluginOrNull(Webjars) == null) { application.install(Webjars) } - apiSpecJson = ApiSpecBuilder().build(application, pluginConfig) + val routes = RouteCollector(RouteDocumentationMerger()).collectRoutes(application, pluginConfig) + val schemaContext = SchemaContext( pluginConfig, JsonSchemaBuilder()).also { it.initialize(routes.toList()) } + apiSpecJson = Json.pretty(builder(pluginConfig, schemaContext).build(routes.toList())) +// apiSpecJson = ApiSpecBuilder().build(application, pluginConfig) } SwaggerRouting( @@ -31,3 +36,82 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration ) { apiSpecJson }.setup(application) } + + +private fun builder(config: SwaggerUIPluginConfig, schemaContext: SchemaContext): OpenApiBuilder { + return OpenApiBuilder( + config = config, + schemaContext = schemaContext, + infoBuilder = InfoBuilder( + contactBuilder = ContactBuilder(), + licenseBuilder = LicenseBuilder() + ), + serverBuilder = ServerBuilder(), + tagBuilder = TagBuilder( + externalDocumentationBuilder = ExternalDocumentationBuilder() + ), + pathsBuilder = PathsBuilder( + pathBuilder = PathBuilder( + operationBuilder = OperationBuilder( + operationTagsBuilder = OperationTagsBuilder(config), + parameterBuilder = ParameterBuilder(schemaContext), + requestBodyBuilder = RequestBodyBuilder( + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + responsesBuilder = ResponsesBuilder( + responseBuilder = ResponseBuilder( + headerBuilder = HeaderBuilder(schemaContext), + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + config = config + ), + securityRequirementsBuilder = SecurityRequirementsBuilder(config), + ) + ) + ), + componentsBuilder = ComponentsBuilder( + config = config, + securitySchemesBuilder = SecuritySchemesBuilder( + oAuthFlowsBuilder = OAuthFlowsBuilder() + ) + ) + ) +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt new file mode 100644 index 0000000..e8fa8b6 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ComponentsBuilder.kt @@ -0,0 +1,21 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.swagger.v3.oas.models.Components +import io.swagger.v3.oas.models.media.Schema + +class ComponentsBuilder( + private val config: SwaggerUIPluginConfig, + private val securitySchemesBuilder: SecuritySchemesBuilder +) { + + fun build(schemas: Map>): Components { + return Components().also { + it.schemas = schemas + if (config.getSecuritySchemes().isNotEmpty()) { + it.securitySchemes = securitySchemesBuilder.build(config.getSecuritySchemes()) + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt index 8834e76..b653552 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt @@ -1,11 +1,18 @@ package io.github.smiley4.ktorswaggerui.spec.openapi -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +import io.github.smiley4.ktorswaggerui.dsl.* +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.ktor.http.* import io.swagger.v3.oas.models.media.Content +import io.swagger.v3.oas.models.media.Encoding +import io.swagger.v3.oas.models.media.MediaType +import io.swagger.v3.oas.models.media.Schema -class ContentBuilder { +class ContentBuilder( + private val schemaContext: SchemaContext, + private val exampleBuilder: ExampleBuilder, + private val headerBuilder: HeaderBuilder +) { fun build(body: OpenApiBaseBody): Content = when (body) { @@ -14,13 +21,97 @@ class ContentBuilder { } private fun buildSimpleBody(body: OpenApiSimpleBody): Content = - Content().also { - // TODO + Content().also { content -> + buildSimpleMediaTypes(body, getSchema(body)).forEach { (contentType, mediaType) -> + content.addMediaType(contentType.toString(), mediaType) + } } - private fun buildMultipartBody(body: OpenApiMultipartBody): Content = - Content().also { - // TODO + private fun buildMultipartBody(body: OpenApiMultipartBody): Content { + return Content().also { content -> + buildMultipartMediaTypes(body).forEach { (contentType, mediaType) -> + content.addMediaType(contentType.toString(), mediaType) + } } + } + + private fun buildSimpleMediaTypes(body: OpenApiSimpleBody, schema: Schema<*>): Map { + val mediaTypes = body.getMediaTypes().ifEmpty { setOf(chooseMediaType(schema)) } + return mediaTypes.associateWith { buildSimpleMediaType(schema, body.getExamples()) } + } + + private fun buildSimpleMediaType(schema: Schema<*>, examples: Map): MediaType { + return MediaType().also { + it.schema = schema + examples.forEach { (name, obj) -> + it.addExamples(name, exampleBuilder.build(obj)) + } + } + } + + private fun buildMultipartMediaTypes(body: OpenApiMultipartBody): Map { + val mediaTypes = body.getMediaTypes().ifEmpty { setOf(ContentType.MultiPart.FormData) } + return mediaTypes.associateWith { buildMultipartMediaType(body) } + } + + private fun buildMultipartMediaType(body: OpenApiMultipartBody): MediaType { + return MediaType().also { mediaType -> + mediaType.schema = Schema().also { schema -> + schema.type = "object" + schema.properties = mutableMapOf?>().also { props -> + body.getParts().forEach { part -> + props[part.name] = getSchema(part) + } + } + } + mediaType.encoding = buildMultipartEncoding(body) + } + } + + private fun buildMultipartEncoding(body: OpenApiMultipartBody): MutableMap? { + return if (body.getParts().flatMap { it.mediaTypes }.isEmpty()) { + null + } else { + mutableMapOf().also { encodings -> + body.getParts() + .filter { it.mediaTypes.isNotEmpty() || it.getHeaders().isNotEmpty() } + .forEach { part -> + encodings[part.name] = Encoding().apply { + contentType = part.mediaTypes.joinToString(", ") { it.toString() } + headers = part.getHeaders().mapValues { headerBuilder.build(it.value) } + } + } + } + } + } + + private fun getSchema(body: OpenApiSimpleBody): Schema<*> { + return if (body.customSchema != null) { + schemaContext.getSchema(body.customSchema!!) + } else { + schemaContext.getSchema(body.type!!) + } + } + + private fun getSchema(part: OpenapiMultipartPart): Schema<*> { + return if (part.customSchema != null) { + schemaContext.getSchema(part.customSchema!!) + } else { + schemaContext.getSchema(part.type!!) + } + } + + private fun chooseMediaType(schema: Schema<*>): ContentType { + return when (schema.type) { + "integer" -> ContentType.Text.Plain + "number" -> ContentType.Text.Plain + "boolean" -> ContentType.Text.Plain + "string" -> ContentType.Text.Plain + "object" -> ContentType.Application.Json + "array" -> ContentType.Application.Json + null -> ContentType.Application.Json + else -> ContentType.Text.Plain + } + } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt new file mode 100644 index 0000000..c1fb8c3 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt @@ -0,0 +1,15 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample +import io.swagger.v3.oas.models.examples.Example + +class ExampleBuilder { + + fun build(example: OpenApiExample): Example = + Example().also { + it.value = example.value + it.summary = example.summary + it.description = example.description + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt index a1023c4..c6a6dfb 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/HeaderBuilder.kt @@ -1,10 +1,11 @@ package io.github.smiley4.ktorswaggerui.spec.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiHeader +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.swagger.v3.oas.models.headers.Header class HeaderBuilder( - private val schemaBuilder: SchemaBuilder + private val schemaContext: SchemaContext ) { fun build(header: OpenApiHeader): Header = @@ -12,7 +13,7 @@ class HeaderBuilder( it.description = header.description it.required = header.required it.deprecated = header.deprecated - it.schema = header.type?.let { t -> schemaBuilder.build(t) } + it.schema = header.type?.let { t -> schemaContext.getSchema(t) } } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt new file mode 100644 index 0000000..a2c661e --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OAuthFlowsBuilder.kt @@ -0,0 +1,33 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.OpenIdOAuthFlow +import io.github.smiley4.ktorswaggerui.dsl.OpenIdOAuthFlows +import io.swagger.v3.oas.models.security.OAuthFlow +import io.swagger.v3.oas.models.security.OAuthFlows +import io.swagger.v3.oas.models.security.Scopes + +class OAuthFlowsBuilder { + + fun build(flows: OpenIdOAuthFlows): OAuthFlows { + return OAuthFlows().apply { + implicit = flows.getImplicit()?.let { build(it) } + password = flows.getPassword()?.let { build(it) } + clientCredentials = flows.getClientCredentials()?.let { build(it) } + authorizationCode = flows.getAuthorizationCode()?.let { build(it) } + } + } + + private fun build(flow: OpenIdOAuthFlow): OAuthFlow { + return OAuthFlow().apply { + authorizationUrl = flow.authorizationUrl + tokenUrl = flow.tokenUrl + refreshUrl = flow.refreshUrl + scopes = flow.scopes?.let { s -> + Scopes().apply { + s.forEach { (k, v) -> addString(k, v) } + } + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt index d811729..8575e54 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt @@ -1,24 +1,28 @@ package io.github.smiley4.ktorswaggerui.spec.openapi import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.swagger.v3.oas.models.OpenAPI class OpenApiBuilder( private val config: SwaggerUIPluginConfig, + private val schemaContext: SchemaContext, private val infoBuilder: InfoBuilder, private val serverBuilder: ServerBuilder, private val tagBuilder: TagBuilder, - private val pathsBuilder: PathsBuilder + private val pathsBuilder: PathsBuilder, + private val componentsBuilder: ComponentsBuilder, ) { - fun build(routes: Collection): OpenAPI = - OpenAPI().also { + fun build(routes: Collection): OpenAPI { + return OpenAPI().also { it.info = infoBuilder.build(config.getInfo()) it.servers = config.getServers().map { server -> serverBuilder.build(server) } it.tags = config.getTags().map { tag -> tagBuilder.build(tag) } it.paths = pathsBuilder.build(routes) - it.components = TODO() + it.components = componentsBuilder.build(schemaContext.getComponentSection()) } + } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt index ed05595..74e790c 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationBuilder.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui.spec.openapi -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.swagger.v3.oas.models.Operation class OperationBuilder( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt index b25733a..6aa50cf 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt @@ -1,7 +1,7 @@ package io.github.smiley4.ktorswaggerui.spec.openapi import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta class OperationTagsBuilder( private val config: SwaggerUIPluginConfig diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt index f32b893..cd7fa69 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ParameterBuilder.kt @@ -1,11 +1,11 @@ package io.github.smiley4.ktorswaggerui.spec.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.swagger.v3.oas.models.parameters.Parameter class ParameterBuilder( - private val schemaBuilder: SchemaBuilder + private val schemaContext: SchemaContext ) { fun build(parameter: OpenApiRequestParameter): Parameter = @@ -23,7 +23,7 @@ class ParameterBuilder( it.explode = parameter.explode it.example = parameter.example it.allowReserved = parameter.allowReserved - it.schema = schemaBuilder.build(parameter.type) + it.schema = schemaContext.getSchema(parameter.type) } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt index f114dcd..e05e2a0 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathBuilder.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui.spec.openapi -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.ktor.http.HttpMethod import io.swagger.v3.oas.models.PathItem diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt index 3f2ebc9..60b4c52 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/PathsBuilder.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui.spec.openapi -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.swagger.v3.oas.models.PathItem import io.swagger.v3.oas.models.Paths diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt deleted file mode 100644 index 175e469..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SchemaBuilder.kt +++ /dev/null @@ -1,12 +0,0 @@ -package io.github.smiley4.ktorswaggerui.spec.openapi - -import io.swagger.v3.oas.models.media.Schema -import java.lang.reflect.Type - -class SchemaBuilder { - - fun build(type: Type): Schema<*> { - TODO() - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt index 56730f5..07fe315 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecurityRequirementsBuilder.kt @@ -1,7 +1,7 @@ package io.github.smiley4.ktorswaggerui.spec.openapi import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.swagger.v3.oas.models.security.SecurityRequirement class SecurityRequirementsBuilder( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt new file mode 100644 index 0000000..5619059 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/SecuritySchemesBuilder.kt @@ -0,0 +1,53 @@ +package io.github.smiley4.ktorswaggerui.spec.openapi + +import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation +import io.github.smiley4.ktorswaggerui.dsl.AuthType +import io.github.smiley4.ktorswaggerui.dsl.AuthScheme +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme +import io.swagger.v3.oas.models.security.SecurityScheme + +class SecuritySchemesBuilder( + private val oAuthFlowsBuilder: OAuthFlowsBuilder +) { + + fun build(securitySchemes: List): Map { + return mutableMapOf().apply { + securitySchemes.forEach { + put(it.name, SecurityScheme().apply { + description = it.description + name = it.name + type = when (it.type) { + AuthType.API_KEY -> SecurityScheme.Type.APIKEY + AuthType.HTTP -> SecurityScheme.Type.HTTP + AuthType.OAUTH2 -> SecurityScheme.Type.OAUTH2 + AuthType.OPENID_CONNECT -> SecurityScheme.Type.OPENIDCONNECT + AuthType.MUTUAL_TLS -> SecurityScheme.Type.MUTUALTLS + null -> null + } + `in` = when (it.location) { + AuthKeyLocation.QUERY -> SecurityScheme.In.QUERY + AuthKeyLocation.HEADER -> SecurityScheme.In.HEADER + AuthKeyLocation.COOKIE -> SecurityScheme.In.COOKIE + null -> null + } + scheme = when (it.scheme) { + AuthScheme.BASIC -> "Basic" + AuthScheme.BEARER -> "Bearer" + AuthScheme.DIGEST -> "Digest" + AuthScheme.HOBA -> "HOBA" + AuthScheme.MUTUAL -> "Mutual" + AuthScheme.OAUTH -> "OAuth" + AuthScheme.SCRAM_SHA_1 -> "SCRAM-SHA-1" + AuthScheme.SCRAM_SHA_256 -> "SCRAM-SHA-256" + AuthScheme.VAPID -> "vapid" + else -> null + } + bearerFormat = it.bearerFormat + flows = it.getFlows()?.let { f -> oAuthFlowsBuilder.build(f) } + openIdConnectUrl = it.openIdConnectUrl + }) + } + } + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt similarity index 77% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index ff87b5f..5e8e36e 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/experimental/SchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -1,31 +1,27 @@ -package io.github.smiley4.ktorswaggerui.experimental +package io.github.smiley4.ktorswaggerui.spec.schema import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.generator.* import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module import io.swagger.v3.oas.models.media.Schema import java.lang.reflect.Type -class SchemaBuilder { +class JsonSchemaBuilder { companion object { - data class JsonSchema( + data class JsonSchemaInfo( val rootSchema: String, val schemas: Map ) - data class OpenApiSchema( + data class OpenApiSchemaInfo( val rootSchema: String, val schemas: Map> ) @@ -36,22 +32,23 @@ class SchemaBuilder { SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) .with(JacksonModule()) .with(Swagger2Module()) - .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_ALL_SCHEMAS) .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) .with(Option.ALLOF_CLEANUP_AT_THE_END) .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) .with(Option.DEFINITION_FOR_MAIN_SCHEMA) .without(Option.INLINE_ALL_SCHEMAS) - .build() ) - fun build(type: Type): OpenApiSchema { + fun build(type: Type): OpenApiSchemaInfo { return type .let { buildJsonSchema(it) } + .let { build(it) } + } + + fun build(schema: JsonNode): OpenApiSchemaInfo { + return schema .let { processJsonSchema(it) } .let { buildOpenApiSchema(it) } } @@ -60,17 +57,17 @@ class SchemaBuilder { return generator.generateSchema(type) } - private fun processJsonSchema(json: JsonNode): JsonSchema { + private fun processJsonSchema(json: JsonNode): JsonSchemaInfo { if (json is ObjectNode && json.get("\$defs") != null) { val mainDefinition = json.get("\$ref").asText().replace("#/\$defs/", "") val definitions = json.get("\$defs").fields().asSequence().map { it.key to it.value }.toList() definitions.forEach { cleanupRefPaths(it.second) } - return JsonSchema( + return JsonSchemaInfo( rootSchema = mainDefinition, schemas = definitions.associate { it } ) } else { - return JsonSchema( + return JsonSchemaInfo( rootSchema = "root", schemas = mapOf("root" to json) ) @@ -86,14 +83,15 @@ class SchemaBuilder { } node.elements().asSequence().forEach { cleanupRefPaths(it) } } + is ArrayNode -> { node.elements().asSequence().forEach { cleanupRefPaths(it) } } } } - private fun buildOpenApiSchema(json: JsonSchema): OpenApiSchema { - return OpenApiSchema( + private fun buildOpenApiSchema(json: JsonSchemaInfo): OpenApiSchemaInfo { + return OpenApiSchemaInfo( rootSchema = json.rootSchema, schemas = json.schemas.mapValues { (_, schema) -> buildOpenApiSchema(schema) } ) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt new file mode 100644 index 0000000..9f77058 --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -0,0 +1,176 @@ +package io.github.smiley4.ktorswaggerui.spec.schema + +import com.fasterxml.jackson.databind.ObjectMapper +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.* +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder.Companion.OpenApiSchemaInfo +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.swagger.v3.oas.models.media.Schema +import java.lang.reflect.Type + +class SchemaContext( + private val config: SwaggerUIPluginConfig, + private val jsonSchemaBuilder: JsonSchemaBuilder +) { + + private val schemas = mutableMapOf() + private val customSchemas = mutableMapOf>() + + + fun initialize(routes: Collection) { + routes.forEach { handle(it) } + config.getDefaultUnauthorizedResponse()?.also { handle(it) } + } + + + private fun handle(route: RouteMeta) { + route.documentation.getRequest().getBody()?.also { handle(it) } + route.documentation.getRequest().getParameters().forEach { handle(it) } + route.documentation.getResponses().getResponses().forEach { handle(it) } + } + + + private fun handle(response: OpenApiResponse) { + response.getHeaders().forEach { (_, header) -> + header.type?.also { headerType -> + createSchema(headerType) + } + } + response.getBody()?.also { handle(it) } + } + + + private fun handle(body: OpenApiBaseBody) { + return when (body) { + is OpenApiSimpleBody -> handle(body) + is OpenApiMultipartBody -> handle(body) + } + } + + + private fun handle(body: OpenApiSimpleBody) { + if (body.customSchema != null) { + body.customSchema?.also { createSchema(it) } + } else { + body.type?.also { createSchema(it) } + } + } + + + private fun handle(body: OpenApiMultipartBody) { + body.getParts().forEach { part -> + if (part.customSchema != null) { + part.customSchema?.also { createSchema(it) } + } else { + part.type?.also { createSchema(it) } + } + } + } + + + private fun handle(parameter: OpenApiRequestParameter) { + createSchema(parameter.type) + } + + + private fun createSchema(type: Type) { + if(schemas.containsKey(type.typeName)) { + return + } + schemas[type.typeName] = jsonSchemaBuilder.build(type) + } + + + private fun createSchema(customSchemaRef: CustomSchemaRef) { + if(customSchemas.containsKey(customSchemaRef)) { + return + } + val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) + if (customSchema == null) { + customSchemas[customSchemaRef] = Schema() + } else { + when (customSchema) { + is CustomJsonSchema -> { + jsonSchemaBuilder.build(ObjectMapper().readTree(customSchema.provider())).let { + it.schemas[it.rootSchema] + } + } + is CustomOpenApiSchema -> { + customSchema.provider() + } + is RemoteSchema -> { + Schema().apply { + type = "object" + `$ref` = customSchema.url + } + } + }.let { schema -> + when (customSchemaRef) { + is CustomObjectSchemaRef -> schema + is CustomArraySchemaRef -> Schema().apply { + this.type = "array" + this.items = schema + } + } + } + } + } + + + fun getComponentSection(): Map> { + val componentSection = mutableMapOf>() + schemas.forEach { (schemaId, schemaInfo) -> + val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! + if(schemaInfo.schemas.size == 1 && (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema))) { + // skip + } else { + componentSection.putAll(schemaInfo.schemas) + } + } + return componentSection + } + + + + + fun getSchema(customSchemaRef: CustomSchemaRef): Schema<*> { + return customSchemas[customSchemaRef] + ?: throw IllegalStateException("Could not retrieve schema for type '${customSchemaRef.schemaId}'") + } + + + fun getSchema(type: Type): Schema<*> { + println("Get schema for type '$type'") + val schemaInfo = getSchemaInfo(type) + val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! + + if(isPrimitive(rootSchema) && schemaInfo.schemas.size == 1) { + println("-> is primitive, return root-schema") + return rootSchema + } + if(isPrimitiveArray(rootSchema) && schemaInfo.schemas.size == 1) { + println("-> is primitive-array, return root-schema") + return rootSchema + } + println("-> is complex, return ref '#/components/schemas/${schemaInfo.rootSchema}'") + return Schema().also { + it.`$ref` = "#/components/schemas/${schemaInfo.rootSchema}" + } + } + + + private fun getSchemaInfo(type: Type): OpenApiSchemaInfo { + return type.typeName.let { typeName -> + schemas[typeName] ?: throw IllegalStateException("Could not retrieve schema for type '${typeName}'") + } + } + + private fun isPrimitive(schema: Schema<*>): Boolean { + return schema.type != "object" && schema.type != "array" + } + + private fun isPrimitiveArray(schema: Schema<*>): Boolean { + return schema.type == "array" && (isPrimitive(schema.items) || isPrimitiveArray(schema.items)) // todo: check if recursive nested-nested-arrays really work + } + +} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt index 20f4b7f..db02e97 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt @@ -11,7 +11,7 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder import com.github.victools.jsonschema.generator.SchemaVersion import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.experimental.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.kotest.core.spec.style.StringSpec import io.swagger.v3.oas.models.media.Schema import java.lang.reflect.Type @@ -21,13 +21,13 @@ class JsonSchemToOpenApiSchema : StringSpec({ "test 1" { val type: Type = object : TypeReference() {}.type - val schema = SchemaBuilder().build(type) + val schema = JsonSchemaBuilder().build(type) println(schema) } "test 2" { val type: Type = object : TypeReference() {}.type - val schema = SchemaBuilder().build(type) + val schema = JsonSchemaBuilder().build(type) println(schema) } From 2b3eec163180ac99aaaa13cf4026bec8170a6332 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Mon, 15 May 2023 19:09:59 +0200 Subject: [PATCH 06/27] fixes --- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 35 ++++- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 5 +- .../spec/openapi/ContentBuilder.kt | 45 ++++-- .../spec/openapi/ResponseBuilder.kt | 2 +- .../spec/route/RouteCollector.kt | 17 +++ .../spec/schema/JsonSchemaBuilder.kt | 18 +-- .../spec/schema/SchemaContext.kt | 35 +++-- .../ktorswaggerui/examples/CompleteExample.kt | 2 +- .../CustomJsonSchemaBuilderExample.kt | 4 +- .../examples/CustomJsonSchemaExample.kt | 142 +++++++++--------- .../tests/JsonSchemToOpenApiSchema.kt | 59 ++++---- 11 files changed, 216 insertions(+), 148 deletions(-) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 71a1007..9589697 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -1,13 +1,37 @@ package io.github.smiley4.ktorswaggerui -import io.github.smiley4.ktorswaggerui.spec.openapi.* +import io.github.smiley4.ktorswaggerui.spec.openapi.ComponentsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OpenApiBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.ktor.server.application.* -import io.ktor.server.application.hooks.* -import io.ktor.server.webjars.* +import io.ktor.server.application.ApplicationStarted +import io.ktor.server.application.createApplicationPlugin +import io.ktor.server.application.hooks.MonitoringEvent +import io.ktor.server.application.install +import io.ktor.server.application.pluginOrNull +import io.ktor.server.webjars.Webjars import io.swagger.v3.core.util.Json /** @@ -24,7 +48,8 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration application.install(Webjars) } val routes = RouteCollector(RouteDocumentationMerger()).collectRoutes(application, pluginConfig) - val schemaContext = SchemaContext( pluginConfig, JsonSchemaBuilder()).also { it.initialize(routes.toList()) } + val schemaContext = SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + .also { it.initialize(routes.toList()) } apiSpecJson = Json.pretty(builder(pluginConfig, schemaContext).build(routes.toList())) // apiSpecJson = ApiSpecBuilder().build(application, pluginConfig) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index 5c440a6..1d09de2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -164,11 +164,12 @@ class SwaggerUIPluginConfig { SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) .with(JacksonModule()) .with(Swagger2Module()) - .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_ALL_SCHEMAS) .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) .with(Option.ALLOF_CLEANUP_AT_THE_END) .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.DEFINITION_FOR_MAIN_SCHEMA) + .without(Option.INLINE_ALL_SCHEMAS) .also { it.forTypesInGeneral() .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt index b653552..e718fa9 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt @@ -1,12 +1,31 @@ package io.github.smiley4.ktorswaggerui.spec.openapi -import io.github.smiley4.ktorswaggerui.dsl.* +import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample +import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +import io.github.smiley4.ktorswaggerui.dsl.OpenapiMultipartPart import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.ktor.http.* +import io.ktor.http.ContentType import io.swagger.v3.oas.models.media.Content import io.swagger.v3.oas.models.media.Encoding import io.swagger.v3.oas.models.media.MediaType import io.swagger.v3.oas.models.media.Schema +import kotlin.collections.Map +import kotlin.collections.MutableMap +import kotlin.collections.associateWith +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.filter +import kotlin.collections.flatMap +import kotlin.collections.forEach +import kotlin.collections.ifEmpty +import kotlin.collections.isNotEmpty +import kotlin.collections.joinToString +import kotlin.collections.mapValues +import kotlin.collections.mutableMapOf +import kotlin.collections.set +import kotlin.collections.setOf class ContentBuilder( private val schemaContext: SchemaContext, @@ -35,12 +54,12 @@ class ContentBuilder( } } - private fun buildSimpleMediaTypes(body: OpenApiSimpleBody, schema: Schema<*>): Map { - val mediaTypes = body.getMediaTypes().ifEmpty { setOf(chooseMediaType(schema)) } + private fun buildSimpleMediaTypes(body: OpenApiSimpleBody, schema: Schema<*>?): Map { + val mediaTypes = body.getMediaTypes().ifEmpty { schema?.let { setOf(chooseMediaType(schema)) } ?: setOf() } return mediaTypes.associateWith { buildSimpleMediaType(schema, body.getExamples()) } } - private fun buildSimpleMediaType(schema: Schema<*>, examples: Map): MediaType { + private fun buildSimpleMediaType(schema: Schema<*>?, examples: Map): MediaType { return MediaType().also { it.schema = schema examples.forEach { (name, obj) -> @@ -60,7 +79,9 @@ class ContentBuilder( schema.type = "object" schema.properties = mutableMapOf?>().also { props -> body.getParts().forEach { part -> - props[part.name] = getSchema(part) + getSchema(part)?.also { + props[part.name] = getSchema(part) + } } } } @@ -85,19 +106,23 @@ class ContentBuilder( } } - private fun getSchema(body: OpenApiSimpleBody): Schema<*> { + private fun getSchema(body: OpenApiSimpleBody): Schema<*>? { return if (body.customSchema != null) { schemaContext.getSchema(body.customSchema!!) + } else if (body.type != null) { + schemaContext.getSchema(body.type) } else { - schemaContext.getSchema(body.type!!) + null } } - private fun getSchema(part: OpenapiMultipartPart): Schema<*> { + private fun getSchema(part: OpenapiMultipartPart): Schema<*>? { return if (part.customSchema != null) { schemaContext.getSchema(part.customSchema!!) + } else if (part.type != null) { + schemaContext.getSchema(part.type) } else { - schemaContext.getSchema(part.type!!) + null } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt index b95f9de..80a25f1 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ResponseBuilder.kt @@ -9,7 +9,7 @@ class ResponseBuilder( ) { fun build(response: OpenApiResponse): Pair = - "" to ApiResponse().also { + response.statusCode to ApiResponse().also { it.description = response.description it.headers = response.getHeaders().mapValues { header -> headerBuilder.build(header.value) } response.getBody()?.let { body -> diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt index f844201..40bbc56 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt @@ -33,8 +33,25 @@ class RouteCollector( protected = isProtected(route) ) } + .filter { removeLeadingSlash(it.path) != removeLeadingSlash(config.getSwaggerUI().swaggerUrl) } + .filter { removeLeadingSlash(it.path) != removeLeadingSlash("${config.getSwaggerUI().swaggerUrl}/api.json") } + .filter { removeLeadingSlash(it.path) != removeLeadingSlash("${config.getSwaggerUI().swaggerUrl}/{filename}") } + .filter { !config.getSwaggerUI().forwardRoot || it.path != "/" } + .filter { !it.documentation.hidden } + .filter { path -> + config.pathFilter + ?.let { it(path.method, path.path.split("/").filter { it.isNotEmpty() }) } + ?: true + } } + private fun removeLeadingSlash(str: String): String = + if (str.startsWith("/")) { + str.substring(1) + } else { + str + } + private fun getDocumentation(route: Route, base: OpenApiRoute): OpenApiRoute { var documentation = base if (route.selector is DocumentedRouteSelector) { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index 5e8e36e..274a592 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -12,7 +12,9 @@ import io.swagger.v3.oas.models.media.Schema import java.lang.reflect.Type -class JsonSchemaBuilder { +class JsonSchemaBuilder( + schemaGeneratorConfig: SchemaGeneratorConfig +) { companion object { @@ -28,18 +30,7 @@ class JsonSchemaBuilder { } - private val generator = SchemaGenerator( - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.DEFINITION_FOR_MAIN_SCHEMA) - .without(Option.INLINE_ALL_SCHEMAS) - .build() - ) + private val generator = SchemaGenerator(schemaGeneratorConfig) fun build(type: Type): OpenApiSchemaInfo { return type @@ -78,7 +69,6 @@ class JsonSchemaBuilder { when (node) { is ObjectNode -> { node.get("\$ref")?.also { - println(it) node.set("\$ref", TextNode(it.asText().replace("#/\$defs/", ""))) } node.elements().asSequence().forEach { cleanupRefPaths(it) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index 9f77058..b249ff8 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -14,7 +14,7 @@ class SchemaContext( ) { private val schemas = mutableMapOf() - private val customSchemas = mutableMapOf>() + private val customSchemas = mutableMapOf>() fun initialize(routes: Collection) { @@ -82,17 +82,17 @@ class SchemaContext( private fun createSchema(customSchemaRef: CustomSchemaRef) { - if(customSchemas.containsKey(customSchemaRef)) { + if(customSchemas.containsKey(customSchemaRef.schemaId)) { return } val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) if (customSchema == null) { - customSchemas[customSchemaRef] = Schema() + customSchemas[customSchemaRef.schemaId] = Schema() } else { when (customSchema) { is CustomJsonSchema -> { jsonSchemaBuilder.build(ObjectMapper().readTree(customSchema.provider())).let { - it.schemas[it.rootSchema] + it.schemas[it.rootSchema]!! } } is CustomOpenApiSchema -> { @@ -112,6 +112,8 @@ class SchemaContext( this.items = schema } } + }.also { + customSchemas[customSchemaRef.schemaId] = it } } } @@ -119,7 +121,7 @@ class SchemaContext( fun getComponentSection(): Map> { val componentSection = mutableMapOf>() - schemas.forEach { (schemaId, schemaInfo) -> + schemas.forEach { (_, schemaInfo) -> val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! if(schemaInfo.schemas.size == 1 && (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema))) { // skip @@ -127,6 +129,9 @@ class SchemaContext( componentSection.putAll(schemaInfo.schemas) } } + customSchemas.forEach { (schemaId, schema) -> + componentSection[schemaId] = schema + } return componentSection } @@ -134,27 +139,27 @@ class SchemaContext( fun getSchema(customSchemaRef: CustomSchemaRef): Schema<*> { - return customSchemas[customSchemaRef] + val schema = customSchemas[customSchemaRef.schemaId] ?: throw IllegalStateException("Could not retrieve schema for type '${customSchemaRef.schemaId}'") + return buildInlineSchema(customSchemaRef.schemaId, schema, 1) } fun getSchema(type: Type): Schema<*> { - println("Get schema for type '$type'") val schemaInfo = getSchemaInfo(type) val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! + return buildInlineSchema(schemaInfo.rootSchema, rootSchema, schemaInfo.schemas.size) + } - if(isPrimitive(rootSchema) && schemaInfo.schemas.size == 1) { - println("-> is primitive, return root-schema") - return rootSchema + private fun buildInlineSchema(schemaId: String, schema: Schema<*>, connectedSchemaCount: Int): Schema<*> { + if(isPrimitive(schema) && connectedSchemaCount == 1) { + return schema } - if(isPrimitiveArray(rootSchema) && schemaInfo.schemas.size == 1) { - println("-> is primitive-array, return root-schema") - return rootSchema + if(isPrimitiveArray(schema) && connectedSchemaCount == 1) { + return schema } - println("-> is complex, return ref '#/components/schemas/${schemaInfo.rootSchema}'") return Schema().also { - it.`$ref` = "#/components/schemas/${schemaInfo.rootSchema}" + it.`$ref` = "#/components/schemas/$schemaId" } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt index 17cd958..12328a0 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt @@ -245,7 +245,7 @@ private fun Application.myModule() { } } }) { - call.respond(HttpStatusCode.NotImplemented, "todo") + call.respond(HttpStatusCode.NotImplemented, "...") } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt index e4537ec..207209b 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt @@ -65,11 +65,11 @@ private fun Application.myModule() { routing { get("something", { request { - body() + body() } response { HttpStatusCode.OK to { - body() + body() } } }) { diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt index f4a06de..76bbcad 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt @@ -19,29 +19,29 @@ import io.ktor.server.routing.routing * An example for defining custom json-schemas */ fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) } private fun Application.myModule() { - data class MyRequestData( - val someText: String, - val someBoolean: Boolean - ) + data class MyRequestData( + val someText: String, + val someBoolean: Boolean + ) - data class MyResponseData( - val someText: String, - val someNumber: Long - ) + data class MyResponseData( + val someText: String, + val someNumber: Long + ) - install(SwaggerUI) { - // don't show the test-routes providing json-schemas - pathFilter = { _, url -> url.firstOrNull() != "schema" } - schemasInComponentSection - schemas { - // specify a custom json-schema with the id 'myRequestData' - json("myRequestData") { - """ + install(SwaggerUI) { + // don't show the test-routes providing json-schemas + pathFilter = { _, url -> url.firstOrNull() != "schema" } + schemasInComponentSection + schemas { + // specify a custom json-schema with the id 'myRequestData' + json("myRequestData") { + """ { "type": "object", "properties": { @@ -54,64 +54,64 @@ private fun Application.myModule() { } } """.trimIndent() - } - // specify a remote json-schema with the id 'myRequestData' - remote("myResponseData", "http://localhost:8080/schema/myResponseData") - } - } + } + // specify a remote json-schema with the id 'myRequestData' + remote("myResponseData", "http://localhost:8080/schema/myResponseData") + } + } - routing { + routing { - get("something", { - request { - // body referencing the custom schema with id 'myRequestData' - body(obj("myRequestData")) - } - response { - HttpStatusCode.OK to { - // body referencing the custom schema with id 'myResponseData' - body(obj("myResponseData")) - } - } - }) { - val text = call.receive().someText - call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) - } + get("something", { + request { + // body referencing the custom schema with id 'myRequestData' + body(obj("myRequestData")) + } + response { + HttpStatusCode.OK to { + // body referencing the custom schema with id 'myResponseData' + body(obj("myResponseData")) + } + } + }) { + val text = call.receive().someText + call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) + } - get("something/many", { - request { - // body referencing the custom schema with id 'myRequestData' - body(array("myRequestData")) - } - response { - HttpStatusCode.OK to { - // body referencing the custom schema with id 'myResponseData' - body(array("myResponseData")) - } - } - }) { - val text = call.receive().someText - call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) - } + get("something/many", { + request { + // body referencing the custom schema with id 'myRequestData' + body(array("myRequestData")) + } + response { + HttpStatusCode.OK to { + // body referencing the custom schema with id 'myResponseData' + body(array("myResponseData")) + } + } + }) { + val text = call.receive().someText + call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) + } - // route providing a json-schema - get("schema/myResponseData") { - call.respondText( - """ - { - "type": "object", - "properties": { - "someNumber": { - "type": "integer", - "format": "int64" - }, - "someText": { - "type": "string" - } + // (external) endpoint providing a json-schema + get("schema/myResponseData") { + call.respondText( + """ + { + "type": "object", + "properties": { + "someNumber": { + "type": "integer", + "format": "int64" + }, + "someText": { + "type": "string" } } - """.trimIndent() - ) - } - } + } + """.trimIndent() + ) + } + } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt index db02e97..75c9efd 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt @@ -4,13 +4,18 @@ import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.core.type.TypeReference import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ObjectNode +import com.github.victools.jsonschema.generator.FieldScope import com.github.victools.jsonschema.generator.Option import com.github.victools.jsonschema.generator.OptionPreset +import com.github.victools.jsonschema.generator.SchemaGenerationContext import com.github.victools.jsonschema.generator.SchemaGenerator import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.generator.TypeScope import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.github.smiley4.ktorswaggerui.dsl.Example import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.kotest.core.spec.style.StringSpec import io.swagger.v3.oas.models.media.Schema @@ -21,13 +26,13 @@ class JsonSchemToOpenApiSchema : StringSpec({ "test 1" { val type: Type = object : TypeReference() {}.type - val schema = JsonSchemaBuilder().build(type) + val schema = JsonSchemaBuilder(generatorConfig).build(type) println(schema) } "test 2" { val type: Type = object : TypeReference() {}.type - val schema = JsonSchemaBuilder().build(type) + val schema = JsonSchemaBuilder(generatorConfig).build(type) println(schema) } @@ -35,31 +40,31 @@ class JsonSchemToOpenApiSchema : StringSpec({ companion object { - private val generator = SchemaGenerator( - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .with(Swagger2Module()) - .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_ALL_SCHEMAS) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.DEFINITION_FOR_MAIN_SCHEMA) - .without(Option.INLINE_ALL_SCHEMAS) - - .build() - ) - - private inline fun generateJsonSchema(): String { - val type: Type = object : TypeReference() {}.type - return generator.generateSchema(type).toPrettyString() - } - - private inline fun generateOpenApiSchema(): Schema<*> { - return ObjectMapper().readValue(generateJsonSchema(), Schema::class.java) - } + private val generatorConfig = SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.DEFINITION_FOR_MAIN_SCHEMA) + .without(Option.INLINE_ALL_SCHEMAS) + .also { + it.forTypesInGeneral() + .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> + if (typeScope is FieldScope) { + typeScope.getAnnotation(io.swagger.v3.oas.annotations.media.Schema::class.java)?.also { annotation -> + if (annotation.example != "") { + objectNode.put("example", annotation.example) + } + } + typeScope.getAnnotation(Example::class.java)?.also { annotation -> + objectNode.put("example", annotation.value) + } + } + } + } + .build() data class GenericObject( val flag: Boolean, From 1eb095bafbc7f5ed4de8d40de7ea7a16f0bb6aae Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Mon, 15 May 2023 19:33:04 +0200 Subject: [PATCH 07/27] inline arrays of referenced objects --- .../spec/schema/SchemaContext.kt | 45 ++++++++++++++----- .../ktorswaggerui/examples/CompleteExample.kt | 11 +++++ 2 files changed, 46 insertions(+), 10 deletions(-) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index b249ff8..1e67370 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -2,11 +2,24 @@ package io.github.smiley4.ktorswaggerui.spec.schema import com.fasterxml.jackson.databind.ObjectMapper import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.* -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder.Companion.OpenApiSchemaInfo +import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef +import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema +import io.github.smiley4.ktorswaggerui.dsl.CustomObjectSchemaRef +import io.github.smiley4.ktorswaggerui.dsl.CustomOpenApiSchema +import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef +import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter +import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder.Companion.OpenApiSchemaInfo import io.swagger.v3.oas.models.media.Schema import java.lang.reflect.Type +import kotlin.collections.component1 +import kotlin.collections.component2 +import kotlin.collections.set class SchemaContext( private val config: SwaggerUIPluginConfig, @@ -74,7 +87,7 @@ class SchemaContext( private fun createSchema(type: Type) { - if(schemas.containsKey(type.typeName)) { + if (schemas.containsKey(type.typeName)) { return } schemas[type.typeName] = jsonSchemaBuilder.build(type) @@ -82,7 +95,7 @@ class SchemaContext( private fun createSchema(customSchemaRef: CustomSchemaRef) { - if(customSchemas.containsKey(customSchemaRef.schemaId)) { + if (customSchemas.containsKey(customSchemaRef.schemaId)) { return } val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) @@ -123,7 +136,7 @@ class SchemaContext( val componentSection = mutableMapOf>() schemas.forEach { (_, schemaInfo) -> val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! - if(schemaInfo.schemas.size == 1 && (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema))) { + if (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema) || isWrapperArray(rootSchema)) { // skip } else { componentSection.putAll(schemaInfo.schemas) @@ -136,8 +149,6 @@ class SchemaContext( } - - fun getSchema(customSchemaRef: CustomSchemaRef): Schema<*> { val schema = customSchemas[customSchemaRef.schemaId] ?: throw IllegalStateException("Could not retrieve schema for type '${customSchemaRef.schemaId}'") @@ -151,13 +162,22 @@ class SchemaContext( return buildInlineSchema(schemaInfo.rootSchema, rootSchema, schemaInfo.schemas.size) } + private fun buildInlineSchema(schemaId: String, schema: Schema<*>, connectedSchemaCount: Int): Schema<*> { - if(isPrimitive(schema) && connectedSchemaCount == 1) { + if (isPrimitive(schema) && connectedSchemaCount == 1) { return schema } - if(isPrimitiveArray(schema) && connectedSchemaCount == 1) { + if (isPrimitiveArray(schema) && connectedSchemaCount == 1) { return schema } + if (isWrapperArray(schema)) { + return Schema().also { wrapper -> + wrapper.type = "array" + wrapper.items = Schema().also { + it.`$ref` = schema.items.`$ref` + } + } + } return Schema().also { it.`$ref` = "#/components/schemas/$schemaId" } @@ -170,12 +190,17 @@ class SchemaContext( } } + private fun isPrimitive(schema: Schema<*>): Boolean { return schema.type != "object" && schema.type != "array" } private fun isPrimitiveArray(schema: Schema<*>): Boolean { - return schema.type == "array" && (isPrimitive(schema.items) || isPrimitiveArray(schema.items)) // todo: check if recursive nested-nested-arrays really work + return schema.type == "array" && (isPrimitive(schema.items) || isPrimitiveArray(schema.items)) + } + + private fun isWrapperArray(schema: Schema<*>): Boolean { + return schema.type == "array" && schema.items.type == null && schema.items.`$ref` != null } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt index 12328a0..cf4683c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt @@ -250,6 +250,17 @@ private fun Application.myModule() { } + get("2dIntArray", { + description = "Returns a 2d-array of integers" + response { + HttpStatusCode.OK to { + body>>() + } + } + }){ + call.respond(HttpStatusCode.NotImplemented, "todo") + } + get("hidden", { hidden = true description = "This route is hidden and not visible in swagger" From ac5f78497b10a3e9396099d9fe0f72f767d146a2 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Mon, 15 May 2023 19:44:10 +0200 Subject: [PATCH 08/27] enchrich schema with xml-data --- .../ktorswaggerui/spec/schema/JsonSchemaBuilder.kt | 11 ++++++++--- .../ktorswaggerui/spec/schema/SchemaContext.kt | 2 +- 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index 274a592..ecb1b24 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -9,6 +9,7 @@ import com.github.victools.jsonschema.generator.* import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module import io.swagger.v3.oas.models.media.Schema +import io.swagger.v3.oas.models.media.XML import java.lang.reflect.Type @@ -83,12 +84,16 @@ class JsonSchemaBuilder( private fun buildOpenApiSchema(json: JsonSchemaInfo): OpenApiSchemaInfo { return OpenApiSchemaInfo( rootSchema = json.rootSchema, - schemas = json.schemas.mapValues { (_, schema) -> buildOpenApiSchema(schema) } + schemas = json.schemas.mapValues { (name, schema) -> buildOpenApiSchema(schema, name) } ) } - private fun buildOpenApiSchema(json: JsonNode): Schema<*> { - return ObjectMapper().readValue(json.toString(), Schema::class.java) + private fun buildOpenApiSchema(json: JsonNode, name: String): Schema<*> { + return ObjectMapper().readValue(json.toString(), Schema::class.java).also { schema -> + schema.xml = XML().also { + it.name = name + } + } } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index 1e67370..9006dc5 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -190,7 +190,7 @@ class SchemaContext( } } - + private fun isPrimitive(schema: Schema<*>): Boolean { return schema.type != "object" && schema.type != "array" } From deef88231269053d9982acfb1593f5080a1c9282 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Tue, 16 May 2023 01:28:32 +0200 Subject: [PATCH 09/27] wip tests --- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 20 +- .../ktorswaggerui/spec/openapi/InfoBuilder.kt | 1 + .../spec/schema/SchemaContext.kt | 11 +- .../ktorswaggerui/tests/AssertionUtils.kt | 146 ---- .../ktorswaggerui/tests/BuilderUtils.kt | 27 +- .../tests/ComponentsObjectTest.kt | 240 ------ .../ktorswaggerui/tests/ContentObjectTest.kt | 638 +++++++-------- .../ktorswaggerui/tests/ExampleObjectTest.kt | 67 -- .../ktorswaggerui/tests/InfoObjectTest.kt | 62 -- .../tests/JsonSchemToOpenApiSchema.kt | 146 ---- .../tests/JsonSchemaGenerationTests.kt | 616 +++++++------- .../ktorswaggerui/tests/PathObjectTest.kt | 338 -------- .../ktorswaggerui/tests/PathsObjectTest.kt | 148 ---- ...tTest.kt => SecuritySchemesBuilderTest.kt} | 16 +- .../ktorswaggerui/tests/ServersObjectTest.kt | 68 -- .../ktorswaggerui/tests/TagsObjectTest.kt | 87 -- .../tests/builders/InfoBuilderTest.kt | 80 ++ .../tests/builders/OperationBuilderTest.kt | 760 ++++++++++++++++++ .../tests/builders/PathsBuilderTest.kt | 126 +++ .../tests/builders/ServersBuilderTest.kt | 43 + .../tests/builders/TagsBuilderTest.kt | 54 ++ .../tests/schemas/OpenApiSchemaTest.kt | 288 +++++++ 22 files changed, 2013 insertions(+), 1969 deletions(-) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ComponentsObjectTest.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ExampleObjectTest.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoObjectTest.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathObjectTest.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{SecuritySchemeObjectTest.kt => SecuritySchemesBuilderTest.kt} (92%) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsObjectTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 9589697..7744db2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -24,8 +24,10 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.ktor.server.application.Application import io.ktor.server.application.ApplicationStarted import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent @@ -47,11 +49,9 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration if (application.pluginOrNull(Webjars) == null) { application.install(Webjars) } - val routes = RouteCollector(RouteDocumentationMerger()).collectRoutes(application, pluginConfig) - val schemaContext = SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) - .also { it.initialize(routes.toList()) } - apiSpecJson = Json.pretty(builder(pluginConfig, schemaContext).build(routes.toList())) -// apiSpecJson = ApiSpecBuilder().build(application, pluginConfig) + val routes = routes(application, pluginConfig) + val schemaContext = schemaContext(pluginConfig, routes) + apiSpecJson = Json.pretty(builder(pluginConfig, schemaContext).build(routes)) } SwaggerRouting( @@ -62,6 +62,16 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration } +private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig): List { + return RouteCollector(RouteDocumentationMerger()).collectRoutes(application, pluginConfig).toList() +} + +private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { + return SchemaContext( + config = pluginConfig, + jsonSchemaBuilder = JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build()) + ).initialize(routes.toList()) +} private fun builder(config: SwaggerUIPluginConfig, schemaContext: SchemaContext): OpenApiBuilder { return OpenApiBuilder( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt index 4054527..6a62191 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/InfoBuilder.kt @@ -13,6 +13,7 @@ class InfoBuilder( it.title = info.title it.version = info.version it.description = info.description + it.termsOfService = info.termsOfService info.getContact()?.also { contact -> it.contact = contactBuilder.build(contact) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index 9006dc5..4cc7d23 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -30,9 +30,10 @@ class SchemaContext( private val customSchemas = mutableMapOf>() - fun initialize(routes: Collection) { + fun initialize(routes: Collection): SchemaContext { routes.forEach { handle(it) } config.getDefaultUnauthorizedResponse()?.also { handle(it) } + return this } @@ -136,8 +137,12 @@ class SchemaContext( val componentSection = mutableMapOf>() schemas.forEach { (_, schemaInfo) -> val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! - if (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema) || isWrapperArray(rootSchema)) { + if (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema)) { // skip + } else if (isWrapperArray(rootSchema)) { + schemaInfo.schemas.toMutableMap() + .also { it.remove(schemaInfo.rootSchema) } + .also { componentSection.putAll(it) } } else { componentSection.putAll(schemaInfo.schemas) } @@ -192,7 +197,7 @@ class SchemaContext( private fun isPrimitive(schema: Schema<*>): Boolean { - return schema.type != "object" && schema.type != "array" + return schema.type != "object" && schema.type != "array" && schema.type != null } private fun isPrimitiveArray(schema: Schema<*>): Boolean { diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt index c57aae8..8583ca5 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt @@ -18,137 +18,6 @@ import io.swagger.v3.oas.models.servers.Server import io.swagger.v3.oas.models.tags.Tag -infix fun PathItem.shouldBePath(expectedBuilder: PathItem.() -> Unit) { - this shouldBePath PathItem().apply(expectedBuilder) -} - -infix fun PathItem.shouldBePath(expected: PathItem?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - assertNullSafe(this.get, expected.get) { assertPathOperation(this.get, expected.get) } - assertNullSafe(this.put, expected.put) { assertPathOperation(this.put, expected.put) } - assertNullSafe(this.post, expected.post) { assertPathOperation(this.post, expected.post) } - assertNullSafe(this.head, expected.head) { assertPathOperation(this.head, expected.head) } - assertNullSafe(this.delete, expected.delete) { assertPathOperation(this.delete, expected.delete) } - assertNullSafe(this.patch, expected.patch) { assertPathOperation(this.patch, expected.patch) } - assertNullSafe(this.options, expected.options) { assertPathOperation(this.options, expected.options) } -} - -fun assertPathOperation(actual: Operation, expected: Operation) { - assertNullSafe(actual.tags, expected.tags) { actual.tags shouldContainExactlyInAnyOrder expected.tags } - actual.summary shouldBe expected.summary - actual.description shouldBe expected.description - actual.operationId shouldBe expected.operationId - assertNullSafe(actual.parameters, expected.parameters) { - actual.parameters shouldHaveSize expected.parameters.size - actual.parameters.map { it.name } shouldContainExactlyInAnyOrder expected.parameters.map { it.name } - } - assertNullSafe(actual.requestBody, expected.requestBody) { - if(expected.requestBody.content == null) { - actual.requestBody.content.shouldBeNull() - } else { - actual.requestBody.content.shouldNotBeNull() - } - actual.requestBody.description shouldBe expected.requestBody.description - actual.requestBody.required shouldBe expected.requestBody.required - actual.requestBody.`$ref` shouldBe expected.requestBody.`$ref` - } - assertNullSafe(actual.responses, expected.responses) { - actual.responses.keys shouldContainExactlyInAnyOrder expected.responses.keys - } - assertNullSafe(actual.security, expected.security) { - actual.security shouldHaveSize expected.security.size - actual.security.flatMap { it.keys } shouldContainExactlyInAnyOrder expected.security.flatMap { it.keys } - } - actual.deprecated shouldBe if(expected.deprecated == null) false else expected.deprecated - assertNullSafe(actual.responses, expected.responses) { - actual.responses.keys shouldContainExactlyInAnyOrder expected.responses.keys - } - assertNullSafe(actual.security, expected.security) { - expected.security.forEachIndexed { index, expectedElement -> - val actualElement = actual.security[index] - actualElement.shouldNotBeNull() - assertMapEntries(actualElement, expectedElement) { _, actualEntry, expectedEntry -> - actualEntry shouldContainExactlyInAnyOrder expectedEntry - } - } - } -} - - -infix fun Content.shouldBeContent(expectedBuilder: Content.() -> Unit) { - this shouldBeContent Content().apply(expectedBuilder) -} - -infix fun Content.shouldBeContent(expected: Content?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - - assertMapEntries(this, expected) { _, actualMediaType, expectedMediaType -> - assertNullSafe(actualMediaType.schema, expectedMediaType.schema) { - actualMediaType.schema shouldBeSchema expectedMediaType.schema - } - if (expectedMediaType.example == null) { - actualMediaType.example.shouldBeNull() - } else { - actualMediaType.example.shouldNotBeNull() - } - assertMapEntries(actualMediaType.examples, expectedMediaType.examples) { _, actualExample, expectedExample -> - actualExample shouldBeExample expectedExample - } - } -} - - -infix fun Example.shouldBeExample(expectedBuilder: Example.() -> Unit) { - this shouldBeExample Example().apply(expectedBuilder) -} - -infix fun Example.shouldBeExample(expected: Example?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - this.summary shouldBe expected.summary - this.description shouldBe expected.description - if (expected.value == null) { - this.value.shouldBeNull() - } else { - this.value.shouldNotBeNull() - } - this.externalValue shouldBe expected.externalValue - this.`$ref` shouldBe expected.`$ref` -} - - -infix fun Tag.shouldBeTag(expectedBuilder: Tag.() -> Unit) { - this shouldBeTag Tag().apply(expectedBuilder) -} - -infix fun Tag.shouldBeTag(expected: Tag?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - this.name shouldBe expected.name - this.description shouldBe expected.description - assertNullSafe(this.externalDocs, expected.externalDocs) { - this.externalDocs.description shouldBe expected.externalDocs.description - this.externalDocs.url shouldBe expected.externalDocs.url - } -} infix fun SecurityScheme.shouldBeSecurityScheme(expectedBuilder: SecurityScheme.() -> Unit) { @@ -297,10 +166,6 @@ infix fun Schema<*>.shouldBeSchema(expected: Schema<*>?) { this.xml.wrapped shouldBe expected.xml.wrapped } -// this.discriminator shouldBe ... -// this.example shouldBe ... -// this.externalDocs shouldBe ... -// this.extensions shouldBe ... } private fun assertSchemaList(actual: List>?, expected: List>?) { @@ -323,15 +188,4 @@ private fun assertNullSafe(actual: T?, expected: T?, assertion: () -> Unit) actual.shouldNotBeNull() assertion() } -} - -private fun assertMapEntries(actual: Map?, expected: Map?, block: (key: K, actual: V, expected: V) -> Unit) { - assertNullSafe(actual, expected) { - actual!!.keys shouldContainExactlyInAnyOrder expected!!.keys - expected.keys.forEach { key -> - val valueActual = actual[key].let { it.shouldNotBeNull() } - val valueExpected = expected[key].let { it.shouldNotBeNull() } - block(key, valueActual, valueExpected) - } - } } \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt index 5e1b33f..d4d2f07 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt @@ -1,33 +1,8 @@ package io.github.smiley4.ktorswaggerui.tests -import io.github.smiley4.ktorswaggerui.specbuilder.OApiComponentsBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiContentBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiExampleBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiInfoBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiPathBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiPathsBuilder import io.github.smiley4.ktorswaggerui.specbuilder.OApiSchemaBuilder import io.github.smiley4.ktorswaggerui.specbuilder.OApiSecuritySchemesBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiServersBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiTagsBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.RouteCollector - -fun getOApiInfoBuilder() = OApiInfoBuilder() - -fun getOApiComponentsBuilder() = OApiComponentsBuilder() fun getOApiSchemaBuilder() = OApiSchemaBuilder() -fun getOApiExampleBuilder() = OApiExampleBuilder() - -fun getOApiContentBuilder() = OApiContentBuilder() - -fun getOApiPathBuilder() = OApiPathBuilder() - -fun getOApiPathsBuilder(routeCollector: RouteCollector) = OApiPathsBuilder(routeCollector) - -fun getOApiSecuritySchemesBuilder() = OApiSecuritySchemesBuilder() - -fun getOApiServersBuilder() = OApiServersBuilder() - -fun getOApiTagsBuilder() = OApiTagsBuilder() \ No newline at end of file +fun getOApiSecuritySchemesBuilder() = OApiSecuritySchemesBuilder() \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ComponentsObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ComponentsObjectTest.kt deleted file mode 100644 index dcf4bd1..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ComponentsObjectTest.kt +++ /dev/null @@ -1,240 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder -import io.kotest.matchers.maps.shouldHaveSize -import io.kotest.matchers.nulls.shouldBeNull -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import io.swagger.v3.oas.models.Components -import io.swagger.v3.oas.models.examples.Example -import io.swagger.v3.oas.models.media.Schema -import kotlin.reflect.KClass - -class ComponentsObjectTest : StringSpec({ - - "test nothing in components section" { - val context = ComponentsContext(false, mutableMapOf(), false, mutableMapOf(), false) - - buildSchema(ComponentsTestClass1::class, context).let { - it.type shouldBe "object" - it.properties shouldHaveSize 2 - it.`$ref`.shouldBeNull() - } - - buildSchema(ComponentsTestClass2::class, context).let { - it.type shouldBe "object" - it.properties shouldHaveSize 2 - it.`$ref`.shouldBeNull() - } - - buildSchema(Array::class, context).let { - it.type shouldBe "array" - it.items.shouldNotBeNull() - it.`$ref`.shouldBeNull() - it.items.shouldNotBeNull() - it.items.type shouldBe "object" - } - - buildExample("Example1", ComponentsTestClass1("test1", true), context).let { - it.value.shouldNotBeNull() - it.`$ref`.shouldBeNull() - } - - buildExample("Example1", ComponentsTestClass1("test1-different", false), context).let { - it.value.shouldNotBeNull() - it.`$ref`.shouldBeNull() - } - - buildExample("Example2", ComponentsTestClass2("testCounter", 42), context).let { - it.value.shouldNotBeNull() - it.`$ref`.shouldBeNull() - } - - buildComponentsObject(context).let { - it.schemas shouldHaveSize 0 - it.examples shouldHaveSize 0 - it.responses.shouldBeNull() - it.parameters.shouldBeNull() - it.requestBodies.shouldBeNull() - it.headers.shouldBeNull() - it.securitySchemes.shouldBeNull() - it.links.shouldBeNull() - it.callbacks.shouldBeNull() - it.extensions.shouldBeNull() - } - } - - "test schemas in components section" { - val context = ComponentsContext(true, mutableMapOf(), false, mutableMapOf(), false) - - buildSchema(ComponentsTestClass1::class, context).let { - it.type.shouldBeNull() - it.properties.shouldBeNull() - it.`$ref` shouldBe "#/components/schemas/ComponentsTestClass1" - } - - buildSchema(ComponentsTestClass2::class, context).let { - it.type.shouldBeNull() - it.properties.shouldBeNull() - it.`$ref` shouldBe "#/components/schemas/ComponentsTestClass2" - } - - buildSchema(Array::class, context).let { - it.type shouldBe "array" - it.properties.shouldBeNull() - it.`$ref`.shouldBeNull() - it.items.shouldNotBeNull() - it.items.type.shouldBeNull() - it.items.`$ref` shouldBe "#/components/schemas/ComponentsTestClass2" - } - - buildExample("Example1", ComponentsTestClass1("test1", true), context).let { - it.value.shouldNotBeNull() - it.`$ref`.shouldBeNull() - } - - buildExample("Example1", ComponentsTestClass1("test1-different", false), context).let { - it.value.shouldNotBeNull() - it.`$ref`.shouldBeNull() - } - - buildExample("Example2", ComponentsTestClass2("testCounter", 42), context).let { - it.value.shouldNotBeNull() - it.`$ref`.shouldBeNull() - } - - buildComponentsObject(context).let { - it.schemas shouldHaveSize 2 - it.schemas.keys shouldContainExactlyInAnyOrder listOf( - "ComponentsTestClass1", - "ComponentsTestClass2" - ) - it.schemas["ComponentsTestClass1"]!!.let { schema -> - schema.type shouldBe "object" - schema.properties shouldHaveSize 2 - schema.`$ref`.shouldBeNull() - } - it.schemas["ComponentsTestClass2"]!!.let { schema -> - schema.type shouldBe "object" - schema.properties shouldHaveSize 2 - schema.`$ref`.shouldBeNull() - } - it.examples shouldHaveSize 0 - it.responses.shouldBeNull() - it.parameters.shouldBeNull() - it.requestBodies.shouldBeNull() - it.headers.shouldBeNull() - it.securitySchemes.shouldBeNull() - it.links.shouldBeNull() - it.callbacks.shouldBeNull() - it.extensions.shouldBeNull() - } - } - - "test examples in components section" { - val context = ComponentsContext(false, mutableMapOf(), true, mutableMapOf(), false) - - buildSchema(ComponentsTestClass1::class, context).let { - it.type shouldBe "object" - it.properties shouldHaveSize 2 - it.`$ref`.shouldBeNull() - } - - buildSchema(ComponentsTestClass2::class, context).let { - it.type shouldBe "object" - it.properties shouldHaveSize 2 - it.`$ref`.shouldBeNull() - } - - buildSchema(Array::class, context).let { - it.type shouldBe "array" - it.items.shouldNotBeNull() - it.`$ref`.shouldBeNull() - it.items.shouldNotBeNull() - it.items.type shouldBe "object" - } - - buildExample("Example1", ComponentsTestClass1("test1", true), context).let { - it.value.shouldBeNull() - it.`$ref` shouldBe "#/components/examples/Example1" - } - - val exampleValue1Different = OpenApiExample(ComponentsTestClass1("test1-different", false)) - buildExample("Example1", exampleValue1Different, context).let { - it.value.shouldBeNull() - it.`$ref` shouldBe "#/components/examples/Example1#" + exampleValue1Different.hashCode().toString(16) - } - - buildExample("Example2", ComponentsTestClass2("testCounter", 42), context).let { - it.value.shouldBeNull() - it.`$ref` shouldBe "#/components/examples/Example2" - } - - buildComponentsObject(context).let { - it.examples shouldHaveSize 3 - it.examples.keys shouldContainExactlyInAnyOrder listOf( - "Example1", - "Example1#" + exampleValue1Different.hashCode().toString(16), - "Example2", - ) - it.examples.values.forEach { example -> - example.value.shouldNotBeNull() - example.`$ref`.shouldBeNull() - } - it.schemas shouldHaveSize 0 - it.responses.shouldBeNull() - it.parameters.shouldBeNull() - it.requestBodies.shouldBeNull() - it.headers.shouldBeNull() - it.securitySchemes.shouldBeNull() - it.links.shouldBeNull() - it.callbacks.shouldBeNull() - it.extensions.shouldBeNull() - } - } - - "test schemas in component section using canonical name object refs" { - val context = ComponentsContext(true, mutableMapOf(), false, mutableMapOf(), true) - - buildSchema(ComponentsTestClass1::class, context).let { - it.`$ref` shouldBe "#/components/schemas/io.github.smiley4.ktorswaggerui.tests.ComponentsObjectTest.Companion.ComponentsTestClass1" - } - } - -}) { - - companion object { - - private fun buildComponentsObject(context: ComponentsContext): Components { - return getOApiComponentsBuilder().build(context, listOf()) - } - - private fun buildSchema(type: KClass<*>, context: ComponentsContext): Schema<*> { - return getOApiSchemaBuilder().build(type.java, context, SwaggerUIPluginConfig()) - } - - private fun buildExample(name: String, example: Any, context: ComponentsContext): Example { - return getOApiExampleBuilder().build(name, OpenApiExample(example), context) - } - - private fun buildExample(name: String, example: OpenApiExample, context: ComponentsContext): Example { - return getOApiExampleBuilder().build(name, example, context) - } - - private data class ComponentsTestClass1( - val someText: String, - val someFlat: Boolean - ) - - private data class ComponentsTestClass2( - val name: String, - val counter: Int, - ) - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt index 63afd30..b21041b 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt @@ -1,319 +1,319 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.array -import io.github.smiley4.ktorswaggerui.dsl.obj -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.kotest.core.spec.style.StringSpec -import io.ktor.http.ContentType -import io.swagger.v3.oas.models.examples.Example -import io.swagger.v3.oas.models.media.Content -import io.swagger.v3.oas.models.media.Encoding -import io.swagger.v3.oas.models.media.MediaType -import io.swagger.v3.oas.models.media.Schema -import io.swagger.v3.oas.models.media.XML -import java.io.File -import kotlin.reflect.KClass - -class ContentObjectTest : StringSpec({ - - "test default (plain-text) content object" { - val content = buildContentObject(String::class) {} - content shouldBeContent { - addMediaType(ContentType.Text.Plain.toString(), MediaType().apply { - schema = Schema().apply { - type = "string" - xml = XML().apply { name = "String" } - } - }) - } - } - - "test default (json) content object" { - val content = buildContentObject(SimpleBody::class) {} - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "object" - xml = XML().apply { name = "SimpleBody" } - properties = mapOf( - "someText" to Schema().apply { - type = "string" - } - ) - } - }) - } - } - - "test complete (plain-text) content object" { - val content = buildContentObject(String::class) { - description = "Test Description" - required = true - example("Example1", "Example Value 1") - example("Example2", "Example Value 2") - } - content shouldBeContent { - addMediaType(ContentType.Text.Plain.toString(), MediaType().apply { - schema = Schema().apply { - type = "string" - xml = XML().apply { name = "String" } - } - examples = mapOf( - "Example1" to Example().apply { - value = "Example Value 1" - }, - "Example2" to Example().apply { - value = "Example Value 2" - } - ) - }) - } - } - - "test xml content object" { - val content = buildContentObject(SimpleBody::class) { - mediaType(ContentType.Application.Xml) - } - content shouldBeContent { - addMediaType(ContentType.Application.Xml.toString(), MediaType().apply { - schema = Schema().apply { - type = "object" - xml = XML().apply { name = "SimpleBody" } - properties = mapOf( - "someText" to Schema().apply { - type = "string" - } - ) - } - }) - } - } - - "test image content object" { - val content = buildContentObject(null) { - mediaType(ContentType.Image.SVG) - mediaType(ContentType.Image.PNG) - mediaType(ContentType.Image.JPEG) - } - content shouldBeContent { - addMediaType(ContentType.Image.SVG.toString(), MediaType()) - addMediaType(ContentType.Image.PNG.toString(), MediaType()) - addMediaType(ContentType.Image.JPEG.toString(), MediaType()) - } - } - - "test content object with custom (remote) json-schema" { - val content = buildCustomContentObject(obj("remote")) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "object" - `$ref` = "/my/test/schema" - } - }) - } - } - - "test content array with custom (remote) json-schema" { - val content = buildCustomContentObject(array("remote")) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "array" - items = Schema().apply { - type = "object" - `$ref` = "/my/test/schema" - } - } - }) - } - } - - "test content object with custom (remote) json-schema and components-section enabled" { - val content = buildCustomContentObject(obj("remote"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "object" - `$ref` = "/my/test/schema" - } - }) - } - } - - "test content array with custom (remote) json-schema and components-section enabled" { - val content = buildCustomContentObject(array("remote"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "array" - items = Schema().apply { - type = "object" - `$ref` = "/my/test/schema" - } - } - }) - } - } - - "test content object with custom json-schema" { - val content = buildCustomContentObject(obj("custom")) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "object" - properties = mapOf( - "someBoolean" to Schema().apply { - type = "boolean" - }, - "someText" to Schema().apply { - type = "string" - } - ) - } - }) - } - } - - "test content array with custom json-schema" { - val content = buildCustomContentObject(array("custom")) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "array" - items = Schema().apply { - type = "object" - properties = mapOf( - "someBoolean" to Schema().apply { - type = "boolean" - }, - "someText" to Schema().apply { - type = "string" - } - ) - } - } - }) - } - } - - "test content object with custom json-schema and components-section enabled" { - val content = buildCustomContentObject(obj("custom"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - `$ref` = "#/components/schemas/custom" - } - }) - } - } - - "test content array with custom json-schema and components-section enabled" { - val content = buildCustomContentObject(array("custom"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) - content shouldBeContent { - addMediaType(ContentType.Application.Json.toString(), MediaType().apply { - schema = Schema().apply { - type = "array" - items = Schema().apply { - `$ref` = "#/components/schemas/custom" - } - } - }) - } - } - - "test multipart content object" { - val content = buildMultipartContentObject { - description = "Test Description" - part("myFile") { - mediaTypes = setOf(ContentType.Image.JPEG, ContentType.Image.PNG) - } - part("myData") - } - content shouldBeContent { - addMediaType(ContentType.MultiPart.FormData.toString(), MediaType().apply { - schema = Schema().apply { - type = "object" - properties = mapOf( - "myFile" to Schema().apply { - type = "string" - format = "binary" - xml = XML().apply { name = "File" } - }, - "myData" to Schema().apply { - type = "object" - xml = XML().apply { name = "SimpleBody" } - properties = mapOf( - "someText" to Schema().apply { - type = "string" - } - ) - } - ) - } - addEncoding("myFile", Encoding().contentType("image/png, image/jpeg")) - }) - } - } - -}) { - - companion object { - - private fun pluginConfig() = SwaggerUIPluginConfig().apply { - schemas { - remote("remote", "/my/test/schema") - json("custom") { - """ - { - "type": "object", - "properties": { - "someBoolean": { - "type": "boolean" - }, - "someText": { - "type": "string" - } - } - } - """.trimIndent() - } - } - } - - private fun buildContentObject(schema: KClass<*>?, builder: OpenApiSimpleBody.() -> Unit): Content { - return buildContentObject(ComponentsContext.NOOP, schema, builder) - } - - private fun buildContentObject( - componentCtx: ComponentsContext, - type: KClass<*>?, - builder: OpenApiSimpleBody.() -> Unit - ): Content { - return getOApiContentBuilder().build(OpenApiSimpleBody(type?.java).apply(builder), componentCtx, pluginConfig()) - } - - private fun buildCustomContentObject(schema: CustomSchemaRef, componentCtx: ComponentsContext = ComponentsContext.NOOP): Content { - return getOApiContentBuilder().build(OpenApiSimpleBody(null) - .apply { customSchema = schema }, componentCtx, pluginConfig() - ) - } - - private fun buildMultipartContentObject(builder: OpenApiMultipartBody.() -> Unit): Content { - return getOApiContentBuilder().build( - OpenApiMultipartBody() - .apply(builder), ComponentsContext.NOOP, pluginConfig() - ) - } - - private data class SimpleBody( - val someText: String - ) - - } - -} \ No newline at end of file +//package io.github.smiley4.ktorswaggerui.tests +// +//import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +//import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef +//import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody +//import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +//import io.github.smiley4.ktorswaggerui.dsl.array +//import io.github.smiley4.ktorswaggerui.dsl.obj +//import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext +//import io.kotest.core.spec.style.StringSpec +//import io.ktor.http.ContentType +//import io.swagger.v3.oas.models.examples.Example +//import io.swagger.v3.oas.models.media.Content +//import io.swagger.v3.oas.models.media.Encoding +//import io.swagger.v3.oas.models.media.MediaType +//import io.swagger.v3.oas.models.media.Schema +//import io.swagger.v3.oas.models.media.XML +//import java.io.File +//import kotlin.reflect.KClass +// +//class ContentObjectTest : StringSpec({ +// +// "test default (plain-text) content object" { +// val content = buildContentObject(String::class) {} +// content shouldBeContent { +// addMediaType(ContentType.Text.Plain.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "string" +// xml = XML().apply { name = "String" } +// } +// }) +// } +// } +// +// "test default (json) content object" { +// val content = buildContentObject(SimpleBody::class) {} +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "object" +// xml = XML().apply { name = "SimpleBody" } +// properties = mapOf( +// "someText" to Schema().apply { +// type = "string" +// } +// ) +// } +// }) +// } +// } +// +// "test complete (plain-text) content object" { +// val content = buildContentObject(String::class) { +// description = "Test Description" +// required = true +// example("Example1", "Example Value 1") +// example("Example2", "Example Value 2") +// } +// content shouldBeContent { +// addMediaType(ContentType.Text.Plain.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "string" +// xml = XML().apply { name = "String" } +// } +// examples = mapOf( +// "Example1" to Example().apply { +// value = "Example Value 1" +// }, +// "Example2" to Example().apply { +// value = "Example Value 2" +// } +// ) +// }) +// } +// } +// +// "test xml content object" { +// val content = buildContentObject(SimpleBody::class) { +// mediaType(ContentType.Application.Xml) +// } +// content shouldBeContent { +// addMediaType(ContentType.Application.Xml.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "object" +// xml = XML().apply { name = "SimpleBody" } +// properties = mapOf( +// "someText" to Schema().apply { +// type = "string" +// } +// ) +// } +// }) +// } +// } +// +// "test image content object" { +// val content = buildContentObject(null) { +// mediaType(ContentType.Image.SVG) +// mediaType(ContentType.Image.PNG) +// mediaType(ContentType.Image.JPEG) +// } +// content shouldBeContent { +// addMediaType(ContentType.Image.SVG.toString(), MediaType()) +// addMediaType(ContentType.Image.PNG.toString(), MediaType()) +// addMediaType(ContentType.Image.JPEG.toString(), MediaType()) +// } +// } +// +// "test content object with custom (remote) json-schema" { +// val content = buildCustomContentObject(obj("remote")) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "object" +// `$ref` = "/my/test/schema" +// } +// }) +// } +// } +// +// "test content array with custom (remote) json-schema" { +// val content = buildCustomContentObject(array("remote")) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "object" +// `$ref` = "/my/test/schema" +// } +// } +// }) +// } +// } +// +// "test content object with custom (remote) json-schema and components-section enabled" { +// val content = buildCustomContentObject(obj("remote"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "object" +// `$ref` = "/my/test/schema" +// } +// }) +// } +// } +// +// "test content array with custom (remote) json-schema and components-section enabled" { +// val content = buildCustomContentObject(array("remote"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "object" +// `$ref` = "/my/test/schema" +// } +// } +// }) +// } +// } +// +// "test content object with custom json-schema" { +// val content = buildCustomContentObject(obj("custom")) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "object" +// properties = mapOf( +// "someBoolean" to Schema().apply { +// type = "boolean" +// }, +// "someText" to Schema().apply { +// type = "string" +// } +// ) +// } +// }) +// } +// } +// +// "test content array with custom json-schema" { +// val content = buildCustomContentObject(array("custom")) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "object" +// properties = mapOf( +// "someBoolean" to Schema().apply { +// type = "boolean" +// }, +// "someText" to Schema().apply { +// type = "string" +// } +// ) +// } +// } +// }) +// } +// } +// +// "test content object with custom json-schema and components-section enabled" { +// val content = buildCustomContentObject(obj("custom"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// `$ref` = "#/components/schemas/custom" +// } +// }) +// } +// } +// +// "test content array with custom json-schema and components-section enabled" { +// val content = buildCustomContentObject(array("custom"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) +// content shouldBeContent { +// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "array" +// items = Schema().apply { +// `$ref` = "#/components/schemas/custom" +// } +// } +// }) +// } +// } +// +// "test multipart content object" { +// val content = buildMultipartContentObject { +// description = "Test Description" +// part("myFile") { +// mediaTypes = setOf(ContentType.Image.JPEG, ContentType.Image.PNG) +// } +// part("myData") +// } +// content shouldBeContent { +// addMediaType(ContentType.MultiPart.FormData.toString(), MediaType().apply { +// schema = Schema().apply { +// type = "object" +// properties = mapOf( +// "myFile" to Schema().apply { +// type = "string" +// format = "binary" +// xml = XML().apply { name = "File" } +// }, +// "myData" to Schema().apply { +// type = "object" +// xml = XML().apply { name = "SimpleBody" } +// properties = mapOf( +// "someText" to Schema().apply { +// type = "string" +// } +// ) +// } +// ) +// } +// addEncoding("myFile", Encoding().contentType("image/png, image/jpeg")) +// }) +// } +// } +// +//}) { +// +// companion object { +// +// private fun pluginConfig() = SwaggerUIPluginConfig().apply { +// schemas { +// remote("remote", "/my/test/schema") +// json("custom") { +// """ +// { +// "type": "object", +// "properties": { +// "someBoolean": { +// "type": "boolean" +// }, +// "someText": { +// "type": "string" +// } +// } +// } +// """.trimIndent() +// } +// } +// } +// +// private fun buildContentObject(schema: KClass<*>?, builder: OpenApiSimpleBody.() -> Unit): Content { +// return buildContentObject(ComponentsContext.NOOP, schema, builder) +// } +// +// private fun buildContentObject( +// componentCtx: ComponentsContext, +// type: KClass<*>?, +// builder: OpenApiSimpleBody.() -> Unit +// ): Content { +// return getOApiContentBuilder().build(OpenApiSimpleBody(type?.java).apply(builder), componentCtx, pluginConfig()) +// } +// +// private fun buildCustomContentObject(schema: CustomSchemaRef, componentCtx: ComponentsContext = ComponentsContext.NOOP): Content { +// return getOApiContentBuilder().build(OpenApiSimpleBody(null) +// .apply { customSchema = schema }, componentCtx, pluginConfig() +// ) +// } +// +// private fun buildMultipartContentObject(builder: OpenApiMultipartBody.() -> Unit): Content { +// return getOApiContentBuilder().build( +// OpenApiMultipartBody() +// .apply(builder), ComponentsContext.NOOP, pluginConfig() +// ) +// } +// +// private data class SimpleBody( +// val someText: String +// ) +// +// } +// +//} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ExampleObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ExampleObjectTest.kt deleted file mode 100644 index 682147a..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ExampleObjectTest.kt +++ /dev/null @@ -1,67 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.kotest.core.spec.style.StringSpec -import io.swagger.v3.oas.models.examples.Example - -class ExampleObjectTest : StringSpec({ - - "test default example object" { - val exampleValue = ExampleTestClass("TestText", true) - val example = buildExampleObject("TestExample", exampleValue, ComponentsContext.NOOP) - example shouldBeExample { - value = exampleValue - } - } - - "test complete example object" { - val exampleValue = ExampleTestClass("TestText", true) - val example = buildExampleObject("TestExample", exampleValue, ComponentsContext.NOOP) { - summary = "Test Summary" - description = "Test Description" - } - example shouldBeExample { - value = exampleValue - summary = "Test Summary" - description = "Test Description" - } - } - - "test referencing example in components" { - val componentsContext = ComponentsContext(false, mutableMapOf(), true, mutableMapOf(), false) - val exampleValue = ExampleTestClass("TestText", true) - val example = buildExampleObject("TestExample", exampleValue, componentsContext) { - summary = "Test Summary" - description = "Test Description" - } - example shouldBeExample { - `$ref` = "#/components/examples/TestExample" - } - } - -}) { - - companion object { - - private fun buildExampleObject(name: String, example: Any, context: ComponentsContext): Example { - return getOApiExampleBuilder().build(name, OpenApiExample(example), context) - } - - private fun buildExampleObject( - name: String, - example: Any, - context: ComponentsContext, - builder: OpenApiExample.() -> Unit - ): Example { - return getOApiExampleBuilder().build(name, OpenApiExample(example).apply(builder), context) - } - - private data class ExampleTestClass( - val someText: String, - val someFlat: Boolean - ) - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoObjectTest.kt deleted file mode 100644 index 6647736..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoObjectTest.kt +++ /dev/null @@ -1,62 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo -import io.kotest.core.spec.style.StringSpec -import io.swagger.v3.oas.models.info.Contact -import io.swagger.v3.oas.models.info.Info -import io.swagger.v3.oas.models.info.License - -class InfoObjectTest : StringSpec({ - - "test default info object" { - val info = buildInfoObject {} - info shouldBeInfo { - title = "API" - version = "latest" - } - } - - "test complete info object" { - val info = buildInfoObject { - title = "Test Title" - version = "test" - description = "Test Description" - termsOfService = "Test Terms of Service" - contact { - name = "Test Contact Name" - url = "Test Contact URL" - email = "Test Contact Email" - } - license { - name = "Test License Name" - url = "Test License URL" - } - } - info shouldBeInfo { - title = "Test Title" - version = "test" - description = "Test Description" - termsOfService = "Test Terms of Service" - contact = Contact().apply { - name = "Test Contact Name" - url = "Test Contact URL" - email = "Test Contact Email" - } - license = License().apply { - name = "Test License Name" - url = "Test License URL" - } - } - } - -}) { - - companion object { - - private fun buildInfoObject(builder: OpenApiInfo.() -> Unit): Info { - return getOApiInfoBuilder().build(OpenApiInfo().apply(builder)) - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt deleted file mode 100644 index 75c9efd..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemToOpenApiSchema.kt +++ /dev/null @@ -1,146 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.core.type.TypeReference -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import com.github.victools.jsonschema.generator.FieldScope -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerationContext -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.generator.TypeScope -import com.github.victools.jsonschema.module.jackson.JacksonModule -import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.dsl.Example -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder -import io.kotest.core.spec.style.StringSpec -import io.swagger.v3.oas.models.media.Schema -import java.lang.reflect.Type - -class JsonSchemToOpenApiSchema : StringSpec({ - - - "test 1" { - val type: Type = object : TypeReference() {}.type - val schema = JsonSchemaBuilder(generatorConfig).build(type) - println(schema) - } - - "test 2" { - val type: Type = object : TypeReference() {}.type - val schema = JsonSchemaBuilder(generatorConfig).build(type) - println(schema) - } - -}) { - - companion object { - - private val generatorConfig = SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.DEFINITION_FOR_MAIN_SCHEMA) - .without(Option.INLINE_ALL_SCHEMAS) - .also { - it.forTypesInGeneral() - .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> - if (typeScope is FieldScope) { - typeScope.getAnnotation(io.swagger.v3.oas.annotations.media.Schema::class.java)?.also { annotation -> - if (annotation.example != "") { - objectNode.put("example", annotation.example) - } - } - typeScope.getAnnotation(Example::class.java)?.also { annotation -> - objectNode.put("example", annotation.value) - } - } - } - } - .build() - - data class GenericObject( - val flag: Boolean, - val data: T - ) - - data class SpecificObject( - val text: String, - val number: Long - ) - - data class Y(val a: String) - - data class X(val y: Y) - - enum class SimpleEnum { - RED, GREEN, BLUE - } - - data class SimpleDataClass( - val text: String, - val value: Float - ) - - data class DataClassWithMaps( - val mapStringValues: Map, - val mapLongValues: Map - ) - - data class AnotherDataClass( - val primitiveValue: Int, - val primitiveList: List, - private val privateValue: String, - val nestedClass: SimpleDataClass, - val nestedClassList: List - ) - - @JsonTypeInfo( - use = JsonTypeInfo.Id.CLASS, - include = JsonTypeInfo.As.PROPERTY, - property = "_type", - ) - @JsonSubTypes( - JsonSubTypes.Type(value = SubClassA::class), - JsonSubTypes.Type(value = SubClassB::class), - ) - abstract class Superclass( - val superField: String, - ) - - class SubClassA( - superField: String, - val subFieldA: Int - ) : Superclass(superField) - - class SubClassB( - superField: String, - val subFieldB: Boolean - ) : Superclass(superField) - - - data class ClassWithNestedAbstractClass( - val nestedClass: Superclass, - val someField: String - ) - - class ClassWithGenerics( - val genericField: T, - val genericList: List - ) - - class WrapperForClassWithGenerics( - val genericClass: ClassWithGenerics - ) - - } - - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt index 131fddc..423ae75 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt @@ -1,308 +1,308 @@ -package io.github.smiley4.ktorswaggerui.tests - -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.core.type.TypeReference -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.kotest.core.spec.style.StringSpec -import io.swagger.v3.oas.models.media.Schema - -class JsonSchemaGenerationTests : StringSpec({ - - "generate schema for a simple enum" { - getOApiSchemaBuilder().build(SimpleEnum::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "string" - enum = SimpleEnum.values().map { it.name } - } - } - - "generate schema for maps" { - getOApiSchemaBuilder().build(DataClassWithMaps::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "object" - properties = mapOf( - "mapStringValues" to Schema().apply { - type = "object" - additionalProperties = Schema().apply { - type = "string" - } - }, - "mapLongValues" to Schema().apply { - type = "object" - additionalProperties = Schema().apply { - type = "integer" - format = "int64" - } - }, - ) - } - } - - "generate schema for a list of simple classes" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "object" - properties = mapOf( - "text" to Schema().apply { - type = "string" - }, - "value" to Schema().apply { - type = "number" - format = "float" - } - ) - } - } - } - - "generate schema for a simple class" { - getOApiSchemaBuilder().build(SimpleDataClass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "object" - properties = mapOf( - "text" to Schema().apply { - type = "string" - }, - "value" to Schema().apply { - type = "number" - format = "float" - } - ) - } - } - - "generate schema for a another class" { - getOApiSchemaBuilder().build(AnotherDataClass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "object" - properties = mapOf( - "primitiveValue" to Schema().apply { - type = "integer" - format = "int32" - }, - "primitiveList" to Schema().apply { - type = "array" - items = Schema().apply { - type = "integer" - format = "int32" - } - }, - "nestedClass" to Schema().apply { - type = "object" - properties = mapOf( - "text" to Schema().apply { - type = "string" - }, - "value" to Schema().apply { - type = "number" - format = "float" - } - ) - }, - "nestedClassList" to Schema().apply { - type = "array" - items = Schema().apply { - type = "object" - properties = mapOf( - "text" to Schema().apply { - type = "string" - }, - "value" to Schema().apply { - type = "number" - format = "float" - } - ) - } - }, - ) - } - } - - "generate schema for a class with inheritance" { - getOApiSchemaBuilder().build(SubClassA::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "object" - properties = mapOf( - "superField" to Schema().apply { - type = "string" - }, - "subFieldA" to Schema().apply { - type = "integer" - format = "int32" - }, - "_type" to Schema().apply { - setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassA") - }, - ) - required = listOf("_type") - } - } - - "generate schema for a class with sub-classes" { - getOApiSchemaBuilder().build(Superclass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - anyOf = listOf( - Schema().apply { - type = "object" - properties = mapOf( - "superField" to Schema().apply { - type = "string" - }, - "subFieldA" to Schema().apply { - type = "integer" - format = "int32" - }, - "_type" to Schema().apply { - setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassA") - } - ) - required = listOf("_type") - }, - Schema().apply { - type = "object" - properties = mapOf( - "superField" to Schema().apply { - type = "string" - }, - "subFieldB" to Schema().apply { - type = "boolean" - }, - "_type" to Schema().apply { - setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassB") - } - ) - required = listOf("_type") - } - ) - } - } - - "generate schema for a class with nested generic type" { - getOApiSchemaBuilder().build( - WrapperForClassWithGenerics::class.java, - ComponentsContext.NOOP, - SwaggerUIPluginConfig() - ) shouldBeSchema { - type = "object" - properties = mapOf( - "genericClass" to Schema().apply { - type = "object" - properties = mapOf( - "genericField" to Schema().apply { - type = "string" - }, - "genericList" to Schema().apply { - type = "array" - items = Schema().apply { - type = "string" - } - } - ) - } - ) - } - } - - "generate schema for a class with generic types" { - getOApiSchemaBuilder().build( - getType>(), - ComponentsContext.NOOP, - SwaggerUIPluginConfig() - ) shouldBeSchema { - type = "object" - properties = mapOf( - "genericField" to Schema().apply { - type = "object" - properties = mapOf( - "text" to Schema().apply { - type = "string" - }, - "value" to Schema().apply { - type = "number" - format = "float" - } - ) - }, - "genericList" to Schema().apply { - type = "array" - items = Schema().apply { - type = "object" - properties = mapOf( - "text" to Schema().apply { - type = "string" - }, - "value" to Schema().apply { - type = "number" - format = "float" - } - ) - } - } - ) - } - } - -}) { - companion object { - - inline fun getType() = object : TypeReference() {}.type - - enum class SimpleEnum { - RED, GREEN, BLUE - } - - data class SimpleDataClass( - val text: String, - val value: Float - ) - - data class DataClassWithMaps( - val mapStringValues: Map, - val mapLongValues: Map - ) - - data class AnotherDataClass( - val primitiveValue: Int, - val primitiveList: List, - private val privateValue: String, - val nestedClass: SimpleDataClass, - val nestedClassList: List - ) - - @JsonTypeInfo( - use = JsonTypeInfo.Id.CLASS, - include = JsonTypeInfo.As.PROPERTY, - property = "_type", - ) - @JsonSubTypes( - JsonSubTypes.Type(value = SubClassA::class), - JsonSubTypes.Type(value = SubClassB::class), - ) - abstract class Superclass( - val superField: String, - ) - - class SubClassA( - superField: String, - val subFieldA: Int - ) : Superclass(superField) - - class SubClassB( - superField: String, - val subFieldB: Boolean - ) : Superclass(superField) - - - data class ClassWithNestedAbstractClass( - val nestedClass: Superclass, - val someField: String - ) - - class ClassWithGenerics( - val genericField: T, - val genericList: List - ) - - class WrapperForClassWithGenerics( - val genericClass: ClassWithGenerics - ) - - } -} +//package io.github.smiley4.ktorswaggerui.tests +// +//import com.fasterxml.jackson.annotation.JsonSubTypes +//import com.fasterxml.jackson.annotation.JsonTypeInfo +//import com.fasterxml.jackson.core.type.TypeReference +//import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +//import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext +//import io.kotest.core.spec.style.StringSpec +//import io.swagger.v3.oas.models.media.Schema +// +//class JsonSchemaGenerationTests : StringSpec({ +// +// "generate schema for a simple enum" { +// getOApiSchemaBuilder().build(SimpleEnum::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// type = "string" +// enum = SimpleEnum.values().map { it.name } +// } +// } +// +// "generate schema for maps" { +// getOApiSchemaBuilder().build(DataClassWithMaps::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// type = "object" +// properties = mapOf( +// "mapStringValues" to Schema().apply { +// type = "object" +// additionalProperties = Schema().apply { +// type = "string" +// } +// }, +// "mapLongValues" to Schema().apply { +// type = "object" +// additionalProperties = Schema().apply { +// type = "integer" +// format = "int64" +// } +// }, +// ) +// } +// } +// +// "generate schema for a list of simple classes" { +// getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// type = "array" +// items = Schema().apply { +// type = "object" +// properties = mapOf( +// "text" to Schema().apply { +// type = "string" +// }, +// "value" to Schema().apply { +// type = "number" +// format = "float" +// } +// ) +// } +// } +// } +// +// "generate schema for a simple class" { +// getOApiSchemaBuilder().build(SimpleDataClass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// type = "object" +// properties = mapOf( +// "text" to Schema().apply { +// type = "string" +// }, +// "value" to Schema().apply { +// type = "number" +// format = "float" +// } +// ) +// } +// } +// +// "generate schema for a another class" { +// getOApiSchemaBuilder().build(AnotherDataClass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// type = "object" +// properties = mapOf( +// "primitiveValue" to Schema().apply { +// type = "integer" +// format = "int32" +// }, +// "primitiveList" to Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "integer" +// format = "int32" +// } +// }, +// "nestedClass" to Schema().apply { +// type = "object" +// properties = mapOf( +// "text" to Schema().apply { +// type = "string" +// }, +// "value" to Schema().apply { +// type = "number" +// format = "float" +// } +// ) +// }, +// "nestedClassList" to Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "object" +// properties = mapOf( +// "text" to Schema().apply { +// type = "string" +// }, +// "value" to Schema().apply { +// type = "number" +// format = "float" +// } +// ) +// } +// }, +// ) +// } +// } +// +// "generate schema for a class with inheritance" { +// getOApiSchemaBuilder().build(SubClassA::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// type = "object" +// properties = mapOf( +// "superField" to Schema().apply { +// type = "string" +// }, +// "subFieldA" to Schema().apply { +// type = "integer" +// format = "int32" +// }, +// "_type" to Schema().apply { +// setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassA") +// }, +// ) +// required = listOf("_type") +// } +// } +// +// "generate schema for a class with sub-classes" { +// getOApiSchemaBuilder().build(Superclass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { +// anyOf = listOf( +// Schema().apply { +// type = "object" +// properties = mapOf( +// "superField" to Schema().apply { +// type = "string" +// }, +// "subFieldA" to Schema().apply { +// type = "integer" +// format = "int32" +// }, +// "_type" to Schema().apply { +// setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassA") +// } +// ) +// required = listOf("_type") +// }, +// Schema().apply { +// type = "object" +// properties = mapOf( +// "superField" to Schema().apply { +// type = "string" +// }, +// "subFieldB" to Schema().apply { +// type = "boolean" +// }, +// "_type" to Schema().apply { +// setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassB") +// } +// ) +// required = listOf("_type") +// } +// ) +// } +// } +// +// "generate schema for a class with nested generic type" { +// getOApiSchemaBuilder().build( +// WrapperForClassWithGenerics::class.java, +// ComponentsContext.NOOP, +// SwaggerUIPluginConfig() +// ) shouldBeSchema { +// type = "object" +// properties = mapOf( +// "genericClass" to Schema().apply { +// type = "object" +// properties = mapOf( +// "genericField" to Schema().apply { +// type = "string" +// }, +// "genericList" to Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "string" +// } +// } +// ) +// } +// ) +// } +// } +// +// "generate schema for a class with generic types" { +// getOApiSchemaBuilder().build( +// getType>(), +// ComponentsContext.NOOP, +// SwaggerUIPluginConfig() +// ) shouldBeSchema { +// type = "object" +// properties = mapOf( +// "genericField" to Schema().apply { +// type = "object" +// properties = mapOf( +// "text" to Schema().apply { +// type = "string" +// }, +// "value" to Schema().apply { +// type = "number" +// format = "float" +// } +// ) +// }, +// "genericList" to Schema().apply { +// type = "array" +// items = Schema().apply { +// type = "object" +// properties = mapOf( +// "text" to Schema().apply { +// type = "string" +// }, +// "value" to Schema().apply { +// type = "number" +// format = "float" +// } +// ) +// } +// } +// ) +// } +// } +// +//}) { +// companion object { +// +// inline fun getType() = object : TypeReference() {}.type +// +// enum class SimpleEnum { +// RED, GREEN, BLUE +// } +// +// data class SimpleDataClass( +// val text: String, +// val value: Float +// ) +// +// data class DataClassWithMaps( +// val mapStringValues: Map, +// val mapLongValues: Map +// ) +// +// data class AnotherDataClass( +// val primitiveValue: Int, +// val primitiveList: List, +// private val privateValue: String, +// val nestedClass: SimpleDataClass, +// val nestedClassList: List +// ) +// +// @JsonTypeInfo( +// use = JsonTypeInfo.Id.CLASS, +// include = JsonTypeInfo.As.PROPERTY, +// property = "_type", +// ) +// @JsonSubTypes( +// JsonSubTypes.Type(value = SubClassA::class), +// JsonSubTypes.Type(value = SubClassB::class), +// ) +// abstract class Superclass( +// val superField: String, +// ) +// +// class SubClassA( +// superField: String, +// val subFieldA: Int +// ) : Superclass(superField) +// +// class SubClassB( +// superField: String, +// val subFieldB: Boolean +// ) : Superclass(superField) +// +// +// data class ClassWithNestedAbstractClass( +// val nestedClass: Superclass, +// val someField: String +// ) +// +// class ClassWithGenerics( +// val genericField: T, +// val genericList: List +// ) +// +// class WrapperForClassWithGenerics( +// val genericClass: ClassWithGenerics +// ) +// +// } +//} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathObjectTest.kt deleted file mode 100644 index 84cde84..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathObjectTest.kt +++ /dev/null @@ -1,338 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.swagger.v3.oas.models.Operation -import io.swagger.v3.oas.models.PathItem -import io.swagger.v3.oas.models.headers.Header -import io.swagger.v3.oas.models.media.Content -import io.swagger.v3.oas.models.media.Schema -import io.swagger.v3.oas.models.parameters.Parameter -import io.swagger.v3.oas.models.parameters.RequestBody -import io.swagger.v3.oas.models.responses.ApiResponse -import io.swagger.v3.oas.models.responses.ApiResponses -import io.swagger.v3.oas.models.security.SecurityRequirement - -class PathObjectTest : StringSpec({ - - "test get-route" { - val path = buildPath(HttpMethod.Get, "test/path") {} - path.first shouldBe "test/path" - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - } - } - } - - "test route with operationId" { - val path = buildPath(HttpMethod.Get, "test/path") { - operationId = "testPath" - } - path.first shouldBe "test/path" - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - operationId = "testPath" - } - } - } - - "test post-route" { - val path = buildPath(HttpMethod.Post, "test/path") {} - path.first shouldBe "test/path" - path.second shouldBePath { - post = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - } - } - } - - "test complete route" { - val path = buildPath(HttpMethod.Get, "test/path") { - tags = mutableListOf("tag1", "tag2") - summary = "Test Summary" - description = "Test Description" - request { - queryParameter("testParam", Int::class) - body(String::class) { - description = "Test Request body" - required = true - } - } - response { - HttpStatusCode.OK to { - description = "Test OK-Response" - header("testHeader") { - description = "Test Header" - } - body(String::class) { - description = "Test Response body" - required = true - } - } - HttpStatusCode.Conflict to { - description = "Test Conflict-Response" - } - } - } - path.first shouldBe "test/path" - path.second shouldBePath { - get = Operation().apply { - tags = listOf("tag1", "tag2") - summary = "Test Summary" - description = "Test Description" - parameters = listOf( - Parameter().apply { - name = "testParam" - } - ) - requestBody = RequestBody().apply { - description = "Test Request body" - required = true - content = Content().apply {/*...*/ } - } - responses = ApiResponses().apply { - addApiResponse("200", ApiResponse().apply { - description = "Test OK-Response" - headers = mapOf( - "testHeader" to Header().apply { - description = "Test Header" - schema = Schema().apply { - type = "string" - } - } - ) - }) - addApiResponse("409", ApiResponse().apply { - description = "Test Conflict-Response" - }) - } - } - } - } - - "test automatic tag generator" { - val path = buildPath(HttpMethod.Get, "test/path", tagGenerator = tagGenerator()) { - tags = mutableListOf("tag1", "tag2") - } - path.second shouldBePath { - get = Operation().apply { - tags = listOf("tag1", "tag2", "test") - parameters = emptyList() - responses = ApiResponses() - } - } - } - - "test security scheme for non-protected path" { - val path = buildPath(HttpMethod.Get, "test/path") { - securitySchemeName = "TestAuth" - } - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - } - } - } - - "test security scheme for protected path" { - val path = buildProtectedPath(HttpMethod.Get, "test/path") { - securitySchemeName = "TestAuth" - } - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - security = listOf(SecurityRequirement().apply { - addList("TestAuth", emptyList()) - }) - } - } - } - - "test default security scheme for non-protected path" { - val path = buildPath(HttpMethod.Get, "test/path", defaultSecuritySchemeName = "DefaultAuth") {} - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - } - } - } - - "test default security scheme" { - val path = buildProtectedPath(HttpMethod.Get, "test/path", defaultSecuritySchemeName = "DefaultAuth") {} - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - security = listOf(SecurityRequirement().apply { - addList("DefaultAuth", emptyList()) - }) - } - } - } - - "test overwriting default security scheme" { - val path = buildProtectedPath(HttpMethod.Get, "test/path", defaultSecuritySchemeName = "DefaultAuth") { - securitySchemeName = "TestAuth" - } - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - security = listOf(SecurityRequirement().apply { - addList("TestAuth", emptyList()) - }) - } - } - } - - "test default unauthorized response for non-protected path" { - val path = buildPath(HttpMethod.Get, "test/path", defaultUnauthorizedResponse()) {} - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses() - } - } - } - - "test default unauthorized response" { - val path = buildProtectedPath(HttpMethod.Get, "test/path", defaultUnauthorizedResponse()) {} - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses().apply { - addApiResponse("401", ApiResponse().apply { - description = "Authentication failed" - }) - } - } - } - } - - "test overwriting default unauthorized response" { - val path = buildProtectedPath(HttpMethod.Get, "test/path", defaultUnauthorizedResponse()) { - response { - HttpStatusCode.Unauthorized to { - description = "Test Unauthorized-Response" - } - } - } - path.second shouldBePath { - get = Operation().apply { - tags = emptyList() - parameters = emptyList() - responses = ApiResponses().apply { - addApiResponse("401", ApiResponse().apply { - description = "Test Unauthorized-Response" - }) - } - } - } - } - -}) { - - companion object { - - private fun buildPath( - method: HttpMethod, - path: String, - defaultUnauthorizedResponse: OpenApiResponse? = null, - tagGenerator: ((url: List) -> String?)? = null, - defaultSecuritySchemeName: String? = null, - builder: OpenApiRoute.() -> Unit - ): Pair { - return getOApiPathBuilder().build( - routeMeta(method, path, builder), - ComponentsContext.NOOP, - SwaggerUIPluginConfig().apply { - if (defaultUnauthorizedResponse != null) { - this.defaultUnauthorizedResponse { - description = defaultUnauthorizedResponse.description - } - } - this.defaultSecuritySchemeName = defaultSecuritySchemeName - this.automaticTagGenerator = tagGenerator - } - ) - } - - private fun buildProtectedPath( - method: HttpMethod, - path: String, - defaultUnauthorizedResponse: OpenApiResponse? = null, - tagGenerator: ((url: List) -> String?)? = null, - defaultSecuritySchemeName: String? = null, - builder: OpenApiRoute.() -> Unit - ): Pair { - return getOApiPathBuilder().build( - protectedRouteMeta(method, path, builder), - ComponentsContext.NOOP, - SwaggerUIPluginConfig().apply { - if (defaultUnauthorizedResponse != null) { - this.defaultUnauthorizedResponse { - description = defaultUnauthorizedResponse.description - } - } - this.defaultSecuritySchemeName = defaultSecuritySchemeName - this.automaticTagGenerator = tagGenerator - } - ) - } - - private fun routeMeta(method: HttpMethod, path: String, builder: OpenApiRoute.() -> Unit): RouteMeta { - return RouteMeta( - path = path, - method = method, - documentation = OpenApiRoute().apply(builder), - protected = false - ) - } - - private fun protectedRouteMeta(method: HttpMethod, path: String, builder: OpenApiRoute.() -> Unit): RouteMeta { - return RouteMeta( - path = path, - method = method, - documentation = OpenApiRoute().apply(builder), - protected = true - ) - } - - private fun defaultUnauthorizedResponse(): OpenApiResponse { - return OpenApiResponse(HttpStatusCode.Unauthorized.value.toString()).apply { - description = "Authentication failed" - } - } - - private fun tagGenerator(): ((url: List) -> String?) { - return { url -> url.firstOrNull() } - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt deleted file mode 100644 index 2599b48..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsObjectTest.kt +++ /dev/null @@ -1,148 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.github.smiley4.ktorswaggerui.specbuilder.RouteCollector -import io.github.smiley4.ktorswaggerui.specbuilder.RouteMeta -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder -import io.kotest.matchers.maps.shouldHaveSize -import io.kotest.matchers.nulls.shouldNotBeNull -import io.ktor.http.HttpMethod -import io.ktor.server.application.Application -import io.mockk.every -import io.mockk.mockk -import io.swagger.v3.oas.models.Paths - -class PathsObjectTest : StringSpec({ - - "test paths" { - val config = pluginConfig {} - val paths = buildPaths( - config, listOf( - HttpMethod.Get to "/", - HttpMethod.Delete to "/test/path", - HttpMethod.Post to "/other/test/route", - ) - ) - paths shouldHaveSize 3 - paths.keys shouldContainExactlyInAnyOrder listOf( - "/", - "/test/path", - "/other/test/route" - ) - paths["/"]!!.get.shouldNotBeNull() - paths["/test/path"]!!.delete.shouldNotBeNull() - paths["/other/test/route"]!!.post.shouldNotBeNull() - } - - "test filter out swagger-ui routes" { - val config = pluginConfig { - swagger { - swaggerUrl = "swagger-ui" - forwardRoot = false - } - } - val paths = buildPaths( - config, listOf( - HttpMethod.Get to "/", - HttpMethod.Get to "/swagger-ui", - HttpMethod.Get to "swagger-ui", - HttpMethod.Get to "/swagger-ui/{filename}", - HttpMethod.Get to "swagger-ui/{filename}", - HttpMethod.Delete to "/test/path", - ) - ) - paths shouldHaveSize 2 - paths.keys shouldContainExactlyInAnyOrder listOf("/", "/test/path") - } - - "test filter out swagger-ui routes (forward root)" { - val config = pluginConfig { - swagger { - swaggerUrl = "swagger-ui" - forwardRoot = true - } - } - val paths = buildPaths( - config, listOf( - HttpMethod.Get to "/", - HttpMethod.Get to "/swagger-ui", - HttpMethod.Get to "swagger-ui", - HttpMethod.Get to "/swagger-ui/{filename}", - HttpMethod.Get to "swagger-ui/{filename}", - HttpMethod.Delete to "/test/path", - ) - ) - paths shouldHaveSize 1 - paths.keys shouldContainExactlyInAnyOrder listOf("/test/path") - } - - "test merge paths" { - val config = pluginConfig {} - val paths = buildPaths( - config, listOf( - HttpMethod.Get to "/different/path", - HttpMethod.Get to "/test/path", - HttpMethod.Post to "/test/path", - ) - ) - paths shouldHaveSize 2 - paths.keys shouldContainExactlyInAnyOrder listOf( - "/different/path", - "/test/path", - ) - paths["/different/path"]!!.get.shouldNotBeNull() - paths["/test/path"]!!.get.shouldNotBeNull() - paths["/test/path"]!!.post.shouldNotBeNull() - } - - "test filter paths" { - val config = pluginConfig { - pathFilter = {_, url -> url.firstOrNull() == "test"} - } - val paths = buildPaths( - config, listOf( - HttpMethod.Get to "/different/path", - HttpMethod.Get to "/test/path", - HttpMethod.Post to "/test/path/2", - ) - ) - paths shouldHaveSize 2 - paths.keys shouldContainExactlyInAnyOrder listOf( - "/test/path", - "/test/path/2", - ) - } - -}) { - - companion object { - - private fun buildPaths(config: SwaggerUIPluginConfig, routes: List>): Paths { - return getOApiPathsBuilder(routeCollector(routes)).build(config, application(), ComponentsContext.NOOP) - } - - private fun pluginConfig(builder: SwaggerUIPluginConfig.() -> Unit): SwaggerUIPluginConfig { - return SwaggerUIPluginConfig().apply(builder) - } - - private fun application() = mockk() - - private fun routeCollector(routes: List>) = mockk().also { - every { it.collectRoutes(any(), any()) } returns routes - .map { - RouteMeta( - method = it.first, - path = it.second, - documentation = OpenApiRoute(), - protected = false, - ) - } - .asSequence() - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt similarity index 92% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt index 938eb3b..f0b84c7 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemeObjectTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt @@ -4,6 +4,8 @@ import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation import io.github.smiley4.ktorswaggerui.dsl.AuthScheme import io.github.smiley4.ktorswaggerui.dsl.AuthType import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme +import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldContainKey @@ -13,7 +15,7 @@ import io.swagger.v3.oas.models.security.OAuthFlows import io.swagger.v3.oas.models.security.Scopes import io.swagger.v3.oas.models.security.SecurityScheme -class SecuritySchemeObjectTest : StringSpec({ +class SecuritySchemesBuilderTest : StringSpec({ "test default security scheme object" { val securityScheme = buildSecuritySchemeObject("TestAuth") {} @@ -146,11 +148,13 @@ class SecuritySchemeObjectTest : StringSpec({ companion object { private fun buildSecuritySchemeObject(name: String, builder: OpenApiSecurityScheme.() -> Unit): SecurityScheme { - return getOApiSecuritySchemesBuilder().build(listOf(OpenApiSecurityScheme(name).apply(builder))).let { - it shouldHaveSize 1 - it shouldContainKey name - it[name]!! - } + return SecuritySchemesBuilder(OAuthFlowsBuilder()) + .build(listOf(OpenApiSecurityScheme(name).apply(builder))) + .let { + it shouldHaveSize 1 + it shouldContainKey name + it[name]!! + } } private fun buildSecuritySchemeObjects(builders: Map Unit>): List { diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt deleted file mode 100644 index 23c3b39..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersObjectTest.kt +++ /dev/null @@ -1,68 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldHaveSize -import io.swagger.v3.oas.models.servers.Server - -class ServersObjectTest : StringSpec({ - - "test default server object" { - val server = buildServerObject {} - server shouldBeServer { - url = "/" - } - } - - "test complete server object" { - val server = buildServerObject { - url = "Test URL" - description = "Test Description" - } - server shouldBeServer { - url = "Test URL" - description = "Test Description" - } - } - - "test multiple server objects" { - val servers = buildServerObjects( - { - url = "Test URL 1" - description = "Test Description 1" - }, - { - url = "Test URL 2" - description = "Test Description 2" - } - ) - servers[0] shouldBeServer { - url = "Test URL 1" - description = "Test Description 1" - } - servers[1] shouldBeServer { - url = "Test URL 2" - description = "Test Description 2" - } - } - -}) { - - companion object { - - private fun buildServerObject(builder: OpenApiServer.() -> Unit): Server { - return getOApiServersBuilder().build(listOf(OpenApiServer().apply(builder))).let { - it shouldHaveSize 1 - it.first() - } - } - - private fun buildServerObjects(vararg builder: OpenApiServer.() -> Unit): List { - return getOApiServersBuilder().build(builder.map { OpenApiServer().apply(it) }).also { - it shouldHaveSize builder.size - } - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsObjectTest.kt deleted file mode 100644 index c66d120..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsObjectTest.kt +++ /dev/null @@ -1,87 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldHaveSize -import io.swagger.v3.oas.models.ExternalDocumentation -import io.swagger.v3.oas.models.tags.Tag - -class TagsObjectTest : StringSpec({ - - "test default tag object" { - val tag = buildTagsObject("TestTag") {} - tag shouldBeTag { - name = "TestTag" - } - } - - "test complete tag object" { - val tag = buildTagsObject("TestTag") { - description = "Test Description" - externalDocDescription = "External Doc Description" - externalDocUrl = "External Doc URL" - } - tag shouldBeTag { - name = "TestTag" - description = "Test Description" - externalDocs = ExternalDocumentation().apply { - description = "External Doc Description" - url = "External Doc URL" - } - } - } - - "test multiple tag objects" { - val tags = buildTagsObjects(mapOf( - "TestTag 1" to { - description = "Test Description 1" - externalDocDescription = "External Doc Description 1" - externalDocUrl = "External Doc URL 1" - }, - "TestTag 2" to { - description = "Test Description 2" - externalDocDescription = "External Doc Description 2" - externalDocUrl = "External Doc URL 2" - } - )) - tags[0] shouldBeTag { - name = "TestTag 1" - description = "Test Description 1" - externalDocs = ExternalDocumentation().apply { - description = "External Doc Description 1" - url = "External Doc URL 1" - } - } - tags[1] shouldBeTag { - name = "TestTag 2" - description = "Test Description 2" - externalDocs = ExternalDocumentation().apply { - description = "External Doc Description 2" - url = "External Doc URL 2" - } - } - } - -}) { - - companion object { - - private fun buildTagsObject(name: String, builder: OpenApiTag.() -> Unit): Tag { - return getOApiTagsBuilder().build(listOf(OpenApiTag(name).apply(builder))).let { - it shouldHaveSize 1 - it.first() - } - } - - private fun buildTagsObjects(builders: Map Unit>): List { - val tags = mutableListOf() - builders.forEach { (name, builder) -> - tags.addAll(getOApiTagsBuilder().build(listOf(OpenApiTag(name).apply(builder)))) - } - tags shouldHaveSize builders.size - return tags - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt new file mode 100644 index 0000000..55e0be2 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt @@ -0,0 +1,80 @@ +package io.github.smiley4.ktorswaggerui.tests.builders + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo +import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.LicenseBuilder +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.swagger.v3.oas.models.info.Info + + +class InfoBuilderTest : StringSpec({ + + "empty info object" { + buildInfoObject {}.also { info -> + info.title shouldBe "API" + info.version shouldBe "latest" + info.description shouldBe null + info.termsOfService shouldBe null + info.contact shouldBe null + info.license shouldBe null + info.extensions shouldBe null + info.summary shouldBe null + } + } + + "full info object" { + buildInfoObject { + title = "Test Api" + version = "1.0" + description = "Api for testing" + termsOfService = "test-tos" + contact { + name = "Test Person" + url = "example.com" + email = "test.mail" + + } + license { + name = "Test License" + url = "example.com" + } + }.also { info -> + info.title shouldBe "Test Api" + info.version shouldBe "1.0" + info.description shouldBe "Api for testing" + info.termsOfService shouldBe "test-tos" + info.contact + .also { contact -> contact.shouldNotBeNull() } + ?.also { contact -> + contact.name shouldBe "Test Person" + contact.url shouldBe "example.com" + contact.email shouldBe "test.mail" + } + info.license + .also { license -> license.shouldNotBeNull() } + ?.also { license -> + license.name shouldBe "Test License" + license.url shouldBe "example.com" + } + info.extensions shouldBe null + info.summary shouldBe null + } + } + +}) { + + companion object { + + private fun buildInfoObject(builder: OpenApiInfo.() -> Unit): Info { + return InfoBuilder( + contactBuilder = ContactBuilder(), + licenseBuilder = LicenseBuilder() + ).build(OpenApiInfo().apply(builder)) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt new file mode 100644 index 0000000..4287e54 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt @@ -0,0 +1,760 @@ +package io.github.smiley4.ktorswaggerui.tests.builders + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.ktor.http.ContentType +import io.ktor.http.HttpMethod +import io.ktor.http.HttpStatusCode +import io.swagger.v3.oas.models.Operation +import java.io.File + +class OperationBuilderTest : StringSpec({ + + "empty operation" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute(), + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.tags.shouldBeEmpty() + operation.summary shouldBe null + operation.description shouldBe null + operation.externalDocs shouldBe null + operation.operationId shouldBe null + operation.parameters.shouldBeEmpty() + operation.requestBody shouldBe null + operation.responses.shouldBeEmpty() + operation.deprecated shouldBe false + operation.security shouldBe null + operation.servers shouldBe null + operation.extensions shouldBe null + } + } + + "basic operation" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.tags = listOf("tag1", "tag2") + route.description = "route for testing" + route.summary = "this is some test route" + route.operationId = "testRoute" + route.deprecated = true + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.tags shouldContainExactlyInAnyOrder listOf("tag1", "tag2") + operation.summary shouldBe "this is some test route" + operation.description shouldBe "route for testing" + operation.externalDocs shouldBe null + operation.operationId shouldBe "testRoute" + operation.parameters.shouldBeEmpty() + operation.requestBody shouldBe null + operation.responses.shouldBeEmpty() + operation.deprecated shouldBe true + operation.security shouldBe null + operation.servers shouldBe null + operation.extensions shouldBe null + } + } + + "protected route with security-scheme-names" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.securitySchemeNames = listOf("security1", "security2") + }, + protected = true + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.security + .also { it.shouldNotBeNull() } + ?.also { security -> + security shouldHaveSize 2 + security.find { it.containsKey("security1") }.shouldNotBeNull() + security.find { it.containsKey("security2") }.shouldNotBeNull() + } + } + } + + "protected route without security-scheme-names" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute(), + protected = true + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.tags.shouldBeEmpty() + operation.summary shouldBe null + operation.description shouldBe null + operation.externalDocs shouldBe null + operation.operationId shouldBe null + operation.parameters.shouldBeEmpty() + operation.requestBody shouldBe null + operation.responses.shouldBeEmpty() + operation.deprecated shouldBe false + operation.security shouldBe null + operation.servers shouldBe null + operation.extensions shouldBe null + } + } + + "unprotected route with security-scheme-names" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.securitySchemeNames = listOf("security1", "security2") + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.tags.shouldBeEmpty() + operation.summary shouldBe null + operation.description shouldBe null + operation.externalDocs shouldBe null + operation.operationId shouldBe null + operation.parameters.shouldBeEmpty() + operation.requestBody shouldBe null + operation.responses.shouldBeEmpty() + operation.deprecated shouldBe false + operation.security shouldBe null + operation.servers shouldBe null + operation.extensions shouldBe null + } + } + + "route with basic request" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + queryParameter("queryParam") + pathParameter("pathParam") + headerParameter("headerParam") + body>() + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.tags.shouldBeEmpty() + operation.summary shouldBe null + operation.description shouldBe null + operation.externalDocs shouldBe null + operation.operationId shouldBe null + operation.parameters.also { parameters -> + parameters shouldHaveSize 3 + parameters.find { it.name == "queryParam" } + .also { it.shouldNotBeNull() } + ?.also { param -> + param.`in` shouldBe "query" + param.description shouldBe null + param.required shouldBe null + param.deprecated shouldBe null + param.allowEmptyValue shouldBe null + param.`$ref` shouldBe null + param.style shouldBe null + param.explode shouldBe null + param.allowReserved shouldBe null + param.schema + .also { it.shouldNotBeNull() } + ?.also { it.type = "string" } + param.example shouldBe null + param.examples shouldBe null + param.content shouldBe null + param.extensions shouldBe null + } + parameters.find { it.name == "pathParam" } + .also { it.shouldNotBeNull() } + ?.also { param -> + param.`in` shouldBe "path" + param.description shouldBe null + param.required shouldBe null + param.deprecated shouldBe null + param.allowEmptyValue shouldBe null + param.`$ref` shouldBe null + param.style shouldBe null + param.explode shouldBe null + param.allowReserved shouldBe null + param.schema + .also { it.shouldNotBeNull() } + ?.also { it.type = "integer" } + param.example shouldBe null + param.examples shouldBe null + param.content shouldBe null + param.extensions shouldBe null + } + parameters.find { it.name == "headerParam" } + .also { it.shouldNotBeNull() } + ?.also { param -> + param.`in` shouldBe "header" + param.description shouldBe null + param.required shouldBe null + param.deprecated shouldBe null + param.allowEmptyValue shouldBe null + param.`$ref` shouldBe null + param.style shouldBe null + param.explode shouldBe null + param.allowReserved shouldBe null + param.schema + .also { it.shouldNotBeNull() } + ?.also { it.type = "boolean" } + param.example shouldBe null + param.examples shouldBe null + param.content shouldBe null + param.extensions shouldBe null + } + } + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.description shouldBe null + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 1 + content.get("application/json") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { schema -> + schema.type shouldBe "array" + schema.items.also { item -> item.type shouldBe "string" } + } + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + + } + body.required shouldBe null + body.extensions shouldBe null + body.`$ref` shouldBe null + } + operation.responses.shouldBeEmpty() + operation.deprecated shouldBe false + operation.security shouldBe null + operation.servers shouldBe null + operation.extensions shouldBe null + } + } + + "route with basic response" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.response { + "test" to { + description = "Test Response" + header("test-header") + body>() + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.tags.shouldBeEmpty() + operation.summary shouldBe null + operation.description shouldBe null + operation.externalDocs shouldBe null + operation.operationId shouldBe null + operation.parameters.shouldBeEmpty() + operation.requestBody shouldBe null + operation.responses + .also { it shouldHaveSize 1 } + .let { it.get("test") } + .also { it.shouldNotBeNull() } + ?.also { response -> + response.description shouldBe "Test Response" + response.headers + .also { it.shouldNotBeNull() } + .let { it["test-header"] } + .also { it.shouldNotBeNull() } + ?.also { header -> + header.schema + .also { it.shouldNotBeNull() } + ?.also { it.type shouldBe "string" } + } + + response.content + .also { it.shouldNotBeNull() } + .let { it.get("application/json") } + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { schema -> + schema.type shouldBe "array" + schema.items.also { item -> item.type shouldBe "string" } + } + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + response.links shouldBe null + response.extensions shouldBe null + response.`$ref` shouldBe null + } + operation.deprecated shouldBe false + operation.security shouldBe null + operation.servers shouldBe null + operation.extensions shouldBe null + } + } + + "documented parameter" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + queryParameter("param") { + description = "test parameter" + example = "MyExample" + required = true + deprecated = true + allowEmptyValue = true + explode = true + allowReserved = true + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.parameters.also { parameters -> + parameters shouldHaveSize 1 + parameters[0].also { param -> + param.`in` shouldBe "query" + param.description shouldBe "test parameter" + param.required shouldBe true + param.deprecated shouldBe true + param.allowEmptyValue shouldBe true + param.`$ref` shouldBe null + param.style shouldBe null + param.explode shouldBe true + param.allowReserved shouldBe true + param.schema + .also { it.shouldNotBeNull() } + ?.also { it.type = "string" } + param.example shouldBe "MyExample" + param.examples shouldBe null + param.content shouldBe null + param.extensions shouldBe null + } + } + } + } + + "documented body" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + body { + description = "the test body" + required = true + mediaType(ContentType.Application.Json) + mediaType(ContentType.Application.Xml) + example("example 1", "MyExample1") { + summary = "the example 1" + description = "the first example" + } + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.description shouldBe "the test body" + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 2 + content.get("application/json") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { it.type shouldBe "string" } + mediaType.example shouldBe null + mediaType.examples + .also { it shouldHaveSize 1 } + .let { it["example 1"] } + .also { it.shouldNotBeNull() } + ?.also { example -> + example.summary shouldBe "the example 1" + example.description shouldBe "the first example" + example.value shouldBe "MyExample1" + example.externalValue shouldBe null + example.`$ref` shouldBe null + example.extensions shouldBe null + example.valueSetFlag shouldBe true + } + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + content.get("application/xml") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { it.type shouldBe "string" } + mediaType.example shouldBe null + mediaType.examples + .also { it shouldHaveSize 1 } + .let { it["example 1"] } + .also { it.shouldNotBeNull() } + ?.also { example -> + example.summary shouldBe "the example 1" + example.description shouldBe "the first example" + example.value shouldBe "MyExample1" + example.externalValue shouldBe null + example.`$ref` shouldBe null + example.extensions shouldBe null + example.valueSetFlag shouldBe true + } + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + } + body.required shouldBe true + body.extensions shouldBe null + body.`$ref` shouldBe null + } + } + } + + "multipart body" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + multipartBody { + mediaType(ContentType.MultiPart.FormData) + part("image") { + mediaTypes = setOf( + ContentType.Image.PNG, + ContentType.Image.JPEG, + ContentType.Image.GIF + ) + } + part("data") { + mediaTypes = setOf(ContentType.Text.Plain) + } + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 1 + content.get("multipart/form-data") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("image", "data") + + } + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding + .also { it shouldHaveSize 2 } + .also { it.keys shouldContainExactlyInAnyOrder listOf("image", "data") } + .also { encoding -> + encoding.get("image")?.also { image -> + image.contentType shouldBe "image/png, image/jpeg, image/gif" + image.headers shouldHaveSize 0 + image.style shouldBe null + image.explode shouldBe null + image.allowReserved shouldBe null + image.extensions shouldBe null + } + encoding.get("data")?.also { data -> + data.contentType shouldBe "text/plain" + data.headers shouldHaveSize 0 + data.style shouldBe null + data.explode shouldBe null + data.allowReserved shouldBe null + data.extensions shouldBe null + } + } + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + } + } + } + } + + "multiple responses" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.response { + default { + description = "Default Response" + } + HttpStatusCode.OK to { + description = "Successful Request" + } + HttpStatusCode.InternalServerError to { + description = "Failed Request" + } + "Custom" to { + description = "Custom Response" + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.responses + .also { it shouldHaveSize 4 } + ?.also { responses -> + responses.get("default") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Default Response" } + responses.get("200") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Successful Request" } + responses.get("500") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Failed Request" } + responses.get("Custom") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Custom Response" } + } + } + } + + "automatic unauthorized response for protected route" { + val config = SwaggerUIPluginConfig().also { + it.defaultUnauthorizedResponse { + description = "Default unauthorized Response" + } + } + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.response { + default { + description = "Default Response" + } + } + }, + protected = true + ) + val schemaContext = schemaContext(config).initialize(listOf(route)) + buildOperationObject(route, schemaContext, config).also { operation -> + operation.responses + .also { it shouldHaveSize 2 } + ?.also { responses -> + responses.get("401") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Default unauthorized Response" } + responses.get("default") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Default Response" } + } + } + } + + "automatic unauthorized response for unprotected route" { + val config = SwaggerUIPluginConfig().also { + it.defaultUnauthorizedResponse { + description = "Default unauthorized Response" + } + } + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.response { + default { + description = "Default Response" + } + } + }, + protected = false + ) + val schemaContext = schemaContext(config).initialize(listOf(route)) + buildOperationObject(route, schemaContext, config).also { operation -> + operation.responses + .also { it shouldHaveSize 1 } + ?.also { responses -> + responses.get("default") + .also { it.shouldNotBeNull() } + ?.also { it.description shouldBe "Default Response" } + } + } + } + + "complex body datatype" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + body>() + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.description shouldBe null + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 1 + content.get("application/json") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { schema -> + schema.type shouldBe "array" + schema.items.also { item -> + item.type shouldBe null + item.`$ref` shouldBe "#/components/schemas/SimpleObject" + } + } + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + + } + body.required shouldBe null + body.extensions shouldBe null + body.`$ref` shouldBe null + } + } + schemaContext.getComponentSection().also { section -> + section.keys shouldContainExactlyInAnyOrder listOf("SimpleObject") + section["SimpleObject"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("number", "text") + } + } + } + +}) { + + companion object { + + private data class SimpleObject( + val text: String, + val number: Int + ) + + private val defaultPluginConfig = SwaggerUIPluginConfig() + + private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + } + + private fun buildOperationObject( + route: RouteMeta, + schemaContext: SchemaContext, + pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + ): Operation { + return OperationBuilder( + operationTagsBuilder = OperationTagsBuilder(pluginConfig), + parameterBuilder = ParameterBuilder(schemaContext), + requestBodyBuilder = RequestBodyBuilder( + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + responsesBuilder = ResponsesBuilder( + responseBuilder = ResponseBuilder( + headerBuilder = HeaderBuilder(schemaContext), + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + config = pluginConfig + ), + securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfig), + ).build(route) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt new file mode 100644 index 0000000..6830943 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt @@ -0,0 +1,126 @@ +package io.github.smiley4.ktorswaggerui.tests.builders + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.ktor.http.HttpMethod +import io.swagger.v3.oas.models.Paths + +class PathsBuilderTest : StringSpec({ + + "simple paths" { + val routes = listOf( + route(HttpMethod.Get, "/"), + route(HttpMethod.Delete, "/test/path"), + route(HttpMethod.Post, "/other/test/route") + ) + val schemaContext = schemaContext().initialize(routes) + buildPathsObject(routes, schemaContext).also { paths -> + paths shouldHaveSize 3 + paths.keys shouldContainExactlyInAnyOrder listOf( + "/", + "/test/path", + "/other/test/route" + ) + paths["/"]!!.get.shouldNotBeNull() + paths["/test/path"]!!.delete.shouldNotBeNull() + paths["/other/test/route"]!!.post.shouldNotBeNull() + } + } + "merge paths" { + val config = defaultPluginConfig.also { + it.swagger { + swaggerUrl = "swagger-ui" + forwardRoot = true + } + } + val routes = listOf( + route(HttpMethod.Get, "/different/path"), + route(HttpMethod.Get, "/test/path"), + route(HttpMethod.Post, "/test/path"), + ) + val schemaContext = schemaContext().initialize(routes) + buildPathsObject(routes, schemaContext, config).also { paths -> + paths shouldHaveSize 2 + paths.keys shouldContainExactlyInAnyOrder listOf( + "/different/path", + "/test/path" + ) + paths["/different/path"]!!.get.shouldNotBeNull() + paths["/test/path"]!!.get.shouldNotBeNull() + paths["/test/path"]!!.post.shouldNotBeNull() + } + } + +}) { + + companion object { + + private fun route(method: HttpMethod, url: String) = RouteMeta( + path = url, + method = method, + documentation = OpenApiRoute(), + protected = false + ) + + private val defaultPluginConfig = SwaggerUIPluginConfig() + + private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + } + + private fun buildPathsObject( + routes: Collection, + schemaContext: SchemaContext, + pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + ): Paths { + return PathsBuilder( + pathBuilder = PathBuilder( + operationBuilder = OperationBuilder( + operationTagsBuilder = OperationTagsBuilder(pluginConfig), + parameterBuilder = ParameterBuilder(schemaContext), + requestBodyBuilder = RequestBodyBuilder( + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + responsesBuilder = ResponsesBuilder( + responseBuilder = ResponseBuilder( + headerBuilder = HeaderBuilder(schemaContext), + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + config = pluginConfig + ), + securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfig), + ) + ) + ).build(routes) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt new file mode 100644 index 0000000..7d3c31c --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt @@ -0,0 +1,43 @@ +package io.github.smiley4.ktorswaggerui.tests.builders + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer +import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.shouldBe +import io.swagger.v3.oas.models.servers.Server + +class ServersBuilderTest : StringSpec({ + + "default server object" { + buildServerObject {}.also { server -> + server.url shouldBe "/" + server.description shouldBe null + server.variables shouldBe null + server.extensions shouldBe null + + } + } + + "complete server object" { + buildServerObject { + url = "Test URL" + description = "Test Description" + }.also { server -> + server.url shouldBe "Test URL" + server.description shouldBe "Test Description" + server.variables shouldBe null + server.extensions shouldBe null + } + } + +}) { + + companion object { + + private fun buildServerObject(builder: OpenApiServer.() -> Unit): Server { + return ServerBuilder().build(OpenApiServer().apply(builder)) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt new file mode 100644 index 0000000..ae8b51e --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt @@ -0,0 +1,54 @@ +package io.github.smiley4.ktorswaggerui.tests.builders + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag +import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.swagger.v3.oas.models.tags.Tag + + +class TagsBuilderTest : StringSpec({ + + "empty tag object" { + buildTagObject("test-tag") {}.also { tag -> + tag.name shouldBe "test-tag" + tag.description shouldBe null + tag.externalDocs shouldBe null + tag.extensions shouldBe null + } + } + + "full tag object" { + buildTagObject("test-tag") { + description = "Description of tag" + externalDocDescription = "Description of external docs" + externalDocUrl = "example.com" + }.also { tag -> + tag.name shouldBe "test-tag" + tag.description shouldBe "Description of tag" + tag.externalDocs + .also { docs -> docs.shouldNotBeNull() } + ?.also { docs -> + docs.description shouldBe "Description of external docs" + docs.url shouldBe "example.com" + docs.extensions shouldBe null + } + tag.extensions shouldBe null + } + } + +}) { + + companion object { + + private fun buildTagObject(name: String, builder: OpenApiTag.() -> Unit): Tag { + return TagBuilder( + externalDocumentationBuilder = ExternalDocumentationBuilder() + ).build(OpenApiTag(name).apply(builder)) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt new file mode 100644 index 0000000..a8b88b3 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt @@ -0,0 +1,288 @@ +package io.github.smiley4.ktorswaggerui.tests.schemas + +import com.fasterxml.jackson.core.type.TypeReference +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.shouldBe +import io.ktor.http.HttpMethod + +class OpenApiSchemaTest : StringSpec({ + + "route with all schemas" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + queryParameter("queryParam") + pathParameter("pathParam") + headerParameter("headerParam") + body() + } + response { + default { + header("header") + body() + } + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(QueryParamType::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/QueryParamType" + } + schemaContext.getSchema(PathParamType::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/PathParamType" + } + schemaContext.getSchema(HeaderParamType::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/HeaderParamType" + } + schemaContext.getSchema(RequestBodyType::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/RequestBodyType" + } + schemaContext.getSchema(ResponseHeaderType::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/ResponseHeaderType" + } + schemaContext.getSchema(ResponseBodyType::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/ResponseBodyType" + } + schemaContext.getComponentSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf( + "QueryParamType", + "PathParamType", + "HeaderParamType", + "RequestBodyType", + "ResponseHeaderType", + "ResponseBodyType", + ) + components.forEach { (_, schema) -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("value") + } + } + } + + "primitive type" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(Integer::class.java).also { schema -> + schema.type shouldBe "integer" + schema.format shouldBe "int32" + schema.`$ref` shouldBe null + } + schemaContext.getComponentSection().also { components -> + components.shouldBeEmpty() + } + } + + "primitive array" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body>() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(getType>()).also { schema -> + schema.type shouldBe "array" + schema.`$ref` shouldBe null + schema.items.also { item -> + item.type shouldBe "string" + } + } + schemaContext.getComponentSection().also { components -> + components.shouldBeEmpty() + } + } + + "primitive deep array" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body>>>() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(getType>>>()).also { schema -> + schema.type shouldBe "array" + schema.`$ref` shouldBe null + schema.items.also { item0 -> + item0.type shouldBe "array" + item0.`$ref` shouldBe null + item0.items.also { item1 -> + item1.type shouldBe "array" + item1.`$ref` shouldBe null + item1.items.also { item2 -> + item2.type shouldBe "boolean" + } + } + } + } + schemaContext.getComponentSection().also { components -> + components.shouldBeEmpty() + } + } + + "object" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(Data::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/Data" + } + schemaContext.getComponentSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf("Data") + components["Data"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") + } + } + } + + "object array" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body>() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(getType>()).also { schema -> + schema.type shouldBe "array" + schema.`$ref` shouldBe null + schema.items.also { item -> + item.type shouldBe null + item.`$ref` shouldBe "#/components/schemas/Data" + } + } + schemaContext.getComponentSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf("Data") + components["Data"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") + } + } + } + + "nested objects" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(DataWrapper::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/DataWrapper" + } + schemaContext.getComponentSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf("Data", "DataWrapper") + components["Data"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") + } + components["DataWrapper"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("data", "enabled") + schema.properties["data"]?.also { nestedSchema -> + nestedSchema.type shouldBe null + nestedSchema.`$ref` shouldBe "#/components/schemas/Data" + } + } + } + } + +}) { + + companion object { + + inline fun getType() = object : TypeReference() {}.type + + private data class QueryParamType(val value: String) + private data class PathParamType(val value: String) + private data class HeaderParamType(val value: String) + private data class RequestBodyType(val value: String) + private data class ResponseHeaderType(val value: String) + private data class ResponseBodyType(val value: String) + + private data class Data( + val text: String, + val number: Int + ) + + private data class DataWrapper( + val enabled: Boolean, + val data: Data + ) + + private val defaultPluginConfig = SwaggerUIPluginConfig() + + private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + } + + } + +} \ No newline at end of file From 4107a80cf57664c6c75d85a71c97973cb4bf1dd1 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 15:18:57 +0200 Subject: [PATCH 10/27] tests --- .../spec/schema/JsonSchemaBuilder.kt | 9 +- .../ktorswaggerui/tests/AssertionUtils.kt | 191 ----------- .../ktorswaggerui/tests/BuilderUtils.kt | 8 - .../ktorswaggerui/tests/ContentObjectTest.kt | 319 ------------------ .../tests/{builders => }/InfoBuilderTest.kt | 2 +- .../tests/JsonSchemaGenerationTests.kt | 308 ----------------- .../ktorswaggerui/tests/JsonSchemaTest.kt | 98 ------ .../{builders => }/OperationBuilderTest.kt | 2 +- .../tests/{builders => }/PathsBuilderTest.kt | 2 +- .../PrimitiveArraysSchemaGenerationTests.kt | 189 ----------- .../tests/PrimitiveSchemaGenerationTests.kt | 105 ------ ...nApiSchemaTest.kt => SchemaContextTest.kt} | 142 +++++++- .../tests/SecuritySchemesBuilderTest.kt | 171 ---------- .../{builders => }/ServersBuilderTest.kt | 2 +- .../tests/{builders => }/TagsBuilderTest.kt | 2 +- 15 files changed, 139 insertions(+), 1411 deletions(-) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{builders => }/InfoBuilderTest.kt (97%) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaTest.kt rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{builders => }/OperationBuilderTest.kt (99%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{builders => }/PathsBuilderTest.kt (98%) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{schemas/OpenApiSchemaTest.kt => SchemaContextTest.kt} (69%) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{builders => }/ServersBuilderTest.kt (95%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{builders => }/TagsBuilderTest.kt (96%) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index ecb1b24..6abd892 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -1,13 +1,12 @@ package io.github.smiley4.ktorswaggerui.spec.schema import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode -import com.github.victools.jsonschema.generator.* -import com.github.victools.jsonschema.module.jackson.JacksonModule -import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import com.github.victools.jsonschema.generator.SchemaGenerator +import com.github.victools.jsonschema.generator.SchemaGeneratorConfig +import io.swagger.v3.core.util.Json import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.media.XML import java.lang.reflect.Type @@ -89,7 +88,7 @@ class JsonSchemaBuilder( } private fun buildOpenApiSchema(json: JsonNode, name: String): Schema<*> { - return ObjectMapper().readValue(json.toString(), Schema::class.java).also { schema -> + return Json.mapper().readValue(json.toString(), Schema::class.java).also { schema -> schema.xml = XML().also { it.name = name } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt deleted file mode 100644 index 8583ca5..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/AssertionUtils.kt +++ /dev/null @@ -1,191 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.maps.shouldContainExactly -import io.kotest.matchers.nulls.shouldBeNull -import io.kotest.matchers.nulls.shouldNotBeNull -import io.kotest.matchers.shouldBe -import io.swagger.v3.oas.models.Operation -import io.swagger.v3.oas.models.PathItem -import io.swagger.v3.oas.models.examples.Example -import io.swagger.v3.oas.models.info.Info -import io.swagger.v3.oas.models.media.Content -import io.swagger.v3.oas.models.media.Schema -import io.swagger.v3.oas.models.security.OAuthFlow -import io.swagger.v3.oas.models.security.SecurityScheme -import io.swagger.v3.oas.models.servers.Server -import io.swagger.v3.oas.models.tags.Tag - - - - -infix fun SecurityScheme.shouldBeSecurityScheme(expectedBuilder: SecurityScheme.() -> Unit) { - this shouldBeSecurityScheme SecurityScheme().apply(expectedBuilder) -} - -infix fun SecurityScheme.shouldBeSecurityScheme(expected: SecurityScheme?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - this.type shouldBe expected.type - this.description shouldBe expected.description - this.name shouldBe expected.name - this.`$ref` shouldBe expected.`$ref` - this.`in` shouldBe expected.`in` - this.scheme shouldBe expected.scheme - this.openIdConnectUrl shouldBe expected.openIdConnectUrl - assertNullSafe(this.flows, expected.flows) { - this.flows.implicit shouldBeOAuthFlow expected.flows.implicit - this.flows.password shouldBeOAuthFlow expected.flows.password - this.flows.clientCredentials shouldBeOAuthFlow expected.flows.clientCredentials - this.flows.authorizationCode shouldBeOAuthFlow expected.flows.authorizationCode - } -} - -infix fun OAuthFlow.shouldBeOAuthFlow(expected: OAuthFlow) { - this.authorizationUrl shouldBe expected.authorizationUrl - this.tokenUrl shouldBe expected.tokenUrl - this.refreshUrl shouldBe expected.refreshUrl - this.scopes shouldContainExactly expected.scopes -} - - -infix fun Server.shouldBeServer(expectedBuilder: Server.() -> Unit) { - this shouldBeServer Server().apply(expectedBuilder) -} - -infix fun Server.shouldBeServer(expected: Server?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - this.url shouldBe expected.url - this.description shouldBe expected.description -} - - -infix fun Info.shouldBeInfo(expectedBuilder: Info.() -> Unit) { - this shouldBeInfo Info().apply(expectedBuilder) -} - -infix fun Info.shouldBeInfo(expected: Info?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - this.title shouldBe expected.title - this.version shouldBe expected.version - this.description shouldBe expected.description - this.termsOfService shouldBe expected.termsOfService - assertNullSafe(this.contact, expected.contact) { - this.contact.name shouldBe expected.contact.name - this.contact.url shouldBe expected.contact.url - this.contact.email shouldBe expected.contact.email - } - assertNullSafe(this.license, expected.license) { - this.license.name shouldBe expected.license.name - this.license.url shouldBe expected.license.url - } -} - - -infix fun Schema<*>.shouldBeSchema(expectedBuilder: Schema<*>.() -> Unit) { - this shouldBeSchema Schema().apply(expectedBuilder) -} - -infix fun Schema<*>.shouldBeSchema(expected: Schema<*>?) { - if (expected == null) { - this.shouldBeNull() - return - } else { - this.shouldNotBeNull() - } - this.default shouldBe expected.default - this.const shouldBe expected.const - this.title shouldBe expected.title - this.format shouldBe expected.format - this.multipleOf shouldBe expected.multipleOf - this.maximum shouldBe expected.maximum - this.exclusiveMaximum shouldBe expected.exclusiveMaximum - this.minimum shouldBe expected.minimum - this.exclusiveMinimum shouldBe expected.exclusiveMinimum - this.maxLength shouldBe expected.maxLength - this.minLength shouldBe expected.minLength - this.pattern shouldBe expected.pattern - this.maxItems shouldBe expected.maxItems - this.minItems shouldBe expected.minItems - this.uniqueItems shouldBe expected.uniqueItems - this.maxProperties shouldBe expected.maxProperties - this.minProperties shouldBe expected.minProperties - assertNullSafe(this.required, expected.required) { - this.required shouldContainExactlyInAnyOrder expected.required - } - this.type shouldBe expected.type - assertNullSafe(this.not, expected.not) { - this.not shouldBeSchema expected.not - } - assertNullSafe(this.properties, expected.properties) { - this.properties.keys shouldContainExactlyInAnyOrder expected.properties.keys - expected.properties.keys.forEach { key -> - this.properties[key]!! shouldBeSchema expected.properties[key] - } - } - assertNullSafe(this.additionalProperties, expected.additionalProperties) { - (this.additionalProperties as Schema) shouldBeSchema (expected.additionalProperties as Schema) - } - this.description shouldBe expected.description - this.`$ref` shouldBe expected.`$ref` - this.nullable shouldBe expected.nullable - this.readOnly shouldBe expected.readOnly - this.writeOnly shouldBe expected.writeOnly - this.deprecated shouldBe expected.deprecated - assertNullSafe(this.enum, expected.enum) { - this.enum shouldContainExactlyInAnyOrder expected.enum - } - assertSchemaList(this.prefixItems, expected.prefixItems) - assertSchemaList(this.allOf, expected.allOf) - assertSchemaList(this.anyOf, expected.anyOf) - assertSchemaList(this.oneOf, expected.oneOf) - assertSchemaList(this.prefixItems, expected.prefixItems) - assertNullSafe(this.items, expected.items) { - this.items shouldBeSchema expected.items - } - assertNullSafe(this.xml, expected.xml) { - this.xml.name shouldBe expected.xml.name - this.xml.namespace shouldBe expected.xml.namespace - this.xml.prefix shouldBe expected.xml.prefix - this.xml.attribute shouldBe expected.xml.attribute - this.xml.wrapped shouldBe expected.xml.wrapped - - } -} - -private fun assertSchemaList(actual: List>?, expected: List>?) { - if (expected == null) { - actual.shouldBeNull() - } else { - actual.shouldNotBeNull() - actual shouldHaveSize expected.size - expected.forEachIndexed { index, expectedItem -> - actual[index] shouldBeSchema expectedItem - } - } -} - - -private fun assertNullSafe(actual: T?, expected: T?, assertion: () -> Unit) { - if (expected == null) { - actual.shouldBeNull() - } else { - actual.shouldNotBeNull() - assertion() - } -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt deleted file mode 100644 index d4d2f07..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/BuilderUtils.kt +++ /dev/null @@ -1,8 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.specbuilder.OApiSchemaBuilder -import io.github.smiley4.ktorswaggerui.specbuilder.OApiSecuritySchemesBuilder - -fun getOApiSchemaBuilder() = OApiSchemaBuilder() - -fun getOApiSecuritySchemesBuilder() = OApiSecuritySchemesBuilder() \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt deleted file mode 100644 index b21041b..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ContentObjectTest.kt +++ /dev/null @@ -1,319 +0,0 @@ -//package io.github.smiley4.ktorswaggerui.tests -// -//import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -//import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef -//import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -//import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -//import io.github.smiley4.ktorswaggerui.dsl.array -//import io.github.smiley4.ktorswaggerui.dsl.obj -//import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -//import io.kotest.core.spec.style.StringSpec -//import io.ktor.http.ContentType -//import io.swagger.v3.oas.models.examples.Example -//import io.swagger.v3.oas.models.media.Content -//import io.swagger.v3.oas.models.media.Encoding -//import io.swagger.v3.oas.models.media.MediaType -//import io.swagger.v3.oas.models.media.Schema -//import io.swagger.v3.oas.models.media.XML -//import java.io.File -//import kotlin.reflect.KClass -// -//class ContentObjectTest : StringSpec({ -// -// "test default (plain-text) content object" { -// val content = buildContentObject(String::class) {} -// content shouldBeContent { -// addMediaType(ContentType.Text.Plain.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "string" -// xml = XML().apply { name = "String" } -// } -// }) -// } -// } -// -// "test default (json) content object" { -// val content = buildContentObject(SimpleBody::class) {} -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "object" -// xml = XML().apply { name = "SimpleBody" } -// properties = mapOf( -// "someText" to Schema().apply { -// type = "string" -// } -// ) -// } -// }) -// } -// } -// -// "test complete (plain-text) content object" { -// val content = buildContentObject(String::class) { -// description = "Test Description" -// required = true -// example("Example1", "Example Value 1") -// example("Example2", "Example Value 2") -// } -// content shouldBeContent { -// addMediaType(ContentType.Text.Plain.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "string" -// xml = XML().apply { name = "String" } -// } -// examples = mapOf( -// "Example1" to Example().apply { -// value = "Example Value 1" -// }, -// "Example2" to Example().apply { -// value = "Example Value 2" -// } -// ) -// }) -// } -// } -// -// "test xml content object" { -// val content = buildContentObject(SimpleBody::class) { -// mediaType(ContentType.Application.Xml) -// } -// content shouldBeContent { -// addMediaType(ContentType.Application.Xml.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "object" -// xml = XML().apply { name = "SimpleBody" } -// properties = mapOf( -// "someText" to Schema().apply { -// type = "string" -// } -// ) -// } -// }) -// } -// } -// -// "test image content object" { -// val content = buildContentObject(null) { -// mediaType(ContentType.Image.SVG) -// mediaType(ContentType.Image.PNG) -// mediaType(ContentType.Image.JPEG) -// } -// content shouldBeContent { -// addMediaType(ContentType.Image.SVG.toString(), MediaType()) -// addMediaType(ContentType.Image.PNG.toString(), MediaType()) -// addMediaType(ContentType.Image.JPEG.toString(), MediaType()) -// } -// } -// -// "test content object with custom (remote) json-schema" { -// val content = buildCustomContentObject(obj("remote")) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "object" -// `$ref` = "/my/test/schema" -// } -// }) -// } -// } -// -// "test content array with custom (remote) json-schema" { -// val content = buildCustomContentObject(array("remote")) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "object" -// `$ref` = "/my/test/schema" -// } -// } -// }) -// } -// } -// -// "test content object with custom (remote) json-schema and components-section enabled" { -// val content = buildCustomContentObject(obj("remote"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "object" -// `$ref` = "/my/test/schema" -// } -// }) -// } -// } -// -// "test content array with custom (remote) json-schema and components-section enabled" { -// val content = buildCustomContentObject(array("remote"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "object" -// `$ref` = "/my/test/schema" -// } -// } -// }) -// } -// } -// -// "test content object with custom json-schema" { -// val content = buildCustomContentObject(obj("custom")) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "object" -// properties = mapOf( -// "someBoolean" to Schema().apply { -// type = "boolean" -// }, -// "someText" to Schema().apply { -// type = "string" -// } -// ) -// } -// }) -// } -// } -// -// "test content array with custom json-schema" { -// val content = buildCustomContentObject(array("custom")) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "object" -// properties = mapOf( -// "someBoolean" to Schema().apply { -// type = "boolean" -// }, -// "someText" to Schema().apply { -// type = "string" -// } -// ) -// } -// } -// }) -// } -// } -// -// "test content object with custom json-schema and components-section enabled" { -// val content = buildCustomContentObject(obj("custom"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// `$ref` = "#/components/schemas/custom" -// } -// }) -// } -// } -// -// "test content array with custom json-schema and components-section enabled" { -// val content = buildCustomContentObject(array("custom"), ComponentsContext(true, mutableMapOf(), true, mutableMapOf(), false)) -// content shouldBeContent { -// addMediaType(ContentType.Application.Json.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "array" -// items = Schema().apply { -// `$ref` = "#/components/schemas/custom" -// } -// } -// }) -// } -// } -// -// "test multipart content object" { -// val content = buildMultipartContentObject { -// description = "Test Description" -// part("myFile") { -// mediaTypes = setOf(ContentType.Image.JPEG, ContentType.Image.PNG) -// } -// part("myData") -// } -// content shouldBeContent { -// addMediaType(ContentType.MultiPart.FormData.toString(), MediaType().apply { -// schema = Schema().apply { -// type = "object" -// properties = mapOf( -// "myFile" to Schema().apply { -// type = "string" -// format = "binary" -// xml = XML().apply { name = "File" } -// }, -// "myData" to Schema().apply { -// type = "object" -// xml = XML().apply { name = "SimpleBody" } -// properties = mapOf( -// "someText" to Schema().apply { -// type = "string" -// } -// ) -// } -// ) -// } -// addEncoding("myFile", Encoding().contentType("image/png, image/jpeg")) -// }) -// } -// } -// -//}) { -// -// companion object { -// -// private fun pluginConfig() = SwaggerUIPluginConfig().apply { -// schemas { -// remote("remote", "/my/test/schema") -// json("custom") { -// """ -// { -// "type": "object", -// "properties": { -// "someBoolean": { -// "type": "boolean" -// }, -// "someText": { -// "type": "string" -// } -// } -// } -// """.trimIndent() -// } -// } -// } -// -// private fun buildContentObject(schema: KClass<*>?, builder: OpenApiSimpleBody.() -> Unit): Content { -// return buildContentObject(ComponentsContext.NOOP, schema, builder) -// } -// -// private fun buildContentObject( -// componentCtx: ComponentsContext, -// type: KClass<*>?, -// builder: OpenApiSimpleBody.() -> Unit -// ): Content { -// return getOApiContentBuilder().build(OpenApiSimpleBody(type?.java).apply(builder), componentCtx, pluginConfig()) -// } -// -// private fun buildCustomContentObject(schema: CustomSchemaRef, componentCtx: ComponentsContext = ComponentsContext.NOOP): Content { -// return getOApiContentBuilder().build(OpenApiSimpleBody(null) -// .apply { customSchema = schema }, componentCtx, pluginConfig() -// ) -// } -// -// private fun buildMultipartContentObject(builder: OpenApiMultipartBody.() -> Unit): Content { -// return getOApiContentBuilder().build( -// OpenApiMultipartBody() -// .apply(builder), ComponentsContext.NOOP, pluginConfig() -// ) -// } -// -// private data class SimpleBody( -// val someText: String -// ) -// -// } -// -//} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoBuilderTest.kt similarity index 97% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoBuilderTest.kt index 55e0be2..63fd32e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/InfoBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests.builders +package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt deleted file mode 100644 index 423ae75..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaGenerationTests.kt +++ /dev/null @@ -1,308 +0,0 @@ -//package io.github.smiley4.ktorswaggerui.tests -// -//import com.fasterxml.jackson.annotation.JsonSubTypes -//import com.fasterxml.jackson.annotation.JsonTypeInfo -//import com.fasterxml.jackson.core.type.TypeReference -//import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -//import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -//import io.kotest.core.spec.style.StringSpec -//import io.swagger.v3.oas.models.media.Schema -// -//class JsonSchemaGenerationTests : StringSpec({ -// -// "generate schema for a simple enum" { -// getOApiSchemaBuilder().build(SimpleEnum::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// type = "string" -// enum = SimpleEnum.values().map { it.name } -// } -// } -// -// "generate schema for maps" { -// getOApiSchemaBuilder().build(DataClassWithMaps::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// type = "object" -// properties = mapOf( -// "mapStringValues" to Schema().apply { -// type = "object" -// additionalProperties = Schema().apply { -// type = "string" -// } -// }, -// "mapLongValues" to Schema().apply { -// type = "object" -// additionalProperties = Schema().apply { -// type = "integer" -// format = "int64" -// } -// }, -// ) -// } -// } -// -// "generate schema for a list of simple classes" { -// getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// type = "array" -// items = Schema().apply { -// type = "object" -// properties = mapOf( -// "text" to Schema().apply { -// type = "string" -// }, -// "value" to Schema().apply { -// type = "number" -// format = "float" -// } -// ) -// } -// } -// } -// -// "generate schema for a simple class" { -// getOApiSchemaBuilder().build(SimpleDataClass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// type = "object" -// properties = mapOf( -// "text" to Schema().apply { -// type = "string" -// }, -// "value" to Schema().apply { -// type = "number" -// format = "float" -// } -// ) -// } -// } -// -// "generate schema for a another class" { -// getOApiSchemaBuilder().build(AnotherDataClass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// type = "object" -// properties = mapOf( -// "primitiveValue" to Schema().apply { -// type = "integer" -// format = "int32" -// }, -// "primitiveList" to Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "integer" -// format = "int32" -// } -// }, -// "nestedClass" to Schema().apply { -// type = "object" -// properties = mapOf( -// "text" to Schema().apply { -// type = "string" -// }, -// "value" to Schema().apply { -// type = "number" -// format = "float" -// } -// ) -// }, -// "nestedClassList" to Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "object" -// properties = mapOf( -// "text" to Schema().apply { -// type = "string" -// }, -// "value" to Schema().apply { -// type = "number" -// format = "float" -// } -// ) -// } -// }, -// ) -// } -// } -// -// "generate schema for a class with inheritance" { -// getOApiSchemaBuilder().build(SubClassA::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// type = "object" -// properties = mapOf( -// "superField" to Schema().apply { -// type = "string" -// }, -// "subFieldA" to Schema().apply { -// type = "integer" -// format = "int32" -// }, -// "_type" to Schema().apply { -// setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassA") -// }, -// ) -// required = listOf("_type") -// } -// } -// -// "generate schema for a class with sub-classes" { -// getOApiSchemaBuilder().build(Superclass::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { -// anyOf = listOf( -// Schema().apply { -// type = "object" -// properties = mapOf( -// "superField" to Schema().apply { -// type = "string" -// }, -// "subFieldA" to Schema().apply { -// type = "integer" -// format = "int32" -// }, -// "_type" to Schema().apply { -// setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassA") -// } -// ) -// required = listOf("_type") -// }, -// Schema().apply { -// type = "object" -// properties = mapOf( -// "superField" to Schema().apply { -// type = "string" -// }, -// "subFieldB" to Schema().apply { -// type = "boolean" -// }, -// "_type" to Schema().apply { -// setConst("io.github.smiley4.ktorswaggerui.tests.JsonSchemaGenerationTests\$Companion\$SubClassB") -// } -// ) -// required = listOf("_type") -// } -// ) -// } -// } -// -// "generate schema for a class with nested generic type" { -// getOApiSchemaBuilder().build( -// WrapperForClassWithGenerics::class.java, -// ComponentsContext.NOOP, -// SwaggerUIPluginConfig() -// ) shouldBeSchema { -// type = "object" -// properties = mapOf( -// "genericClass" to Schema().apply { -// type = "object" -// properties = mapOf( -// "genericField" to Schema().apply { -// type = "string" -// }, -// "genericList" to Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "string" -// } -// } -// ) -// } -// ) -// } -// } -// -// "generate schema for a class with generic types" { -// getOApiSchemaBuilder().build( -// getType>(), -// ComponentsContext.NOOP, -// SwaggerUIPluginConfig() -// ) shouldBeSchema { -// type = "object" -// properties = mapOf( -// "genericField" to Schema().apply { -// type = "object" -// properties = mapOf( -// "text" to Schema().apply { -// type = "string" -// }, -// "value" to Schema().apply { -// type = "number" -// format = "float" -// } -// ) -// }, -// "genericList" to Schema().apply { -// type = "array" -// items = Schema().apply { -// type = "object" -// properties = mapOf( -// "text" to Schema().apply { -// type = "string" -// }, -// "value" to Schema().apply { -// type = "number" -// format = "float" -// } -// ) -// } -// } -// ) -// } -// } -// -//}) { -// companion object { -// -// inline fun getType() = object : TypeReference() {}.type -// -// enum class SimpleEnum { -// RED, GREEN, BLUE -// } -// -// data class SimpleDataClass( -// val text: String, -// val value: Float -// ) -// -// data class DataClassWithMaps( -// val mapStringValues: Map, -// val mapLongValues: Map -// ) -// -// data class AnotherDataClass( -// val primitiveValue: Int, -// val primitiveList: List, -// private val privateValue: String, -// val nestedClass: SimpleDataClass, -// val nestedClassList: List -// ) -// -// @JsonTypeInfo( -// use = JsonTypeInfo.Id.CLASS, -// include = JsonTypeInfo.As.PROPERTY, -// property = "_type", -// ) -// @JsonSubTypes( -// JsonSubTypes.Type(value = SubClassA::class), -// JsonSubTypes.Type(value = SubClassB::class), -// ) -// abstract class Superclass( -// val superField: String, -// ) -// -// class SubClassA( -// superField: String, -// val subFieldA: Int -// ) : Superclass(superField) -// -// class SubClassB( -// superField: String, -// val subFieldB: Boolean -// ) : Superclass(superField) -// -// -// data class ClassWithNestedAbstractClass( -// val nestedClass: Superclass, -// val someField: String -// ) -// -// class ClassWithGenerics( -// val genericField: T, -// val genericList: List -// ) -// -// class WrapperForClassWithGenerics( -// val genericClass: ClassWithGenerics -// ) -// -// } -//} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaTest.kt deleted file mode 100644 index 3bc17f2..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/JsonSchemaTest.kt +++ /dev/null @@ -1,98 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import com.fasterxml.jackson.core.type.TypeReference -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.module.jackson.JacksonModule -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.shouldBe -import java.lang.reflect.Type - -class JsonSchemaTest : StringSpec({ - - "schema of integer" { - generateSchema() shouldBe "{\"type\":\"integer\",\"format\":\"int32\"}" - } - - "schema of string" { - generateSchema() shouldBe "{\"type\":\"string\"}" - } - - "schema of object" { - generateSchema() shouldBe "{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"integer\",\"format\":\"int64\"},\"text\":{\"type\":\"string\"}}}" - } - - "schema of float-array" { - generateSchema() shouldBe "{\"type\":\"array\",\"items\":{\"type\":\"number\",\"format\":\"float\"}}" - } - - "schema of object-array" { - generateSchema>() shouldBe "{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"integer\",\"format\":\"int64\"},\"text\":{\"type\":\"string\"}}}}" - } - - "schema of list of string" { - generateSchema>() shouldBe "{\"type\":\"array\",\"items\":{\"type\":\"string\"}}" - } - - "schema of list of objects" { - generateSchema>() shouldBe "{\"type\":\"array\",\"items\":{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"integer\",\"format\":\"int64\"},\"text\":{\"type\":\"string\"}}}}" - } - - "schema of generic-object" { - generateSchema>() shouldBe "{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"integer\",\"format\":\"int64\"},\"text\":{\"type\":\"string\"}}},\"flag\":{\"type\":\"boolean\"}}}" - } - - "schema of pair with generic objects" { - generateSchema>() shouldBe "{\"type\":\"object\",\"properties\":{\"first\":{\"type\":\"string\"},\"second\":{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"integer\",\"format\":\"int64\"},\"text\":{\"type\":\"string\"}}}}}" - } - - "schema of nested generic objects" { - generateSchema>>() shouldBe "{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"object\",\"properties\":{\"data\":{\"type\":\"object\",\"properties\":{\"number\":{\"type\":\"integer\",\"format\":\"int64\"},\"text\":{\"type\":\"string\"}}},\"flag\":{\"type\":\"boolean\"}}},\"flag\":{\"type\":\"boolean\"}}}" - } - - "schema of generic-object with wildcard" { - generateSchema>() shouldBe "{\"type\":\"object\",\"properties\":{\"data\":{},\"flag\":{\"type\":\"boolean\"}}}" - } - - "schema of generic-object with any" { - generateSchema>() shouldBe "{\"type\":\"object\",\"properties\":{\"data\":{},\"flag\":{\"type\":\"boolean\"}}}" - } - - -}) { - - companion object { - - private val generator = SchemaGenerator( - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_ALL_SCHEMAS) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - // remove schema-definition for testing - .without(Option.SCHEMA_VERSION_INDICATOR) - .without(Option.DEFINITION_FOR_MAIN_SCHEMA) - .build() - ) - - private inline fun generateSchema(): String { - val type: Type = object : TypeReference() {}.type - return generator.generateSchema(type).toString() - } - - data class GenericObject( - val flag: Boolean, - val data: T - ) - - data class SpecificObject( - val text: String, - val number: Long - ) - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/OperationBuilderTest.kt similarity index 99% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/OperationBuilderTest.kt index 4287e54..8b01880 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/OperationBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests.builders +package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsBuilderTest.kt similarity index 98% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsBuilderTest.kt index 6830943..28e6c25 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests.builders +package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt deleted file mode 100644 index 811c607..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveArraysSchemaGenerationTests.kt +++ /dev/null @@ -1,189 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.kotest.core.spec.style.StringSpec -import io.swagger.v3.oas.models.media.Schema -import java.math.BigDecimal - -class PrimitiveArraysSchemaGenerationTests : StringSpec({ - - "generate schema for byte-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(-128) - maximum = BigDecimal.valueOf(127) - } - } - getOApiSchemaBuilder().build(ByteArray::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(-128) - maximum = BigDecimal.valueOf(127) - } - } - } - - "generate schema for unsigned byte" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(0) - maximum = BigDecimal.valueOf(255) - } - } - } - - "generate schema for short-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(-32768) - maximum = BigDecimal.valueOf(32767) - } - } - getOApiSchemaBuilder().build(ShortArray::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(-32768) - maximum = BigDecimal.valueOf(32767) - } - } - } - - "generate schema for unsigned short" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(0) - maximum = BigDecimal.valueOf(65535) - } - } - } - - "generate schema for integer-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - format = "int32" - } - } - getOApiSchemaBuilder().build(IntArray::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - format = "int32" - } - } - } - - "generate schema for unsigned integer" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(0) - maximum = BigDecimal.valueOf(4294967295) - } - } - } - - "generate schema for long-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - format = "int64" - } - } - getOApiSchemaBuilder().build(LongArray::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - format = "int64" - } - } - } - - "generate schema for unsigned long" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "integer" - minimum = BigDecimal.valueOf(0) - } - } - } - - "generate schema for float-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "number" - format = "float" - } - } - getOApiSchemaBuilder().build(FloatArray::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "number" - format = "float" - } - } - } - - "generate schema for double-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "number" - format = "double" - } - } - getOApiSchemaBuilder().build(DoubleArray::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "number" - format = "double" - } - } - } - - "generate schema for character-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "string" - minLength = 1 - maxLength = 1 - } - } - } - - "generate schema for string-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "string" - } - } - } - - "generate schema for boolean-array" { - getOApiSchemaBuilder().build(Array::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "array" - items = Schema().apply { - type = "boolean" - } - } - } - -}) \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt deleted file mode 100644 index 4bfc213..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PrimitiveSchemaGenerationTests.kt +++ /dev/null @@ -1,105 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.specbuilder.ComponentsContext -import io.kotest.core.spec.style.StringSpec -import java.math.BigDecimal - -class PrimitiveSchemaGenerationTests : StringSpec({ - - "generate schema for byte" { - getOApiSchemaBuilder().build(Byte::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - minimum = BigDecimal.valueOf(-128) - maximum = BigDecimal.valueOf(127) - } - } - - "generate schema for unsigned byte" { - getOApiSchemaBuilder().build(UByte::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - minimum = BigDecimal.valueOf(0) - maximum = BigDecimal.valueOf(255) - } - } - - "generate schema for short" { - getOApiSchemaBuilder().build(Short::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - minimum = BigDecimal.valueOf(-32768) - maximum = BigDecimal.valueOf(32767) - } - } - - "generate schema for unsigned short" { - getOApiSchemaBuilder().build(UShort::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - minimum = BigDecimal.valueOf(0) - maximum = BigDecimal.valueOf(65535) - } - } - - "generate schema for integer" { - getOApiSchemaBuilder().build(Int::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - format = "int32" - } - } - - "generate schema for unsigned integer" { - getOApiSchemaBuilder().build(UInt::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - minimum = BigDecimal.valueOf(0) - maximum = BigDecimal.valueOf(4294967295) - } - } - - "generate schema for long" { - getOApiSchemaBuilder().build(Long::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - format = "int64" - } - } - - "generate schema for unsigned long" { - getOApiSchemaBuilder().build(ULong::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "integer" - minimum = BigDecimal.valueOf(0) - } - } - - "generate schema for float" { - getOApiSchemaBuilder().build(Float::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "number" - format = "float" - } - } - - "generate schema for double" { - getOApiSchemaBuilder().build(Double::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "number" - format = "double" - } - } - - "generate schema for character" { - getOApiSchemaBuilder().build(Char::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "string" - minLength = 1 - maxLength = 1 - } - } - - "generate schema for string" { - getOApiSchemaBuilder().build(String::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "string" - } - } - - "generate schema for boolean" { - getOApiSchemaBuilder().build(Boolean::class.java, ComponentsContext.NOOP, SwaggerUIPluginConfig()) shouldBeSchema { - type = "boolean" - } - } - -}) \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SchemaContextTest.kt similarity index 69% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SchemaContextTest.kt index a8b88b3..e0b2d3b 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schemas/OpenApiSchemaTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SchemaContextTest.kt @@ -1,5 +1,7 @@ -package io.github.smiley4.ktorswaggerui.tests.schemas +package io.github.smiley4.ktorswaggerui.tests +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.core.type.TypeReference import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute @@ -8,11 +10,12 @@ import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldNotContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.shouldBe import io.ktor.http.HttpMethod -class OpenApiSchemaTest : StringSpec({ +class SchemaContextTest : StringSpec({ "route with all schemas" { val routes = listOf( @@ -168,14 +171,14 @@ class OpenApiSchemaTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().apply { request { - body() + body() } }, protected = false ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(Data::class.java).also { schema -> + schemaContext.getSchema(SimpleDataClass::class.java).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/Data" } @@ -195,14 +198,14 @@ class OpenApiSchemaTest : StringSpec({ method = HttpMethod.Get, documentation = OpenApiRoute().apply { request { - body>() + body>() } }, protected = false ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(getType>()).also { schema -> + schemaContext.getSchema(getType>()).also { schema -> schema.type shouldBe "array" schema.`$ref` shouldBe null schema.items.also { item -> @@ -254,12 +257,77 @@ class OpenApiSchemaTest : StringSpec({ } } + "simple enum" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(SimpleEnum::class.java).also { schema -> + schema.type shouldBe "string" + schema.enum shouldNotContainExactlyInAnyOrder SimpleEnum.values().map { it.name } + schema.`$ref` shouldBe null + } + schemaContext.getComponentSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf("") + } + } + + "maps" { + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body() + } + }, + protected = false + ) + ) + val schemaContext = schemaContext().initialize(routes) + schemaContext.getSchema(DataClassWithMaps::class.java).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/DataClassWithMaps" + } + schemaContext.getComponentSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf("DataClassWithMaps", "Map(String,Long)", "Map(String,String)") + components["DataClassWithMaps"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("mapStringValues", "mapLongValues") + schema.properties["mapStringValues"]?.also { nestedSchema -> + nestedSchema.type shouldBe null + nestedSchema.`$ref` shouldBe "#/components/schemas/Map(String,String)" + } + schema.properties["mapLongValues"]?.also { nestedSchema -> + nestedSchema.type shouldBe null + nestedSchema.`$ref` shouldBe "#/components/schemas/Map(String,Long)" + } + } + } + } + }) { companion object { inline fun getType() = object : TypeReference() {}.type + private val defaultPluginConfig = SwaggerUIPluginConfig() + + private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + } + private data class QueryParamType(val value: String) private data class PathParamType(val value: String) private data class HeaderParamType(val value: String) @@ -267,22 +335,72 @@ class OpenApiSchemaTest : StringSpec({ private data class ResponseHeaderType(val value: String) private data class ResponseBodyType(val value: String) - private data class Data( + private data class SimpleDataClass( val text: String, val number: Int ) private data class DataWrapper( val enabled: Boolean, - val data: Data + val data: SimpleDataClass ) - private val defaultPluginConfig = SwaggerUIPluginConfig() - - private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + private enum class SimpleEnum { + RED, GREEN, BLUE } + private data class DataClassWithMaps( + val mapStringValues: Map, + val mapLongValues: Map + ) + + private data class AnotherDataClass( + val primitiveValue: Int, + val primitiveList: List, + private val privateValue: String, + val nestedClass: SimpleDataClass, + val nestedClassList: List + ) + + + @JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "_type", + ) + @JsonSubTypes( + JsonSubTypes.Type(value = SubClassA::class), + JsonSubTypes.Type(value = SubClassB::class), + ) + private abstract class Superclass( + val superField: String, + ) + + private class SubClassA( + superField: String, + val subFieldA: Int + ) : Superclass(superField) + + private class SubClassB( + superField: String, + val subFieldB: Boolean + ) : Superclass(superField) + + + private data class ClassWithNestedAbstractClass( + val nestedClass: Superclass, + val someField: String + ) + + private class ClassWithGenerics( + val genericField: T, + val genericList: List + ) + + private class WrapperForClassWithGenerics( + val genericClass: ClassWithGenerics + ) + } } \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt deleted file mode 100644 index f0b84c7..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SecuritySchemesBuilderTest.kt +++ /dev/null @@ -1,171 +0,0 @@ -package io.github.smiley4.ktorswaggerui.tests - -import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation -import io.github.smiley4.ktorswaggerui.dsl.AuthScheme -import io.github.smiley4.ktorswaggerui.dsl.AuthType -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme -import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder -import io.kotest.core.spec.style.StringSpec -import io.kotest.matchers.collections.shouldHaveSize -import io.kotest.matchers.maps.shouldContainKey -import io.kotest.matchers.maps.shouldHaveSize -import io.swagger.v3.oas.models.security.OAuthFlow -import io.swagger.v3.oas.models.security.OAuthFlows -import io.swagger.v3.oas.models.security.Scopes -import io.swagger.v3.oas.models.security.SecurityScheme - -class SecuritySchemesBuilderTest : StringSpec({ - - "test default security scheme object" { - val securityScheme = buildSecuritySchemeObject("TestAuth") {} - securityScheme shouldBeSecurityScheme { - name = "TestAuth" - } - } - - "test multiple security scheme objects" { - val securitySchemes = buildSecuritySchemeObjects(mapOf( - "TestAuth1" to { - type = AuthType.HTTP - scheme = AuthScheme.BASIC - }, - "TestAuth2" to { - type = AuthType.HTTP - scheme = AuthScheme.BASIC - } - )) - securitySchemes[0] shouldBeSecurityScheme { - name = "TestAuth1" - type = SecurityScheme.Type.HTTP - scheme = "Basic" - } - securitySchemes[1] shouldBeSecurityScheme { - name = "TestAuth2" - type = SecurityScheme.Type.HTTP - scheme = "Basic" - } - } - - "test complete security scheme object" { - val securityScheme = buildSecuritySchemeObject("TestAuth") { - type = AuthType.HTTP - location = AuthKeyLocation.COOKIE - scheme = AuthScheme.BASIC - bearerFormat = "test" - openIdConnectUrl = "Test IOD-Connect URL" - description = "Test Description" - flows { - implicit { - authorizationUrl = "Implicit Auth Url" - tokenUrl = "Implicity Token Url" - refreshUrl = "Implicity Token Url" - scopes = mapOf( - "implicit1" to "scope1", - "implicit2" to "scope2" - ) - } - password { - authorizationUrl = "Password Auth Url" - tokenUrl = "Password Token Url" - refreshUrl = "Password Token Url" - scopes = mapOf( - "password1" to "scope1", - "password2" to "scope2" - ) - } - clientCredentials { - authorizationUrl = "ClientCredentials Auth Url" - tokenUrl = "ClientCredentials Token Url" - refreshUrl = "ClientCredentials Token Url" - scopes = mapOf( - "clientCredentials1" to "scope1", - "clientCredentials2" to "scope2" - ) - } - authorizationCode { - authorizationUrl = "AuthorizationCode Auth Url" - tokenUrl = "AuthorizationCode Token Url" - refreshUrl = "AuthorizationCode Token Url" - scopes = mapOf( - "authorizationCode1" to "scope1", - "authorizationCode2" to "scope2" - ) - } - } - } - securityScheme shouldBeSecurityScheme { - name = "TestAuth" - type = SecurityScheme.Type.HTTP - `in` = SecurityScheme.In.COOKIE - scheme = "Basic" - bearerFormat = "test" - openIdConnectUrl = "Test IOD-Connect URL" - description = "Test Description" - flows = OAuthFlows().apply { - implicit = OAuthFlow().apply { - authorizationUrl = "Implicit Auth Url" - tokenUrl = "Implicity Token Url" - refreshUrl = "Implicity Token Url" - scopes = Scopes().apply { - addString("implicit1", "scope1") - addString("implicit2", "scope2") - } - } - password = OAuthFlow().apply { - authorizationUrl = "Password Auth Url" - tokenUrl = "Password Token Url" - refreshUrl = "Password Token Url" - scopes = Scopes().apply { - addString("password1", "scope1") - addString("password2", "scope2") - } - } - clientCredentials = OAuthFlow().apply { - authorizationUrl = "ClientCredentials Auth Url" - tokenUrl = "ClientCredentials Token Url" - refreshUrl = "ClientCredentials Token Url" - scopes = Scopes().apply { - addString("clientCredentials1", "scope1") - addString("clientCredentials2", "scope2") - } - } - authorizationCode = OAuthFlow().apply { - authorizationUrl = "AuthorizationCode Auth Url" - tokenUrl = "AuthorizationCode Token Url" - refreshUrl = "AuthorizationCode Token Url" - scopes = Scopes().apply { - addString("authorizationCode1", "scope1") - addString("authorizationCode2", "scope2") - } - } - } - } - } - -}) { - - companion object { - - private fun buildSecuritySchemeObject(name: String, builder: OpenApiSecurityScheme.() -> Unit): SecurityScheme { - return SecuritySchemesBuilder(OAuthFlowsBuilder()) - .build(listOf(OpenApiSecurityScheme(name).apply(builder))) - .let { - it shouldHaveSize 1 - it shouldContainKey name - it[name]!! - } - } - - private fun buildSecuritySchemeObjects(builders: Map Unit>): List { - val schemes = mutableListOf() - builders.forEach { (name, builder) -> - schemes.addAll(getOApiSecuritySchemesBuilder().build(listOf(OpenApiSecurityScheme(name).apply(builder))).values) - } - schemes shouldHaveSize builders.size - return schemes - } - - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersBuilderTest.kt similarity index 95% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersBuilderTest.kt index 7d3c31c..f96e16e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/ServersBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests.builders +package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsBuilderTest.kt similarity index 96% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsBuilderTest.kt index ae8b51e..916e62e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/builders/TagsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests.builders +package io.github.smiley4.ktorswaggerui.tests import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder From c545943f7fa41ceb8f1df479881811d23dddf9d2 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 15:29:50 +0200 Subject: [PATCH 11/27] deprecate component-section plugin-config props --- .../io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index 1d09de2..7de00d8 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -67,12 +67,14 @@ class SwaggerUIPluginConfig { * Whether to put json-schemas in the component section and reference them or inline the schemas at the actual place of usage. * (https://swagger.io/specification/#components-object) */ + @Deprecated("not used in versions 2+") var schemasInComponentSection: Boolean = false /** * Whether to put example objects in the component section and reference them or inline the examples at the actual place of usage. */ + @Deprecated("not used in versions 2+") var examplesInComponentSection: Boolean = false From 9072617c34afd96a1c6fb4ff88362ad34620c547 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 16:51:11 +0200 Subject: [PATCH 12/27] tests --- .../spec/schema/SchemaContext.kt | 13 +- .../tests/{ => openapi}/InfoBuilderTest.kt | 2 +- .../{ => openapi}/OperationBuilderTest.kt | 162 +++++++++++++++++- .../tests/{ => openapi}/PathsBuilderTest.kt | 2 +- .../tests/{ => openapi}/ServersBuilderTest.kt | 2 +- .../tests/{ => openapi}/TagsBuilderTest.kt | 2 +- .../tests/{ => schema}/SchemaContextTest.kt | 26 +-- 7 files changed, 188 insertions(+), 21 deletions(-) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{ => openapi}/InfoBuilderTest.kt (97%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{ => openapi}/OperationBuilderTest.kt (83%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{ => openapi}/PathsBuilderTest.kt (98%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{ => openapi}/ServersBuilderTest.kt (95%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{ => openapi}/TagsBuilderTest.kt (96%) rename src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/{ => schema}/SchemaContextTest.kt (95%) diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index 4cc7d23..3900187 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -91,7 +91,7 @@ class SchemaContext( if (schemas.containsKey(type.typeName)) { return } - schemas[type.typeName] = jsonSchemaBuilder.build(type) + addSchema(type, jsonSchemaBuilder.build(type)) } @@ -101,7 +101,7 @@ class SchemaContext( } val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) if (customSchema == null) { - customSchemas[customSchemaRef.schemaId] = Schema() + addSchema(customSchemaRef, Schema()) } else { when (customSchema) { is CustomJsonSchema -> { @@ -127,11 +127,18 @@ class SchemaContext( } } }.also { - customSchemas[customSchemaRef.schemaId] = it + addSchema(customSchemaRef, it) } } } + fun addSchema(type: Type, schema: OpenApiSchemaInfo) { + schemas[type.typeName] = schema + } + + fun addSchema(customSchemaRef: CustomSchemaRef, schema: Schema<*>) { + customSchemas[customSchemaRef.schemaId] = schema + } fun getComponentSection(): Map> { val componentSection = mutableMapOf>() diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt similarity index 97% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt index 63fd32e..181eef1 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/InfoBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/InfoBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests +package io.github.smiley4.ktorswaggerui.tests.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt similarity index 83% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/OperationBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index 8b01880..2adcbf1 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -1,7 +1,8 @@ -package io.github.smiley4.ktorswaggerui.tests +package io.github.smiley4.ktorswaggerui.tests.openapi import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.dsl.obj import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder @@ -27,6 +28,7 @@ import io.ktor.http.ContentType import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.swagger.v3.oas.models.Operation +import io.swagger.v3.oas.models.media.Schema import java.io.File class OperationBuilderTest : StringSpec({ @@ -85,6 +87,35 @@ class OperationBuilderTest : StringSpec({ } } + "operation with auto-generated tags" { + val config = SwaggerUIPluginConfig().also { + it.automaticTagGenerator = { url -> url.firstOrNull() } + } + val routeA = RouteMeta( + path = "a/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.tags = listOf("defaultTag") + }, + protected = false + ) + val routeB = RouteMeta( + path = "b/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.tags = listOf("defaultTag") + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(routeA, routeB)) + buildOperationObject(routeA, schemaContext, config).also { operation -> + operation.tags shouldContainExactlyInAnyOrder listOf("a", "defaultTag") + } + buildOperationObject(routeB, schemaContext, config).also { operation -> + operation.tags shouldContainExactlyInAnyOrder listOf("b", "defaultTag") + } + } + "protected route with security-scheme-names" { val route = RouteMeta( path = "/test", @@ -549,6 +580,43 @@ class OperationBuilderTest : StringSpec({ } } + "multipart body without parts" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + multipartBody { + mediaType(ContentType.MultiPart.FormData) + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + buildOperationObject(route, schemaContext).also { operation -> + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 1 + content.get("multipart/form-data") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema.shouldNotBeNull() + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + } + } + } + } + "multiple responses" { val route = RouteMeta( path = "/test", @@ -710,6 +778,98 @@ class OperationBuilderTest : StringSpec({ } } + "custom body schema" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + body(obj("myCustomSchema")) + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + schemaContext.addSchema(obj("myCustomSchema"), Schema().also { + it.type = "custom_type" + }) + buildOperationObject(route, schemaContext).also { operation -> + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.description shouldBe null + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 1 + content.get("text/plain") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { schema -> schema.type shouldBe "custom_type" } + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + + } + body.required shouldBe null + body.extensions shouldBe null + body.`$ref` shouldBe null + } + } + } + + "custom multipart-body schema" { + val route = RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().also { route -> + route.request { + multipartBody { + mediaType(ContentType.MultiPart.FormData) + part("customData", obj("myCustomSchema")) + } + } + }, + protected = false + ) + val schemaContext = schemaContext().initialize(listOf(route)) + schemaContext.addSchema(obj("myCustomSchema"), Schema().also { + it.type = "custom_type" + }) + buildOperationObject(route, schemaContext).also { operation -> + operation.requestBody + .also { it.shouldNotBeNull() } + ?.also { body -> + body.content + .also { it.shouldNotBeNull() } + ?.also { content -> + content shouldHaveSize 1 + content.get("multipart/form-data") + .also { it.shouldNotBeNull() } + ?.also { mediaType -> + mediaType.schema + .also { it.shouldNotBeNull() } + ?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("customData") + schema.properties["customData"]!!.type shouldBe "custom_type" + } + mediaType.example shouldBe null + mediaType.examples shouldBe null + mediaType.encoding shouldBe null + mediaType.extensions shouldBe null + mediaType.exampleSetFlag shouldBe false + } + } + } + } + } + }) { companion object { diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt similarity index 98% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index 28e6c25..f97d8e1 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests +package io.github.smiley4.ktorswaggerui.tests.openapi import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt similarity index 95% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt index f96e16e..3119c85 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/ServersBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/ServersBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests +package io.github.smiley4.ktorswaggerui.tests.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt similarity index 96% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsBuilderTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt index 916e62e..c13714c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/TagsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/TagsBuilderTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests +package io.github.smiley4.ktorswaggerui.tests.openapi import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt similarity index 95% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SchemaContextTest.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index e0b2d3b..5c29e3b 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.tests +package io.github.smiley4.ktorswaggerui.tests.schema import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo @@ -9,8 +9,8 @@ import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder -import io.kotest.matchers.collections.shouldNotContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.shouldBe import io.ktor.http.HttpMethod @@ -180,11 +180,11 @@ class SchemaContextTest : StringSpec({ val schemaContext = schemaContext().initialize(routes) schemaContext.getSchema(SimpleDataClass::class.java).also { schema -> schema.type shouldBe null - schema.`$ref` shouldBe "#/components/schemas/Data" + schema.`$ref` shouldBe "#/components/schemas/SimpleDataClass" } schemaContext.getComponentSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("Data") - components["Data"]?.also { schema -> + components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass") + components["SimpleDataClass"]?.also { schema -> schema.type shouldBe "object" schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") } @@ -210,12 +210,12 @@ class SchemaContextTest : StringSpec({ schema.`$ref` shouldBe null schema.items.also { item -> item.type shouldBe null - item.`$ref` shouldBe "#/components/schemas/Data" + item.`$ref` shouldBe "#/components/schemas/SimpleDataClass" } } schemaContext.getComponentSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("Data") - components["Data"]?.also { schema -> + components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass") + components["SimpleDataClass"]?.also { schema -> schema.type shouldBe "object" schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") } @@ -241,8 +241,8 @@ class SchemaContextTest : StringSpec({ schema.`$ref` shouldBe "#/components/schemas/DataWrapper" } schemaContext.getComponentSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("Data", "DataWrapper") - components["Data"]?.also { schema -> + components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass", "DataWrapper") + components["SimpleDataClass"]?.also { schema -> schema.type shouldBe "object" schema.properties.keys shouldContainExactlyInAnyOrder listOf("text", "number") } @@ -251,7 +251,7 @@ class SchemaContextTest : StringSpec({ schema.properties.keys shouldContainExactlyInAnyOrder listOf("data", "enabled") schema.properties["data"]?.also { nestedSchema -> nestedSchema.type shouldBe null - nestedSchema.`$ref` shouldBe "#/components/schemas/Data" + nestedSchema.`$ref` shouldBe "#/components/schemas/SimpleDataClass" } } } @@ -273,11 +273,11 @@ class SchemaContextTest : StringSpec({ val schemaContext = schemaContext().initialize(routes) schemaContext.getSchema(SimpleEnum::class.java).also { schema -> schema.type shouldBe "string" - schema.enum shouldNotContainExactlyInAnyOrder SimpleEnum.values().map { it.name } + schema.enum shouldContainExactlyInAnyOrder SimpleEnum.values().map { it.name } schema.`$ref` shouldBe null } schemaContext.getComponentSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("") + components.keys.shouldBeEmpty() } } From 4bb5fe4f70914bffe3d69c740870958626815398 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 17:43:55 +0200 Subject: [PATCH 13/27] test, drop old logic --- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 4 +- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 15 -- .../spec/route/RouteCollector.kt | 4 +- .../specbuilder/ApiSpecBuilder.kt | 36 --- .../specbuilder/ComponentsContext.kt | 135 ---------- .../JsonToOpenApiSchemaConverter.kt | 56 ---- .../specbuilder/OApiComponentsBuilder.kt | 26 -- .../specbuilder/OApiContentBuilder.kt | 188 ------------- .../specbuilder/OApiExampleBuilder.kt | 25 -- .../specbuilder/OApiHeaderBuilder.kt | 23 -- .../specbuilder/OApiInfoBuilder.kt | 35 --- .../specbuilder/OApiJsonSchemaBuilder.kt | 75 ------ .../specbuilder/OApiOAuthFlowsBuilder.kt | 36 --- .../specbuilder/OApiParametersBuilder.kt | 36 --- .../specbuilder/OApiPathBuilder.kt | 86 ------ .../specbuilder/OApiPathsBuilder.kt | 60 ----- .../specbuilder/OApiRequestBodyBuilder.kt | 22 -- .../specbuilder/OApiResponsesBuilder.kt | 27 -- .../specbuilder/OApiSchemaBuilder.kt | 251 ------------------ .../specbuilder/OApiSecuritySchemesBuilder.kt | 56 ---- .../specbuilder/OApiServersBuilder.kt | 20 -- .../specbuilder/OApiTagsBuilder.kt | 27 -- .../specbuilder/RouteCollector.kt | 125 --------- .../ktorswaggerui/specbuilder/RouteMeta.kt | 14 - .../ktorswaggerui/examples/CompleteExample.kt | 2 - .../examples/CustomJsonSchemaExample.kt | 1 - .../tests/openapi/OpenApiBuilderTest.kt | 155 +++++++++++ .../openapi/SecuritySchemesBuilderTest.kt | 199 ++++++++++++++ .../route/RouteDocumentationMergerTest.kt | 131 +++++++++ 29 files changed, 490 insertions(+), 1380 deletions(-) delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ApiSpecBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ComponentsContext.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/JsonToOpenApiSchemaConverter.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiComponentsBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiContentBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiExampleBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiHeaderBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiInfoBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiJsonSchemaBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiOAuthFlowsBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiParametersBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathsBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiRequestBodyBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiResponsesBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSchemaBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSecuritySchemesBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiServersBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiTagsBuilder.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteCollector.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteMeta.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 7744db2..46d17bf 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -32,7 +32,9 @@ import io.ktor.server.application.ApplicationStarted import io.ktor.server.application.createApplicationPlugin import io.ktor.server.application.hooks.MonitoringEvent import io.ktor.server.application.install +import io.ktor.server.application.plugin import io.ktor.server.application.pluginOrNull +import io.ktor.server.routing.Routing import io.ktor.server.webjars.Webjars import io.swagger.v3.core.util.Json @@ -63,7 +65,7 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration } private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig): List { - return RouteCollector(RouteDocumentationMerger()).collectRoutes(application, pluginConfig).toList() + return RouteCollector(RouteDocumentationMerger()).collectRoutes({ application.plugin(Routing) }, pluginConfig).toList() } private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index 7de00d8..abe1a09 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -63,21 +63,6 @@ class SwaggerUIPluginConfig { var automaticTagGenerator: ((url: List) -> String?)? = null - /** - * Whether to put json-schemas in the component section and reference them or inline the schemas at the actual place of usage. - * (https://swagger.io/specification/#components-object) - */ - @Deprecated("not used in versions 2+") - var schemasInComponentSection: Boolean = false - - - /** - * Whether to put example objects in the component section and reference them or inline the examples at the actual place of usage. - */ - @Deprecated("not used in versions 2+") - var examplesInComponentSection: Boolean = false - - /** * Filter to apply to all routes. Return 'false' for routes to not include them in the OpenApi-Spec and Swagger-UI. * The url of the paths are already split at '/'. diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt index 40bbc56..9a8fa85 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/route/RouteCollector.kt @@ -22,8 +22,8 @@ class RouteCollector( /** * Collect all routes from the given application */ - fun collectRoutes(application: Application, config: SwaggerUIPluginConfig): Sequence { - return allRoutes(application.plugin(Routing)) + fun collectRoutes(routeProvider: () -> Route, config: SwaggerUIPluginConfig): Sequence { + return allRoutes(routeProvider()) .asSequence() .map { route -> RouteMeta( diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ApiSpecBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ApiSpecBuilder.kt deleted file mode 100644 index fa200e0..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ApiSpecBuilder.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.ktor.server.application.Application -import io.swagger.v3.core.util.Json -import io.swagger.v3.oas.models.OpenAPI - -/** - * Build the OpenApi-json for the given application - */ -class ApiSpecBuilder { - - private val infoBuilder = OApiInfoBuilder() - private val serversBuilder = OApiServersBuilder() - private val tagsBuilder = OApiTagsBuilder() - private val pathsBuilder = OApiPathsBuilder(RouteCollector()) - private val componentsBuilder = OApiComponentsBuilder() - - - fun build(application: Application, config: SwaggerUIPluginConfig): String { - val componentCtx = ComponentsContext( - config.schemasInComponentSection, mutableMapOf(), - config.examplesInComponentSection, mutableMapOf(), - config.canonicalNameObjectRefs - ) - val openAPI = OpenAPI().apply { - info = infoBuilder.build(config.getInfo()) - servers = serversBuilder.build(config.getServers()) - tags = tagsBuilder.build(config.getTags()) - paths = pathsBuilder.build(config, application, componentCtx) - components = componentsBuilder.build(componentCtx, config.getSecuritySchemes()) - } - return Json.pretty(openAPI) - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ComponentsContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ComponentsContext.kt deleted file mode 100644 index 6583a07..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/ComponentsContext.kt +++ /dev/null @@ -1,135 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.swagger.v3.oas.models.media.Schema -import java.lang.reflect.ParameterizedType -import java.lang.reflect.Type -import java.lang.reflect.WildcardType - -/** - * Container holding and collecting information about the OpenApi "Components"-Object - */ -data class ComponentsContext( - val schemasInComponents: Boolean, - val schemas: MutableMap>, - val examplesInComponents: Boolean, - val examples: MutableMap, - val canonicalNameObjectRefs: Boolean -) { - - companion object { - val NOOP = ComponentsContext(false, mutableMapOf(), false, mutableMapOf(), false) - } - - - /** - * Add the given schema for the given type to the components-section. - * The schema is an array, only the element type is added to the components-section - * @return a schema referencing the complete schema (or the original schema if 'schemasInComponents' = false) - */ - fun addArraySchema(type: Type, schema: Schema<*>): Schema { - if (this.schemasInComponents) { - val innerSchema: Schema = when (type) { - is Class<*> -> addSchema(type.componentType, schema.items) - is ParameterizedType -> { - when (val actualTypeArgument = type.actualTypeArguments.firstOrNull()) { - is Class<*> -> addSchema(actualTypeArgument, schema.items) - is WildcardType -> { - addSchema(actualTypeArgument.upperBounds.first(), schema.items) - } - - else -> throw Exception("Could not add array-schema to components ($type)") - } - } - - else -> throw Exception("Could not add array-schema to components ($type)") - } - return Schema().apply { - this.type = "array" - this.items = Schema().apply { - `$ref` = innerSchema.`$ref` - } - } - } else { - @Suppress("UNCHECKED_CAST") - return schema as Schema - } - } - - - /** - * Add the given schema for the given type to the components-section - * @return a schema referencing the complete schema (or the original schema if 'schemasInComponents' = false) - */ - fun addSchema(type: Type, schema: Schema<*>): Schema { - return addSchema(getIdentifyingName(type), schema) - } - - - /** - * Add the given schema for the given type to the components-section - * @return a schema referencing the complete schema (or the original schema if 'schemasInComponents' = false) - */ - fun addSchema(id: String, schema: Schema<*>): Schema { - if (schemasInComponents) { - if (!schemas.containsKey(id)) { - schemas[id] = schema - } - return Schema().apply { - `$ref` = asSchemaRef(id) - } - } else { - @Suppress("UNCHECKED_CAST") - return schema as Schema - } - } - - - private fun getIdentifyingName(type: Type): String { - return when (type) { - is Class<*> -> if (canonicalNameObjectRefs) type.canonicalName else type.simpleName - is ParameterizedType -> getIdentifyingName(type.rawType) + "<" + getIdentifyingName(type.actualTypeArguments.first()) + ">" - is WildcardType -> getIdentifyingName(type.upperBounds.first()) - else -> throw Exception("Could not get identifying name from $type") - } - } - - - /** - * Add the given example with the given name to the components-section - * @return the ref-string for the example - */ - fun addExample(name: String, example: OpenApiExample): String { - if (!examples.containsKey(name)) { - examples[name] = example - return asExampleRef(name) - } else { - if (isSameExample(examples[name]!!, example)) { - return asExampleRef(name) - } else { - val key = asUniqueName(name, example) - examples[key] = example - return asExampleRef(key) - } - } - } - - - private fun isSameExample(a: OpenApiExample, b: OpenApiExample): Boolean { - return a.value == b.value - && a.description == b.description - && a.summary == b.summary - } - - - private fun asUniqueName(name: String, example: OpenApiExample): String { - return name + "#" + example.hashCode().toString(16) - } - - - private fun asSchemaRef(key: String) = "#/components/schemas/$key" - - - private fun asExampleRef(key: String) = "#/components/examples/$key" - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/JsonToOpenApiSchemaConverter.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/JsonToOpenApiSchemaConverter.kt deleted file mode 100644 index 60a5635..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/JsonToOpenApiSchemaConverter.kt +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import io.swagger.v3.oas.models.media.Schema -import java.math.BigDecimal - -class JsonToOpenApiSchemaConverter { - - fun toSchema(json: String) = toSchema(ObjectMapper().readTree(json)) - - fun toSchema(node: JsonNode): Schema { - return Schema().apply { - node["\$schema"]?.let { this.`$schema` = it.asText() } - node["description"]?.let { this.description = it.asText() } - node["title"]?.let { this.title = it.asText() } - node["example"]?.let { this.example(it.asText()) } - node["type"]?.let { - val types = if (it is ArrayNode) it.collectElements().map { e -> e.asText() } else listOf(it.asText()) - this.type = types.firstOrNull { e -> e != "null" } - if (types.contains("null")) { - this.nullable = true - } - } - node["deprecated"]?.let { this.deprecated = it.asBoolean() } - node["minLength"]?.let { this.minLength = it.asInt() } - node["maxLength"]?.let { this.maxLength = it.asInt() } - node["minimum"]?.let { this.minimum = BigDecimal(it.asText()) } - node["maximum"]?.let { this.maximum = BigDecimal(it.asText()) } - node["maxItems"]?.let { this.maxItems = it.asInt() } - node["minItems"]?.let { this.minItems = it.asInt() } - node["uniqueItems"]?.let { this.uniqueItems = it.asBoolean() } - node["format"]?.let { this.format = it.asText() } - node["items"]?.let { this.items = toSchema(it) } - node["properties"]?.let { this.properties = it.collectFields().associate { prop -> prop.key to toSchema(prop.value) } } - node["additionalProperties"]?.let { this.additionalProperties = toSchema(it) } - node["allOf"]?.let { this.allOf = it.collectElements().map { prop -> toSchema(prop) } } - node["anyOf"]?.let { this.anyOf = it.collectElements().map { prop -> toSchema(prop) } } - node["required"]?.let { this.required = it.collectElements().map { prop -> prop.asText() } } - node["enum"]?.let { this.enum = it.collectElements().map { prop -> prop.asText() } } - node["const"]?.let { this.setConst(it.asText()) } - node["\$defs"]?.let { throw UnsupportedOperationException("'\"defs' in json-schema are not supported") } - node["\$ref"]?.let { throw UnsupportedOperationException("'\"refs' in json-schema are not supported") } - } - } - - private fun JsonNode.collectFields(): List> { - return this.fields().asSequence().toList() - } - - private fun JsonNode.collectElements(): List { - return this.elements().asSequence().toList() - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiComponentsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiComponentsBuilder.kt deleted file mode 100644 index 92e1936..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiComponentsBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme -import io.swagger.v3.oas.models.Components - -/** - * Builder for the OpenAPI Components Object - */ -class OApiComponentsBuilder { - - private val exampleBuilder = OApiExampleBuilder() - private val securitySchemesBuilder = OApiSecuritySchemesBuilder() - - fun build(ctx: ComponentsContext, securitySchemes: List): Components { - return Components().apply { - schemas = ctx.schemas - examples = ctx.examples.mapValues { - exampleBuilder.build("", it.value, ComponentsContext.NOOP) - } - if (securitySchemes.isNotEmpty()) { - this.securitySchemes = securitySchemesBuilder.build(securitySchemes) - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiContentBuilder.kt deleted file mode 100644 index 9ec1f5b..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiContentBuilder.kt +++ /dev/null @@ -1,188 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomObjectSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomOpenApiSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemas -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema -import io.ktor.http.ContentType -import io.swagger.v3.oas.models.media.Content -import io.swagger.v3.oas.models.media.Encoding -import io.swagger.v3.oas.models.media.MediaType -import io.swagger.v3.oas.models.media.Schema -import io.swagger.v3.oas.models.media.XML -import java.lang.reflect.Type - -/** - * Builder for the OpenAPI Content Object (e.g. request and response bodies) - */ -class OApiContentBuilder { - - private val schemaBuilder = OApiSchemaBuilder() - private val exampleBuilder = OApiExampleBuilder() - private val headerBuilder = OApiHeaderBuilder() - private val jsonToSchemaConverter = JsonToOpenApiSchemaConverter() - - fun build(body: OpenApiBaseBody, components: ComponentsContext, config: SwaggerUIPluginConfig): Content { - return when (body) { - is OpenApiSimpleBody -> buildSimpleBody(body, components, config) - is OpenApiMultipartBody -> buildMultipartBody(body, components, config) - } - } - - private fun buildSimpleBody(body: OpenApiSimpleBody, components: ComponentsContext, config: SwaggerUIPluginConfig): Content { - return Content().apply { - val maybeSchemaObj = buildSchema(body, components, config) - body.getMediaTypes().forEach { mediaType -> - if (maybeSchemaObj == null) { - addMediaType(mediaType.toString(), MediaType()) - } else { - addMediaType(mediaType.toString(), buildMediaType(body.getExamples(), maybeSchemaObj, components)) - } - } - if (body.getMediaTypes().isEmpty() && maybeSchemaObj != null) { - addMediaType(chooseMediaType(maybeSchemaObj).toString(), buildMediaType(body.getExamples(), maybeSchemaObj, components)) - } - } - } - - private fun buildMultipartBody(body: OpenApiMultipartBody, components: ComponentsContext, config: SwaggerUIPluginConfig): Content { - val mediaTypes = body.getMediaTypes().ifEmpty { setOf(ContentType.MultiPart.FormData) } - return Content().apply { - mediaTypes.forEach { mediaType -> - addMediaType(mediaType.toString(), MediaType().apply { - schema = buildMultipartSchema(body, components, config) - encoding = buildMultipartEncoding(body, config) - }) - } - } - } - - private fun buildMultipartSchema( - body: OpenApiMultipartBody, - components: ComponentsContext, - config: SwaggerUIPluginConfig - ): Schema { - return Schema().apply { - type = "object" - properties = mutableMapOf?>().also { props -> - body.getParts().forEach { part -> - if (part.customSchema != null) { - buildSchemaFromCustom(part.customSchema!!, components, config.getCustomSchemas()) - } else { - props[part.name] = buildSchemaFromType(part.type, components, config) - } - } - } - } - } - - private fun buildMultipartEncoding(body: OpenApiMultipartBody, config: SwaggerUIPluginConfig): MutableMap? { - if (body.getParts().flatMap { it.mediaTypes }.isEmpty()) { - return null - } else { - return mutableMapOf().also { - body.getParts() - .filter { it.mediaTypes.isNotEmpty() || it.getHeaders().isNotEmpty() } - .forEach { part -> - it[part.name] = Encoding().apply { - contentType = part.mediaTypes.joinToString(", ") { it.toString() } - headers = part.getHeaders().mapValues { headerBuilder.build(it.value, config) } - } - } - } - } - } - - private fun buildSchema(body: OpenApiSimpleBody, components: ComponentsContext, config: SwaggerUIPluginConfig): Schema? { - return if (body.customSchema != null) { - buildSchemaFromCustom(body.customSchema!!, components, config.getCustomSchemas()) - } else { - buildSchemaFromType(body.type, components, config) - } - } - - private fun buildSchemaFromType(type: Type?, components: ComponentsContext, config: SwaggerUIPluginConfig): Schema? { - return type - ?.let { schemaBuilder.build(it, components, config) } - ?.let { prepareForXml(type, it) } - } - - private fun buildSchemaFromCustom( - customSchema: CustomSchemaRef, - components: ComponentsContext, - customSchemas: CustomSchemas - ): Schema { - val custom = customSchemas.getSchema(customSchema.schemaId) - if (custom == null) { - return Schema() - } else { - return when (custom) { - is CustomJsonSchema -> { - val schema = jsonToSchemaConverter.toSchema(custom.provider()) - components.addSchema(customSchema.schemaId, schema) - } - is CustomOpenApiSchema -> { - components.addSchema(customSchema.schemaId, custom.provider()) - } - is RemoteSchema -> { - Schema().apply { - type = "object" - `$ref` = custom.url - } - } - }.let { schema -> - when (customSchema) { - is CustomObjectSchemaRef -> schema - is CustomArraySchemaRef -> Schema().apply { - this.type = "array" - this.items = schema - } - } - } - } - } - - private fun buildMediaType(examples: Map, schema: Schema<*>, components: ComponentsContext): MediaType { - return MediaType().apply { - this.schema = schema - examples.forEach { (name, obj) -> - addExamples(name, exampleBuilder.build(name, obj, components)) - } - } - } - - private fun chooseMediaType(schema: Schema<*>): ContentType { - return when (schema.type) { - "integer" -> ContentType.Text.Plain - "number" -> ContentType.Text.Plain - "boolean" -> ContentType.Text.Plain - "string" -> ContentType.Text.Plain - "object" -> ContentType.Application.Json - "array" -> ContentType.Application.Json - null -> ContentType.Application.Json - else -> ContentType.Text.Plain - } - } - - private fun prepareForXml(type: Type, schema: Schema): Schema { - schema.xml = XML().apply { - if (type is Class<*>) { - name = if (type.isArray) { - type.componentType.simpleName - } else { - type.simpleName - } - } - } - return schema - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiExampleBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiExampleBuilder.kt deleted file mode 100644 index e1548e2..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiExampleBuilder.kt +++ /dev/null @@ -1,25 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample -import io.swagger.v3.oas.models.examples.Example - -/** - * Builder for the OpenAPI Example-Object - */ -class OApiExampleBuilder { - - fun build(name: String, example: OpenApiExample, components: ComponentsContext): Example { - return if (components.examplesInComponents) { - Example().apply { - `$ref` = components.addExample(name, example) - } - } else { - Example().apply { - value = example.value - summary = example.summary - description = example.description - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiHeaderBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiHeaderBuilder.kt deleted file mode 100644 index 1b2fc92..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiHeaderBuilder.kt +++ /dev/null @@ -1,23 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiHeader -import io.swagger.v3.oas.models.headers.Header - -/** - * Builder for the OpenAPI Header Object - */ -class OApiHeaderBuilder { - - private val schemaBuilder = OApiSchemaBuilder() - - fun build(header: OpenApiHeader, config: SwaggerUIPluginConfig): Header { - return Header().apply { - description = header.description - required = header.required - deprecated = header.deprecated - schema = header.type?.let { t -> schemaBuilder.build(t, ComponentsContext.NOOP, config) } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiInfoBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiInfoBuilder.kt deleted file mode 100644 index a64d32e..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiInfoBuilder.kt +++ /dev/null @@ -1,35 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo -import io.swagger.v3.oas.models.info.Contact -import io.swagger.v3.oas.models.info.Info -import io.swagger.v3.oas.models.info.License - -/** - * Builder for the OpenAPI Info-Object - */ -class OApiInfoBuilder { - - fun build(info: OpenApiInfo): Info { - return Info().apply { - title = info.title - version = info.version - description = info.description - termsOfService = info.termsOfService - info.getContact()?.let { cfgContact -> - contact = Contact().apply { - name = cfgContact.name - url = cfgContact.url - email = cfgContact.email - } - } - info.getLicense()?.let { cfgLicense -> - license = License().apply { - name = cfgLicense.name - url = cfgLicense.url - } - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiJsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiJsonSchemaBuilder.kt deleted file mode 100644 index a2583b8..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiJsonSchemaBuilder.kt +++ /dev/null @@ -1,75 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ObjectNode -import com.github.victools.jsonschema.generator.SchemaGenerator -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.swagger.v3.oas.models.media.Schema -import java.lang.reflect.Type - -/** - * Builder for an OpenAPI Schema Object that describes a json-object (or array) - */ -class OApiJsonSchemaBuilder { - - private val jsonToSchemaConverter = JsonToOpenApiSchemaConverter() - - fun build(type: Type, components: ComponentsContext, config: SwaggerUIPluginConfig): Schema { - if (components.schemasInComponents) { - val schema = createSchema(type, config) - if (schema.type == "array") { - return components.addArraySchema(type, schema) - } else { - return components.addSchema(type, schema) - } - } else { - return createSchema(type, config) - } - } - - - private fun createSchema(type: Type, config: SwaggerUIPluginConfig): Schema { - return if (type is Class<*> && type.isArray) { - Schema().apply { - this.type = "array" - this.items = createObjectSchema(type.componentType, config) - } - } else if (type is Class<*> && type.isEnum) { - Schema().apply { - this.type = "string" - this.enum = type.enumConstants.map { it.toString() } - } - } else { - return createObjectSchema(type, config) - } - } - - - private fun createObjectSchema(type: Type, config: SwaggerUIPluginConfig): Schema { - return if (type is Class<*> && type.isEnum) { - Schema().apply { - this.type = "string" - this.enum = type.enumConstants.map { it.toString() } - } - } else { - val jsonSchema = createObjectJsonSchema(type, config) - return jsonToSchemaConverter.toSchema(jsonSchema) - } - } - - private fun createObjectJsonSchema(type: Type, config: SwaggerUIPluginConfig): ObjectNode { - if (config.getCustomSchemas().getJsonSchemaBuilder() != null) { - val jsonSchema = config.getCustomSchemas().getJsonSchemaBuilder()?.let { it(type) } - if (jsonSchema != null) { - return ObjectMapper().readTree(jsonSchema) as ObjectNode - } - } - return generateJsonSchema(type, config) - } - - private fun generateJsonSchema(type: Type, config: SwaggerUIPluginConfig): ObjectNode { - val generatorConfig = config.schemaGeneratorConfigBuilder.build() - return SchemaGenerator(generatorConfig).generateSchema(type) - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiOAuthFlowsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiOAuthFlowsBuilder.kt deleted file mode 100644 index 597f0ea..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiOAuthFlowsBuilder.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenIdOAuthFlow -import io.github.smiley4.ktorswaggerui.dsl.OpenIdOAuthFlows -import io.swagger.v3.oas.models.security.OAuthFlow -import io.swagger.v3.oas.models.security.OAuthFlows -import io.swagger.v3.oas.models.security.Scopes - -/** - * Builder for the Flows Info-Object - */ -class OApiOAuthFlowsBuilder { - - fun build(flows: OpenIdOAuthFlows): OAuthFlows { - return OAuthFlows().apply { - implicit = flows.getImplicit()?.let { build(it) } - password = flows.getPassword()?.let { build(it) } - clientCredentials = flows.getClientCredentials()?.let { build(it) } - authorizationCode = flows.getAuthorizationCode()?.let { build(it) } - } - } - - private fun build(flow: OpenIdOAuthFlow): OAuthFlow { - return OAuthFlow().apply { - authorizationUrl = flow.authorizationUrl - tokenUrl = flow.tokenUrl - refreshUrl = flow.refreshUrl - scopes = flow.scopes?.let { s -> - Scopes().apply { - s.forEach { (k, v) -> addString(k, v) } - } - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiParametersBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiParametersBuilder.kt deleted file mode 100644 index afcb826..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiParametersBuilder.kt +++ /dev/null @@ -1,36 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.swagger.v3.oas.models.parameters.Parameter - -/** - * Builder for OpenAPI Parameters - */ -class OApiParametersBuilder { - - private val schemaBuilder = OApiSchemaBuilder() - - - fun build(parameters: List, config: SwaggerUIPluginConfig): List { - return parameters.map { parameter -> - Parameter().apply { - `in` = when (parameter.location) { - OpenApiRequestParameter.Location.QUERY -> "query" - OpenApiRequestParameter.Location.HEADER -> "header" - OpenApiRequestParameter.Location.PATH -> "path" - } - name = parameter.name - schema = schemaBuilder.build(parameter.type, ComponentsContext.NOOP, config) - description = parameter.description - required = parameter.required - deprecated = parameter.deprecated - allowEmptyValue = parameter.allowEmptyValue - explode = parameter.explode - example = parameter.example - allowReserved = parameter.allowReserved - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathBuilder.kt deleted file mode 100644 index 56a6047..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathBuilder.kt +++ /dev/null @@ -1,86 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.swagger.v3.oas.models.Operation -import io.swagger.v3.oas.models.PathItem -import io.swagger.v3.oas.models.responses.ApiResponses -import io.swagger.v3.oas.models.security.SecurityRequirement - -/** - * Builder for a single OpenAPI Path - */ -class OApiPathBuilder { - - private val parametersBuilder = OApiParametersBuilder() - private val requestBodyBuilder = OApiRequestBodyBuilder() - private val responsesBuilder = OApiResponsesBuilder() - - fun build(route: RouteMeta, components: ComponentsContext, config: SwaggerUIPluginConfig): Pair { - return route.path to PathItem().apply { - val operation = Operation().apply { - tags = buildTags(route, config.automaticTagGenerator) - summary = route.documentation.summary - description = route.documentation.description - operationId = route.documentation.operationId - parameters = parametersBuilder.build(route.documentation.getRequest().getParameters(), config) - deprecated = route.documentation.deprecated - route.documentation.getRequest().getBody()?.let { - requestBody = requestBodyBuilder.build(it, components, config) - } - responses = ApiResponses().apply { - responsesBuilder.build(route.documentation.getResponses().getResponses(), components, config).forEach { - addApiResponse(it.first, it.second) - } - if (shouldAddUnauthorized(route, config.getDefaultUnauthorizedResponse())) { - responsesBuilder.build(listOf(config.getDefaultUnauthorizedResponse()!!), components, config).forEach { - addApiResponse(it.first, it.second) - } - } - } - if (route.protected) { - val securitySchemes = mutableSetOf().also { schemes -> - route.documentation.securitySchemeName?.also { schemes.add(it) } - route.documentation.securitySchemeNames?.also { schemes.addAll(it) } - } - if (securitySchemes.isEmpty()) { - config.defaultSecuritySchemeName?.also { securitySchemes.add(it) } - config.defaultSecuritySchemeNames?.also { securitySchemes.addAll(it) } - } - if (securitySchemes.isNotEmpty()) { - security = securitySchemes.map { - SecurityRequirement().apply { - addList(it, emptyList()) - } - } - } - } - } - when (route.method) { - HttpMethod.Get -> get = operation - HttpMethod.Post -> post = operation - HttpMethod.Put -> put = operation - HttpMethod.Patch -> patch = operation - HttpMethod.Delete -> delete = operation - HttpMethod.Head -> head = operation - HttpMethod.Options -> options = operation - } - } - } - - private fun buildTags(route: RouteMeta, tagGenerator: ((url: List) -> String?)?): List { - val generatedTags = tagGenerator?.let { - it(route.path.split("/").filter { it.isNotEmpty() }) - } - return (route.documentation.tags + generatedTags).filterNotNull() - } - - private fun shouldAddUnauthorized(config: RouteMeta, defaultUnauthorizedResponses: OpenApiResponse?): Boolean { - val unauthorizedCode = HttpStatusCode.Unauthorized.value.toString(); - return defaultUnauthorizedResponses != null - && config.protected - && config.documentation.getResponses().getResponses().count { it.statusCode == unauthorizedCode } == 0 - } -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathsBuilder.kt deleted file mode 100644 index 7f293e2..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiPathsBuilder.kt +++ /dev/null @@ -1,60 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.ktor.server.application.Application -import io.swagger.v3.oas.models.PathItem -import io.swagger.v3.oas.models.Paths -import mu.KotlinLogging - -/** - * Builder for the OpenAPI Paths - */ -class OApiPathsBuilder(private val routeCollector: RouteCollector) { - - private val pathBuilder = OApiPathBuilder() - private val logger = KotlinLogging.logger {} - - fun build(config: SwaggerUIPluginConfig, application: Application, components: ComponentsContext): Paths { - return Paths().apply { - routeCollector.collectRoutes(application, config) - .filter { removeLeadingSlash(it.path) != removeLeadingSlash(config.getSwaggerUI().swaggerUrl) } - .filter { removeLeadingSlash(it.path) != removeLeadingSlash("${config.getSwaggerUI().swaggerUrl}/api.json") } - .filter { removeLeadingSlash(it.path) != removeLeadingSlash("${config.getSwaggerUI().swaggerUrl}/{filename}") } - .filter { !config.getSwaggerUI().forwardRoot || it.path != "/" } - .filter { !it.documentation.hidden } - .filter { path -> - config.pathFilter - ?.let { it(path.method, path.path.split("/").filter { it.isNotEmpty() }) } - ?: true - } - .onEach { logger.debug("Configure path: ${it.method.value} ${it.path}") } - .map { pathBuilder.build(it, components, config) } - .forEach { addToPaths(this, it.first, it.second) } - } - } - - private fun removeLeadingSlash(str: String): String { - return if (str.startsWith("/")) { - str.substring(1) - } else { - str - } - } - - private fun addToPaths(paths: Paths, name: String, item: PathItem) { - paths[name] - ?.let { - it.get = if (item.get != null) item.get else it.get - it.put = if (item.put != null) item.put else it.put - it.post = if (item.post != null) item.post else it.post - it.delete = if (item.delete != null) item.delete else it.delete - it.options = if (item.options != null) item.options else it.options - it.head = if (item.head != null) item.head else it.head - it.patch = if (item.patch != null) item.patch else it.patch - it.trace = if (item.trace != null) item.trace else it.trace - } - ?: paths.addPathItem(name, item) - } - - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiRequestBodyBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiRequestBodyBuilder.kt deleted file mode 100644 index 8349796..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiRequestBodyBuilder.kt +++ /dev/null @@ -1,22 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.swagger.v3.oas.models.parameters.RequestBody - -/** - * Builder for the OpenAPI Request Body - */ -class OApiRequestBodyBuilder { - - private val contentBuilder = OApiContentBuilder() - - fun build(body: OpenApiBaseBody, components: ComponentsContext, config: SwaggerUIPluginConfig): RequestBody { - return RequestBody().apply { - description = body.description - required = body.required - content = contentBuilder.build(body, components, config) - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiResponsesBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiResponsesBuilder.kt deleted file mode 100644 index 6ddd1ff..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiResponsesBuilder.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.swagger.v3.oas.models.responses.ApiResponse - -/** - * Builder for the OpenAPI Responses - */ -class OApiResponsesBuilder { - - private val contentBuilder = OApiContentBuilder() - private val headerBuilder = OApiHeaderBuilder() - - fun build(responses: List, components: ComponentsContext, config: SwaggerUIPluginConfig): List> { - return responses.map { responseCfg -> - responseCfg.statusCode to ApiResponse().apply { - description = responseCfg.description - responseCfg.getBody()?.let { - content = contentBuilder.build(responseCfg.getBody()!!, components, config) - } - headers = responseCfg.getHeaders().mapValues { headerBuilder.build(it.value, config) } - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSchemaBuilder.kt deleted file mode 100644 index 0db699c..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSchemaBuilder.kt +++ /dev/null @@ -1,251 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import com.fasterxml.jackson.core.type.TypeReference -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.swagger.v3.oas.models.media.Schema -import java.io.File -import java.lang.reflect.Type -import java.math.BigDecimal - -/** - * Builder for an OpenAPI Schema Object - */ -class OApiSchemaBuilder { - - private companion object { - - private val BYTE: Type = (object : TypeReference() {}.type) - private val SHORT: Type = (object : TypeReference() {}.type) - private val INT: Type = (object : TypeReference() {}.type) - private val LONG: Type = (object : TypeReference() {}.type) - private val UBYTE: Type = (object : TypeReference() {}.type) - private val USHORT: Type = (object : TypeReference() {}.type) - private val UINT: Type = (object : TypeReference() {}.type) - private val ULONG: Type = (object : TypeReference() {}.type) - private val FLOAT: Type = (object : TypeReference() {}.type) - private val DOUBLE: Type = (object : TypeReference() {}.type) - private val BOOLEAN: Type = (object : TypeReference() {}.type) - private val CHAR: Type = (object : TypeReference() {}.type) - private val STRING: Type = (object : TypeReference() {}.type) - private val ARRAY_BYTE: Type = (object : TypeReference>() {}.type) - private val ARRAY_SHORT: Type = (object : TypeReference>() {}.type) - private val ARRAY_INT: Type = (object : TypeReference>() {}.type) - private val ARRAY_LONG: Type = (object : TypeReference>() {}.type) - private val ARRAY_UBYTE: Type = (object : TypeReference>() {}.type) - private val ARRAY_USHORT: Type = (object : TypeReference>() {}.type) - private val ARRAY_UINT: Type = (object : TypeReference>() {}.type) - private val ARRAY_ULONG: Type = (object : TypeReference>() {}.type) - private val ARRAY_FLOAT: Type = (object : TypeReference>() {}.type) - private val ARRAY_DOUBLE: Type = (object : TypeReference>() {}.type) - private val ARRAY_CHAR: Type = (object : TypeReference>() {}.type) - private val ARRAY_STRING: Type = (object : TypeReference>() {}.type) - private val FILE: Type = (object : TypeReference() {}.type) - - fun typeRefToJavaType(type: Type): Type = when (type) { - BYTE -> Byte::class.java - SHORT -> Short::class.java - INT -> Int::class.java - LONG -> Long::class.java - UBYTE -> UByte::class.java - USHORT -> UShort::class.java - UINT -> UInt::class.java - ULONG -> ULong::class.java - FLOAT -> Float::class.java - DOUBLE -> Double::class.java - BOOLEAN -> Boolean::class.java - CHAR -> Char::class.java - STRING -> String::class.java - ARRAY_BYTE -> Array::class.java - ARRAY_SHORT -> Array::class.java - ARRAY_INT -> Array::class.java - ARRAY_LONG -> Array::class.java - ARRAY_UBYTE -> Array::class.java - ARRAY_USHORT -> Array::class.java - ARRAY_UINT -> Array::class.java - ARRAY_ULONG -> Array::class.java - ARRAY_FLOAT -> Array::class.java - ARRAY_DOUBLE -> Array::class.java - ARRAY_CHAR -> Array::class.java - ARRAY_STRING -> Array::class.java - FILE -> File::class.java - else -> type - } - - } - - private val jsonSchemaBuilder = OApiJsonSchemaBuilder() - - fun build(type: Type, components: ComponentsContext, config: SwaggerUIPluginConfig): Schema { - return Schema().apply { - when (typeRefToJavaType(type)) { - Byte::class.java -> { - this.type = "integer" - minimum = BigDecimal.valueOf(Byte.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(Byte.MAX_VALUE.toLong()) - } - Short::class.java -> { - this.type = "integer" - minimum = BigDecimal.valueOf(Short.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(Short.MAX_VALUE.toLong()) - } - Int::class.java -> { - this.type = "integer" - format = "int32" - } - Long::class.java -> { - this.type = "integer" - format = "int64" - } - UByte::class.java -> { - this.type = "integer" - minimum = BigDecimal.valueOf(UByte.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(UByte.MAX_VALUE.toLong()) - } - UShort::class.java -> { - this.type = "integer" - minimum = BigDecimal.valueOf(UShort.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(UShort.MAX_VALUE.toLong()) - } - UInt::class.java -> { - this.type = "integer" - minimum = BigDecimal.valueOf(UInt.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(UInt.MAX_VALUE.toLong()) - } - ULong::class.java -> { - this.type = "integer" - minimum = BigDecimal.valueOf(ULong.MIN_VALUE.toLong()) - } - Float::class.java -> { - this.type = "number" - format = "float" - } - Double::class.java -> { - this.type = "number" - format = "double" - } - Boolean::class.java -> { - this.type = "boolean" - } - Char::class.java -> { - this.type = "string" - minLength = 1 - maxLength = 1 - } - String::class.java -> { - this.type = "string" - } - Array::class.java, ByteArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - minimum = BigDecimal.valueOf(Byte.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(Byte.MAX_VALUE.toLong()) - } - } - Array::class.java, ShortArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - minimum = BigDecimal.valueOf(Short.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(Short.MAX_VALUE.toLong()) - } - } - Array::class.java, IntArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - format = "int32" - } - } - Array::class.java, LongArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - format = "int64" - } - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - minimum = BigDecimal.valueOf(UByte.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(UByte.MAX_VALUE.toLong()) - } - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - minimum = BigDecimal.valueOf(UShort.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(UShort.MAX_VALUE.toLong()) - } - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - minimum = BigDecimal.valueOf(UInt.MIN_VALUE.toLong()) - maximum = BigDecimal.valueOf(UInt.MAX_VALUE.toLong()) - } - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "integer" - minimum = BigDecimal.valueOf(ULong.MIN_VALUE.toLong()) - } - } - Array::class.java, FloatArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "number" - format = "float" - } - } - Array::class.java, DoubleArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "number" - format = "double" - } - } - - Array::class.java, BooleanArray::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "boolean" - } - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "string" - minLength = 1 - maxLength = 1 - } - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "string" - } - } - File::class.java -> { - this.type = "string" - format = "binary" - } - Array::class.java -> { - this.type = "array" - items = Schema().apply { - this.type = "string" - format = "binary" - } - } - else -> { - return jsonSchemaBuilder.build(type, components, config) - } - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSecuritySchemesBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSecuritySchemesBuilder.kt deleted file mode 100644 index 2342aeb..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiSecuritySchemesBuilder.kt +++ /dev/null @@ -1,56 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation -import io.github.smiley4.ktorswaggerui.dsl.AuthScheme -import io.github.smiley4.ktorswaggerui.dsl.AuthType -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme -import io.swagger.v3.oas.models.security.SecurityScheme - -/** - * Builder for OpenAPI SecurityScheme-Objects - */ -class OApiSecuritySchemesBuilder { - - private val authFlowsBuilder = OApiOAuthFlowsBuilder() - - fun build(securitySchemes: List): Map { - return mutableMapOf().apply { - securitySchemes.forEach { - put(it.name, SecurityScheme().apply { - description = it.description - name = it.name - type = when (it.type) { - AuthType.API_KEY -> SecurityScheme.Type.APIKEY - AuthType.HTTP -> SecurityScheme.Type.HTTP - AuthType.OAUTH2 -> SecurityScheme.Type.OAUTH2 - AuthType.OPENID_CONNECT -> SecurityScheme.Type.OPENIDCONNECT - AuthType.MUTUAL_TLS -> SecurityScheme.Type.MUTUALTLS - null -> null - } - `in` = when (it.location) { - AuthKeyLocation.QUERY -> SecurityScheme.In.QUERY - AuthKeyLocation.HEADER -> SecurityScheme.In.HEADER - AuthKeyLocation.COOKIE -> SecurityScheme.In.COOKIE - null -> null - } - scheme = when (it.scheme) { - AuthScheme.BASIC -> "Basic" - AuthScheme.BEARER -> "Bearer" - AuthScheme.DIGEST -> "Digest" - AuthScheme.HOBA -> "HOBA" - AuthScheme.MUTUAL -> "Mutual" - AuthScheme.OAUTH -> "OAuth" - AuthScheme.SCRAM_SHA_1 -> "SCRAM-SHA-1" - AuthScheme.SCRAM_SHA_256 -> "SCRAM-SHA-256" - AuthScheme.VAPID -> "vapid" - else -> null - } - bearerFormat = it.bearerFormat - flows = it.getFlows()?.let { f -> authFlowsBuilder.build(f) } - openIdConnectUrl = it.openIdConnectUrl - }) - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiServersBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiServersBuilder.kt deleted file mode 100644 index 64518bd..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiServersBuilder.kt +++ /dev/null @@ -1,20 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer -import io.swagger.v3.oas.models.servers.Server - -/** - * Builder for the OpenAPI Server-Objects - */ -class OApiServersBuilder { - - fun build(servers: List): List { - return servers.map { - Server().apply { - url = it.url - description = it.description - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiTagsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiTagsBuilder.kt deleted file mode 100644 index 28af0ea..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/OApiTagsBuilder.kt +++ /dev/null @@ -1,27 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag -import io.swagger.v3.oas.models.ExternalDocumentation -import io.swagger.v3.oas.models.tags.Tag - -/** - * Builder for the OpenAPI Tag-Objects - */ -class OApiTagsBuilder { - - fun build(tags: List): List { - return tags.map { - Tag().apply { - name = it.name - description = it.description - if (it.externalDocDescription != null || it.externalDocUrl != null) { - externalDocs = ExternalDocumentation().apply { - description = it.externalDocDescription - url = it.externalDocUrl - } - } - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteCollector.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteCollector.kt deleted file mode 100644 index cafadda..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteCollector.kt +++ /dev/null @@ -1,125 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.DocumentedRouteSelector -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.ktor.http.HttpMethod -import io.ktor.server.application.Application -import io.ktor.server.application.plugin -import io.ktor.server.auth.AuthenticationRouteSelector -import io.ktor.server.routing.HttpMethodRouteSelector -import io.ktor.server.routing.RootRouteSelector -import io.ktor.server.routing.Route -import io.ktor.server.routing.RouteSelector -import io.ktor.server.routing.Routing -import io.ktor.server.routing.TrailingSlashRouteSelector -import kotlin.reflect.full.isSubclassOf - -class RouteCollector { - - /** - * Collect all routes from the given application - */ - fun collectRoutes(application: Application, config: SwaggerUIPluginConfig): Sequence { - return allRoutes(application.plugin(Routing)) - .asSequence() - .map { route -> - RouteMeta( - method = getMethod(route), - path = getPath(route, config), - documentation = getDocumentation(route, OpenApiRoute()), - protected = isProtected(route) - ) - } - } - - private fun getDocumentation(route: Route, base: OpenApiRoute): OpenApiRoute { - var documentation = base - if (route.selector is DocumentedRouteSelector) { - documentation = merge(documentation, (route.selector as DocumentedRouteSelector).documentation) - } - return if (route.parent != null) { - getDocumentation(route.parent!!, documentation) - } else { - documentation - } - } - - private fun getMethod(route: Route): HttpMethod { - return (route.selector as HttpMethodRouteSelector).method - } - - private fun getPath(route: Route, config: SwaggerUIPluginConfig): String { - val selector = route.selector - return if (isIgnoredSelector(selector, config)) { - route.parent?.let { getPath(it, config) } ?: "" - } else { - when (route.selector) { - is TrailingSlashRouteSelector -> "/" - is RootRouteSelector -> "" - is DocumentedRouteSelector -> route.parent?.let { getPath(it, config) } ?: "" - is HttpMethodRouteSelector -> route.parent?.let { getPath(it, config) } ?: "" - is AuthenticationRouteSelector -> route.parent?.let { getPath(it, config) } ?: "" - else -> (route.parent?.let { getPath(it, config) } ?: "") + "/" + route.selector.toString() - } - } - } - - private fun isIgnoredSelector(selector: RouteSelector, config: SwaggerUIPluginConfig): Boolean { - return when (selector) { - is TrailingSlashRouteSelector -> false - is RootRouteSelector -> false - is DocumentedRouteSelector -> true - is HttpMethodRouteSelector -> true - is AuthenticationRouteSelector -> true - else -> config.ignoredRouteSelectors.any { selector::class.isSubclassOf(it) } - } - } - - private fun isProtected(route: Route): Boolean { - return when (route.selector) { - is AuthenticationRouteSelector -> true - is TrailingSlashRouteSelector -> false - is RootRouteSelector -> false - is DocumentedRouteSelector -> route.parent?.let { isProtected(it) } ?: false - is HttpMethodRouteSelector -> route.parent?.let { isProtected(it) } ?: false - else -> route.parent?.let { isProtected(it) } ?: false - } - } - - private fun allRoutes(root: Route): List { - return (listOf(root) + root.children.flatMap { allRoutes(it) }) - .filter { it.selector is HttpMethodRouteSelector } - } - - private fun merge(a: OpenApiRoute, b: OpenApiRoute): OpenApiRoute { - return OpenApiRoute().apply { - tags = mutableListOf().also { - it.addAll(a.tags) - it.addAll(b.tags) - } - summary = a.summary ?: b.summary - description = a.description ?: b.description - operationId = a.operationId ?: b.operationId - securitySchemeName = a.securitySchemeName ?: b.securitySchemeName - securitySchemeNames = mutableSetOf().also { merged -> - a.securitySchemeNames?.let { merged.addAll(it) } - b.securitySchemeNames?.let { merged.addAll(it) } - } - deprecated = a.deprecated || b.deprecated - hidden = a.hidden || b.hidden - request { - (getParameters() as MutableList).also { - it.addAll(a.getRequest().getParameters()) - it.addAll(b.getRequest().getParameters()) - } - setBody(a.getRequest().getBody() ?: b.getRequest().getBody()) - } - response { - b.getResponses().getResponses().forEach { response -> addResponse(response) } - a.getResponses().getResponses().forEach { response -> addResponse(response) } - } - } - } - -} diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteMeta.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteMeta.kt deleted file mode 100644 index ac0ab6e..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/specbuilder/RouteMeta.kt +++ /dev/null @@ -1,14 +0,0 @@ -package io.github.smiley4.ktorswaggerui.specbuilder - -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.ktor.http.HttpMethod - -/** - * Information about a route - */ -data class RouteMeta( - val path: String, - val method: HttpMethod, - val documentation: OpenApiRoute, - val protected: Boolean -) diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt index cf4683c..7dabc11 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompleteExample.kt @@ -76,8 +76,6 @@ private fun Application.myModule() { tag("math") { description = "Routes for math related operations" } - schemasInComponentSection = true - examplesInComponentSection = true automaticTagGenerator = { url -> url.firstOrNull() } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt index 76bbcad..ff7f250 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt @@ -37,7 +37,6 @@ private fun Application.myModule() { install(SwaggerUI) { // don't show the test-routes providing json-schemas pathFilter = { _, url -> url.firstOrNull() != "schema" } - schemasInComponentSection schemas { // specify a custom json-schema with the id 'myRequestData' json("myRequestData") { diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt new file mode 100644 index 0000000..79f0897 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -0,0 +1,155 @@ +package io.github.smiley4.ktorswaggerui.tests.openapi + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo +import io.github.smiley4.ktorswaggerui.spec.openapi.ComponentsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.InfoBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.LicenseBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OpenApiBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.PathBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.PathsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.kotest.core.spec.style.StringSpec +import io.kotest.engine.test.logging.info +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.collections.shouldHaveSize +import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import io.swagger.v3.oas.models.OpenAPI +import io.swagger.v3.oas.models.info.Info + + +class OpenApiBuilderTest : StringSpec({ + + "default openapi object" { + buildOpenApiObject(emptyList()).also { openapi -> + openapi.info shouldNotBe null + openapi.extensions shouldBe null + openapi.servers shouldHaveSize 0 + openapi.security shouldBe null + openapi.tags shouldHaveSize 0 + openapi.paths shouldHaveSize 0 + openapi.components shouldNotBe null + openapi.extensions shouldBe null + } + } + + "multiple servers" { + val config = SwaggerUIPluginConfig().also { + it.server { + url = "http://localhost:8080" + description = "Development Server" + } + it.server { + url = "https://127.0.0.1" + description = "Production Server" + } + } + buildOpenApiObject(emptyList(), config).also { openapi -> + openapi.servers shouldHaveSize 2 + openapi.servers.map { it.url } shouldContainExactlyInAnyOrder listOf( + "http://localhost:8080", + "https://127.0.0.1" + ) + } + } + + "multiple tags" { + val config = SwaggerUIPluginConfig().also { + it.tag("tag-1") { + description = "first test tag" + } + it.tag("tag-2") { + description = "second test tag" + } + } + buildOpenApiObject(emptyList(), config).also { openapi -> + openapi.tags shouldHaveSize 2 + openapi.tags.map { it.name} shouldContainExactlyInAnyOrder listOf( + "tag-1", + "tag-2" + ) + } + } + +}) { + + companion object { + + private val defaultPluginConfig = SwaggerUIPluginConfig() + + private fun schemaContext(pluginConfig: SwaggerUIPluginConfig): SchemaContext { + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + } + + private fun buildOpenApiObject(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): OpenAPI { + val schemaContext = schemaContext(pluginConfig).initialize(routes) + return OpenApiBuilder( + config = pluginConfig, + schemaContext = schemaContext, + infoBuilder = InfoBuilder( + contactBuilder = ContactBuilder(), + licenseBuilder = LicenseBuilder() + ), + serverBuilder = ServerBuilder(), + tagBuilder = TagBuilder( + externalDocumentationBuilder = ExternalDocumentationBuilder() + ), + pathsBuilder = PathsBuilder( + pathBuilder = PathBuilder( + operationBuilder = OperationBuilder( + operationTagsBuilder = OperationTagsBuilder(pluginConfig), + parameterBuilder = ParameterBuilder(schemaContext), + requestBodyBuilder = RequestBodyBuilder( + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + responsesBuilder = ResponsesBuilder( + responseBuilder = ResponseBuilder( + headerBuilder = HeaderBuilder(schemaContext), + contentBuilder = ContentBuilder( + schemaContext = schemaContext, + exampleBuilder = ExampleBuilder(), + headerBuilder = HeaderBuilder(schemaContext) + ) + ), + config = pluginConfig + ), + securityRequirementsBuilder = SecurityRequirementsBuilder(pluginConfig), + ) + ) + ), + componentsBuilder = ComponentsBuilder( + config = pluginConfig, + securitySchemesBuilder = SecuritySchemesBuilder( + oAuthFlowsBuilder = OAuthFlowsBuilder() + ) + ) + ).build(routes) + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt new file mode 100644 index 0000000..596b8a2 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/SecuritySchemesBuilderTest.kt @@ -0,0 +1,199 @@ +package io.github.smiley4.ktorswaggerui.tests.openapi + +import io.github.smiley4.ktorswaggerui.dsl.AuthKeyLocation +import io.github.smiley4.ktorswaggerui.dsl.AuthScheme +import io.github.smiley4.ktorswaggerui.dsl.AuthType +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme +import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.maps.shouldBeEmpty +import io.kotest.matchers.nulls.shouldNotBeNull +import io.kotest.matchers.shouldBe +import io.swagger.v3.oas.models.security.SecurityScheme + +class SecuritySchemesBuilderTest : StringSpec({ + + "test empty" { + buildSecuritySchemeObjects(mapOf()).also { schemes -> + schemes.shouldBeEmpty() + } + } + + "test default security scheme object" { + buildSecuritySchemeObjects(mapOf("TestAuth" to {})).also { schemes -> + schemes.keys shouldContainExactlyInAnyOrder listOf("TestAuth") + schemes["TestAuth"]!!.also { scheme -> + scheme.type shouldBe null + scheme.description shouldBe null + scheme.name shouldBe "TestAuth" + scheme.`$ref` shouldBe null + scheme.`in` shouldBe null + scheme.scheme shouldBe null + scheme.bearerFormat shouldBe null + scheme.flows shouldBe null + scheme.openIdConnectUrl shouldBe null + scheme.extensions shouldBe null + } + } + } + + "test basic security scheme objects" { + buildSecuritySchemeObjects(mapOf( + "TestAuth1" to { + type = AuthType.HTTP + scheme = AuthScheme.BASIC + }, + "TestAuth2" to { + type = AuthType.HTTP + scheme = AuthScheme.BASIC + } + )).also { schemes -> + schemes.keys shouldContainExactlyInAnyOrder listOf("TestAuth1", "TestAuth2") + schemes["TestAuth1"]!!.also { scheme -> + scheme.name shouldBe "TestAuth1" + scheme.type shouldBe SecurityScheme.Type.HTTP + scheme.scheme shouldBe "Basic" + } + schemes["TestAuth2"]!!.also { scheme -> + scheme.name shouldBe "TestAuth2" + scheme.type shouldBe SecurityScheme.Type.HTTP + scheme.scheme shouldBe "Basic" + } + } + } + + "test complete security scheme object" { + buildSecuritySchemeObjects(mapOf("TestAuth" to { + type = AuthType.HTTP + location = AuthKeyLocation.COOKIE + scheme = AuthScheme.BASIC + bearerFormat = "test" + openIdConnectUrl = "Test IOD-Connect URL" + description = "Test Description" + flows { + implicit { + authorizationUrl = "Implicit Auth Url" + tokenUrl = "Implicity Token Url" + refreshUrl = "Implicity Token Url" + scopes = mapOf( + "implicit1" to "scope1", + "implicit2" to "scope2" + ) + } + password { + authorizationUrl = "Password Auth Url" + tokenUrl = "Password Token Url" + refreshUrl = "Password Token Url" + scopes = mapOf( + "password1" to "scope1", + "password2" to "scope2" + ) + } + clientCredentials { + authorizationUrl = "ClientCredentials Auth Url" + tokenUrl = "ClientCredentials Token Url" + refreshUrl = "ClientCredentials Token Url" + scopes = mapOf( + "clientCredentials1" to "scope1", + "clientCredentials2" to "scope2" + ) + } + authorizationCode { + authorizationUrl = "AuthorizationCode Auth Url" + tokenUrl = "AuthorizationCode Token Url" + refreshUrl = "AuthorizationCode Token Url" + scopes = mapOf( + "authorizationCode1" to "scope1", + "authorizationCode2" to "scope2" + ) + } + } + })).also { schemes -> + schemes.keys shouldContainExactlyInAnyOrder listOf("TestAuth") + schemes["TestAuth"]!!.also { scheme -> + scheme.name shouldBe "TestAuth" + scheme.type shouldBe SecurityScheme.Type.HTTP + scheme.`in` shouldBe SecurityScheme.In.COOKIE + scheme.scheme shouldBe "Basic" + scheme.bearerFormat shouldBe "test" + scheme.openIdConnectUrl shouldBe "Test IOD-Connect URL" + scheme.description shouldBe "Test Description" + scheme.flows + .also { it.shouldNotBeNull() } + ?.also { flows -> + flows.implicit + .also { it.shouldNotBeNull() } + ?.also { implicit -> + implicit.authorizationUrl shouldBe "Implicit Auth Url" + implicit.tokenUrl shouldBe "Implicity Token Url" + implicit.refreshUrl shouldBe "Implicity Token Url" + implicit.scopes + .also { it.shouldNotBeNull() } + ?.also { scopes -> + scopes.keys shouldContainExactlyInAnyOrder listOf("implicit1", "implicit2") + scopes["implicit1"] shouldBe "scope1" + scopes["implicit2"] shouldBe "scope2" + } + } + flows.password + .also { it.shouldNotBeNull() } + ?.also { password -> + password.authorizationUrl shouldBe "Password Auth Url" + password.tokenUrl shouldBe "Password Token Url" + password.refreshUrl shouldBe "Password Token Url" + password.scopes + .also { it.shouldNotBeNull() } + ?.also { scopes -> + scopes.keys shouldContainExactlyInAnyOrder listOf("password1", "password2") + scopes["password1"] shouldBe "scope1" + scopes["password2"] shouldBe "scope2" + } + } + flows.clientCredentials + .also { it.shouldNotBeNull() } + ?.also { clientCredentials -> + clientCredentials.authorizationUrl shouldBe "ClientCredentials Auth Url" + clientCredentials.tokenUrl shouldBe "ClientCredentials Token Url" + clientCredentials.refreshUrl shouldBe "ClientCredentials Token Url" + clientCredentials.scopes + .also { it.shouldNotBeNull() } + ?.also { scopes -> + scopes.keys shouldContainExactlyInAnyOrder listOf("clientCredentials1", "clientCredentials2") + scopes["clientCredentials1"] shouldBe "scope1" + scopes["clientCredentials2"] shouldBe "scope2" + } + } + flows.authorizationCode + .also { it.shouldNotBeNull() } + ?.also { authorizationCode -> + authorizationCode.authorizationUrl shouldBe "AuthorizationCode Auth Url" + authorizationCode.tokenUrl shouldBe "AuthorizationCode Token Url" + authorizationCode.refreshUrl shouldBe "AuthorizationCode Token Url" + authorizationCode.scopes + .also { it.shouldNotBeNull() } + ?.also { scopes -> + scopes.keys shouldContainExactlyInAnyOrder listOf("authorizationCode1", "authorizationCode2") + scopes["authorizationCode1"] shouldBe "scope1" + scopes["authorizationCode2"] shouldBe "scope2" + } + } + } + } + } + } + +}) { + + companion object { + + private fun buildSecuritySchemeObjects(builders: Map Unit>): Map { + return SecuritySchemesBuilder( + oAuthFlowsBuilder = OAuthFlowsBuilder() + ).build(builders.map { (name, entry) -> OpenApiSecurityScheme(name).apply(entry) }) + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt new file mode 100644 index 0000000..6c86f6f --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/route/RouteDocumentationMergerTest.kt @@ -0,0 +1,131 @@ +package io.github.smiley4.ktorswaggerui.tests.route + +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldBeEmpty +import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe + +class RouteDocumentationMergerTest : StringSpec({ + + "merge empty routes" { + merge( + route {}, + route {} + ).also { route -> + route.tags.shouldBeEmpty() + route.summary shouldBe null + route.description shouldBe null + route.operationId shouldBe null + route.deprecated shouldBe false + route.hidden shouldBe false + route.securitySchemeName shouldBe null + route.securitySchemeNames.shouldBeEmpty() + route.getRequest().also { requests -> + requests.getParameters().shouldBeEmpty() + requests.getBody() shouldBe null + } + route.getResponses().also { responses -> + responses.getResponses().shouldBeEmpty() + } + } + } + + "merge complete routes" { + merge( + route { + tags = listOf("a1", "a2") + summary = "Summary A" + description = "Description A" + operationId = "operationA" + securitySchemeName = "securitySchemeNameA" + securitySchemeNames = listOf("securitySchemeNameA1", "securitySchemeNameA2") + deprecated = true + hidden = false + request { + queryParameter("query") + pathParameter("pathA1") + pathParameter("pathA2") + body { + description = "body A" + } + } + response { + "a1" to { description = "response a1" } + "a2" to { description = "response a1" } + } + }, + route { + tags = listOf("b1", "b2") + summary = "Summary B" + description = "Description B" + operationId = "operationB" + securitySchemeName = "securitySchemeNameB" + securitySchemeNames = listOf("securitySchemeNameB1", "securitySchemeNameB2") + deprecated = false + hidden = true + request { + queryParameter("query") + pathParameter("pathB1") + pathParameter("pathB2") + body { + description = "body B" + } + } + response { + "b1" to { description = "response b1" } + "b2" to { description = "response b1" } + } + } + ).also { route -> + route.tags shouldContainExactlyInAnyOrder listOf("a1", "a2", "b1", "b2") + route.summary shouldBe "Summary A" + route.description shouldBe "Description A" + route.operationId shouldBe "operationA" + route.deprecated shouldBe true + route.hidden shouldBe true + route.securitySchemeName shouldBe "securitySchemeNameA" + route.securitySchemeNames shouldContainExactlyInAnyOrder listOf( + "securitySchemeNameA1", + "securitySchemeNameA2", + "securitySchemeNameB1", + "securitySchemeNameB2" + ) + route.getRequest().also { requests -> + requests.getParameters().map { it.name } shouldContainExactlyInAnyOrder listOf( + "query", + "pathA1", + "pathA2", + "query", + "pathB1", + "pathB2" + ) + requests.getBody() + .also { it shouldNotBe null } + ?.also { it.description shouldBe "body A" } + } + route.getResponses().also { responses -> + responses.getResponses().map { it.statusCode } shouldContainExactlyInAnyOrder listOf( + "b1", "b2", "a1", "a2" + ) + } + } + } + +}) { + + companion object { + + fun route(builder: OpenApiRoute.() -> Unit): OpenApiRoute { + return OpenApiRoute().apply(builder) + } + + fun merge(a: OpenApiRoute, b: OpenApiRoute): OpenApiRoute { + return RouteDocumentationMerger().merge(a, b) + } + + } + +} \ No newline at end of file From 71153fb3fc2ff4385478709ad6d764592010210f Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 17:44:50 +0200 Subject: [PATCH 14/27] bump version to 2.0, rause ktor to 2.3.0 --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 1472e9b..1e229b0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -7,7 +7,7 @@ plugins { } group = "io.github.smiley4" -version = "1.7.0" +version = "2.0.0" repositories { mavenCentral() @@ -15,7 +15,7 @@ repositories { dependencies { - val ktorVersion = "2.2.4" + val ktorVersion = "2.3.0" implementation("io.ktor:ktor-server-core-jvm:$ktorVersion") implementation("io.ktor:ktor-server-webjars:$ktorVersion") implementation("io.ktor:ktor-server-auth:$ktorVersion") From 4fbd2d09382c861739dd973c291ab332845c02e2 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 17:59:28 +0200 Subject: [PATCH 15/27] wip: sealed class issue --- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 6 -- .../ktorswaggerui/examples/SealedClassTest.kt | 75 +++++++++++++++++++ 2 files changed, 75 insertions(+), 6 deletions(-) create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index abe1a09..1e9582a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -72,12 +72,6 @@ class SwaggerUIPluginConfig { private var swaggerUI = SwaggerUI() - /** - * Whether to use canonical instead of simple name for component object references - */ - var canonicalNameObjectRefs: Boolean = false - - /** * Swagger-UI configuration */ diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt new file mode 100644 index 0000000..af6f6c4 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt @@ -0,0 +1,75 @@ +package io.github.smiley4.ktorswaggerui.examples + +import com.fasterxml.jackson.annotation.JsonSubTypes +import com.fasterxml.jackson.annotation.JsonTypeInfo +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.get +import io.github.smiley4.ktorswaggerui.tests.schema.SchemaContextTest +import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.routing.routing + +/** + * A minimal working example + */ +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + install(SwaggerUI) + + + routing { + get("test/sealed", { + response { + HttpStatusCode.OK to { + body() + } + } + }) { + //... + } + get("test/a", { + response { + HttpStatusCode.OK to { + body() + } + } + }) { + //... + } + get("test/b", { + response { + HttpStatusCode.OK to { + body() + } + } + }) { + //... + } + } +} + + +@JsonTypeInfo( + use = JsonTypeInfo.Id.CLASS, + include = JsonTypeInfo.As.PROPERTY, + property = "_type", +) +@JsonSubTypes( + JsonSubTypes.Type(value = ExampleResponse.A::class), + JsonSubTypes.Type(value = ExampleResponse.B::class), +) +sealed class ExampleResponse { + data class A( + val thisIsA: Boolean + ) : ExampleResponse() + + data class B( + val thisIsB: Boolean + ) : ExampleResponse() +} \ No newline at end of file From dc9eeebe84985648000821acaec0c52127e10120 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 19 May 2023 18:05:16 +0200 Subject: [PATCH 16/27] wip --- .../ktorswaggerui/examples/SealedClassTest.kt | 75 ------------------- 1 file changed, 75 deletions(-) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt deleted file mode 100644 index af6f6c4..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/SealedClassTest.kt +++ /dev/null @@ -1,75 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import com.fasterxml.jackson.annotation.JsonSubTypes -import com.fasterxml.jackson.annotation.JsonTypeInfo -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.github.smiley4.ktorswaggerui.tests.schema.SchemaContextTest -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.routing.routing - -/** - * A minimal working example - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - install(SwaggerUI) - - - routing { - get("test/sealed", { - response { - HttpStatusCode.OK to { - body() - } - } - }) { - //... - } - get("test/a", { - response { - HttpStatusCode.OK to { - body() - } - } - }) { - //... - } - get("test/b", { - response { - HttpStatusCode.OK to { - body() - } - } - }) { - //... - } - } -} - - -@JsonTypeInfo( - use = JsonTypeInfo.Id.CLASS, - include = JsonTypeInfo.As.PROPERTY, - property = "_type", -) -@JsonSubTypes( - JsonSubTypes.Type(value = ExampleResponse.A::class), - JsonSubTypes.Type(value = ExampleResponse.B::class), -) -sealed class ExampleResponse { - data class A( - val thisIsA: Boolean - ) : ExampleResponse() - - data class B( - val thisIsB: Boolean - ) : ExampleResponse() -} \ No newline at end of file From 7c023bb724ccc57ec61670990aa78e16e17ca415 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Sun, 21 May 2023 22:35:02 +0200 Subject: [PATCH 17/27] minor cleanup --- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 69 +++++-------------- .../ktorswaggerui/dsl/OpenApiMultipartBody.kt | 16 ++--- ...ltipartPart.kt => OpenApiMultipartPart.kt} | 2 +- .../spec/openapi/ContentBuilder.kt | 4 +- .../spec/schema/JsonSchemaConfig.kt | 44 ++++++++++++ 5 files changed, 74 insertions(+), 61 deletions(-) rename src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/{OpenapiMultipartPart.kt => OpenApiMultipartPart.kt} (98%) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index 1e9582a..45f1282 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -1,17 +1,7 @@ package io.github.smiley4.ktorswaggerui -import com.fasterxml.jackson.databind.node.ObjectNode -import com.github.victools.jsonschema.generator.FieldScope -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerationContext import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.generator.TypeScope -import com.github.victools.jsonschema.module.jackson.JacksonModule -import com.github.victools.jsonschema.module.swagger2.Swagger2Module import io.github.smiley4.ktorswaggerui.dsl.CustomSchemas -import io.github.smiley4.ktorswaggerui.dsl.Example import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse @@ -19,10 +9,10 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag import io.github.smiley4.ktorswaggerui.dsl.SwaggerUI +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaConfig import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode import io.ktor.server.routing.RouteSelector -import io.swagger.v3.oas.annotations.media.Schema import kotlin.reflect.KClass /** @@ -31,9 +21,6 @@ import kotlin.reflect.KClass @OpenApiDslMarker class SwaggerUIPluginConfig { - private var defaultUnauthorizedResponse: OpenApiResponse? = null - - /** * Default response to automatically add to each protected route for the "Unauthorized"-Response-Code. * Generated response can be overwritten with custom response. @@ -42,6 +29,8 @@ class SwaggerUIPluginConfig { defaultUnauthorizedResponse = OpenApiResponse(HttpStatusCode.Unauthorized.value.toString()).apply(block) } + private var defaultUnauthorizedResponse: OpenApiResponse? = null + fun getDefaultUnauthorizedResponse() = defaultUnauthorizedResponse @@ -69,8 +58,6 @@ class SwaggerUIPluginConfig { */ var pathFilter: ((method: HttpMethod, url: List) -> Boolean)? = null - private var swaggerUI = SwaggerUI() - /** * Swagger-UI configuration @@ -79,9 +66,9 @@ class SwaggerUIPluginConfig { swaggerUI = SwaggerUI().apply(block) } - fun getSwaggerUI() = swaggerUI + private var swaggerUI = SwaggerUI() - private var info = OpenApiInfo() + fun getSwaggerUI() = swaggerUI /** @@ -91,9 +78,9 @@ class SwaggerUIPluginConfig { info = OpenApiInfo().apply(block) } - fun getInfo() = info + private var info = OpenApiInfo() - private val servers = mutableListOf() + fun getInfo() = info /** @@ -103,9 +90,9 @@ class SwaggerUIPluginConfig { servers.add(OpenApiServer().apply(block)) } - fun getServers(): List = servers + private val servers = mutableListOf() - private val securitySchemes = mutableListOf() + fun getServers(): List = servers /** @@ -115,9 +102,9 @@ class SwaggerUIPluginConfig { securitySchemes.add(OpenApiSecurityScheme(name).apply(block)) } - fun getSecuritySchemes(): List = securitySchemes + private val securitySchemes = mutableListOf() - private val tags = mutableListOf() + fun getSecuritySchemes(): List = securitySchemes /** @@ -127,45 +114,27 @@ class SwaggerUIPluginConfig { tags.add(OpenApiTag(name).apply(block)) } + private val tags = mutableListOf() + fun getTags(): List = tags - private var customSchemas = CustomSchemas() + /** + * Custom schemas to reference via [io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef] + */ fun schemas(block: CustomSchemas.() -> Unit) { this.customSchemas = CustomSchemas().apply(block) } + private var customSchemas = CustomSchemas() + fun getCustomSchemas() = customSchemas /** * Customize or replace the configuration-builder for the json-schema-generator (see https://victools.github.io/jsonschema-generator/#generator-options for more information) */ - var schemaGeneratorConfigBuilder: SchemaGeneratorConfigBuilder = - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.DEFINITION_FOR_MAIN_SCHEMA) - .without(Option.INLINE_ALL_SCHEMAS) - .also { - it.forTypesInGeneral() - .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> - if (typeScope is FieldScope) { - typeScope.getAnnotation(Schema::class.java)?.also { annotation -> - if (annotation.example != "") { - objectNode.put("example", annotation.example) - } - } - typeScope.getAnnotation(Example::class.java)?.also { annotation -> - objectNode.put("example", annotation.value) - } - } - } - } + var schemaGeneratorConfigBuilder: SchemaGeneratorConfigBuilder = JsonSchemaConfig.schemaGeneratorConfigBuilder /** diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt index beb0d88..270db77 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt @@ -10,14 +10,14 @@ import java.lang.reflect.Type @OpenApiDslMarker class OpenApiMultipartBody : OpenApiBaseBody() { - private val parts = mutableListOf() + private val parts = mutableListOf() /** * One part of a multipart-body */ - fun part(name: String, type: Type, block: OpenapiMultipartPart.() -> Unit) { - parts.add(OpenapiMultipartPart(name, type).apply(block)) + fun part(name: String, type: Type, block: OpenApiMultipartPart.() -> Unit) { + parts.add(OpenApiMultipartPart(name, type).apply(block)) } @@ -36,15 +36,15 @@ class OpenApiMultipartBody : OpenApiBaseBody() { /** * One part of a multipart-body */ - inline fun part(name: String, noinline block: OpenapiMultipartPart.() -> Unit) = + inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit) = part(name, object : TypeReference() {}.type, block) /** * One part of a multipart-body */ - fun part(name: String, customSchema: CustomSchemaRef, block: OpenapiMultipartPart.() -> Unit) { - parts.add(OpenapiMultipartPart(name, null).apply(block).apply { + fun part(name: String, customSchema: CustomSchemaRef, block: OpenApiMultipartPart.() -> Unit) { + parts.add(OpenApiMultipartPart(name, null).apply(block).apply { this.customSchema = customSchema }) } @@ -59,7 +59,7 @@ class OpenApiMultipartBody : OpenApiBaseBody() { /** * One part of a multipart-body */ - fun part(name: String, customSchemaId: String, block: OpenapiMultipartPart.() -> Unit) = part(name, obj(customSchemaId), block) + fun part(name: String, customSchemaId: String, block: OpenApiMultipartPart.() -> Unit) = part(name, obj(customSchemaId), block) /** @@ -67,6 +67,6 @@ class OpenApiMultipartBody : OpenApiBaseBody() { */ fun part(name: String, customSchemaId: String) = part(name, customSchemaId) {} - fun getParts(): List = parts + fun getParts(): List = parts } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenapiMultipartPart.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt similarity index 98% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenapiMultipartPart.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt index 211df4b..6a7623c 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenapiMultipartPart.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt @@ -10,7 +10,7 @@ import kotlin.reflect.KClass * See https://swagger.io/docs/specification/describing-request-body/multipart-requests/ for more info */ @OpenApiDslMarker -class OpenapiMultipartPart( +class OpenApiMultipartPart( /** * The name of this part */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt index e718fa9..0ce3915 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt @@ -4,7 +4,7 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.OpenapiMultipartPart +import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartPart import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.ktor.http.ContentType import io.swagger.v3.oas.models.media.Content @@ -116,7 +116,7 @@ class ContentBuilder( } } - private fun getSchema(part: OpenapiMultipartPart): Schema<*>? { + private fun getSchema(part: OpenApiMultipartPart): Schema<*>? { return if (part.customSchema != null) { schemaContext.getSchema(part.customSchema!!) } else if (part.type != null) { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt new file mode 100644 index 0000000..448d3cc --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt @@ -0,0 +1,44 @@ +package io.github.smiley4.ktorswaggerui.spec.schema + +import com.fasterxml.jackson.databind.node.ObjectNode +import com.github.victools.jsonschema.generator.FieldScope +import com.github.victools.jsonschema.generator.Option +import com.github.victools.jsonschema.generator.OptionPreset +import com.github.victools.jsonschema.generator.SchemaGenerationContext +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder +import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.generator.TypeScope +import com.github.victools.jsonschema.module.jackson.JacksonModule +import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.github.smiley4.ktorswaggerui.dsl.Example +import io.swagger.v3.oas.annotations.media.Schema + +object JsonSchemaConfig { + + var schemaGeneratorConfigBuilder: SchemaGeneratorConfigBuilder = + SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.DEFINITION_FOR_MAIN_SCHEMA) + .without(Option.INLINE_ALL_SCHEMAS) + .also { + it.forTypesInGeneral() + .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> + if (typeScope is FieldScope) { + typeScope.getAnnotation(Schema::class.java)?.also { annotation -> + if (annotation.example != "") { + objectNode.put("example", annotation.example) + } + } + typeScope.getAnnotation(Example::class.java)?.also { annotation -> + objectNode.put("example", annotation.value) + } + } + } + } + +} \ No newline at end of file From 0d54ab9d092f1928319d493371d480907a110e82 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Sun, 21 May 2023 23:18:15 +0200 Subject: [PATCH 18/27] wip --- .../ktorswaggerui/dsl/CustomSchemas.kt | 5 +- .../examples/CompletePluginConfigExample.kt | 91 +++++++++++++++++++ .../smiley4/ktorswaggerui/examples/Test.kt | 55 +++++++++++ 3 files changed, 148 insertions(+), 3 deletions(-) create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt index d22deb9..5340faf 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt @@ -6,9 +6,6 @@ import java.lang.reflect.Type @OpenApiDslMarker class CustomSchemas { - private var jsonSchemaBuilder: ((type: Type) -> String?)? = null - - /** * Custom builder for building json-schemas from a given type. Return null to not use this builder for the given type. */ @@ -16,6 +13,8 @@ class CustomSchemas { jsonSchemaBuilder = builder } + private var jsonSchemaBuilder: ((type: Type) -> String?)? = null + fun getJsonSchemaBuilder() = jsonSchemaBuilder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt new file mode 100644 index 0000000..be90d1f --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt @@ -0,0 +1,91 @@ +package io.github.smiley4.ktorswaggerui.examples + +import com.github.victools.jsonschema.generator.SchemaGenerator +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.AuthScheme +import io.github.smiley4.ktorswaggerui.dsl.AuthType +import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSort +import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSyntaxHighlight +import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaConfig +import io.ktor.server.application.Application +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.swagger.v3.oas.models.media.Schema + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + install(SwaggerUI) { + securityScheme("ApiAuth") { + type = AuthType.HTTP + scheme = AuthScheme.BASIC + } + securityScheme("SwaggerAuth") { + type = AuthType.HTTP + scheme = AuthScheme.BASIC + } + defaultSecuritySchemeName = "ApiAuth" + defaultUnauthorizedResponse { + description = "invalid username or password" + } + swagger { + forwardRoot = false + swaggerUrl = "/api/swagger-ui" + authentication = "SwaggerAuth" + disableSpecValidator() + displayOperationId = true + showTagFilterInput = true + sort = SwaggerUiSort.ALPHANUMERICALLY + syntaxHighlight = SwaggerUiSyntaxHighlight.AGATE + } + pathFilter = { _, url -> url.firstOrNull() != "test" } + info { + title = "Example API" + version = "latest" + description = "This is an example api" + termsOfService = "example.com" + contact { + name = "Mr. Example" + url = "example.com/contact" + email = "example@mail.com" + } + license { + name = "Mr. Example" + url = "example.com/license" + } + } + server { + url = "localhost:8080" + description = "develop server" + } + server { + url = "127.0.0.1:8080" + description = "production server" + } + tag("greet") { + description = "routes for greeting" + externalDocDescription = "documentation for greetings" + externalDocUrl = "example.com/doc" + } + automaticTagGenerator = { url -> url.firstOrNull() } + schemas { + jsonSchemaBuilder { type -> + SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type).toPrettyString() + } + json("customSchema1") { + """{"type": "string"}""" + } + openApi("customSchema2") { + Schema().also { + it.type = "string" + } + } + remote("customSchema3", "example.com/schema") + } + schemaGeneratorConfigBuilder = schemaGeneratorConfigBuilder.let { /*...*/ it } + } +} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt new file mode 100644 index 0000000..8603911 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt @@ -0,0 +1,55 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.get +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.routing + +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + install(SwaggerUI) + + routing { + get("object", { + request { + body { + example( + "example", + TestData( + someString = "Hello", + someNumber = 42 + ) + ) + } + } + }) { + call.respondText("Hello World!") + } + get("jsonString", { + request { + body { + example( + "example", + """{"someString": "World", "someNumber": 420, "somethingElse": true}""" + ) + } + } + }) { + call.respondText("Hello World!") + } + } +} + + +private class TestData( + val someString: String, + val someNumber: Int +) \ No newline at end of file From 418c3f5d43d8fee7818de45eea5f6d0dc43fefbd Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Mon, 22 May 2023 18:17:50 +0200 Subject: [PATCH 19/27] refine tag-generator dsl, deprecated old --- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 20 +++++++ .../spec/openapi/OperationTagsBuilder.kt | 16 ++++-- .../examples/CompletePluginConfigExample.kt | 2 +- .../smiley4/ktorswaggerui/examples/Test.kt | 55 ------------------- 4 files changed, 33 insertions(+), 60 deletions(-) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index 45f1282..f3bcb55 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -49,9 +49,23 @@ class SwaggerUIPluginConfig { /** * function to generate a tag from the given url for a path. Result will be added to the tags defined for each path */ + @Deprecated("use 'generateTags' instead") var automaticTagGenerator: ((url: List) -> String?)? = null + /** + * Automatically add tags to the route with the given url. + * The returned (non-null) tags will be added to the tags specified in the route-specific documentation. + */ + fun generateTags(generator: TagGenerator) { + tagGenerator = generator + } + + private var tagGenerator: TagGenerator = { emptyList() } + + fun getTagGenerator() = tagGenerator + + /** * Filter to apply to all routes. Return 'false' for routes to not include them in the OpenApi-Spec and Swagger-UI. * The url of the paths are already split at '/'. @@ -143,3 +157,9 @@ class SwaggerUIPluginConfig { var ignoredRouteSelectors: List> = listOf() } + +/** + * url - the parts of the route-url split at all `/`. + * return a collection of tags. "Null"-entries will be ignored. + */ +typealias TagGenerator = (url: List) -> Collection diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt index 6aa50cf..95de940 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OperationTagsBuilder.kt @@ -8,10 +8,18 @@ class OperationTagsBuilder( ) { fun build(route: RouteMeta): List { - val generatedTags = config.automaticTagGenerator?.let { - it(route.path.split("/").filter { it.isNotEmpty() }) - } - return (route.documentation.tags + generatedTags).filterNotNull() + return mutableSetOf().also { tags -> + tags.add(getGeneratedTagsDeprecated(route)) + tags.addAll(getGeneratedTags(route)) + tags.addAll(getRouteTags(route)) + }.filterNotNull() } + private fun getRouteTags(route: RouteMeta) = route.documentation.tags + + @Deprecated("see PluginConfig#automaticTagGenerator") + private fun getGeneratedTagsDeprecated(route: RouteMeta) = config.automaticTagGenerator?.let { it(route.path.split("/")) } + + private fun getGeneratedTags(route: RouteMeta) = config.getTagGenerator()(route.path.split("/")) + } \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt index be90d1f..48ca1d5 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt @@ -71,7 +71,7 @@ private fun Application.myModule() { externalDocDescription = "documentation for greetings" externalDocUrl = "example.com/doc" } - automaticTagGenerator = { url -> url.firstOrNull() } + generateTags { url -> listOf(url.firstOrNull()) } schemas { jsonSchemaBuilder { type -> SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type).toPrettyString() diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt deleted file mode 100644 index 8603911..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Test.kt +++ /dev/null @@ -1,55 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.response.respondText -import io.ktor.server.routing.routing - -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - -private fun Application.myModule() { - install(SwaggerUI) - - routing { - get("object", { - request { - body { - example( - "example", - TestData( - someString = "Hello", - someNumber = 42 - ) - ) - } - } - }) { - call.respondText("Hello World!") - } - get("jsonString", { - request { - body { - example( - "example", - """{"someString": "World", "someNumber": 420, "somethingElse": true}""" - ) - } - } - }) { - call.respondText("Hello World!") - } - } -} - - -private class TestData( - val someString: String, - val someNumber: Int -) \ No newline at end of file From 6eb106e776f3baf5f64c0f15267f5945325319a0 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Tue, 23 May 2023 00:30:32 +0200 Subject: [PATCH 20/27] wip --- build.gradle.kts | 6 ++ .../ktorswaggerui/SwaggerController.kt | 4 +- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 10 ++- .../smiley4/ktorswaggerui/SwaggerRouting.kt | 4 +- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 26 +++++- .../ktorswaggerui/dsl/CustomSchemas.kt | 15 ++-- .../ktorswaggerui/dsl/OpenApiBaseBody.kt | 1 - .../ktorswaggerui/dsl/OpenApiHeader.kt | 3 +- .../ktorswaggerui/dsl/OpenApiMultipartBody.kt | 17 ++-- .../ktorswaggerui/dsl/OpenApiMultipartPart.kt | 25 ++++-- .../ktorswaggerui/dsl/OpenApiRequest.kt | 73 +++++++-------- .../dsl/OpenApiRequestParameter.kt | 3 +- .../ktorswaggerui/dsl/OpenApiResponse.kt | 49 +++++----- .../ktorswaggerui/dsl/OpenApiSimpleBody.kt | 4 +- .../smiley4/ktorswaggerui/dsl/SchemaType.kt | 29 ++++++ .../ktorswaggerui/dsl/SerializationConfig.kt | 40 +++++++++ .../dsl/{SwaggerUI.kt => SwaggerUIDsl.kt} | 2 +- .../spec/openapi/ContentBuilder.kt | 7 +- .../spec/openapi/ExampleBuilder.kt | 14 ++- .../spec/schema/JsonSchemaBuilder.kt | 49 +++++++--- .../spec/schema/SchemaContext.kt | 19 ++-- .../examples/CompletePluginConfigExample.kt | 13 ++- .../CustomJsonSchemaBuilderExample.kt | 5 +- .../examples/KotlinxSerializationExample.kt | 90 +++++++++++++++++++ .../tests/openapi/OpenApiBuilderTest.kt | 10 ++- .../tests/openapi/OperationBuilderTest.kt | 10 ++- .../tests/openapi/PathsBuilderTest.kt | 10 ++- .../tests/schema/SchemaContextTest.kt | 29 +++--- 28 files changed, 398 insertions(+), 169 deletions(-) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt rename src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/{SwaggerUI.kt => SwaggerUIDsl.kt} (99%) create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt diff --git a/build.gradle.kts b/build.gradle.kts index 1e229b0..4d6b1b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ plugins { kotlin("jvm") version "1.7.21" `maven-publish` id("org.owasp.dependencycheck") version "8.2.1" + kotlin("plugin.serialization") version "1.8.21" // TODO: remove!!!! } group = "io.github.smiley4" @@ -11,6 +12,8 @@ version = "2.0.0" repositories { mavenCentral() + jcenter() // TODO: remove!!!! + maven(url = "https://raw.githubusercontent.com/glureau/json-schema-serialization/mvn-repo") } dependencies { @@ -53,6 +56,9 @@ dependencies { val versionKotlinTest = "1.7.21" testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")// TODO: remove!!!! + testImplementation("com.github.Ricky12Awesome:json-schema-serialization:0.9.9")// TODO: remove!!!! + testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // TODO: remove!!! } tasks.test { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt index 761053f..d5f4fcd 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerController.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.SwaggerUIDsl import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSort import io.ktor.http.ContentType import io.ktor.http.HttpStatusCode @@ -15,7 +15,7 @@ class SwaggerController( private val swaggerWebjarVersion: String, private val apiSpecUrl: String, private val jsonSpecProvider: () -> String, - private val swaggerUiConfig: SwaggerUI + private val swaggerUiConfig: SwaggerUIDsl ) { suspend fun serveOpenApiSpec(call: ApplicationCall) { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index 46d17bf..ef9afee 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -71,7 +71,7 @@ private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { return SchemaContext( config = pluginConfig, - jsonSchemaBuilder = JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build()) + jsonSchemaBuilder = JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build()) ).initialize(routes.toList()) } @@ -95,7 +95,9 @@ private fun builder(config: SwaggerUIPluginConfig, schemaContext: SchemaContext) requestBodyBuilder = RequestBodyBuilder( contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = config + ), headerBuilder = HeaderBuilder(schemaContext) ) ), @@ -104,7 +106,9 @@ private fun builder(config: SwaggerUIPluginConfig, schemaContext: SchemaContext) headerBuilder = HeaderBuilder(schemaContext), contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = config + ), headerBuilder = HeaderBuilder(schemaContext) ) ), diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt index 0172ca6..b5e2575 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerRouting.kt @@ -1,6 +1,6 @@ package io.github.smiley4.ktorswaggerui -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.SwaggerUIDsl import io.ktor.server.application.Application import io.ktor.server.application.call import io.ktor.server.auth.authenticate @@ -16,7 +16,7 @@ import mu.KotlinLogging * Registers and handles routes required for the swagger-ui */ class SwaggerRouting( - private val swaggerUiConfig: SwaggerUI, + private val swaggerUiConfig: SwaggerUIDsl, appConfig: ApplicationConfig, swaggerWebjarVersion: String, jsonSpecProvider: () -> String diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index f3bcb55..6bbdd70 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -8,7 +8,8 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.SerializationConfig +import io.github.smiley4.ktorswaggerui.dsl.SwaggerUIDsl import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaConfig import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode @@ -76,11 +77,11 @@ class SwaggerUIPluginConfig { /** * Swagger-UI configuration */ - fun swagger(block: SwaggerUI.() -> Unit) { - swaggerUI = SwaggerUI().apply(block) + fun swagger(block: SwaggerUIDsl.() -> Unit) { + swaggerUI = SwaggerUIDsl().apply(block) } - private var swaggerUI = SwaggerUI() + private var swaggerUI = SwaggerUIDsl() fun getSwaggerUI() = swaggerUI @@ -145,9 +146,26 @@ class SwaggerUIPluginConfig { fun getCustomSchemas() = customSchemas + /** + * customize the behaviour of different serializers (examples, schemas, ...) + */ + fun serialization(block: SerializationConfig.() -> Unit) { + block(serializationConfig) + } + + val serializationConfig: SerializationConfig = SerializationConfig() + + + /** + * whether to inline all schemas or move keep them in the components section. + */ + var inlineAllSchemas: Boolean = false // TODO + + /** * Customize or replace the configuration-builder for the json-schema-generator (see https://victools.github.io/jsonschema-generator/#generator-options for more information) */ + @Deprecated("") var schemaGeneratorConfigBuilder: SchemaGeneratorConfigBuilder = JsonSchemaConfig.schemaGeneratorConfigBuilder diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt index 5340faf..25b4705 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt @@ -1,7 +1,6 @@ package io.github.smiley4.ktorswaggerui.dsl import io.swagger.v3.oas.models.media.Schema -import java.lang.reflect.Type @OpenApiDslMarker class CustomSchemas { @@ -9,17 +8,24 @@ class CustomSchemas { /** * Custom builder for building json-schemas from a given type. Return null to not use this builder for the given type. */ - fun jsonSchemaBuilder(builder: (type: Type) -> String?) { + @Deprecated("") + fun jsonSchemaBuilder(builder: (type: SchemaType) -> String?) { jsonSchemaBuilder = builder } - private var jsonSchemaBuilder: ((type: Type) -> String?)? = null + @Deprecated("") + private var jsonSchemaBuilder: ((type: SchemaType) -> String?)? = null + + + @Deprecated("") fun getJsonSchemaBuilder() = jsonSchemaBuilder private val customSchemas = mutableMapOf() + fun getSchema(id: String): BaseCustomSchema? = customSchemas[id] + /** * Define the json-schema for an object/body with the given id @@ -44,9 +50,6 @@ class CustomSchemas { customSchemas[id] = RemoteSchema(url) } - fun getSchema(id: String): BaseCustomSchema? = customSchemas[id] - - } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt index 7ab2466..f751c49 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiBaseBody.kt @@ -1,7 +1,6 @@ package io.github.smiley4.ktorswaggerui.dsl import io.ktor.http.ContentType -import java.lang.reflect.Type /** * Describes a single request/response body with a single schema. diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt index d0a6cc7..2a7472b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiHeader.kt @@ -1,6 +1,5 @@ package io.github.smiley4.ktorswaggerui.dsl -import java.lang.reflect.Type @OpenApiDslMarker @@ -15,7 +14,7 @@ class OpenApiHeader { /** * The schema of the header */ - var type: Type? = null + var type: SchemaType? = null /** diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt index 270db77..5a0b087 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartBody.kt @@ -1,7 +1,7 @@ package io.github.smiley4.ktorswaggerui.dsl -import com.fasterxml.jackson.core.type.TypeReference -import java.lang.reflect.Type +import kotlin.reflect.KClass + /** * Describes a single request/response body with multipart content. @@ -12,11 +12,13 @@ class OpenApiMultipartBody : OpenApiBaseBody() { private val parts = mutableListOf() + fun getParts(): List = parts + /** * One part of a multipart-body */ - fun part(name: String, type: Type, block: OpenApiMultipartPart.() -> Unit) { + fun part(name: String, type: SchemaType, block: OpenApiMultipartPart.() -> Unit) { parts.add(OpenApiMultipartPart(name, type).apply(block)) } @@ -24,20 +26,19 @@ class OpenApiMultipartBody : OpenApiBaseBody() { /** * One part of a multipart-body */ - fun part(name: String, type: Type) = part(name, type) {} + fun part(name: String, type: KClass<*>) = part(name, type.asSchemaType()) {} /** * One part of a multipart-body */ - inline fun part(name: String) = part(name, object : TypeReference() {}.type) + inline fun part(name: String) = part(name, getSchemaType()) {} /** * One part of a multipart-body */ - inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit) = - part(name, object : TypeReference() {}.type, block) + inline fun part(name: String, noinline block: OpenApiMultipartPart.() -> Unit) = part(name, getSchemaType(), block) /** @@ -67,6 +68,4 @@ class OpenApiMultipartBody : OpenApiBaseBody() { */ fun part(name: String, customSchemaId: String) = part(name, customSchemaId) {} - fun getParts(): List = parts - } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt index 6a7623c..dbd9780 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiMultipartPart.kt @@ -1,8 +1,6 @@ package io.github.smiley4.ktorswaggerui.dsl -import com.fasterxml.jackson.core.type.TypeReference import io.ktor.http.ContentType -import java.lang.reflect.Type import kotlin.reflect.KClass /** @@ -16,7 +14,7 @@ class OpenApiMultipartPart( */ val name: String, - val type: Type? + val type: SchemaType? ) { /** @@ -24,6 +22,7 @@ class OpenApiMultipartPart( */ var customSchema: CustomSchemaRef? = null + /** * Set a specific content type for this part */ @@ -31,10 +30,13 @@ class OpenApiMultipartPart( private val headers = mutableMapOf() + fun getHeaders(): Map = headers + + /** * Possible headers for this part */ - fun header(name: String, type: Type, block: OpenApiHeader.() -> Unit) { + fun header(name: String, type: SchemaType, block: OpenApiHeader.() -> Unit) { headers[name] = OpenApiHeader().apply(block).apply { this.type = type } @@ -43,19 +45,24 @@ class OpenApiMultipartPart( /** * Possible headers for this part */ - fun header(name: String, type: KClass<*>) = header(name, type.java) {} + fun header(name: String, type: KClass<*>, block: OpenApiHeader.() -> Unit) = header(name, type.asSchemaType(), block) /** * Possible headers for this part */ - inline fun header(name: String) = header(name, object : TypeReference() {}.type) {} + fun header(name: String, type: KClass<*>) = header(name, type) {} + /** * Possible headers for this part */ - inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit) = - header(name, object : TypeReference() {}.type, block) + inline fun header(name: String) = header(name, getSchemaType()) {} + + + /** + * Possible headers for this part + */ + inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit) = header(name, getSchemaType(), block) - fun getHeaders(): Map = headers } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt index a617e2e..0ef7ad4 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequest.kt @@ -1,9 +1,9 @@ package io.github.smiley4.ktorswaggerui.dsl -import com.fasterxml.jackson.core.type.TypeReference -import java.lang.reflect.Type +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter.Location import kotlin.reflect.KClass + @OpenApiDslMarker class OpenApiRequest { @@ -12,19 +12,22 @@ class OpenApiRequest { */ private val parameters = mutableListOf() + fun getParameters(): List = parameters + /** * A path parameters that is applicable for this operation */ - fun pathParameter(name: String, type: Type, block: OpenApiRequestParameter.() -> Unit) { - parameters.add(OpenApiRequestParameter(name, type, OpenApiRequestParameter.Location.PATH).apply(block)) + fun parameter(location: Location, name: String, type: SchemaType, block: OpenApiRequestParameter.() -> Unit) { + parameters.add(OpenApiRequestParameter(name, type, location).apply(block)) } /** * A path parameters that is applicable for this operation */ - fun pathParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = pathParameter(name, type.java, block) + fun pathParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = + parameter(Location.PATH, name, type.asSchemaType(), block) /** @@ -36,28 +39,22 @@ class OpenApiRequest { /** * A path parameters that is applicable for this operation */ - inline fun pathParameter(name: String) = pathParameter(name, object : TypeReference() {}.type) {} + inline fun pathParameter(name: String) = + parameter(Location.PATH, name, getSchemaType()) {} /** * A path parameters that is applicable for this operation */ inline fun pathParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit) = - pathParameter(name, object : TypeReference() {}.type, block) + parameter(Location.PATH, name, getSchemaType(), block) /** * A query parameters that is applicable for this operation */ - fun queryParameter(name: String, type: Type, block: OpenApiRequestParameter.() -> Unit) { - parameters.add(OpenApiRequestParameter(name, type, OpenApiRequestParameter.Location.QUERY).apply(block)) - } - - - /** - * A query parameters that is applicable for this operation - */ - fun queryParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = queryParameter(name, type.java, block) + fun queryParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = + parameter(Location.QUERY, name, type.asSchemaType(), block) /** @@ -69,94 +66,85 @@ class OpenApiRequest { /** * A query parameters that is applicable for this operation */ - inline fun queryParameter(name: String) = queryParameter(name, object : TypeReference() {}.type) {} + inline fun queryParameter(name: String) = + parameter(Location.QUERY, name, getSchemaType()) {} /** * A query parameters that is applicable for this operation */ inline fun queryParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit) = - queryParameter(name, object : TypeReference() {}.type, block) - - - /** - * A header parameters that is applicable for this operation - */ - fun headerParameter(name: String, type: Type, block: OpenApiRequestParameter.() -> Unit) { - parameters.add(OpenApiRequestParameter(name, type, OpenApiRequestParameter.Location.HEADER).apply(block)) - } + parameter(Location.QUERY, name, getSchemaType(), block) /** * A header parameters that is applicable for this operation */ - fun headerParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = headerParameter(name, type.java, block) + fun headerParameter(name: String, type: KClass<*>, block: OpenApiRequestParameter.() -> Unit) = + parameter(Location.HEADER, name, type.asSchemaType(), block) /** * A header parameters that is applicable for this operation */ - fun headerParameter(name: String, type: KClass<*>) = headerParameter(name, type) {} + fun headerParameter(name: String, type: SchemaType) = parameter(Location.HEADER, name, type) {} /** * A header parameters that is applicable for this operation */ - inline fun headerParameter(name: String) = headerParameter(name, object : TypeReference() {}.type) {} + inline fun headerParameter(name: String) = + parameter(Location.HEADER, name, getSchemaType()) {} /** * A header parameters that is applicable for this operation */ inline fun headerParameter(name: String, noinline block: OpenApiRequestParameter.() -> Unit) = - headerParameter(name, object : TypeReference() {}.type, block) + parameter(Location.HEADER, name, getSchemaType(), block) - fun getParameters(): List = parameters private var body: OpenApiBaseBody? = null + fun getBody() = body + /** * The request body applicable for this operation */ - fun body(type: Type, block: OpenApiSimpleBody.() -> Unit) { + fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) { body = OpenApiSimpleBody(type).apply(block) } - /** * The request body applicable for this operation */ - fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(type.java).apply(block) - } + fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) /** * The request body applicable for this operation */ @JvmName("bodyGenericType") - inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(object : TypeReference() {}.type, block) + inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) /** * The request body applicable for this operation */ - fun body(type: KClass<*>) = body(type.java) {} + fun body(type: KClass<*>) = body(type) {} /** * The request body applicable for this operation */ - inline fun body() = body(object : TypeReference() {}.type) {} + inline fun body() = body(getSchemaType()) {} /** * The request body applicable for this operation */ - fun body(block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(null).apply(block) - } + fun body(block: OpenApiSimpleBody.() -> Unit) = body(null, block) /** @@ -202,6 +190,5 @@ class OpenApiRequest { this.body = body } - fun getBody() = body } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt index 7d515d8..9f07798 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiRequestParameter.kt @@ -1,6 +1,5 @@ package io.github.smiley4.ktorswaggerui.dsl -import java.lang.reflect.Type @OpenApiDslMarker class OpenApiRequestParameter( @@ -11,7 +10,7 @@ class OpenApiRequestParameter( /** * The type defining the schema used for the parameter. */ - val type: Type, + val type: SchemaType, /** * Location of the parameter */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt index a44f39e..77b244a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiResponse.kt @@ -1,7 +1,5 @@ package io.github.smiley4.ktorswaggerui.dsl -import com.fasterxml.jackson.core.type.TypeReference -import java.lang.reflect.Type import kotlin.reflect.KClass /** @@ -18,11 +16,13 @@ class OpenApiResponse(val statusCode: String) { private val headers = mutableMapOf() + fun getHeaders(): Map = headers + /** * Possible headers returned with this response */ - fun header(name: String, type: Type, block: OpenApiHeader.() -> Unit) { + fun header(name: String, type: SchemaType, block: OpenApiHeader.() -> Unit) { headers[name] = OpenApiHeader().apply(block).apply { this.type = type } @@ -32,30 +32,36 @@ class OpenApiResponse(val statusCode: String) { /** * Possible headers returned with this response */ - fun header(name: String, type: KClass<*>) = header(name, type.java) {} + fun header(name: String, type: KClass<*>, block: OpenApiHeader.() -> Unit) = header(name, type.asSchemaType(), block) /** * Possible headers returned with this response */ - inline fun header(name: String) = header(name, object : TypeReference() {}.type) {} + fun header(name: String, type: KClass<*>) = header(name, type.asSchemaType()) {} /** * Possible headers returned with this response */ - inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit) = - header(name, object : TypeReference() {}.type, block) + inline fun header(name: String) = header(name, getSchemaType()) {} + + + /** + * Possible headers returned with this response + */ + inline fun header(name: String, noinline block: OpenApiHeader.() -> Unit) = header(name, getSchemaType(), block) - fun getHeaders(): Map = headers private var body: OpenApiBaseBody? = null + fun getBody() = body + /** * The body returned with this response */ - fun body(type: Type, block: OpenApiSimpleBody.() -> Unit) { + fun body(type: SchemaType?, block: OpenApiSimpleBody.() -> Unit) { body = OpenApiSimpleBody(type).apply(block) } @@ -63,37 +69,32 @@ class OpenApiResponse(val statusCode: String) { /** * The body returned with this response */ - fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(type.java).apply(block) - } + fun body(type: KClass<*>, block: OpenApiSimpleBody.() -> Unit) = body(type.asSchemaType(), block) /** * The body returned with this response */ @JvmName("bodyGenericType") - inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = - body(object : TypeReference() {}.type, block) + inline fun body(noinline block: OpenApiSimpleBody.() -> Unit) = body(getSchemaType(), block) /** * The body returned with this response */ - fun body(type: KClass<*>) = body(type.java) {} + fun body(type: KClass<*>) = body(type) {} /** * The body returned with this response */ - inline fun body() = body(object : TypeReference() {}.type) {} + inline fun body() = body(getSchemaType()) {} /** * The body returned with this response */ - fun body(block: OpenApiSimpleBody.() -> Unit) { - body = OpenApiSimpleBody(null).apply(block) - } + fun body(block: OpenApiSimpleBody.() -> Unit) = body(null, block) /** @@ -131,14 +132,4 @@ class OpenApiResponse(val statusCode: String) { body = OpenApiMultipartBody().apply(block) } - - /** - * Set the body of this response. Intended for internal use. - */ - fun setBody(body: OpenApiBaseBody?) { - this.body = body - } - - fun getBody() = body - } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt index 09d72b9..0dc426d 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/OpenApiSimpleBody.kt @@ -1,6 +1,5 @@ package io.github.smiley4.ktorswaggerui.dsl -import java.lang.reflect.Type /** * Describes the base of a single request/response body. @@ -10,7 +9,7 @@ class OpenApiSimpleBody( /** * The type defining the schema used for the body. */ - val type: Type?, + val type: SchemaType?, ) : OpenApiBaseBody() { /** @@ -18,6 +17,7 @@ class OpenApiSimpleBody( */ var customSchema: CustomSchemaRef? = null + /** * Examples for this body */ diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt new file mode 100644 index 0000000..95915fb --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt @@ -0,0 +1,29 @@ +package io.github.smiley4.ktorswaggerui.dsl + +import kotlin.reflect.KClass +import kotlin.reflect.KType +import kotlin.reflect.full.starProjectedType +import kotlin.reflect.javaType +import kotlin.reflect.typeOf + +typealias SchemaType = KType + + +inline fun getSchemaType(): SchemaType { + return typeOf() +} + +@OptIn(ExperimentalStdlibApi::class) +fun SchemaType.getTypeName() = this.javaType.typeName + +fun SchemaType.getSimpleTypeName(): String { + val rawName = getTypeName() + if(rawName.contains("<") || rawName.contains(">")) { + return rawName + } else { + return (this.classifier as KClass<*>).simpleName ?: rawName + } +} + +fun KClass<*>.asSchemaType() = this.starProjectedType + diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt new file mode 100644 index 0000000..f2033fa --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt @@ -0,0 +1,40 @@ +package io.github.smiley4.ktorswaggerui.dsl + +/** + * Configuration for serializing examples, schemas, ... + */ +@OpenApiDslMarker +class SerializationConfig { + + /** + * Serialize the given example object into a json-string. + * Return 'null' to use the default serializer for the given value instead. + */ + fun exampleSerializer(serializer: CustomExampleSerializer) { + customExampleSerializer = serializer + } + + private var customExampleSerializer: CustomExampleSerializer = { _, _ -> null } + + fun getCustomExampleSerializer() = customExampleSerializer + + + /** + * Serialize the given type into a valid json-schema. + * Return 'null' to use the default serializer for the given type instead. + * This serializer does not affect custom-schemas provided in the plugin-config. + */ + fun schemaSerializer(serializer: CustomSchemaSerializer) { + customSchemaSerializer = serializer + } + + private var customSchemaSerializer: CustomSchemaSerializer = { null } + + + fun getCustomSchemaSerializer() = customSchemaSerializer + +} + +typealias CustomExampleSerializer = (type: SchemaType?, example: Any) -> String? + +typealias CustomSchemaSerializer = (type: SchemaType) -> String? diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUI.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt similarity index 99% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUI.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt index 1641af4..76428be 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUI.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SwaggerUIDsl.kt @@ -1,7 +1,7 @@ package io.github.smiley4.ktorswaggerui.dsl @OpenApiDslMarker -class SwaggerUI { +class SwaggerUIDsl { /** * Whether to forward the root-url to the swagger-url diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt index 0ce3915..ce9cd23 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ContentBuilder.kt @@ -5,6 +5,7 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartPart +import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.ktor.http.ContentType import io.swagger.v3.oas.models.media.Content @@ -56,14 +57,14 @@ class ContentBuilder( private fun buildSimpleMediaTypes(body: OpenApiSimpleBody, schema: Schema<*>?): Map { val mediaTypes = body.getMediaTypes().ifEmpty { schema?.let { setOf(chooseMediaType(schema)) } ?: setOf() } - return mediaTypes.associateWith { buildSimpleMediaType(schema, body.getExamples()) } + return mediaTypes.associateWith { buildSimpleMediaType(schema, body.type, body.getExamples()) } } - private fun buildSimpleMediaType(schema: Schema<*>?, examples: Map): MediaType { + private fun buildSimpleMediaType(schema: Schema<*>?, type: SchemaType?, examples: Map): MediaType { return MediaType().also { it.schema = schema examples.forEach { (name, obj) -> - it.addExamples(name, exampleBuilder.build(obj)) + it.addExamples(name, exampleBuilder.build(type, obj)) } } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt index c1fb8c3..a8049a4 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt @@ -1,15 +1,23 @@ package io.github.smiley4.ktorswaggerui.spec.openapi +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiExample +import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.swagger.v3.oas.models.examples.Example -class ExampleBuilder { +class ExampleBuilder( + private val config: SwaggerUIPluginConfig +) { - fun build(example: OpenApiExample): Example = + fun build(type: SchemaType?, example: OpenApiExample): Example = Example().also { - it.value = example.value + it.value = buildExampleValue(type, example.value) it.summary = example.summary it.description = example.description } + private fun buildExampleValue(type: SchemaType?, value: Any): Any { + return config.serializationConfig.getCustomExampleSerializer()(type, value) ?: value + } + } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index 6abd892..a3cf64d 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -1,18 +1,23 @@ package io.github.smiley4.ktorswaggerui.spec.schema import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode import com.github.victools.jsonschema.generator.SchemaGenerator import com.github.victools.jsonschema.generator.SchemaGeneratorConfig +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.github.smiley4.ktorswaggerui.dsl.getSimpleTypeName import io.swagger.v3.core.util.Json import io.swagger.v3.oas.models.media.Schema import io.swagger.v3.oas.models.media.XML -import java.lang.reflect.Type +import kotlin.reflect.javaType class JsonSchemaBuilder( + private val pluginConfig: SwaggerUIPluginConfig, schemaGeneratorConfig: SchemaGeneratorConfig ) { @@ -32,26 +37,32 @@ class JsonSchemaBuilder( private val generator = SchemaGenerator(schemaGeneratorConfig) - fun build(type: Type): OpenApiSchemaInfo { + fun build(type: SchemaType): OpenApiSchemaInfo { return type .let { buildJsonSchema(it) } - .let { build(it) } + .let { build(it, type.getSimpleTypeName()) } } - fun build(schema: JsonNode): OpenApiSchemaInfo { + fun build(schema: JsonNode, typeName: String): OpenApiSchemaInfo { + println("hello") return schema - .let { processJsonSchema(it) } + .let { processJsonSchema(it, typeName) } .let { buildOpenApiSchema(it) } } - private fun buildJsonSchema(type: Type): JsonNode { - return generator.generateSchema(type) + + @OptIn(ExperimentalStdlibApi::class) + private fun buildJsonSchema(type: SchemaType): JsonNode { + return pluginConfig.serializationConfig.getCustomSchemaSerializer() + .let { customSerializer -> customSerializer(type) } + ?.let { ObjectMapper().readTree(it) } + ?: generator.generateSchema(type.javaType) } - private fun processJsonSchema(json: JsonNode): JsonSchemaInfo { - if (json is ObjectNode && json.get("\$defs") != null) { - val mainDefinition = json.get("\$ref").asText().replace("#/\$defs/", "") - val definitions = json.get("\$defs").fields().asSequence().map { it.key to it.value }.toList() + private fun processJsonSchema(json: JsonNode, typeName: String): JsonSchemaInfo { + if (json is ObjectNode && hasDefinitions(json)) { + val mainDefinition = getMainDefinition(json) + val definitions = getDefinitions(json) definitions.forEach { cleanupRefPaths(it.second) } return JsonSchemaInfo( rootSchema = mainDefinition, @@ -59,12 +70,23 @@ class JsonSchemaBuilder( ) } else { return JsonSchemaInfo( - rootSchema = "root", - schemas = mapOf("root" to json) + rootSchema = typeName, + schemas = mapOf(typeName to json) ) } } + private fun hasDefinitions(json: JsonNode) = json.get("\$defs") != null || json.get("definitions") != null + + private fun getDefinitions(json: JsonNode): List> { + return (json.get("\$defs") ?: json["definitions"]).fields().asSequence().map { it.key to it.value }.toList() + } + + private fun getMainDefinition(json: JsonNode) = + json.get("\$ref").asText() + .replace("#/definitions/", "") + .replace("#/\$defs/", "") + private fun cleanupRefPaths(node: JsonNode) { when (node) { is ObjectNode -> { @@ -81,6 +103,7 @@ class JsonSchemaBuilder( } private fun buildOpenApiSchema(json: JsonSchemaInfo): OpenApiSchemaInfo { + println(json) return OpenApiSchemaInfo( rootSchema = json.rootSchema, schemas = json.schemas.mapValues { (name, schema) -> buildOpenApiSchema(schema, name) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index 3900187..cbf2b73 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -13,10 +13,11 @@ import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema +import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.github.smiley4.ktorswaggerui.dsl.getTypeName import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder.Companion.OpenApiSchemaInfo import io.swagger.v3.oas.models.media.Schema -import java.lang.reflect.Type import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set @@ -87,8 +88,8 @@ class SchemaContext( } - private fun createSchema(type: Type) { - if (schemas.containsKey(type.typeName)) { + private fun createSchema(type: SchemaType) { + if (schemas.containsKey(type.getTypeName())) { return } addSchema(type, jsonSchemaBuilder.build(type)) @@ -105,7 +106,7 @@ class SchemaContext( } else { when (customSchema) { is CustomJsonSchema -> { - jsonSchemaBuilder.build(ObjectMapper().readTree(customSchema.provider())).let { + jsonSchemaBuilder.build(ObjectMapper().readTree(customSchema.provider()), customSchemaRef.schemaId).let { it.schemas[it.rootSchema]!! } } @@ -132,8 +133,8 @@ class SchemaContext( } } - fun addSchema(type: Type, schema: OpenApiSchemaInfo) { - schemas[type.typeName] = schema + fun addSchema(type: SchemaType, schema: OpenApiSchemaInfo) { + schemas[type.getTypeName()] = schema } fun addSchema(customSchemaRef: CustomSchemaRef, schema: Schema<*>) { @@ -168,7 +169,7 @@ class SchemaContext( } - fun getSchema(type: Type): Schema<*> { + fun getSchema(type: SchemaType): Schema<*> { val schemaInfo = getSchemaInfo(type) val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! return buildInlineSchema(schemaInfo.rootSchema, rootSchema, schemaInfo.schemas.size) @@ -196,8 +197,8 @@ class SchemaContext( } - private fun getSchemaInfo(type: Type): OpenApiSchemaInfo { - return type.typeName.let { typeName -> + private fun getSchemaInfo(type: SchemaType): OpenApiSchemaInfo { + return type.getTypeName().let { typeName -> schemas[typeName] ?: throw IllegalStateException("Could not retrieve schema for type '${typeName}'") } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt index 48ca1d5..2a6fc94 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt @@ -1,5 +1,6 @@ package io.github.smiley4.ktorswaggerui.examples +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUI import io.github.smiley4.ktorswaggerui.dsl.AuthScheme @@ -12,6 +13,7 @@ import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import io.swagger.v3.oas.models.media.Schema +import kotlin.reflect.jvm.javaType fun main() { embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) @@ -74,7 +76,7 @@ private fun Application.myModule() { generateTags { url -> listOf(url.firstOrNull()) } schemas { jsonSchemaBuilder { type -> - SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type).toPrettyString() + SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toPrettyString() } json("customSchema1") { """{"type": "string"}""" @@ -86,6 +88,15 @@ private fun Application.myModule() { } remote("customSchema3", "example.com/schema") } + inlineAllSchemas = true + serialization { + exampleSerializer { type, example -> + jacksonObjectMapper().writeValueAsString(example) + } + schemaSerializer { type -> + SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toPrettyString() + } + } schemaGeneratorConfigBuilder = schemaGeneratorConfigBuilder.let { /*...*/ it } } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt index 207209b..041167e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt @@ -1,6 +1,5 @@ package io.github.smiley4.ktorswaggerui.examples -import io.ktor.server.application.Application import com.github.victools.jsonschema.generator.Option import com.github.victools.jsonschema.generator.OptionPreset import com.github.victools.jsonschema.generator.SchemaGenerator @@ -10,6 +9,7 @@ import com.github.victools.jsonschema.module.jackson.JacksonModule import io.github.smiley4.ktorswaggerui.SwaggerUI import io.github.smiley4.ktorswaggerui.dsl.get import io.ktor.http.HttpStatusCode +import io.ktor.server.application.Application import io.ktor.server.application.call import io.ktor.server.application.install import io.ktor.server.engine.embeddedServer @@ -18,6 +18,7 @@ import io.ktor.server.request.receive import io.ktor.server.response.respond import io.ktor.server.routing.routing import java.lang.reflect.Type +import kotlin.reflect.jvm.javaType /** * An example for building custom json-schemas @@ -58,7 +59,7 @@ private fun Application.myModule() { schemas { jsonSchemaBuilder { type -> // custom converter from the given 'type' to a json-schema - typeToJsonSchema(type) + typeToJsonSchema(type.javaType) } } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt new file mode 100644 index 0000000..c291167 --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt @@ -0,0 +1,90 @@ +package io.github.smiley4.ktorswaggerui.examples + +import io.github.smiley4.ktorswaggerui.SwaggerUI +import io.github.smiley4.ktorswaggerui.dsl.get +import io.ktor.server.application.Application +import io.ktor.server.application.call +import io.ktor.server.application.install +import io.ktor.server.engine.embeddedServer +import io.ktor.server.netty.Netty +import io.ktor.server.response.respondText +import io.ktor.server.routing.routing +import kotlinx.datetime.Instant +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import com.github.ricky12awesome.jss.encodeToSchema + +/** + * An example showcasing compatibility with kotlinx serializer and kotlinx multiplatform using: + * - https://github.com/Kotlin/kotlinx.serialization + * - https://github.com/Kotlin/kotlinx-datetime + * - https://github.com/Ricky12Awesome/json-schema-serialization + */ +fun main() { + embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) +} + +private fun Application.myModule() { + + val kotlinxJson = Json { + prettyPrint = true + encodeDefaults = true + } + + install(SwaggerUI) { + serialization { + schemaSerializer { type -> + kotlinxJson.encodeToSchema(serializer(type), generateDefinitions = true) +// globalJson.encodeToSchema(serializer(type), generateDefinitions = false) + } + exampleSerializer { type, value -> + kotlinxJson.encodeToString(serializer(type!!), value) + } + } + } + routing { + get("example/one", { + request { + body { + example( + "default", ExampleRequest.B( + thisIsB = Instant.fromEpochMilliseconds(System.currentTimeMillis()) + ) + ) + } + } + }) { + call.respondText("...") + } +// get("example/many", { +// request { +// body> { +// example("default", listOf( +// ExampleRequest.B(Instant.fromEpochMilliseconds(System.currentTimeMillis())), +// ExampleRequest.A(true) +// )) +// } +// } +// }) { +// call.respondText("...") +// } + } +} + + +@Serializable +private sealed class ExampleRequest { + + @Serializable + data class A( + val thisIsA: Boolean + ) : ExampleRequest() + + + @Serializable + data class B( + val thisIsB: Instant + ) : ExampleRequest() + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt index 79f0897..0d6e4e2 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -99,7 +99,7 @@ class OpenApiBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) } private fun buildOpenApiObject(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): OpenAPI { @@ -123,7 +123,9 @@ class OpenApiBuilderTest : StringSpec({ requestBodyBuilder = RequestBodyBuilder( contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = pluginConfig + ), headerBuilder = HeaderBuilder(schemaContext) ) ), @@ -132,7 +134,9 @@ class OpenApiBuilderTest : StringSpec({ headerBuilder = HeaderBuilder(schemaContext), contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = pluginConfig + ), headerBuilder = HeaderBuilder(schemaContext) ) ), diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index 2adcbf1..33b7d49 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -882,7 +882,7 @@ class OperationBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) } private fun buildOperationObject( @@ -896,7 +896,9 @@ class OperationBuilderTest : StringSpec({ requestBodyBuilder = RequestBodyBuilder( contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = pluginConfig + ), headerBuilder = HeaderBuilder(schemaContext) ) ), @@ -905,7 +907,9 @@ class OperationBuilderTest : StringSpec({ headerBuilder = HeaderBuilder(schemaContext), contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = pluginConfig + ), headerBuilder = HeaderBuilder(schemaContext) ) ), diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index f97d8e1..3336fc0 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -84,7 +84,7 @@ class PathsBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) } private fun buildPathsObject( @@ -100,7 +100,9 @@ class PathsBuilderTest : StringSpec({ requestBodyBuilder = RequestBodyBuilder( contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = pluginConfig + ), headerBuilder = HeaderBuilder(schemaContext) ) ), @@ -109,7 +111,9 @@ class PathsBuilderTest : StringSpec({ headerBuilder = HeaderBuilder(schemaContext), contentBuilder = ContentBuilder( schemaContext = schemaContext, - exampleBuilder = ExampleBuilder(), + exampleBuilder = ExampleBuilder( + config = pluginConfig + ), headerBuilder = HeaderBuilder(schemaContext) ) ), diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index 5c29e3b..510f67c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -2,9 +2,10 @@ package io.github.smiley4.ktorswaggerui.tests.schema import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo -import com.fasterxml.jackson.core.type.TypeReference import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute +import io.github.smiley4.ktorswaggerui.dsl.asSchemaType +import io.github.smiley4.ktorswaggerui.dsl.getSchemaType import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext @@ -40,27 +41,27 @@ class SchemaContextTest : StringSpec({ ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(QueryParamType::class.java).also { schema -> + schemaContext.getSchema(QueryParamType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/QueryParamType" } - schemaContext.getSchema(PathParamType::class.java).also { schema -> + schemaContext.getSchema(PathParamType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/PathParamType" } - schemaContext.getSchema(HeaderParamType::class.java).also { schema -> + schemaContext.getSchema(HeaderParamType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/HeaderParamType" } - schemaContext.getSchema(RequestBodyType::class.java).also { schema -> + schemaContext.getSchema(RequestBodyType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/RequestBodyType" } - schemaContext.getSchema(ResponseHeaderType::class.java).also { schema -> + schemaContext.getSchema(ResponseHeaderType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/ResponseHeaderType" } - schemaContext.getSchema(ResponseBodyType::class.java).also { schema -> + schemaContext.getSchema(ResponseBodyType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/ResponseBodyType" } @@ -94,7 +95,7 @@ class SchemaContextTest : StringSpec({ ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(Integer::class.java).also { schema -> + schemaContext.getSchema(Integer::class.asSchemaType()).also { schema -> schema.type shouldBe "integer" schema.format shouldBe "int32" schema.`$ref` shouldBe null @@ -178,7 +179,7 @@ class SchemaContextTest : StringSpec({ ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(SimpleDataClass::class.java).also { schema -> + schemaContext.getSchema(SimpleDataClass::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/SimpleDataClass" } @@ -236,7 +237,7 @@ class SchemaContextTest : StringSpec({ ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(DataWrapper::class.java).also { schema -> + schemaContext.getSchema(DataWrapper::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/DataWrapper" } @@ -271,7 +272,7 @@ class SchemaContextTest : StringSpec({ ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(SimpleEnum::class.java).also { schema -> + schemaContext.getSchema(SimpleEnum::class.asSchemaType()).also { schema -> schema.type shouldBe "string" schema.enum shouldContainExactlyInAnyOrder SimpleEnum.values().map { it.name } schema.`$ref` shouldBe null @@ -295,7 +296,7 @@ class SchemaContextTest : StringSpec({ ) ) val schemaContext = schemaContext().initialize(routes) - schemaContext.getSchema(DataClassWithMaps::class.java).also { schema -> + schemaContext.getSchema(DataClassWithMaps::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/DataClassWithMaps" } @@ -320,12 +321,12 @@ class SchemaContextTest : StringSpec({ companion object { - inline fun getType() = object : TypeReference() {}.type + inline fun getType() = getSchemaType() private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) } private data class QueryParamType(val value: String) From 66fec7368cda1110e067719a97599e48e7031b05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20R=C3=BCgner?= Date: Wed, 24 May 2023 14:21:22 +0200 Subject: [PATCH 21/27] wip --- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 48 +-- .../smiley4/ktorswaggerui/dsl/SchemaType.kt | 3 +- .../spec/schema/JsonSchemaBuilder.kt | 1 - .../spec/schema/SchemaBuilder.kt | 88 ++++ .../spec/schema/SchemaContext.kt | 50 +-- .../tests/openapi/OpenApiBuilderTest.kt | 10 +- .../tests/openapi/OperationBuilderTest.kt | 10 +- .../tests/openapi/PathsBuilderTest.kt | 10 +- .../tests/schema/SchemaBuilderTest.kt | 385 ++++++++++++++++++ .../tests/schema/SchemaContextTest.kt | 19 +- 10 files changed, 544 insertions(+), 80 deletions(-) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt create mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index ef9afee..f5e4eee 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -1,42 +1,18 @@ package io.github.smiley4.ktorswaggerui -import io.github.smiley4.ktorswaggerui.spec.openapi.ComponentsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ExternalDocumentationBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.InfoBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.LicenseBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OAuthFlowsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OpenApiBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.PathBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.PathsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder +import com.github.victools.jsonschema.generator.SchemaGenerator +import io.github.smiley4.ktorswaggerui.spec.openapi.* import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext -import io.ktor.server.application.Application -import io.ktor.server.application.ApplicationStarted -import io.ktor.server.application.createApplicationPlugin -import io.ktor.server.application.hooks.MonitoringEvent -import io.ktor.server.application.install -import io.ktor.server.application.plugin -import io.ktor.server.application.pluginOrNull -import io.ktor.server.routing.Routing -import io.ktor.server.webjars.Webjars +import io.ktor.server.application.* +import io.ktor.server.application.hooks.* +import io.ktor.server.routing.* +import io.ktor.server.webjars.* import io.swagger.v3.core.util.Json +import kotlin.reflect.jvm.javaType /** * This version must match the version of the gradle dependency @@ -65,13 +41,17 @@ val SwaggerUI = createApplicationPlugin(name = "SwaggerUI", createConfiguration } private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig): List { - return RouteCollector(RouteDocumentationMerger()).collectRoutes({ application.plugin(Routing) }, pluginConfig).toList() + return RouteCollector(RouteDocumentationMerger()) + .collectRoutes({ application.plugin(Routing) }, pluginConfig) + .toList() } private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { return SchemaContext( config = pluginConfig, - jsonSchemaBuilder = JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build()) + schemaBuilder = SchemaBuilder("\$defs") { type -> + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + } ).initialize(routes.toList()) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt index 95915fb..634b538 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SchemaType.kt @@ -13,8 +13,7 @@ inline fun getSchemaType(): SchemaType { return typeOf() } -@OptIn(ExperimentalStdlibApi::class) -fun SchemaType.getTypeName() = this.javaType.typeName +fun SchemaType.getTypeName() = this.toString() fun SchemaType.getSimpleTypeName(): String { val rawName = getTypeName() diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index a3cf64d..55cd5f1 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -44,7 +44,6 @@ class JsonSchemaBuilder( } fun build(schema: JsonNode, typeName: String): OpenApiSchemaInfo { - println("hello") return schema .let { processJsonSchema(it, typeName) } .let { buildOpenApiSchema(it) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt new file mode 100644 index 0000000..9126fab --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt @@ -0,0 +1,88 @@ +package io.github.smiley4.ktorswaggerui.spec.schema + +import com.fasterxml.jackson.databind.JsonNode +import com.fasterxml.jackson.databind.ObjectMapper +import com.fasterxml.jackson.databind.node.ArrayNode +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.databind.node.TextNode +import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaSerializer +import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.swagger.v3.core.util.Json +import io.swagger.v3.oas.models.media.Schema + + +data class SchemaDefinitions( + val root: Schema<*>, + val definitions: Map> +) + +class SchemaBuilder( + private val definitionsField: String? = null, + private val schemaSerializer: CustomSchemaSerializer +) { + + + fun create(type: SchemaType): SchemaDefinitions { + return create(createJsonSchema(type)) + } + + fun create(jsonSchema: String): SchemaDefinitions { + return create(ObjectMapper().readTree(jsonSchema)) + } + + fun create(jsonSchema: JsonNode): SchemaDefinitions { + normalizeRefs(jsonSchema) { normalizeRef(it) } + val additionalDefinitions = extractAdditionalDefinitions(jsonSchema) + val rootSchema = toOpenApiSchema(jsonSchema) + val additionalSchemas = additionalDefinitions.mapValues { toOpenApiSchema(it.value) } + return SchemaDefinitions( + root = rootSchema, + definitions = additionalSchemas + ) + } + + private fun createJsonSchema(type: SchemaType): JsonNode { + val str = schemaSerializer(type) + return ObjectMapper().readTree(str) + } + + private fun normalizeRefs(node: JsonNode, normalizer: (ref: String) -> String) { + when (node) { + is ObjectNode -> { + node.get("\$ref")?.also { + node.set("\$ref", TextNode(normalizer(it.asText()))) + } + node.elements().asSequence().forEach { normalizeRefs(it, normalizer) } + } + + is ArrayNode -> { + node.elements().asSequence().forEach { normalizeRefs(it, normalizer) } + } + } + } + + private fun normalizeRef(ref: String): String { + val prefix = "#/$definitionsField" + return if (ref.startsWith(prefix)) { + ref.replace(prefix, "#/components/schemas") + } else { + ref + } + } + + private fun extractAdditionalDefinitions(schema: JsonNode): Map { + val definitionsPath = definitionsField?.let { "/$definitionsField" } + return if (definitionsPath == null || schema.at(definitionsPath).isMissingNode) { + emptyMap() + } else { + (schema.at(definitionsPath) as ObjectNode) + .fields().asSequence().toList() + .associate { (key, node) -> key to node } + } + } + + private fun toOpenApiSchema(jsonSchema: JsonNode): Schema<*> { + return Json.mapper().readValue(jsonSchema.toString(), Schema::class.java) + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index cbf2b73..b63fe4c 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -1,6 +1,5 @@ package io.github.smiley4.ktorswaggerui.spec.schema -import com.fasterxml.jackson.databind.ObjectMapper import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema @@ -16,7 +15,6 @@ import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.github.smiley4.ktorswaggerui.dsl.getTypeName import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder.Companion.OpenApiSchemaInfo import io.swagger.v3.oas.models.media.Schema import kotlin.collections.component1 import kotlin.collections.component2 @@ -24,10 +22,10 @@ import kotlin.collections.set class SchemaContext( private val config: SwaggerUIPluginConfig, - private val jsonSchemaBuilder: JsonSchemaBuilder + private val schemaBuilder: SchemaBuilder ) { - private val schemas = mutableMapOf() + private val schemas = mutableMapOf() private val customSchemas = mutableMapOf>() @@ -92,7 +90,7 @@ class SchemaContext( if (schemas.containsKey(type.getTypeName())) { return } - addSchema(type, jsonSchemaBuilder.build(type)) + addSchema(type, schemaBuilder.create(type)) } @@ -106,9 +104,7 @@ class SchemaContext( } else { when (customSchema) { is CustomJsonSchema -> { - jsonSchemaBuilder.build(ObjectMapper().readTree(customSchema.provider()), customSchemaRef.schemaId).let { - it.schemas[it.rootSchema]!! - } + schemaBuilder.create(customSchema.provider()).root } is CustomOpenApiSchema -> { customSchema.provider() @@ -133,7 +129,7 @@ class SchemaContext( } } - fun addSchema(type: SchemaType, schema: OpenApiSchemaInfo) { + fun addSchema(type: SchemaType, schema: SchemaDefinitions) { schemas[type.getTypeName()] = schema } @@ -143,16 +139,12 @@ class SchemaContext( fun getComponentSection(): Map> { val componentSection = mutableMapOf>() - schemas.forEach { (_, schemaInfo) -> - val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! + schemas.forEach { (_, schemaDefinitions) -> + val rootSchema = schemaDefinitions.root if (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema)) { // skip - } else if (isWrapperArray(rootSchema)) { - schemaInfo.schemas.toMutableMap() - .also { it.remove(schemaInfo.rootSchema) } - .also { componentSection.putAll(it) } } else { - componentSection.putAll(schemaInfo.schemas) + componentSection.putAll(schemaDefinitions.definitions) } } customSchemas.forEach { (schemaId, schema) -> @@ -165,39 +157,29 @@ class SchemaContext( fun getSchema(customSchemaRef: CustomSchemaRef): Schema<*> { val schema = customSchemas[customSchemaRef.schemaId] ?: throw IllegalStateException("Could not retrieve schema for type '${customSchemaRef.schemaId}'") - return buildInlineSchema(customSchemaRef.schemaId, schema, 1) + return buildInlineSchema(customSchemaRef.schemaId, schema) } fun getSchema(type: SchemaType): Schema<*> { - val schemaInfo = getSchemaInfo(type) - val rootSchema = schemaInfo.schemas[schemaInfo.rootSchema]!! - return buildInlineSchema(schemaInfo.rootSchema, rootSchema, schemaInfo.schemas.size) + return getSchemaDefinitions(type).root } - private fun buildInlineSchema(schemaId: String, schema: Schema<*>, connectedSchemaCount: Int): Schema<*> { - if (isPrimitive(schema) && connectedSchemaCount == 1) { + private fun buildInlineSchema(schemaId: String, schema: Schema<*>): Schema<*> { + if (isPrimitive(schema)) { return schema } - if (isPrimitiveArray(schema) && connectedSchemaCount == 1) { + if (isPrimitiveArray(schema)) { return schema } - if (isWrapperArray(schema)) { - return Schema().also { wrapper -> - wrapper.type = "array" - wrapper.items = Schema().also { - it.`$ref` = schema.items.`$ref` - } - } - } return Schema().also { it.`$ref` = "#/components/schemas/$schemaId" } } - private fun getSchemaInfo(type: SchemaType): OpenApiSchemaInfo { + private fun getSchemaDefinitions(type: SchemaType): SchemaDefinitions { return type.getTypeName().let { typeName -> schemas[typeName] ?: throw IllegalStateException("Could not retrieve schema for type '${typeName}'") } @@ -212,8 +194,4 @@ class SchemaContext( return schema.type == "array" && (isPrimitive(schema.items) || isPrimitiveArray(schema.items)) } - private fun isWrapperArray(schema: Schema<*>): Boolean { - return schema.type == "array" && schema.items.type == null && schema.items.`$ref` != null - } - } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt index 0d6e4e2..6c8c4f9 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -1,5 +1,6 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo import io.github.smiley4.ktorswaggerui.spec.openapi.ComponentsBuilder @@ -26,6 +27,7 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.engine.test.logging.info @@ -37,6 +39,7 @@ import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.swagger.v3.oas.models.OpenAPI import io.swagger.v3.oas.models.info.Info +import kotlin.reflect.jvm.javaType class OpenApiBuilderTest : StringSpec({ @@ -99,7 +102,12 @@ class OpenApiBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext( + config = pluginConfig, + schemaBuilder = SchemaBuilder("\$defs") { type -> + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + } + ) } private fun buildOpenApiObject(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): OpenAPI { diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index 33b7d49..48526e6 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -1,5 +1,6 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.dsl.obj @@ -15,6 +16,7 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty @@ -30,6 +32,7 @@ import io.ktor.http.HttpStatusCode import io.swagger.v3.oas.models.Operation import io.swagger.v3.oas.models.media.Schema import java.io.File +import kotlin.reflect.jvm.javaType class OperationBuilderTest : StringSpec({ @@ -882,7 +885,12 @@ class OperationBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext( + config = pluginConfig, + schemaBuilder = SchemaBuilder("\$defs") { type -> + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + } + ) } private fun buildOperationObject( diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index 3336fc0..6d94180 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -1,5 +1,6 @@ package io.github.smiley4.ktorswaggerui.tests.openapi +import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder @@ -16,6 +17,7 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -23,6 +25,7 @@ import io.kotest.matchers.maps.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import io.ktor.http.HttpMethod import io.swagger.v3.oas.models.Paths +import kotlin.reflect.jvm.javaType class PathsBuilderTest : StringSpec({ @@ -84,7 +87,12 @@ class PathsBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext( + config = pluginConfig, + schemaBuilder = SchemaBuilder("\$defs") { type -> + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + } + ) } private fun buildPathsObject( diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt new file mode 100644 index 0000000..fae20af --- /dev/null +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt @@ -0,0 +1,385 @@ +package io.github.smiley4.ktorswaggerui.tests.schema + +import com.github.ricky12awesome.jss.encodeToSchema +import com.github.victools.jsonschema.generator.Option +import com.github.victools.jsonschema.generator.OptionPreset +import com.github.victools.jsonschema.generator.SchemaGenerator +import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder +import com.github.victools.jsonschema.generator.SchemaVersion +import com.github.victools.jsonschema.module.jackson.JacksonModule +import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaSerializer +import io.github.smiley4.ktorswaggerui.dsl.getSchemaType +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions +import io.kotest.core.spec.style.StringSpec +import io.kotest.matchers.collections.shouldContainExactly +import io.kotest.matchers.maps.shouldHaveSize +import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe +import kotlinx.serialization.Serializable +import kotlinx.serialization.json.Json +import kotlinx.serialization.serializer +import kotlin.reflect.jvm.javaType + +class SchemaBuilderTest : StringSpec({ + + //==== PRIMITIVE TYPE ================================================== + + "test primitive (victools, all definitions)" { + createSchemaVictools(true).also { defs -> + defs.definitions.keys shouldContainExactly setOf("int") + defs.root.also { schema -> + schema.`$ref` shouldBe "#/components/schemas/int" + schema.type shouldBe null + } + defs.definitions["int"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "integer" + } + } + } + + "test primitive (victools, no definitions)" { + createSchemaVictools(false).also { defs -> + defs.definitions shouldHaveSize 0 + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "integer" + schema.properties shouldBe null + } + } + } + + "test primitive (kotlinx, definitions)" { + createSchemaKotlinX(true).also { defs -> + defs.definitions.keys shouldContainExactly setOf("x1xjd3yo2dbzzz") + defs.root.also { schema -> + schema.`$ref` shouldBe "#/components/schemas/x1xjd3yo2dbzzz" + schema.type shouldBe null + } + defs.definitions["x1xjd3yo2dbzzz"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "number" + schema.properties shouldBe null + } + } + } + + "test primitive (kotlinx, no definitions)" { + createSchemaKotlinX(false).also { defs -> + defs.definitions shouldHaveSize 0 + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "number" + schema.properties shouldBe null + } + } + } + + //==== SIMPLE OBJECT =================================================== + + "test simple object (victools, all definitions)" { + createSchemaVictools(true).also { defs -> + defs.definitions.keys shouldContainExactly setOf("Pet") + defs.root.also { schema -> + schema.`$ref` shouldBe "#/components/schemas/Pet" + schema.type shouldBe null + } + defs.definitions["Pet"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("id", "name", "tag") + } + } + } + + "test simple object (victools, no definitions)" { + createSchemaVictools(false).also { defs -> + println(defs) + defs.definitions shouldHaveSize 0 + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("id", "name", "tag") + schema.properties["id"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "integer" + } + schema.properties["name"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + schema.properties["tag"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + } + } + } + + "test simple object (kotlinx, definitions)" { + createSchemaKotlinX(true).also { defs -> + defs.definitions.keys shouldContainExactly setOf("1d8t6cs0dbcap", "x1xjd3yo2dbzzz", "xq0zwcprkn9j3") + defs.root.also { schema -> + schema.`$ref` shouldBe "#/components/schemas/1d8t6cs0dbcap" + schema.type shouldBe null + } + defs.definitions["1d8t6cs0dbcap"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("id", "name", "tag") + schema.properties["id"]!!.also { prop -> + prop.`$ref` shouldBe "#/components/schemas/x1xjd3yo2dbzzz" + prop.type shouldBe null + } + schema.properties["name"]!!.also { prop -> + prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" + prop.type shouldBe null + } + schema.properties["tag"]!!.also { prop -> + prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" + prop.type shouldBe null + } + } + defs.definitions["x1xjd3yo2dbzzz"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "number" + schema.properties shouldBe null + } + defs.definitions["xq0zwcprkn9j3"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "string" + schema.properties shouldBe null + } + } + } + + "test simple object (kotlinx, no definitions)" { + createSchemaKotlinX(false).also { defs -> + defs.definitions shouldHaveSize 0 + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("id", "name", "tag") + schema.properties["id"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "number" + } + schema.properties["name"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + schema.properties["tag"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + } + } + } + + //==== SIMPLE LIST ===================================================== + + "test simple list (victools, all definitions)" { + createSchemaVictools>(true).also { defs -> + defs.definitions.keys shouldContainExactly setOf("List(Pet)", "Pet") + defs.root.also { schema -> + schema.`$ref` shouldBe "#/components/schemas/List(Pet)" + schema.type shouldBe null + } + defs.definitions["List(Pet)"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "array" + schema.items + .also { it shouldNotBe null } + ?.also { item -> + item.type shouldBe null + item.`$ref` shouldBe "#/components/schemas/Pet" + } + } + defs.definitions["Pet"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("id", "name", "tag") + } + } + } + + "test simple list (victools, no definitions)" { + createSchemaVictools>(false).also { defs -> + defs.definitions shouldHaveSize 0 + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "array" + schema.properties shouldBe null + schema.items + .also { it shouldNotBe null } + ?.also { item -> + item.type shouldBe "object" + item.`$ref` shouldBe null + item.properties.keys shouldContainExactly setOf("id", "name", "tag") + item.properties["id"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "integer" + } + item.properties["name"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + item.properties["tag"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + } + } + } + } + + + "test simple list (kotlinx, definitions)" { + createSchemaKotlinX>(true).also { defs -> + defs.definitions.keys shouldContainExactly setOf( + "1tonzv7il5q0x", + "1d8t6cs0dbcap", + "x1xjd3yo2dbzzz", + "xq0zwcprkn9j3" + ) + defs.root.also { schema -> + schema.`$ref` shouldBe "#/components/schemas/1tonzv7il5q0x" + schema.type shouldBe null + } + defs.definitions["1tonzv7il5q0x"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "array" + schema.properties shouldBe null + schema.items + .also { it shouldNotBe null } + ?.also { item -> + item.type shouldBe null + item.`$ref` shouldBe "#/components/schemas/1d8t6cs0dbcap" + } + } + defs.definitions["1d8t6cs0dbcap"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "object" + schema.properties.keys shouldContainExactly setOf("id", "name", "tag") + schema.properties["id"]!!.also { prop -> + prop.`$ref` shouldBe "#/components/schemas/x1xjd3yo2dbzzz" + prop.type shouldBe null + } + schema.properties["name"]!!.also { prop -> + prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" + prop.type shouldBe null + } + schema.properties["tag"]!!.also { prop -> + prop.`$ref` shouldBe "#/components/schemas/xq0zwcprkn9j3" + prop.type shouldBe null + } + } + defs.definitions["x1xjd3yo2dbzzz"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "number" + schema.properties shouldBe null + } + defs.definitions["xq0zwcprkn9j3"]!!.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "string" + schema.properties shouldBe null + } + } + } + + "test simple list (kotlinx, no definitions)" { + createSchemaKotlinX>(false).also { defs -> + defs.definitions shouldHaveSize 0 + defs.root.also { schema -> + schema.`$ref` shouldBe null + schema.type shouldBe "array" + schema.properties shouldBe null + schema.items + .also { it shouldNotBe null } + ?.also { item -> + item.type shouldBe "object" + item.`$ref` shouldBe null + item.properties.keys shouldContainExactly setOf("id", "name", "tag") + item.properties["id"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "number" + } + item.properties["name"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + item.properties["tag"]!!.also { prop -> + prop.`$ref` shouldBe null + prop.type shouldBe "string" + } + } + } + } + } + +}) { + + companion object { + + @Serializable + private data class Pet( + val id: Int, + val name: String, + val tag: String + ) + + inline fun createSchemaVictools(definitions: Boolean) = + createSchema("\$defs", serializerVictools(definitions)) + + inline fun createSchemaKotlinX(generateDefinitions: Boolean) = + createSchema("definitions", serializerKotlinX(generateDefinitions)) + + inline fun createSchema( + defs: String, + noinline serializer: CustomSchemaSerializer + ): SchemaDefinitions { + return SchemaBuilder(defs, serializer).create(getSchemaType()) + } + + fun serializerVictools(definitions: Boolean): CustomSchemaSerializer { + return { type -> + SchemaGenerator( + SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES).also { + if (definitions) { + it + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .with(Option.DEFINITION_FOR_MAIN_SCHEMA) + .without(Option.INLINE_ALL_SCHEMAS) + } else { + it.with(Option.INLINE_ALL_SCHEMAS) + } + } + .build() + ).generateSchema(type.javaType).toPrettyString() + } + } + + fun serializerKotlinX(generateDefinitions: Boolean): CustomSchemaSerializer { + val kotlinxJson = Json { + prettyPrint = true + encodeDefaults = true + } + return { type -> + kotlinxJson.encodeToSchema( + serializer(type), + generateDefinitions = generateDefinitions, + exposeClassDiscriminator = false + ) + } + } + + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index 510f67c..a1852fa 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -2,19 +2,21 @@ package io.github.smiley4.ktorswaggerui.tests.schema import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.dsl.asSchemaType import io.github.smiley4.ktorswaggerui.dsl.getSchemaType import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.shouldBe -import io.ktor.http.HttpMethod +import io.ktor.http.* +import kotlin.reflect.jvm.javaType class SchemaContextTest : StringSpec({ @@ -301,7 +303,11 @@ class SchemaContextTest : StringSpec({ schema.`$ref` shouldBe "#/components/schemas/DataClassWithMaps" } schemaContext.getComponentSection().also { components -> - components.keys shouldContainExactlyInAnyOrder listOf("DataClassWithMaps", "Map(String,Long)", "Map(String,String)") + components.keys shouldContainExactlyInAnyOrder listOf( + "DataClassWithMaps", + "Map(String,Long)", + "Map(String,String)" + ) components["DataClassWithMaps"]?.also { schema -> schema.type shouldBe "object" schema.properties.keys shouldContainExactlyInAnyOrder listOf("mapStringValues", "mapLongValues") @@ -326,7 +332,12 @@ class SchemaContextTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext(pluginConfig, JsonSchemaBuilder(pluginConfig, pluginConfig.schemaGeneratorConfigBuilder.build())) + return SchemaContext( + config = pluginConfig, + schemaBuilder = SchemaBuilder("\$defs") { type -> + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + } + ) } private data class QueryParamType(val value: String) From 307ddab30978c68c25a4b52d66855424d754e805 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Thu, 25 May 2023 00:34:43 +0200 Subject: [PATCH 22/27] wip --- build.gradle.kts | 6 +- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 2 +- .../spec/schema/JsonSchemaBuilder.kt | 1 - .../spec/schema/SchemaContext.kt | 6 + .../{schema => schemaV2}/SchemaBuilder.kt | 2 +- .../spec/schemaV2/SchemaContext.kt | 166 ++++++++++++++++++ .../spec/schemaV2/SchemaContextBuilder.kt | 129 ++++++++++++++ .../tests/openapi/OpenApiBuilderTest.kt | 7 +- .../tests/openapi/OperationBuilderTest.kt | 3 +- .../tests/openapi/PathsBuilderTest.kt | 3 +- .../tests/schema/SchemaBuilderTest.kt | 4 +- .../tests/schema/SchemaContextTest.kt | 9 +- 12 files changed, 317 insertions(+), 21 deletions(-) rename src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/{schema => schemaV2}/SchemaBuilder.kt (98%) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt diff --git a/build.gradle.kts b/build.gradle.kts index 4d6b1b7..2b8cd1c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -56,9 +56,9 @@ dependencies { val versionKotlinTest = "1.7.21" testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") - testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")// TODO: remove!!!! - testImplementation("com.github.Ricky12Awesome:json-schema-serialization:0.9.9")// TODO: remove!!!! - testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // TODO: remove!!! + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")// TODO + testImplementation("com.github.Ricky12Awesome:json-schema-serialization:0.9.9")// TODO -> https://github.com/tillersystems/json-schema-serialization/tree/glureau + testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // TODO } tasks.test { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index f5e4eee..d04fe61 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -5,7 +5,7 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.* import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.ktor.server.application.* import io.ktor.server.application.hooks.* diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt index 55cd5f1..a3fbb92 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt @@ -102,7 +102,6 @@ class JsonSchemaBuilder( } private fun buildOpenApiSchema(json: JsonSchemaInfo): OpenApiSchemaInfo { - println(json) return OpenApiSchemaInfo( rootSchema = json.rootSchema, schemas = json.schemas.mapValues { (name, schema) -> buildOpenApiSchema(schema, name) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index b63fe4c..6504612 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -15,6 +15,8 @@ import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.github.smiley4.ktorswaggerui.dsl.getTypeName import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaDefinitions import io.swagger.v3.oas.models.media.Schema import kotlin.collections.component1 import kotlin.collections.component2 @@ -194,4 +196,8 @@ class SchemaContext( return schema.type == "array" && (isPrimitive(schema.items) || isPrimitiveArray(schema.items)) } + private fun isReference(schema: Schema<*>): Boolean { + return schema.type == null && schema.`$ref` != null + } + } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaBuilder.kt similarity index 98% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaBuilder.kt index 9126fab..4eb93fe 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.schema +package io.github.smiley4.ktorswaggerui.spec.schemaV2 import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt new file mode 100644 index 0000000..090447f --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt @@ -0,0 +1,166 @@ +package io.github.smiley4.ktorswaggerui.spec.schemaV2 + +import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef +import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.swagger.v3.oas.models.media.Schema + +class SchemaContext { + + private val schemas = mutableMapOf() + private val schemasCustom = mutableMapOf() + + private val componentsSection = mutableMapOf>() + private val inlineSchemas = mutableMapOf>() + private val inlineSchemasCustom = mutableMapOf>() + + + fun addSchema(type: SchemaType, schema: SchemaDefinitions) { + schemas[type] = schema + } + + + fun addSchema(ref: CustomSchemaRef, schema: SchemaDefinitions) { + schemasCustom[ref] = schema + } + + + fun getComponentsSection(): Map> = componentsSection + + + fun getSchema(type: SchemaType) = inlineSchemas[type] ?: throw Exception("No schema for type '$type'!") + + + fun getSchema(ref: CustomSchemaRef) = inlineSchemasCustom[ref] ?: throw Exception("No schema for ref '$ref'!") + + + fun finalize() { + schemas.forEach { (type, schemaDefinitions) -> + // only root definition + if (schemaDefinitions.definitions.isEmpty()) { + val root = schemaDefinitions.root + if (root.isPrimitive() || root.isPrimitiveArray()) { + inlineRoot(schemaDefinitions) + } else if (root.isObjectArray()) { + unwrapRootArray(schemaDefinitions) + } else { + createInlineReference(schemaDefinitions) + } + } + // only one additional definition + if (schemaDefinitions.definitions.size == 1) { + val root = schemaDefinitions.root + val definition = schemaDefinitions.definitions.entries.first().value + if (root.isReference() && (definition.isPrimitive() || definition.isPrimitiveArray())) { + inlineDefinition(schemaDefinitions) + } else if (root.isReference() || root.isReferenceArray()) { + inlineRoot(schemaDefinitions) + } else if (root.isObjectArray()) { + unwrapRootArray(schemaDefinitions) + } else { + createInlineReference(schemaDefinitions) + } + } + // multiple additional definitions + if (schemaDefinitions.definitions.size > 1) { + val root = schemaDefinitions.root + if (root.isReference() || root.isReferenceArray()) { + inlineRoot(schemaDefinitions) + } else if (root.isObjectArray()) { + unwrapRootArray(schemaDefinitions) + } else { + createInlineReference(schemaDefinitions) + } + } + } + } + + private fun inlineRoot(schemaDefinitions: SchemaDefinitions) { + /* + - root-schema: inline + - definitions: in components section + */ + addInline(TODO(), schemaDefinitions.root) + schemaDefinitions.definitions.forEach { (name, schema) -> + addToComponentsSection(name, schema) + } + } + + private fun inlineDefinition(schemaDefinitions: SchemaDefinitions) { + /* + - assumption: size(definitions) == 1 + - root-schema: discard + - definition: inline + */ + if(schemaDefinitions.definitions.size != 1) { + throw Exception("Unexpected amount of additional schema-definitions: ${schemaDefinitions.definitions.size}") + } + schemaDefinitions.definitions.entries.first() + .also { addInline(TODO(), it.value) } + } + + private fun createInlineReference(schemaDefinitions: SchemaDefinitions) { + /* + - root-schema: in components section + - definitions: in components section + - create inline ref to root + */ + schemaDefinitions.definitions.forEach { (name, schema) -> + addToComponentsSection(name, schema) + } + addToComponentsSection(TODO(), schemaDefinitions.root) + TODO("create inline ref to root") + } + + private fun unwrapRootArray(schemaDefinitions: SchemaDefinitions) { + /* + - assumption: root schema.type == array + - root-schema: unwrap + - item -> component section + - create inline array-ref to item + - definitions: in components section + */ + if(schemaDefinitions.root.items == null) { + throw Exception("Expected items for array-schema but items were 'null'.") + } + schemaDefinitions.definitions.forEach { (name, schema) -> + addToComponentsSection(name, schema) + } + addToComponentsSection(TODO(), schemaDefinitions.root.items) + addInline(TODO(), TODO("array-ref to item")) + } + + + private fun addToComponentsSection(name: String, schema: Schema<*>) { + componentsSection[name] = schema + } + + private fun addInline(type: SchemaType, schema: Schema<*>) { + inlineSchemas[type] = schema + } + + private fun addInline(ref: CustomSchemaRef, schema: Schema<*>) { + inlineSchemasCustom[ref] = schema + } + + + private fun Schema<*>.isPrimitive(): Boolean { + return type != "object" && type != "array" && type != null + } + + private fun Schema<*>.isPrimitiveArray(): Boolean { + return type == "array" && (items.isPrimitive() || items.isPrimitiveArray()) + } + + private fun Schema<*>.isObjectArray(): Boolean { + return type == "array" && !items.isPrimitive() && !items.isPrimitiveArray() + } + + private fun Schema<*>.isReference(): Boolean { + return type == null && `$ref` != null + } + + private fun Schema<*>.isReferenceArray(): Boolean { + return type == "array" && items.isReference() + } + +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt new file mode 100644 index 0000000..4ecef2e --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt @@ -0,0 +1,129 @@ +package io.github.smiley4.ktorswaggerui.spec.schemaV2 + +import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig +import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef +import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema +import io.github.smiley4.ktorswaggerui.dsl.CustomObjectSchemaRef +import io.github.smiley4.ktorswaggerui.dsl.CustomOpenApiSchema +import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef +import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody +import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter +import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse +import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody +import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema +import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta +import io.swagger.v3.oas.models.media.Schema + +class SchemaContextBuilder( + private val config: SwaggerUIPluginConfig, + private val schemaBuilder: SchemaBuilder +) { + + fun build(routes: Collection): SchemaContext { + return SchemaContext() + .also { ctx -> routes.forEach { handle(ctx, it) } } + .also { ctx -> ctx.finalize() } + } + + + private fun handle(ctx: SchemaContext, route: RouteMeta) { + route.documentation.getRequest().getBody()?.also { handle(ctx, it) } + route.documentation.getRequest().getParameters().forEach { handle(ctx, it) } + route.documentation.getResponses().getResponses().forEach { handle(ctx, it) } + } + + private fun handle(ctx: SchemaContext, response: OpenApiResponse) { + response.getHeaders().forEach { (_, header) -> + header.type?.also { headerType -> + ctx.addSchema(headerType, createSchema(headerType)) + } + } + response.getBody()?.also { handle(ctx, it) } + } + + + private fun handle(ctx: SchemaContext, body: OpenApiBaseBody) { + return when (body) { + is OpenApiSimpleBody -> handle(ctx, body) + is OpenApiMultipartBody -> handle(ctx, body) + } + } + + + private fun handle(ctx: SchemaContext, body: OpenApiSimpleBody) { + if (body.customSchema != null) { + body.customSchema?.also { ctx.addSchema(it, createSchema(it)) } + } else { + body.type?.also { ctx.addSchema(it, createSchema(it)) } + } + } + + + private fun handle(ctx: SchemaContext, body: OpenApiMultipartBody) { + body.getParts().forEach { part -> + if (part.customSchema != null) { + part.customSchema?.also { ctx.addSchema(it, createSchema(it)) } + } else { + part.type?.also { ctx.addSchema(it, createSchema(it)) } + } + } + } + + + private fun handle(ctx: SchemaContext, parameter: OpenApiRequestParameter) { + ctx.addSchema(parameter.type, createSchema(parameter.type)) + } + + + private fun createSchema(type: SchemaType): SchemaDefinitions { + return schemaBuilder.create(type) + } + + + private fun createSchema(customSchemaRef: CustomSchemaRef): SchemaDefinitions { + val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) + if (customSchema == null) { + return SchemaDefinitions( + root = Schema(), + definitions = emptyMap() + ) + } else { + return when (customSchema) { + is CustomJsonSchema -> { + schemaBuilder.create(customSchema.provider()) + } + is CustomOpenApiSchema -> { + SchemaDefinitions( + root = customSchema.provider(), // what if provided schema has definitions ? + definitions = emptyMap() + ) + } + is RemoteSchema -> { + SchemaDefinitions( + root = Schema().apply { + type = "object" + `$ref` = customSchema.url + }, + definitions = emptyMap() + ) + } + }.let { schemaDefinitions -> + when (customSchemaRef) { + is CustomObjectSchemaRef -> schemaDefinitions + is CustomArraySchemaRef -> { + SchemaDefinitions( + root = Schema().apply { + type = "array" + items = schemaDefinitions.root + }, + definitions = schemaDefinitions.definitions + ) + } + } + } + } + } + +} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt index 6c8c4f9..c8da31c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -2,7 +2,6 @@ package io.github.smiley4.ktorswaggerui.tests.openapi import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo import io.github.smiley4.ktorswaggerui.spec.openapi.ComponentsBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ContactBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder @@ -26,19 +25,15 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec -import io.kotest.engine.test.logging.info import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize import io.kotest.matchers.maps.shouldHaveSize -import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.swagger.v3.oas.models.OpenAPI -import io.swagger.v3.oas.models.info.Info import kotlin.reflect.jvm.javaType diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index 48526e6..36bace9 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -15,8 +15,7 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index 6d94180..903550e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -16,8 +16,7 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt index fae20af..57b1e7d 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt @@ -10,8 +10,8 @@ import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaSerializer import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaDefinitions import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.maps.shouldHaveSize diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index a1852fa..42a99da 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -2,13 +2,14 @@ package io.github.smiley4.ktorswaggerui.tests.schema import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo +import com.github.victools.jsonschema.generator.Option import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.dsl.asSchemaType import io.github.smiley4.ktorswaggerui.dsl.getSchemaType import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty @@ -329,7 +330,9 @@ class SchemaContextTest : StringSpec({ inline fun getType() = getSchemaType() - private val defaultPluginConfig = SwaggerUIPluginConfig() + private val defaultPluginConfig = SwaggerUIPluginConfig().also { + it.schemaGeneratorConfigBuilder = it.schemaGeneratorConfigBuilder.without(Option.DEFINITION_FOR_MAIN_SCHEMA) + } private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { return SchemaContext( @@ -339,8 +342,8 @@ class SchemaContextTest : StringSpec({ } ) } - private data class QueryParamType(val value: String) + private data class PathParamType(val value: String) private data class HeaderParamType(val value: String) private data class RequestBodyType(val value: String) From 3722cc07723347ef6ce1adcae4ac454ab9757b35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20R=C3=BCgner?= Date: Thu, 25 May 2023 13:46:12 +0200 Subject: [PATCH 23/27] fix/finalize schema context --- .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 9 +- .../spec/openapi/OpenApiBuilder.kt | 2 +- .../spec/schema/JsonSchemaBuilder.kt | 119 ------- .../{schemaV2 => schema}/SchemaBuilder.kt | 2 +- .../spec/schema/SchemaContext.kt | 305 +++++++++--------- .../SchemaContextBuilder.kt | 18 +- .../spec/schemaV2/SchemaContext.kt | 166 ---------- .../tests/openapi/OpenApiBuilderTest.kt | 11 +- .../tests/openapi/OperationBuilderTest.kt | 116 ++++--- .../tests/openapi/PathsBuilderTest.kt | 13 +- .../tests/schema/SchemaBuilderTest.kt | 4 +- .../tests/schema/SchemaContextTest.kt | 149 +++++++-- 12 files changed, 377 insertions(+), 537 deletions(-) delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt rename src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/{schemaV2 => schema}/SchemaBuilder.kt (98%) rename src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/{schemaV2 => schema}/SchemaContextBuilder.kt (81%) delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index d04fe61..ea6ebfc 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -5,8 +5,9 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.* import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder import io.ktor.server.application.* import io.ktor.server.application.hooks.* import io.ktor.server.routing.* @@ -47,12 +48,12 @@ private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig } private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { - return SchemaContext( + return SchemaContextBuilder( config = pluginConfig, - schemaBuilder = SchemaBuilder("\$defs") { type -> + schemaBuilder = SchemaBuilder("\$defs") { type -> // TODO: customizable SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() } - ).initialize(routes.toList()) + ).build(routes.toList()) } private fun builder(config: SwaggerUIPluginConfig, schemaContext: SchemaContext): OpenApiBuilder { diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt index 8575e54..722eab2 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/OpenApiBuilder.kt @@ -21,7 +21,7 @@ class OpenApiBuilder( it.servers = config.getServers().map { server -> serverBuilder.build(server) } it.tags = config.getTags().map { tag -> tagBuilder.build(tag) } it.paths = pathsBuilder.build(routes) - it.components = componentsBuilder.build(schemaContext.getComponentSection()) + it.components = componentsBuilder.build(schemaContext.getComponentsSection()) } } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt deleted file mode 100644 index a3fbb92..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaBuilder.kt +++ /dev/null @@ -1,119 +0,0 @@ -package io.github.smiley4.ktorswaggerui.spec.schema - -import com.fasterxml.jackson.databind.JsonNode -import com.fasterxml.jackson.databind.ObjectMapper -import com.fasterxml.jackson.databind.node.ArrayNode -import com.fasterxml.jackson.databind.node.ObjectNode -import com.fasterxml.jackson.databind.node.TextNode -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfig -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.github.smiley4.ktorswaggerui.dsl.getSimpleTypeName -import io.swagger.v3.core.util.Json -import io.swagger.v3.oas.models.media.Schema -import io.swagger.v3.oas.models.media.XML -import kotlin.reflect.javaType - - -class JsonSchemaBuilder( - private val pluginConfig: SwaggerUIPluginConfig, - schemaGeneratorConfig: SchemaGeneratorConfig -) { - - companion object { - - data class JsonSchemaInfo( - val rootSchema: String, - val schemas: Map - ) - - data class OpenApiSchemaInfo( - val rootSchema: String, - val schemas: Map> - ) - - } - - private val generator = SchemaGenerator(schemaGeneratorConfig) - - fun build(type: SchemaType): OpenApiSchemaInfo { - return type - .let { buildJsonSchema(it) } - .let { build(it, type.getSimpleTypeName()) } - } - - fun build(schema: JsonNode, typeName: String): OpenApiSchemaInfo { - return schema - .let { processJsonSchema(it, typeName) } - .let { buildOpenApiSchema(it) } - } - - - @OptIn(ExperimentalStdlibApi::class) - private fun buildJsonSchema(type: SchemaType): JsonNode { - return pluginConfig.serializationConfig.getCustomSchemaSerializer() - .let { customSerializer -> customSerializer(type) } - ?.let { ObjectMapper().readTree(it) } - ?: generator.generateSchema(type.javaType) - } - - private fun processJsonSchema(json: JsonNode, typeName: String): JsonSchemaInfo { - if (json is ObjectNode && hasDefinitions(json)) { - val mainDefinition = getMainDefinition(json) - val definitions = getDefinitions(json) - definitions.forEach { cleanupRefPaths(it.second) } - return JsonSchemaInfo( - rootSchema = mainDefinition, - schemas = definitions.associate { it } - ) - } else { - return JsonSchemaInfo( - rootSchema = typeName, - schemas = mapOf(typeName to json) - ) - } - } - - private fun hasDefinitions(json: JsonNode) = json.get("\$defs") != null || json.get("definitions") != null - - private fun getDefinitions(json: JsonNode): List> { - return (json.get("\$defs") ?: json["definitions"]).fields().asSequence().map { it.key to it.value }.toList() - } - - private fun getMainDefinition(json: JsonNode) = - json.get("\$ref").asText() - .replace("#/definitions/", "") - .replace("#/\$defs/", "") - - private fun cleanupRefPaths(node: JsonNode) { - when (node) { - is ObjectNode -> { - node.get("\$ref")?.also { - node.set("\$ref", TextNode(it.asText().replace("#/\$defs/", ""))) - } - node.elements().asSequence().forEach { cleanupRefPaths(it) } - } - - is ArrayNode -> { - node.elements().asSequence().forEach { cleanupRefPaths(it) } - } - } - } - - private fun buildOpenApiSchema(json: JsonSchemaInfo): OpenApiSchemaInfo { - return OpenApiSchemaInfo( - rootSchema = json.rootSchema, - schemas = json.schemas.mapValues { (name, schema) -> buildOpenApiSchema(schema, name) } - ) - } - - private fun buildOpenApiSchema(json: JsonNode, name: String): Schema<*> { - return Json.mapper().readValue(json.toString(), Schema::class.java).also { schema -> - schema.xml = XML().also { - it.name = name - } - } - } - -} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt similarity index 98% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt index 4eb93fe..9126fab 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt @@ -1,4 +1,4 @@ -package io.github.smiley4.ktorswaggerui.spec.schemaV2 +package io.github.smiley4.ktorswaggerui.spec.schema import com.fasterxml.jackson.databind.JsonNode import com.fasterxml.jackson.databind.ObjectMapper diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt index 6504612..ba54dd4 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContext.kt @@ -1,203 +1,218 @@ package io.github.smiley4.ktorswaggerui.spec.schema -import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomObjectSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomOpenApiSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.github.smiley4.ktorswaggerui.dsl.getTypeName -import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaDefinitions +import io.github.smiley4.ktorswaggerui.dsl.* import io.swagger.v3.oas.models.media.Schema import kotlin.collections.component1 import kotlin.collections.component2 import kotlin.collections.set -class SchemaContext( - private val config: SwaggerUIPluginConfig, - private val schemaBuilder: SchemaBuilder -) { - private val schemas = mutableMapOf() - private val customSchemas = mutableMapOf>() +class SchemaContext { + companion object { - fun initialize(routes: Collection): SchemaContext { - routes.forEach { handle(it) } - config.getDefaultUnauthorizedResponse()?.also { handle(it) } - return this - } - + private data class SchemaKeyWrapper( + val type: SchemaType, + val schemaId: String, + val isCustom: Boolean + ) { - private fun handle(route: RouteMeta) { - route.documentation.getRequest().getBody()?.also { handle(it) } - route.documentation.getRequest().getParameters().forEach { handle(it) } - route.documentation.getResponses().getResponses().forEach { handle(it) } - } + companion object { + val PLACEHOLDER_TYPE = getSchemaType() + const val PLACEHOLDER_SCHEMAID = "" + fun type(type: SchemaType) = SchemaKeyWrapper( + type = type, + schemaId = PLACEHOLDER_SCHEMAID, + isCustom = false + ) - private fun handle(response: OpenApiResponse) { - response.getHeaders().forEach { (_, header) -> - header.type?.also { headerType -> - createSchema(headerType) + fun custom(schemaId: String) = SchemaKeyWrapper( + type = PLACEHOLDER_TYPE, + schemaId = schemaId, + isCustom = true + ) } } - response.getBody()?.also { handle(it) } } + private val schemas = mutableMapOf() + private val schemasCustom = mutableMapOf() - private fun handle(body: OpenApiBaseBody) { - return when (body) { - is OpenApiSimpleBody -> handle(body) - is OpenApiMultipartBody -> handle(body) - } - } + private val componentsSection = mutableMapOf>() + private val inlineSchemas = mutableMapOf>() + private val inlineSchemasCustom = mutableMapOf>() - private fun handle(body: OpenApiSimpleBody) { - if (body.customSchema != null) { - body.customSchema?.also { createSchema(it) } - } else { - body.type?.also { createSchema(it) } - } + fun addSchema(type: SchemaType, schema: SchemaDefinitions) { + schemas[type] = schema } - private fun handle(body: OpenApiMultipartBody) { - body.getParts().forEach { part -> - if (part.customSchema != null) { - part.customSchema?.also { createSchema(it) } - } else { - part.type?.also { createSchema(it) } - } - } + fun addSchema(ref: CustomSchemaRef, schema: SchemaDefinitions) { + schemasCustom[ref.schemaId] = schema } - private fun handle(parameter: OpenApiRequestParameter) { - createSchema(parameter.type) - } + fun getComponentsSection(): Map> = componentsSection - private fun createSchema(type: SchemaType) { - if (schemas.containsKey(type.getTypeName())) { - return - } - addSchema(type, schemaBuilder.create(type)) - } + fun getSchema(type: SchemaType) = inlineSchemas[type] ?: throw Exception("No schema for type '$type'!") - private fun createSchema(customSchemaRef: CustomSchemaRef) { - if (customSchemas.containsKey(customSchemaRef.schemaId)) { - return - } - val customSchema = config.getCustomSchemas().getSchema(customSchemaRef.schemaId) - if (customSchema == null) { - addSchema(customSchemaRef, Schema()) - } else { - when (customSchema) { - is CustomJsonSchema -> { - schemaBuilder.create(customSchema.provider()).root - } - is CustomOpenApiSchema -> { - customSchema.provider() - } - is RemoteSchema -> { - Schema().apply { - type = "object" - `$ref` = customSchema.url - } - } - }.let { schema -> - when (customSchemaRef) { - is CustomObjectSchemaRef -> schema - is CustomArraySchemaRef -> Schema().apply { - this.type = "array" - this.items = schema - } - } - }.also { - addSchema(customSchemaRef, it) - } - } - } + fun getSchema(ref: CustomSchemaRef) = inlineSchemasCustom[ref.schemaId] ?: throw Exception("No schema for ref '$ref'!") - fun addSchema(type: SchemaType, schema: SchemaDefinitions) { - schemas[type.getTypeName()] = schema - } - fun addSchema(customSchemaRef: CustomSchemaRef, schema: Schema<*>) { - customSchemas[customSchemaRef.schemaId] = schema + fun finalize() { + schemas.forEach { (type, schemaDefinitions) -> + finalize(SchemaKeyWrapper.type(type), schemaDefinitions) + } + schemasCustom.forEach { (schemaId, schemaDefinitions) -> + finalize(SchemaKeyWrapper.custom(schemaId), schemaDefinitions) + } } - fun getComponentSection(): Map> { - val componentSection = mutableMapOf>() - schemas.forEach { (_, schemaDefinitions) -> - val rootSchema = schemaDefinitions.root - if (isPrimitive(rootSchema) || isPrimitiveArray(rootSchema)) { - // skip + private fun finalize(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { + // only root definition + if (schemaDefinitions.definitions.isEmpty()) { + val root = schemaDefinitions.root + if (root.isPrimitive() || root.isPrimitiveArray()) { + inlineRoot(key, schemaDefinitions) + } else if (root.isObjectArray()) { + unwrapRootArray(key, schemaDefinitions) } else { - componentSection.putAll(schemaDefinitions.definitions) + createInlineReference(key, schemaDefinitions) } } - customSchemas.forEach { (schemaId, schema) -> - componentSection[schemaId] = schema + // only one additional definition + if (schemaDefinitions.definitions.size == 1) { + val root = schemaDefinitions.root + val definition = schemaDefinitions.definitions.entries.first().value + if (root.isReference() && (definition.isPrimitive() || definition.isPrimitiveArray())) { + inlineSingleDefinition(key, schemaDefinitions) + } else if (root.isReference() || root.isReferenceArray()) { + inlineRoot(key, schemaDefinitions) + } else if (root.isObjectArray()) { + unwrapRootArray(key, schemaDefinitions) + } else { + createInlineReference(key, schemaDefinitions) + } + } + // multiple additional definitions + if (schemaDefinitions.definitions.size > 1) { + val root = schemaDefinitions.root + if (root.isReference() || root.isReferenceArray()) { + inlineRoot(key, schemaDefinitions) + } else if (root.isObjectArray()) { + unwrapRootArray(key, schemaDefinitions) + } else { + createInlineReference(key, schemaDefinitions) + } } - return componentSection - } - - - fun getSchema(customSchemaRef: CustomSchemaRef): Schema<*> { - val schema = customSchemas[customSchemaRef.schemaId] - ?: throw IllegalStateException("Could not retrieve schema for type '${customSchemaRef.schemaId}'") - return buildInlineSchema(customSchemaRef.schemaId, schema) } - - fun getSchema(type: SchemaType): Schema<*> { - return getSchemaDefinitions(type).root + private fun inlineRoot(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { + /* + - root-schema: inline + - definitions: in components section + */ + addInline(key, schemaDefinitions.root) + schemaDefinitions.definitions.forEach { (name, schema) -> + addToComponentsSection(name, schema) + } } - - private fun buildInlineSchema(schemaId: String, schema: Schema<*>): Schema<*> { - if (isPrimitive(schema)) { - return schema + private fun inlineSingleDefinition(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { + /* + - assumption: size(definitions) == 1 + - root-schema: discard + - definition: inline + */ + if (schemaDefinitions.definitions.size != 1) { + throw Exception("Unexpected amount of additional schema-definitions: ${schemaDefinitions.definitions.size}") } - if (isPrimitiveArray(schema)) { - return schema + schemaDefinitions.definitions.entries.first() + .also { addInline(key, it.value) } + } + + private fun createInlineReference(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { + /* + - root-schema: in components section + - definitions: in components section + - create inline ref to root + */ + schemaDefinitions.definitions.forEach { (name, schema) -> + addToComponentsSection(name, schema) } - return Schema().also { - it.`$ref` = "#/components/schemas/$schemaId" + val rootName = schemaName(key) + addToComponentsSection(rootName, schemaDefinitions.root) + addInline(key, Schema().also { + it.`$ref` = "#/components/schemas/$rootName" + }) + } + + private fun unwrapRootArray(key: SchemaKeyWrapper, schemaDefinitions: SchemaDefinitions) { + /* + - assumption: root schema.type == array + - root-schema: unwrap + - item -> component section + - create inline array-ref to item + - definitions: in components section + */ + if (schemaDefinitions.root.items == null) { + throw Exception("Expected items for array-schema but items were 'null'.") } + schemaDefinitions.definitions.forEach { (name, schema) -> + addToComponentsSection(name, schema) + } + val rootName = schemaName(key) + addToComponentsSection(rootName, schemaDefinitions.root.items) + addInline(key, Schema().also { array -> + array.type = "array" + array.items = Schema().also { item -> + item.`$ref` = "#/components/schemas/$rootName" + } + }) } + private fun schemaName(key: SchemaKeyWrapper): String { + return if (key.isCustom) { + key.schemaId + } else { + key.type.getSimpleTypeName() + } + } - private fun getSchemaDefinitions(type: SchemaType): SchemaDefinitions { - return type.getTypeName().let { typeName -> - schemas[typeName] ?: throw IllegalStateException("Could not retrieve schema for type '${typeName}'") + private fun addToComponentsSection(name: String, schema: Schema<*>) { + componentsSection[name] = schema + } + + private fun addInline(key: SchemaKeyWrapper, schema: Schema<*>) { + if (key.isCustom) { + inlineSchemasCustom[key.schemaId] = schema + } else { + inlineSchemas[key.type] = schema } } + private fun Schema<*>.isPrimitive(): Boolean { + return type != "object" && type != "array" && type != null + } + + private fun Schema<*>.isPrimitiveArray(): Boolean { + return type == "array" && (items.isPrimitive() || items.isPrimitiveArray()) + } - private fun isPrimitive(schema: Schema<*>): Boolean { - return schema.type != "object" && schema.type != "array" && schema.type != null + private fun Schema<*>.isObjectArray(): Boolean { + return type == "array" && !items.isPrimitive() && !items.isPrimitiveArray() } - private fun isPrimitiveArray(schema: Schema<*>): Boolean { - return schema.type == "array" && (isPrimitive(schema.items) || isPrimitiveArray(schema.items)) + private fun Schema<*>.isReference(): Boolean { + return type == null && `$ref` != null } - private fun isReference(schema: Schema<*>): Boolean { - return schema.type == null && schema.`$ref` != null + private fun Schema<*>.isReferenceArray(): Boolean { + return type == "array" && items.isReference() } -} +} \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContextBuilder.kt similarity index 81% rename from src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt rename to src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContextBuilder.kt index 4ecef2e..8e7d21a 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContextBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaContextBuilder.kt @@ -1,18 +1,7 @@ -package io.github.smiley4.ktorswaggerui.spec.schemaV2 +package io.github.smiley4.ktorswaggerui.spec.schema import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.CustomArraySchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomJsonSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomObjectSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.CustomOpenApiSchema -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.OpenApiBaseBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiMultipartBody -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRequestParameter -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSimpleBody -import io.github.smiley4.ktorswaggerui.dsl.RemoteSchema -import io.github.smiley4.ktorswaggerui.dsl.SchemaType +import io.github.smiley4.ktorswaggerui.dsl.* import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.swagger.v3.oas.models.media.Schema @@ -96,7 +85,8 @@ class SchemaContextBuilder( } is CustomOpenApiSchema -> { SchemaDefinitions( - root = customSchema.provider(), // what if provided schema has definitions ? + // provided schema should not have a 'definitions'-section, i.e. schema should be inline-able as is. + root = customSchema.provider(), definitions = emptyMap() ) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt deleted file mode 100644 index 090447f..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schemaV2/SchemaContext.kt +++ /dev/null @@ -1,166 +0,0 @@ -package io.github.smiley4.ktorswaggerui.spec.schemaV2 - -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef -import io.github.smiley4.ktorswaggerui.dsl.SchemaType -import io.swagger.v3.oas.models.media.Schema - -class SchemaContext { - - private val schemas = mutableMapOf() - private val schemasCustom = mutableMapOf() - - private val componentsSection = mutableMapOf>() - private val inlineSchemas = mutableMapOf>() - private val inlineSchemasCustom = mutableMapOf>() - - - fun addSchema(type: SchemaType, schema: SchemaDefinitions) { - schemas[type] = schema - } - - - fun addSchema(ref: CustomSchemaRef, schema: SchemaDefinitions) { - schemasCustom[ref] = schema - } - - - fun getComponentsSection(): Map> = componentsSection - - - fun getSchema(type: SchemaType) = inlineSchemas[type] ?: throw Exception("No schema for type '$type'!") - - - fun getSchema(ref: CustomSchemaRef) = inlineSchemasCustom[ref] ?: throw Exception("No schema for ref '$ref'!") - - - fun finalize() { - schemas.forEach { (type, schemaDefinitions) -> - // only root definition - if (schemaDefinitions.definitions.isEmpty()) { - val root = schemaDefinitions.root - if (root.isPrimitive() || root.isPrimitiveArray()) { - inlineRoot(schemaDefinitions) - } else if (root.isObjectArray()) { - unwrapRootArray(schemaDefinitions) - } else { - createInlineReference(schemaDefinitions) - } - } - // only one additional definition - if (schemaDefinitions.definitions.size == 1) { - val root = schemaDefinitions.root - val definition = schemaDefinitions.definitions.entries.first().value - if (root.isReference() && (definition.isPrimitive() || definition.isPrimitiveArray())) { - inlineDefinition(schemaDefinitions) - } else if (root.isReference() || root.isReferenceArray()) { - inlineRoot(schemaDefinitions) - } else if (root.isObjectArray()) { - unwrapRootArray(schemaDefinitions) - } else { - createInlineReference(schemaDefinitions) - } - } - // multiple additional definitions - if (schemaDefinitions.definitions.size > 1) { - val root = schemaDefinitions.root - if (root.isReference() || root.isReferenceArray()) { - inlineRoot(schemaDefinitions) - } else if (root.isObjectArray()) { - unwrapRootArray(schemaDefinitions) - } else { - createInlineReference(schemaDefinitions) - } - } - } - } - - private fun inlineRoot(schemaDefinitions: SchemaDefinitions) { - /* - - root-schema: inline - - definitions: in components section - */ - addInline(TODO(), schemaDefinitions.root) - schemaDefinitions.definitions.forEach { (name, schema) -> - addToComponentsSection(name, schema) - } - } - - private fun inlineDefinition(schemaDefinitions: SchemaDefinitions) { - /* - - assumption: size(definitions) == 1 - - root-schema: discard - - definition: inline - */ - if(schemaDefinitions.definitions.size != 1) { - throw Exception("Unexpected amount of additional schema-definitions: ${schemaDefinitions.definitions.size}") - } - schemaDefinitions.definitions.entries.first() - .also { addInline(TODO(), it.value) } - } - - private fun createInlineReference(schemaDefinitions: SchemaDefinitions) { - /* - - root-schema: in components section - - definitions: in components section - - create inline ref to root - */ - schemaDefinitions.definitions.forEach { (name, schema) -> - addToComponentsSection(name, schema) - } - addToComponentsSection(TODO(), schemaDefinitions.root) - TODO("create inline ref to root") - } - - private fun unwrapRootArray(schemaDefinitions: SchemaDefinitions) { - /* - - assumption: root schema.type == array - - root-schema: unwrap - - item -> component section - - create inline array-ref to item - - definitions: in components section - */ - if(schemaDefinitions.root.items == null) { - throw Exception("Expected items for array-schema but items were 'null'.") - } - schemaDefinitions.definitions.forEach { (name, schema) -> - addToComponentsSection(name, schema) - } - addToComponentsSection(TODO(), schemaDefinitions.root.items) - addInline(TODO(), TODO("array-ref to item")) - } - - - private fun addToComponentsSection(name: String, schema: Schema<*>) { - componentsSection[name] = schema - } - - private fun addInline(type: SchemaType, schema: Schema<*>) { - inlineSchemas[type] = schema - } - - private fun addInline(ref: CustomSchemaRef, schema: Schema<*>) { - inlineSchemasCustom[ref] = schema - } - - - private fun Schema<*>.isPrimitive(): Boolean { - return type != "object" && type != "array" && type != null - } - - private fun Schema<*>.isPrimitiveArray(): Boolean { - return type == "array" && (items.isPrimitive() || items.isPrimitiveArray()) - } - - private fun Schema<*>.isObjectArray(): Boolean { - return type == "array" && !items.isPrimitive() && !items.isPrimitiveArray() - } - - private fun Schema<*>.isReference(): Boolean { - return type == null && `$ref` != null - } - - private fun Schema<*>.isReferenceArray(): Boolean { - return type == "array" && items.isReference() - } - -} \ No newline at end of file diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt index c8da31c..3780a1e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -25,8 +25,9 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.SecuritySchemesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ServerBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.TagBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.collections.shouldHaveSize @@ -96,17 +97,17 @@ class OpenApiBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() - private fun schemaContext(pluginConfig: SwaggerUIPluginConfig): SchemaContext { - return SchemaContext( + private fun schemaContext(routes: List, pluginConfig: SwaggerUIPluginConfig): SchemaContext { + return SchemaContextBuilder( config = pluginConfig, schemaBuilder = SchemaBuilder("\$defs") { type -> SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() } - ) + ).build(routes) } private fun buildOpenApiObject(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): OpenAPI { - val schemaContext = schemaContext(pluginConfig).initialize(routes) + val schemaContext = schemaContext(routes, pluginConfig) return OpenApiBuilder( config = pluginConfig, schemaContext = schemaContext, diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index 36bace9..cdbc36b 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -4,19 +4,12 @@ import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute import io.github.smiley4.ktorswaggerui.dsl.obj -import io.github.smiley4.ktorswaggerui.spec.openapi.ContentBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ExampleBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.HeaderBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.OperationTagsBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ParameterBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.RequestBodyBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder -import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder +import io.github.smiley4.ktorswaggerui.spec.openapi.* import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -25,9 +18,7 @@ import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.maps.shouldHaveSize import io.kotest.matchers.nulls.shouldNotBeNull import io.kotest.matchers.shouldBe -import io.ktor.http.ContentType -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode +import io.ktor.http.* import io.swagger.v3.oas.models.Operation import io.swagger.v3.oas.models.media.Schema import java.io.File @@ -42,7 +33,7 @@ class OperationBuilderTest : StringSpec({ documentation = OpenApiRoute(), protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.tags.shouldBeEmpty() operation.summary shouldBe null @@ -72,7 +63,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.tags shouldContainExactlyInAnyOrder listOf("tag1", "tag2") operation.summary shouldBe "this is some test route" @@ -109,7 +100,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(routeA, routeB)) + val schemaContext = schemaContext(listOf(routeA, routeB)) buildOperationObject(routeA, schemaContext, config).also { operation -> operation.tags shouldContainExactlyInAnyOrder listOf("a", "defaultTag") } @@ -127,7 +118,7 @@ class OperationBuilderTest : StringSpec({ }, protected = true ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.security .also { it.shouldNotBeNull() } @@ -146,7 +137,7 @@ class OperationBuilderTest : StringSpec({ documentation = OpenApiRoute(), protected = true ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.tags.shouldBeEmpty() operation.summary shouldBe null @@ -172,7 +163,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.tags.shouldBeEmpty() operation.summary shouldBe null @@ -203,7 +194,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.tags.shouldBeEmpty() operation.summary shouldBe null @@ -325,7 +316,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.tags.shouldBeEmpty() operation.summary shouldBe null @@ -397,7 +388,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.parameters.also { parameters -> parameters shouldHaveSize 1 @@ -443,7 +434,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.requestBody .also { it.shouldNotBeNull() } @@ -532,7 +523,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.requestBody .also { it.shouldNotBeNull() } @@ -548,7 +539,10 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { schema -> schema.type shouldBe "object" - schema.properties.keys shouldContainExactlyInAnyOrder listOf("image", "data") + schema.properties.keys shouldContainExactlyInAnyOrder listOf( + "image", + "data" + ) } mediaType.example shouldBe null @@ -595,7 +589,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.requestBody .also { it.shouldNotBeNull() } @@ -641,7 +635,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.responses .also { it shouldHaveSize 4 } @@ -680,7 +674,7 @@ class OperationBuilderTest : StringSpec({ }, protected = true ) - val schemaContext = schemaContext(config).initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route), config) buildOperationObject(route, schemaContext, config).also { operation -> operation.responses .also { it shouldHaveSize 2 } @@ -713,7 +707,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext(config).initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route), config) buildOperationObject(route, schemaContext, config).also { operation -> operation.responses .also { it shouldHaveSize 1 } @@ -736,7 +730,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) + val schemaContext = schemaContext(listOf(route)) buildOperationObject(route, schemaContext).also { operation -> operation.requestBody .also { it.shouldNotBeNull() } @@ -771,7 +765,7 @@ class OperationBuilderTest : StringSpec({ body.`$ref` shouldBe null } } - schemaContext.getComponentSection().also { section -> + schemaContext.getComponentsSection().also { section -> section.keys shouldContainExactlyInAnyOrder listOf("SimpleObject") section["SimpleObject"]?.also { schema -> schema.type shouldBe "object" @@ -781,6 +775,20 @@ class OperationBuilderTest : StringSpec({ } "custom body schema" { + val config = SwaggerUIPluginConfig().also { + it.schemas { + openApi("myCustomSchema") { + Schema().also { schema -> + schema.type = "object" + schema.properties = mapOf( + "custom" to Schema().also { prop -> + prop.type = "string" + } + ) + } + } + } + } val route = RouteMeta( path = "/test", method = HttpMethod.Get, @@ -791,10 +799,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) - schemaContext.addSchema(obj("myCustomSchema"), Schema().also { - it.type = "custom_type" - }) + val schemaContext = schemaContext(listOf(route), config) buildOperationObject(route, schemaContext).also { operation -> operation.requestBody .also { it.shouldNotBeNull() } @@ -804,12 +809,12 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 1 - content.get("text/plain") + content["application/json"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema .also { it.shouldNotBeNull() } - ?.also { schema -> schema.type shouldBe "custom_type" } + ?.also { schema -> schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" } mediaType.example shouldBe null mediaType.examples shouldBe null mediaType.encoding shouldBe null @@ -826,6 +831,20 @@ class OperationBuilderTest : StringSpec({ } "custom multipart-body schema" { + val config = SwaggerUIPluginConfig().also { + it.schemas { + openApi("myCustomSchema") { + Schema().also { schema -> + schema.type = "object" + schema.properties = mapOf( + "custom" to Schema().also { prop -> + prop.type = "string" + } + ) + } + } + } + } val route = RouteMeta( path = "/test", method = HttpMethod.Get, @@ -839,10 +858,7 @@ class OperationBuilderTest : StringSpec({ }, protected = false ) - val schemaContext = schemaContext().initialize(listOf(route)) - schemaContext.addSchema(obj("myCustomSchema"), Schema().also { - it.type = "custom_type" - }) + val schemaContext = schemaContext(listOf(route), config) buildOperationObject(route, schemaContext).also { operation -> operation.requestBody .also { it.shouldNotBeNull() } @@ -851,7 +867,7 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 1 - content.get("multipart/form-data") + content["multipart/form-data"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -859,7 +875,7 @@ class OperationBuilderTest : StringSpec({ ?.also { schema -> schema.type shouldBe "object" schema.properties.keys shouldContainExactlyInAnyOrder listOf("customData") - schema.properties["customData"]!!.type shouldBe "custom_type" + schema.properties["customData"]!!.`$ref` shouldBe "#/components/schemas/myCustomSchema" } mediaType.example shouldBe null mediaType.examples shouldBe null @@ -883,13 +899,17 @@ class OperationBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() - private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext( + private fun schemaContext( + routes: List, + pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + ): SchemaContext { + return SchemaContextBuilder( config = pluginConfig, schemaBuilder = SchemaBuilder("\$defs") { type -> - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType) + .toString() } - ) + ).build(routes) } private fun buildOperationObject( diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index 903550e..b67d65a 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -16,8 +16,9 @@ import io.github.smiley4.ktorswaggerui.spec.openapi.ResponseBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.ResponsesBuilder import io.github.smiley4.ktorswaggerui.spec.openapi.SecurityRequirementsBuilder import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldHaveSize @@ -34,7 +35,7 @@ class PathsBuilderTest : StringSpec({ route(HttpMethod.Delete, "/test/path"), route(HttpMethod.Post, "/other/test/route") ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) buildPathsObject(routes, schemaContext).also { paths -> paths shouldHaveSize 3 paths.keys shouldContainExactlyInAnyOrder listOf( @@ -59,7 +60,7 @@ class PathsBuilderTest : StringSpec({ route(HttpMethod.Get, "/test/path"), route(HttpMethod.Post, "/test/path"), ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) buildPathsObject(routes, schemaContext, config).also { paths -> paths shouldHaveSize 2 paths.keys shouldContainExactlyInAnyOrder listOf( @@ -85,13 +86,13 @@ class PathsBuilderTest : StringSpec({ private val defaultPluginConfig = SwaggerUIPluginConfig() - private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext( + private fun schemaContext(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { + return SchemaContextBuilder( config = pluginConfig, schemaBuilder = SchemaBuilder("\$defs") { type -> SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() } - ) + ).build(routes) } private fun buildPathsObject( diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt index 57b1e7d..fae20af 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt @@ -10,8 +10,8 @@ import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaSerializer import io.github.smiley4.ktorswaggerui.dsl.getSchemaType -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaDefinitions +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldContainExactly import io.kotest.matchers.maps.shouldHaveSize diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index 42a99da..a41c90f 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -5,18 +5,19 @@ import com.fasterxml.jackson.annotation.JsonTypeInfo import com.github.victools.jsonschema.generator.Option import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUIPluginConfig -import io.github.smiley4.ktorswaggerui.dsl.OpenApiRoute -import io.github.smiley4.ktorswaggerui.dsl.asSchemaType -import io.github.smiley4.ktorswaggerui.dsl.getSchemaType +import io.github.smiley4.ktorswaggerui.dsl.* import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta -import io.github.smiley4.ktorswaggerui.spec.schemaV2.SchemaBuilder +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext +import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder import io.kotest.matchers.maps.shouldBeEmpty import io.kotest.matchers.shouldBe +import io.kotest.matchers.shouldNotBe import io.ktor.http.* +import io.swagger.v3.oas.models.media.Schema import kotlin.reflect.jvm.javaType class SchemaContextTest : StringSpec({ @@ -43,7 +44,7 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(QueryParamType::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/QueryParamType" @@ -68,7 +69,7 @@ class SchemaContextTest : StringSpec({ schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/ResponseBodyType" } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.keys shouldContainExactlyInAnyOrder listOf( "QueryParamType", "PathParamType", @@ -97,13 +98,13 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(Integer::class.asSchemaType()).also { schema -> schema.type shouldBe "integer" schema.format shouldBe "int32" schema.`$ref` shouldBe null } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.shouldBeEmpty() } } @@ -121,7 +122,7 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(getType>()).also { schema -> schema.type shouldBe "array" schema.`$ref` shouldBe null @@ -129,7 +130,7 @@ class SchemaContextTest : StringSpec({ item.type shouldBe "string" } } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.shouldBeEmpty() } } @@ -147,7 +148,7 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(getType>>>()).also { schema -> schema.type shouldBe "array" schema.`$ref` shouldBe null @@ -163,7 +164,7 @@ class SchemaContextTest : StringSpec({ } } } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.shouldBeEmpty() } } @@ -181,12 +182,12 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(SimpleDataClass::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/SimpleDataClass" } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass") components["SimpleDataClass"]?.also { schema -> schema.type shouldBe "object" @@ -208,7 +209,7 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(getType>()).also { schema -> schema.type shouldBe "array" schema.`$ref` shouldBe null @@ -217,7 +218,7 @@ class SchemaContextTest : StringSpec({ item.`$ref` shouldBe "#/components/schemas/SimpleDataClass" } } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass") components["SimpleDataClass"]?.also { schema -> schema.type shouldBe "object" @@ -239,12 +240,12 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(DataWrapper::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/DataWrapper" } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.keys shouldContainExactlyInAnyOrder listOf("SimpleDataClass", "DataWrapper") components["SimpleDataClass"]?.also { schema -> schema.type shouldBe "object" @@ -274,13 +275,13 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(SimpleEnum::class.asSchemaType()).also { schema -> schema.type shouldBe "string" schema.enum shouldContainExactlyInAnyOrder SimpleEnum.values().map { it.name } schema.`$ref` shouldBe null } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.keys.shouldBeEmpty() } } @@ -298,12 +299,12 @@ class SchemaContextTest : StringSpec({ protected = false ) ) - val schemaContext = schemaContext().initialize(routes) + val schemaContext = schemaContext(routes) schemaContext.getSchema(DataClassWithMaps::class.asSchemaType()).also { schema -> schema.type shouldBe null schema.`$ref` shouldBe "#/components/schemas/DataClassWithMaps" } - schemaContext.getComponentSection().also { components -> + schemaContext.getComponentsSection().also { components -> components.keys shouldContainExactlyInAnyOrder listOf( "DataClassWithMaps", "Map(String,Long)", @@ -324,6 +325,97 @@ class SchemaContextTest : StringSpec({ } } + "custom schema object" { + val config = SwaggerUIPluginConfig().also { + it.schemas { + openApi("myCustomSchema") { + Schema().also { schema -> + schema.type = "object" + schema.properties = mapOf( + "custom" to Schema().also { prop -> + prop.type = "string" + } + ) + } + } + } + } + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body(obj("myCustomSchema")) + } + }, + protected = false + ) + ) + val schemaContext = schemaContext(routes, config) + schemaContext.getSchema(obj("myCustomSchema")).also { schema -> + schema.type shouldBe null + schema.`$ref` shouldBe "#/components/schemas/myCustomSchema" + } + schemaContext.getComponentsSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf( + "myCustomSchema", + ) + components["myCustomSchema"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("custom") + } + } + } + + "custom schema array" { + val config = SwaggerUIPluginConfig().also { + it.schemas { + openApi("myCustomSchema") { + Schema().also { schema -> + schema.type = "object" + schema.properties = mapOf( + "custom" to Schema().also { prop -> + prop.type = "string" + } + ) + } + } + } + } + val routes = listOf( + RouteMeta( + path = "/test", + method = HttpMethod.Get, + documentation = OpenApiRoute().apply { + request { + body(array("myCustomSchema")) + } + }, + protected = false + ) + ) + val schemaContext = schemaContext(routes, config) + schemaContext.getSchema(array("myCustomSchema")).also { schema -> + schema.type shouldBe "array" + schema.`$ref` shouldBe null + schema.items + .also { it shouldNotBe null } + ?.also { items -> + items.`$ref` shouldBe "#/components/schemas/myCustomSchema" + } + } + schemaContext.getComponentsSection().also { components -> + components.keys shouldContainExactlyInAnyOrder listOf( + "myCustomSchema", + ) + components["myCustomSchema"]?.also { schema -> + schema.type shouldBe "object" + schema.properties.keys shouldContainExactlyInAnyOrder listOf("custom") + } + } + } + }) { companion object { @@ -334,14 +426,19 @@ class SchemaContextTest : StringSpec({ it.schemaGeneratorConfigBuilder = it.schemaGeneratorConfigBuilder.without(Option.DEFINITION_FOR_MAIN_SCHEMA) } - private fun schemaContext(pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { - return SchemaContext( + private fun schemaContext( + routes: Collection, + pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig + ): SchemaContext { + return SchemaContextBuilder( config = pluginConfig, schemaBuilder = SchemaBuilder("\$defs") { type -> - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() + SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType) + .toString() } - ) + ).build(routes) } + private data class QueryParamType(val value: String) private data class PathParamType(val value: String) From ea7a05fa2ba988ba30284a5811de9c7b7c46919e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20R=C3=BCgner?= Date: Thu, 25 May 2023 14:51:25 +0200 Subject: [PATCH 24/27] refine serializing/encoding config --- .../ktorswaggerui/dsl/EncodingConfig.kt | 44 +++++++++++++++++++ .../ktorswaggerui/dsl/SerializationConfig.kt | 40 ----------------- .../spec/schema/JsonSchemaConfig.kt | 44 ------------------- 3 files changed, 44 insertions(+), 84 deletions(-) create mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt delete mode 100644 src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt new file mode 100644 index 0000000..4d9ed5f --- /dev/null +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt @@ -0,0 +1,44 @@ +package io.github.smiley4.ktorswaggerui.dsl + + +typealias ExampleSerializer = (type: SchemaType?, example: Any) -> String? + +typealias SchemaSerializer = (type: SchemaType) -> String? + +/** + * Configuration for serializing examples, schemas, ... + */ +@OpenApiDslMarker +class SerializationConfig { + + /** + * Serialize the given example object into a json-string. + */ + fun exampleSerializer(serializer: ExampleSerializer) { + exampleSerializer = serializer + } + + private var exampleSerializer: ExampleSerializer = { _, _ -> null } + + fun getExampleSerializer() = exampleSerializer + + + /** + * Serialize the given type into a valid json-schema. + * This serializer does not affect custom-schemas provided in the plugin-config. + */ + fun schemaSerializer(serializer: SchemaSerializer) { + schemaSerializer = serializer + } + + private var schemaSerializer: SchemaSerializer = { null } + + fun getSchemaSerializer() = schemaSerializer + + /** + * + */ + var schemaDefinitionsField = "\$defs" + +} + diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt deleted file mode 100644 index f2033fa..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/SerializationConfig.kt +++ /dev/null @@ -1,40 +0,0 @@ -package io.github.smiley4.ktorswaggerui.dsl - -/** - * Configuration for serializing examples, schemas, ... - */ -@OpenApiDslMarker -class SerializationConfig { - - /** - * Serialize the given example object into a json-string. - * Return 'null' to use the default serializer for the given value instead. - */ - fun exampleSerializer(serializer: CustomExampleSerializer) { - customExampleSerializer = serializer - } - - private var customExampleSerializer: CustomExampleSerializer = { _, _ -> null } - - fun getCustomExampleSerializer() = customExampleSerializer - - - /** - * Serialize the given type into a valid json-schema. - * Return 'null' to use the default serializer for the given type instead. - * This serializer does not affect custom-schemas provided in the plugin-config. - */ - fun schemaSerializer(serializer: CustomSchemaSerializer) { - customSchemaSerializer = serializer - } - - private var customSchemaSerializer: CustomSchemaSerializer = { null } - - - fun getCustomSchemaSerializer() = customSchemaSerializer - -} - -typealias CustomExampleSerializer = (type: SchemaType?, example: Any) -> String? - -typealias CustomSchemaSerializer = (type: SchemaType) -> String? diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt deleted file mode 100644 index 448d3cc..0000000 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/JsonSchemaConfig.kt +++ /dev/null @@ -1,44 +0,0 @@ -package io.github.smiley4.ktorswaggerui.spec.schema - -import com.fasterxml.jackson.databind.node.ObjectNode -import com.github.victools.jsonschema.generator.FieldScope -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerationContext -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.generator.TypeScope -import com.github.victools.jsonschema.module.jackson.JacksonModule -import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.dsl.Example -import io.swagger.v3.oas.annotations.media.Schema - -object JsonSchemaConfig { - - var schemaGeneratorConfigBuilder: SchemaGeneratorConfigBuilder = - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .with(Swagger2Module()) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) - .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.DEFINITION_FOR_MAIN_SCHEMA) - .without(Option.INLINE_ALL_SCHEMAS) - .also { - it.forTypesInGeneral() - .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> - if (typeScope is FieldScope) { - typeScope.getAnnotation(Schema::class.java)?.also { annotation -> - if (annotation.example != "") { - objectNode.put("example", annotation.example) - } - } - typeScope.getAnnotation(Example::class.java)?.also { annotation -> - objectNode.put("example", annotation.value) - } - } - } - } - -} \ No newline at end of file From 3c89e097fdc72378d6469277ced4ac4990c37456 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lukas=20R=C3=BCgner?= Date: Thu, 25 May 2023 14:51:28 +0200 Subject: [PATCH 25/27] refine serializing/encoding config --- build.gradle.kts | 3 + .../smiley4/ktorswaggerui/SwaggerPlugin.kt | 9 +- .../ktorswaggerui/SwaggerUIPluginConfig.kt | 40 ++---- .../ktorswaggerui/dsl/CustomSchemas.kt | 17 --- .../ktorswaggerui/dsl/EncodingConfig.kt | 117 +++++++++++++++--- .../spec/openapi/ExampleBuilder.kt | 2 +- .../spec/schema/SchemaBuilder.kt | 6 +- .../examples/CompletePluginConfigExample.kt | 36 +++--- .../CustomJsonSchemaBuilderExample.kt | 81 ------------ .../examples/CustomJsonSchemaExample.kt | 2 +- ...ializationExample.kt => KotlinxExample.kt} | 38 +++--- .../ktorswaggerui/examples/MinimalExample.kt | 7 ++ .../ktorswaggerui/examples/Petstore.kt | 2 +- .../tests/openapi/OpenApiBuilderTest.kt | 7 +- .../tests/openapi/OperationBuilderTest.kt | 19 ++- .../tests/openapi/PathsBuilderTest.kt | 7 +- .../tests/schema/SchemaBuilderTest.kt | 8 +- .../tests/schema/SchemaContextTest.kt | 16 ++- 18 files changed, 192 insertions(+), 225 deletions(-) delete mode 100644 src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt rename src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/{KotlinxSerializationExample.kt => KotlinxExample.kt} (69%) diff --git a/build.gradle.kts b/build.gradle.kts index 2b8cd1c..23882d2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -40,6 +40,9 @@ dependencies { implementation("com.github.victools:jsonschema-module-jackson:$jsonSchemaGeneratorVersion") implementation("com.github.victools:jsonschema-module-swagger-2:$jsonSchemaGeneratorVersion") + val jacksonVersion = "2.14.2" + implementation("com.fasterxml.jackson.module:jackson-module-kotlin:${jacksonVersion}") + val kotlinLoggingVersion = "2.1.23" implementation("io.github.microutils:kotlin-logging-jvm:$kotlinLoggingVersion") diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt index ea6ebfc..5797f37 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerPlugin.kt @@ -1,6 +1,5 @@ package io.github.smiley4.ktorswaggerui -import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.spec.openapi.* import io.github.smiley4.ktorswaggerui.spec.route.RouteCollector import io.github.smiley4.ktorswaggerui.spec.route.RouteDocumentationMerger @@ -13,7 +12,6 @@ import io.ktor.server.application.hooks.* import io.ktor.server.routing.* import io.ktor.server.webjars.* import io.swagger.v3.core.util.Json -import kotlin.reflect.jvm.javaType /** * This version must match the version of the gradle dependency @@ -50,9 +48,10 @@ private fun routes(application: Application, pluginConfig: SwaggerUIPluginConfig private fun schemaContext(pluginConfig: SwaggerUIPluginConfig, routes: List): SchemaContext { return SchemaContextBuilder( config = pluginConfig, - schemaBuilder = SchemaBuilder("\$defs") { type -> // TODO: customizable - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() - } + schemaBuilder = SchemaBuilder( + definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, + schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder() + ), ).build(routes.toList()) } diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt index 6bbdd70..22c00bc 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/SwaggerUIPluginConfig.kt @@ -1,19 +1,8 @@ package io.github.smiley4.ktorswaggerui -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemas -import io.github.smiley4.ktorswaggerui.dsl.OpenApiDslMarker -import io.github.smiley4.ktorswaggerui.dsl.OpenApiInfo -import io.github.smiley4.ktorswaggerui.dsl.OpenApiResponse -import io.github.smiley4.ktorswaggerui.dsl.OpenApiSecurityScheme -import io.github.smiley4.ktorswaggerui.dsl.OpenApiServer -import io.github.smiley4.ktorswaggerui.dsl.OpenApiTag -import io.github.smiley4.ktorswaggerui.dsl.SerializationConfig -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUIDsl -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaConfig -import io.ktor.http.HttpMethod -import io.ktor.http.HttpStatusCode -import io.ktor.server.routing.RouteSelector +import io.github.smiley4.ktorswaggerui.dsl.* +import io.ktor.http.* +import io.ktor.server.routing.* import kotlin.reflect.KClass /** @@ -137,7 +126,7 @@ class SwaggerUIPluginConfig { /** * Custom schemas to reference via [io.github.smiley4.ktorswaggerui.dsl.CustomSchemaRef] */ - fun schemas(block: CustomSchemas.() -> Unit) { + fun customSchemas(block: CustomSchemas.() -> Unit) { this.customSchemas = CustomSchemas().apply(block) } @@ -147,26 +136,13 @@ class SwaggerUIPluginConfig { /** - * customize the behaviour of different serializers (examples, schemas, ...) + * customize the behaviour of different encoders (examples, schemas, ...) */ - fun serialization(block: SerializationConfig.() -> Unit) { - block(serializationConfig) + fun encoding(block: EncodingConfig.() -> Unit) { + block(encodingConfig) } - val serializationConfig: SerializationConfig = SerializationConfig() - - - /** - * whether to inline all schemas or move keep them in the components section. - */ - var inlineAllSchemas: Boolean = false // TODO - - - /** - * Customize or replace the configuration-builder for the json-schema-generator (see https://victools.github.io/jsonschema-generator/#generator-options for more information) - */ - @Deprecated("") - var schemaGeneratorConfigBuilder: SchemaGeneratorConfigBuilder = JsonSchemaConfig.schemaGeneratorConfigBuilder + val encodingConfig: EncodingConfig = EncodingConfig() /** diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt index 25b4705..4d99d9b 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/CustomSchemas.kt @@ -5,23 +5,6 @@ import io.swagger.v3.oas.models.media.Schema @OpenApiDslMarker class CustomSchemas { - /** - * Custom builder for building json-schemas from a given type. Return null to not use this builder for the given type. - */ - @Deprecated("") - fun jsonSchemaBuilder(builder: (type: SchemaType) -> String?) { - jsonSchemaBuilder = builder - } - - - @Deprecated("") - private var jsonSchemaBuilder: ((type: SchemaType) -> String?)? = null - - - @Deprecated("") - fun getJsonSchemaBuilder() = jsonSchemaBuilder - - private val customSchemas = mutableMapOf() fun getSchema(id: String): BaseCustomSchema? = customSchemas[id] diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt index 4d9ed5f..572b3d3 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/dsl/EncodingConfig.kt @@ -1,44 +1,129 @@ package io.github.smiley4.ktorswaggerui.dsl +import com.fasterxml.jackson.databind.node.ObjectNode +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.github.victools.jsonschema.generator.* +import com.github.victools.jsonschema.module.jackson.JacksonModule +import com.github.victools.jsonschema.module.swagger2.Swagger2Module +import io.swagger.v3.oas.annotations.media.Schema +import kotlin.reflect.jvm.javaType -typealias ExampleSerializer = (type: SchemaType?, example: Any) -> String? -typealias SchemaSerializer = (type: SchemaType) -> String? +typealias ExampleEncoder = (type: SchemaType?, example: Any) -> String? + +typealias SchemaEncoder = (type: SchemaType) -> String? /** - * Configuration for serializing examples, schemas, ... + * Configuration for encoding examples, schemas, ... */ @OpenApiDslMarker -class SerializationConfig { +class EncodingConfig { /** - * Serialize the given example object into a json-string. + * Encode the given example object into a json-string. */ - fun exampleSerializer(serializer: ExampleSerializer) { - exampleSerializer = serializer + fun exampleEncoder(encoder: ExampleEncoder) { + exampleEncoder = encoder } - private var exampleSerializer: ExampleSerializer = { _, _ -> null } + private var exampleEncoder: ExampleEncoder = defaultExampleEncoder() - fun getExampleSerializer() = exampleSerializer + fun getExampleEncoder() = exampleEncoder /** - * Serialize the given type into a valid json-schema. - * This serializer does not affect custom-schemas provided in the plugin-config. + * Encode the given type into a valid json-schema. + * This encoder does not affect custom-schemas provided in the plugin-config. */ - fun schemaSerializer(serializer: SchemaSerializer) { - schemaSerializer = serializer + fun schemaEncoder(encoder: SchemaEncoder) { + schemaEncoder = encoder } - private var schemaSerializer: SchemaSerializer = { null } + private var schemaEncoder: SchemaEncoder = defaultSchemaEncoder() - fun getSchemaSerializer() = schemaSerializer + fun getSchemaEncoder() = schemaEncoder /** - * + * the name of the field (if it exists) in the json-schema containing schema-definitions. */ var schemaDefinitionsField = "\$defs" + companion object { + + /** + * The default jackson object mapper used for encoding examples to json. + */ + var DEFAULT_EXAMPLE_OBJECT_MAPPER = jacksonObjectMapper() + + /** + * The default [SchemaGenerator] used to encode types to json-schema. + * See https://victools.github.io/jsonschema-generator/#generator-options for more information. + */ + var DEFAULT_SCHEMA_GENERATOR = SchemaGenerator(schemaGeneratorConfigBuilder().build()) + + /** + * The default [ExampleEncoder] + */ + fun defaultExampleEncoder(): ExampleEncoder { + return { _, value -> encodeExample(value) } + } + + /** + * encode the given value to a json string + */ + fun encodeExample(value: Any): String { + return if (value is String) { + value + } else { + DEFAULT_EXAMPLE_OBJECT_MAPPER.writeValueAsString(value) + } + } + + + /** + * The default [SchemaEncoder] + */ + fun defaultSchemaEncoder(): SchemaEncoder { + return { type -> encodeSchema(type) } + } + + /** + * encode the given type to a json-schema + */ + fun encodeSchema(type: SchemaType): String { + return DEFAULT_SCHEMA_GENERATOR.generateSchema(type.javaType).toPrettyString() + } + + /** + * The default [SchemaGeneratorConfigBuilder] + */ + fun schemaGeneratorConfigBuilder(): SchemaGeneratorConfigBuilder = + SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) + .with(JacksonModule()) + .with(Swagger2Module()) + .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) + .with(Option.ALLOF_CLEANUP_AT_THE_END) + .with(Option.MAP_VALUES_AS_ADDITIONAL_PROPERTIES) + .with(Option.DEFINITIONS_FOR_ALL_OBJECTS) + .without(Option.INLINE_ALL_SCHEMAS) + .also { + it.forTypesInGeneral() + .withTypeAttributeOverride { objectNode: ObjectNode, typeScope: TypeScope, _: SchemaGenerationContext -> + if (typeScope is FieldScope) { + typeScope.getAnnotation(Schema::class.java)?.also { annotation -> + if (annotation.example != "") { + objectNode.put("example", annotation.example) + } + } + typeScope.getAnnotation(Example::class.java)?.also { annotation -> + objectNode.put("example", annotation.value) + } + } + } + } + + } + } + diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt index a8049a4..dec7256 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/openapi/ExampleBuilder.kt @@ -17,7 +17,7 @@ class ExampleBuilder( } private fun buildExampleValue(type: SchemaType?, value: Any): Any { - return config.serializationConfig.getCustomExampleSerializer()(type, value) ?: value + return config.encodingConfig.getExampleEncoder()(type, value) ?: value } } \ No newline at end of file diff --git a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt index 9126fab..93db372 100644 --- a/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt +++ b/src/main/kotlin/io/github/smiley4/ktorswaggerui/spec/schema/SchemaBuilder.kt @@ -5,7 +5,7 @@ import com.fasterxml.jackson.databind.ObjectMapper import com.fasterxml.jackson.databind.node.ArrayNode import com.fasterxml.jackson.databind.node.ObjectNode import com.fasterxml.jackson.databind.node.TextNode -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaSerializer +import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder import io.github.smiley4.ktorswaggerui.dsl.SchemaType import io.swagger.v3.core.util.Json import io.swagger.v3.oas.models.media.Schema @@ -18,7 +18,7 @@ data class SchemaDefinitions( class SchemaBuilder( private val definitionsField: String? = null, - private val schemaSerializer: CustomSchemaSerializer + private val schemaEncoder: SchemaEncoder ) { @@ -42,7 +42,7 @@ class SchemaBuilder( } private fun createJsonSchema(type: SchemaType): JsonNode { - val str = schemaSerializer(type) + val str = schemaEncoder(type) return ObjectMapper().readTree(str) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt index 2a6fc94..8ed947e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CompletePluginConfigExample.kt @@ -3,15 +3,10 @@ package io.github.smiley4.ktorswaggerui.examples import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.github.victools.jsonschema.generator.SchemaGenerator import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.AuthScheme -import io.github.smiley4.ktorswaggerui.dsl.AuthType -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSort -import io.github.smiley4.ktorswaggerui.dsl.SwaggerUiSyntaxHighlight -import io.github.smiley4.ktorswaggerui.spec.schema.JsonSchemaConfig -import io.ktor.server.application.Application -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty +import io.github.smiley4.ktorswaggerui.dsl.* +import io.ktor.server.application.* +import io.ktor.server.engine.* +import io.ktor.server.netty.* import io.swagger.v3.oas.models.media.Schema import kotlin.reflect.jvm.javaType @@ -19,6 +14,9 @@ fun main() { embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) } +/** + * Example of an (almost) complete plugin config. This config will (probably) not work, but is only supposed to show all/most configuration options. + */ private fun Application.myModule() { install(SwaggerUI) { @@ -74,10 +72,7 @@ private fun Application.myModule() { externalDocUrl = "example.com/doc" } generateTags { url -> listOf(url.firstOrNull()) } - schemas { - jsonSchemaBuilder { type -> - SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toPrettyString() - } + customSchemas { json("customSchema1") { """{"type": "string"}""" } @@ -88,15 +83,16 @@ private fun Application.myModule() { } remote("customSchema3", "example.com/schema") } - inlineAllSchemas = true - serialization { - exampleSerializer { type, example -> - jacksonObjectMapper().writeValueAsString(example) + encoding { + schemaEncoder { type -> + SchemaGenerator(EncodingConfig.schemaGeneratorConfigBuilder().build()) + .generateSchema(type.javaType) + .toPrettyString() } - schemaSerializer { type -> - SchemaGenerator(JsonSchemaConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toPrettyString() + schemaDefinitionsField = "\$defs" + exampleEncoder { type, example -> + jacksonObjectMapper().writeValueAsString(example) } } - schemaGeneratorConfigBuilder = schemaGeneratorConfigBuilder.let { /*...*/ it } } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt deleted file mode 100644 index 041167e..0000000 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaBuilderExample.kt +++ /dev/null @@ -1,81 +0,0 @@ -package io.github.smiley4.ktorswaggerui.examples - -import com.github.victools.jsonschema.generator.Option -import com.github.victools.jsonschema.generator.OptionPreset -import com.github.victools.jsonschema.generator.SchemaGenerator -import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder -import com.github.victools.jsonschema.generator.SchemaVersion -import com.github.victools.jsonschema.module.jackson.JacksonModule -import io.github.smiley4.ktorswaggerui.SwaggerUI -import io.github.smiley4.ktorswaggerui.dsl.get -import io.ktor.http.HttpStatusCode -import io.ktor.server.application.Application -import io.ktor.server.application.call -import io.ktor.server.application.install -import io.ktor.server.engine.embeddedServer -import io.ktor.server.netty.Netty -import io.ktor.server.request.receive -import io.ktor.server.response.respond -import io.ktor.server.routing.routing -import java.lang.reflect.Type -import kotlin.reflect.jvm.javaType - -/** - * An example for building custom json-schemas - */ -fun main() { - embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) -} - - -fun typeToJsonSchema(type: Type): String { - return SchemaGenerator( - SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) - .with(JacksonModule()) - .without(Option.DEFINITIONS_FOR_ALL_OBJECTS) - .with(Option.INLINE_ALL_SCHEMAS) - .with(Option.EXTRA_OPEN_API_FORMAT_VALUES) - .with(Option.ALLOF_CLEANUP_AT_THE_END) - .build() - ) - .generateSchema(type) - .toString() -} - -private fun Application.myModule() { - - data class MyRequestData( - val someText: String, - val someBoolean: Boolean - ) - - - data class MyResponseData( - val someText: String, - val someNumber: Long - ) - - install(SwaggerUI) { - schemas { - jsonSchemaBuilder { type -> - // custom converter from the given 'type' to a json-schema - typeToJsonSchema(type.javaType) - } - } - } - routing { - get("something", { - request { - body() - } - response { - HttpStatusCode.OK to { - body() - } - } - }) { - val text = call.receive().someText - call.respond(HttpStatusCode.OK, MyResponseData(text, 42)) - } - } -} diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt index ff7f250..23b3ffd 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/CustomJsonSchemaExample.kt @@ -37,7 +37,7 @@ private fun Application.myModule() { install(SwaggerUI) { // don't show the test-routes providing json-schemas pathFilter = { _, url -> url.firstOrNull() != "schema" } - schemas { + customSchemas { // specify a custom json-schema with the id 'myRequestData' json("myRequestData") { """ diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxExample.kt similarity index 69% rename from src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt rename to src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxExample.kt index c291167..44e7d7e 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxSerializationExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/KotlinxExample.kt @@ -16,10 +16,10 @@ import kotlinx.serialization.serializer import com.github.ricky12awesome.jss.encodeToSchema /** - * An example showcasing compatibility with kotlinx serializer and kotlinx multiplatform using: + * An example showing compatibility with kotlinx serializer and kotlinx multiplatform using: * - https://github.com/Kotlin/kotlinx.serialization * - https://github.com/Kotlin/kotlinx-datetime - * - https://github.com/Ricky12Awesome/json-schema-serialization + * - https://github.com/tillersystems/json-schema-serialization */ fun main() { embeddedServer(Netty, port = 8080, host = "localhost", module = Application::myModule).start(wait = true) @@ -33,12 +33,12 @@ private fun Application.myModule() { } install(SwaggerUI) { - serialization { - schemaSerializer { type -> - kotlinxJson.encodeToSchema(serializer(type), generateDefinitions = true) -// globalJson.encodeToSchema(serializer(type), generateDefinitions = false) + encoding { + schemaEncoder { type -> + kotlinxJson.encodeToSchema(serializer(type), generateDefinitions = false) } - exampleSerializer { type, value -> + schemaDefinitionsField = "definitions" + exampleEncoder { type, value -> kotlinxJson.encodeToString(serializer(type!!), value) } } @@ -57,18 +57,18 @@ private fun Application.myModule() { }) { call.respondText("...") } -// get("example/many", { -// request { -// body> { -// example("default", listOf( -// ExampleRequest.B(Instant.fromEpochMilliseconds(System.currentTimeMillis())), -// ExampleRequest.A(true) -// )) -// } -// } -// }) { -// call.respondText("...") -// } + get("example/many", { + request { + body> { + example("default", listOf( + ExampleRequest.B(Instant.fromEpochMilliseconds(System.currentTimeMillis())), + ExampleRequest.A(true) + )) + } + } + }) { + call.respondText("...") + } } } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt index ced9c17..3dc897f 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/MinimalExample.kt @@ -26,6 +26,13 @@ private fun Application.myModule() { routing { // documented "get"-route get("hello", { + + request { + body { + example("example", 42) + } + } + // a description of the route description = "Simple 'Hello World'- Route" // information about possible responses diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt index 3df2a0a..de3068f 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/examples/Petstore.kt @@ -60,7 +60,7 @@ private fun Application.myModule() { response { HttpStatusCode.OK to { description = "pet response" - body(Array::class) { + body>() { mediaType(ContentType.Application.Json) mediaType(ContentType.Application.Xml) example( diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt index 3780a1e..bc6ad76 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OpenApiBuilderTest.kt @@ -100,9 +100,10 @@ class OpenApiBuilderTest : StringSpec({ private fun schemaContext(routes: List, pluginConfig: SwaggerUIPluginConfig): SchemaContext { return SchemaContextBuilder( config = pluginConfig, - schemaBuilder = SchemaBuilder("\$defs") { type -> - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() - } + schemaBuilder = SchemaBuilder( + definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, + schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder() + ) ).build(routes) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt index cdbc36b..7a62be9 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/OperationBuilderTest.kt @@ -9,7 +9,6 @@ import io.github.smiley4.ktorswaggerui.spec.route.RouteMeta import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContext import io.github.smiley4.ktorswaggerui.spec.schema.SchemaContextBuilder -import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions import io.kotest.core.spec.style.StringSpec import io.kotest.matchers.collections.shouldBeEmpty import io.kotest.matchers.collections.shouldContainExactlyInAnyOrder @@ -444,7 +443,7 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 2 - content.get("application/json") + content["application/json"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -468,7 +467,7 @@ class OperationBuilderTest : StringSpec({ mediaType.extensions shouldBe null mediaType.exampleSetFlag shouldBe false } - content.get("application/xml") + content["application/xml"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -740,7 +739,7 @@ class OperationBuilderTest : StringSpec({ .also { it.shouldNotBeNull() } ?.also { content -> content shouldHaveSize 1 - content.get("application/json") + content["application/json"] .also { it.shouldNotBeNull() } ?.also { mediaType -> mediaType.schema @@ -776,7 +775,7 @@ class OperationBuilderTest : StringSpec({ "custom body schema" { val config = SwaggerUIPluginConfig().also { - it.schemas { + it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> schema.type = "object" @@ -832,7 +831,7 @@ class OperationBuilderTest : StringSpec({ "custom multipart-body schema" { val config = SwaggerUIPluginConfig().also { - it.schemas { + it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> schema.type = "object" @@ -905,10 +904,10 @@ class OperationBuilderTest : StringSpec({ ): SchemaContext { return SchemaContextBuilder( config = pluginConfig, - schemaBuilder = SchemaBuilder("\$defs") { type -> - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType) - .toString() - } + schemaBuilder = SchemaBuilder( + definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, + schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder() + ) ).build(routes) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt index b67d65a..fe9a87a 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/openapi/PathsBuilderTest.kt @@ -89,9 +89,10 @@ class PathsBuilderTest : StringSpec({ private fun schemaContext(routes: List, pluginConfig: SwaggerUIPluginConfig = defaultPluginConfig): SchemaContext { return SchemaContextBuilder( config = pluginConfig, - schemaBuilder = SchemaBuilder("\$defs") { type -> - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType).toString() - } + schemaBuilder = SchemaBuilder( + definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, + schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder() + ) ).build(routes) } diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt index fae20af..f20d7ef 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaBuilderTest.kt @@ -8,7 +8,7 @@ import com.github.victools.jsonschema.generator.SchemaGeneratorConfigBuilder import com.github.victools.jsonschema.generator.SchemaVersion import com.github.victools.jsonschema.module.jackson.JacksonModule import com.github.victools.jsonschema.module.swagger2.Swagger2Module -import io.github.smiley4.ktorswaggerui.dsl.CustomSchemaSerializer +import io.github.smiley4.ktorswaggerui.dsl.SchemaEncoder import io.github.smiley4.ktorswaggerui.dsl.getSchemaType import io.github.smiley4.ktorswaggerui.spec.schema.SchemaBuilder import io.github.smiley4.ktorswaggerui.spec.schema.SchemaDefinitions @@ -338,12 +338,12 @@ class SchemaBuilderTest : StringSpec({ inline fun createSchema( defs: String, - noinline serializer: CustomSchemaSerializer + noinline serializer: SchemaEncoder ): SchemaDefinitions { return SchemaBuilder(defs, serializer).create(getSchemaType()) } - fun serializerVictools(definitions: Boolean): CustomSchemaSerializer { + fun serializerVictools(definitions: Boolean): SchemaEncoder { return { type -> SchemaGenerator( SchemaGeneratorConfigBuilder(SchemaVersion.DRAFT_2019_09, OptionPreset.PLAIN_JSON) @@ -366,7 +366,7 @@ class SchemaBuilderTest : StringSpec({ } } - fun serializerKotlinX(generateDefinitions: Boolean): CustomSchemaSerializer { + fun serializerKotlinX(generateDefinitions: Boolean): SchemaEncoder { val kotlinxJson = Json { prettyPrint = true encodeDefaults = true diff --git a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt index a41c90f..33c919c 100644 --- a/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt +++ b/src/test/kotlin/io/github/smiley4/ktorswaggerui/tests/schema/SchemaContextTest.kt @@ -327,7 +327,7 @@ class SchemaContextTest : StringSpec({ "custom schema object" { val config = SwaggerUIPluginConfig().also { - it.schemas { + it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> schema.type = "object" @@ -370,7 +370,7 @@ class SchemaContextTest : StringSpec({ "custom schema array" { val config = SwaggerUIPluginConfig().also { - it.schemas { + it.customSchemas { openApi("myCustomSchema") { Schema().also { schema -> schema.type = "object" @@ -422,9 +422,7 @@ class SchemaContextTest : StringSpec({ inline fun getType() = getSchemaType() - private val defaultPluginConfig = SwaggerUIPluginConfig().also { - it.schemaGeneratorConfigBuilder = it.schemaGeneratorConfigBuilder.without(Option.DEFINITION_FOR_MAIN_SCHEMA) - } + private val defaultPluginConfig = SwaggerUIPluginConfig() private fun schemaContext( routes: Collection, @@ -432,10 +430,10 @@ class SchemaContextTest : StringSpec({ ): SchemaContext { return SchemaContextBuilder( config = pluginConfig, - schemaBuilder = SchemaBuilder("\$defs") { type -> - SchemaGenerator(pluginConfig.schemaGeneratorConfigBuilder.build()).generateSchema(type.javaType) - .toString() - } + schemaBuilder = SchemaBuilder( + definitionsField = pluginConfig.encodingConfig.schemaDefinitionsField, + schemaEncoder = pluginConfig.encodingConfig.getSchemaEncoder() + ) ).build(routes) } From f754c55efd070c0fa2b4a0932614dbb4dcef20f9 Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Thu, 25 May 2023 21:48:40 +0200 Subject: [PATCH 26/27] cleanup dependencies --- build.gradle.kts | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 23882d2..16e1fb2 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -12,7 +12,6 @@ version = "2.0.0" repositories { mavenCentral() - jcenter() // TODO: remove!!!! maven(url = "https://raw.githubusercontent.com/glureau/json-schema-serialization/mvn-repo") } @@ -59,9 +58,9 @@ dependencies { val versionKotlinTest = "1.7.21" testImplementation("org.jetbrains.kotlin:kotlin-test:$versionKotlinTest") - testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1")// TODO - testImplementation("com.github.Ricky12Awesome:json-schema-serialization:0.9.9")// TODO -> https://github.com/tillersystems/json-schema-serialization/tree/glureau - testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") // TODO + testImplementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1") + testImplementation("com.github.Ricky12Awesome:json-schema-serialization:0.9.9") + testImplementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0") } tasks.test { From 36ccb04b719124c036a143b3551dc0663a4dfa1f Mon Sep 17 00:00:00 2001 From: Lukas Ruegner Date: Fri, 26 May 2023 18:28:45 +0200 Subject: [PATCH 27/27] set version to 2.0.0-rc --- build.gradle.kts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build.gradle.kts b/build.gradle.kts index 16e1fb2..3e4ac5d 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,11 +4,11 @@ plugins { kotlin("jvm") version "1.7.21" `maven-publish` id("org.owasp.dependencycheck") version "8.2.1" - kotlin("plugin.serialization") version "1.8.21" // TODO: remove!!!! + kotlin("plugin.serialization") version "1.8.21" } group = "io.github.smiley4" -version = "2.0.0" +version = "2.0.0-rc" repositories { mavenCentral()