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

Intergate Swagger UI Hosting as Ktor Feature #453

Closed
JLLeitschuh opened this issue Jun 27, 2018 · 20 comments
Closed

Intergate Swagger UI Hosting as Ktor Feature #453

JLLeitschuh opened this issue Jun 27, 2018 · 20 comments

Comments

@JLLeitschuh
Copy link
Contributor

It would be really nice if Ktor could support hosting a Swagger UI that is generated from your routes configuration.

For example, the following could be used to generate a Swagger UI.

data class PetModel(val id: Int?, val name: String)

data class PetsModel(val pets: MutableList<PetModel>)

val data = PetsModel(mutableListOf(PetModel(1, "max"), PetModel(2, "moritz")))
fun newId() = ((data.pets.map { it.id ?: 0 }.max()) ?: 0) + 1

@Group("pet operations")
@Location("/pets/{id}")
class pet(val id: Int)

@Group("pet operations")
@Location("/pets")
class pets

@Group("debug")
@Location("/request/info")
class requestInfo

@Group("debug")
@Location("/request/withHeader")
class withHeader

class Header(val optionalHeader: String?, val mandatoryHeader: Int)

@Group("debug")
@Location("/request/withQueryParameter")
class withQueryParameter

class QueryParameter(val optionalParameter: String?, val mandatoryParameter: Int)

fun main(args: Array<String>) {
    val server = embeddedServer(Netty, getInteger("server.port", 8080)) {
        install(DefaultHeaders)
        install(Compression)
        install(CallLogging)
        install(ContentNegotiation) {
            gson {
                setPrettyPrinting()
            }
        }
        install(Locations)
        install(SwaggerSupport) {
            forwardRoot = true
            swagger.info = Information(
                version = "0.1",
                title = "sample api implemented in ktor",
                description = "This is a sample which combines [ktor](https://github.com/Kotlin/ktor) with [swaggerUi](https://swagger.io/). You find the sources on [github](https://github.com/nielsfalk/ktor-swagger)",
                contact = Contact(
                    name = "Niels Falk",
                    url = "https://nielsfalk.de"
                )
            )
        }
        routing {
            get<pets>("all".responds(ok<PetsModel>())) {
                call.respond(data)
            }
            post<pets, PetModel>("create".responds(ok<PetModel>())) { _, entity ->
                // http201 would be better but there is no way to do this see org.jetbrains.ktor.gson.GsonSupport.renderJsonContent
                call.respond(entity.copy(id = newId()).apply {
                    data.pets.add(this)
                })
            }
            get<pet>("find".responds(ok<PetModel>(), notFound())) { params ->
                data.pets.find { it.id == params.id }
                    ?.let {
                        call.respond(it)
                    }
            }
            put<pet, PetModel>("update".responds(ok<PetModel>(), notFound())) { params, entity ->
                if (data.pets.removeIf { it.id == params.id && it.id == entity.id }) {
                    data.pets.add(entity)
                    call.respond(entity)
                }
            }
            delete<pet>("delete".responds(ok<Unit>(), notFound())) { params ->
                if (data.pets.removeIf { it.id == params.id }) {
                    call.respond(Unit)
                }
            }
            get<requestInfo>(
                responds(ok<Unit>()),
                respondRequestDetails()
            )
            get<withQueryParameter>(
                responds(ok<Unit>())
                    .parameter<QueryParameter>(),
                respondRequestDetails()
            )
            get<withHeader>(
                responds(ok<Unit>())
                    .header<Header>(),
                respondRequestDetails()
            )
        }
    }
    server.start(wait = true)
}

fun respondRequestDetails(): suspend PipelineContext<Unit, ApplicationCall>.(Any) -> Unit {
    return {
        call.respond(
            mapOf(
                "parameter" to call.parameters,
                "header" to call.request.headers
            ).format()
        )
    }
}

private fun Map<String, StringValues>.format() =
    mapValues {
        it.value.toMap()
            .flatMap { (key, value) -> value.map { key to it } }
            .map { (key, value) -> "$key: $value" }
            .joinToString(separator = ",\n")
    }
        .map { (key, value) -> "$key:\n$value" }
        .joinToString(separator = "\n\n")

This is the swagger UI generated from the above.

screen shot 2018-06-26 at 10 11 29 pm

I spent today overhauling @nielsfalk's project ktor-swagger to use the newest version of Ktor and also use Gradle to build the application PR here.

I think this project has quite a bit of potential and could satisfy a need in the community by allowing for a fast way to create documentation for API's written using Ktor.

If the Ktor team would like to adopt this project as a feature, I'm happy to try to make the port from the external project it is today into this repository.

If the interest does not exist to adopt a new feature, I totally understand. The concern that I have with publishing this myself (or with @nielsfalk assistance) is the issue of incompatible breaking changes in Ktor (as Ktor is pre-1.0).

I open the floor to the developers of this project. I'd love to see this integrated as a fully supported feature, but I understand if this is outside the scope of this project.

@JLLeitschuh
Copy link
Contributor Author

JLLeitschuh commented Jun 27, 2018

Some of the concerns that I have with this code base as it is is that it generates all of the schemas from kotlin data classes and expects that some JSON content negotiation feature be installed (ie. Jackson or Gson).

The downside of this is that you are unable to define your JSON schemas externally for objects easily (for example, you would like to put custom descriptions on fields and apply JSON schema validation).

Currently, the way the feature is written the Swagger object is expected to be converted by SOME content negotiator.

typealias ModelName = String
typealias PropertyName = String
typealias Path = String
typealias Definitions = MutableMap<ModelName, ModelData>
typealias Paths = MutableMap<Path, Methods>
typealias MethodName = String
typealias HttpStatus = String
typealias Methods = MutableMap<MethodName, Operation>

class Swagger {
    val swagger = "2.0"
    var info: Information? = null
    val paths: Paths = mutableMapOf()
    val definitions: Definitions = mutableMapOf()
}

class Information(
    val description: String? = null,
    val version: String? = null,
    val title: String? = null,
    val contact: Contact? = null
)

data class Tag(
    val name: String
)

class Contact(
    val name: String? = null,
    val url: String? = null,
    val email: String? = null
)

class Operation(
    metadata: Metadata,
    val responses: Map<HttpStatus, Response>,
    val parameters: List<Parameter>,
    location: Location,
    group: Group?,
    method: HttpMethod,
    locationType: KClass<*>,
    entityType: KClass<*>
) {
    val tags = group?.toList()
    val summary = metadata.summary ?: "${method.value} ${location.path}"
}

class ModelData(val properties: Map<PropertyName, Property>)

The above structure generates the JSON that looks like the following when GSON is installed.

{
  "swagger": "2.0",
  "info": {
    "description": "This is a sample which combines [ktor](https://github.com/Kotlin/ktor) with [swaggerUi](https://swagger.io/). You find the sources on [github](https://github.com/nielsfalk/ktor-swagger)",
    "version": "0.1",
    "title": "sample api implemented in ktor",
    "contact": {
      "name": "Niels Falk",
      "url": "https://nielsfalk.de"
    }
  },
  "paths": {
    "/pets": {
      "get": {
        "tags": [
          {
            "name": "pet operations"
          }
        ],
        "summary": "all",
        "responses": {
          "200": {
            "description": "PetsModel",
            "schema": {
              "$ref": "#/definitions/PetsModel"
            }
          }
        },
        "parameters": []
      },
      "post": {
        "tags": [
          {
            "name": "pet operations"
          }
        ],
        "summary": "create",
        "responses": {
          "201": {
            "description": "PetModel",
            "schema": {
              "$ref": "#/definitions/PetModel"
            }
          }
        },
        "parameters": [
          {
            "name": "body",
            "in": "body",
            "description": "PetModel",
            "required": true,
            "schema": {
              "$ref": "#/definitions/PetModel"
            }
          }
        ]
      }
    },
    "/pets/{id}": {
      "get": {
        "tags": [
          {
            "name": "pet operations"
          }
        ],
        "summary": "find",
        "responses": {
          "200": {
            "description": "PetModel",
            "schema": {
              "$ref": "#/definitions/PetModel"
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "id",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ]
      },
      "put": {
        "tags": [
          {
            "name": "pet operations"
          }
        ],
        "summary": "update",
        "responses": {
          "200": {
            "description": "PetModel",
            "schema": {
              "$ref": "#/definitions/PetModel"
            }
          },
          "404": {
            "description": "Not Found"
          }
        },
        "parameters": [
          {
            "name": "body",
            "in": "body",
            "description": "PetModel",
            "required": true,
            "schema": {
              "$ref": "#/definitions/PetModel"
            }
          },
          {
            "name": "id",
            "in": "path",
            "description": "id",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ]
      },
      "delete": {
        "tags": [
          {
            "name": "pet operations"
          }
        ],
        "summary": "delete",
        "responses": {
          "200": {
            "description": "OK"
          },
          "404": {
            "description": "Not Found"
          }
        },
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "description": "id",
            "required": true,
            "type": "integer",
            "format": "int32"
          }
        ]
      }
    },
    "/request/info": {
      "get": {
        "tags": [
          {
            "name": "debug"
          }
        ],
        "summary": "GET /request/info",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "parameters": []
      }
    },
    "/request/withQueryParameter": {
      "get": {
        "tags": [
          {
            "name": "debug"
          }
        ],
        "summary": "GET /request/withQueryParameter",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "parameters": [
          {
            "name": "mandatoryParameter",
            "in": "query",
            "description": "mandatoryParameter",
            "required": true,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "optionalParameter",
            "in": "query",
            "description": "optionalParameter",
            "required": false,
            "type": "string"
          }
        ]
      }
    },
    "/request/withHeader": {
      "get": {
        "tags": [
          {
            "name": "debug"
          }
        ],
        "summary": "GET /request/withHeader",
        "responses": {
          "200": {
            "description": "OK"
          }
        },
        "parameters": [
          {
            "name": "mandatoryHeader",
            "in": "header",
            "description": "mandatoryHeader",
            "required": true,
            "type": "integer",
            "format": "int32"
          },
          {
            "name": "optionalHeader",
            "in": "header",
            "description": "optionalHeader",
            "required": false,
            "type": "string"
          }
        ]
      }
    }
  },
  "definitions": {
    "PetsModel": {
      "properties": {
        "pets": {
          "type": "array",
          "items": {
            "description": "PetModel",
            "$ref": "#/definitions/PetModel"
          }
        }
      }
    },
    "PetModel": {
      "properties": {
        "id": {
          "type": "integer",
          "format": "int32"
        },
        "name": {
          "type": "string"
        }
      }
    }
  }
}

You could fix this perhaps by defining:

typealias Definitions = MutableMap<ModelName, Any>

And allowing a user to define either a class or pass a custom schema as a string.

Another component that is missing from this example that I'd really like in my application is some sort of JSON Schema validation before the content is deserialized by the content negotiator.
That way you could have the JSON schema validator provide more intelligent validation than Gson or Jackson could provide.

@JLLeitschuh
Copy link
Contributor Author

I'm looking for some feedback from @cy6erGn0m or @orangy before I proceed with a PR.
This will require quite a bit of work to integrate this into the ktor repository and I don't want to dedicate the time to do so if the PR will be rejected as an undesired feature.

@orangefiredragon
Copy link

Are there plans to release this?

@JLLeitschuh
Copy link
Contributor Author

@orangefiredragon It's currently released here:
https://github.com/nielsfalk/ktor-swagger

I'd love to integrate it officially.

Currently the project is 90% developed for my own use case at my company. The API is also pretty fluid currently as I'm still figuring out where we are missing things/need new functionality.

But in its current state it's very functional.

@galex
Copy link

galex commented Jan 20, 2019

I'd love to see this integrated into Ktor. Automatic API docs is great!

@astoialtsev
Copy link

astoialtsev commented Jan 23, 2019

Really hope to see the official GO from Ktor crew.

@volkert-fastned
Copy link

@JLLeitschuh @nielsfalk I tried the example code at the top of this issue thread, but I'm running into "$ref: must be a string (JSON-Ref)" errors whenever I click to expand any of the operations in the Swagger UI page. I'm trying to use ktor-swagger with Ktor 1.1.3 and Jackson 2.9.8. Am I doing something wrong, or is this an incompatibility with the latest version of Ktor?

See this ktor-swagger issue for details: nielsfalk/ktor-swagger#29

Thanks.

@noahbetzen-wk
Copy link

This missing feature is the only thing keeping my company from adopting Ktor. Until then we're using Spring and SpringFox.

Huge props to @JLLeitschuh for the work he did. I'd love to see it (or a similar implementation) merged into Ktor itself.

I looked around online for a public roadmap but couldn't find one; it'd be nice to at least know if the Ktor/Kotlin/JetBrains team has a plan for this. Even just a "more/less than 6 months" estimate would be great.

@JLLeitschuh
Copy link
Contributor Author

There hasn't been any discussion about this with me or anyone else from the JB team about integrating this.

@snackycracky
Copy link

Hey :) Thanks for your Effort @JLLeitschuh I just tried your library, and was a little shocked that I needed to use the standard ktor routing functions like get, post and other stuff from your lib just to get the openapi-docs. I reverted the integration, sorry. Are there plans for integrating OpenAPI docs in ktor ? How can I generate those docs the ktor way in August 2019 @cy6erGn0m ?

@JLLeitschuh
Copy link
Contributor Author

JLLeitschuh commented Aug 26, 2019

I'm no longer working for the company that was actually using this library (just moved from working for HPE to working for Gradle). As such, this is no longer the biggest priority for me anymore unfortunately.

I'm happy to help anyone else out who wants to add this support to Ktor. As for feature development, I can no longer offer anything more than PR review support and feedback at this point.

Sorry.

@Aditya94A
Copy link

Any updates on this? This is really good, it belongs in the official repo! @cy6erGn0m @orangy

@Globegitter
Copy link

I agree it would be great to hear at least something official back given that this has nearly 80 thumbs-up.

CC @cy6erGn0m @orangy @e5l

@kemalizing
Copy link

yes, exactly. please at least let us know if this will be added or not. that will be very helpful.

@dragneelfps
Copy link
Contributor

any update on this?

@ramrock93
Copy link

Any updates on this one yet?

@SerVB
Copy link

SerVB commented Mar 21, 2020

I'm using this variant: https://github.com/papsign/Ktor-OpenAPI-Generator. It's pretty handy but requires testing and discussions with the author. Please join!

@stale
Copy link

stale bot commented Jun 19, 2020

This issue has been automatically marked as stale because it has not had recent activity.

@stale stale bot added the stale label Jun 19, 2020
@e5l e5l removed the stale label Jun 19, 2020
@oleg-larshin
Copy link

Please check the following ticket on YouTrack for follow-ups to this issue. GitHub issues will be closed in the coming weeks.

@Marek00Malik
Copy link

What is the status of this feature?
The best project kompendium that gave OAS support to Ktor is not supporting Ktor 2.0.0 because of this issue...
kompendium

@Stexxe Stexxe closed this as completed Nov 28, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests