diff --git a/CHANGELOG.md b/CHANGELOG.md index 6b97d5e15..73c449e69 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,8 @@ ### Changed - Cleaned up and broke out handlers into separate classes +- Serializer cleanup +- Tests now run against Jackson, Gson and kotlinx on every run ### Remove diff --git a/kompendium-auth/src/test/kotlin/io/bkbn/kompendium/auth/KompendiumAuthTest.kt b/kompendium-auth/src/test/kotlin/io/bkbn/kompendium/auth/KompendiumAuthTest.kt index ada56e8ef..88246623a 100644 --- a/kompendium-auth/src/test/kotlin/io/bkbn/kompendium/auth/KompendiumAuthTest.kt +++ b/kompendium-auth/src/test/kotlin/io/bkbn/kompendium/auth/KompendiumAuthTest.kt @@ -8,7 +8,7 @@ import io.bkbn.kompendium.auth.util.configBasicAuth import io.bkbn.kompendium.auth.util.configJwtAuth import io.bkbn.kompendium.auth.util.notarizedAuthRoute import io.bkbn.kompendium.auth.util.setupOauth -import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTest +import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.oas.security.OAuth import io.kotest.core.spec.style.DescribeSpec @@ -21,7 +21,7 @@ class KompendiumAuthTest : DescribeSpec({ } // act - openApiTest("notarized_basic_authenticated_get.json") { + openApiTestAllSerializers("notarized_basic_authenticated_get.json") { configBasicAuth() notarizedAuthRoute(authConfig) } @@ -35,7 +35,7 @@ class KompendiumAuthTest : DescribeSpec({ } // act - openApiTest("notarized_jwt_authenticated_get.json") { + openApiTestAllSerializers("notarized_jwt_authenticated_get.json") { configJwtAuth() notarizedAuthRoute(authConfig) } @@ -60,7 +60,7 @@ class KompendiumAuthTest : DescribeSpec({ } // act - openApiTest("notarized_oauth_all_flows.json") { + openApiTestAllSerializers("notarized_oauth_all_flows.json") { setupOauth() notarizedAuthRoute(authConfig) } diff --git a/kompendium-core/Module.md b/kompendium-core/Module.md index 628d446ca..6224a2668 100644 --- a/kompendium-core/Module.md +++ b/kompendium-core/Module.md @@ -21,6 +21,14 @@ The downside is that issues could exist in serialization frameworks that have no Gson and KotlinX serialization have all been tested. If you run into any serialization issues, particularly with a serializer not listed above, please open an issue on GitHub 🙏 +Note for Kotlinx ⚠️ + +You will need to include the `SerializersModule` provided in `KompendiumSerializersModule` in order to serialize +any provided defaults. This comes down to how Kotlinx expects users to handle serializing `Any`. Essentially, this +serializer module will convert any `Any` serialization to be `Contextual`. This is pretty hacky, but seemed to be the +only way to get Kotlinx to play nice with serializing `Any`. If you come up with a better solution, definitely go ahead +and open up a PR! + ## Notarization Central to Kompendium is the concept of notarization. diff --git a/kompendium-core/build.gradle.kts b/kompendium-core/build.gradle.kts index 8272e4320..689d18f23 100644 --- a/kompendium-core/build.gradle.kts +++ b/kompendium-core/build.gradle.kts @@ -1,5 +1,6 @@ plugins { kotlin("jvm") + kotlin("plugin.serialization") id("io.bkbn.sourdough.library.jvm") id("io.gitlab.arturbosch.detekt") id("com.adarshr.test-logger") @@ -40,6 +41,7 @@ dependencies { testFixturesApi(group = "io.ktor", name = "ktor-server-core", version = ktorVersion) testFixturesApi(group = "io.ktor", name = "ktor-server-test-host", version = ktorVersion) testFixturesApi(group = "io.ktor", name = "ktor-jackson", version = ktorVersion) + testFixturesApi(group = "io.ktor", name = "ktor-gson", version = ktorVersion) testFixturesApi(group = "io.ktor", name = "ktor-serialization", version = ktorVersion) testFixturesApi(group = "org.jetbrains.kotlinx", name = "kotlinx-serialization-json", version = "1.3.2") diff --git a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt index 0e5c42bdc..9524889f1 100644 --- a/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt +++ b/kompendium-core/src/test/kotlin/io/bkbn/kompendium/core/KompendiumTest.kt @@ -2,7 +2,7 @@ package io.bkbn.kompendium.core import io.bkbn.kompendium.core.fixtures.TestHelpers.apiFunctionalityTest import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot -import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTest +import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.core.util.complexType import io.bkbn.kompendium.core.util.constrainedDoubleInfo import io.bkbn.kompendium.core.util.constrainedIntInfo @@ -60,37 +60,37 @@ import io.ktor.http.HttpStatusCode class KompendiumTest : DescribeSpec({ describe("Notarized Open API Metadata Tests") { it("Can notarize a get request") { - openApiTest("notarized_get.json") { notarizedGetModule() } + openApiTestAllSerializers("notarized_get.json") { notarizedGetModule() } } it("Can notarize a post request") { - openApiTest("notarized_post.json") { notarizedPostModule() } + openApiTestAllSerializers("notarized_post.json") { notarizedPostModule() } } it("Can notarize a put request") { - openApiTest("notarized_put.json") { notarizedPutModule() } + openApiTestAllSerializers("notarized_put.json") { notarizedPutModule() } } it("Can notarize a delete request") { - openApiTest("notarized_delete.json") { notarizedDeleteModule() } + openApiTestAllSerializers("notarized_delete.json") { notarizedDeleteModule() } } it("Can notarize a patch request") { - openApiTest("notarized_patch.json") { notarizedPatchModule() } + openApiTestAllSerializers("notarized_patch.json") { notarizedPatchModule() } } it("Can notarize a head request") { - openApiTest("notarized_head.json") { notarizedHeadModule() } + openApiTestAllSerializers("notarized_head.json") { notarizedHeadModule() } } it("Can notarize an options request") { - openApiTest("notarized_options.json") { notarizedOptionsModule() } + openApiTestAllSerializers("notarized_options.json") { notarizedOptionsModule() } } it("Can notarize a complex type") { - openApiTest("complex_type.json") { complexType() } + openApiTestAllSerializers("complex_type.json") { complexType() } } it("Can notarize primitives") { - openApiTest("notarized_primitives.json") { primitives() } + openApiTestAllSerializers("notarized_primitives.json") { primitives() } } it("Can notarize a top level list response") { - openApiTest("response_list.json") { returnsList() } + openApiTestAllSerializers("response_list.json") { returnsList() } } it("Can notarize a route with non-required params") { - openApiTest("non_required_params.json") { nonRequiredParamsGet() } + openApiTestAllSerializers("non_required_params.json") { nonRequiredParamsGet() } } } describe("Notarized Ktor Functionality Tests") { @@ -122,80 +122,80 @@ class KompendiumTest : DescribeSpec({ } describe("Route Parsing") { it("Can parse a simple path and store it under the expected route") { - openApiTest("path_parser.json") { pathParsingTestModule() } + openApiTestAllSerializers("path_parser.json") { pathParsingTestModule() } } it("Can notarize the root route") { - openApiTest("root_route.json") { rootModule() } + openApiTestAllSerializers("root_route.json") { rootModule() } } it("Can notarize a route under the root module without appending trailing slash") { - openApiTest("nested_under_root.json") { nestedUnderRootModule() } + openApiTestAllSerializers("nested_under_root.json") { nestedUnderRootModule() } } it("Can notarize a route with a trailing slash") { - openApiTest("trailing_slash.json") { trailingSlash() } + openApiTestAllSerializers("trailing_slash.json") { trailingSlash() } } } describe("Exceptions") { it("Can add an exception status code to a response") { - openApiTest("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() } + openApiTestAllSerializers("notarized_get_with_exception_response.json") { notarizedGetWithNotarizedException() } } it("Can support multiple response codes") { - openApiTest("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() } + openApiTestAllSerializers("notarized_get_with_multiple_exception_responses.json") { notarizedGetWithMultipleThrowables() } } it("Can add a polymorphic exception response") { - openApiTest("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() } + openApiTestAllSerializers("polymorphic_error_status_codes.json") { notarizedGetWithPolymorphicErrorResponse() } } it("Can add a generic exception response") { - openApiTest("generic_exception.json") { notarizedGetWithGenericErrorResponse() } + openApiTestAllSerializers("generic_exception.json") { notarizedGetWithGenericErrorResponse() } } } describe("Examples") { it("Can generate example response and request bodies") { - openApiTest("example_req_and_resp.json") { withExamples() } + openApiTestAllSerializers("example_req_and_resp.json") { withExamples() } } it("Can describe example parameters") { - openApiTest("example_parameters.json") { exampleParams() } + openApiTestAllSerializers("example_parameters.json") { exampleParams() } } } describe("Defaults") { it("Can generate a default parameter values") { - openApiTest("query_with_default_parameter.json") { withDefaultParameter() } + openApiTestAllSerializers("query_with_default_parameter.json") { withDefaultParameter() } } } describe("Required Fields") { it("Marks a parameter required if there is no default and it is not marked nullable") { - openApiTest("required_param.json") { requiredParameter() } + openApiTestAllSerializers("required_param.json") { requiredParameter() } } it("Does not mark a parameter as required if a default value is provided") { - openApiTest("default_param.json") { defaultParameter() } + openApiTestAllSerializers("default_param.json") { defaultParameter() } } it("Does not mark a field as required if a default value is provided") { - openApiTest("default_field.json") { defaultField() } + openApiTestAllSerializers("default_field.json") { defaultField() } } it("Marks a field as nullable when expected") { - openApiTest("nullable_field.json") { nullableField() } + openApiTestAllSerializers("nullable_field.json") { nullableField() } } } describe("Polymorphism and Generics") { it("can generate a polymorphic response type") { - openApiTest("polymorphic_response.json") { polymorphicResponse() } + openApiTestAllSerializers("polymorphic_response.json") { polymorphicResponse() } } it("Can generate a collection with polymorphic response type") { - openApiTest("polymorphic_list_response.json") { polymorphicCollectionResponse() } + openApiTestAllSerializers("polymorphic_list_response.json") { polymorphicCollectionResponse() } } it("Can generate a map with a polymorphic response type") { - openApiTest("polymorphic_map_response.json") { polymorphicMapResponse() } + openApiTestAllSerializers("polymorphic_map_response.json") { polymorphicMapResponse() } } it("Can generate a polymorphic response from a sealed interface") { - openApiTest("sealed_interface_response.json") { polymorphicInterfaceResponse() } + openApiTestAllSerializers("sealed_interface_response.json") { polymorphicInterfaceResponse() } } it("Can generate a response type with a generic type") { - openApiTest("generic_response.json") { simpleGenericResponse() } + openApiTestAllSerializers("generic_response.json") { simpleGenericResponse() } } it("Can generate a polymorphic response type with generics") { - openApiTest("polymorphic_response_with_generics.json") { genericPolymorphicResponse() } + openApiTestAllSerializers("polymorphic_response_with_generics.json") { genericPolymorphicResponse() } } it("Can handle an absolutely psycho inheritance test") { - openApiTest("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() } + openApiTestAllSerializers("crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() } } } describe("Miscellaneous") { @@ -203,59 +203,59 @@ class KompendiumTest : DescribeSpec({ apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() } } it("Can add an operation id to a notarized route") { - openApiTest("notarized_get_with_operation_id.json") { withOperationId() } + openApiTestAllSerializers("notarized_get_with_operation_id.json") { withOperationId() } } it("Can add an undeclared field") { - openApiTest("undeclared_field.json") { undeclaredType() } + openApiTestAllSerializers("undeclared_field.json") { undeclaredType() } } it("Can add a custom header parameter with a name override") { - openApiTest("override_parameter_name.json") { headerParameter() } + openApiTestAllSerializers("override_parameter_name.json") { headerParameter() } } it("Can override field values via annotation") { - openApiTest("field_override.json") { overrideFieldInfo() } + openApiTestAllSerializers("field_override.json") { overrideFieldInfo() } } it("Can serialize a recursive type using references") { - openApiTest("simple_recursive.json") { simpleRecursive() } + openApiTestAllSerializers("simple_recursive.json") { simpleRecursive() } } } describe("Constraints") { it("Can set a minimum and maximum integer value") { - openApiTest("min_max_int_field.json") { constrainedIntInfo() } + openApiTestAllSerializers("min_max_int_field.json") { constrainedIntInfo() } } it("Can set a minimum and maximum double value") { - openApiTest("min_max_double_field.json") { constrainedDoubleInfo() } + openApiTestAllSerializers("min_max_double_field.json") { constrainedDoubleInfo() } } it("Can set an exclusive min and exclusive max integer value") { - openApiTest("exclusive_min_max.json") { exclusiveMinMax() } + openApiTestAllSerializers("exclusive_min_max.json") { exclusiveMinMax() } } it("Can add a custom format to a string field") { - openApiTest("formatted_param_type.json") { formattedParam() } + openApiTestAllSerializers("formatted_param_type.json") { formattedParam() } } it("Can set a minimum and maximum length on a string field") { - openApiTest("min_max_string.json") { minMaxString() } + openApiTestAllSerializers("min_max_string.json") { minMaxString() } } it("Can set a custom regex pattern on a string field") { - openApiTest("regex_string.json") { regexString() } + openApiTestAllSerializers("regex_string.json") { regexString() } } it("Can set a minimum and maximum item count on an array field") { - openApiTest("min_max_array.json") { minMaxArray() } + openApiTestAllSerializers("min_max_array.json") { minMaxArray() } } it("Can set a unique items constraint on an array field") { - openApiTest("unique_array.json") { uniqueArray() } + openApiTestAllSerializers("unique_array.json") { uniqueArray() } } it("Can set a multiple-of constraint on an int field") { - openApiTest("multiple_of_int.json") { multipleOfInt() } + openApiTestAllSerializers("multiple_of_int.json") { multipleOfInt() } } it("Can set a multiple of constraint on an double field") { - openApiTest("multiple_of_double.json") { multipleOfDouble() } + openApiTestAllSerializers("multiple_of_double.json") { multipleOfDouble() } } it("Can set a minimum and maximum number of properties on a free-form type") { - openApiTest("min_max_free_form.json") { minMaxFreeForm() } + openApiTestAllSerializers("min_max_free_form.json") { minMaxFreeForm() } } } describe("Free Form") { it("Can create a free-form field") { - openApiTest("free_form_object.json") { freeFormObject() } + openApiTestAllSerializers("free_form_object.json") { freeFormObject() } } } }) diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SupportedSerializers.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SupportedSerializers.kt new file mode 100644 index 000000000..35db0c376 --- /dev/null +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/SupportedSerializers.kt @@ -0,0 +1,7 @@ +package io.bkbn.kompendium.core.fixtures + +enum class SupportedSerializer { + KOTLINX, + GSON, + JACKSON +} diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt index ba4b9425d..3e1818cb9 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestHelpers.kt @@ -1,16 +1,22 @@ package io.bkbn.kompendium.core.fixtures +import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.kotest.assertions.json.shouldEqualJson import io.kotest.assertions.ktor.shouldHaveStatus import io.kotest.matchers.shouldBe import io.kotest.matchers.shouldNotBe import io.ktor.application.Application +import io.ktor.application.install +import io.ktor.features.ContentNegotiation +import io.ktor.gson.gson import io.ktor.http.HttpMethod import io.ktor.http.HttpStatusCode +import io.ktor.serialization.json import io.ktor.server.testing.TestApplicationEngine import io.ktor.server.testing.createTestEnvironment import io.ktor.server.testing.handleRequest import io.ktor.server.testing.withApplication +import kotlinx.serialization.json.Json import java.io.File object TestHelpers { @@ -42,16 +48,44 @@ object TestHelpers { /** * This will take a provided JSON snapshot file, retrieve it from the resource folder, * and build a test ktor server to compare the expected output with the output found in the default - * OpenAPI json endpoint + * OpenAPI json endpoint. By default, this will run the same test with Gson, Kotlinx, and Jackson serializers * @param snapshotName The snapshot file to retrieve from the resources folder * @param moduleFunction Initializer for the application to allow tests to pass the required Ktor modules */ - fun openApiTest(snapshotName: String, moduleFunction: Application.() -> Unit) { + fun openApiTestAllSerializers(snapshotName: String, moduleFunction: Application.() -> Unit) { + openApiTest(snapshotName, SupportedSerializer.KOTLINX, moduleFunction) + openApiTest(snapshotName, SupportedSerializer.JACKSON, moduleFunction) + openApiTest(snapshotName, SupportedSerializer.GSON, moduleFunction) + } + + private fun openApiTest( + snapshotName: String, + serializer: SupportedSerializer, + moduleFunction: Application.() -> Unit + ) { withApplication(createTestEnvironment()) { moduleFunction(application.apply { kompendium() docs() - jacksonConfigModule() + when (serializer) { + SupportedSerializer.KOTLINX -> { + install(ContentNegotiation) { + json(Json { + encodeDefaults = true + explicitNulls = false + serializersModule = KompendiumSerializersModule.module + }) + } + } + SupportedSerializer.GSON -> { + install(ContentNegotiation) { + gson() + } + } + SupportedSerializer.JACKSON -> { + jacksonConfigModule() + } + } }) compareOpenAPISpec(snapshotName) } diff --git a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt index d5a19a0ca..d9c969569 100644 --- a/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt +++ b/kompendium-core/src/testFixtures/kotlin/io/bkbn/kompendium/core/fixtures/TestModels.kt @@ -18,6 +18,7 @@ import io.bkbn.kompendium.annotations.constraint.Minimum import io.bkbn.kompendium.annotations.constraint.MultipleOf import io.bkbn.kompendium.annotations.constraint.Pattern import io.bkbn.kompendium.annotations.constraint.UniqueItems +import kotlinx.serialization.Serializable import kotlinx.serialization.json.JsonElement import java.math.BigDecimal import java.math.BigInteger @@ -46,10 +47,12 @@ data class TestParams( @Param(ParamType.QUERY) val aa: Int ) +@Serializable data class TestNested(val nesty: String) data class TestWithUUID(val id: UUID) +@Serializable data class TestRequest( @Field(name = "field_name") val fieldName: TestNested, @@ -57,6 +60,7 @@ data class TestRequest( val aaa: List ) +@Serializable data class TestResponse(val c: String) data class TestGeneric(val messy: String, val potato: T) diff --git a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt index 2b9214d1e..cd415c420 100644 --- a/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt +++ b/kompendium-locations/src/test/kotlin/io/bkbn/kompendium/locations/KompendiumLocationsTest.kt @@ -1,6 +1,6 @@ package io.bkbn.kompendium.locations -import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTest +import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers import io.bkbn.kompendium.locations.util.locationsConfig import io.bkbn.kompendium.locations.util.notarizedDeleteNestedLocation import io.bkbn.kompendium.locations.util.notarizedDeleteSimpleLocation @@ -16,56 +16,56 @@ class KompendiumLocationsTest : DescribeSpec({ describe("Locations") { it("Can notarize a get request with a simple location") { // act - openApiTest("notarized_get_simple_location.json") { + openApiTestAllSerializers("notarized_get_simple_location.json") { locationsConfig() notarizedGetSimpleLocation() } } it("Can notarize a get request with a nested location") { // act - openApiTest("notarized_get_nested_location.json") { + openApiTestAllSerializers("notarized_get_nested_location.json") { locationsConfig() notarizedGetNestedLocation() } } it("Can notarize a post with a simple location") { // act - openApiTest("notarized_post_simple_location.json") { + openApiTestAllSerializers("notarized_post_simple_location.json") { locationsConfig() notarizedPostSimpleLocation() } } it("Can notarize a post with a nested location") { // act - openApiTest("notarized_post_nested_location.json") { + openApiTestAllSerializers("notarized_post_nested_location.json") { locationsConfig() notarizedPostNestedLocation() } } it("Can notarize a put with a simple location") { // act - openApiTest("notarized_put_simple_location.json") { + openApiTestAllSerializers("notarized_put_simple_location.json") { locationsConfig() notarizedPutSimpleLocation() } } it("Can notarize a put with a nested location") { // act - openApiTest("notarized_put_nested_location.json") { + openApiTestAllSerializers("notarized_put_nested_location.json") { locationsConfig() notarizedPutNestedLocation() } } it("Can notarize a delete with a simple location") { // act - openApiTest("notarized_delete_simple_location.json") { + openApiTestAllSerializers("notarized_delete_simple_location.json") { locationsConfig() notarizedDeleteSimpleLocation() } } it("Can notarize a delete with a nested location") { // act - openApiTest("notarized_delete_nested_location.json") { + openApiTestAllSerializers("notarized_delete_nested_location.json") { locationsConfig() notarizedDeleteNestedLocation() } diff --git a/kompendium-oas/build.gradle.kts b/kompendium-oas/build.gradle.kts index 7f51748a0..6c010bd98 100644 --- a/kompendium-oas/build.gradle.kts +++ b/kompendium-oas/build.gradle.kts @@ -17,7 +17,7 @@ sourdough { } dependencies { - implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1") + implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.2") } testing { diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/ComponentSchema.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/ComponentSchema.kt index df9fa5bfd..c6ac7951e 100644 --- a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/ComponentSchema.kt +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/schema/ComponentSchema.kt @@ -1,10 +1,9 @@ package io.bkbn.kompendium.oas.schema -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.JsonClassDiscriminator +import io.bkbn.kompendium.oas.serialization.ComponentSchemaSerializer +import kotlinx.serialization.Serializable -@OptIn(ExperimentalSerializationApi::class) -@JsonClassDiscriminator("component_type") // todo figure out a way to filter this +@Serializable(with = ComponentSchemaSerializer::class) sealed interface ComponentSchema { val description: String? get() = null diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/security/SecuritySchema.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/security/SecuritySchema.kt index e36055281..a74725c9c 100644 --- a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/security/SecuritySchema.kt +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/security/SecuritySchema.kt @@ -1,8 +1,7 @@ package io.bkbn.kompendium.oas.security -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.JsonClassDiscriminator +import io.bkbn.kompendium.oas.serialization.SecuritySchemaSerializer +import kotlinx.serialization.Serializable -@OptIn(ExperimentalSerializationApi::class) -@JsonClassDiscriminator("schema_type") // todo figure out a way to filter this +@Serializable(with = SecuritySchemaSerializer::class) sealed interface SecuritySchema diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/AnySerializer.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/AnySerializer.kt index 85cadeb6d..6fd7e5397 100644 --- a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/AnySerializer.kt +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/AnySerializer.kt @@ -3,6 +3,8 @@ package io.bkbn.kompendium.oas.serialization import kotlin.reflect.KClass import kotlinx.serialization.InternalSerializationApi import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor import kotlinx.serialization.descriptors.SerialDescriptor import kotlinx.serialization.encoding.Decoder import kotlinx.serialization.encoding.Encoder @@ -17,8 +19,7 @@ class AnySerializer : KSerializer { error("Abandon all hope ye who enter 💀") } - override val descriptor: SerialDescriptor - get() = TODO("Not yet implemented") + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("KompendiumAny", PrimitiveKind.STRING) @OptIn(InternalSerializationApi::class) fun serialize(encoder: Encoder, obj: T, clazz: KClass) { diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/ComponentSchemaSerializer.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/ComponentSchemaSerializer.kt new file mode 100644 index 000000000..3baf534aa --- /dev/null +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/ComponentSchemaSerializer.kt @@ -0,0 +1,45 @@ +package io.bkbn.kompendium.oas.serialization + +import io.bkbn.kompendium.oas.schema.AnyOfSchema +import io.bkbn.kompendium.oas.schema.ArraySchema +import io.bkbn.kompendium.oas.schema.ComponentSchema +import io.bkbn.kompendium.oas.schema.DictionarySchema +import io.bkbn.kompendium.oas.schema.EnumSchema +import io.bkbn.kompendium.oas.schema.FormattedSchema +import io.bkbn.kompendium.oas.schema.FreeFormSchema +import io.bkbn.kompendium.oas.schema.ObjectSchema +import io.bkbn.kompendium.oas.schema.ReferencedSchema +import io.bkbn.kompendium.oas.schema.SimpleSchema +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = ComponentSchema::class) +object ComponentSchemaSerializer : KSerializer { + override fun deserialize(decoder: Decoder): ComponentSchema { + error("Abandon all hope ye who enter 💀") + } + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ComponentSchema", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: ComponentSchema) { + when (value) { + is AnyOfSchema -> AnyOfSchema.serializer().serialize(encoder, value) + is ReferencedSchema -> ReferencedSchema.serializer().serialize(encoder, value) + is ArraySchema -> ArraySchema.serializer().serialize(encoder, value) + is DictionarySchema -> DictionarySchema.serializer().serialize(encoder, value) + is EnumSchema -> EnumSchema.serializer().serialize(encoder, value) + is FormattedSchema -> FormattedSchema.serializer().serialize(encoder, value) + is FreeFormSchema -> FreeFormSchema.serializer().serialize(encoder, value) + is ObjectSchema -> ObjectSchema.serializer().serialize(encoder, value) + is SimpleSchema -> SimpleSchema.serializer().serialize(encoder, value) + } + } + +} diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/KompendiumSerializersModule.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/KompendiumSerializersModule.kt index 8c4ddac78..0427a955a 100644 --- a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/KompendiumSerializersModule.kt +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/KompendiumSerializersModule.kt @@ -1,44 +1,9 @@ package io.bkbn.kompendium.oas.serialization -import io.bkbn.kompendium.oas.schema.AnyOfSchema -import io.bkbn.kompendium.oas.schema.ArraySchema -import io.bkbn.kompendium.oas.schema.ComponentSchema -import io.bkbn.kompendium.oas.schema.DictionarySchema -import io.bkbn.kompendium.oas.schema.EnumSchema -import io.bkbn.kompendium.oas.schema.FormattedSchema -import io.bkbn.kompendium.oas.schema.FreeFormSchema -import io.bkbn.kompendium.oas.schema.ObjectSchema -import io.bkbn.kompendium.oas.schema.ReferencedSchema -import io.bkbn.kompendium.oas.schema.SimpleSchema -import io.bkbn.kompendium.oas.security.ApiKeyAuth -import io.bkbn.kompendium.oas.security.BasicAuth -import io.bkbn.kompendium.oas.security.BearerAuth -import io.bkbn.kompendium.oas.security.OAuth -import io.bkbn.kompendium.oas.security.SecuritySchema import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.polymorphic object KompendiumSerializersModule { - val module = SerializersModule { - polymorphic(ComponentSchema::class) { - subclass(SimpleSchema::class, SimpleSchema.serializer()) - subclass(FormattedSchema::class, FormattedSchema.serializer()) - subclass(ObjectSchema::class, ObjectSchema.serializer()) - subclass(AnyOfSchema::class, AnyOfSchema.serializer()) - subclass(ArraySchema::class, ArraySchema.serializer()) - subclass(DictionarySchema::class, DictionarySchema.serializer()) - subclass(EnumSchema::class, EnumSchema.serializer()) - subclass(FreeFormSchema::class, FreeFormSchema.serializer()) - subclass(ReferencedSchema::class, ReferencedSchema.serializer()) - } - polymorphic(SecuritySchema::class) { - subclass(ApiKeyAuth::class, ApiKeyAuth.serializer()) - subclass(BasicAuth::class, BasicAuth.serializer()) - subclass(BearerAuth::class, BearerAuth.serializer()) - subclass(OAuth::class, OAuth.serializer()) - } contextual(Any::class, AnySerializer()) } - } diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/NumberSerializer.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/NumberSerializer.kt index 5b30e105d..7313dc5a6 100644 --- a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/NumberSerializer.kt +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/NumberSerializer.kt @@ -18,6 +18,10 @@ object NumberSerializer : KSerializer { override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Number", PrimitiveKind.DOUBLE) override fun serialize(encoder: Encoder, value: Number) { - encoder.encodeString(value.toString()) + when (value) { + is Int -> encoder.encodeInt(value) + is Double -> encoder.encodeDouble(value) + else -> error("Invalid OpenApi Number $value") + } } } diff --git a/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/SecuritySchemaSerializer.kt b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/SecuritySchemaSerializer.kt new file mode 100644 index 000000000..da20ad296 --- /dev/null +++ b/kompendium-oas/src/main/kotlin/io/bkbn/kompendium/oas/serialization/SecuritySchemaSerializer.kt @@ -0,0 +1,34 @@ +package io.bkbn.kompendium.oas.serialization + +import io.bkbn.kompendium.oas.security.ApiKeyAuth +import io.bkbn.kompendium.oas.security.BasicAuth +import io.bkbn.kompendium.oas.security.BearerAuth +import io.bkbn.kompendium.oas.security.OAuth +import io.bkbn.kompendium.oas.security.SecuritySchema +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializer +import kotlinx.serialization.descriptors.PrimitiveKind +import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@OptIn(ExperimentalSerializationApi::class) +@Serializer(forClass = SecuritySchema::class) +object SecuritySchemaSerializer : KSerializer { + override fun deserialize(decoder: Decoder): SecuritySchema { + error("Abandon all hope ye who enter 💀") + } + + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("SecuritySchema", PrimitiveKind.STRING) + + override fun serialize(encoder: Encoder, value: SecuritySchema) { + when (value) { + is ApiKeyAuth -> ApiKeyAuth.serializer().serialize(encoder, value) + is BasicAuth -> BasicAuth.serializer().serialize(encoder, value) + is BearerAuth -> BearerAuth.serializer().serialize(encoder, value) + is OAuth -> OAuth.serializer().serialize(encoder, value) + } + } +} diff --git a/kompendium-playground/build.gradle.kts b/kompendium-playground/build.gradle.kts index 9d7d75c5c..0d81c614c 100644 --- a/kompendium-playground/build.gradle.kts +++ b/kompendium-playground/build.gradle.kts @@ -36,7 +36,7 @@ dependencies { implementation("org.slf4j:slf4j-simple:1.7.35") - implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.1") + implementation(group = "org.jetbrains.kotlinx", "kotlinx-serialization-json", version = "1.3.2") implementation(group = "joda-time", name = "joda-time", version = "2.10.13") } diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt index 59a5d075d..e9236bb63 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/AuthPlayground.kt @@ -39,7 +39,7 @@ fun main() { // Application Module private fun Application.mainModule() { install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } install(Kompendium) { spec = Util.baseSpec diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt index b6a9a729d..60b8226c0 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/BasicPlayground.kt @@ -15,7 +15,6 @@ import io.bkbn.kompendium.core.metadata.method.DeleteInfo import io.bkbn.kompendium.core.metadata.method.GetInfo import io.bkbn.kompendium.core.metadata.method.PostInfo import io.bkbn.kompendium.core.routes.redoc -import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.bkbn.kompendium.playground.BasicModels.BasicParameters import io.bkbn.kompendium.playground.BasicModels.BasicRequest import io.bkbn.kompendium.playground.BasicModels.BasicResponse @@ -38,7 +37,6 @@ import io.ktor.server.engine.embeddedServer import io.ktor.server.netty.Netty import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable -import kotlinx.serialization.json.Json import java.util.UUID /** @@ -57,7 +55,7 @@ fun main() { private fun Application.mainModule() { // Installs Simple JSON Content Negotiation install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } // Installs the Kompendium Plugin and sets up baseline server metadata install(Kompendium) { @@ -76,7 +74,7 @@ private fun Application.mainModule() { call.respond(HttpStatusCode.NoContent) } // It can also infer path parameters - route("/{a}") { + route("/testerino/{a}") { notarizedGet(simpleGetExampleWithParameters) { val a = call.parameters["a"] ?: error("Unable to read expected path parameter") val b = call.request.queryParameters["b"]?.toInt() ?: error("Unable to read expected query parameter") diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ConstraintPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ConstraintPlayground.kt index 34bc2449c..bbe7369b4 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ConstraintPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ConstraintPlayground.kt @@ -54,7 +54,7 @@ fun main() { private fun Application.mainModule() { // Installs Simple JSON Content Negotiation install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } // Installs the Kompendium Plugin and sets up baseline server metadata install(Kompendium) { diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt index 490a90bf1..ec0dda8c9 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/ExceptionPlayground.kt @@ -36,7 +36,7 @@ fun main() { private fun Application.mainModule() { // Installs Simple JSON Content Negotiation install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } // Installs the Kompendium Plugin and sets up baseline server metadata install(Kompendium) { diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GenericPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GenericPlayground.kt index c6b1fff52..d9dc487bb 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GenericPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/GenericPlayground.kt @@ -35,7 +35,7 @@ fun main() { private fun Application.mainModule() { // Installs Simple JSON Content Negotiation install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } // Installs the Kompendium Plugin and sets up baseline server metadata install(Kompendium) { diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt index 56d7081e5..adb16a4c9 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/LocationPlayground.kt @@ -39,7 +39,7 @@ fun main() { private fun Application.mainModule() { install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } install(Kompendium) { spec = Util.baseSpec diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PolymorphicPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PolymorphicPlayground.kt index 1fc0f152f..669e19ee5 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PolymorphicPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/PolymorphicPlayground.kt @@ -34,7 +34,7 @@ fun main() { private fun Application.mainModule() { // Installs Simple JSON Content Negotiation install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } // Installs the Kompendium Plugin and sets up baseline server metadata install(Kompendium) { diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/RecursionPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/RecursionPlayground.kt index caacb398c..7132aa612 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/RecursionPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/RecursionPlayground.kt @@ -58,7 +58,7 @@ fun main() { private fun Application.mainModule() { install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } install(Kompendium) { spec = Util.baseSpec diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SwaggerPlayground.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SwaggerPlayground.kt index 6f4016ed2..11a72632a 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SwaggerPlayground.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/SwaggerPlayground.kt @@ -33,7 +33,7 @@ fun main() { private fun Application.mainModule() { // Installs Simple JSON Content Negotiation install(ContentNegotiation) { - json(json = Util.kotlinxConfig) + json() } install(Webjars) // Installs the Kompendium Plugin and sets up baseline server metadata diff --git a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Util.kt b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Util.kt index c817bada2..774b2f45f 100644 --- a/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Util.kt +++ b/kompendium-playground/src/main/kotlin/io/bkbn/kompendium/playground/util/Util.kt @@ -4,22 +4,12 @@ import io.bkbn.kompendium.oas.OpenApiSpec import io.bkbn.kompendium.oas.info.Contact import io.bkbn.kompendium.oas.info.Info import io.bkbn.kompendium.oas.info.License -import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule import io.bkbn.kompendium.oas.server.Server import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json import java.net.URI @OptIn(ExperimentalSerializationApi::class) object Util { - val kotlinxConfig = Json { - classDiscriminator = "class" - serializersModule = KompendiumSerializersModule.module - prettyPrint = true - explicitNulls = false - encodeDefaults = true - } - val baseSpec = OpenApiSpec( info = Info( title = "Simple Demo API",