Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: allow media type overrides #369

Merged
merged 3 commits into from
Nov 5, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@

## Released

## [3.7.0] - November 5th, 2022

### Added

- Allow users to override media type in request and response

## [3.6.0] - November 5th, 2022

### Changed
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ import kotlin.reflect.typeOf
class RequestInfo private constructor(
val requestType: KType,
val description: String,
val examples: Map<String, MediaType.Example>?
val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>
) {

companion object {
Expand All @@ -22,6 +23,7 @@ class RequestInfo private constructor(
private var requestType: KType? = null
private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null

fun requestType(t: KType) = apply {
this.requestType = t
Expand All @@ -35,10 +37,15 @@ class RequestInfo private constructor(
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
}

fun mediaTypes(vararg m: String) = apply {
this.mediaTypes = m.toSet()
}

fun build() = RequestInfo(
requestType = requestType ?: error("Request type must be present"),
description = description ?: error("Description must be present"),
examples = examples
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ class ResponseInfo private constructor(
val responseCode: HttpStatusCode,
val responseType: KType,
val description: String,
val examples: Map<String, MediaType.Example>?
val examples: Map<String, MediaType.Example>?,
val mediaTypes: Set<String>
) {

companion object {
Expand All @@ -25,6 +26,7 @@ class ResponseInfo private constructor(
private var responseType: KType? = null
private var description: String? = null
private var examples: Map<String, MediaType.Example>? = null
private var mediaTypes: Set<String>? = null

fun responseCode(code: HttpStatusCode) = apply {
this.responseCode = code
Expand All @@ -42,11 +44,16 @@ class ResponseInfo private constructor(
this.examples = e.toMap().mapValues { (_, v) -> MediaType.Example(v) }
}

fun mediaTypes(vararg m: String) = apply {
this.mediaTypes = m.toSet()
}

fun build() = ResponseInfo(
responseCode = responseCode ?: error("You must provide a response code in order to build a Response!"),
responseType = responseType ?: error("You must provide a response type in order to build a Response!"),
description = description ?: error("You must provide a description in order to build a Response!"),
examples = examples
examples = examples,
mediaTypes = mediaTypes ?: setOf("application/json")
)
}
}
17 changes: 10 additions & 7 deletions core/src/main/kotlin/io/bkbn/kompendium/core/util/Helpers.kt
Original file line number Diff line number Diff line change
Expand Up @@ -84,7 +84,7 @@ object Helpers {
requestBody = when (this) {
is MethodInfoWithRequest -> Request(
description = this.request.description,
content = this.request.requestType.toReferenceContent(this.request.examples),
content = this.request.requestType.toReferenceContent(this.request.examples, this.request.mediaTypes),
required = true
)

Expand All @@ -93,29 +93,32 @@ object Helpers {
responses = mapOf(
this.response.responseCode.value to Response(
description = this.response.description,
content = this.response.responseType.toReferenceContent(this.response.examples)
content = this.response.responseType.toReferenceContent(this.response.examples, this.response.mediaTypes)
)
).plus(this.errors.toResponseMap())
)

private fun List<ResponseInfo>.toResponseMap(): Map<Int, Response> = associate { error ->
error.responseCode.value to Response(
description = error.description,
content = error.responseType.toReferenceContent(error.examples)
content = error.responseType.toReferenceContent(error.examples, error.mediaTypes)
)
}

private fun KType.toReferenceContent(examples: Map<String, MediaType.Example>?): Map<String, MediaType>? =
private fun KType.toReferenceContent(
examples: Map<String, MediaType.Example>?,
mediaTypes: Set<String>
): Map<String, MediaType>? =
when (this.classifier as KClass<*>) {
Unit::class -> null
else -> mapOf(
"application/json" to MediaType(
else -> mediaTypes.associateWith {
MediaType(
schema = if (this.isMarkedNullable) OneOfDefinition(
NullableDefinition(),
ReferenceDefinition(this.getReferenceSlug())
) else ReferenceDefinition(this.getReferenceSlug()),
examples = examples
)
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import io.bkbn.kompendium.core.util.TestModules.nullableEnumField
import io.bkbn.kompendium.core.util.TestModules.nullableField
import io.bkbn.kompendium.core.util.TestModules.nullableNestedObject
import io.bkbn.kompendium.core.util.TestModules.nullableReference
import io.bkbn.kompendium.core.util.TestModules.overrideMediaTypes
import io.bkbn.kompendium.core.util.TestModules.polymorphicCollectionResponse
import io.bkbn.kompendium.core.util.TestModules.polymorphicException
import io.bkbn.kompendium.core.util.TestModules.polymorphicMapResponse
Expand Down Expand Up @@ -112,6 +113,9 @@ class KompendiumTest : DescribeSpec({
it("Can notarize a route with non-required params") {
openApiTestAllSerializers("T0011__non_required_params.json") { nonRequiredParams() }
}
it("Can override media types") {
openApiTestAllSerializers("T0052__override_media_types.json") { overrideMediaTypes() }
}
}
describe("Route Parsing") {
it("Can parse a simple path and store it under the expected route") {
Expand Down
22 changes: 22 additions & 0 deletions core/src/test/kotlin/io/bkbn/kompendium/core/util/TestModules.kt
Original file line number Diff line number Diff line change
Expand Up @@ -315,6 +315,28 @@ object TestModules {
}
}

fun Routing.overrideMediaTypes() {
route("/media_types") {
install(NotarizedRoute()) {
put = PutInfo.builder {
summary(defaultPathSummary)
description(defaultPathDescription)
request {
mediaTypes("multipart/form-data", "application/json")
requestType<TestRequest>()
description("A cool request")
}
response {
mediaTypes("application/xml")
responseType<TestResponse>()
description("A good response")
responseCode(HttpStatusCode.Created)
}
}
}
}
}

fun Routing.simplePathParsing() {
route("/this") {
route("/is") {
Expand Down
123 changes: 123 additions & 0 deletions core/src/test/resources/T0052__override_media_types.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
{
"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": "[email protected]"
},
"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": {
"/media_types": {
"put": {
"tags": [],
"summary": "Great Summary!",
"description": "testing more",
"parameters": [],
"requestBody": {
"description": "A cool request",
"content": {
"multipart/form-data": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
}
},
"application/json": {
"schema": {
"$ref": "#/components/schemas/TestRequest"
}
}
},
"required": true
},
"responses": {
"201": {
"description": "A good response",
"content": {
"application/xml": {
"schema": {
"$ref": "#/components/schemas/TestResponse"
}
}
}
}
},
"deprecated": false
},
"parameters": []
}
},
"webhooks": {},
"components": {
"schemas": {
"TestResponse": {
"type": "object",
"properties": {
"c": {
"type": "string"
}
},
"required": [
"c"
]
},
"TestRequest": {
"type": "object",
"properties": {
"aaa": {
"items": {
"type": "number",
"format": "int64"
},
"type": "array"
},
"b": {
"type": "number",
"format": "double"
},
"fieldName": {
"$ref": "#/components/schemas/TestNested"
}
},
"required": [
"aaa",
"b",
"fieldName"
]
},
"TestNested": {
"type": "object",
"properties": {
"nesty": {
"type": "string"
}
},
"required": [
"nesty"
]
}
},
"securitySchemes": {}
},
"security": [],
"tags": []
}
18 changes: 18 additions & 0 deletions docs/plugins/notarized_route.md
Original file line number Diff line number Diff line change
Expand Up @@ -165,3 +165,21 @@ get = GetInfo.builder {
}
}
```

## Media Types

By default, Kompendium will set the only media type to "application/json". If you would like to override the media type
for a specific request or response (including errors), you can do so with the `mediaTypes` method

```kotlin
get = GetInfo.builder {
summary("Get user by id")
description("A very neat endpoint!")
response {
mediaTypes("application/xml")
responseCode(HttpStatusCode.OK)
responseType<ExampleResponse>()
description("Will return whether or not the user is real 😱")
}
}
```