feat: enriched enrichments (#566)
This commit is contained in:
@ -7,7 +7,7 @@ import kotlin.reflect.typeOf
|
|||||||
|
|
||||||
class RequestInfo private constructor(
|
class RequestInfo private constructor(
|
||||||
val requestType: KType,
|
val requestType: KType,
|
||||||
val typeEnrichment: TypeEnrichment<*>?,
|
val enrichment: TypeEnrichment<*>?,
|
||||||
val description: String,
|
val description: String,
|
||||||
val examples: Map<String, MediaType.Example>?,
|
val examples: Map<String, MediaType.Example>?,
|
||||||
val mediaTypes: Set<String>,
|
val mediaTypes: Set<String>,
|
||||||
@ -60,7 +60,7 @@ class RequestInfo private constructor(
|
|||||||
fun build() = RequestInfo(
|
fun build() = RequestInfo(
|
||||||
requestType = requestType ?: error("Request type must be present"),
|
requestType = requestType ?: error("Request type must be present"),
|
||||||
description = description ?: error("Description must be present"),
|
description = description ?: error("Description must be present"),
|
||||||
typeEnrichment = typeEnrichment,
|
enrichment = typeEnrichment,
|
||||||
examples = examples,
|
examples = examples,
|
||||||
mediaTypes = mediaTypes ?: setOf("application/json"),
|
mediaTypes = mediaTypes ?: setOf("application/json"),
|
||||||
required = required ?: true
|
required = required ?: true
|
||||||
|
@ -10,7 +10,7 @@ import kotlin.reflect.typeOf
|
|||||||
class ResponseInfo private constructor(
|
class ResponseInfo private constructor(
|
||||||
val responseCode: HttpStatusCode,
|
val responseCode: HttpStatusCode,
|
||||||
val responseType: KType,
|
val responseType: KType,
|
||||||
val typeEnrichment: TypeEnrichment<*>?,
|
val enrichment: TypeEnrichment<*>?,
|
||||||
val description: String,
|
val description: String,
|
||||||
val examples: Map<String, MediaType.Example>?,
|
val examples: Map<String, MediaType.Example>?,
|
||||||
val mediaTypes: Set<String>,
|
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!"),
|
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!"),
|
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!"),
|
description = description ?: error("You must provide a description in order to build a Response!"),
|
||||||
typeEnrichment = typeEnrichment,
|
enrichment = typeEnrichment,
|
||||||
examples = examples,
|
examples = examples,
|
||||||
mediaTypes = mediaTypes ?: setOf("application/json"),
|
mediaTypes = mediaTypes ?: setOf("application/json"),
|
||||||
responseHeaders = responseHeaders
|
responseHeaders = responseHeaders
|
||||||
|
@ -51,36 +51,45 @@ object Helpers {
|
|||||||
routePath: String,
|
routePath: String,
|
||||||
authMethods: List<String> = emptyList()
|
authMethods: List<String> = emptyList()
|
||||||
) {
|
) {
|
||||||
|
val type = this.response.responseType
|
||||||
|
val enrichment = this.response.enrichment
|
||||||
SchemaGenerator.fromTypeOrUnit(
|
SchemaGenerator.fromTypeOrUnit(
|
||||||
type = this.response.responseType,
|
type = type,
|
||||||
cache = spec.components.schemas,
|
cache = spec.components.schemas,
|
||||||
schemaConfigurator = schemaConfigurator,
|
schemaConfigurator = schemaConfigurator,
|
||||||
enrichment = this.response.typeEnrichment,
|
enrichment = enrichment,
|
||||||
)?.let { schema ->
|
)?.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 ->
|
errors.forEach { error ->
|
||||||
|
val errorEnrichment = error.enrichment
|
||||||
|
val errorType = error.responseType
|
||||||
SchemaGenerator.fromTypeOrUnit(
|
SchemaGenerator.fromTypeOrUnit(
|
||||||
type = error.responseType,
|
type = errorType,
|
||||||
cache = spec.components.schemas,
|
cache = spec.components.schemas,
|
||||||
schemaConfigurator = schemaConfigurator,
|
schemaConfigurator = schemaConfigurator,
|
||||||
enrichment = error.typeEnrichment,
|
enrichment = errorEnrichment,
|
||||||
)?.let { schema ->
|
)?.let { schema ->
|
||||||
spec.components.schemas[error.responseType.getSlug(error.typeEnrichment)] = schema
|
val slug = errorType.getSlug(errorEnrichment)
|
||||||
|
spec.components.schemas[slug] = schema
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
when (this) {
|
when (this) {
|
||||||
is MethodInfoWithRequest -> {
|
is MethodInfoWithRequest -> {
|
||||||
this.request?.let { reqInfo ->
|
this.request?.let { reqInfo ->
|
||||||
|
val reqEnrichment = reqInfo.enrichment
|
||||||
|
val reqType = reqInfo.requestType
|
||||||
SchemaGenerator.fromTypeOrUnit(
|
SchemaGenerator.fromTypeOrUnit(
|
||||||
type = reqInfo.requestType,
|
type = reqType,
|
||||||
cache = spec.components.schemas,
|
cache = spec.components.schemas,
|
||||||
schemaConfigurator = schemaConfigurator,
|
schemaConfigurator = schemaConfigurator,
|
||||||
enrichment = reqInfo.typeEnrichment,
|
enrichment = reqEnrichment,
|
||||||
)?.let { schema ->
|
)?.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(
|
content = reqInfo.requestType.toReferenceContent(
|
||||||
examples = reqInfo.examples,
|
examples = reqInfo.examples,
|
||||||
mediaTypes = reqInfo.mediaTypes,
|
mediaTypes = reqInfo.mediaTypes,
|
||||||
enrichment = reqInfo.typeEnrichment
|
enrichment = reqInfo.enrichment
|
||||||
),
|
),
|
||||||
required = reqInfo.required
|
required = reqInfo.required
|
||||||
)
|
)
|
||||||
@ -142,7 +151,7 @@ object Helpers {
|
|||||||
content = this.response.responseType.toReferenceContent(
|
content = this.response.responseType.toReferenceContent(
|
||||||
examples = this.response.examples,
|
examples = this.response.examples,
|
||||||
mediaTypes = this.response.mediaTypes,
|
mediaTypes = this.response.mediaTypes,
|
||||||
enrichment = this.response.typeEnrichment
|
enrichment = this.response.enrichment
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
).plus(this.errors.toResponseMap())
|
).plus(this.errors.toResponseMap())
|
||||||
@ -174,7 +183,7 @@ object Helpers {
|
|||||||
content = error.responseType.toReferenceContent(
|
content = error.responseType.toReferenceContent(
|
||||||
examples = error.examples,
|
examples = error.examples,
|
||||||
mediaTypes = error.mediaTypes,
|
mediaTypes = error.mediaTypes,
|
||||||
enrichment = error.typeEnrichment
|
enrichment = error.enrichment
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,7 @@
|
|||||||
|
package io.bkbn.kompendium.core
|
||||||
|
|
||||||
|
import io.kotest.core.spec.style.DescribeSpec
|
||||||
|
|
||||||
|
class KompendiumFreeFormTest : DescribeSpec({
|
||||||
|
// todo Assess strategies here
|
||||||
|
})
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -1,39 +1,13 @@
|
|||||||
package io.bkbn.kompendium.core
|
package io.bkbn.kompendium.core
|
||||||
|
|
||||||
import dev.forst.ktor.apikey.apiKey
|
|
||||||
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
|
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.complexRequest
|
||||||
import io.bkbn.kompendium.core.util.customAuthConfig
|
|
||||||
import io.bkbn.kompendium.core.util.customFieldNameResponse
|
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.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.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.headerParameter
|
||||||
import io.bkbn.kompendium.core.util.ignoredFieldsResponse
|
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.nestedTypeName
|
||||||
import io.bkbn.kompendium.core.util.nestedUnderRoot
|
|
||||||
import io.bkbn.kompendium.core.util.nonRequiredParam
|
import io.bkbn.kompendium.core.util.nonRequiredParam
|
||||||
import io.bkbn.kompendium.core.util.nonRequiredParams
|
import io.bkbn.kompendium.core.util.nonRequiredParams
|
||||||
import io.bkbn.kompendium.core.util.notarizedDelete
|
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.nullableField
|
||||||
import io.bkbn.kompendium.core.util.nullableNestedObject
|
import io.bkbn.kompendium.core.util.nullableNestedObject
|
||||||
import io.bkbn.kompendium.core.util.nullableReference
|
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.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.postNoReqBody
|
||||||
import io.bkbn.kompendium.core.util.primitives
|
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.requiredParams
|
||||||
import io.bkbn.kompendium.core.util.responseHeaders
|
import io.bkbn.kompendium.core.util.responseHeaders
|
||||||
import io.bkbn.kompendium.core.util.returnsEnumList
|
import io.bkbn.kompendium.core.util.returnsEnumList
|
||||||
import io.bkbn.kompendium.core.util.returnsList
|
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.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.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.topLevelNullable
|
||||||
import io.bkbn.kompendium.core.util.trailingSlash
|
|
||||||
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
|
import io.bkbn.kompendium.core.util.unbackedFieldsResponse
|
||||||
import io.bkbn.kompendium.core.util.withOperationId
|
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.component.Components
|
||||||
import io.bkbn.kompendium.oas.security.ApiKeyAuth
|
|
||||||
import io.bkbn.kompendium.oas.security.BasicAuth
|
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.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
|
||||||
import io.kotest.core.spec.style.DescribeSpec
|
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.serialization.kotlinx.json.json
|
||||||
import io.ktor.server.application.call
|
import io.ktor.server.application.call
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.auth.Authentication
|
import io.ktor.server.auth.Authentication
|
||||||
import io.ktor.server.auth.OAuthServerSettings
|
|
||||||
import io.ktor.server.auth.UserIdPrincipal
|
import io.ktor.server.auth.UserIdPrincipal
|
||||||
import io.ktor.server.auth.basic
|
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.response.respondText
|
||||||
import io.ktor.server.routing.get
|
import io.ktor.server.routing.get
|
||||||
import io.ktor.server.routing.route
|
import io.ktor.server.routing.route
|
||||||
import kotlinx.serialization.encodeToString
|
import kotlinx.serialization.encodeToString
|
||||||
import kotlinx.serialization.json.Json
|
import kotlinx.serialization.json.Json
|
||||||
import java.net.URI
|
import java.net.URI
|
||||||
import java.time.Instant
|
|
||||||
import kotlin.reflect.typeOf
|
|
||||||
|
|
||||||
class KompendiumTest : DescribeSpec({
|
class KompendiumTest : DescribeSpec({
|
||||||
describe("Notarized Open API Metadata Tests") {
|
describe("Notarized Open API Metadata Tests") {
|
||||||
@ -155,56 +95,6 @@ class KompendiumTest : DescribeSpec({
|
|||||||
openApiTestAllSerializers("T0066__notarized_get_with_response_headers.json") { responseHeaders() }
|
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") {
|
describe("Required Fields") {
|
||||||
it("Marks a parameter as required if there is no default and it is not marked nullable") {
|
it("Marks a parameter as required if there is no default and it is not marked nullable") {
|
||||||
openApiTestAllSerializers("T0023__required_param.json") { requiredParams() }
|
openApiTestAllSerializers("T0023__required_param.json") { requiredParams() }
|
||||||
@ -219,377 +109,121 @@ class KompendiumTest : DescribeSpec({
|
|||||||
openApiTestAllSerializers("T0026__nullable_field.json") { nullableField() }
|
openApiTestAllSerializers("T0026__nullable_field.json") { nullableField() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
describe("Polymorphism and Generics") {
|
describe("Custom Serializable Reader tests") {
|
||||||
it("can generate a polymorphic response type") {
|
it("Can support ignoring fields") {
|
||||||
openApiTestAllSerializers("T0027__polymorphic_response.json") { polymorphicResponse() }
|
openApiTestAllSerializers("T0048__ignored_property.json") { ignoredFieldsResponse() }
|
||||||
}
|
}
|
||||||
it("Can generate a collection with polymorphic response type") {
|
it("Can support un-backed fields") {
|
||||||
openApiTestAllSerializers("T0028__polymorphic_list_response.json") { polymorphicCollectionResponse() }
|
openApiTestAllSerializers("T0049__unbacked_property.json") { unbackedFieldsResponse() }
|
||||||
}
|
}
|
||||||
it("Can generate a map with a polymorphic response type") {
|
it("Can support custom named fields") {
|
||||||
openApiTestAllSerializers("T0029__polymorphic_map_response.json") { polymorphicMapResponse() }
|
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") {
|
it("Can add an operation id to a notarized route") {
|
||||||
openApiTestAllSerializers("T0031__nested_generic_response.json") { nestedGenericResponse() }
|
openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() }
|
||||||
}
|
}
|
||||||
it("Can generate a polymorphic response type with generics") {
|
xit("Can add an undeclared field") {
|
||||||
openApiTestAllSerializers("T0032__polymorphic_response_with_generics.json") { genericPolymorphicResponse() }
|
// TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() }
|
||||||
}
|
}
|
||||||
it("Can handle an absolutely psycho inheritance test") {
|
it("Can add a custom header parameter with a name override") {
|
||||||
openApiTestAllSerializers("T0033__crazy_polymorphic_example.json") { genericPolymorphicResponseMultipleImpls() }
|
openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() }
|
||||||
}
|
}
|
||||||
it("Can support nested generic collections") {
|
xit("Can override field name") {
|
||||||
openApiTestAllSerializers("T0039__nested_generic_collection.json") { nestedGenericCollection() }
|
// TODO Assess strategies here
|
||||||
}
|
}
|
||||||
it("Can support nested generics with multiple type parameters") {
|
it("Can serialize a recursive type") {
|
||||||
openApiTestAllSerializers("T0040__nested_generic_multiple_type_params.json") {
|
openApiTestAllSerializers("T0042__simple_recursive.json") { simpleRecursive() }
|
||||||
nestedGenericMultipleParamsCollection()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
it("Can handle a really gnarly generic example") {
|
it("Nullable fields do not lead to doom") {
|
||||||
openApiTestAllSerializers("T0043__gnarly_generic_example.json") { gnarlyGenericResponse() }
|
openApiTestAllSerializers("T0036__nullable_fields.json") { nullableNestedObject() }
|
||||||
}
|
}
|
||||||
it("Can override the type name for a sealed interface implementation") {
|
it("Can have a nullable enum as a member field") {
|
||||||
openApiTestAllSerializers("T0070__sealed_interface_type_name_override.json") { overrideSealedTypeIdentifier() }
|
openApiTestAllSerializers("T0037__nullable_enum_field.json") { nullableEnumField() }
|
||||||
}
|
}
|
||||||
it("Can serialize an object where the subtype is not a complete set of parent properties") {
|
it("Can have a list of enums as a field") {
|
||||||
openApiTestAllSerializers("T0071__subtype_not_complete_set_of_parent_properties.json") {
|
openApiTestAllSerializers("T0076__list_of_enums.json") { returnsEnumList() }
|
||||||
subtypeNotCompleteSetOfParentProperties()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
describe("Custom Serializable Reader tests") {
|
it("Can have a nullable reference without impacting base type") {
|
||||||
it("Can support ignoring fields") {
|
openApiTestAllSerializers("T0041__nullable_reference.json") { nullableReference() }
|
||||||
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() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
describe("Miscellaneous") {
|
it("Can handle nested type names") {
|
||||||
xit("Can generate the necessary ReDoc home page") {
|
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
|
||||||
// TODO apiFunctionalityTest(getFileSnapshot("redoc.html"), "/docs") { returnsList() }
|
}
|
||||||
}
|
it("Can handle top level nullable types") {
|
||||||
it("Can add an operation id to a notarized route") {
|
openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() }
|
||||||
openApiTestAllSerializers("T0034__notarized_get_with_operation_id.json") { withOperationId() }
|
}
|
||||||
}
|
it("Can handle multiple registrations for different methods with the same path and different auth") {
|
||||||
xit("Can add an undeclared field") {
|
openApiTestAllSerializers(
|
||||||
// TODO openApiTestAllSerializers("undeclared_field.json") { undeclaredType() }
|
"T0053__same_path_different_methods_and_auth.json",
|
||||||
}
|
applicationSetup = {
|
||||||
it("Can add a custom header parameter with a name override") {
|
install(Authentication) {
|
||||||
openApiTestAllSerializers("T0035__override_parameter_name.json") { headerParameter() }
|
basic("basic") {
|
||||||
}
|
realm = "Ktor Server"
|
||||||
xit("Can override field name") {
|
validate { UserIdPrincipal("Placeholder") }
|
||||||
// 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") }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
}
|
||||||
specOverrides = {
|
},
|
||||||
this.copy(
|
specOverrides = {
|
||||||
components = Components(
|
this.copy(
|
||||||
securitySchemes = mutableMapOf(
|
components = Components(
|
||||||
"basic" to BasicAuth()
|
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(
|
) { samePathDifferentMethodsAndAuth() }
|
||||||
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() }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
describe("Error Handling") {
|
it("Can generate paths without application root-path") {
|
||||||
it("Throws a clear exception when an unidentified type is encountered") {
|
openApiTestAllSerializers(
|
||||||
val exception = shouldThrow<UnknownSchemaException> { openApiTestAllSerializers("") { dateTimeString() } }
|
"T0054__app_with_rootpath.json",
|
||||||
exception.message should startWith("An unknown type was encountered: class java.time.Instant")
|
applicationEnvironmentBuilder = {
|
||||||
}
|
rootPath = "/example"
|
||||||
it("Throws an exception when same method for same path has been previously registered") {
|
},
|
||||||
val exception = shouldThrow<IllegalArgumentException> {
|
specOverrides = {
|
||||||
openApiTestAllSerializers(
|
copy(
|
||||||
snapshotName = "",
|
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
|
||||||
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")
|
) { notarizedGet() }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
describe("Formats") {
|
it("Can apply a custom serialization strategy to the openapi document") {
|
||||||
it("Can set a format for a simple type schema") {
|
val customJsonEncoder = Json {
|
||||||
openApiTestAllSerializers(
|
serializersModule = KompendiumSerializersModule.module
|
||||||
snapshotName = "T0038__formatted_date_time_string.json",
|
encodeDefaults = true
|
||||||
customTypes = mapOf(typeOf<Instant>() to TypeDefinition(type = "string", format = "date"))
|
explicitNulls = false
|
||||||
) { dateTimeString() }
|
|
||||||
}
|
}
|
||||||
}
|
openApiTestAllSerializers(
|
||||||
describe("Free Form") {
|
snapshotName = "T0072__custom_serialization_strategy.json",
|
||||||
// todo Assess strategies here
|
notarizedApplicationConfigOverrides = {
|
||||||
}
|
specRoute = { spec, routing ->
|
||||||
describe("Authentication") {
|
routing {
|
||||||
it("Can add a default auth config by default") {
|
route("/openapi.json") {
|
||||||
openApiTestAllSerializers(
|
get {
|
||||||
snapshotName = "T0045__default_auth_config.json",
|
call.response.headers.append("Content-Type", "application/json")
|
||||||
applicationSetup = {
|
call.respondText { customJsonEncoder.encodeToString(spec) }
|
||||||
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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
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() }
|
},
|
||||||
}
|
contentNegotiation = {
|
||||||
it("Can provide different scopes on path operations in the same route") {
|
json(
|
||||||
openApiTestAllSerializers(
|
Json {
|
||||||
snapshotName = "T0074__auth_on_specific_path_operation.json",
|
encodeDefaults = true
|
||||||
applicationSetup = {
|
explicitNulls = true
|
||||||
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() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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()
|
|
||||||
}
|
}
|
||||||
}
|
) { notarizedGet() }
|
||||||
it("Can apply constraints to an array field") {
|
}
|
||||||
openApiTestAllSerializers("T0064__array_constraints.json") { arrayConstraints() }
|
it("Can serialize a data class with a field outside of the constructor") {
|
||||||
}
|
openApiTestAllSerializers("T0073__data_class_with_field_outside_constructor.json") { fieldOutsideConstructor() }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -7,7 +7,10 @@ import io.bkbn.kompendium.core.fixtures.TestNested
|
|||||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||||
import io.bkbn.kompendium.core.util.TestModules.defaultPath
|
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.http.HttpStatusCode
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
@ -23,15 +26,17 @@ fun Routing.intConstraints() {
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
description("An int")
|
description("An int")
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
TestCreatedResponse::id {
|
TestCreatedResponse::id {
|
||||||
minimum = 2
|
NumberEnrichment("blah-blah-blah") {
|
||||||
maximum = 100
|
minimum = 2
|
||||||
multipleOf = 2
|
maximum = 100
|
||||||
|
multipleOf = 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
responseCode(HttpStatusCode.OK)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -48,11 +53,13 @@ fun Routing.doubleConstraints() {
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
description("A double")
|
description("A double")
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
DoubleResponse::payload {
|
DoubleResponse::payload {
|
||||||
minimum = 2.0
|
NumberEnrichment("blah-blah-blah") {
|
||||||
maximum = 100.0
|
minimum = 2.0
|
||||||
multipleOf = 2.0
|
maximum = 100.0
|
||||||
|
multipleOf = 2.0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -73,10 +80,12 @@ fun Routing.stringConstraints() {
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
description("A string")
|
description("A string")
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
TestNested::nesty {
|
TestNested::nesty {
|
||||||
maxLength = 10
|
StringEnrichment("blah") {
|
||||||
minLength = 2
|
maxLength = 10
|
||||||
|
minLength = 2
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -97,9 +106,11 @@ fun Routing.stringPatternConstraints() {
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
description("A string")
|
description("A string")
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
TestNested::nesty {
|
TestNested::nesty {
|
||||||
pattern = "[a-z]+"
|
StringEnrichment("blah") {
|
||||||
|
pattern = "[a-z]+"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -120,10 +131,12 @@ fun Routing.stringContentEncodingConstraints() {
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
description("A string")
|
description("A string")
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
TestNested::nesty {
|
TestNested::nesty {
|
||||||
contentEncoding = "base64"
|
StringEnrichment("blah") {
|
||||||
contentMediaType = "image/png"
|
contentEncoding = "base64"
|
||||||
|
contentMediaType = "image/png"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -144,11 +157,13 @@ fun Routing.arrayConstraints() {
|
|||||||
responseCode(HttpStatusCode.OK)
|
responseCode(HttpStatusCode.OK)
|
||||||
description("An array")
|
description("An array")
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
Page<String>::content {
|
Page<String>::content {
|
||||||
minItems = 2
|
CollectionEnrichment<String>("blah") {
|
||||||
maxItems = 10
|
minItems = 2
|
||||||
uniqueItems = true
|
maxItems = 10
|
||||||
|
uniqueItems = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
@ -1,16 +1,20 @@
|
|||||||
package io.bkbn.kompendium.core.util
|
package io.bkbn.kompendium.core.util
|
||||||
|
|
||||||
import io.bkbn.kompendium.core.fixtures.ComplexRequest
|
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.MultiNestedGenerics
|
||||||
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
|
import io.bkbn.kompendium.core.fixtures.NestedComplexItem
|
||||||
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
|
import io.bkbn.kompendium.core.fixtures.TestCreatedResponse
|
||||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||||
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
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.GetInfo
|
||||||
import io.bkbn.kompendium.core.metadata.PostInfo
|
import io.bkbn.kompendium.core.metadata.PostInfo
|
||||||
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
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.http.HttpStatusCode
|
||||||
import io.ktor.server.application.install
|
import io.ktor.server.application.install
|
||||||
import io.ktor.server.routing.Routing
|
import io.ktor.server.routing.Routing
|
||||||
@ -24,9 +28,11 @@ fun Routing.enrichedSimpleResponse() {
|
|||||||
description(TestModules.defaultPathDescription)
|
description(TestModules.defaultPathDescription)
|
||||||
response {
|
response {
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("simple") {
|
enrichment = ObjectEnrichment("simple") {
|
||||||
TestResponse::c {
|
TestResponse::c {
|
||||||
description = "A simple description"
|
StringEnrichment("blah-blah-blah") {
|
||||||
|
description = "A simple description"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -47,12 +53,16 @@ fun Routing.enrichedSimpleRequest() {
|
|||||||
description(TestModules.defaultPathDescription)
|
description(TestModules.defaultPathDescription)
|
||||||
request {
|
request {
|
||||||
requestType(
|
requestType(
|
||||||
enrichment = TypeEnrichment("simple") {
|
enrichment = ObjectEnrichment("simple") {
|
||||||
TestSimpleRequest::a {
|
TestSimpleRequest::a {
|
||||||
description = "A simple description"
|
StringEnrichment("blah-blah-blah") {
|
||||||
|
description = "A simple description"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TestSimpleRequest::b {
|
TestSimpleRequest::b {
|
||||||
deprecated = true
|
NumberEnrichment("blah-blah-blah") {
|
||||||
|
deprecated = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@ -77,12 +87,52 @@ fun Routing.enrichedNestedCollection() {
|
|||||||
description(TestModules.defaultPathDescription)
|
description(TestModules.defaultPathDescription)
|
||||||
request {
|
request {
|
||||||
requestType(
|
requestType(
|
||||||
enrichment = TypeEnrichment("simple") {
|
enrichment = ObjectEnrichment("simple") {
|
||||||
ComplexRequest::tables {
|
ComplexRequest::tables {
|
||||||
description = "A nested item"
|
CollectionEnrichment<NestedComplexItem>("blah-blah") {
|
||||||
typeEnrichment = TypeEnrichment("nested") {
|
description = "A nested description"
|
||||||
NestedComplexItem::name {
|
itemEnrichment = ObjectEnrichment("nested") {
|
||||||
description = "A nested description"
|
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)
|
description(TestModules.defaultPathDescription)
|
||||||
request {
|
request {
|
||||||
requestType(
|
requestType(
|
||||||
enrichment = TypeEnrichment("simple") {
|
enrichment = ObjectEnrichment("simple") {
|
||||||
MultiNestedGenerics<String, ComplexRequest>::content {
|
MultiNestedGenerics<String, ComplexRequest>::content {
|
||||||
description = "Getting pretty crazy"
|
MapEnrichment<ComplexRequest>("blah") {
|
||||||
typeEnrichment = TypeEnrichment("nested") {
|
description = "A nested description"
|
||||||
ComplexRequest::tables {
|
valueEnrichment = ObjectEnrichment("nested") {
|
||||||
description = "A nested item"
|
ComplexRequest::tables {
|
||||||
typeEnrichment = TypeEnrichment("nested") {
|
CollectionEnrichment<NestedComplexItem>("blah-blah") {
|
||||||
NestedComplexItem::name {
|
|
||||||
description = "A nested description"
|
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)
|
description(TestModules.defaultPathDescription)
|
||||||
response {
|
response {
|
||||||
responseType(
|
responseType(
|
||||||
enrichment = TypeEnrichment("generic") {
|
enrichment = ObjectEnrichment("generic") {
|
||||||
|
description = "another description"
|
||||||
GenericObject<TestSimpleRequest>::data {
|
GenericObject<TestSimpleRequest>::data {
|
||||||
description = "A simple description"
|
ObjectEnrichment("simple") {
|
||||||
typeEnrichment = TypeEnrichment("simple") {
|
description = "also a description"
|
||||||
TestSimpleRequest::a {
|
TestSimpleRequest::a {
|
||||||
description = "A simple description"
|
StringEnrichment("blah-blah-blah") {
|
||||||
|
description = "A simple description"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TestSimpleRequest::b {
|
TestSimpleRequest::b {
|
||||||
deprecated = true
|
NumberEnrichment("blah-blah-blah") {
|
||||||
|
deprecated = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -111,9 +111,9 @@
|
|||||||
},
|
},
|
||||||
"tables": {
|
"tables": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/NestedComplexItem-nested"
|
"$ref": "#/components/schemas/NestedComplexItem-blah-blah"
|
||||||
},
|
},
|
||||||
"description": "A nested item",
|
"description": "A nested description",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -158,6 +158,25 @@
|
|||||||
"ONE",
|
"ONE",
|
||||||
"TWO"
|
"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": {}
|
"securitySchemes": {}
|
||||||
|
@ -105,9 +105,9 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"content": {
|
"content": {
|
||||||
"additionalProperties": {
|
"additionalProperties": {
|
||||||
"$ref": "#/components/schemas/ComplexRequest-nested"
|
"$ref": "#/components/schemas/ComplexRequest-blah"
|
||||||
},
|
},
|
||||||
"description": "Getting pretty crazy",
|
"description": "A nested description",
|
||||||
"type": "object"
|
"type": "object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -126,9 +126,9 @@
|
|||||||
},
|
},
|
||||||
"tables": {
|
"tables": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/NestedComplexItem-nested"
|
"$ref": "#/components/schemas/NestedComplexItem-blah-blah"
|
||||||
},
|
},
|
||||||
"description": "A nested item",
|
"description": "A nested description",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -173,6 +173,48 @@
|
|||||||
"ONE",
|
"ONE",
|
||||||
"TWO"
|
"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": {}
|
"securitySchemes": {}
|
||||||
|
@ -58,7 +58,7 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"$ref": "#/components/schemas/TestSimpleRequest-simple",
|
"$ref": "#/components/schemas/TestSimpleRequest-simple",
|
||||||
"description": "A simple description"
|
"description": "also a description"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
150
core/src/test/resources/T0077__enriched_top_level_list.json
Normal file
150
core/src/test/resources/T0077__enriched_top_level_list.json
Normal 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": []
|
||||||
|
}
|
@ -5,8 +5,8 @@ complexity:
|
|||||||
active: true
|
active: true
|
||||||
functionThreshold: 10
|
functionThreshold: 10
|
||||||
constructorThreshold: 15
|
constructorThreshold: 15
|
||||||
ComplexMethod:
|
CyclomaticComplexMethod:
|
||||||
threshold: 20
|
threshold: 15
|
||||||
style:
|
style:
|
||||||
MaxLineLength:
|
MaxLineLength:
|
||||||
excludes: ['**/test/**/*']
|
excludes: ['**/test/**/*']
|
||||||
|
@ -1,10 +1,10 @@
|
|||||||
Kompendium allows users to enrich their data types with additional information. This can be done by defining a
|
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
|
```kotlin
|
||||||
data class SimpleData(val a: String, val b: Int? = null)
|
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 {
|
SimpleData::a {
|
||||||
description = "This will update the field description"
|
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 ParentData(val a: String, val b: ChildData)
|
||||||
data class ChildData(val c: String, val d: Int? = null)
|
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 {
|
ChildData::c {
|
||||||
description = "This will update the field description of field c on child data"
|
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 {
|
ParentData::a {
|
||||||
description = "This will update the field description"
|
description = "This will update the field description"
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,3 +1,7 @@
|
|||||||
package io.bkbn.kompendium.enrichment
|
package io.bkbn.kompendium.enrichment
|
||||||
|
|
||||||
sealed interface Enrichment
|
sealed interface Enrichment {
|
||||||
|
val id: String
|
||||||
|
var deprecated: Boolean?
|
||||||
|
var description: String?
|
||||||
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
|
||||||
}
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -1,25 +1,3 @@
|
|||||||
package io.bkbn.kompendium.enrichment
|
package io.bkbn.kompendium.enrichment
|
||||||
|
|
||||||
import kotlin.reflect.KProperty
|
sealed interface TypeEnrichment<T> : Enrichment
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=4.0.0-alpha
|
project.version=4.0.0-alpha
|
||||||
|
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
kotlin.experimental.tryK2=true
|
#kotlin.experimental.tryK2=true
|
||||||
|
|
||||||
# Gradle
|
# Gradle
|
||||||
org.gradle.vfs.watch=true
|
org.gradle.vfs.watch=true
|
||||||
org.gradle.vfs.verbose=true
|
org.gradle.vfs.verbose=true
|
||||||
|
@ -26,7 +26,7 @@ class KotlinXSchemaConfigurator : SchemaConfigurator {
|
|||||||
.filterIsInstance<SerialName>()
|
.filterIsInstance<SerialName>()
|
||||||
.firstOrNull()?.value ?: property.name
|
.firstOrNull()?.value ?: property.name
|
||||||
|
|
||||||
override fun sealedTypeEnrichment(
|
override fun sealedObjectEnrichment(
|
||||||
implementationType: KType,
|
implementationType: KType,
|
||||||
implementationSchema: JsonSchema,
|
implementationSchema: JsonSchema,
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
|
@ -9,7 +9,7 @@ interface SchemaConfigurator {
|
|||||||
fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>>
|
fun serializableMemberProperties(clazz: KClass<*>): Collection<KProperty1<out Any, *>>
|
||||||
fun serializableName(property: KProperty1<out Any, *>): String
|
fun serializableName(property: KProperty1<out Any, *>): String
|
||||||
|
|
||||||
fun sealedTypeEnrichment(
|
fun sealedObjectEnrichment(
|
||||||
implementationType: KType,
|
implementationType: KType,
|
||||||
implementationSchema: JsonSchema
|
implementationSchema: JsonSchema
|
||||||
): JsonSchema
|
): JsonSchema
|
||||||
|
@ -1,5 +1,9 @@
|
|||||||
package io.bkbn.kompendium.json.schema
|
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.enrichment.TypeEnrichment
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
||||||
@ -23,7 +27,7 @@ object SchemaGenerator {
|
|||||||
type: KType,
|
type: KType,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
enrichment: TypeEnrichment<*>? = null
|
enrichment: Enrichment? = null
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
val slug = type.getSlug(enrichment)
|
val slug = type.getSlug(enrichment)
|
||||||
|
|
||||||
@ -47,18 +51,7 @@ object SchemaGenerator {
|
|||||||
String::class -> checkForNull(type, TypeDefinition.STRING)
|
String::class -> checkForNull(type, TypeDefinition.STRING)
|
||||||
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
|
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
|
||||||
UUID::class -> checkForNull(type, TypeDefinition.UUID)
|
UUID::class -> checkForNull(type, TypeDefinition.UUID)
|
||||||
else -> when {
|
else -> complexTypeToSchema(clazz, type, cache, schemaConfigurator, enrichment as? TypeEnrichment<*>?)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -77,4 +70,59 @@ object SchemaGenerator {
|
|||||||
true -> OneOfDefinition(NullableDefinition(), schema)
|
true -> OneOfDefinition(NullableDefinition(), schema)
|
||||||
false -> 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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,6 +5,8 @@ import kotlinx.serialization.Serializable
|
|||||||
@Serializable
|
@Serializable
|
||||||
data class MapDefinition(
|
data class MapDefinition(
|
||||||
val additionalProperties: JsonSchema,
|
val additionalProperties: JsonSchema,
|
||||||
|
val maxProperties: Int? = null,
|
||||||
|
val minProperties: Int? = null,
|
||||||
override val deprecated: Boolean? = null,
|
override val deprecated: Boolean? = null,
|
||||||
override val description: String? = null,
|
override val description: String? = null,
|
||||||
) : JsonSchema {
|
) : JsonSchema {
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package io.bkbn.kompendium.json.schema.handler
|
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.SchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||||
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
|
import io.bkbn.kompendium.json.schema.definition.ArrayDefinition
|
||||||
@ -19,18 +19,23 @@ object CollectionHandler {
|
|||||||
type: KType,
|
type: KType,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
enrichment: TypeEnrichment<*>? = null
|
enrichment: CollectionEnrichment<*>? = null
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
|
require(enrichment is CollectionEnrichment<*> || enrichment == null) {
|
||||||
|
"Enrichment for collection must be either null or a CollectionEnrichment"
|
||||||
|
}
|
||||||
|
|
||||||
val collectionType = type.arguments.first().type
|
val collectionType = type.arguments.first().type
|
||||||
?: error("This indicates a bug in Kompendium, please open a GitHub issue!")
|
?: error("This indicates a bug in Kompendium, please open a GitHub issue!")
|
||||||
val typeSchema = SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator, enrichment).let {
|
val typeSchema =
|
||||||
if ((it is TypeDefinition && it.type == "object") || it is EnumDefinition) {
|
SchemaGenerator.fromTypeToSchema(collectionType, cache, schemaConfigurator, enrichment?.itemEnrichment).let {
|
||||||
cache[collectionType.getSlug(enrichment)] = it
|
if ((it is TypeDefinition && it.type == "object") || it is EnumDefinition) {
|
||||||
ReferenceDefinition(collectionType.getReferenceSlug(enrichment))
|
cache[collectionType.getSlug(enrichment)] = it
|
||||||
} else {
|
ReferenceDefinition(collectionType.getReferenceSlug(enrichment))
|
||||||
it
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val definition = ArrayDefinition(typeSchema)
|
val definition = ArrayDefinition(typeSchema)
|
||||||
return when (type.isMarkedNullable) {
|
return when (type.isMarkedNullable) {
|
||||||
true -> OneOfDefinition(NullableDefinition(), definition)
|
true -> OneOfDefinition(NullableDefinition(), definition)
|
||||||
|
@ -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,
|
||||||
|
)
|
||||||
|
}
|
@ -1,6 +1,5 @@
|
|||||||
package io.bkbn.kompendium.json.schema.handler
|
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.EnumDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
||||||
@ -14,9 +13,8 @@ object EnumHandler {
|
|||||||
type: KType,
|
type: KType,
|
||||||
clazz: KClass<*>,
|
clazz: KClass<*>,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
enrichment: TypeEnrichment<*>? = null
|
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
cache[type.getSlug(enrichment)] = ReferenceDefinition(type.getReferenceSlug(enrichment))
|
cache[type.getSlug()] = ReferenceDefinition(type.getReferenceSlug())
|
||||||
|
|
||||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||||
return EnumDefinition(enum = options)
|
return EnumDefinition(enum = options)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package io.bkbn.kompendium.json.schema.handler
|
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.SchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
@ -20,20 +20,24 @@ object MapHandler {
|
|||||||
type: KType,
|
type: KType,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
enrichment: TypeEnrichment<*>? = null
|
enrichment: MapEnrichment<*>? = null
|
||||||
): JsonSchema {
|
): 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) {
|
require(type.arguments.first().type?.classifier as KClass<*> == String::class) {
|
||||||
"JSON requires that map keys MUST be Strings. You provided ${type.arguments.first().type}"
|
"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 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 {
|
val valueSchema =
|
||||||
if (it is TypeDefinition && it.type == "object") {
|
SchemaGenerator.fromTypeToSchema(valueType, cache, schemaConfigurator, enrichment?.valueEnrichment).let {
|
||||||
cache[valueType.getSlug(enrichment)] = it
|
if (it is TypeDefinition && it.type == "object") {
|
||||||
ReferenceDefinition(valueType.getReferenceSlug(enrichment))
|
cache[valueType.getSlug(enrichment)] = it
|
||||||
} else {
|
ReferenceDefinition(valueType.getReferenceSlug(enrichment))
|
||||||
it
|
} else {
|
||||||
|
it
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
val definition = MapDefinition(valueSchema)
|
val definition = MapDefinition(valueSchema)
|
||||||
return when (type.isMarkedNullable) {
|
return when (type.isMarkedNullable) {
|
||||||
true -> OneOfDefinition(NullableDefinition(), definition)
|
true -> OneOfDefinition(NullableDefinition(), definition)
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
package io.bkbn.kompendium.json.schema.handler
|
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.SchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||||
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
|
import io.bkbn.kompendium.json.schema.definition.AnyOfDefinition
|
||||||
@ -20,14 +20,14 @@ object SealedObjectHandler {
|
|||||||
clazz: KClass<*>,
|
clazz: KClass<*>,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
enrichment: TypeEnrichment<*>? = null,
|
enrichment: ObjectEnrichment<*>? = null,
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
val subclasses = clazz.sealedSubclasses
|
val subclasses = clazz.sealedSubclasses
|
||||||
.map { it.createType(type.arguments) }
|
.map { it.createType(type.arguments) }
|
||||||
.map { t ->
|
.map { t ->
|
||||||
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment)
|
SchemaGenerator.fromTypeToSchema(t, cache, schemaConfigurator, enrichment)
|
||||||
.let {
|
.let {
|
||||||
schemaConfigurator.sealedTypeEnrichment(t, it)
|
schemaConfigurator.sealedObjectEnrichment(t, it)
|
||||||
}.let { js ->
|
}.let { js ->
|
||||||
if (js is TypeDefinition && js.type == "object") {
|
if (js is TypeDefinition && js.type == "object") {
|
||||||
val slug = t.getSlug(enrichment)
|
val slug = t.getSlug(enrichment)
|
||||||
|
@ -1,19 +1,17 @@
|
|||||||
package io.bkbn.kompendium.json.schema.handler
|
package io.bkbn.kompendium.json.schema.handler
|
||||||
|
|
||||||
import io.bkbn.kompendium.enrichment.PropertyEnrichment
|
import io.bkbn.kompendium.enrichment.Enrichment
|
||||||
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.SchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
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.EnumDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
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.NullableDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||||
import io.bkbn.kompendium.json.schema.exception.UnknownSchemaException
|
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.getReferenceSlug
|
||||||
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
|
import io.bkbn.kompendium.json.schema.util.Helpers.getSlug
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
@ -32,28 +30,31 @@ object SimpleObjectHandler {
|
|||||||
clazz: KClass<*>,
|
clazz: KClass<*>,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
enrichment: TypeEnrichment<*>?,
|
enrichment: ObjectEnrichment<*>?,
|
||||||
): JsonSchema {
|
): 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 typeMap = clazz.typeParameters.zip(type.arguments).toMap()
|
||||||
val props = schemaConfigurator.serializableMemberProperties(clazz)
|
val props = schemaConfigurator.serializableMemberProperties(clazz)
|
||||||
.filterNot { it.javaField == null }
|
.filterNot { it.javaField == null }
|
||||||
.associate { prop ->
|
.associate { prop ->
|
||||||
val propTypeEnrichment = when (val pe = enrichment?.getEnrichmentForProperty(prop)) {
|
val propEnrichment = enrichment?.propertyEnrichment?.get(prop)
|
||||||
is PropertyEnrichment -> pe
|
|
||||||
else -> null
|
|
||||||
}
|
|
||||||
|
|
||||||
val schema = when (prop.needsToInjectGenerics(typeMap)) {
|
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)) {
|
false -> when (typeMap.containsKey(prop.returnType.classifier)) {
|
||||||
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator, propTypeEnrichment)
|
true -> handleGenericProperty(prop, typeMap, cache, schemaConfigurator, propEnrichment)
|
||||||
false -> handleProperty(prop, cache, schemaConfigurator, propTypeEnrichment?.typeEnrichment)
|
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()) {
|
val nullCheckSchema = when (prop.returnType.isMarkedNullable && !enrichedSchema.isNullable()) {
|
||||||
true -> OneOfDefinition(NullableDefinition(), enrichedSchema)
|
true -> OneOfDefinition(NullableDefinition(), enrichedSchema)
|
||||||
@ -84,11 +85,14 @@ object SimpleObjectHandler {
|
|||||||
.map { schemaConfigurator.serializableName(it) }
|
.map { schemaConfigurator.serializableName(it) }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
return TypeDefinition(
|
val definition = TypeDefinition(
|
||||||
type = "object",
|
type = "object",
|
||||||
properties = props,
|
properties = props,
|
||||||
required = required
|
required = required
|
||||||
)
|
)
|
||||||
|
|
||||||
|
cache[slug] = definition
|
||||||
|
return definition
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KProperty<*>.needsToInjectGenerics(
|
private fun KProperty<*>.needsToInjectGenerics(
|
||||||
@ -103,7 +107,7 @@ object SimpleObjectHandler {
|
|||||||
prop: KProperty<*>,
|
prop: KProperty<*>,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
propEnrichment: PropertyEnrichment?
|
propEnrichment: Enrichment?
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
val propClass = prop.returnType.classifier as KClass<*>
|
val propClass = prop.returnType.classifier as KClass<*>
|
||||||
val types = prop.returnType.arguments.map {
|
val types = prop.returnType.arguments.map {
|
||||||
@ -111,7 +115,7 @@ object SimpleObjectHandler {
|
|||||||
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
|
typeMap.filterKeys { k -> k.name == typeSymbol }.values.first()
|
||||||
}
|
}
|
||||||
val constructedType = propClass.createType(types)
|
val constructedType = propClass.createType(types)
|
||||||
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator, propEnrichment?.typeEnrichment)
|
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator, propEnrichment)
|
||||||
.let {
|
.let {
|
||||||
if (it.isOrContainsObjectOrEnumDef()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[constructedType.getSlug(propEnrichment)] = it
|
cache[constructedType.getSlug(propEnrichment)] = it
|
||||||
@ -127,14 +131,14 @@ object SimpleObjectHandler {
|
|||||||
typeMap: Map<KTypeParameter, KTypeProjection>,
|
typeMap: Map<KTypeParameter, KTypeProjection>,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
propEnrichment: PropertyEnrichment?
|
propEnrichment: Enrichment?
|
||||||
): JsonSchema {
|
): JsonSchema {
|
||||||
val type = typeMap[prop.returnType.classifier]?.type
|
val type = typeMap[prop.returnType.classifier]?.type
|
||||||
?: error("This indicates a bug in Kompendium, please open a GitHub issue")
|
?: 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()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[type.getSlug(propEnrichment?.typeEnrichment)] = it
|
cache[type.getSlug(propEnrichment)] = it
|
||||||
ReferenceDefinition(type.getReferenceSlug(propEnrichment?.typeEnrichment))
|
ReferenceDefinition(type.getReferenceSlug(propEnrichment))
|
||||||
} else {
|
} else {
|
||||||
it
|
it
|
||||||
}
|
}
|
||||||
@ -145,7 +149,7 @@ object SimpleObjectHandler {
|
|||||||
prop: KProperty<*>,
|
prop: KProperty<*>,
|
||||||
cache: MutableMap<String, JsonSchema>,
|
cache: MutableMap<String, JsonSchema>,
|
||||||
schemaConfigurator: SchemaConfigurator,
|
schemaConfigurator: SchemaConfigurator,
|
||||||
propEnrichment: TypeEnrichment<*>?
|
propEnrichment: Enrichment?
|
||||||
): JsonSchema =
|
): JsonSchema =
|
||||||
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator, propEnrichment).let {
|
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator, propEnrichment).let {
|
||||||
if (it.isOrContainsObjectOrEnumDef()) {
|
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 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,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
package io.bkbn.kompendium.json.schema.util
|
package io.bkbn.kompendium.json.schema.util
|
||||||
|
|
||||||
import io.bkbn.kompendium.enrichment.Enrichment
|
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.KClass
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
|
|
||||||
@ -11,9 +9,8 @@ object Helpers {
|
|||||||
const val COMPONENT_SLUG = "#/components/schemas"
|
const val COMPONENT_SLUG = "#/components/schemas"
|
||||||
|
|
||||||
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
|
fun KType.getSlug(enrichment: Enrichment? = null) = when (enrichment) {
|
||||||
is TypeEnrichment<*> -> getEnrichedSlug(enrichment)
|
null -> getSimpleSlug()
|
||||||
is PropertyEnrichment -> error("Slugs should not be generated for field enrichments")
|
else -> getEnrichedSlug(enrichment)
|
||||||
else -> getSimpleSlug()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun KType.getSimpleSlug(): String = when {
|
fun KType.getSimpleSlug(): String = when {
|
||||||
@ -21,12 +18,11 @@ object Helpers {
|
|||||||
else -> (classifier as KClass<*>).kompendiumSlug() ?: error("Could not determine simple name for $this")
|
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) {
|
fun KType.getReferenceSlug(enrichment: Enrichment? = null): String = when (enrichment) {
|
||||||
is TypeEnrichment<*> -> getSimpleReferenceSlug() + "-${enrichment.id}"
|
null -> getSimpleReferenceSlug()
|
||||||
is PropertyEnrichment -> error("Reference slugs should never be generated for field enrichments")
|
else -> getSimpleReferenceSlug() + "-${enrichment.id}"
|
||||||
else -> getSimpleReferenceSlug()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KType.getSimpleReferenceSlug() = when {
|
private fun KType.getSimpleReferenceSlug() = when {
|
||||||
|
@ -13,7 +13,10 @@ import io.bkbn.kompendium.core.fixtures.TransientObject
|
|||||||
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
||||||
import io.bkbn.kompendium.core.fixtures.GenericObject
|
import io.bkbn.kompendium.core.fixtures.GenericObject
|
||||||
import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
|
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.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
import io.kotest.assertions.json.shouldEqualJson
|
import io.kotest.assertions.json.shouldEqualJson
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
@ -114,12 +117,16 @@ class SchemaGeneratorTest : DescribeSpec({
|
|||||||
it("Can attach an enrichment to a simple type") {
|
it("Can attach an enrichment to a simple type") {
|
||||||
jsonSchemaTest<TestSimpleRequest>(
|
jsonSchemaTest<TestSimpleRequest>(
|
||||||
snapshotName = "T0022__enriched_simple_object.json",
|
snapshotName = "T0022__enriched_simple_object.json",
|
||||||
enrichment = TypeEnrichment("simple") {
|
enrichment = ObjectEnrichment("simple") {
|
||||||
TestSimpleRequest::a {
|
TestSimpleRequest::a {
|
||||||
description = "This is a simple description"
|
StringEnrichment("blah") {
|
||||||
|
description = "This is a simple description"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TestSimpleRequest::b {
|
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") {
|
it("Can properly assign a reference to a nested enrichment") {
|
||||||
jsonSchemaTest<ComplexRequest>(
|
jsonSchemaTest<ComplexRequest>(
|
||||||
snapshotName = "T0023__enriched_nested_reference.json",
|
snapshotName = "T0023__enriched_nested_reference.json",
|
||||||
enrichment = TypeEnrichment("example") {
|
enrichment = ObjectEnrichment("example") {
|
||||||
ComplexRequest::tables {
|
ComplexRequest::tables {
|
||||||
description = "Collection of important items"
|
CollectionEnrichment<List<NestedComplexItem>>("tables") {
|
||||||
typeEnrichment = TypeEnrichment("table") {
|
description = "Collection of important items"
|
||||||
NestedComplexItem::name {
|
itemEnrichment = ObjectEnrichment("table") {
|
||||||
description = "The name of the 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") {
|
it("Can properly assign a reference to a generic object") {
|
||||||
jsonSchemaTest<GenericObject<TestSimpleRequest>>(
|
jsonSchemaTest<GenericObject<TestSimpleRequest>>(
|
||||||
snapshotName = "T0025__enrichment_generic_object.json",
|
snapshotName = "T0025__enrichment_generic_object.json",
|
||||||
enrichment = TypeEnrichment("generic") {
|
enrichment = ObjectEnrichment("generic") {
|
||||||
GenericObject<TestSimpleRequest>::data {
|
GenericObject<TestSimpleRequest>::data {
|
||||||
description = "This is a generic param"
|
ObjectEnrichment<TestSimpleRequest>("blob") {
|
||||||
typeEnrichment = TypeEnrichment("simple") {
|
description = "This is a generic object"
|
||||||
TestSimpleRequest::a {
|
TestSimpleRequest::a {
|
||||||
description = "This is a simple description"
|
StringEnrichment("blah") {
|
||||||
|
description = "This is a simple description"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
TestSimpleRequest::b {
|
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 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
|
// act
|
||||||
val schema = SchemaGenerator.fromTypeToSchema(
|
val schema = SchemaGenerator.fromTypeToSchema(
|
||||||
type = typeOf<T>(),
|
type = typeOf<T>(),
|
||||||
|
@ -9,7 +9,7 @@
|
|||||||
},
|
},
|
||||||
"tables": {
|
"tables": {
|
||||||
"items": {
|
"items": {
|
||||||
"$ref": "#/components/schemas/NestedComplexItem-table"
|
"$ref": "#/components/schemas/NestedComplexItem-tables"
|
||||||
},
|
},
|
||||||
"description": "Collection of important items",
|
"description": "Collection of important items",
|
||||||
"type": "array"
|
"type": "array"
|
||||||
|
@ -2,8 +2,8 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"data": {
|
"data": {
|
||||||
"description": "This is a generic param",
|
"$ref": "#/components/schemas/TestSimpleRequest-blob",
|
||||||
"$ref": "#/components/schemas/TestSimpleRequest-simple"
|
"description": "This is a generic object"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
|
@ -6,7 +6,10 @@ import io.bkbn.kompendium.core.plugin.NotarizedApplication
|
|||||||
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||||
import io.bkbn.kompendium.core.routes.redoc
|
import io.bkbn.kompendium.core.routes.redoc
|
||||||
import io.bkbn.kompendium.core.routes.swagger
|
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.KotlinXSchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||||
import io.bkbn.kompendium.oas.payload.Parameter
|
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 {
|
ExampleRequest::thingA {
|
||||||
description = "This is a thing"
|
StringEnrichment("thingA") {
|
||||||
|
description = "This is a thing"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ExampleRequest::thingB {
|
ExampleRequest::thingB {
|
||||||
description = "This is another thing"
|
NumberEnrichment("thingB") {
|
||||||
|
description = "This is another thing"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
ExampleRequest::thingC {
|
ExampleRequest::thingC {
|
||||||
deprecated = true
|
ObjectEnrichment<InnerRequest>("thingC") {
|
||||||
description = "A good but old field"
|
deprecated = true
|
||||||
typeEnrichment = TypeEnrichment("big-tings") {
|
description = "A good but old field"
|
||||||
InnerRequest::d {
|
InnerRequest::d {
|
||||||
exclusiveMaximum = 10.0
|
NumberEnrichment("blahblah") {
|
||||||
exclusiveMinimum = 1.1
|
exclusiveMinimum = 1.1
|
||||||
description = "THE BIG D"
|
exclusiveMaximum = 10.0
|
||||||
|
description = "THE BIG D"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val testResponseEnrichment = TypeEnrichment("testerino") {
|
private val testResponseEnrichment = ObjectEnrichment("testerino") {
|
||||||
ExampleResponse::isReal {
|
ExampleResponse::isReal {
|
||||||
description = "Is this thing real or not?"
|
BooleanEnrichment("blah") {
|
||||||
|
description = "Is this thing real or not?"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user