feat: enriched enrichments (#566)

This commit is contained in:
Ryan Brink
2024-01-21 14:15:21 -05:00
committed by GitHub
parent c7545b1072
commit ac464ea9ca
49 changed files with 1371 additions and 724 deletions

View File

@ -7,7 +7,7 @@ import kotlin.reflect.typeOf
class RequestInfo private constructor(
val requestType: KType,
val typeEnrichment: TypeEnrichment<*>?,
val enrichment: TypeEnrichment<*>?,
val description: String,
val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>,
@ -60,7 +60,7 @@ class RequestInfo private constructor(
fun build() = RequestInfo(
requestType = requestType ?: error("Request type must be present"),
description = description ?: error("Description must be present"),
typeEnrichment = typeEnrichment,
enrichment = typeEnrichment,
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json"),
required = required ?: true

View File

@ -10,7 +10,7 @@ import kotlin.reflect.typeOf
class ResponseInfo private constructor(
val responseCode: HttpStatusCode,
val responseType: KType,
val typeEnrichment: TypeEnrichment<*>?,
val enrichment: TypeEnrichment<*>?,
val description: String,
val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>,
@ -69,7 +69,7 @@ class ResponseInfo private constructor(
responseCode = responseCode ?: error("You must provide a response code in order to build a Response!"),
responseType = responseType ?: error("You must provide a response type in order to build a Response!"),
description = description ?: error("You must provide a description in order to build a Response!"),
typeEnrichment = typeEnrichment,
enrichment = typeEnrichment,
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json"),
responseHeaders = responseHeaders

View File

@ -51,36 +51,45 @@ object Helpers {
routePath: String,
authMethods: List<String> = emptyList()
) {
val type = this.response.responseType
val enrichment = this.response.enrichment
SchemaGenerator.fromTypeOrUnit(
type = this.response.responseType,
type = type,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = this.response.typeEnrichment,
enrichment = enrichment,
)?.let { schema ->
spec.components.schemas[this.response.responseType.getSlug(this.response.typeEnrichment)] = schema
val slug = type.getSlug(enrichment)
spec.components.schemas[slug] = schema
}
errors.forEach { error ->
val errorEnrichment = error.enrichment
val errorType = error.responseType
SchemaGenerator.fromTypeOrUnit(
type = error.responseType,
type = errorType,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = error.typeEnrichment,
enrichment = errorEnrichment,
)?.let { schema ->
spec.components.schemas[error.responseType.getSlug(error.typeEnrichment)] = schema
val slug = errorType.getSlug(errorEnrichment)
spec.components.schemas[slug] = schema
}
}
when (this) {
is MethodInfoWithRequest -> {
this.request?.let { reqInfo ->
val reqEnrichment = reqInfo.enrichment
val reqType = reqInfo.requestType
SchemaGenerator.fromTypeOrUnit(
type = reqInfo.requestType,
type = reqType,
cache = spec.components.schemas,
schemaConfigurator = schemaConfigurator,
enrichment = reqInfo.typeEnrichment,
enrichment = reqEnrichment,
)?.let { schema ->
spec.components.schemas[reqInfo.requestType.getSlug(reqInfo.typeEnrichment)] = schema
val slug = reqType.getSlug(reqEnrichment)
spec.components.schemas[slug] = schema
}
}
}
@ -127,7 +136,7 @@ object Helpers {
content = reqInfo.requestType.toReferenceContent(
examples = reqInfo.examples,
mediaTypes = reqInfo.mediaTypes,
enrichment = reqInfo.typeEnrichment
enrichment = reqInfo.enrichment
),
required = reqInfo.required
)
@ -142,7 +151,7 @@ object Helpers {
content = this.response.responseType.toReferenceContent(
examples = this.response.examples,
mediaTypes = this.response.mediaTypes,
enrichment = this.response.typeEnrichment
enrichment = this.response.enrichment
)
)
).plus(this.errors.toResponseMap())
@ -174,7 +183,7 @@ object Helpers {
content = error.responseType.toReferenceContent(
examples = error.examples,
mediaTypes = error.mediaTypes,
enrichment = error.typeEnrichment
enrichment = error.enrichment
)
)
}

View File

@ -0,0 +1,167 @@
package io.bkbn.kompendium.core
import dev.forst.ktor.apikey.apiKey
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.customAuthConfig
import io.bkbn.kompendium.core.util.customScopesOnSiblingPathOperations
import io.bkbn.kompendium.core.util.defaultAuthConfig
import io.bkbn.kompendium.core.util.multipleAuthStrategies
import io.bkbn.kompendium.oas.component.Components
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.kotest.core.spec.style.DescribeSpec
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.http.HttpMethod
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.OAuthServerSettings
import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.basic
import io.ktor.server.auth.jwt.jwt
import io.ktor.server.auth.oauth
class KompendiumAuthenticationTest : DescribeSpec({
describe("Authentication") {
it("Can add a default auth config by default") {
TestHelpers.openApiTestAllSerializers(
snapshotName = "T0045__default_auth_config.json",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
)
)
}
) { defaultAuthConfig() }
}
it("Can provide custom auth config with proper scopes") {
TestHelpers.openApiTestAllSerializers(
snapshotName = "T0046__custom_auth_config.json",
applicationSetup = {
install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = "DUMMY_VAL",
clientSecret = "DUMMY_VAL",
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile"),
extraTokenParameters = listOf("access_type" to "offline")
)
}
client = HttpClient(CIO)
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"auth-oauth-google" to OAuth(
flows = OAuth.Flows(
implicit = OAuth.Flows.Implicit(
authorizationUrl = "https://accounts.google.com/o/oauth2/auth",
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
)
)
)
)
}
) { customAuthConfig() }
}
it("Can provide multiple authentication strategies") {
TestHelpers.openApiTestAllSerializers(
snapshotName = "T0047__multiple_auth_strategies.json",
applicationSetup = {
install(Authentication) {
apiKey("api-key") {
headerName = "X-API-KEY"
validate {
UserIdPrincipal("Placeholder")
}
}
jwt("jwt") {
realm = "Server"
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"jwt" to BearerAuth("JWT"),
"api-key" to ApiKeyAuth(ApiKeyAuth.ApiKeyLocation.HEADER, "X-API-KEY")
)
)
)
}
) { multipleAuthStrategies() }
}
it("Can provide different scopes on path operations in the same route") {
TestHelpers.openApiTestAllSerializers(
snapshotName = "T0074__auth_on_specific_path_operation.json",
applicationSetup = {
install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = "DUMMY_VAL",
clientSecret = "DUMMY_VAL",
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile"),
extraTokenParameters = listOf("access_type" to "offline")
)
}
client = HttpClient(CIO)
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"auth-oauth-google" to OAuth(
flows = OAuth.Flows(
implicit = OAuth.Flows.Implicit(
authorizationUrl = "https://accounts.google.com/o/oauth2/auth",
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
)
)
)
)
}
) { customScopesOnSiblingPathOperations() }
}
}
})

View File

@ -0,0 +1,35 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.arrayConstraints
import io.bkbn.kompendium.core.util.doubleConstraints
import io.bkbn.kompendium.core.util.intConstraints
import io.bkbn.kompendium.core.util.stringConstraints
import io.bkbn.kompendium.core.util.stringContentEncodingConstraints
import io.bkbn.kompendium.core.util.stringPatternConstraints
import io.kotest.core.spec.style.DescribeSpec
class KompendiumConstraintsTest : DescribeSpec({
describe("Constraints") {
it("Can apply constraints to an int field") {
TestHelpers.openApiTestAllSerializers("T0059__int_constraints.json") { intConstraints() }
}
it("Can apply constraints to a double field") {
TestHelpers.openApiTestAllSerializers("T0060__double_constraints.json") { doubleConstraints() }
}
it("Can apply a min and max length to a string field") {
TestHelpers.openApiTestAllSerializers("T0061__string_min_max_constraints.json") { stringConstraints() }
}
it("Can apply a pattern to a string field") {
TestHelpers.openApiTestAllSerializers("T0062__string_pattern_constraints.json") { stringPatternConstraints() }
}
it("Can apply a content encoding and media type to a string field") {
TestHelpers.openApiTestAllSerializers("T0063__string_content_encoding_constraints.json") {
stringContentEncodingConstraints()
}
}
it("Can apply constraints to an array field") {
TestHelpers.openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() }
}
}
})

View File

@ -0,0 +1,13 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.defaultParameter
import io.kotest.core.spec.style.DescribeSpec
class KompendiumDefaultsTest : DescribeSpec({
describe("Defaults") {
it("Can generate a default parameter value") {
TestHelpers.openApiTestAllSerializers("T0022__query_with_default_parameter.json") { defaultParameter() }
}
}
})

View File

@ -0,0 +1,35 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.enrichedComplexGenericType
import io.bkbn.kompendium.core.util.enrichedGenericResponse
import io.bkbn.kompendium.core.util.enrichedNestedCollection
import io.bkbn.kompendium.core.util.enrichedSimpleRequest
import io.bkbn.kompendium.core.util.enrichedSimpleResponse
import io.bkbn.kompendium.core.util.enrichedTopLevelCollection
import io.kotest.core.spec.style.DescribeSpec
class KompendiumEnrichmentTest : DescribeSpec({
describe("Enrichment") {
it("Can enrich a simple request") {
TestHelpers.openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() }
}
it("Can enrich a simple response") {
TestHelpers.openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() }
}
it("Can enrich a nested collection") {
TestHelpers.openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() }
}
it("Can enrich a complex generic type") {
TestHelpers.openApiTestAllSerializers(
"T0057__enriched_complex_generic_type.json"
) { enrichedComplexGenericType() }
}
it("Can enrich a generic object") {
TestHelpers.openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() }
}
it("Can enrich a top level list type") {
TestHelpers.openApiTestAllSerializers("T0077__enriched_top_level_list.json") { enrichedTopLevelCollection() }
}
}
})

View File

@ -0,0 +1,45 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.core.util.samePathSameMethod
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.should
import io.kotest.matchers.string.startWith
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.basic
class KompendiumErrorHandlingTest : DescribeSpec({
describe("Error Handling") {
it("Throws a clear exception when an unidentified type is encountered") {
val exception = shouldThrow<UnknownSchemaException> {
TestHelpers.openApiTestAllSerializers(
""
) { dateTimeString() }
}
exception.message should startWith("An unknown type was encountered: class java.time.Instant")
}
it("Throws an exception when same method for same path has been previously registered") {
val exception = shouldThrow<IllegalArgumentException> {
TestHelpers.openApiTestAllSerializers(
snapshotName = "",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
) {
samePathSameMethod()
}
}
exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET")
}
}
})

View File

@ -0,0 +1,27 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exampleSummaryAndDescription
import io.bkbn.kompendium.core.util.optionalReqExample
import io.bkbn.kompendium.core.util.reqRespExamples
import io.kotest.core.spec.style.DescribeSpec
class KompendiumExamplesTest : DescribeSpec({
describe("Examples") {
it("Can generate example response and request bodies") {
TestHelpers.openApiTestAllSerializers("T0020__example_req_and_resp.json") { reqRespExamples() }
}
it("Can describe example parameters") {
TestHelpers.openApiTestAllSerializers("T0021__example_parameters.json") { exampleParams() }
}
it("Can generate example optional request body") {
TestHelpers.openApiTestAllSerializers("T0069__example_optional_req.json") { optionalReqExample() }
}
it("Can generate example summary and description") {
TestHelpers.openApiTestAllSerializers(
"T0075__example_summary_and_description.json"
) { exampleSummaryAndDescription() }
}
}
})

View File

@ -0,0 +1,27 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.genericException
import io.bkbn.kompendium.core.util.multipleExceptions
import io.bkbn.kompendium.core.util.polymorphicException
import io.bkbn.kompendium.core.util.singleException
import io.kotest.core.spec.style.DescribeSpec
class KompendiumExceptionsTest : DescribeSpec({
describe("Exceptions") {
it("Can add an exception status code to a response") {
TestHelpers.openApiTestAllSerializers("T0016__notarized_get_with_exception_response.json") { singleException() }
}
it("Can support multiple response codes") {
TestHelpers.openApiTestAllSerializers("T0017__notarized_get_with_multiple_exception_responses.json") {
multipleExceptions()
}
}
it("Can add a polymorphic exception response") {
TestHelpers.openApiTestAllSerializers("T0018__polymorphic_error_status_codes.json") { polymorphicException() }
}
it("Can add a generic exception response") {
TestHelpers.openApiTestAllSerializers("T0019__generic_exception.json") { genericException() }
}
}
})

View File

@ -0,0 +1,7 @@
package io.bkbn.kompendium.core
import io.kotest.core.spec.style.DescribeSpec
class KompendiumFreeFormTest : DescribeSpec({
// todo Assess strategies here
})

View File

@ -0,0 +1,67 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.gnarlyGenericResponse
import io.bkbn.kompendium.core.util.nestedGenericCollection
import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.nestedGenericResponse
import io.bkbn.kompendium.core.util.overrideSealedTypeIdentifier
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.polymorphicMapResponse
import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.subtypeNotCompleteSetOfParentProperties
import io.kotest.core.spec.style.DescribeSpec
class KompendiumPolymorphismAndGenericsTest : DescribeSpec({
describe("Polymorphism and Generics") {
it("can generate a polymorphic response type") {
TestHelpers.openApiTestAllSerializers("T0027__polymorphic_response.json") { polymorphicResponse() }
}
it("Can generate a collection with polymorphic response type") {
TestHelpers.openApiTestAllSerializers("T0028__polymorphic_list_response.json") { polymorphicCollectionResponse() }
}
it("Can generate a map with a polymorphic response type") {
TestHelpers.openApiTestAllSerializers("T0029__polymorphic_map_response.json") { polymorphicMapResponse() }
}
it("Can generate a response type with a generic type") {
TestHelpers.openApiTestAllSerializers("T0030__simple_generic_response.json") { simpleGenericResponse() }
}
it("Can generate a response type with a nested generic type") {
TestHelpers.openApiTestAllSerializers("T0031__nested_generic_response.json") { nestedGenericResponse() }
}
it("Can generate a polymorphic response type with generics") {
TestHelpers.openApiTestAllSerializers(
"T0032__polymorphic_response_with_generics.json"
) { genericPolymorphicResponse() }
}
it("Can handle an absolutely psycho inheritance test") {
TestHelpers.openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") {
genericPolymorphicResponseMultipleImpls()
}
}
it("Can support nested generic collections") {
TestHelpers.openApiTestAllSerializers("T0039__nested_generic_collection.json") { nestedGenericCollection() }
}
it("Can support nested generics with multiple type parameters") {
TestHelpers.openApiTestAllSerializers("T0040__nested_generic_multiple_type_params.json") {
nestedGenericMultipleParamsCollection()
}
}
it("Can handle a really gnarly generic example") {
TestHelpers.openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() }
}
it("Can override the type name for a sealed interface implementation") {
TestHelpers.openApiTestAllSerializers("T0070__sealed_interface_type_name_override.json") {
overrideSealedTypeIdentifier()
}
}
it("Can serialize an object where the subtype is not a complete set of parent properties") {
TestHelpers.openApiTestAllSerializers("T0071__subtype_not_complete_set_of_parent_properties.json") {
subtypeNotCompleteSetOfParentProperties()
}
}
}
})

View File

@ -0,0 +1,25 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.nonRequiredParam
import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.requiredParams
import io.kotest.core.spec.style.DescribeSpec
class KompendiumRequiredFieldsTest : DescribeSpec({
describe("Required Fields") {
it("Marks a parameter as required if there is no default and it is not marked nullable") {
TestHelpers.openApiTestAllSerializers("T0023__required_param.json") { requiredParams() }
}
it("Can mark a parameter as not required") {
TestHelpers.openApiTestAllSerializers("T0024__non_required_param.json") { nonRequiredParam() }
}
it("Does not mark a field as required if a default value is provided") {
TestHelpers.openApiTestAllSerializers("T0025__default_field.json") { defaultField() }
}
it("Does not mark a nullable field as required") {
TestHelpers.openApiTestAllSerializers("T0026__nullable_field.json") { nullableField() }
}
}
})

View File

@ -0,0 +1,29 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.nestedUnderRoot
import io.bkbn.kompendium.core.util.paramWrapper
import io.bkbn.kompendium.core.util.rootRoute
import io.bkbn.kompendium.core.util.simplePathParsing
import io.bkbn.kompendium.core.util.trailingSlash
import io.kotest.core.spec.style.DescribeSpec
class KompendiumRouteParsingTest : DescribeSpec({
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
TestHelpers.openApiTestAllSerializers("T0012__path_parser.json") { simplePathParsing() }
}
it("Can notarize the root route") {
TestHelpers.openApiTestAllSerializers("T0013__root_route.json") { rootRoute() }
}
it("Can notarize a route under the root module without appending trailing slash") {
TestHelpers.openApiTestAllSerializers("T0014__nested_under_root.json") { nestedUnderRoot() }
}
it("Can notarize a route with a trailing slash") {
TestHelpers.openApiTestAllSerializers("T0015__trailing_slash.json") { trailingSlash() }
}
it("Can notarize a route with a parameter") {
TestHelpers.openApiTestAllSerializers("T0068__param_wrapper.json") { paramWrapper() }
}
}
})

View File

@ -1,39 +1,13 @@
package io.bkbn.kompendium.core
import dev.forst.ktor.apikey.apiKey
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
import io.bkbn.kompendium.core.util.arrayConstraints
import io.bkbn.kompendium.core.util.complexRequest
import io.bkbn.kompendium.core.util.customAuthConfig
import io.bkbn.kompendium.core.util.customFieldNameResponse
import io.bkbn.kompendium.core.util.customScopesOnSiblingPathOperations
import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.core.util.defaultAuthConfig
import io.bkbn.kompendium.core.util.defaultField
import io.bkbn.kompendium.core.util.defaultParameter
import io.bkbn.kompendium.core.util.doubleConstraints
import io.bkbn.kompendium.core.util.enrichedComplexGenericType
import io.bkbn.kompendium.core.util.enrichedGenericResponse
import io.bkbn.kompendium.core.util.enrichedNestedCollection
import io.bkbn.kompendium.core.util.enrichedSimpleRequest
import io.bkbn.kompendium.core.util.enrichedSimpleResponse
import io.bkbn.kompendium.core.util.exampleParams
import io.bkbn.kompendium.core.util.exampleSummaryAndDescription
import io.bkbn.kompendium.core.util.fieldOutsideConstructor
import io.bkbn.kompendium.core.util.genericException
import io.bkbn.kompendium.core.util.genericPolymorphicResponse
import io.bkbn.kompendium.core.util.genericPolymorphicResponseMultipleImpls
import io.bkbn.kompendium.core.util.gnarlyGenericResponse
import io.bkbn.kompendium.core.util.headerParameter
import io.bkbn.kompendium.core.util.ignoredFieldsResponse
import io.bkbn.kompendium.core.util.intConstraints
import io.bkbn.kompendium.core.util.multipleAuthStrategies
import io.bkbn.kompendium.core.util.multipleExceptions
import io.bkbn.kompendium.core.util.nestedGenericCollection
import io.bkbn.kompendium.core.util.nestedGenericMultipleParamsCollection
import io.bkbn.kompendium.core.util.nestedGenericResponse
import io.bkbn.kompendium.core.util.nestedTypeName
import io.bkbn.kompendium.core.util.nestedUnderRoot
import io.bkbn.kompendium.core.util.nonRequiredParam
import io.bkbn.kompendium.core.util.nonRequiredParams
import io.bkbn.kompendium.core.util.notarizedDelete
@ -47,68 +21,34 @@ import io.bkbn.kompendium.core.util.nullableEnumField
import io.bkbn.kompendium.core.util.nullableField
import io.bkbn.kompendium.core.util.nullableNestedObject
import io.bkbn.kompendium.core.util.nullableReference
import io.bkbn.kompendium.core.util.optionalReqExample
import io.bkbn.kompendium.core.util.overrideMediaTypes
import io.bkbn.kompendium.core.util.overrideSealedTypeIdentifier
import io.bkbn.kompendium.core.util.paramWrapper
import io.bkbn.kompendium.core.util.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.polymorphicException
import io.bkbn.kompendium.core.util.polymorphicMapResponse
import io.bkbn.kompendium.core.util.polymorphicResponse
import io.bkbn.kompendium.core.util.postNoReqBody
import io.bkbn.kompendium.core.util.primitives
import io.bkbn.kompendium.core.util.reqRespExamples
import io.bkbn.kompendium.core.util.requiredParams
import io.bkbn.kompendium.core.util.responseHeaders
import io.bkbn.kompendium.core.util.returnsEnumList
import io.bkbn.kompendium.core.util.returnsList
import io.bkbn.kompendium.core.util.rootRoute
import io.bkbn.kompendium.core.util.samePathDifferentMethodsAndAuth
import io.bkbn.kompendium.core.util.samePathSameMethod
import io.bkbn.kompendium.core.util.simpleGenericResponse
import io.bkbn.kompendium.core.util.simplePathParsing
import io.bkbn.kompendium.core.util.simpleRecursive
import io.bkbn.kompendium.core.util.singleException
import io.bkbn.kompendium.core.util.stringConstraints
import io.bkbn.kompendium.core.util.stringContentEncodingConstraints
import io.bkbn.kompendium.core.util.stringPatternConstraints
import io.bkbn.kompendium.core.util.subtypeNotCompleteSetOfParentProperties
import io.bkbn.kompendium.core.util.topLevelNullable
import io.bkbn.kompendium.core.util.trailingSlash
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
import io.bkbn.kompendium.core.util.withOperationId
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
import io.bkbn.kompendium.oas.component.Components
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.serialization.KompendiumSerializersModule
import io.kotest.assertions.throwables.shouldThrow
import io.kotest.core.spec.style.DescribeSpec
import io.kotest.matchers.should
import io.kotest.matchers.string.startWith
import io.ktor.client.HttpClient
import io.ktor.client.engine.cio.CIO
import io.ktor.http.HttpMethod
import io.ktor.serialization.kotlinx.json.json
import io.ktor.server.application.call
import io.ktor.server.application.install
import io.ktor.server.auth.Authentication
import io.ktor.server.auth.OAuthServerSettings
import io.ktor.server.auth.UserIdPrincipal
import io.ktor.server.auth.basic
import io.ktor.server.auth.jwt.jwt
import io.ktor.server.auth.oauth
import io.ktor.server.response.respondText
import io.ktor.server.routing.get
import io.ktor.server.routing.route
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.net.URI
import java.time.Instant
import kotlin.reflect.typeOf
class KompendiumTest : DescribeSpec({
describe("Notarized Open API Metadata Tests") {
@ -155,56 +95,6 @@ class KompendiumTest : DescribeSpec({
openApiTestAllSerializers("T0066__notarized_get_with_response_headers.json") { responseHeaders() }
}
}
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
openApiTestAllSerializers("T0012__path_parser.json") { simplePathParsing() }
}
it("Can notarize the root route") {
openApiTestAllSerializers("T0013__root_route.json") { rootRoute() }
}
it("Can notarize a route under the root module without appending trailing slash") {
openApiTestAllSerializers("T0014__nested_under_root.json") { nestedUnderRoot() }
}
it("Can notarize a route with a trailing slash") {
openApiTestAllSerializers("T0015__trailing_slash.json") { trailingSlash() }
}
it("Can notarize a route with a parameter") {
openApiTestAllSerializers("T0068__param_wrapper.json") { paramWrapper() }
}
}
describe("Exceptions") {
it("Can add an exception status code to a response") {
openApiTestAllSerializers("T0016__notarized_get_with_exception_response.json") { singleException() }
}
it("Can support multiple response codes") {
openApiTestAllSerializers("T0017__notarized_get_with_multiple_exception_responses.json") { multipleExceptions() }
}
it("Can add a polymorphic exception response") {
openApiTestAllSerializers("T0018__polymorphic_error_status_codes.json") { polymorphicException() }
}
it("Can add a generic exception response") {
openApiTestAllSerializers("T0019__generic_exception.json") { genericException() }
}
}
describe("Examples") {
it("Can generate example response and request bodies") {
openApiTestAllSerializers("T0020__example_req_and_resp.json") { reqRespExamples() }
}
it("Can describe example parameters") {
openApiTestAllSerializers("T0021__example_parameters.json") { exampleParams() }
}
it("Can generate example optional request body") {
openApiTestAllSerializers("T0069__example_optional_req.json") { optionalReqExample() }
}
it("Can generate example summary and description") {
openApiTestAllSerializers("T0075__example_summary_and_description.json") { exampleSummaryAndDescription() }
}
}
describe("Defaults") {
it("Can generate a default parameter value") {
openApiTestAllSerializers("T0022__query_with_default_parameter.json") { defaultParameter() }
}
}
describe("Required Fields") {
it("Marks a parameter as required if there is no default and it is not marked nullable") {
openApiTestAllSerializers("T0023__required_param.json") { requiredParams() }
@ -219,377 +109,121 @@ class KompendiumTest : DescribeSpec({
openApiTestAllSerializers("T0026__nullable_field.json") { nullableField() }
}
}
describe("Polymorphism and Generics") {
it("can generate a polymorphic response type") {
openApiTestAllSerializers("T0027__polymorphic_response.json") { polymorphicResponse() }
describe("Custom Serializable Reader tests") {
it("Can support ignoring fields") {
openApiTestAllSerializers("T0048__ignored_property.json") { ignoredFieldsResponse() }
}
it("Can generate a collection with polymorphic response type") {
openApiTestAllSerializers("T0028__polymorphic_list_response.json") { polymorphicCollectionResponse() }
it("Can support un-backed fields") {
openApiTestAllSerializers("T0049__unbacked_property.json") { unbackedFieldsResponse() }
}
it("Can generate a map with a polymorphic response type") {
openApiTestAllSerializers("T0029__polymorphic_map_response.json") { polymorphicMapResponse() }
it("Can support custom named fields") {
openApiTestAllSerializers("T0050__custom_named_property.json") { customFieldNameResponse() }
}
it("Can generate a response type with a generic type") {
openApiTestAllSerializers("T0030__simple_generic_response.json") { simpleGenericResponse() }
}
describe("Miscellaneous") {
xit("Can generate the necessary ReDoc home page") {
// TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
}
it("Can generate a response type with a nested generic type") {
openApiTestAllSerializers("T0031__nested_generic_response.json") { nestedGenericResponse() }
it("Can add an operation id to a notarized route") {
openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() }
}
it("Can generate a polymorphic response type with generics") {
openApiTestAllSerializers("T0032__polymorphic_response_with_generics.json") { genericPolymorphicResponse() }
xit("Can add an undeclared field") {
// TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() }
}
it("Can handle an absolutely psycho inheritance test") {
openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
it("Can add a custom header parameter with a name override") {
openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() }
}
it("Can support nested generic collections") {
openApiTestAllSerializers("T0039__nested_generic_collection.json") { nestedGenericCollection() }
xit("Can override field name") {
// TODO Assess strategies here
}
it("Can support nested generics with multiple type parameters") {
openApiTestAllSerializers("T0040__nested_generic_multiple_type_params.json") {
nestedGenericMultipleParamsCollection()
}
it("Can serialize a recursive type") {
openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() }
}
it("Can handle a really gnarly generic example") {
openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() }
it("Nullable fields do not lead to doom") {
openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() }
}
it("Can override the type name for a sealed interface implementation") {
openApiTestAllSerializers("T0070__sealed_interface_type_name_override.json") { overrideSealedTypeIdentifier() }
it("Can have a nullable enum as a member field") {
openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() }
}
it("Can serialize an object where the subtype is not a complete set of parent properties") {
openApiTestAllSerializers("T0071__subtype_not_complete_set_of_parent_properties.json") {
subtypeNotCompleteSetOfParentProperties()
}
it("Can have a list of enums as a field") {
openApiTestAllSerializers("T0076__list_of_enums.json") { returnsEnumList() }
}
describe("Custom Serializable Reader tests") {
it("Can support ignoring fields") {
openApiTestAllSerializers("T0048__ignored_property.json") { ignoredFieldsResponse() }
}
it("Can support un-backed fields") {
openApiTestAllSerializers("T0049__unbacked_property.json") { unbackedFieldsResponse() }
}
it("Can support custom named fields") {
openApiTestAllSerializers("T0050__custom_named_property.json") { customFieldNameResponse() }
}
it("Can have a nullable reference without impacting base type") {
openApiTestAllSerializers("T0041__nullable_reference.json") { nullableReference() }
}
describe("Miscellaneous") {
xit("Can generate the necessary ReDoc home page") {
// TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
}
it("Can add an operation id to a notarized route") {
openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() }
}
xit("Can add an undeclared field") {
// TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() }
}
it("Can add a custom header parameter with a name override") {
openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() }
}
xit("Can override field name") {
// TODO Assess strategies here
}
it("Can serialize a recursive type") {
openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() }
}
it("Nullable fields do not lead to doom") {
openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() }
}
it("Can have a nullable enum as a member field") {
openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() }
}
it("Can have a list of enums as a field") {
openApiTestAllSerializers("T0076__list_of_enums.json") { returnsEnumList() }
}
it("Can have a nullable reference without impacting base type") {
openApiTestAllSerializers("T0041__nullable_reference.json") { nullableReference() }
}
it("Can handle nested type names") {
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
}
it("Can handle top level nullable types") {
openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() }
}
it("Can handle multiple registrations for different methods with the same path and different auth") {
openApiTestAllSerializers(
"T0053__same_path_different_methods_and_auth.json",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
it("Can handle nested type names") {
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
}
it("Can handle top level nullable types") {
openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() }
}
it("Can handle multiple registrations for different methods with the same path and different auth") {
openApiTestAllSerializers(
"T0053__same_path_different_methods_and_auth.json",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
)
}
) { samePathDifferentMethodsAndAuth() }
}
it("Can generate paths without application root-path") {
openApiTestAllSerializers(
"T0054__app_with_rootpath.json",
applicationEnvironmentBuilder = {
rootPath = "/example"
},
specOverrides = {
copy(
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
)
}
) { notarizedGet() }
}
it("Can apply a custom serialization strategy to the openapi document") {
val customJsonEncoder = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
)
}
openApiTestAllSerializers(
snapshotName = "T0072__custom_serialization_strategy.json",
notarizedApplicationConfigOverrides = {
specRoute = { spec, routing ->
routing {
route("/openapi.json") {
get {
call.response.headers.append("Content-Type", "application/json")
call.respondText { customJsonEncoder.encodeToString(spec) }
}
}
}
}
},
contentNegotiation = {
json(
Json {
encodeDefaults = true
explicitNulls = true
}
)
}
) { notarizedGet() }
}
it("Can serialize a data class with a field outside of the constructor") {
openApiTestAllSerializers("T0073__data_class_with_field_outside_constructor.json") { fieldOutsideConstructor() }
}
) { samePathDifferentMethodsAndAuth() }
}
describe("Error Handling") {
it("Throws a clear exception when an unidentified type is encountered") {
val exception = shouldThrow<UnknownSchemaException> { openApiTestAllSerializers("") { dateTimeString() } }
exception.message should startWith("An unknown type was encountered: class java.time.Instant")
}
it("Throws an exception when same method for same path has been previously registered") {
val exception = shouldThrow<IllegalArgumentException> {
openApiTestAllSerializers(
snapshotName = "",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
) {
samePathSameMethod()
}
it("Can generate paths without application root-path") {
openApiTestAllSerializers(
"T0054__app_with_rootpath.json",
applicationEnvironmentBuilder = {
rootPath = "/example"
},
specOverrides = {
copy(
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
)
}
exception.message should startWith("A route has already been registered for path: /test/{a} and method: GET")
}
) { notarizedGet() }
}
describe("Formats") {
it("Can set a format for a simple type schema") {
openApiTestAllSerializers(
snapshotName = "T0038__formatted_date_time_string.json",
customTypes = mapOf(typeOf<Instant>() to TypeDefinition(type = "string", format = "date"))
) { dateTimeString() }
it("Can apply a custom serialization strategy to the openapi document") {
val customJsonEncoder = Json {
serializersModule = KompendiumSerializersModule.module
encodeDefaults = true
explicitNulls = false
}
}
describe("Free Form") {
// todo Assess strategies here
}
describe("Authentication") {
it("Can add a default auth config by default") {
openApiTestAllSerializers(
snapshotName = "T0045__default_auth_config.json",
applicationSetup = {
install(Authentication) {
basic("basic") {
realm = "Ktor Server"
validate { UserIdPrincipal("Placeholder") }
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"basic" to BasicAuth()
)
)
)
}
) { defaultAuthConfig() }
}
it("Can provide custom auth config with proper scopes") {
openApiTestAllSerializers(
snapshotName = "T0046__custom_auth_config.json",
applicationSetup = {
install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = "DUMMY_VAL",
clientSecret = "DUMMY_VAL",
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile"),
extraTokenParameters = listOf("access_type" to "offline")
)
}
client = HttpClient(CIO)
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"auth-oauth-google" to OAuth(
flows = OAuth.Flows(
implicit = OAuth.Flows.Implicit(
authorizationUrl = "https://accounts.google.com/o/oauth2/auth",
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
)
)
)
)
}
) { customAuthConfig() }
}
it("Can provide multiple authentication strategies") {
openApiTestAllSerializers(
snapshotName = "T0047__multiple_auth_strategies.json",
applicationSetup = {
install(Authentication) {
apiKey("api-key") {
headerName = "X-API-KEY"
validate {
UserIdPrincipal("Placeholder")
openApiTestAllSerializers(
snapshotName = "T0072__custom_serialization_strategy.json",
notarizedApplicationConfigOverrides = {
specRoute = { spec, routing ->
routing {
route("/openapi.json") {
get {
call.response.headers.append("Content-Type", "application/json")
call.respondText { customJsonEncoder.encodeToString(spec) }
}
}
jwt("jwt") {
realm = "Server"
}
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"jwt" to BearerAuth("JWT"),
"api-key" to ApiKeyAuth(ApiKeyAuth.ApiKeyLocation.HEADER, "X-API-KEY")
)
)
)
}
) { multipleAuthStrategies() }
}
it("Can provide different scopes on path operations in the same route") {
openApiTestAllSerializers(
snapshotName = "T0074__auth_on_specific_path_operation.json",
applicationSetup = {
install(Authentication) {
oauth("auth-oauth-google") {
urlProvider = { "http://localhost:8080/callback" }
providerLookup = {
OAuthServerSettings.OAuth2ServerSettings(
name = "google",
authorizeUrl = "https://accounts.google.com/o/oauth2/auth",
accessTokenUrl = "https://accounts.google.com/o/oauth2/token",
requestMethod = HttpMethod.Post,
clientId = "DUMMY_VAL",
clientSecret = "DUMMY_VAL",
defaultScopes = listOf("https://www.googleapis.com/auth/userinfo.profile"),
extraTokenParameters = listOf("access_type" to "offline")
)
}
client = HttpClient(CIO)
}
},
contentNegotiation = {
json(
Json {
encodeDefaults = true
explicitNulls = true
}
},
specOverrides = {
this.copy(
components = Components(
securitySchemes = mutableMapOf(
"auth-oauth-google" to OAuth(
flows = OAuth.Flows(
implicit = OAuth.Flows.Implicit(
authorizationUrl = "https://accounts.google.com/o/oauth2/auth",
scopes = mapOf(
"write:pets" to "modify pets in your account",
"read:pets" to "read your pets"
)
)
)
)
)
)
)
}
) { customScopesOnSiblingPathOperations() }
}
}
describe("Enrichment") {
it("Can enrich a simple request") {
openApiTestAllSerializers("T0055__enriched_simple_request.json") { enrichedSimpleRequest() }
}
it("Can enrich a simple response") {
openApiTestAllSerializers("T0058__enriched_simple_response.json") { enrichedSimpleResponse() }
}
it("Can enrich a nested collection") {
openApiTestAllSerializers("T0056__enriched_nested_collection.json") { enrichedNestedCollection() }
}
it("Can enrich a complex generic type") {
openApiTestAllSerializers("T0057__enriched_complex_generic_type.json") { enrichedComplexGenericType() }
}
it("Can enrich a generic object") {
openApiTestAllSerializers("T0067__enriched_generic_object.json") { enrichedGenericResponse() }
}
}
describe("Constraints") {
it("Can apply constraints to an int field") {
openApiTestAllSerializers("T0059__int_constraints.json") { intConstraints() }
}
it("Can apply constraints to a double field") {
openApiTestAllSerializers("T0060__double_constraints.json") { doubleConstraints() }
}
it("Can apply a min and max length to a string field") {
openApiTestAllSerializers("T0061__string_min_max_constraints.json") { stringConstraints() }
}
it("Can apply a pattern to a string field") {
openApiTestAllSerializers("T0062__string_pattern_constraints.json") { stringPatternConstraints() }
}
it("Can apply a content encoding and media type to a string field") {
openApiTestAllSerializers("T0063__string_content_encoding_constraints.json") {
stringContentEncodingConstraints()
)
}
}
it("Can apply constraints to an array field") {
openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() }
}
) { notarizedGet() }
}
it("Can serialize a data class with a field outside of the constructor") {
openApiTestAllSerializers("T0073__data_class_with_field_outside_constructor.json") { fieldOutsideConstructor() }
}
}
})

View File

@ -0,0 +1,19 @@
package io.bkbn.kompendium.core
import io.bkbn.kompendium.core.fixtures.TestHelpers
import io.bkbn.kompendium.core.util.dateTimeString
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.kotest.core.spec.style.DescribeSpec
import java.time.Instant
import kotlin.reflect.typeOf
class KompendiumTypeFormatTest : DescribeSpec({
describe("Formats") {
it("Can set a format for a simple type schema") {
TestHelpers.openApiTestAllSerializers(
snapshotName = "T0038__formatted_date_time_string.json",
customTypes = mapOf(typeOf<Instant>() to TypeDefinition(type = "string", format = "date"))
) { dateTimeString() }
}
}
})

View File

@ -7,7 +7,10 @@ import io.bkbn.kompendium.core.fixtures.TestNested
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.util.TestModules.defaultPath
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.CollectionEnrichment
import io.bkbn.kompendium.enrichment.NumberEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.enrichment.StringEnrichment
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Routing
@ -23,15 +26,17 @@ fun Routing.intConstraints() {
responseCode(HttpStatusCode.OK)
description("An int")
responseType(
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
TestCreatedResponse::id {
minimum = 2
maximum = 100
multipleOf = 2
NumberEnrichment("blah-blah-blah") {
minimum = 2
maximum = 100
multipleOf = 2
}
}
responseCode(HttpStatusCode.OK)
}
)
responseCode(HttpStatusCode.OK)
}
}
}
@ -48,11 +53,13 @@ fun Routing.doubleConstraints() {
responseCode(HttpStatusCode.OK)
description("A double")
responseType(
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
DoubleResponse::payload {
minimum = 2.0
maximum = 100.0
multipleOf = 2.0
NumberEnrichment("blah-blah-blah") {
minimum = 2.0
maximum = 100.0
multipleOf = 2.0
}
}
}
)
@ -73,10 +80,12 @@ fun Routing.stringConstraints() {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
TestNested::nesty {
maxLength = 10
minLength = 2
StringEnrichment("blah") {
maxLength = 10
minLength = 2
}
}
}
)
@ -97,9 +106,11 @@ fun Routing.stringPatternConstraints() {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
TestNested::nesty {
pattern = "[a-z]+"
StringEnrichment("blah") {
pattern = "[a-z]+"
}
}
}
)
@ -120,10 +131,12 @@ fun Routing.stringContentEncodingConstraints() {
responseCode(HttpStatusCode.OK)
description("A string")
responseType(
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
TestNested::nesty {
contentEncoding = "base64"
contentMediaType = "image/png"
StringEnrichment("blah") {
contentEncoding = "base64"
contentMediaType = "image/png"
}
}
}
)
@ -144,11 +157,13 @@ fun Routing.arrayConstraints() {
responseCode(HttpStatusCode.OK)
description("An array")
responseType(
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
Page<String>::content {
minItems = 2
maxItems = 10
uniqueItems = true
CollectionEnrichment<String>("blah") {
minItems = 2
maxItems = 10
uniqueItems = true
}
}
}
)

View File

@ -1,16 +1,20 @@
package io.bkbn.kompendium.core.util
import io.bkbn.kompendium.core.fixtures.ComplexRequest
import io.bkbn.kompendium.core.fixtures.GenericObject
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
import io.bkbn.kompendium.core.fixtures.TestResponse
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
import io.bkbn.kompendium.core.fixtures.GenericObject
import io.bkbn.kompendium.core.metadata.GetInfo
import io.bkbn.kompendium.core.metadata.PostInfo
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.CollectionEnrichment
import io.bkbn.kompendium.enrichment.MapEnrichment
import io.bkbn.kompendium.enrichment.NumberEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.enrichment.StringEnrichment
import io.ktor.http.HttpStatusCode
import io.ktor.server.application.install
import io.ktor.server.routing.Routing
@ -24,9 +28,11 @@ fun Routing.enrichedSimpleResponse() {
description(TestModules.defaultPathDescription)
response {
responseType(
enrichment = TypeEnrichment("simple") {
enrichment = ObjectEnrichment("simple") {
TestResponse::c {
description = "A simple description"
StringEnrichment("blah-blah-blah") {
description = "A simple description"
}
}
}
)
@ -47,12 +53,16 @@ fun Routing.enrichedSimpleRequest() {
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
enrichment = ObjectEnrichment("simple") {
TestSimpleRequest::a {
description = "A simple description"
StringEnrichment("blah-blah-blah") {
description = "A simple description"
}
}
TestSimpleRequest::b {
deprecated = true
NumberEnrichment("blah-blah-blah") {
deprecated = true
}
}
}
)
@ -77,12 +87,52 @@ fun Routing.enrichedNestedCollection() {
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
enrichment = ObjectEnrichment("simple") {
ComplexRequest::tables {
description = "A nested item"
typeEnrichment = TypeEnrichment("nested") {
NestedComplexItem::name {
description = "A nested description"
CollectionEnrichment<NestedComplexItem>("blah-blah") {
description = "A nested description"
itemEnrichment = ObjectEnrichment("nested") {
NestedComplexItem::name {
StringEnrichment("beleheh") {
description = "A nested description"
}
}
}
}
}
}
)
description("A test request")
}
response {
responseCode(HttpStatusCode.Created)
responseType<TestCreatedResponse>()
description(TestModules.defaultResponseDescription)
}
}
}
}
}
fun Routing.enrichedTopLevelCollection() {
route("/example") {
install(NotarizedRoute()) {
parameters = TestModules.defaultParams
post = PostInfo.builder {
summary(TestModules.defaultPathSummary)
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = CollectionEnrichment<List<TestSimpleRequest>>("blah-blah") {
itemEnrichment = ObjectEnrichment("simple") {
TestSimpleRequest::a {
StringEnrichment("blah-blah-blah") {
description = "A simple description"
}
}
TestSimpleRequest::b {
NumberEnrichment("blah-blah-blah") {
deprecated = true
}
}
}
@ -109,15 +159,21 @@ fun Routing.enrichedComplexGenericType() {
description(TestModules.defaultPathDescription)
request {
requestType(
enrichment = TypeEnrichment("simple") {
enrichment = ObjectEnrichment("simple") {
MultiNestedGenerics<String, ComplexRequest>::content {
description = "Getting pretty crazy"
typeEnrichment = TypeEnrichment("nested") {
ComplexRequest::tables {
description = "A nested item"
typeEnrichment = TypeEnrichment("nested") {
NestedComplexItem::name {
MapEnrichment<ComplexRequest>("blah") {
description = "A nested description"
valueEnrichment = ObjectEnrichment("nested") {
ComplexRequest::tables {
CollectionEnrichment<NestedComplexItem>("blah-blah") {
description = "A nested description"
itemEnrichment = ObjectEnrichment("nested") {
NestedComplexItem::name {
StringEnrichment("beleheh") {
description = "A nested description"
}
}
}
}
}
}
@ -145,15 +201,20 @@ fun Routing.enrichedGenericResponse() {
description(TestModules.defaultPathDescription)
response {
responseType(
enrichment = TypeEnrichment("generic") {
enrichment = ObjectEnrichment("generic") {
description = "another description"
GenericObject<TestSimpleRequest>::data {
description = "A simple description"
typeEnrichment = TypeEnrichment("simple") {
ObjectEnrichment("simple") {
description = "also a description"
TestSimpleRequest::a {
description = "A simple description"
StringEnrichment("blah-blah-blah") {
description = "A simple description"
}
}
TestSimpleRequest::b {
deprecated = true
NumberEnrichment("blah-blah-blah") {
deprecated = true
}
}
}
}

View File

@ -111,9 +111,9 @@
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-nested"
"$ref": "#/components/schemas/NestedComplexItem-blah-blah"
},
"description": "A nested item",
"description": "A nested description",
"type": "array"
}
},
@ -158,6 +158,25 @@
"ONE",
"TWO"
]
},
"NestedComplexItem-blah-blah": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string",
"description": "A nested description"
}
},
"required": [
"alias",
"name"
]
}
},
"securitySchemes": {}

View File

@ -105,9 +105,9 @@
"properties": {
"content": {
"additionalProperties": {
"$ref": "#/components/schemas/ComplexRequest-nested"
"$ref": "#/components/schemas/ComplexRequest-blah"
},
"description": "Getting pretty crazy",
"description": "A nested description",
"type": "object"
}
},
@ -126,9 +126,9 @@
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-nested"
"$ref": "#/components/schemas/NestedComplexItem-blah-blah"
},
"description": "A nested item",
"description": "A nested description",
"type": "array"
}
},
@ -173,6 +173,48 @@
"ONE",
"TWO"
]
},
"NestedComplexItem-blah-blah": {
"type": "object",
"properties": {
"alias": {
"additionalProperties": {
"$ref": "#/components/schemas/CrazyItem"
},
"type": "object"
},
"name": {
"type": "string",
"description": "A nested description"
}
},
"required": [
"alias",
"name"
]
},
"ComplexRequest-blah": {
"type": "object",
"properties": {
"amazingField": {
"type": "string"
},
"org": {
"type": "string"
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-blah-blah"
},
"description": "A nested description",
"type": "array"
}
},
"required": [
"amazingField",
"org",
"tables"
]
}
},
"securitySchemes": {}

View File

@ -58,7 +58,7 @@
"properties": {
"data": {
"$ref": "#/components/schemas/TestSimpleRequest-simple",
"description": "A simple description"
"description": "also a description"
}
},
"required": [

View File

@ -0,0 +1,150 @@
{
"openapi": "3.1.0",
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "Test API",
"version": "1.33.7",
"description": "An amazing, fully-ish 😉 generated API spec",
"termsOfService": "https://example.com",
"contact": {
"name": "Homer Simpson",
"url": "https://gph.is/1NPUDiM",
"email": "chunkylover53@aol.com"
},
"license": {
"name": "MIT",
"url": "https://github.com/bkbnio/kompendium/blob/main/LICENSE"
}
},
"servers": [
{
"url": "https://myawesomeapi.com",
"description": "Production instance of my API"
},
{
"url": "https://staging.myawesomeapi.com",
"description": "Where the fun stuff happens"
}
],
"paths": {
"/example": {
"post": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A test request",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/List-TestSimpleRequest-blah-blah"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A Successful Endeavor",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestCreatedResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": [
{
"name": "a",
"in": "path",
"schema": {
"type": "string"
},
"required": true,
"deprecated": false
},
{
"name": "aa",
"in": "query",
"schema": {
"type": "number",
"format": "int32"
},
"required": true,
"deprecated": false
}
]
}
},
"webhooks": {},
"components": {
"schemas": {
"TestCreatedResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
},
"id": {
"type": "number",
"format": "int32"
}
},
"required": [
"c",
"id"
]
},
"TestSimpleRequest-simple": {
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "A simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
},
"TestSimpleRequest-blah-blah": {
"type": "object",
"properties": {
"a": {
"type": "string",
"description": "A simple description"
},
"b": {
"type": "number",
"format": "int32",
"deprecated": true
}
},
"required": [
"a",
"b"
]
},
"List-TestSimpleRequest-blah-blah": {
"items": {
"$ref": "#/components/schemas/TestSimpleRequest-blah-blah"
},
"type": "array"
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}

View File

@ -5,8 +5,8 @@ complexity:
active: true
functionThreshold: 10
constructorThreshold: 15
ComplexMethod:
threshold: 20
CyclomaticComplexMethod:
threshold: 15
style:
MaxLineLength:
excludes: ['**/test/**/*']

View File

@ -1,10 +1,10 @@
Kompendium allows users to enrich their data types with additional information. This can be done by defining a
`TypeEnrichment` object and passing it to the `enrichment` parameter of the relevant `requestType` or `responseType`.
`ObjectEnrichment` object and passing it to the `enrichment` parameter of the relevant `requestType` or `responseType`.
```kotlin
data class SimpleData(val a: String, val b: Int? = null)
val myEnrichment = TypeEnrichment<SimpleData>(id = "simple-enrichment") {
val myEnrichment = ObjectEnrichment<SimpleData>(id = "simple-enrichment") {
SimpleData::a {
description = "This will update the field description"
}
@ -56,7 +56,7 @@ and apply it inside a parent data class using the `typeEnrichment` property.
data class ParentData(val a: String, val b: ChildData)
data class ChildData(val c: String, val d: Int? = null)
val childEnrichment = TypeEnrichment<ChildData>(id = "child-enrichment") {
val childEnrichment = ObjectEnrichment<ChildData>(id = "child-enrichment") {
ChildData::c {
description = "This will update the field description of field c on child data"
}
@ -65,7 +65,7 @@ val childEnrichment = TypeEnrichment<ChildData>(id = "child-enrichment") {
}
}
val parentEnrichment = TypeEnrichment<ParentData>(id = "parent-enrichment") {
val parentEnrichment = ObjectEnrichment<ParentData>(id = "parent-enrichment") {
ParentData::a {
description = "This will update the field description"
}

View File

@ -0,0 +1,16 @@
package io.bkbn.kompendium.enrichment
class BooleanEnrichment(override val id: String) : Enrichment {
override var deprecated: Boolean? = null
override var description: String? = null
companion object {
inline operator fun invoke(
id: String,
init: BooleanEnrichment.() -> Unit
): BooleanEnrichment {
val builder = BooleanEnrichment(id)
return builder.apply(init)
}
}
}

View File

@ -0,0 +1,24 @@
package io.bkbn.kompendium.enrichment
class CollectionEnrichment<T>(override val id: String) : TypeEnrichment<T> {
override var deprecated: Boolean? = null
override var description: String? = null
var maxItems: Int? = null
var minItems: Int? = null
var uniqueItems: Boolean? = null
// TODO How to handle contains, minContains, maxContains?
var itemEnrichment: TypeEnrichment<*>? = null
companion object {
inline operator fun <reified T> invoke(
id: String,
init: CollectionEnrichment<T>.() -> Unit
): CollectionEnrichment<T> {
val builder = CollectionEnrichment<T>(id)
return builder.apply(init)
}
}
}

View File

@ -1,3 +1,7 @@
package io.bkbn.kompendium.enrichment
sealed interface Enrichment
sealed interface Enrichment {
val id: String
var deprecated: Boolean?
var description: String?
}

View File

@ -0,0 +1,23 @@
package io.bkbn.kompendium.enrichment
class MapEnrichment<V>(override val id: String) : TypeEnrichment<V> {
override var deprecated: Boolean? = null
override var description: String? = null
var maxProperties: Int? = null
var minProperties: Int? = null
lateinit var keyEnrichment: StringEnrichment
lateinit var valueEnrichment: TypeEnrichment<*>
companion object {
inline operator fun <reified V> invoke(
id: String,
init: MapEnrichment<V>.() -> Unit
): MapEnrichment<V> {
val builder = MapEnrichment<V>(id)
return builder.apply(init)
}
}
}

View File

@ -0,0 +1,19 @@
package io.bkbn.kompendium.enrichment
class NumberEnrichment(override val id: String) : Enrichment {
override var deprecated: Boolean? = null
override var description: String? = null
var multipleOf: Number? = null
var maximum: Number? = null
var exclusiveMaximum: Number? = null
var minimum: Number? = null
var exclusiveMinimum: Number? = null
companion object {
inline operator fun invoke(id: String, init: NumberEnrichment.() -> Unit): NumberEnrichment {
val builder = NumberEnrichment(id)
return builder.apply(init)
}
}
}

View File

@ -0,0 +1,27 @@
package io.bkbn.kompendium.enrichment
import kotlin.reflect.KProperty1
class ObjectEnrichment<T>(override val id: String) : TypeEnrichment<T> {
override var deprecated: Boolean? = null
override var description: String? = null
private val _propertyEnrichments: MutableMap<KProperty1<*, *>, Enrichment> = mutableMapOf()
val propertyEnrichment: Map<KProperty1<*, *>, Enrichment>
get() = _propertyEnrichments.toMap()
operator fun <R> KProperty1<T, R>.invoke(init: () -> Enrichment) {
require(!_propertyEnrichments.containsKey(this)) { "${this.name} has already been registered" }
val enrichment = init.invoke()
_propertyEnrichments[this] = enrichment
}
companion object {
inline operator fun <reified T> invoke(id: String, init: ObjectEnrichment<T>.() -> Unit): ObjectEnrichment<T> {
val builder = ObjectEnrichment<T>(id)
return builder.apply(init)
}
}
}

View File

@ -1,36 +0,0 @@
package io.bkbn.kompendium.enrichment
/**
* Reference https://json-schema.org/draft/2020-12/json-schema-validation.html#name-multipleof
*/
class PropertyEnrichment : Enrichment {
// Metadata
var deprecated: Boolean? = null
var description: String? = null
var typeEnrichment: TypeEnrichment<*>? = null
// Number and Integer Constraints
var multipleOf: Number? = null
var maximum: Number? = null
var exclusiveMaximum: Number? = null
var minimum: Number? = null
var exclusiveMinimum: Number? = null
// String constraints
var maxLength: Int? = null
var minLength: Int? = null
var pattern: String? = null
var contentEncoding: String? = null
var contentMediaType: String? = null
// TODO how to handle contentSchema?
// Array constraints
var maxItems: Int? = null
var minItems: Int? = null
var uniqueItems: Boolean? = null
// TODO How to handle contains, minContains, maxContains?
// Object constraints
var maxProperties: Int? = null
var minProperties: Int? = null
}

View File

@ -0,0 +1,20 @@
package io.bkbn.kompendium.enrichment
class StringEnrichment(override val id: String) : Enrichment {
override var deprecated: Boolean? = null
override var description: String? = null
var maxLength: Int? = null
var minLength: Int? = null
var pattern: String? = null
var contentEncoding: String? = null
var contentMediaType: String? = null
// TODO how to handle contentSchema?
companion object {
inline operator fun invoke(id: String, init: StringEnrichment.() -> Unit): StringEnrichment {
val builder = StringEnrichment(id)
return builder.apply(init)
}
}
}

View File

@ -1,25 +1,3 @@
package io.bkbn.kompendium.enrichment
import kotlin.reflect.KProperty
import kotlin.reflect.KProperty1
class TypeEnrichment<T>(val id: String) : Enrichment {
private val enrichments: MutableMap<KProperty1<*, *>, Enrichment> = mutableMapOf()
fun getEnrichmentForProperty(property: KProperty<*>): Enrichment? = enrichments[property]
operator fun <R> KProperty1<T, R>.invoke(init: PropertyEnrichment.() -> Unit) {
require(!enrichments.containsKey(this)) { "${this.name} has already been registered" }
val propertyEnrichment = PropertyEnrichment()
init.invoke(propertyEnrichment)
enrichments[this] = propertyEnrichment
}
companion object {
inline operator fun <reified T> invoke(id: String, init: TypeEnrichment<T>.() -> Unit): TypeEnrichment<T> {
val builder = TypeEnrichment<T>(id)
return builder.apply(init)
}
}
}
sealed interface TypeEnrichment<T> : Enrichment

View File

@ -1,8 +1,10 @@
# Kompendium
project.version=4.0.0-alpha
# Kotlin
kotlin.code.style=official
kotlin.experimental.tryK2=true
#kotlin.experimental.tryK2=true
# Gradle
org.gradle.vfs.watch=true
org.gradle.vfs.verbose=true

View File

@ -26,7 +26,7 @@ class KotlinXSchemaConfigurator : SchemaConfigurator {
.filterIsInstance<SerialName>()
.firstOrNull()?.value ?: property.name
override fun sealedTypeEnrichment(
override fun sealedObjectEnrichment(
implementationType: KType,
implementationSchema: JsonSchema,
): JsonSchema {

View File

@ -9,7 +9,7 @@ interface SchemaConfigurator {
fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>>
fun serializableName(property: KProperty1<out Any, *>): String
fun sealedTypeEnrichment(
fun sealedObjectEnrichment(
implementationType: KType,
implementationSchema: JsonSchema
): JsonSchema

View File

@ -1,5 +1,9 @@
package io.bkbn.kompendium.json.schema
import io.bkbn.kompendium.enrichment.CollectionEnrichment
import io.bkbn.kompendium.enrichment.Enrichment
import io.bkbn.kompendium.enrichment.MapEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
@ -23,7 +27,7 @@ object SchemaGenerator {
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
enrichment: Enrichment? = null
): JsonSchema {
val slug = type.getSlug(enrichment)
@ -47,18 +51,7 @@ object SchemaGenerator {
String::class -> checkForNull(type, TypeDefinition.STRING)
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
UUID::class -> checkForNull(type, TypeDefinition.UUID)
else -> when {
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache, enrichment)
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator, enrichment)
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator, enrichment)
else -> {
if (clazz.isSealed) {
SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
} else {
SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
}
}
}
else -> complexTypeToSchema(clazz, type, cache, schemaConfigurator, enrichment as? TypeEnrichment<*>?)
}
}
@ -77,4 +70,59 @@ object SchemaGenerator {
true -> OneOfDefinition(NullableDefinition(), schema)
false -> schema
}
private fun complexTypeToSchema(
clazz: KClass<*>,
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: Enrichment? = null
): JsonSchema = when {
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
clazz.isSubclassOf(Collection::class) -> handleCollection(type, cache, schemaConfigurator, enrichment)
clazz.isSubclassOf(Map::class) -> handleMap(type, cache, schemaConfigurator, enrichment)
else -> handleObject(type, clazz, cache, schemaConfigurator, enrichment)
}
private fun handleCollection(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: Enrichment?
) = when (enrichment) {
is CollectionEnrichment<*> -> CollectionHandler.handle(type, cache, schemaConfigurator, enrichment)
null -> CollectionHandler.handle(type, cache, schemaConfigurator, null)
else -> error("Incorrect enrichment type for enrichment id: ${enrichment.id}")
}
private fun handleMap(
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: Enrichment?
) = when (enrichment) {
is MapEnrichment<*> -> MapHandler.handle(type, cache, schemaConfigurator, enrichment)
null -> MapHandler.handle(type, cache, schemaConfigurator, null)
else -> error("Incorrect enrichment type for enrichment id: ${enrichment.id}")
}
private fun handleObject(
type: KType,
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: Enrichment?
) = when (clazz.isSealed) {
true -> when (enrichment) {
is ObjectEnrichment<*> -> SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
null -> SealedObjectHandler.handle(type, clazz, cache, schemaConfigurator, null)
else -> error("Incorrect enrichment type for enrichment id: ${enrichment.id}")
}
false -> when (enrichment) {
is ObjectEnrichment<*> -> SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator, enrichment)
null -> SimpleObjectHandler.handle(type, clazz, cache, schemaConfigurator, null)
else -> error("Incorrect enrichment type for enrichment id: ${enrichment.id}")
}
}
}

View File

@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable
@Serializable
data class MapDefinition(
val additionalProperties: JsonSchema,
val maxProperties: Int? = null,
val minProperties: Int? = null,
override val deprecated: Boolean? = null,
override val description: String? = null,
) : JsonSchema {

View File

@ -1,6 +1,6 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.CollectionEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
@ -19,18 +19,23 @@ object CollectionHandler {
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
enrichment: CollectionEnrichment<*>? = null
): JsonSchema {
require(enrichment is CollectionEnrichment<*> || enrichment == null) {
"Enrichment for collection must be either null or a CollectionEnrichment"
}
val collectionType = type.arguments.first().type
?: error("This indicates a bug in Kompendium, please open a GitHub issue!")
val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator, enrichment).let {
if ((it is TypeDefinition && it.type == "object") || it is EnumDefinition) {
cache[collectionType.getSlug(enrichment)] = it
ReferenceDefinition(collectionType.getReferenceSlug(enrichment))
} else {
it
val typeSchema =
SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator, enrichment?.itemEnrichment).let {
if ((it is TypeDefinition && it.type == "object") || it is EnumDefinition) {
cache[collectionType.getSlug(enrichment)] = it
ReferenceDefinition(collectionType.getReferenceSlug(enrichment))
} else {
it
}
}
}
val definition = ArrayDefinition(typeSchema)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)

View File

@ -0,0 +1,94 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.CollectionEnrichment
import io.bkbn.kompendium.enrichment.Enrichment
import io.bkbn.kompendium.enrichment.MapEnrichment
import io.bkbn.kompendium.enrichment.NumberEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.enrichment.StringEnrichment
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.MapDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
object EnrichmentHandler {
fun Enrichment.applyToSchema(schema: JsonSchema): JsonSchema = when (this) {
is NumberEnrichment -> applyToSchema(schema)
is StringEnrichment -> applyToSchema(schema)
is CollectionEnrichment<*> -> applyToSchema(schema)
is MapEnrichment<*> -> applyToSchema(schema)
is ObjectEnrichment<*> -> applyToSchema(schema)
else -> error("Incorrect enrichment type for enrichment id: ${this.id}")
}
private fun ObjectEnrichment<*>.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is TypeDefinition -> schema.copy(deprecated = deprecated, description = description)
is ReferenceDefinition -> schema.copy(deprecated = deprecated, description = description)
else -> error("Incorrect enrichment type for enrichment id: ${this.id}")
}
private fun MapEnrichment<*>.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is MapDefinition -> schema.copyMapEnrichment(this)
else -> error("Incorrect enrichment type for enrichment id: ${this.id}")
}
private fun CollectionEnrichment<*>.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is ArrayDefinition -> schema.copyArrayEnrichment(this)
else -> error("Incorrect enrichment type for enrichment id: ${this.id}")
}
private fun NumberEnrichment.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is TypeDefinition -> schema.copyNumberEnrichment(this)
else -> error("Incorrect enrichment type for enrichment id: ${this.id}")
}
private fun StringEnrichment.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is TypeDefinition -> schema.copyStringEnrichment(this)
else -> error("Incorrect enrichment type for enrichment id: ${this.id}")
}
private fun TypeDefinition.copyNumberEnrichment(
enrichment: NumberEnrichment
): TypeDefinition = copy(
deprecated = enrichment.deprecated,
description = enrichment.description,
multipleOf = enrichment.multipleOf,
maximum = enrichment.maximum,
exclusiveMaximum = enrichment.exclusiveMaximum,
minimum = enrichment.minimum,
exclusiveMinimum = enrichment.exclusiveMinimum,
)
private fun TypeDefinition.copyStringEnrichment(
enrichment: StringEnrichment
): TypeDefinition = copy(
deprecated = enrichment.deprecated,
description = enrichment.description,
maxLength = enrichment.maxLength,
minLength = enrichment.minLength,
pattern = enrichment.pattern,
contentEncoding = enrichment.contentEncoding,
contentMediaType = enrichment.contentMediaType,
)
private fun ArrayDefinition.copyArrayEnrichment(
enrichment: CollectionEnrichment<*>
): ArrayDefinition = copy(
deprecated = enrichment.deprecated,
description = enrichment.description,
minItems = enrichment.minItems,
maxItems = enrichment.maxItems,
uniqueItems = enrichment.uniqueItems,
)
private fun MapDefinition.copyMapEnrichment(
enrichment: MapEnrichment<*>
): MapDefinition = copy(
deprecated = enrichment.deprecated,
description = enrichment.description,
minProperties = enrichment.minProperties,
maxProperties = enrichment.maxProperties,
)
}

View File

@ -1,6 +1,5 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
@ -14,9 +13,8 @@ object EnumHandler {
type: KType,
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
enrichment: TypeEnrichment<*>? = null
): JsonSchema {
cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
cache[type.getSlug()] = ReferenceDefinition(type.getReferenceSlug())
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
return EnumDefinition(enum = options)

View File

@ -1,6 +1,6 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.MapEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.JsonSchema
@ -20,20 +20,24 @@ object MapHandler {
type: KType,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null
enrichment: MapEnrichment<*>? = null
): JsonSchema {
require(enrichment is MapEnrichment<*> || enrichment == null) {
"Enrichment for map must be either null or a MapEnrichment"
}
require(type.arguments.first().type?.classifier as KClass<*> == String::class) {
"JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}"
}
val valueType = type.arguments[1].type ?: error("this indicates a bug in Kompendium, please open a GitHub issue")
val valueSchema = SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator, enrichment).let {
if (it is TypeDefinition && it.type == "object") {
cache[valueType.getSlug(enrichment)] = it
ReferenceDefinition(valueType.getReferenceSlug(enrichment))
} else {
it
val valueSchema =
SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator, enrichment?.valueEnrichment).let {
if (it is TypeDefinition && it.type == "object") {
cache[valueType.getSlug(enrichment)] = it
ReferenceDefinition(valueType.getReferenceSlug(enrichment))
} else {
it
}
}
}
val definition = MapDefinition(valueSchema)
return when (type.isMarkedNullable) {
true -> OneOfDefinition(NullableDefinition(), definition)

View File

@ -1,6 +1,6 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
@ -20,14 +20,14 @@ object SealedObjectHandler {
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>? = null,
enrichment: ObjectEnrichment<*>? = null,
): JsonSchema {
val subclasses = clazz.sealedSubclasses
.map { it.createType(type.arguments) }
.map { t ->
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment)
.let {
schemaConfigurator.sealedTypeEnrichment(t, it)
schemaConfigurator.sealedObjectEnrichment(t, it)
}.let { js ->
if (js is TypeDefinition && js.type == "object") {
val slug = t.getSlug(enrichment)

View File

@ -1,19 +1,17 @@
package io.bkbn.kompendium.json.schema.handler
import io.bkbn.kompendium.enrichment.PropertyEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.Enrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.json.schema.SchemaConfigurator
import io.bkbn.kompendium.json.schema.SchemaGenerator
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.bkbn.kompendium.json.schema.definition.MapDefinition
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
import io.bkbn.kompendium.json.schema.handler.EnrichmentHandler.applyToSchema
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
import kotlin.reflect.KClass
@ -32,28 +30,31 @@ object SimpleObjectHandler {
clazz: KClass<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
enrichment: TypeEnrichment<*>?,
enrichment: ObjectEnrichment<*>?,
): JsonSchema {
cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
require(enrichment is ObjectEnrichment<*> || enrichment == null) {
"Enrichment for object must either be of type ObjectEnrichment or null"
}
val slug = type.getSlug(enrichment)
val referenceSlug = type.getReferenceSlug(enrichment)
cache[slug] = ReferenceDefinition(referenceSlug)
val typeMap = clazz.typeParameters.zip(type.arguments).toMap()
val props = schemaConfigurator.serializableMemberProperties(clazz)
.filterNot { it.javaField == null }
.associate { prop ->
val propTypeEnrichment = when (val pe = enrichment?.getEnrichmentForProperty(prop)) {
is PropertyEnrichment -> pe
else -> null
}
val propEnrichment = enrichment?.propertyEnrichment?.get(prop)
val schema = when (prop.needsToInjectGenerics(typeMap)) {
true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator, propTypeEnrichment)
true -> handleNestedGenerics(typeMap, prop, cache, schemaConfigurator, propEnrichment)
false -> when (typeMap.containsKey(prop.returnType.classifier)) {
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator, propTypeEnrichment)
false -> handleProperty(prop, cache, schemaConfigurator, propTypeEnrichment?.typeEnrichment)
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator, propEnrichment)
false -> handleProperty(prop, cache, schemaConfigurator, propEnrichment)
}
}
val enrichedSchema = propTypeEnrichment?.applyToSchema(schema) ?: schema
val enrichedSchema = propEnrichment?.applyToSchema(schema) ?: schema
val nullCheckSchema = when (prop.returnType.isMarkedNullable && !enrichedSchema.isNullable()) {
true -> OneOfDefinition(NullableDefinition(), enrichedSchema)
@ -84,11 +85,14 @@ object SimpleObjectHandler {
.map { schemaConfigurator.serializableName(it) }
.toSet()
return TypeDefinition(
val definition = TypeDefinition(
type = "object",
properties = props,
required = required
)
cache[slug] = definition
return definition
}
private fun KProperty<*>.needsToInjectGenerics(
@ -103,7 +107,7 @@ object SimpleObjectHandler {
prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
propEnrichment: PropertyEnrichment?
propEnrichment: Enrichment?
): JsonSchema {
val propClass = prop.returnType.classifier as KClass<*>
val types = prop.returnType.arguments.map {
@ -111,7 +115,7 @@ object SimpleObjectHandler {
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
}
val constructedType = propClass.createType(types)
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator, propEnrichment?.typeEnrichment)
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator, propEnrichment)
.let {
if (it.isOrContainsObjectOrEnumDef()) {
cache[constructedType.getSlug(propEnrichment)] = it
@ -127,14 +131,14 @@ object SimpleObjectHandler {
typeMap: Map<KTypeParameter, KTypeProjection>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
propEnrichment: PropertyEnrichment?
propEnrichment: Enrichment?
): JsonSchema {
val type = typeMap[prop.returnType.classifier]?.type
?: error("This indicates a bug in Kompendium, please open a GitHub issue")
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator, propEnrichment?.typeEnrichment).let {
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator, propEnrichment).let {
if (it.isOrContainsObjectOrEnumDef()) {
cache[type.getSlug(propEnrichment?.typeEnrichment)] = it
ReferenceDefinition(type.getReferenceSlug(propEnrichment?.typeEnrichment))
cache[type.getSlug(propEnrichment)] = it
ReferenceDefinition(type.getReferenceSlug(propEnrichment))
} else {
it
}
@ -145,7 +149,7 @@ object SimpleObjectHandler {
prop: KProperty<*>,
cache: MutableMap<String, JsonSchema>,
schemaConfigurator: SchemaConfigurator,
propEnrichment: TypeEnrichment<*>?
propEnrichment: Enrichment?
): JsonSchema =
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator, propEnrichment).let {
if (it.isOrContainsObjectOrEnumDef()) {
@ -165,37 +169,4 @@ object SimpleObjectHandler {
}
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition }
private fun PropertyEnrichment.applyToSchema(schema: JsonSchema): JsonSchema = when (schema) {
is AnyOfDefinition -> schema.copy(deprecated = deprecated, description = description)
is ArrayDefinition -> schema.copy(
deprecated = deprecated,
description = description,
minItems = minItems,
maxItems = maxItems,
uniqueItems = uniqueItems,
)
is EnumDefinition -> schema.copy(deprecated = deprecated, description = description)
is MapDefinition -> schema.copy(deprecated = deprecated, description = description)
is NullableDefinition -> schema.copy(deprecated = deprecated, description = description)
is OneOfDefinition -> schema.copy(deprecated = deprecated, description = description)
is ReferenceDefinition -> schema.copy(deprecated = deprecated, description = description)
is TypeDefinition -> schema.copy(
deprecated = deprecated,
description = description,
multipleOf = multipleOf,
maximum = maximum,
exclusiveMaximum = exclusiveMaximum,
minimum = minimum,
exclusiveMinimum = exclusiveMinimum,
maxLength = maxLength,
minLength = minLength,
pattern = pattern,
contentEncoding = contentEncoding,
contentMediaType = contentMediaType,
maxProperties = maxProperties,
minProperties = minProperties,
)
}
}

View File

@ -1,8 +1,6 @@
package io.bkbn.kompendium.json.schema.util
import io.bkbn.kompendium.enrichment.Enrichment
import io.bkbn.kompendium.enrichment.PropertyEnrichment
import io.bkbn.kompendium.enrichment.TypeEnrichment
import kotlin.reflect.KClass
import kotlin.reflect.KType
@ -11,9 +9,8 @@ object Helpers {
const val COMPONENT_SLUG = "#/components/schemas"
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
is TypeEnrichment<*> -> getEnrichedSlug(enrichment)
is PropertyEnrichment -> error("Slugs should not be generated for field enrichments")
else -> getSimpleSlug()
null -> getSimpleSlug()
else -> getEnrichedSlug(enrichment)
}
fun KType.getSimpleSlug(): String = when {
@ -21,12 +18,11 @@ object Helpers {
else -> (classifier as KClass<*>).kompendiumSlug() ?: error("Could not determine simple name for $this")
}
private fun KType.getEnrichedSlug(enrichment: TypeEnrichment<*>) = getSimpleSlug() + "-${enrichment.id}"
private fun KType.getEnrichedSlug(enrichment: Enrichment) = getSimpleSlug() + "-${enrichment.id}"
fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) {
is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}"
is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments")
else -> getSimpleReferenceSlug()
null -> getSimpleReferenceSlug()
else -> getSimpleReferenceSlug() + "-${enrichment.id}"
}
private fun KType.getSimpleReferenceSlug() = when {

View File

@ -13,7 +13,10 @@ import io.bkbn.kompendium.core.fixtures.TransientObject
import io.bkbn.kompendium.core.fixtures.UnbackedObject
import io.bkbn.kompendium.core.fixtures.GenericObject
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.CollectionEnrichment
import io.bkbn.kompendium.enrichment.NumberEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.enrichment.StringEnrichment
import io.bkbn.kompendium.json.schema.definition.JsonSchema
import io.kotest.assertions.json.shouldEqualJson
import io.kotest.assertions.throwables.shouldThrow
@ -114,12 +117,16 @@ class SchemaGeneratorTest : DescribeSpec({
it("Can attach an enrichment to a simple type") {
jsonSchemaTest<TestSimpleRequest>(
snapshotName = "T0022__enriched_simple_object.json",
enrichment = TypeEnrichment("simple") {
enrichment = ObjectEnrichment("simple") {
TestSimpleRequest::a {
description = "This is a simple description"
StringEnrichment("blah") {
description = "This is a simple description"
}
}
TestSimpleRequest::b {
deprecated = true
NumberEnrichment("bla") {
deprecated = true
}
}
}
)
@ -127,12 +134,16 @@ class SchemaGeneratorTest : DescribeSpec({
it("Can properly assign a reference to a nested enrichment") {
jsonSchemaTest<ComplexRequest>(
snapshotName = "T0023__enriched_nested_reference.json",
enrichment = TypeEnrichment("example") {
enrichment = ObjectEnrichment("example") {
ComplexRequest::tables {
description = "Collection of important items"
typeEnrichment = TypeEnrichment("table") {
NestedComplexItem::name {
description = "The name of the table"
CollectionEnrichment<List<NestedComplexItem>>("tables") {
description = "Collection of important items"
itemEnrichment = ObjectEnrichment("table") {
NestedComplexItem::name {
StringEnrichment("name") {
description = "The name of the table"
}
}
}
}
}
@ -142,15 +153,19 @@ class SchemaGeneratorTest : DescribeSpec({
it("Can properly assign a reference to a generic object") {
jsonSchemaTest<GenericObject<TestSimpleRequest>>(
snapshotName = "T0025__enrichment_generic_object.json",
enrichment = TypeEnrichment("generic") {
enrichment = ObjectEnrichment("generic") {
GenericObject<TestSimpleRequest>::data {
description = "This is a generic param"
typeEnrichment = TypeEnrichment("simple") {
ObjectEnrichment<TestSimpleRequest>("blob") {
description = "This is a generic object"
TestSimpleRequest::a {
description = "This is a simple description"
StringEnrichment("blah") {
description = "This is a simple description"
}
}
TestSimpleRequest::b {
deprecated = true
NumberEnrichment("bla") {
deprecated = true
}
}
}
}
@ -168,7 +183,7 @@ class SchemaGeneratorTest : DescribeSpec({
private fun JsonSchema.serialize() = json.encodeToString(JsonSchema.serializer(), this)
private inline fun <reified T> jsonSchemaTest(snapshotName: String, enrichment: TypeEnrichment<*>? = null) {
private inline fun <reified T> jsonSchemaTest(snapshotName: String, enrichment: ObjectEnrichment<*>? = null) {
// act
val schema = SchemaGenerator.fromTypeToSchema(
type = typeOf<T>(),

View File

@ -9,7 +9,7 @@
},
"tables": {
"items": {
"$ref": "#/components/schemas/NestedComplexItem-table"
"$ref": "#/components/schemas/NestedComplexItem-tables"
},
"description": "Collection of important items",
"type": "array"

View File

@ -2,8 +2,8 @@
"type": "object",
"properties": {
"data": {
"description": "This is a generic param",
"$ref": "#/components/schemas/TestSimpleRequest-simple"
"$ref": "#/components/schemas/TestSimpleRequest-blob",
"description": "This is a generic object"
}
},
"required": [

View File

@ -6,7 +6,10 @@ import io.bkbn.kompendium.core.plugin.NotarizedApplication
import io.bkbn.kompendium.core.plugin.NotarizedRoute
import io.bkbn.kompendium.core.routes.redoc
import io.bkbn.kompendium.core.routes.swagger
import io.bkbn.kompendium.enrichment.TypeEnrichment
import io.bkbn.kompendium.enrichment.BooleanEnrichment
import io.bkbn.kompendium.enrichment.NumberEnrichment
import io.bkbn.kompendium.enrichment.ObjectEnrichment
import io.bkbn.kompendium.enrichment.StringEnrichment
import io.bkbn.kompendium.json.schema.KotlinXSchemaConfigurator
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
import io.bkbn.kompendium.oas.payload.Parameter
@ -60,29 +63,37 @@ private fun Application.mainModule() {
}
}
private val testEnrichment = TypeEnrichment("testerino") {
private val testEnrichment = ObjectEnrichment("testerino") {
ExampleRequest::thingA {
description = "This is a thing"
StringEnrichment("thingA") {
description = "This is a thing"
}
}
ExampleRequest::thingB {
description = "This is another thing"
NumberEnrichment("thingB") {
description = "This is another thing"
}
}
ExampleRequest::thingC {
deprecated = true
description = "A good but old field"
typeEnrichment = TypeEnrichment("big-tings") {
ObjectEnrichment<InnerRequest>("thingC") {
deprecated = true
description = "A good but old field"
InnerRequest::d {
exclusiveMaximum = 10.0
exclusiveMinimum = 1.1
description = "THE BIG D"
NumberEnrichment("blahblah") {
exclusiveMinimum = 1.1
exclusiveMaximum = 10.0
description = "THE BIG D"
}
}
}
}
}
private val testResponseEnrichment = TypeEnrichment("testerino") {
private val testResponseEnrichment = ObjectEnrichment("testerino") {
ExampleResponse::isReal {
description = "Is this thing real or not?"
BooleanEnrichment("blah") {
description = "Is this thing real or not?"
}
}
}