Compare commits
10 Commits
Author | SHA1 | Date | |
---|---|---|---|
2492661f1f | |||
a7b52ec114 | |||
8ebab04a83 | |||
921f6f9691 | |||
558b9fea62 | |||
752cb238d3 | |||
92d4760dbc | |||
4946a27327 | |||
e4217843b7 | |||
b8f2090a8a |
13
CHANGELOG.md
13
CHANGELOG.md
@ -12,6 +12,19 @@
|
|||||||
|
|
||||||
## Released
|
## Released
|
||||||
|
|
||||||
|
## [3.6.0] - November 5th, 2022
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
|
||||||
|
- Schemas for types in nullable properties are no longer nullable themselves
|
||||||
|
- Enums are now generated as references, which makes it possible to generate types for them
|
||||||
|
|
||||||
|
## [3.5.0] - October 29th, 2022
|
||||||
|
|
||||||
|
### Added
|
||||||
|
|
||||||
|
- New `kompendium-resources` plugin to support Ktor Resources API
|
||||||
|
|
||||||
## [3.4.0] - October 26th, 2022
|
## [3.4.0] - October 26th, 2022
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
@ -58,7 +58,7 @@ dependencies {
|
|||||||
testFixturesApi("io.ktor:ktor-client:$ktorVersion")
|
testFixturesApi("io.ktor:ktor-client:$ktorVersion")
|
||||||
testFixturesApi("io.ktor:ktor-client-cio:$ktorVersion")
|
testFixturesApi("io.ktor:ktor-client-cio:$ktorVersion")
|
||||||
|
|
||||||
testFixturesApi("dev.forst:ktor-api-key:2.1.2")
|
testFixturesApi("dev.forst:ktor-api-key:2.1.3")
|
||||||
|
|
||||||
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
testFixturesApi("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
||||||
}
|
}
|
||||||
|
@ -12,6 +12,8 @@ import io.bkbn.kompendium.core.metadata.PutInfo
|
|||||||
import io.bkbn.kompendium.core.metadata.ResponseInfo
|
import io.bkbn.kompendium.core.metadata.ResponseInfo
|
||||||
import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||||
|
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
||||||
|
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
||||||
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
|
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
|
||||||
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
|
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
|
||||||
@ -108,7 +110,10 @@ object Helpers {
|
|||||||
Unit::class -> null
|
Unit::class -> null
|
||||||
else -> mapOf(
|
else -> mapOf(
|
||||||
"application/json" to MediaType(
|
"application/json" to MediaType(
|
||||||
schema = ReferenceDefinition(this.getReferenceSlug()),
|
schema = if (this.isMarkedNullable) OneOfDefinition(
|
||||||
|
NullableDefinition(),
|
||||||
|
ReferenceDefinition(this.getReferenceSlug())
|
||||||
|
) else ReferenceDefinition(this.getReferenceSlug()),
|
||||||
examples = examples
|
examples = examples
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
@ -49,6 +49,7 @@ import io.bkbn.kompendium.core.util.TestModules.simpleGenericResponse
|
|||||||
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing
|
import io.bkbn.kompendium.core.util.TestModules.simplePathParsing
|
||||||
import io.bkbn.kompendium.core.util.TestModules.simpleRecursive
|
import io.bkbn.kompendium.core.util.TestModules.simpleRecursive
|
||||||
import io.bkbn.kompendium.core.util.TestModules.singleException
|
import io.bkbn.kompendium.core.util.TestModules.singleException
|
||||||
|
import io.bkbn.kompendium.core.util.TestModules.topLevelNullable
|
||||||
import io.bkbn.kompendium.core.util.TestModules.trailingSlash
|
import io.bkbn.kompendium.core.util.TestModules.trailingSlash
|
||||||
import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse
|
import io.bkbn.kompendium.core.util.TestModules.unbackedFieldsResponse
|
||||||
import io.bkbn.kompendium.core.util.TestModules.withOperationId
|
import io.bkbn.kompendium.core.util.TestModules.withOperationId
|
||||||
@ -243,6 +244,9 @@ class KompendiumTest : DescribeSpec({
|
|||||||
it("Can handle nested type names") {
|
it("Can handle nested type names") {
|
||||||
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
|
openApiTestAllSerializers("T0044__nested_type_name.json") { nestedTypeName() }
|
||||||
}
|
}
|
||||||
|
it("Can handle top level nullable types") {
|
||||||
|
openApiTestAllSerializers("T0051__top_level_nullable.json") { topLevelNullable() }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
describe("Error Handling") {
|
describe("Error Handling") {
|
||||||
it("Throws a clear exception when an unidentified type is encountered") {
|
it("Throws a clear exception when an unidentified type is encountered") {
|
||||||
|
@ -24,7 +24,7 @@ import io.bkbn.kompendium.core.fixtures.TestRequest
|
|||||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||||
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
||||||
import io.bkbn.kompendium.core.fixtures.TransientObject
|
import io.bkbn.kompendium.core.fixtures.TransientObject
|
||||||
import io.bkbn.kompendium.core.fixtures.UnbakcedObject
|
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
||||||
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
import io.bkbn.kompendium.core.metadata.DeleteInfo
|
||||||
import io.bkbn.kompendium.core.metadata.GetInfo
|
import io.bkbn.kompendium.core.metadata.GetInfo
|
||||||
import io.bkbn.kompendium.core.metadata.HeadInfo
|
import io.bkbn.kompendium.core.metadata.HeadInfo
|
||||||
@ -568,7 +568,7 @@ object TestModules {
|
|||||||
|
|
||||||
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
|
fun Routing.ignoredFieldsResponse() = basicGetGenerator<TransientObject>()
|
||||||
|
|
||||||
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbakcedObject>()
|
fun Routing.unbackedFieldsResponse() = basicGetGenerator<UnbackedObject>()
|
||||||
|
|
||||||
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
|
fun Routing.customFieldNameResponse() = basicGetGenerator<SerialNameObject>()
|
||||||
|
|
||||||
@ -613,6 +613,8 @@ object TestModules {
|
|||||||
|
|
||||||
fun Routing.nestedTypeName() = basicGetGenerator<Nested.Response>()
|
fun Routing.nestedTypeName() = basicGetGenerator<Nested.Response>()
|
||||||
|
|
||||||
|
fun Routing.topLevelNullable() = basicGetGenerator<TestResponse?>()
|
||||||
|
|
||||||
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
|
fun Routing.simpleRecursive() = basicGetGenerator<ColumnSchema>()
|
||||||
|
|
||||||
fun Routing.defaultAuthConfig() {
|
fun Routing.defaultAuthConfig() {
|
||||||
|
@ -124,15 +124,19 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"enumeration": {
|
"enumeration": {
|
||||||
"enum": [
|
"$ref": "#/components/schemas/SimpleEnum"
|
||||||
"ONE",
|
|
||||||
"TWO"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"enumeration"
|
"enumeration"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"SimpleEnum": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ONE",
|
||||||
|
"TWO"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {}
|
"securitySchemes": {}
|
||||||
|
@ -91,37 +91,30 @@
|
|||||||
"required": []
|
"required": []
|
||||||
},
|
},
|
||||||
"ProfileMetadataUpdateRequest": {
|
"ProfileMetadataUpdateRequest": {
|
||||||
"oneOf": [
|
"type": "object",
|
||||||
{
|
"properties": {
|
||||||
"type": "null"
|
"isPrivate": {
|
||||||
},
|
"oneOf": [
|
||||||
{
|
{
|
||||||
"type": "object",
|
"type": "null"
|
||||||
"properties": {
|
|
||||||
"isPrivate": {
|
|
||||||
"oneOf": [
|
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "boolean"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"otherThing": {
|
{
|
||||||
"oneOf": [
|
"type": "boolean"
|
||||||
{
|
|
||||||
"type": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"type": "string"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
]
|
||||||
"required": []
|
},
|
||||||
|
"otherThing": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
},
|
||||||
|
"required": []
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {}
|
"securitySchemes": {}
|
||||||
|
@ -62,16 +62,21 @@
|
|||||||
"type": "null"
|
"type": "null"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"enum": [
|
"$ref": "#/components/schemas/TestEnum"
|
||||||
"YES",
|
|
||||||
"NO"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
}
|
},
|
||||||
|
"TestEnum":
|
||||||
|
{
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"YES",
|
||||||
|
"NO"
|
||||||
|
]
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {}
|
"securitySchemes": {}
|
||||||
},
|
},
|
||||||
|
@ -57,10 +57,7 @@
|
|||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"enumeration": {
|
"enumeration": {
|
||||||
"enum": [
|
"$ref": "#/components/schemas/SimpleEnum"
|
||||||
"ONE",
|
|
||||||
"TWO"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
@ -120,6 +117,13 @@
|
|||||||
"required": [
|
"required": [
|
||||||
"content"
|
"content"
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
"SimpleEnum": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"ONE",
|
||||||
|
"TWO"
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"securitySchemes": {}
|
"securitySchemes": {}
|
||||||
|
@ -53,6 +53,14 @@
|
|||||||
"webhooks": {},
|
"webhooks": {},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
|
"ColumnMode": {
|
||||||
|
"type": "string",
|
||||||
|
"enum": [
|
||||||
|
"NULLABLE",
|
||||||
|
"REQUIRED",
|
||||||
|
"REPEATED"
|
||||||
|
]
|
||||||
|
},
|
||||||
"ColumnSchema": {
|
"ColumnSchema": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -60,11 +68,7 @@
|
|||||||
"type": "string"
|
"type": "string"
|
||||||
},
|
},
|
||||||
"mode": {
|
"mode": {
|
||||||
"enum": [
|
"$ref": "#/components/schemas/ColumnMode"
|
||||||
"NULLABLE",
|
|
||||||
"REQUIRED",
|
|
||||||
"REPEATED"
|
|
||||||
]
|
|
||||||
},
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
@ -39,7 +39,7 @@
|
|||||||
"content": {
|
"content": {
|
||||||
"application/json": {
|
"application/json": {
|
||||||
"schema": {
|
"schema": {
|
||||||
"$ref": "#/components/schemas/UnbakcedObject"
|
"$ref": "#/components/schemas/UnbackedObject"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -53,7 +53,7 @@
|
|||||||
"webhooks": {},
|
"webhooks": {},
|
||||||
"components": {
|
"components": {
|
||||||
"schemas": {
|
"schemas": {
|
||||||
"UnbakcedObject": {
|
"UnbackedObject": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"properties": {
|
"properties": {
|
||||||
"backed": {
|
"backed": {
|
||||||
|
79
core/src/test/resources/T0051__top_level_nullable.json
Normal file
79
core/src/test/resources/T0051__top_level_nullable.json
Normal file
@ -0,0 +1,79 @@
|
|||||||
|
{
|
||||||
|
"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": {
|
||||||
|
"/": {
|
||||||
|
"get": {
|
||||||
|
"tags": [],
|
||||||
|
"summary": "Great Summary!",
|
||||||
|
"description": "testing more",
|
||||||
|
"parameters": [],
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"description": "A Successful Endeavor",
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"oneOf": [
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/components/schemas/TestResponse"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"deprecated": false
|
||||||
|
},
|
||||||
|
"parameters": []
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"webhooks": {},
|
||||||
|
"components": {
|
||||||
|
"schemas": {
|
||||||
|
"TestResponse": {
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"c": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"c"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"securitySchemes": {}
|
||||||
|
},
|
||||||
|
"security": [],
|
||||||
|
"tags": []
|
||||||
|
}
|
@ -163,7 +163,7 @@ data class TransientObject(
|
|||||||
)
|
)
|
||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class UnbakcedObject(
|
data class UnbackedObject(
|
||||||
val backed: String
|
val backed: String
|
||||||
) {
|
) {
|
||||||
val unbacked: String get() = "unbacked"
|
val unbacked: String get() = "unbacked"
|
||||||
@ -176,3 +176,14 @@ data class SerialNameObject(
|
|||||||
@SerialName("snake_case_name")
|
@SerialName("snake_case_name")
|
||||||
val camelCaseName: String
|
val camelCaseName: String
|
||||||
)
|
)
|
||||||
|
|
||||||
|
enum class Color {
|
||||||
|
RED,
|
||||||
|
GREEN,
|
||||||
|
BLUE
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
data class ObjectWithEnum(
|
||||||
|
val color: Color
|
||||||
|
)
|
||||||
|
@ -5,4 +5,5 @@
|
|||||||
* [Notarized Application](plugins/notarized_application.md)
|
* [Notarized Application](plugins/notarized_application.md)
|
||||||
* [Notarized Route](plugins/notarized_route.md)
|
* [Notarized Route](plugins/notarized_route.md)
|
||||||
* [Notarized Locations](plugins/notarized_locations.md)
|
* [Notarized Locations](plugins/notarized_locations.md)
|
||||||
|
* [Notarized Resources](plugins/notarized_resources.md)
|
||||||
* [The Playground](playground.md)
|
* [The Playground](playground.md)
|
||||||
|
@ -13,6 +13,7 @@ At the moment, the following playground applications are
|
|||||||
| Hidden Docs | Place your generated documentation behind authorization |
|
| Hidden Docs | Place your generated documentation behind authorization |
|
||||||
| Jackson | Serialization using Jackson instead of the default KotlinX |
|
| Jackson | Serialization using Jackson instead of the default KotlinX |
|
||||||
| Locations | Using the Ktor Locations API to define routes |
|
| 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
|
You can find all of the playground
|
||||||
examples [here](https://github.com/bkbnio/kompendium/tree/main/playground/src/main/kotlin/io/bkbn/kompendium/playground)
|
examples [here](https://github.com/bkbnio/kompendium/tree/main/playground/src/main/kotlin/io/bkbn/kompendium/playground)
|
||||||
|
60
docs/plugins/notarized_resources.md
Normal file
60
docs/plugins/notarized_resources.md
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
The Ktor Resources API allows users to define their routes in a type-safe manner.
|
||||||
|
|
||||||
|
You can read more about it [here](https://ktor.io/docs/type-safe-routing.html).
|
||||||
|
|
||||||
|
Kompendium supports Ktor-Resources through an ancillary module `kompendium-resources`
|
||||||
|
|
||||||
|
## Adding the Artifact
|
||||||
|
|
||||||
|
Prior to documenting your resources, you will need to add the artifact to your gradle build file.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
dependencies {
|
||||||
|
implementation("io.bkbn:kompendium-resources:$version")
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Installing Plugin
|
||||||
|
|
||||||
|
Once you have installed the dependency, you can install the plugin. The `NotarizedResources` plugin is an _application_ level plugin, and **must** be install after both the `NotarizedApplication` plugin and the Ktor `Resources` plugin.
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
private fun Application.mainModule() {
|
||||||
|
install(Resources)
|
||||||
|
install(NotarizedApplication()) {
|
||||||
|
spec = baseSpec
|
||||||
|
}
|
||||||
|
install(NotarizedResources()) {
|
||||||
|
resources = mapOf(
|
||||||
|
Listing::class to NotarizedResources.ResourceMetadata(
|
||||||
|
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 `resources` property is a map of `KClass<*>` to `ResourceMetadata` instance describing that resource. 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 `@Resource` annotation, you will get a runtime
|
||||||
|
> exception!
|
@ -1,5 +1,5 @@
|
|||||||
# Kompendium
|
# Kompendium
|
||||||
project.version=3.4.0
|
project.version=3.6.0
|
||||||
# Kotlin
|
# Kotlin
|
||||||
kotlin.code.style=official
|
kotlin.code.style=official
|
||||||
# Gradle
|
# Gradle
|
||||||
@ -8,6 +8,6 @@ org.gradle.vfs.verbose=true
|
|||||||
org.gradle.jvmargs=-Xmx2000m
|
org.gradle.jvmargs=-Xmx2000m
|
||||||
|
|
||||||
# Dependencies
|
# Dependencies
|
||||||
ktorVersion=2.1.2
|
ktorVersion=2.1.3
|
||||||
kotestVersion=5.5.2
|
kotestVersion=5.5.4
|
||||||
detektVersion=1.21.0
|
detektVersion=1.21.0
|
||||||
|
@ -48,7 +48,7 @@ object SchemaGenerator {
|
|||||||
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
|
Boolean::class -> checkForNull(type, TypeDefinition.BOOLEAN)
|
||||||
UUID::class -> checkForNull(type, TypeDefinition.UUID)
|
UUID::class -> checkForNull(type, TypeDefinition.UUID)
|
||||||
else -> when {
|
else -> when {
|
||||||
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz)
|
clazz.isSubclassOf(Enum::class) -> EnumHandler.handle(type, clazz, cache)
|
||||||
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator)
|
clazz.isSubclassOf(Collection::class) -> CollectionHandler.handle(type, cache, schemaConfigurator)
|
||||||
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator)
|
clazz.isSubclassOf(Map::class) -> MapHandler.handle(type, cache, schemaConfigurator)
|
||||||
else -> {
|
else -> {
|
||||||
|
@ -4,5 +4,6 @@ import kotlinx.serialization.Serializable
|
|||||||
|
|
||||||
@Serializable
|
@Serializable
|
||||||
data class EnumDefinition(
|
data class EnumDefinition(
|
||||||
|
val type: String,
|
||||||
val enum: Set<String>
|
val enum: Set<String>
|
||||||
) : JsonSchema
|
) : JsonSchema
|
||||||
|
@ -2,18 +2,17 @@ package io.bkbn.kompendium.json.schema.handler
|
|||||||
|
|
||||||
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
|
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
import io.bkbn.kompendium.json.schema.definition.ReferenceDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
import io.bkbn.kompendium.json.schema.util.Helpers.getReferenceSlug
|
||||||
|
import io.bkbn.kompendium.json.schema.util.Helpers.getSimpleSlug
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.KType
|
import kotlin.reflect.KType
|
||||||
|
|
||||||
object EnumHandler {
|
object EnumHandler {
|
||||||
fun handle(type: KType, clazz: KClass<*>): JsonSchema {
|
fun handle(type: KType, clazz: KClass<*>, cache: MutableMap<String, JsonSchema>): JsonSchema {
|
||||||
|
cache[type.getSimpleSlug()] = ReferenceDefinition(type.getReferenceSlug())
|
||||||
|
|
||||||
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
val options = clazz.java.enumConstants.map { it.toString() }.toSet()
|
||||||
val definition = EnumDefinition(enum = options)
|
return EnumDefinition(type = "string", enum = options)
|
||||||
return when (type.isMarkedNullable) {
|
|
||||||
true -> OneOfDefinition(NullableDefinition(), definition)
|
|
||||||
false -> definition
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema.handler
|
|||||||
|
|
||||||
import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
import io.bkbn.kompendium.json.schema.SchemaConfigurator
|
||||||
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
import io.bkbn.kompendium.json.schema.SchemaGenerator
|
||||||
|
import io.bkbn.kompendium.json.schema.definition.EnumDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
import io.bkbn.kompendium.json.schema.definition.NullableDefinition
|
||||||
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
import io.bkbn.kompendium.json.schema.definition.OneOfDefinition
|
||||||
@ -71,16 +72,11 @@ object SimpleObjectHandler {
|
|||||||
.map { schemaConfigurator.serializableName(it) }
|
.map { schemaConfigurator.serializableName(it) }
|
||||||
.toSet()
|
.toSet()
|
||||||
|
|
||||||
val definition = TypeDefinition(
|
return TypeDefinition(
|
||||||
type = "object",
|
type = "object",
|
||||||
properties = props,
|
properties = props,
|
||||||
required = required
|
required = required
|
||||||
)
|
)
|
||||||
|
|
||||||
return when (type.isMarkedNullable) {
|
|
||||||
true -> OneOfDefinition(NullableDefinition(), definition)
|
|
||||||
false -> definition
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun KProperty<*>.needsToInjectGenerics(
|
private fun KProperty<*>.needsToInjectGenerics(
|
||||||
@ -103,7 +99,7 @@ object SimpleObjectHandler {
|
|||||||
}
|
}
|
||||||
val constructedType = propClass.createType(types)
|
val constructedType = propClass.createType(types)
|
||||||
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let {
|
return SchemaGenerator.fromTypeToSchema(constructedType, cache, schemaConfigurator).let {
|
||||||
if (it.isOrContainsObjectDef()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[constructedType.getSimpleSlug()] = it
|
cache[constructedType.getSimpleSlug()] = it
|
||||||
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
||||||
} else {
|
} else {
|
||||||
@ -121,7 +117,7 @@ object SimpleObjectHandler {
|
|||||||
val type = typeMap[prop.returnType.classifier]?.type
|
val type = typeMap[prop.returnType.classifier]?.type
|
||||||
?: error("This indicates a bug in Kompendium, please open a GitHub issue")
|
?: error("This indicates a bug in Kompendium, please open a GitHub issue")
|
||||||
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let {
|
return SchemaGenerator.fromTypeToSchema(type, cache, schemaConfigurator).let {
|
||||||
if (it.isOrContainsObjectDef()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[type.getSimpleSlug()] = it
|
cache[type.getSimpleSlug()] = it
|
||||||
ReferenceDefinition(type.getReferenceSlug())
|
ReferenceDefinition(type.getReferenceSlug())
|
||||||
} else {
|
} else {
|
||||||
@ -136,7 +132,7 @@ object SimpleObjectHandler {
|
|||||||
schemaConfigurator: SchemaConfigurator
|
schemaConfigurator: SchemaConfigurator
|
||||||
): JsonSchema =
|
): JsonSchema =
|
||||||
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let {
|
SchemaGenerator.fromTypeToSchema(prop.returnType, cache, schemaConfigurator).let {
|
||||||
if (it.isOrContainsObjectDef()) {
|
if (it.isOrContainsObjectOrEnumDef()) {
|
||||||
cache[prop.returnType.getSimpleSlug()] = it
|
cache[prop.returnType.getSimpleSlug()] = it
|
||||||
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
ReferenceDefinition(prop.returnType.getReferenceSlug())
|
||||||
} else {
|
} else {
|
||||||
@ -144,10 +140,12 @@ object SimpleObjectHandler {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JsonSchema.isOrContainsObjectDef(): Boolean {
|
private fun JsonSchema.isOrContainsObjectOrEnumDef(): Boolean {
|
||||||
val isTypeDef = this is TypeDefinition && type == "object"
|
val isTypeDef = this is TypeDefinition && type == "object"
|
||||||
val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" }
|
val isTypeDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is TypeDefinition && js.type == "object" }
|
||||||
return isTypeDef || isTypeDefOneOf
|
val isEnumDef = this is EnumDefinition
|
||||||
|
val isEnumDefOneOf = this is OneOfDefinition && this.oneOf.any { js -> js is EnumDefinition }
|
||||||
|
return isTypeDef || isTypeDefOneOf || isEnumDef || isEnumDefOneOf
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition }
|
private fun JsonSchema.isNullable(): Boolean = this is OneOfDefinition && this.oneOf.any { it is NullableDefinition }
|
||||||
|
@ -2,6 +2,7 @@ package io.bkbn.kompendium.json.schema
|
|||||||
|
|
||||||
import io.bkbn.kompendium.core.fixtures.ComplexRequest
|
import io.bkbn.kompendium.core.fixtures.ComplexRequest
|
||||||
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
|
import io.bkbn.kompendium.core.fixtures.FlibbityGibbit
|
||||||
|
import io.bkbn.kompendium.core.fixtures.ObjectWithEnum
|
||||||
import io.bkbn.kompendium.core.fixtures.SerialNameObject
|
import io.bkbn.kompendium.core.fixtures.SerialNameObject
|
||||||
import io.bkbn.kompendium.core.fixtures.SimpleEnum
|
import io.bkbn.kompendium.core.fixtures.SimpleEnum
|
||||||
import io.bkbn.kompendium.core.fixtures.SlammaJamma
|
import io.bkbn.kompendium.core.fixtures.SlammaJamma
|
||||||
@ -9,7 +10,7 @@ import io.bkbn.kompendium.core.fixtures.TestHelpers.getFileSnapshot
|
|||||||
import io.bkbn.kompendium.core.fixtures.TestResponse
|
import io.bkbn.kompendium.core.fixtures.TestResponse
|
||||||
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
import io.bkbn.kompendium.core.fixtures.TestSimpleRequest
|
||||||
import io.bkbn.kompendium.core.fixtures.TransientObject
|
import io.bkbn.kompendium.core.fixtures.TransientObject
|
||||||
import io.bkbn.kompendium.core.fixtures.UnbakcedObject
|
import io.bkbn.kompendium.core.fixtures.UnbackedObject
|
||||||
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
import io.bkbn.kompendium.json.schema.definition.JsonSchema
|
||||||
import io.kotest.assertions.json.shouldEqualJson
|
import io.kotest.assertions.json.shouldEqualJson
|
||||||
import io.kotest.assertions.throwables.shouldThrow
|
import io.kotest.assertions.throwables.shouldThrow
|
||||||
@ -40,6 +41,7 @@ class SchemaGeneratorTest : DescribeSpec({
|
|||||||
jsonSchemaTest<ComplexRequest>("T0005__complex_object.json")
|
jsonSchemaTest<ComplexRequest>("T0005__complex_object.json")
|
||||||
}
|
}
|
||||||
it("Can generate the schema for a nullable object") {
|
it("Can generate the schema for a nullable object") {
|
||||||
|
// Same schema as a non-nullable type, since the nullability will be handled on the property
|
||||||
jsonSchemaTest<TestSimpleRequest?>("T0006__nullable_object.json")
|
jsonSchemaTest<TestSimpleRequest?>("T0006__nullable_object.json")
|
||||||
}
|
}
|
||||||
it("Can generate the schema for a polymorphic object") {
|
it("Can generate the schema for a polymorphic object") {
|
||||||
@ -52,7 +54,7 @@ class SchemaGeneratorTest : DescribeSpec({
|
|||||||
jsonSchemaTest<TransientObject>("T0018__transient_object.json")
|
jsonSchemaTest<TransientObject>("T0018__transient_object.json")
|
||||||
}
|
}
|
||||||
it("Can generate the schema for object with unbacked property") {
|
it("Can generate the schema for object with unbacked property") {
|
||||||
jsonSchemaTest<UnbakcedObject>("T0019__unbacked_object.json")
|
jsonSchemaTest<UnbackedObject>("T0019__unbacked_object.json")
|
||||||
}
|
}
|
||||||
it("Can generate the schema for object with SerialName annotation") {
|
it("Can generate the schema for object with SerialName annotation") {
|
||||||
jsonSchemaTest<SerialNameObject>("T0020__serial_name_object.json")
|
jsonSchemaTest<SerialNameObject>("T0020__serial_name_object.json")
|
||||||
@ -63,8 +65,12 @@ class SchemaGeneratorTest : DescribeSpec({
|
|||||||
jsonSchemaTest<SimpleEnum>("T0007__simple_enum.json")
|
jsonSchemaTest<SimpleEnum>("T0007__simple_enum.json")
|
||||||
}
|
}
|
||||||
it("Can generate the schema for a nullable enum") {
|
it("Can generate the schema for a nullable enum") {
|
||||||
|
// Same schema as a non-nullable enum, since the nullability will be handled on the property
|
||||||
jsonSchemaTest<SimpleEnum?>("T0008__nullable_enum.json")
|
jsonSchemaTest<SimpleEnum?>("T0008__nullable_enum.json")
|
||||||
}
|
}
|
||||||
|
it("Can generate the schema for an object with an enum property") {
|
||||||
|
jsonSchemaTest<ObjectWithEnum>("T0021__object_with_enum.json")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
describe("Arrays") {
|
describe("Arrays") {
|
||||||
it("Can generate the schema for an array of scalars") {
|
it("Can generate the schema for an array of scalars") {
|
||||||
|
@ -1,23 +1,16 @@
|
|||||||
{
|
{
|
||||||
"oneOf": [
|
"type": "object",
|
||||||
{
|
"properties": {
|
||||||
"type": "null"
|
"a": {
|
||||||
|
"type": "string"
|
||||||
},
|
},
|
||||||
{
|
"b": {
|
||||||
"type": "object",
|
"type": "number",
|
||||||
"properties": {
|
"format": "int32"
|
||||||
"a": {
|
|
||||||
"type": "string"
|
|
||||||
},
|
|
||||||
"b": {
|
|
||||||
"type": "number",
|
|
||||||
"format": "int32"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"required": [
|
|
||||||
"a",
|
|
||||||
"b"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"a",
|
||||||
|
"b"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
{
|
{
|
||||||
"enum": [ "ONE", "TWO" ]
|
"enum": [ "ONE", "TWO" ],
|
||||||
|
"type": "string"
|
||||||
}
|
}
|
||||||
|
@ -1,13 +1,4 @@
|
|||||||
{
|
{
|
||||||
"oneOf": [
|
"enum": [ "ONE", "TWO" ],
|
||||||
{
|
"type": "string"
|
||||||
"type": "null"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"enum": [
|
|
||||||
"ONE",
|
|
||||||
"TWO"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
11
json-schema/src/test/resources/T0021__object_with_enum.json
Normal file
11
json-schema/src/test/resources/T0021__object_with_enum.json
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
{
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"color": {
|
||||||
|
"$ref": "#/components/schemas/Color"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"color"
|
||||||
|
]
|
||||||
|
}
|
@ -22,8 +22,8 @@ dependencies {
|
|||||||
// IMPLEMENTATION
|
// IMPLEMENTATION
|
||||||
|
|
||||||
implementation(projects.kompendiumCore)
|
implementation(projects.kompendiumCore)
|
||||||
implementation("io.ktor:ktor-server-core:2.1.2")
|
implementation("io.ktor:ktor-server-core:2.1.3")
|
||||||
implementation("io.ktor:ktor-server-locations:2.1.2")
|
implementation("io.ktor:ktor-server-locations:2.1.3")
|
||||||
|
|
||||||
// TESTING
|
// TESTING
|
||||||
|
|
||||||
|
@ -20,6 +20,11 @@ import kotlin.reflect.full.findAnnotation
|
|||||||
import kotlin.reflect.full.hasAnnotation
|
import kotlin.reflect.full.hasAnnotation
|
||||||
import kotlin.reflect.full.memberProperties
|
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 {
|
object NotarizedLocations {
|
||||||
|
|
||||||
data class LocationMetadata(
|
data class LocationMetadata(
|
||||||
@ -43,7 +48,6 @@ object NotarizedLocations {
|
|||||||
name = "NotarizedLocations",
|
name = "NotarizedLocations",
|
||||||
createConfiguration = ::Config
|
createConfiguration = ::Config
|
||||||
) {
|
) {
|
||||||
println("hi")
|
|
||||||
val spec = application.attributes[KompendiumAttributes.openApiSpec]
|
val spec = application.attributes[KompendiumAttributes.openApiSpec]
|
||||||
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
|
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
|
||||||
pluginConfig.locations.forEach { (k, v) ->
|
pluginConfig.locations.forEach { (k, v) ->
|
||||||
|
@ -14,6 +14,7 @@ dependencies {
|
|||||||
// IMPLEMENTATION
|
// IMPLEMENTATION
|
||||||
implementation(projects.kompendiumCore)
|
implementation(projects.kompendiumCore)
|
||||||
implementation(projects.kompendiumLocations)
|
implementation(projects.kompendiumLocations)
|
||||||
|
implementation(projects.kompendiumResources)
|
||||||
|
|
||||||
// Ktor
|
// Ktor
|
||||||
val ktorVersion: String by project
|
val ktorVersion: String by project
|
||||||
@ -29,6 +30,7 @@ dependencies {
|
|||||||
implementation("io.ktor:ktor-serialization-jackson:$ktorVersion")
|
implementation("io.ktor:ktor-serialization-jackson:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-serialization-gson:$ktorVersion")
|
implementation("io.ktor:ktor-serialization-gson:$ktorVersion")
|
||||||
implementation("io.ktor:ktor-server-locations:$ktorVersion")
|
implementation("io.ktor:ktor-server-locations:$ktorVersion")
|
||||||
|
implementation("io.ktor:ktor-server-resources:$ktorVersion")
|
||||||
|
|
||||||
// Logging
|
// Logging
|
||||||
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.2.0")
|
implementation("org.apache.logging.log4j:log4j-api-kotlin:1.2.0")
|
||||||
@ -41,5 +43,5 @@ dependencies {
|
|||||||
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:1.4.1")
|
||||||
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
implementation("org.jetbrains.kotlinx:kotlinx-datetime:0.4.0")
|
||||||
|
|
||||||
implementation("joda-time:joda-time:2.12.0")
|
implementation("joda-time:joda-time:2.12.1")
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,85 @@
|
|||||||
|
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.json.schema.definition.TypeDefinition
|
||||||
|
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.Util.baseSpec
|
||||||
|
import io.bkbn.kompendium.resources.NotarizedResources
|
||||||
|
import io.ktor.http.HttpStatusCode
|
||||||
|
import io.ktor.resources.Resource
|
||||||
|
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.plugins.contentnegotiation.ContentNegotiation
|
||||||
|
import io.ktor.server.resources.Resources
|
||||||
|
import io.ktor.server.resources.get
|
||||||
|
import io.ktor.server.response.respondText
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
import kotlinx.serialization.json.Json
|
||||||
|
|
||||||
|
fun main() {
|
||||||
|
embeddedServer(
|
||||||
|
CIO,
|
||||||
|
port = 8081,
|
||||||
|
module = Application::mainModule
|
||||||
|
).start(wait = true)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Application.mainModule() {
|
||||||
|
install(Resources)
|
||||||
|
install(ContentNegotiation) {
|
||||||
|
json(Json {
|
||||||
|
serializersModule = KompendiumSerializersModule.module
|
||||||
|
encodeDefaults = true
|
||||||
|
explicitNulls = false
|
||||||
|
})
|
||||||
|
}
|
||||||
|
install(NotarizedApplication()) {
|
||||||
|
spec = baseSpec
|
||||||
|
}
|
||||||
|
install(NotarizedResources()) {
|
||||||
|
resources = mapOf(
|
||||||
|
ListingResource::class to NotarizedResources.ResourceMetadata(
|
||||||
|
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 {
|
||||||
|
redoc(pageTitle = "Simple API Docs")
|
||||||
|
get<ListingResource> { listing ->
|
||||||
|
call.respondText("Listing ${listing.name}, page ${listing.page}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("/list/{name}/page/{page}")
|
||||||
|
data class ListingResource(val name: String, val page: Int)
|
42
resources/build.gradle.kts
Normal file
42
resources/build.gradle.kts
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
plugins {
|
||||||
|
kotlin("jvm")
|
||||||
|
kotlin("plugin.serialization")
|
||||||
|
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 Resources")
|
||||||
|
libraryDescription.set("Supplemental library for Kompendium offering support for Ktor's Resources API")
|
||||||
|
}
|
||||||
|
|
||||||
|
dependencies {
|
||||||
|
// Versions
|
||||||
|
val detektVersion: String by project
|
||||||
|
|
||||||
|
// IMPLEMENTATION
|
||||||
|
|
||||||
|
implementation(projects.kompendiumCore)
|
||||||
|
implementation("io.ktor:ktor-server-core:2.1.3")
|
||||||
|
implementation("io.ktor:ktor-server-resources:2.1.3")
|
||||||
|
|
||||||
|
// TESTING
|
||||||
|
|
||||||
|
testImplementation(testFixtures(projects.kompendiumCore))
|
||||||
|
|
||||||
|
// Formatting
|
||||||
|
detektPlugins("io.gitlab.arturbosch.detekt:detekt-formatting:$detektVersion")
|
||||||
|
}
|
||||||
|
|
||||||
|
testing {
|
||||||
|
suites {
|
||||||
|
named("test", JvmTestSuite::class) {
|
||||||
|
useJUnitJupiter()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,78 @@
|
|||||||
|
package io.bkbn.kompendium.resources
|
||||||
|
|
||||||
|
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.resources.Resource
|
||||||
|
import io.ktor.server.application.createApplicationPlugin
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.hasAnnotation
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
object NotarizedResources {
|
||||||
|
|
||||||
|
data class ResourceMetadata(
|
||||||
|
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 resources: Map<KClass<*>, ResourceMetadata>
|
||||||
|
}
|
||||||
|
|
||||||
|
operator fun invoke() = createApplicationPlugin(
|
||||||
|
name = "NotarizedResources",
|
||||||
|
createConfiguration = NotarizedResources::Config
|
||||||
|
) {
|
||||||
|
val spec = application.attributes[KompendiumAttributes.openApiSpec]
|
||||||
|
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
|
||||||
|
pluginConfig.resources.forEach { (k, v) ->
|
||||||
|
val path = Path()
|
||||||
|
path.parameters = v.parameters
|
||||||
|
v.get?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
v.delete?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
v.head?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
v.options?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
v.post?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
v.put?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
v.patch?.addToSpec(path, spec, v, serializableReader)
|
||||||
|
|
||||||
|
val resource = k.getResourcesFromClass()
|
||||||
|
spec.paths[resource] = path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun KClass<*>.getResourcesFromClass(): String {
|
||||||
|
// todo if parent
|
||||||
|
|
||||||
|
val resource = findAnnotation<Resource>()
|
||||||
|
?: error("Cannot notarize a resource without annotating with @Resource")
|
||||||
|
|
||||||
|
val path = resource.path
|
||||||
|
val parent = memberProperties.map { it.returnType.classifier as KClass<*> }.find { it.hasAnnotation<Resource>() }
|
||||||
|
|
||||||
|
return if (parent == null) {
|
||||||
|
path
|
||||||
|
} else {
|
||||||
|
parent.getResourcesFromClass() + path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,120 @@
|
|||||||
|
package io.bkbn.kompendium.resources
|
||||||
|
|
||||||
|
import Listing
|
||||||
|
import Type
|
||||||
|
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.resources.Resources
|
||||||
|
import io.ktor.server.resources.get
|
||||||
|
import io.ktor.server.response.respondText
|
||||||
|
|
||||||
|
class KompendiumResourcesTest : DescribeSpec({
|
||||||
|
describe("Resource Tests") {
|
||||||
|
it("Can notarize a simple resource") {
|
||||||
|
openApiTestAllSerializers(
|
||||||
|
snapshotName = "T0001__simple_resource.json",
|
||||||
|
applicationSetup = {
|
||||||
|
install(Resources)
|
||||||
|
install(NotarizedResources()) {
|
||||||
|
resources = mapOf(
|
||||||
|
Listing::class to NotarizedResources.ResourceMetadata(
|
||||||
|
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("Resource")
|
||||||
|
description("example resource")
|
||||||
|
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 resources") {
|
||||||
|
openApiTestAllSerializers(
|
||||||
|
snapshotName = "T0002__nested_resources.json",
|
||||||
|
applicationSetup = {
|
||||||
|
install(Resources)
|
||||||
|
install(NotarizedResources()) {
|
||||||
|
resources = mapOf(
|
||||||
|
Type.Edit::class to NotarizedResources.ResourceMetadata(
|
||||||
|
parameters = listOf(
|
||||||
|
Parameter(
|
||||||
|
name = "name",
|
||||||
|
`in` = Parameter.Location.path,
|
||||||
|
schema = TypeDefinition.STRING
|
||||||
|
)
|
||||||
|
),
|
||||||
|
get = GetInfo.builder {
|
||||||
|
summary("Edit")
|
||||||
|
description("example resource")
|
||||||
|
response {
|
||||||
|
responseCode(HttpStatusCode.OK)
|
||||||
|
responseType<TestResponse>()
|
||||||
|
description("does great things")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
Type.Other::class to NotarizedResources.ResourceMetadata(
|
||||||
|
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 resource")
|
||||||
|
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}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
@ -0,0 +1,17 @@
|
|||||||
|
import io.ktor.resources.Resource
|
||||||
|
import kotlinx.serialization.Serializable
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("/list/{name}/page/{page}")
|
||||||
|
data class Listing(val name: String, val page: Int)
|
||||||
|
|
||||||
|
@Serializable
|
||||||
|
@Resource("/type/{name}")
|
||||||
|
data class Type(val name: String) {
|
||||||
|
@Serializable
|
||||||
|
@Resource("/edit")
|
||||||
|
data class Edit(val parent: Type)
|
||||||
|
@Serializable
|
||||||
|
@Resource("/other/{page}")
|
||||||
|
data class Other(val parent: Type, val page: Int)
|
||||||
|
}
|
92
resources/src/test/resources/T0001__simple_resource.json
Normal file
92
resources/src/test/resources/T0001__simple_resource.json
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
{
|
||||||
|
"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": "Resource",
|
||||||
|
"description": "example resource",
|
||||||
|
"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": []
|
||||||
|
}
|
124
resources/src/test/resources/T0002__nested_resources.json
Normal file
124
resources/src/test/resources/T0002__nested_resources.json
Normal file
@ -0,0 +1,124 @@
|
|||||||
|
{
|
||||||
|
"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 resource",
|
||||||
|
"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 resource",
|
||||||
|
"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": []
|
||||||
|
}
|
@ -5,6 +5,7 @@ include("oas")
|
|||||||
include("playground")
|
include("playground")
|
||||||
include("locations")
|
include("locations")
|
||||||
include("json-schema")
|
include("json-schema")
|
||||||
|
include("resources")
|
||||||
|
|
||||||
run {
|
run {
|
||||||
rootProject.children.forEach { it.name = "${rootProject.name}-${it.name}" }
|
rootProject.children.forEach { it.name = "${rootProject.name}-${it.name}" }
|
||||||
|
Reference in New Issue
Block a user