@ -2,6 +2,9 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
- Updates for Ktor 3.x support
|
||||
- Remove deprecated `NotarizedLocations`
|
||||
|
||||
### Added
|
||||
|
||||
### Changed
|
||||
|
@ -1,6 +1,6 @@
|
||||
plugins {
|
||||
kotlin("jvm") version "1.9.25" apply false
|
||||
kotlin("plugin.serialization") version "1.9.25" apply false
|
||||
kotlin("jvm") version "2.0.21" apply false
|
||||
kotlin("plugin.serialization") version "2.0.21" apply false
|
||||
id("io.bkbn.sourdough.library.jvm") version "0.12.2" apply false
|
||||
id("io.bkbn.sourdough.application.jvm") version "0.12.2" apply false
|
||||
id("io.bkbn.sourdough.root") version "0.12.2"
|
||||
|
@ -20,6 +20,7 @@ sourdoughLibrary {
|
||||
dependencies {
|
||||
// VERSIONS
|
||||
val kotestVersion: String by project
|
||||
val kotlinSerializeVersion: String by project
|
||||
val ktorVersion: String by project
|
||||
val detektVersion: String by project
|
||||
|
||||
@ -43,7 +44,7 @@ dependencies {
|
||||
testFixturesApi("io.kotest:kotest-assertions-core-jvm:$kotestVersion")
|
||||
testFixturesApi("io.kotest:kotest-property-jvm:$kotestVersion")
|
||||
testFixturesApi("io.kotest:kotest-assertions-json-jvm:$kotestVersion")
|
||||
testFixturesApi("io.kotest:kotest-assertions-ktor-jvm:4.4.3")
|
||||
testFixturesApi("io.kotest.extensions:kotest-assertions-ktor:2.0.0")
|
||||
|
||||
testFixturesApi("io.ktor:ktor-server-core:$ktorVersion")
|
||||
testFixturesApi("io.ktor:ktor-server-test-host:$ktorVersion")
|
||||
@ -57,7 +58,7 @@ dependencies {
|
||||
|
||||
testFixturesApi("dev.forst:ktor-api-key:2.2.4")
|
||||
|
||||
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializeVersion")
|
||||
}
|
||||
|
||||
testing {
|
||||
|
@ -6,7 +6,6 @@ import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
|
||||
import io.bkbn.kompendium.oas.OpenApiSpec
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.createApplicationPlugin
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.routing.Routing
|
||||
|
@ -78,7 +78,7 @@ object NotarizedRoute {
|
||||
|
||||
fun Route.calculateRoutePath() = toString()
|
||||
.let {
|
||||
application.environment.rootPath.takeIf { root -> root.isNotEmpty() }
|
||||
application.rootPath.takeIf { root -> root.isNotEmpty() }
|
||||
?.let { root ->
|
||||
val sanitizedRoute = if (root.startsWith("/")) root else "/$root"
|
||||
it.replace(sanitizedRoute, "")
|
||||
|
@ -18,6 +18,7 @@ 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.Principal
|
||||
import io.ktor.server.auth.UserIdPrincipal
|
||||
import io.ktor.server.auth.basic
|
||||
import io.ktor.server.auth.jwt.jwt
|
||||
@ -92,14 +93,18 @@ class KompendiumAuthenticationTest : DescribeSpec({
|
||||
) { customAuthConfig() }
|
||||
}
|
||||
it("Can provide multiple authentication strategies") {
|
||||
data class TestAppPrincipal(val key: String) : Principal
|
||||
TestHelpers.openApiTestAllSerializers(
|
||||
snapshotName = "T0047__multiple_auth_strategies.json",
|
||||
applicationSetup = {
|
||||
install(Authentication) {
|
||||
apiKey("api-key") {
|
||||
headerName = "X-API-KEY"
|
||||
validate {
|
||||
UserIdPrincipal("Placeholder")
|
||||
validate { key ->
|
||||
// api key library (dev.forst.ktor.apikey) is using the deprecated `Principal` class
|
||||
key
|
||||
.takeIf { it == "api-key" }
|
||||
?.let { TestAppPrincipal(it) }
|
||||
}
|
||||
}
|
||||
jwt("jwt") {
|
||||
|
@ -38,7 +38,6 @@ import io.bkbn.kompendium.oas.security.BasicAuth
|
||||
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
|
||||
import io.kotest.core.spec.style.DescribeSpec
|
||||
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.UserIdPrincipal
|
||||
@ -182,13 +181,16 @@ class KompendiumTest : DescribeSpec({
|
||||
it("Can generate paths without application root-path") {
|
||||
openApiTestAllSerializers(
|
||||
"T0054__app_with_rootpath.json",
|
||||
applicationEnvironmentBuilder = {
|
||||
applicationSetup = {
|
||||
rootPath = "/example"
|
||||
},
|
||||
specOverrides = {
|
||||
copy(
|
||||
servers = servers.map { it.copy(url = URI("${it.url}/example")) }.toMutableList()
|
||||
)
|
||||
},
|
||||
serverConfigSetup = {
|
||||
rootPath = "/example"
|
||||
}
|
||||
) { notarizedGet() }
|
||||
}
|
||||
@ -202,15 +204,13 @@ class KompendiumTest : DescribeSpec({
|
||||
snapshotName = "T0072__custom_serialization_strategy.json",
|
||||
notarizedApplicationConfigOverrides = {
|
||||
specRoute = { spec, routing ->
|
||||
routing {
|
||||
route("/openapi.json") {
|
||||
routing.route("/openapi.json") {
|
||||
get {
|
||||
call.response.headers.append("Content-Type", "application/json")
|
||||
call.respondText { customJsonEncoder.encodeToString(spec) }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
contentNegotiation = {
|
||||
json(
|
||||
|
@ -9,13 +9,11 @@ import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
|
||||
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
|
||||
import io.bkbn.kompendium.core.util.TestModules.rootPath
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.defaultAuthConfig() {
|
||||
fun Route.defaultAuthConfig() {
|
||||
authenticate("basic") {
|
||||
route(rootPath) {
|
||||
basicGetGenerator<TestResponse>()
|
||||
@ -23,7 +21,7 @@ fun Routing.defaultAuthConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.customAuthConfig() {
|
||||
fun Route.customAuthConfig() {
|
||||
authenticate("auth-oauth-google") {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
@ -44,7 +42,7 @@ fun Routing.customAuthConfig() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.customScopesOnSiblingPathOperations() {
|
||||
fun Route.customScopesOnSiblingPathOperations() {
|
||||
authenticate("auth-oauth-google") {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
@ -81,7 +79,7 @@ fun Routing.customScopesOnSiblingPathOperations() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.multipleAuthStrategies() {
|
||||
fun Route.multipleAuthStrategies() {
|
||||
authenticate("jwt", "api-key") {
|
||||
route(rootPath) {
|
||||
basicGetGenerator<TestResponse>()
|
||||
|
@ -12,11 +12,10 @@ 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
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.intConstraints() {
|
||||
fun Route.intConstraints() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -43,7 +42,7 @@ fun Routing.intConstraints() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.doubleConstraints() {
|
||||
fun Route.doubleConstraints() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -70,7 +69,7 @@ fun Routing.doubleConstraints() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.stringConstraints() {
|
||||
fun Route.stringConstraints() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -96,7 +95,7 @@ fun Routing.stringConstraints() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.stringPatternConstraints() {
|
||||
fun Route.stringPatternConstraints() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -121,7 +120,7 @@ fun Routing.stringPatternConstraints() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.stringContentEncodingConstraints() {
|
||||
fun Route.stringContentEncodingConstraints() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -147,7 +146,7 @@ fun Routing.stringContentEncodingConstraints() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.arrayConstraints() {
|
||||
fun Route.arrayConstraints() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
|
@ -3,8 +3,8 @@ package io.bkbn.kompendium.core.util
|
||||
import io.bkbn.kompendium.core.fixtures.SerialNameObject
|
||||
import io.bkbn.kompendium.core.fixtures.TransientObject
|
||||
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
|
||||
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
|
||||
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbackedObject>()
|
||||
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
|
||||
fun Route.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
|
||||
fun Route.unbackedFieldsResponse() = basicGetGenerator<UnbackedObject>()
|
||||
fun Route.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
|
||||
|
@ -3,9 +3,9 @@ package io.bkbn.kompendium.core.util
|
||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
|
||||
fun Routing.defaultParameter() = basicGetGenerator<TestResponse>(
|
||||
fun Route.defaultParameter() = basicGetGenerator<TestResponse>(
|
||||
params = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
|
@ -16,11 +16,10 @@ 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
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.enrichedSimpleResponse() {
|
||||
fun Route.enrichedSimpleResponse() {
|
||||
route("/enriched") {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -44,7 +43,7 @@ fun Routing.enrichedSimpleResponse() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.enrichedSimpleRequest() {
|
||||
fun Route.enrichedSimpleRequest() {
|
||||
route("/example") {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = TestModules.defaultParams
|
||||
@ -78,7 +77,7 @@ fun Routing.enrichedSimpleRequest() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.enrichedNestedCollection() {
|
||||
fun Route.enrichedNestedCollection() {
|
||||
route("/example") {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = TestModules.defaultParams
|
||||
@ -114,7 +113,7 @@ fun Routing.enrichedNestedCollection() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.enrichedTopLevelCollection() {
|
||||
fun Route.enrichedTopLevelCollection() {
|
||||
route("/example") {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = TestModules.defaultParams
|
||||
@ -150,7 +149,7 @@ fun Routing.enrichedTopLevelCollection() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.enrichedComplexGenericType() {
|
||||
fun Route.enrichedComplexGenericType() {
|
||||
route("/example") {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = TestModules.defaultParams
|
||||
@ -193,7 +192,7 @@ fun Routing.enrichedComplexGenericType() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.enrichedGenericResponse() {
|
||||
fun Route.enrichedGenericResponse() {
|
||||
route("/example") {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -228,7 +227,7 @@ fun Routing.enrichedGenericResponse() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.enrichedMap() {
|
||||
fun Route.enrichedMap() {
|
||||
route("/example") {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
|
@ -3,10 +3,10 @@ package io.bkbn.kompendium.core.util
|
||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||
import io.bkbn.kompendium.core.util.TestModules.defaultPath
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.samePathSameMethod() {
|
||||
fun Route.samePathSameMethod() {
|
||||
route(defaultPath) {
|
||||
basicGetGenerator<TestResponse>()
|
||||
authenticate("basic") {
|
||||
|
@ -14,11 +14,10 @@ import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.MediaType
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.reqRespExamples() {
|
||||
fun Route.reqRespExamples() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
post = PostInfo.builder {
|
||||
@ -44,7 +43,7 @@ fun Routing.reqRespExamples() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.exampleParams() = basicGetGenerator<TestResponse>(
|
||||
fun Route.exampleParams() = basicGetGenerator<TestResponse>(
|
||||
params = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
@ -57,7 +56,7 @@ fun Routing.exampleParams() = basicGetGenerator<TestResponse>(
|
||||
)
|
||||
)
|
||||
|
||||
fun Routing.optionalReqExample() {
|
||||
fun Route.optionalReqExample() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
post = PostInfo.builder {
|
||||
@ -84,7 +83,7 @@ fun Routing.optionalReqExample() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.exampleSummaryAndDescription() {
|
||||
fun Route.exampleSummaryAndDescription() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
post = PostInfo.builder {
|
||||
|
@ -11,11 +11,10 @@ import io.bkbn.kompendium.core.util.TestModules.defaultPathSummary
|
||||
import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
|
||||
import io.bkbn.kompendium.core.util.TestModules.rootPath
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.singleException() {
|
||||
fun Route.singleException() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -36,7 +35,7 @@ fun Routing.singleException() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.multipleExceptions() {
|
||||
fun Route.multipleExceptions() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -62,7 +61,7 @@ fun Routing.multipleExceptions() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.polymorphicException() {
|
||||
fun Route.polymorphicException() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
@ -83,7 +82,7 @@ fun Routing.polymorphicException() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.genericException() {
|
||||
fun Route.genericException() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
get = GetInfo.builder {
|
||||
|
@ -21,17 +21,16 @@ import io.bkbn.kompendium.core.util.TestModules.defaultResponseDescription
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.authenticate
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
|
||||
fun Routing.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
|
||||
fun Routing.nullableEnumField() = basicGetGenerator<NullableEnum>()
|
||||
fun Routing.nullableReference() = basicGetGenerator<ManyThings>()
|
||||
fun Routing.dateTimeString() = basicGetGenerator<DateTimeString>()
|
||||
fun Routing.headerParameter() = basicGetGenerator<TestResponse>(
|
||||
fun Route.withOperationId() = basicGetGenerator<TestResponse>(operationId = "getThisDude")
|
||||
fun Route.nullableNestedObject() = basicGetGenerator<ProfileUpdateRequest>()
|
||||
fun Route.nullableEnumField() = basicGetGenerator<NullableEnum>()
|
||||
fun Route.nullableReference() = basicGetGenerator<ManyThings>()
|
||||
fun Route.dateTimeString() = basicGetGenerator<DateTimeString>()
|
||||
fun Route.headerParameter() = basicGetGenerator<TestResponse>(
|
||||
params = listOf(
|
||||
Parameter(
|
||||
name = "X-User-Email",
|
||||
@ -42,10 +41,10 @@ fun Routing.headerParameter() = basicGetGenerator<TestResponse>(
|
||||
)
|
||||
)
|
||||
|
||||
fun Routing.nestedTypeName() = basicGetGenerator<Nested.Response>()
|
||||
fun Routing.topLevelNullable() = basicGetGenerator<TestResponse?>()
|
||||
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
|
||||
fun Routing.samePathDifferentMethodsAndAuth() {
|
||||
fun Route.nestedTypeName() = basicGetGenerator<Nested.Response>()
|
||||
fun Route.topLevelNullable() = basicGetGenerator<TestResponse?>()
|
||||
fun Route.simpleRecursive() = basicGetGenerator<ColumnSchema>()
|
||||
fun Route.samePathDifferentMethodsAndAuth() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
|
@ -26,11 +26,9 @@ import io.bkbn.kompendium.oas.payload.Header
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.HttpHeaders
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.response.respond
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.delete
|
||||
import io.ktor.server.routing.get
|
||||
import io.ktor.server.routing.head
|
||||
@ -40,7 +38,7 @@ import io.ktor.server.routing.post
|
||||
import io.ktor.server.routing.put
|
||||
import io.ktor.server.routing.route
|
||||
|
||||
fun Routing.notarizedGet() {
|
||||
fun Route.notarizedGet() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -60,7 +58,7 @@ fun Routing.notarizedGet() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.responseHeaders() {
|
||||
fun Route.responseHeaders() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -92,7 +90,7 @@ fun Routing.responseHeaders() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.notarizedPost() {
|
||||
fun Route.notarizedPost() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -116,7 +114,7 @@ fun Routing.notarizedPost() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.notarizedPut() {
|
||||
fun Route.notarizedPut() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -140,7 +138,7 @@ fun Routing.notarizedPut() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.notarizedDelete() {
|
||||
fun Route.notarizedDelete() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -160,7 +158,7 @@ fun Routing.notarizedDelete() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.notarizedPatch() {
|
||||
fun Route.notarizedPatch() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -184,7 +182,7 @@ fun Routing.notarizedPatch() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.notarizedHead() {
|
||||
fun Route.notarizedHead() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -205,7 +203,7 @@ fun Routing.notarizedHead() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.notarizedOptions() {
|
||||
fun Route.notarizedOptions() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -225,7 +223,7 @@ fun Routing.notarizedOptions() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.complexRequest() {
|
||||
fun Route.complexRequest() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
put = PutInfo.builder {
|
||||
@ -248,7 +246,7 @@ fun Routing.complexRequest() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.primitives() {
|
||||
fun Route.primitives() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
put = PutInfo.builder {
|
||||
@ -268,7 +266,7 @@ fun Routing.primitives() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.returnsList() {
|
||||
fun Route.returnsList() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -285,7 +283,7 @@ fun Routing.returnsList() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.returnsEnumList() {
|
||||
fun Route.returnsEnumList() {
|
||||
route(defaultPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = defaultParams
|
||||
@ -302,7 +300,7 @@ fun Routing.returnsEnumList() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.nonRequiredParams() {
|
||||
fun Route.nonRequiredParams() {
|
||||
route("/optional") {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = listOf(
|
||||
@ -331,7 +329,7 @@ fun Routing.nonRequiredParams() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.overrideMediaTypes() {
|
||||
fun Route.overrideMediaTypes() {
|
||||
route("/media_types") {
|
||||
install(NotarizedRoute()) {
|
||||
put = PutInfo.builder {
|
||||
@ -353,7 +351,7 @@ fun Routing.overrideMediaTypes() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.postNoReqBody() {
|
||||
fun Route.postNoReqBody() {
|
||||
route("/no_req_body") {
|
||||
install(NotarizedRoute()) {
|
||||
post = PostInfo.builder {
|
||||
@ -369,7 +367,7 @@ fun Routing.postNoReqBody() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.fieldOutsideConstructor() {
|
||||
fun Route.fieldOutsideConstructor() {
|
||||
route("/field_outside_constructor") {
|
||||
install(NotarizedRoute()) {
|
||||
post = PostInfo.builder {
|
||||
|
@ -10,17 +10,17 @@ import io.bkbn.kompendium.core.fixtures.Gibbity
|
||||
import io.bkbn.kompendium.core.fixtures.Gizmo
|
||||
import io.bkbn.kompendium.core.fixtures.MultiNestedGenerics
|
||||
import io.bkbn.kompendium.core.fixtures.Page
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
|
||||
fun Routing.polymorphicResponse() = basicGetGenerator<FlibbityGibbit>()
|
||||
fun Routing.polymorphicCollectionResponse() = basicGetGenerator<List<FlibbityGibbit>>()
|
||||
fun Routing.polymorphicMapResponse() = basicGetGenerator<Map<String, FlibbityGibbit>>()
|
||||
fun Routing.simpleGenericResponse() = basicGetGenerator<Gibbity<String>>()
|
||||
fun Routing.gnarlyGenericResponse() = basicGetGenerator<Foosy<Barzo<Int>, String>>()
|
||||
fun Routing.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>()
|
||||
fun Routing.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
|
||||
fun Routing.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
|
||||
fun Routing.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
|
||||
fun Routing.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
|
||||
fun Routing.overrideSealedTypeIdentifier() = basicGetGenerator<ChillaxificationMaximization>()
|
||||
fun Routing.subtypeNotCompleteSetOfParentProperties() = basicGetGenerator<Gizmo>()
|
||||
fun Route.polymorphicResponse() = basicGetGenerator<FlibbityGibbit>()
|
||||
fun Route.polymorphicCollectionResponse() = basicGetGenerator<List<FlibbityGibbit>>()
|
||||
fun Route.polymorphicMapResponse() = basicGetGenerator<Map<String, FlibbityGibbit>>()
|
||||
fun Route.simpleGenericResponse() = basicGetGenerator<Gibbity<String>>()
|
||||
fun Route.gnarlyGenericResponse() = basicGetGenerator<Foosy<Barzo<Int>, String>>()
|
||||
fun Route.nestedGenericResponse() = basicGetGenerator<Gibbity<Map<String, String>>>()
|
||||
fun Route.genericPolymorphicResponse() = basicGetGenerator<Flibbity<Double>>()
|
||||
fun Route.genericPolymorphicResponseMultipleImpls() = basicGetGenerator<Flibbity<FlibbityGibbit>>()
|
||||
fun Route.nestedGenericCollection() = basicGetGenerator<Page<Int>>()
|
||||
fun Route.nestedGenericMultipleParamsCollection() = basicGetGenerator<MultiNestedGenerics<String, ComplexRequest>>()
|
||||
fun Route.overrideSealedTypeIdentifier() = basicGetGenerator<ChillaxificationMaximization>()
|
||||
fun Route.subtypeNotCompleteSetOfParentProperties() = basicGetGenerator<Gizmo>()
|
||||
|
@ -5,9 +5,9 @@ import io.bkbn.kompendium.core.fixtures.NullableField
|
||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
|
||||
fun Routing.requiredParams() = basicGetGenerator<TestResponse>(
|
||||
fun Route.requiredParams() = basicGetGenerator<TestResponse>(
|
||||
params = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
@ -17,7 +17,7 @@ fun Routing.requiredParams() = basicGetGenerator<TestResponse>(
|
||||
)
|
||||
)
|
||||
|
||||
fun Routing.nonRequiredParam() = basicGetGenerator<TestResponse>(
|
||||
fun Route.nonRequiredParam() = basicGetGenerator<TestResponse>(
|
||||
params = listOf(
|
||||
Parameter(
|
||||
name = "id",
|
||||
@ -28,5 +28,5 @@ fun Routing.nonRequiredParam() = basicGetGenerator<TestResponse>(
|
||||
)
|
||||
)
|
||||
|
||||
fun Routing.defaultField() = basicGetGenerator<DefaultField>()
|
||||
fun Routing.nullableField() = basicGetGenerator<NullableField>()
|
||||
fun Route.defaultField() = basicGetGenerator<DefaultField>()
|
||||
fun Route.nullableField() = basicGetGenerator<NullableField>()
|
||||
|
@ -11,12 +11,11 @@ import io.bkbn.kompendium.core.util.TestModules.rootPath
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.route
|
||||
import io.ktor.server.routing.param
|
||||
|
||||
fun Routing.simplePathParsing() {
|
||||
fun Route.simplePathParsing() {
|
||||
route("/this") {
|
||||
route("/is") {
|
||||
route("/a") {
|
||||
@ -49,7 +48,7 @@ fun Routing.simplePathParsing() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.rootRoute() {
|
||||
fun Route.rootRoute() {
|
||||
route(rootPath) {
|
||||
install(NotarizedRoute()) {
|
||||
parameters = listOf(defaultParams.last())
|
||||
@ -66,7 +65,7 @@ fun Routing.rootRoute() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.nestedUnderRoot() {
|
||||
fun Route.nestedUnderRoot() {
|
||||
route("/") {
|
||||
route("/testerino") {
|
||||
install(NotarizedRoute()) {
|
||||
@ -84,7 +83,7 @@ fun Routing.nestedUnderRoot() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.trailingSlash() {
|
||||
fun Route.trailingSlash() {
|
||||
route("/test") {
|
||||
route("/") {
|
||||
install(NotarizedRoute()) {
|
||||
@ -102,7 +101,7 @@ fun Routing.trailingSlash() {
|
||||
}
|
||||
}
|
||||
|
||||
fun Routing.paramWrapper() {
|
||||
fun Route.paramWrapper() {
|
||||
route("/test") {
|
||||
param("a") {
|
||||
param("b") {
|
||||
|
@ -9,7 +9,6 @@ import io.bkbn.kompendium.core.util.TestModules.rootPath
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.route
|
||||
|
@ -17,10 +17,11 @@ import io.ktor.client.statement.bodyAsText
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.engine.ApplicationEngineEnvironmentBuilder
|
||||
import io.ktor.server.application.ServerConfigBuilder
|
||||
import io.ktor.server.engine.ApplicationEnvironmentBuilder
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiationConfig
|
||||
import io.ktor.server.routing.Routing
|
||||
import io.ktor.server.routing.Route
|
||||
import io.ktor.server.testing.ApplicationTestBuilder
|
||||
import io.ktor.server.testing.testApplication
|
||||
import kotlinx.serialization.json.Json
|
||||
@ -59,7 +60,7 @@ object TestHelpers {
|
||||
customTypes: Map<KType, JsonSchema> = emptyMap(),
|
||||
applicationSetup: Application.() -> Unit = { },
|
||||
specOverrides: OpenApiSpec.() -> OpenApiSpec = { this },
|
||||
applicationEnvironmentBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit = {},
|
||||
applicationEnvironmentBuilder: ApplicationEnvironmentBuilder.() -> Unit = {},
|
||||
notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit = {},
|
||||
contentNegotiation: ContentNegotiationConfig.() -> Unit = {
|
||||
json(Json {
|
||||
@ -68,7 +69,9 @@ object TestHelpers {
|
||||
serializersModule = KompendiumSerializersModule.module
|
||||
})
|
||||
},
|
||||
routeUnderTest: Routing.() -> Unit
|
||||
|
||||
serverConfigSetup: ServerConfigBuilder.() -> Unit = { },
|
||||
routeUnderTest: Route.() -> Unit
|
||||
) {
|
||||
openApiTest(
|
||||
snapshotName,
|
||||
@ -78,19 +81,21 @@ object TestHelpers {
|
||||
customTypes,
|
||||
notarizedApplicationConfigOverrides,
|
||||
contentNegotiation,
|
||||
applicationEnvironmentBuilder
|
||||
applicationEnvironmentBuilder,
|
||||
serverConfigSetup
|
||||
)
|
||||
}
|
||||
|
||||
private fun openApiTest(
|
||||
snapshotName: String,
|
||||
routeUnderTest: Routing.() -> Unit,
|
||||
routeUnderTest: Route.() -> Unit,
|
||||
applicationSetup: Application.() -> Unit,
|
||||
specOverrides: OpenApiSpec.() -> OpenApiSpec,
|
||||
typeOverrides: Map<KType, JsonSchema> = emptyMap(),
|
||||
notarizedApplicationConfigOverrides: NotarizedApplication.Config.() -> Unit,
|
||||
contentNegotiation: ContentNegotiationConfig.() -> Unit,
|
||||
applicationBuilder: ApplicationEngineEnvironmentBuilder.() -> Unit
|
||||
applicationBuilder: ApplicationEnvironmentBuilder.() -> Unit,
|
||||
serverConfigSetup: ServerConfigBuilder.() -> Unit
|
||||
) = testApplication {
|
||||
environment(applicationBuilder)
|
||||
install(NotarizedApplication()) {
|
||||
@ -103,12 +108,14 @@ object TestHelpers {
|
||||
contentNegotiation()
|
||||
}
|
||||
application(applicationSetup)
|
||||
serverConfig(serverConfigSetup)
|
||||
routing {
|
||||
swagger()
|
||||
redoc()
|
||||
routeUnderTest()
|
||||
}
|
||||
val root = ApplicationEngineEnvironmentBuilder().apply(applicationBuilder).rootPath
|
||||
|
||||
val root = ServerConfigBuilder(ApplicationEnvironmentBuilder().apply(applicationBuilder).build()).apply(serverConfigSetup).rootPath
|
||||
compareOpenAPISpec(root, snapshotName)
|
||||
}
|
||||
}
|
||||
|
@ -4,7 +4,6 @@
|
||||
* [Plugins](plugins/index.md)
|
||||
* [Notarized Application](plugins/notarized_application.md)
|
||||
* [Notarized Route](plugins/notarized_route.md)
|
||||
* [Notarized Locations](plugins/notarized_locations.md)
|
||||
* [Notarized Resources](plugins/notarized_resources.md)
|
||||
* [Concepts](concepts/index.md)
|
||||
* [Enrichment](concepts/enrichment.md)
|
||||
|
@ -12,7 +12,6 @@ At the moment, the following playground applications are
|
||||
| Gson | Serialization using Gson instead of the default Kotlinx |
|
||||
| Hidden Docs | Place your generated documentation behind authorization |
|
||||
| Jackson | Serialization using Jackson instead of the default KotlinX |
|
||||
| Locations | Using the Ktor Locations API to define routes |
|
||||
| Resources | Using the Ktor Resources API to define routes |
|
||||
|
||||
You can find all of the playground
|
||||
|
@ -7,5 +7,5 @@ From there, a `NotarizedRoute` plugin is attached to each route you wish to docu
|
||||
be an iterative process. Each route you notarize will be picked up and injected into the OpenAPI spec that Kompendium
|
||||
generates for you.
|
||||
|
||||
Finally, there is the `NotarizedLocations` plugin that allows you to leverage and document your usage of the
|
||||
Ktor [Locations](https://ktor.io/docs/locations.html) API.
|
||||
Finally, there is the `NotarizedResources` plugin that allows you to leverage and document your usage of the
|
||||
Ktor [Resources](https://ktor.io/docs/server-resources.html) API.
|
||||
|
@ -1,62 +0,0 @@
|
||||
The Ktor Locations API is an experimental API that allows users to add increased type safety to their defined routes.
|
||||
|
||||
You can read more about it [here](https://ktor.io/docs/locations.html).
|
||||
|
||||
Kompendium supports Locations through an ancillary module `kompendium-locations`
|
||||
|
||||
## Adding the Artifact
|
||||
|
||||
Prior to documenting your locations, you will need to add the artifact to your gradle build file.
|
||||
|
||||
```kotlin
|
||||
dependencies {
|
||||
implementation("io.bkbn:kompendium-locations:latest.release")
|
||||
}
|
||||
```
|
||||
|
||||
## Installing Plugin
|
||||
|
||||
Once you have installed the dependency, you can install the plugin. The `NotarizedLocations` plugin is an _application_
|
||||
level plugin, and **must** be install after both the `NotarizedApplication` plugin and the Ktor `Locations` plugin.
|
||||
|
||||
```kotlin
|
||||
private fun Application.mainModule() {
|
||||
install(Locations)
|
||||
install(NotarizedApplication()) {
|
||||
spec = baseSpec
|
||||
}
|
||||
install(NotarizedLocations()) {
|
||||
locations = mapOf(
|
||||
Listing::class to NotarizedLocations.LocationMetadata(
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "name",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING
|
||||
),
|
||||
Parameter(
|
||||
name = "page",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT
|
||||
)
|
||||
),
|
||||
get = GetInfo.builder {
|
||||
summary("Get user by id")
|
||||
description("A very neat endpoint!")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<ExampleResponse>()
|
||||
description("Will return whether or not the user is real 😱")
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Here, the `locations` property is a map of `KClass<*>` to metadata describing that locations metadata. This
|
||||
metadata is functionally identical to how a standard `NotarizedRoute` is defined.
|
||||
|
||||
> ⚠️ If you try to map a class that is not annotated with the ktor `@Location` annotation, you will get a runtime
|
||||
> exception!
|
@ -12,6 +12,8 @@ org.gradle.jvmargs=-Xmx2000m
|
||||
org.gradle.parallel=true
|
||||
|
||||
# Dependencies
|
||||
ktorVersion=2.3.12
|
||||
kotestVersion=5.9.1
|
||||
kotlinVersion=2.0.21
|
||||
kotlinSerializeVersion=1.7.+
|
||||
ktorVersion=3.0.0
|
||||
kotestVersion=6.0.0.M1
|
||||
detektVersion=1.23.7
|
||||
|
@ -19,12 +19,14 @@ sourdoughLibrary {
|
||||
dependencies {
|
||||
// Versions
|
||||
val detektVersion: String by project
|
||||
val kotlinVersion: String by project
|
||||
val kotlinSerializeVersion: String by project
|
||||
|
||||
// Kompendium
|
||||
api(projects.kompendiumEnrichment)
|
||||
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.25")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializeVersion")
|
||||
|
||||
// Formatting
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
|
||||
|
@ -1,42 +0,0 @@
|
||||
plugins {
|
||||
kotlin("jvm")
|
||||
id("io.bkbn.sourdough.library.jvm")
|
||||
id("io.gitlab.arturbosch.detekt")
|
||||
id("com.adarshr.test-logger")
|
||||
id("maven-publish")
|
||||
id("java-library")
|
||||
id("signing")
|
||||
id("org.jetbrains.kotlinx.kover")
|
||||
}
|
||||
|
||||
sourdoughLibrary {
|
||||
libraryName.set("Kompendium Locations")
|
||||
libraryDescription.set("Supplemental library for Kompendium offering support for Ktor's Location API")
|
||||
compilerArgs.set(listOf("-opt-in=kotlin.RequiresOptIn"))
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Versions
|
||||
val detektVersion: String by project
|
||||
|
||||
// IMPLEMENTATION
|
||||
|
||||
implementation(projects.kompendiumCore)
|
||||
implementation("io.ktor:ktor-server-core:2.3.12")
|
||||
implementation("io.ktor:ktor-server-locations:2.3.12")
|
||||
|
||||
// TESTING
|
||||
|
||||
testImplementation(testFixtures(projects.kompendiumCore))
|
||||
|
||||
// Formatting
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
|
||||
}
|
||||
|
||||
testing {
|
||||
suites {
|
||||
named("test", JvmTestSuite::class) {
|
||||
useJUnitJupiter()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,85 +0,0 @@
|
||||
package io.bkbn.kompendium.locations
|
||||
|
||||
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
|
||||
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.bkbn.kompendium.core.metadata.HeadInfo
|
||||
import io.bkbn.kompendium.core.metadata.OptionsInfo
|
||||
import io.bkbn.kompendium.core.metadata.PatchInfo
|
||||
import io.bkbn.kompendium.core.metadata.PostInfo
|
||||
import io.bkbn.kompendium.core.metadata.PutInfo
|
||||
import io.bkbn.kompendium.core.util.Helpers.addToSpec
|
||||
import io.bkbn.kompendium.core.util.SpecConfig
|
||||
import io.bkbn.kompendium.oas.path.Path
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.ktor.server.application.createApplicationPlugin
|
||||
import io.ktor.server.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.server.locations.Location
|
||||
import kotlin.reflect.KClass
|
||||
import kotlin.reflect.full.findAnnotation
|
||||
import kotlin.reflect.full.hasAnnotation
|
||||
import kotlin.reflect.full.memberProperties
|
||||
|
||||
@Deprecated(
|
||||
message = "This functionality is deprecated and will be removed in the future. " +
|
||||
"Use 'ktor-server-resources' with 'kompendium-resources' plugin instead.",
|
||||
level = DeprecationLevel.WARNING
|
||||
)
|
||||
object NotarizedLocations {
|
||||
|
||||
data class LocationMetadata(
|
||||
override var tags: Set<String> = emptySet(),
|
||||
override var parameters: List<Parameter> = emptyList(),
|
||||
override var get: GetInfo? = null,
|
||||
override var post: PostInfo? = null,
|
||||
override var put: PutInfo? = null,
|
||||
override var delete: DeleteInfo? = null,
|
||||
override var patch: PatchInfo? = null,
|
||||
override var head: HeadInfo? = null,
|
||||
override var options: OptionsInfo? = null,
|
||||
override var security: Map<String, List<String>>? = null,
|
||||
) : SpecConfig
|
||||
|
||||
class Config {
|
||||
lateinit var locations: Map<KClass<*>, LocationMetadata>
|
||||
}
|
||||
|
||||
operator fun invoke() = createApplicationPlugin(
|
||||
name = "NotarizedLocations",
|
||||
createConfiguration = ::Config
|
||||
) {
|
||||
val spec = application.attributes[KompendiumAttributes.openApiSpec]
|
||||
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
|
||||
pluginConfig.locations.forEach { (k, v) ->
|
||||
val location = k.getLocationFromClass()
|
||||
val path = spec.paths[location] ?: Path()
|
||||
path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters
|
||||
v.get?.addToSpec(path, spec, v, serializableReader, location)
|
||||
v.delete?.addToSpec(path, spec, v, serializableReader, location)
|
||||
v.head?.addToSpec(path, spec, v, serializableReader, location)
|
||||
v.options?.addToSpec(path, spec, v, serializableReader, location)
|
||||
v.post?.addToSpec(path, spec, v, serializableReader, location)
|
||||
v.put?.addToSpec(path, spec, v, serializableReader, location)
|
||||
v.patch?.addToSpec(path, spec, v, serializableReader, location)
|
||||
|
||||
spec.paths[location] = path
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(KtorExperimentalLocationsAPI::class)
|
||||
private fun KClass<*>.getLocationFromClass(): String {
|
||||
// todo if parent
|
||||
|
||||
val location = findAnnotation<Location>()
|
||||
?: error("Cannot notarize a location without annotating with @Location")
|
||||
|
||||
val path = location.path
|
||||
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Location>() }
|
||||
|
||||
return if (parent == null) {
|
||||
path
|
||||
} else {
|
||||
parent.getLocationFromClass() + path
|
||||
}
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
package io.bkbn.kompendium.locations
|
||||
|
||||
import Listing
|
||||
import io.bkbn.kompendium.core.fixtures.TestHelpers.openApiTestAllSerializers
|
||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.kotest.core.spec.style.DescribeSpec
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.locations.Locations
|
||||
import io.ktor.server.locations.get
|
||||
import io.ktor.server.response.respondText
|
||||
|
||||
class KompendiumLocationsTest : DescribeSpec({
|
||||
describe("Location Tests") {
|
||||
it("Can notarize a simple location") {
|
||||
openApiTestAllSerializers(
|
||||
snapshotName = "T0001__simple_location.json",
|
||||
applicationSetup = {
|
||||
install(Locations)
|
||||
install(NotarizedLocations()) {
|
||||
locations = mapOf(
|
||||
Listing::class to NotarizedLocations.LocationMetadata(
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "name",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING
|
||||
),
|
||||
Parameter(
|
||||
name = "page",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT
|
||||
)
|
||||
),
|
||||
get = GetInfo.builder {
|
||||
summary("Location")
|
||||
description("example location")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<TestResponse>()
|
||||
description("does great things")
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
get<Listing> { listing ->
|
||||
call.respondText("Listing ${listing.name}, page ${listing.page}")
|
||||
}
|
||||
}
|
||||
}
|
||||
it("Can notarize nested locations") {
|
||||
openApiTestAllSerializers(
|
||||
snapshotName = "T0002__nested_locations.json",
|
||||
applicationSetup = {
|
||||
install(Locations)
|
||||
install(NotarizedLocations()) {
|
||||
locations = mapOf(
|
||||
Type.Edit::class to NotarizedLocations.LocationMetadata(
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "name",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING
|
||||
)
|
||||
),
|
||||
get = GetInfo.builder {
|
||||
summary("Edit")
|
||||
description("example location")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<TestResponse>()
|
||||
description("does great things")
|
||||
}
|
||||
}
|
||||
),
|
||||
Type.Other::class to NotarizedLocations.LocationMetadata(
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "name",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING
|
||||
),
|
||||
Parameter(
|
||||
name = "page",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT
|
||||
)
|
||||
),
|
||||
get = GetInfo.builder {
|
||||
summary("Other")
|
||||
description("example location")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<TestResponse>()
|
||||
description("does great things")
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
) {
|
||||
get<Type.Edit> { edit ->
|
||||
call.respondText("Listing ${edit.parent.name}")
|
||||
}
|
||||
get<Type.Other> { other ->
|
||||
call.respondText("Listing ${other.parent.name}, page ${other.page}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
@ -1,13 +0,0 @@
|
||||
import io.ktor.server.locations.Location
|
||||
|
||||
@Location("/list/{name}/page/{page}")
|
||||
data class Listing(val name: String, val page: Int)
|
||||
|
||||
@Location("/type/{name}")
|
||||
data class Type(val name: String) {
|
||||
@Location("/edit")
|
||||
data class Edit(val parent: Type)
|
||||
|
||||
@Location("/other/{page}")
|
||||
data class Other(val parent: Type, val page: Int)
|
||||
}
|
@ -1,92 +0,0 @@
|
||||
{
|
||||
"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": {
|
||||
"/list/{name}/page/{page}": {
|
||||
"get": {
|
||||
"tags": [],
|
||||
"summary": "Location",
|
||||
"description": "example location",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "does great things",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "int32"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"webhooks": {},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"TestResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
"security": [],
|
||||
"tags": []
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
{
|
||||
"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": {
|
||||
"/type/{name}/edit": {
|
||||
"get": {
|
||||
"tags": [],
|
||||
"summary": "Edit",
|
||||
"description": "example location",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "does great things",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
]
|
||||
},
|
||||
"/type/{name}/other/{page}": {
|
||||
"get": {
|
||||
"tags": [],
|
||||
"summary": "Other",
|
||||
"description": "example location",
|
||||
"parameters": [],
|
||||
"responses": {
|
||||
"200": {
|
||||
"description": "does great things",
|
||||
"content": {
|
||||
"application/json": {
|
||||
"schema": {
|
||||
"$ref": "#/components/schemas/TestResponse"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"deprecated": false
|
||||
},
|
||||
"parameters": [
|
||||
{
|
||||
"name": "name",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "string"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
},
|
||||
{
|
||||
"name": "page",
|
||||
"in": "path",
|
||||
"schema": {
|
||||
"type": "number",
|
||||
"format": "int32"
|
||||
},
|
||||
"required": true,
|
||||
"deprecated": false
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
"webhooks": {},
|
||||
"components": {
|
||||
"schemas": {
|
||||
"TestResponse": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"c": {
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"c"
|
||||
]
|
||||
}
|
||||
},
|
||||
"securitySchemes": {}
|
||||
},
|
||||
"security": [],
|
||||
"tags": []
|
||||
}
|
@ -19,10 +19,11 @@ sourdoughLibrary {
|
||||
dependencies {
|
||||
// Versions
|
||||
val detektVersion: String by project
|
||||
val kotlinSerializeVersion: String by project
|
||||
|
||||
api(projects.kompendiumJsonSchema)
|
||||
api(projects.kompendiumEnrichment)
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializeVersion")
|
||||
|
||||
// Formatting
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
|
||||
|
@ -13,11 +13,11 @@ sourdoughApp {
|
||||
dependencies {
|
||||
// IMPLEMENTATION
|
||||
implementation(projects.kompendiumCore)
|
||||
implementation(projects.kompendiumLocations)
|
||||
implementation(projects.kompendiumResources)
|
||||
implementation(projects.kompendiumProtobufJavaConverter)
|
||||
|
||||
// Ktor
|
||||
val kotlinSerializeVersion: String by project
|
||||
val ktorVersion: String by project
|
||||
|
||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||
@ -30,7 +30,6 @@ dependencies {
|
||||
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-jackson:$ktorVersion")
|
||||
implementation("io.ktor:ktor-serialization-gson:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-locations:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-resources:$ktorVersion")
|
||||
|
||||
// Logging
|
||||
@ -41,9 +40,9 @@ dependencies {
|
||||
implementation("org.slf4j:slf4j-simple:2.0.16")
|
||||
|
||||
// YAML
|
||||
implementation("com.charleskorn.kaml:kaml:0.59.0")
|
||||
implementation("com.charleskorn.kaml:kaml:0.61.0")
|
||||
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializeVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.6.1")
|
||||
|
||||
implementation("joda-time:joda-time:2.13.0")
|
||||
|
@ -15,7 +15,6 @@ import io.bkbn.kompendium.playground.util.Util.baseSpec
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.Authentication
|
||||
import io.ktor.server.auth.UserIdPrincipal
|
||||
|
@ -15,7 +15,6 @@ import io.bkbn.kompendium.playground.util.Util.baseSpec
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.auth.Authentication
|
||||
import io.ktor.server.auth.UserIdPrincipal
|
||||
|
@ -1,82 +0,0 @@
|
||||
package io.bkbn.kompendium.playground
|
||||
|
||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||
import io.bkbn.kompendium.core.plugin.NotarizedApplication
|
||||
import io.bkbn.kompendium.core.routes.redoc
|
||||
import io.bkbn.kompendium.core.routes.swagger
|
||||
import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.locations.NotarizedLocations
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.bkbn.kompendium.oas.serialization.KompendiumSerializersModule
|
||||
import io.bkbn.kompendium.playground.util.ExampleResponse
|
||||
import io.bkbn.kompendium.playground.util.Listing
|
||||
import io.bkbn.kompendium.playground.util.Util.baseSpec
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.serialization.kotlinx.json.json
|
||||
import io.ktor.server.application.Application
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.cio.CIO
|
||||
import io.ktor.server.engine.embeddedServer
|
||||
import io.ktor.server.locations.Locations
|
||||
import io.ktor.server.locations.get
|
||||
import io.ktor.server.plugins.contentnegotiation.ContentNegotiation
|
||||
import io.ktor.server.response.respondText
|
||||
import io.ktor.server.routing.routing
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
fun main() {
|
||||
embeddedServer(
|
||||
CIO,
|
||||
port = 8081,
|
||||
module = Application::mainModule
|
||||
).start(wait = true)
|
||||
}
|
||||
|
||||
private fun Application.mainModule() {
|
||||
install(Locations)
|
||||
install(ContentNegotiation) {
|
||||
json(Json {
|
||||
serializersModule = KompendiumSerializersModule.module
|
||||
encodeDefaults = true
|
||||
explicitNulls = false
|
||||
})
|
||||
}
|
||||
install(NotarizedApplication()) {
|
||||
spec = { baseSpec }
|
||||
}
|
||||
install(NotarizedLocations()) {
|
||||
locations = mapOf(
|
||||
Listing::class to NotarizedLocations.LocationMetadata(
|
||||
parameters = listOf(
|
||||
Parameter(
|
||||
name = "name",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.STRING
|
||||
),
|
||||
Parameter(
|
||||
name = "page",
|
||||
`in` = Parameter.Location.path,
|
||||
schema = TypeDefinition.INT
|
||||
)
|
||||
),
|
||||
get = GetInfo.builder {
|
||||
summary("Get user by id")
|
||||
description("A very neat endpoint!")
|
||||
response {
|
||||
responseCode(HttpStatusCode.OK)
|
||||
responseType<ExampleResponse>()
|
||||
description("Will return whether or not the user is real 😱")
|
||||
}
|
||||
}
|
||||
),
|
||||
)
|
||||
}
|
||||
routing {
|
||||
swagger(pageTitle = "Simple API Docs")
|
||||
redoc(pageTitle = "Simple API Docs")
|
||||
get<Listing> { listing ->
|
||||
call.respondText("Listing ${listing.name}, page ${listing.page}")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,7 +1,5 @@
|
||||
package io.bkbn.kompendium.playground.util
|
||||
|
||||
import io.ktor.server.locations.KtorExperimentalLocationsAPI
|
||||
import io.ktor.server.locations.Location
|
||||
import kotlinx.datetime.Instant
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@ -29,12 +27,3 @@ data class CustomTypeResponse(
|
||||
|
||||
@Serializable
|
||||
data class ExceptionResponse(val message: String)
|
||||
|
||||
@Location("/list/{name}/page/{page}")
|
||||
data class Listing(val name: String, val page: Int)
|
||||
|
||||
@Location("/type/{name}") data class Type(val name: String) {
|
||||
// In these classes we have to include the `name` property matching the parent.
|
||||
@Location("/edit") data class Edit(val parent: Type)
|
||||
@Location("/other/{page}") data class Other(val parent: Type, val page: Int)
|
||||
}
|
||||
|
@ -19,12 +19,13 @@ sourdoughLibrary {
|
||||
dependencies {
|
||||
// Versions
|
||||
val detektVersion: String by project
|
||||
|
||||
val kotlinVersion: String by project
|
||||
val kotlinSerializeVersion: String by project
|
||||
|
||||
implementation(projects.kompendiumJsonSchema)
|
||||
implementation("com.google.protobuf:protobuf-java:3.25.5")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:1.9.25")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.6.3")
|
||||
implementation("org.jetbrains.kotlin:kotlin-reflect:$kotlinVersion")
|
||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializeVersion")
|
||||
|
||||
// Formatting
|
||||
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
|
||||
|
@ -18,12 +18,13 @@ sourdoughLibrary {
|
||||
dependencies {
|
||||
// Versions
|
||||
val detektVersion: String by project
|
||||
val ktorVersion: String by project
|
||||
|
||||
// IMPLEMENTATION
|
||||
|
||||
implementation(projects.kompendiumCore)
|
||||
implementation("io.ktor:ktor-server-core:2.3.12")
|
||||
implementation("io.ktor:ktor-server-resources:2.3.12")
|
||||
implementation("io.ktor:ktor-server-core:$ktorVersion")
|
||||
implementation("io.ktor:ktor-server-resources:$ktorVersion")
|
||||
|
||||
// TESTING
|
||||
|
||||
|
@ -9,7 +9,6 @@ import io.bkbn.kompendium.json.schema.definition.TypeDefinition
|
||||
import io.bkbn.kompendium.oas.payload.Parameter
|
||||
import io.kotest.core.spec.style.DescribeSpec
|
||||
import io.ktor.http.HttpStatusCode
|
||||
import io.ktor.server.application.call
|
||||
import io.ktor.server.application.install
|
||||
import io.ktor.server.resources.Resources
|
||||
import io.ktor.server.resources.get
|
||||
|
@ -4,7 +4,6 @@ include("core")
|
||||
include("enrichment")
|
||||
include("oas")
|
||||
include("playground")
|
||||
include("locations")
|
||||
include("json-schema")
|
||||
include("protobuf-java-converter")
|
||||
include("resources")
|
||||
|
Reference in New Issue
Block a user