feat: NotarizedResource plugin
This commit is contained in:
@ -4,6 +4,8 @@
|
|||||||
|
|
||||||
### Added
|
### Added
|
||||||
|
|
||||||
|
- Add support for NotarizedResource plugin scoped to route
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
- Support registering same path with different authentication and methods
|
- Support registering same path with different authentication and methods
|
||||||
|
@ -10,15 +10,16 @@ import io.bkbn.kompendium.core.metadata.PostInfo
|
|||||||
import io.bkbn.kompendium.core.metadata.PutInfo
|
import io.bkbn.kompendium.core.metadata.PutInfo
|
||||||
import io.bkbn.kompendium.core.util.Helpers.addToSpec
|
import io.bkbn.kompendium.core.util.Helpers.addToSpec
|
||||||
import io.bkbn.kompendium.core.util.SpecConfig
|
import io.bkbn.kompendium.core.util.SpecConfig
|
||||||
|
import io.bkbn.kompendium.oas.OpenApiSpec
|
||||||
import io.bkbn.kompendium.oas.path.Path
|
import io.bkbn.kompendium.oas.path.Path
|
||||||
import io.bkbn.kompendium.oas.payload.Parameter
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
import io.ktor.server.application.ApplicationCallPipeline
|
import io.ktor.server.application.ApplicationCallPipeline
|
||||||
import io.ktor.server.application.Hook
|
import io.ktor.server.application.Hook
|
||||||
|
import io.ktor.server.application.PluginBuilder
|
||||||
import io.ktor.server.application.createRouteScopedPlugin
|
import io.ktor.server.application.createRouteScopedPlugin
|
||||||
import io.ktor.server.routing.Route
|
import io.ktor.server.routing.Route
|
||||||
|
|
||||||
object NotarizedRoute {
|
object NotarizedRoute {
|
||||||
|
|
||||||
class Config : SpecConfig {
|
class Config : SpecConfig {
|
||||||
override var tags: Set<String> = emptySet()
|
override var tags: Set<String> = emptySet()
|
||||||
override var parameters: List<Parameter> = emptyList()
|
override var parameters: List<Parameter> = emptyList()
|
||||||
@ -50,25 +51,33 @@ object NotarizedRoute {
|
|||||||
val routePath = route.calculateRoutePath()
|
val routePath = route.calculateRoutePath()
|
||||||
val authMethods = route.collectAuthMethods()
|
val authMethods = route.collectAuthMethods()
|
||||||
|
|
||||||
val path = spec.paths[routePath] ?: Path()
|
addToSpec(spec, routePath, authMethods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fun <T : SpecConfig> PluginBuilder<T>.addToSpec(
|
||||||
|
spec: OpenApiSpec,
|
||||||
|
fullPath: String,
|
||||||
|
authMethods: List<String>
|
||||||
|
) {
|
||||||
|
val path = spec.paths[fullPath] ?: Path()
|
||||||
|
|
||||||
path.parameters = path.parameters?.plus(pluginConfig.parameters) ?: pluginConfig.parameters
|
path.parameters = path.parameters?.plus(pluginConfig.parameters) ?: pluginConfig.parameters
|
||||||
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
|
val serializableReader = application.attributes[KompendiumAttributes.schemaConfigurator]
|
||||||
|
|
||||||
pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.get?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.delete?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.head?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.options?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.post?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.put?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader, routePath, authMethods)
|
pluginConfig.patch?.addToSpec(path, spec, pluginConfig, serializableReader, fullPath, authMethods)
|
||||||
|
|
||||||
spec.paths[routePath] = path
|
spec.paths[fullPath] = path
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
|
fun Route.calculateRoutePath() = toString().replace(Regex("/\\(.+\\)"), "")
|
||||||
private fun Route.collectAuthMethods() = toString()
|
fun Route.collectAuthMethods() = toString()
|
||||||
.split("/")
|
.split("/")
|
||||||
.filter { it.contains(Regex("\\(authenticate .*\\)")) }
|
.filter { it.contains(Regex("\\(authenticate .*\\)")) }
|
||||||
.map { it.replace("(authenticate ", "").replace(")", "") }
|
.map { it.replace("(authenticate ", "").replace(")", "") }
|
||||||
|
@ -4,6 +4,12 @@ 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`
|
Kompendium supports Ktor-Resources through an ancillary module `kompendium-resources`
|
||||||
|
|
||||||
|
{% hint style="warning" %}
|
||||||
|
The resources module contains _two_ plugins: `KompendiumResources` and `KompendiumResource`. You will find more
|
||||||
|
information on both below, but in a nutshell, the former is an application level plugin intended to define your entire
|
||||||
|
application, while the latter is a route level approach should you wish to split out your route definitions.
|
||||||
|
{% endhint %}
|
||||||
|
|
||||||
## Adding the Artifact
|
## Adding the Artifact
|
||||||
|
|
||||||
Prior to documenting your resources, you will need to add the artifact to your gradle build file.
|
Prior to documenting your resources, you will need to add the artifact to your gradle build file.
|
||||||
@ -14,9 +20,11 @@ dependencies {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
## Installing Plugin
|
## NotarizedResources
|
||||||
|
|
||||||
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.
|
The `NotarizedResources` plugin is an _application_ level plugin, and **must** be installed after both the
|
||||||
|
`NotarizedApplication` plugin and the Ktor `Resources` plugin. It is intended to be used to document your entire
|
||||||
|
application in a single block.
|
||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
private fun Application.mainModule() {
|
private fun Application.mainModule() {
|
||||||
@ -54,7 +62,64 @@ private fun Application.mainModule() {
|
|||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
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.
|
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
|
{% hint style="danger" %}
|
||||||
> exception!
|
If you try to map a class that is not annotated with the ktor `@Resource` annotation, you will get a runtime exception!
|
||||||
|
{% endhint %}
|
||||||
|
|
||||||
|
## NotarizedResource
|
||||||
|
|
||||||
|
If you prefer a route-based approach similar to `NotarizedRoute`, you can use the `NotarizedResource<MyResourceType>()`
|
||||||
|
plugin instead of `NotarizedResources`. It will combine paths from any parent route with the route defined in the
|
||||||
|
resource, exactly as Ktor itself does:
|
||||||
|
|
||||||
|
```kotlin
|
||||||
|
@Serializable
|
||||||
|
@Resource("/list/{name}/page/{page}")
|
||||||
|
data class Listing(val name: String, val page: Int)
|
||||||
|
|
||||||
|
private fun Application.mainModule() {
|
||||||
|
install(Resources)
|
||||||
|
route("/api") {
|
||||||
|
listingDocumentation()
|
||||||
|
get<Listing> { listing ->
|
||||||
|
call.respondText("Listing ${listing.name}, page ${listing.page}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.listingDocumentation() {
|
||||||
|
install(NotarizedResource<Listing>()) {
|
||||||
|
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 😱")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
In this case, the generated path will be `/api/list/{name}/page/{page}`, combining the route prefix with the path in the
|
||||||
|
resource.
|
||||||
|
|
||||||
|
{% hint style="danger" %}
|
||||||
|
If you try to map a class that is not annotated with the ktor `@Resource` annotation, you will get a runtime exception!
|
||||||
|
{% endhint %}
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
package io.bkbn.kompendium.resources
|
||||||
|
|
||||||
|
import io.ktor.resources.Resource
|
||||||
|
import kotlin.reflect.KClass
|
||||||
|
import kotlin.reflect.full.findAnnotation
|
||||||
|
import kotlin.reflect.full.hasAnnotation
|
||||||
|
import kotlin.reflect.full.memberProperties
|
||||||
|
|
||||||
|
fun KClass<*>.getResourcePathFromClass(): String {
|
||||||
|
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.getResourcePathFromClass() + path
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,35 @@
|
|||||||
|
package io.bkbn.kompendium.resources
|
||||||
|
|
||||||
|
import io.bkbn.kompendium.core.attribute.KompendiumAttributes
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute.addToSpec
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute.calculateRoutePath
|
||||||
|
import io.bkbn.kompendium.core.plugin.NotarizedRoute.collectAuthMethods
|
||||||
|
import io.ktor.server.application.ApplicationCallPipeline
|
||||||
|
import io.ktor.server.application.Hook
|
||||||
|
import io.ktor.server.application.createRouteScopedPlugin
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
|
||||||
|
object NotarizedResource {
|
||||||
|
object InstallHook : Hook<(ApplicationCallPipeline) -> Unit> {
|
||||||
|
override fun install(pipeline: ApplicationCallPipeline, handler: (ApplicationCallPipeline) -> Unit) {
|
||||||
|
handler(pipeline)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inline operator fun <reified T> invoke() = createRouteScopedPlugin(
|
||||||
|
name = "NotarizedResource<${T::class.qualifiedName}>",
|
||||||
|
createConfiguration = NotarizedRoute::Config
|
||||||
|
) {
|
||||||
|
on(InstallHook) {
|
||||||
|
val route = it as? Route ?: return@on
|
||||||
|
val spec = application.attributes[KompendiumAttributes.openApiSpec]
|
||||||
|
val routePath = route.calculateRoutePath()
|
||||||
|
val authMethods = route.collectAuthMethods()
|
||||||
|
val resourcePath = T::class.getResourcePathFromClass()
|
||||||
|
val fullPath = "$routePath$resourcePath"
|
||||||
|
|
||||||
|
addToSpec(spec, fullPath, authMethods)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -12,12 +12,8 @@ import io.bkbn.kompendium.core.util.Helpers.addToSpec
|
|||||||
import io.bkbn.kompendium.core.util.SpecConfig
|
import io.bkbn.kompendium.core.util.SpecConfig
|
||||||
import io.bkbn.kompendium.oas.path.Path
|
import io.bkbn.kompendium.oas.path.Path
|
||||||
import io.bkbn.kompendium.oas.payload.Parameter
|
import io.bkbn.kompendium.oas.payload.Parameter
|
||||||
import io.ktor.resources.Resource
|
|
||||||
import io.ktor.server.application.createApplicationPlugin
|
import io.ktor.server.application.createApplicationPlugin
|
||||||
import kotlin.reflect.KClass
|
import kotlin.reflect.KClass
|
||||||
import kotlin.reflect.full.findAnnotation
|
|
||||||
import kotlin.reflect.full.hasAnnotation
|
|
||||||
import kotlin.reflect.full.memberProperties
|
|
||||||
|
|
||||||
object NotarizedResources {
|
object NotarizedResources {
|
||||||
|
|
||||||
@ -45,7 +41,7 @@ object NotarizedResources {
|
|||||||
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.resources.forEach { (k, v) ->
|
pluginConfig.resources.forEach { (k, v) ->
|
||||||
val resource = k.getResourcesFromClass()
|
val resource = k.getResourcePathFromClass()
|
||||||
val path = spec.paths[resource] ?: Path()
|
val path = spec.paths[resource] ?: Path()
|
||||||
path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters
|
path.parameters = path.parameters?.plus(v.parameters) ?: v.parameters
|
||||||
v.get?.addToSpec(path, spec, v, serializableReader, resource)
|
v.get?.addToSpec(path, spec, v, serializableReader, resource)
|
||||||
@ -59,20 +55,4 @@ object NotarizedResources {
|
|||||||
spec.paths[resource] = path
|
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@ -14,9 +14,11 @@ import io.ktor.server.application.install
|
|||||||
import io.ktor.server.resources.Resources
|
import io.ktor.server.resources.Resources
|
||||||
import io.ktor.server.resources.get
|
import io.ktor.server.resources.get
|
||||||
import io.ktor.server.response.respondText
|
import io.ktor.server.response.respondText
|
||||||
|
import io.ktor.server.routing.Route
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
|
||||||
class KompendiumResourcesTest : DescribeSpec({
|
class KompendiumResourcesTest : DescribeSpec({
|
||||||
describe("Resource Tests") {
|
describe("NotarizedResources Tests") {
|
||||||
it("Can notarize a simple resource") {
|
it("Can notarize a simple resource") {
|
||||||
openApiTestAllSerializers(
|
openApiTestAllSerializers(
|
||||||
snapshotName = "T0001__simple_resource.json",
|
snapshotName = "T0001__simple_resource.json",
|
||||||
@ -117,4 +119,72 @@ class KompendiumResourcesTest : DescribeSpec({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
describe("NotarizedResource Tests") {
|
||||||
|
it("Can notarize resources in route") {
|
||||||
|
openApiTestAllSerializers(
|
||||||
|
snapshotName = "T0003__resources_in_route.json",
|
||||||
|
applicationSetup = {
|
||||||
|
install(Resources)
|
||||||
|
}
|
||||||
|
) {
|
||||||
|
route("/api") {
|
||||||
|
typeEditDocumentation()
|
||||||
|
get<Type.Edit> { edit ->
|
||||||
|
call.respondText("Listing ${edit.parent.name}")
|
||||||
|
}
|
||||||
|
typeOtherDocumentation()
|
||||||
|
get<Type.Other> { other ->
|
||||||
|
call.respondText("Listing ${other.parent.name}, page ${other.page}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
private fun Route.typeOtherDocumentation() {
|
||||||
|
install(NotarizedResource<Type.Other>()) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun Route.typeEditDocumentation() {
|
||||||
|
install(NotarizedResource<Type.Edit>()) {
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
124
resources/src/test/resources/T0003__resources_in_route.json
Normal file
124
resources/src/test/resources/T0003__resources_in_route.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": {
|
||||||
|
"/api/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
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"/api/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": []
|
||||||
|
}
|
Reference in New Issue
Block a user