feat: enriched enrichments (#566)

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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