Code Monkey home page Code Monkey logo

ktor-openapi-generator's Introduction

Ktor OpenAPI Generator

Build

The Ktor OpenAPI Generator is a library to automatically generate the descriptor as you route your ktor application.

Ktor OpenAPI Generator is:

  • Modular
  • Strongly typed
  • Explicit

Currently Supported:

  • Authentication interoperability with strongly typed Principal (OAuth only, see TestServer in tests)
  • Content Negotiation interoperability (see TestServer in tests)
  • Custom response codes (as parameter in @Response)
  • Automatic and custom content Type routing and parsing (see com.papsign.ktor.openapigen.content.type, Binary Parser and default JSON parser (that uses the ktor implicit parsing/serializing))
  • Exception handling (use .throws(ex) {} in the routes with an APIException object) with Status pages interop (with .withAPI in the StatusPages configuration)
  • tags (.tag(tag) {} in route with a tag object, currently must be an enum, but may be subject to change)
  • Spec compliant Parameter Parsing (see basic example)
  • Legacy Polymorphism with use of @DiscriminatorAnnotation() attribute and sealed classes

Extra Features:

  • Includes Swagger-UI (enabled by default, can be managed in the install(OpenAPIGen) { ... } section)

Examples

Take a look at a few examples

Who is using it?

And others... (add your name above)

Installation

Gradle

Step 1. Add the JitPack repository to your build file:

allprojects {
    repositories {
        ...
        maven { url 'https://jitpack.io' }
    }
}

Step 2. Add the dependency:

dependencies {
        implementation 'com.github.papsign:Ktor-OpenAPI-Generator:-SNAPSHOT'
}

Git Submodule

Install the submodule:

git submodule add https://github.com/papsign/Ktor-OpenAPI-Generator.git openapigen

Declare the folder in settings.gradle:

...
include 'openapigen'

Declare the dependency in the main build.gradle

apply plugin: 'kotlin'

...

dependencies {
    compile project(":openapigen")
    ...
}

ktor-openapi-generator's People

Contributors

ansman avatar aphu-figure avatar bargergo avatar bherbst avatar darkxanter avatar davidhiendl avatar iturchenko avatar javuto2 avatar kkalisz-bt avatar pixellos avatar servb avatar sigmanil avatar szer avatar uuf6429 avatar vansickle avatar wicpar avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

ktor-openapi-generator's Issues

Header without backtits

I want to create header without '-', but if I create @HeaderParam with name Authorization, this header will not appear in request as there
image

Proper handling of library generated errors

Creating this because of #28
Currently there is no system to handle errors that occurred during runtime with this library. Instead i opted to use default values when parsing fails as it has best risk/effort ratio of the low effort options.

But now comes the time to implement it properly.

What it should do:

  • Have a default behaviour that is expected by the user
  • A standard format used by the library and it's extensions.
  • The errors should be aggregatable, all errors that can be known should be shown. (execution must continue after an error to catch other possible ones, i.e. two parameters have the wrong format or missing)
  • Error codes should allow for easy internationalisation
  • Contain What, Why and where it went wrong for every issue.
  • Should not leak information to the frontend beyond what is required to handle the error properly (No stacktraces.)

Usage with kotlinx.serialization

Hi,
This library looks great!

I am having problems with getting things working in my Ktor application which uses kotlinx.serialization for API serialization/deserialization. I can't find if this library should work (or is tested) with kotlinx.serialization? And if so if there are any specific settings that need to be set?

kotlin package name schema Namer

Not issue, only info

kotlin packages for example.in domain with name like
package `in`.example

will need schemaNamer

 schemaNamer = { kType ->
            // Added ` in regex
            val regex = Regex("[`A-Za-z0-9_.]+")
            kType.toString().replace(regex) { it.value.split(".").last() }.replace(Regex(">|<|, "), "_")
        }

How to create custom validators with the latest version?

I've created custom validators with the 0.1-beta.2 version and I wanted to update to the latest version (0.2-beta.5). The validation system changed a lot and I can't figure it out how to write a custom validator. I've only found example on custom validators for the old version.

Support for model parameter descriptions

I would love to be able to annotate model properties with descriptions, like we can do with @HeaderParam, @PathParam, and @QueryParam, and it doesn't look like that is currently possible.

I'd imagine something like the new @OpenAPIName would work well.

Make it possible to specify HTTP response code without using annotations

We've recently started using this project, and we find it provides great value. One issue we're having, however, is code duplication (or alternatively the need for wrappers etc) in our "DTO" classes due to the fact that Ktor-OpenAPI-Generator seems to require HTTP response codes to be specified using the Response-annotation. Very often, the structures we want to return from GET, PUT and POST are the same, except for the http response code - a CRUD-api will want 201 for the POST, but 200 for the GET and PUT, for instance. The only way we've found around this is by having generic wrapper DTOs, but that means we can't have documentation that's specific to the endpoint.

It would be nice if the success status code for an endpoint could be passed as an argument to the post,get,put,etc functions - maybe as an argument to the info()-method?

jitpack build broken

Sorry for opening one issue after another and thanks for being so quick to respond, but here is one more. As I am not always a fan of using git submodules I thought I would give jitpack a try since that has worked for me quite well so far, but for some reason the build for this project is broken, see: https://jitpack.io/#papsign/Ktor-OpenAPI-Generator/c2b7932414 and https://jitpack.io/com/github/papsign/Ktor-OpenAPI-Generator/c2b7932414/build.log

Not sure if it would be a lot of effort to get this building on jitpack but it could be nice to give people an alternative to using git submodules.

is it possible to have an endpoint with multiple query params?

is it possible to have an endpoint with multiple query params?

for example, the following:

@Request("Query Params")
data class QueryParams(@QueryParam("a") val  a: String?, @QueryParam("b") val b: String?)
    apiRouting {
        route("/data/get") {
            get<QueryParams, List<Data>>(
                info("Query Params Endpoint", "This is a Query Params Endpoint"),
                example = mutableListOf(Data("SomeData"))
            ) { params -> 
                respond( Data("SomeData") )
            }
        }
    }

results in:

Exception in thread "main" java.lang.IllegalStateException: Only data classes are currently supported for Parameter objects
        at com.papsign.ktor.openapigen.parameters.parsers.converters.object.ObjectConverter.<init>(ObjectConverter.kt:25)
        at com.papsign.ktor.openapigen.parameters.parsers.converters.object.ObjectConverter$Companion.create(ObjectConverter.kt:44)
        at com.papsign.ktor.openapigen.parameters.parsers.converters.object.ObjectConverter$Companion.create(ObjectConverter.kt:37)
        at com.papsign.ktor.openapigen.parameters.parsers.converters.ConverterSelectorFactory.buildConverter(ConverterSelectorFactory.kt:7)
        at com.papsign.ktor.openapigen.parameters.parsers.builders.query.form.ConverterFormBuilder$Selector.canHandle(ConverterFormBuilder.kt:20)
        at com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderSelectorFactory.buildBuilder(BuilderSelectorFactory.kt:9)
        at com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderFactory$DefaultImpls.buildBuilderForced(BuilderFactory.kt:8)
        at com.papsign.ktor.openapigen.parameters.parsers.builders.BuilderSelectorFactory.buildBuilderForced(BuilderSelectorFactory.kt:6)

Also, could you point me in the right direction of how to do this if it is possible?

serialized name annotation

image
Is there possibility to rename parameters as in Gson library ?
image
I want generate parameters with different name in Kotlin and Json

Feature: allow to provide multiple examples of responces

For example, I have the following endpoint:

        delete<ProductIdParam, SuccessResult>(
            info(
                summary = "Remove a product.",
                description = "The product is removed only if a product with the same ID exists. Returns `${SuccessResult::class.simpleName}` saying whether the product has been removed."
            ),
            example = SuccessResult.OK,
            body = { param -> removeProduct(param.id, this::respond) }
        )

I want to specify different examples, for example, with different status codes. So maybe example should be renamed to examples and receive an iterable. Possible usage:

        delete<ProductIdParam, SuccessResult>(
            info(
                summary = "Remove a product.",
                description = "The product is removed only if a product with the same ID exists. Returns `${SuccessResult::class.simpleName}` saying whether the product has been removed."
            ),
            examples = listOf(SuccessResult.OK, SuccessResult.NOT_OK),
            body = { param -> removeProduct(param.id, this::respond) }
        )

The same for other methods (and parameters like exampleResponse).

sealed class support

Hey, thanks for this project, so far it looks great but I am running into some limitations with sealed classes. I have a model as follows:

data class Tablet(
    val id: String,
    val something: Something,
    val apps: Map<String, App>
)

where-as app is a sealed class and while jackson 2.10.1 recognises that and converts incoming requests perfectly fine in the generated json schema App is only displayed as an empty object. Are sealed classes meant to be supported? As far as I can tell that would probably map to a oneof, right?

Edit: For reference, here is btw the PR that added auto-discovery of sealed classes: FasterXML/jackson-module-kotlin#240

Custom auth on route

I am trying to do custom token based authentication on whole route but using ktor authentication is not working. How to add authentication method for openapi?

fun Routing.userRoute() {
    apiRouting {
        authenticate {
            route("users", Tags.USER) {
                route("agent").get<Unit, UserAgentResponse>(
                    info("Get Agent", "Get agent used to get images from server"),
                    example = UserAgentResponse("qwerty")
                ) {
                    respond(UserAgentResponse(Helper.userAgent))
                }
            }
        }
    }
}

create tag with name and description

inline fun NormalOpenAPIRoute.tag(name: String = "", description: String = "", crossinline fn: NormalOpenAPIRoute.() -> Unit) { tag(object : APITag { override val description = description override val name = name }, fn) }
Sorry now I have a little bit time to create PM. You can add this code to create tags with name and description instead of implementing interface (or write if this functionality already exists)

different body types

Can I use different body types on success and on error? Because in Retrofit Im using different bodies with different types

Multipart Example

I am trying to create route to receive upload file to server but not able to find a way to implement it. I have seen multipart content provider in code but not sure how to use it.

How to write post data server code for multipart something like this:
https://ktor.io/servers/uploads.html

Alternate "syntax" for routing.

Just a wild idea. How about allowing routing like get("path", ::someMethod) and the parameters of that method are annotated the same way the fields of parameter classes are now annotated.

That way you don't have to declare a class for just one or two simple parameters and you get a very natural looking routing style.

Also this would be quite familiar to users of springfox.

jwt authentication support

So, hopefully this will be the last one. In our application we have

install(Authentication) {
        jwt(configure = authenticationProvider)
    }

and I would like for routes within the apiRouting block to be able to make use of that authentication provider. For normal kotlin routes I can just use the authenticate block but that does not seem to exist in apiRouting. Is this use-case supported or not?

Hardcoding statusCode is Response annotation

I don't find any Int constants, so currently I suppose hardcoding like 404 is required.

And if somebody reworks Response annotation so it accepts HttpStatusCode instead of Int, it will be a duplicated description (Response.description and Response.statusCode.description). There is another solution I see: leave only a single parameter in the Response of HttpStatusCode type but it's not pretty because of the class name. Actually, I've planed to open a PR with the first solution but now I don't know what's more elegant in that situation.

It's a related to #24 because it's about description too.

Status code in "throws" block variant

Hi! I have the following code:

image

As you see, status code is duplicated. So I don't understand which has a higher priority...

Can I omit this duplication?

Setting up "Models" section

Hi! Is there an opportunity to change the "Models" section?

Currently, I have the following one (on this commit):

image

I'm not an experienced Swagger user but I think the issues are:

  1. The T model should not be presented.
  2. minimum: null/maximum: null for string and boolean seem redundant.
  3. minimum: -2147483648 for id: int32 isn't appropriate, I want it to start with 0.
  4. kotlin.collections.List is strange, it's a simple JSON array...
  5. io.github.servb.eShop.util.OptionalResult<io.github.servb.eShop.route.product.v1.ProductUsable> has the data: T field, but data: io.github.servb.eShop.route.product.v1.ProductUsable is more logical here.

I've briefly checked the annotations but haven't found suitable ones.

how to implement a repeated request parameter

I must be missing something completely. My usecase is a simple GET endpoint taking a list of Long ids and returning associated data.
I modelled the input parameter "ids" as

data class ProductImagesRequest(@QueryParam("multiple ids.") val ids:List<Long>)

When calling it with parameters like ?ids=1&ids=2&ids=3 I get this error:

com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize instance of `java.util.ArrayList<java.lang.Long>` out of VALUE_STRING token

Why would a request parameter be deserialized from xml or json?

I love this library BTW, it's a very elegant solution!

use parameter not once

image
I have this models and I am using auth param in almost every request (there can be other parameter, not only auth). Can I somehow create data class with this param and then use it in other classes. My vision of it is in screenshot

How to set operationId for endpoint?

I would like to generate client code from the generated swagger doc and use the first Tag and the operationId for naming the methods. I have found in the tests how to add tags, but I can't find anywhere how to add operationId.

Create a proper Example system

A system needs to be created that allows to provide one or more examples with their metadata without adding bulk to the minimal configuration.

Issue with testing post route with body

I wanted to create a PR that would allow for not using Unit when there are no route params (should be ready soon). However, when I was writing some unit test cases for the new code, I ran into a weird issue with the post route not being handled when the body was specified. What makes this weird is that running the same code works fine when standing up the server and sending real post requests.

This commit has a minimal server that shows the post working (ktor only, post route with Any for body, post route with actually specifying body type) and a unit test cases that should represent those same three cases, but testPost_bodyType_expectedBodyAndResponse fails.

Can you take a look? It is possible that I am missing something. I don't know enough about the ktor internals to track down where the request is being rejected. And it is odd that only the test application fails.

"throws" section is not intuitive

Continuing #17.

I've just tried it (using 0d4f589 revision) and I've come up with the following:

Normal version
data class SuccessResult(val ok: Boolean) {

    companion object {

        val NOT_OK = SuccessResult(ok = false)
        val OK = SuccessResult(ok = true)
    }
}

data class ProductUsable(val name: String, val id: Int, val type: Int)

fun NormalOpenAPIRoute.addRoutes() {
    route("product") {
        post<Unit, SuccessResult, ProductUsable>(
            info(
                summary = "Create a product.",
                description = "The product is created only if a product with the same ID does not exist. Returns `${SuccessResult::class.simpleName}` saying whether the product has been created."
            ),
            exampleResponse = SuccessResult.OK,
            exampleRequest = exampleProductUsable,
            body = { _, body -> createProduct(body, this::respond) }
        )

        // ...
    }
}
"Throws" version
data class SuccessResult(val ok: Boolean) {

    companion object {

        val NOT_OK = SuccessResult(ok = false)
        val OK = SuccessResult(ok = true)
    }
}

data class ProductUsable(val name: String, val id: Int, val type: Int)

private fun <T : OpenAPIRoute<T>> T.throwsSuccessResultNotOk(fn: T.() -> Unit) {
    throws(
        APIException.apiException(
            status = HttpStatusCode.BadRequest,
            example = SuccessResult.NOT_OK,
            gen = { _: Throwable -> SuccessResult.NOT_OK }
        ),
        fn = fn
    )
}

fun NormalOpenAPIRoute.addRoutes() {
    route("product") {
        throwsSuccessResultNotOk {
            post<Unit, SuccessResult, ProductUsable>(
                info(
                    summary = "Create a product.",
                    description = "The product is created only if a product with the same ID does not exist. Returns `${SuccessResult::class.simpleName}` saying whether the product has been created."
                ),
                exampleResponse = SuccessResult.OK,
                exampleRequest = exampleProductUsable,
                body = { _, body -> createProduct(body, this::respond) }
            )

            // ...
        }
    }
}

The usability problems I see:

  1. The generated exceptional response example doesn't match my NOT_OK (which is false):
See the image

image

  1. When the request is missing some primitive fields, zeros are inserted. Here are some examples:

    1. Request: ProductUsable(name = "a", id = 5, type = 42)
      Expected response: 200 OK SuccessResult.OK
      Actual behavior in normal version: as expected
      Actual behavior in "throws" version: as expected
    2. Request: ProductUsable(name = "a", type = 42)
      Expected response: 400 Bad Request SuccessResult.NOT_OK (because id is missing)
      Actual behavior in normal version: 500 internal server error
      Actual behavior in "throws" version: 200 OK SuccessResult.OK (transformed as ProductUsable(name = "a", id = 0, type = 42))
    3. Request: ProductUsable(id = 5, type = 42)
      Expected response: 400 Bad Request SuccessResult.NOT_OK
      Actual behavior in normal version: 500 internal server error
      Actual behavior in "throws" version: as expected (maybe because a string is not a primitive type)
  2. "Gen" function looks duplicating logic of the "example".

  3. I don't understand which exception I should specify in "gen" function.

Finally, I propose a bit different design. It's inspired by exception block and I think it solves the 3 and the 4:

// inspired by:
//exception<JsonMappingException, Error>(HttpStatusCode.BadRequest) {
//    it.printStackTrace()
//    Error("mapping.json", it.localizedMessage)
//}

fun NormalOpenAPIRoute.addRoutes() {
    route("product") {
        catches<JsonMappingException, SuccessResult>(  // means that when JsonMappingException happens (can't generate the request body object), "generator" is called
            status = HttpStatusCode.BadRequest,
            example = SuccessResult.NOT_OK,
            generator = null  // means that the example is returned
        ) {
            post<Unit, SuccessResult, ProductUsable>(
                info(
                    summary = "Create a product.",
                    description = "The product is created only if a product with the same ID does not exist. Returns `${SuccessResult::class.simpleName}` saying whether the product has been created."
                ),
                exampleResponse = SuccessResult.OK,
                exampleRequest = exampleProductUsable,
                body = { _, body -> createProduct(body, this::respond) }
            )

            // ...
        }
    }
}

Feature: allow to name namespace

Namespace can have a name and a description (for example, api - Default namespace):

image

However, in Ktor-OpenAPI-Generator, I have only default and I have no idea if this is possible to change now:

image

Models: `minimum: null/maximum: null` for `string` and `boolean` seem redundant

In the models section, there are redundant fields:

image

Current workaround is activating mapper.setSerializationInclusion(Include.NON_NULL) for Jackson.

However, it changes the way the server answers, so it's not a solution in some cases.

I think the solution can be just to omit generating minimum and maximum for string and boolean in the models section.

This is an issue extracted from #12.

Can't catch errors of path params

When I do http://localhost:8080/my/dassda , I want to see a bad request, but I get I received 0. Revision I use is be025c9. Is there a way to handle bad params?

@Path("{iii}")
data class MyParam(@PathParam("I'm int") val iii: Int)

route("my") {
    throws(
        status = HttpStatusCode.BadRequest,
        example = "bad request",
        exClass = Throwable::class
    ) {
        get<MyParam, String> { param ->
            respond("I received ${param.iii}")
        }
    }
}

incorrect 'throws' rendering

throws(APIException.apiException<ApiError, String>(HttpStatusCode.BadRequest, ""), fn = fn)
This code have that result for me
image

@Path not setting proper route

I am using @path like

@Path("/{deviceId}")
data class MyParams(
    @PathParam("Device ID") val deviceId: String
)

When i use it with route, proper openapi definitions are created but ktor route is not working. I listed routes and checked... It doesn't show as parameterized route and also none of post method works...

route("images") {
                post<MyParams, PResponse, PRequest>(
                    info("Add to server", "Add link")
           ) { pr, req ->
                    respond(pr(true))
          }

route listing on ktor show

19-04-2020 17:47:04.633 [main] INFO  Application - route: /(method:GET)
19-04-2020 17:47:04.633 [main] INFO  Application - route: /(authenticate "default")/users/agent/(method:GET)
19-04-2020 17:47:04.633 [main] INFO  Application - route: /(authenticate "default")/images/(method:POST)

Sending port request to images/1 or images/ routes produces error 404

19-04-2020 18:00:01.192 [nioEventLoopGroup-4-3] INFO  Application - Unhandled: POST - /images/
19-04-2020 18:00:32.333 [nioEventLoopGroup-4-3] INFO  Application - Unhandled: POST - /images/1

Why route is shown unhandled and it is generated as above without parameters

"Cannot inline bytecode" error in sample

Hi! I've just copy-pasted the sample and gotten this:

image

The solution has been to update my build.gradle specifying jvmTarget explicitly: https://stackoverflow.com/a/50991772.

Actually, I've never had such an issue with other libs. Maybe there is a way to change the build script or something else somehow to ease the lib usage... Or maybe just giving a warning in README is enough ๐Ÿ˜€

How to auth

It seems that the info in #8 (comment) is irrelevant: classes and functions are missing...

I want to make my own OAuth service to be used by the main service to verify requests.

Can't respond with custom status dynamically

Hi! I don't understand how to respond with a custom status code, for example, 404, depending on the situation.

I try the following: I declare an interface and set it as a response type. Then I create some children of the interface with specified status codes. Example:

interface IResponse {

    val myField: Boolean
}

@Response(statusCode = 200)
object MyGoodResponse : IResponse {

    override val myField = true
}

@Response(statusCode = 404)
object MyBadResponse : IResponse {

    override val myField = false
}

// ...

route("a") {
    get<Unit, IResponse> {
        if (System.currentTimeMillis() % 2 == 0L) {
            respond(MyGoodResponse)
        } else {
            respond(MyBadResponse)
        }
    }
}

I want to see 404, but it's 200:

image

Is it supported now?

If you want to take a look a real-world example, please fresh start this revision of my project and call GET http://localhost:8080/v1/product/0, the status should be 404 but it returns OK and this test fails: https://github.com/SerVB/e-shop/blob/37958c8bb4fee8a42dad229c578f79867c1b5d74/server/src/test/kotlin/io/github/servb/eShop/product/route/singleOperation/EShopProductReturnProductTest.kt .

Why are you using 'crossinline' modifier in body param of NormalOpenApiRoute.route method

I am making DSL for your framework and when I'm trying to make wrapper function the method and getting an internal JVM exception cause of invalid bytecode generation
image
I haven't found the real solution, but in my case changing 'crossinline' modifier to 'noinline' helped, can you ask, why are you using the first one ?
or may be even can you replace crossinline into noinine ?

Multiple responses

Another thing I just ran into and I am not sure on how to specify, what if my endpoint has different types it can return and multiple response codes?

Top be precise for that endpoint we return data class ErrorResponse(val message: String) in cases where we return a 400, 404 and 500 status but not sure how I can define that.

Further when we may return a 201 with en empty response. How can this all be defined? It is not quite clear to me from the existing examples.

Sets not handled properly

If I have a response class with a List<SomeEnum> field, the generated entity looks like:

TheEntity:
  type: object
  properties:
    test:
      type: array
      items:
        $ref: "#/components/schemas/TheEnum"

TheEnum:
  enum: ["A","B","C"]
  nullable: false
  type: string

That looks pretty good, but if instead I had Set<SomeEnum>, I get:

TheEntity:
  type: object
  properties:
    test:
      $ref: "#/components/schemas/TheEnum"

Set_TheEnum_:
  type: object
  properties:
    size:
      type: integer
      format: int32
      nullable: false
      minimum: -2147483648
      maximum: 2147483647
  required: ["size"]
  nullable: false

I think the expected schema should have looked like:

TheEntity:
  type: object
  properties:
    test:
      type: array
      items:
        $ref: "#/components/schemas/TheEnum"
      uniqueItems: true               # <----- very important for sets

TheEnum:
  enum: ["A","B","C"]
  nullable: false
  type: string

In the source code, I saw a few places with clazz.isSubclassOf(List::class), not sure but I'm thinking the fix involves changing those places.

Missing @Example annotation

In the models section, there is a field called "example". Currently it is equal to null for all fields in all classes:

image

There should be a way to specify this field. Also, I think if the field is not specified, it shouldn't be presented in the models section.

This is an issue extracted from #12.

Empty list doesn't contain element at index 0

Sometimes when I run tests, I get the following exception for all tests:

Stacktrace
java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 0.
	at kotlin.collections.EmptyList.get(Collections.kt:35)
	at kotlin.collections.EmptyList.get(Collections.kt:23)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultMapSchemaProvider$Builder.build(DefaultMapSchemaProvider.kt:22)
	at com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProvider$Builder.build(FinalSchemaBuilderProvider.kt:70)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultObjectSchemaProvider$Builder$build$1.invoke(DefaultObjectSchemaProvider.kt:50)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultObjectSchemaProvider$Builder$build$1.invoke(DefaultObjectSchemaProvider.kt:30)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultObjectSchemaProvider$Builder.build(DefaultObjectSchemaProvider.kt:39)
	at com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProvider$Builder.build(FinalSchemaBuilderProvider.kt:70)
	at com.papsign.ktor.openapigen.schema.builder.FinalSchemaBuilder$DefaultImpls.build$default(FinalSchemaBuilder.kt:8)
	at com.papsign.ktor.openapigen.content.type.ktor.KtorContentProvider.getMediaType(KtorContentProvider.kt:63)
	at com.papsign.ktor.openapigen.modules.handlers.ThrowOperationHandler.configure(ThrowOperationHandler.kt:27)
	at com.papsign.ktor.openapigen.modules.handlers.RouteHandler.configure(RouteHandler.kt:32)
	at io.github.servb.eShop.handler.product.v1.CreateProductKt.createProduct(createProduct.kt:169)
	at io.github.servb.eShop.route.product.v1.AddProductV1RoutesKt.addProductV1Routes(addProductV1Routes.kt:7)
	at io.github.servb.eShop.ApplicationKt$module$4.invoke(Application.kt:106)
	at io.github.servb.eShop.ApplicationKt$module$4.invoke(Application.kt)
	at com.papsign.ktor.openapigen.route.RouteConfigKt$apiRouting$1.invoke(RouteConfig.kt:17)
	at com.papsign.ktor.openapigen.route.RouteConfigKt$apiRouting$1.invoke(RouteConfig.kt)
	at io.ktor.routing.RoutingKt.routing(Routing.kt:120)
	at com.papsign.ktor.openapigen.route.RouteConfigKt.apiRouting(RouteConfig.kt:13)
	at io.github.servb.eShop.ApplicationKt.module(Application.kt:89)
	at io.github.servb.eShop.product.InMemoryEShopProductKt.inMemoryEShopProduct(inMemoryEShopProduct.kt:6)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1$1.invoke(EShopProductCreateProductTest.kt:20)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1$1.invoke(EShopProductCreateProductTest.kt:18)
	at io.ktor.server.testing.TestEngineKt$withTestApplication$1.invoke(TestEngine.kt:67)
	at io.ktor.server.testing.TestEngineKt$withTestApplication$1.invoke(TestEngine.kt)
	at io.ktor.server.testing.TestEngineKt.withApplication(TestEngine.kt:49)
	at io.ktor.server.testing.TestEngineKt.withApplication$default(TestEngine.kt:43)
	at io.ktor.server.testing.TestEngineKt.withTestApplication(TestEngine.kt:66)
	at io.github.servb.eShop.util.KotlinFutureKt.withTestApplication(KotlinFuture.kt:13)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1.invokeSuspend(EShopProductCreateProductTest.kt:20)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1.invoke(EShopProductCreateProductTest.kt)
	at io.kotest.core.spec.style.BehaviorSpecDsl$addGivenContext$1.invokeSuspend(behaviorSpecDsl.kt:25)
	at io.kotest.core.spec.style.BehaviorSpecDsl$addGivenContext$1.invoke(behaviorSpecDsl.kt)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2$1.invokeSuspend(executions.kt:16)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2$1.invoke(executions.kt)
	at io.kotest.core.AsserterKt.executeWithGlobalAssertSoftlyCheck(Asserter.kt:37)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2.invokeSuspend(executions.kt:16)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2.invoke(executions.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturnIgnoreTimeout(Undispatched.kt:102)
	at kotlinx.coroutines.TimeoutKt.setupTimeout(Timeout.kt:78)
	at kotlinx.coroutines.TimeoutKt.withTimeout(Timeout.kt:31)
	at io.kotest.core.runtime.ExecutionsKt.executeWithTimeout--MKxnPQ(executions.kt:13)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1$3.invokeSuspend(TestExecutor.kt:181)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1$3.invoke(TestExecutor.kt)
	at io.kotest.core.runtime.ReplayKt.replay(replay.kt:19)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1.invokeSuspend(TestExecutor.kt:176)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1.invoke(TestExecutor.kt)
	at io.kotest.core.runtime.ExecutorExecutionContext$executeWithTimeoutInterruption$$inlined$suspendCoroutine$lambda$2.invokeSuspend(ExecutorExecutionContext.kt:47)
	(Coroutine boundary)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1$3.invokeSuspend(TestExecutor.kt:181)
	at io.kotest.core.runtime.ReplayKt.replay(replay.kt:19)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1.invokeSuspend(TestExecutor.kt:180)
Caused by: java.lang.IndexOutOfBoundsException: Empty list doesn't contain element at index 0.
	at kotlin.collections.EmptyList.get(Collections.kt:35)
	at kotlin.collections.EmptyList.get(Collections.kt:23)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultMapSchemaProvider$Builder.build(DefaultMapSchemaProvider.kt:22)
	at com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProvider$Builder.build(FinalSchemaBuilderProvider.kt:70)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultObjectSchemaProvider$Builder$build$1.invoke(DefaultObjectSchemaProvider.kt:50)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultObjectSchemaProvider$Builder$build$1.invoke(DefaultObjectSchemaProvider.kt:30)
	at com.papsign.ktor.openapigen.schema.builder.provider.DefaultObjectSchemaProvider$Builder.build(DefaultObjectSchemaProvider.kt:39)
	at com.papsign.ktor.openapigen.schema.builder.provider.FinalSchemaBuilderProvider$Builder.build(FinalSchemaBuilderProvider.kt:70)
	at com.papsign.ktor.openapigen.schema.builder.FinalSchemaBuilder$DefaultImpls.build$default(FinalSchemaBuilder.kt:8)
	at com.papsign.ktor.openapigen.content.type.ktor.KtorContentProvider.getMediaType(KtorContentProvider.kt:63)
	at com.papsign.ktor.openapigen.modules.handlers.ThrowOperationHandler.configure(ThrowOperationHandler.kt:27)
	at com.papsign.ktor.openapigen.modules.handlers.RouteHandler.configure(RouteHandler.kt:32)
	at io.github.servb.eShop.handler.product.v1.CreateProductKt.createProduct(createProduct.kt:169)
	at io.github.servb.eShop.route.product.v1.AddProductV1RoutesKt.addProductV1Routes(addProductV1Routes.kt:7)
	at io.github.servb.eShop.ApplicationKt$module$4.invoke(Application.kt:106)
	at io.github.servb.eShop.ApplicationKt$module$4.invoke(Application.kt)
	at com.papsign.ktor.openapigen.route.RouteConfigKt$apiRouting$1.invoke(RouteConfig.kt:17)
	at com.papsign.ktor.openapigen.route.RouteConfigKt$apiRouting$1.invoke(RouteConfig.kt)
	at io.ktor.routing.RoutingKt.routing(Routing.kt:120)
	at com.papsign.ktor.openapigen.route.RouteConfigKt.apiRouting(RouteConfig.kt:13)
	at io.github.servb.eShop.ApplicationKt.module(Application.kt:89)
	at io.github.servb.eShop.product.InMemoryEShopProductKt.inMemoryEShopProduct(inMemoryEShopProduct.kt:6)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1$1.invoke(EShopProductCreateProductTest.kt:20)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1$1.invoke(EShopProductCreateProductTest.kt:18)
	at io.ktor.server.testing.TestEngineKt$withTestApplication$1.invoke(TestEngine.kt:67)
	at io.ktor.server.testing.TestEngineKt$withTestApplication$1.invoke(TestEngine.kt)
	at io.ktor.server.testing.TestEngineKt.withApplication(TestEngine.kt:49)
	at io.ktor.server.testing.TestEngineKt.withApplication$default(TestEngine.kt:43)
	at io.ktor.server.testing.TestEngineKt.withTestApplication(TestEngine.kt:66)
	at io.github.servb.eShop.util.KotlinFutureKt.withTestApplication(KotlinFuture.kt:13)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1.invokeSuspend(EShopProductCreateProductTest.kt:20)
	at io.github.servb.eShop.product.route.singleOperation.EShopProductCreateProductTest$1$1.invoke(EShopProductCreateProductTest.kt)
	at io.kotest.core.spec.style.BehaviorSpecDsl$addGivenContext$1.invokeSuspend(behaviorSpecDsl.kt:25)
	at io.kotest.core.spec.style.BehaviorSpecDsl$addGivenContext$1.invoke(behaviorSpecDsl.kt)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2$1.invokeSuspend(executions.kt:16)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2$1.invoke(executions.kt)
	at io.kotest.core.AsserterKt.executeWithGlobalAssertSoftlyCheck(Asserter.kt:37)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2.invokeSuspend(executions.kt:16)
	at io.kotest.core.runtime.ExecutionsKt$executeWithTimeout$2.invoke(executions.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturnIgnoreTimeout(Undispatched.kt:102)
	at kotlinx.coroutines.TimeoutKt.setupTimeout(Timeout.kt:78)
	at kotlinx.coroutines.TimeoutKt.withTimeout(Timeout.kt:31)
	at io.kotest.core.runtime.ExecutionsKt.executeWithTimeout--MKxnPQ(executions.kt:13)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1$3.invokeSuspend(TestExecutor.kt:181)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1$3.invoke(TestExecutor.kt)
	at io.kotest.core.runtime.ReplayKt.replay(replay.kt:19)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1.invokeSuspend(TestExecutor.kt:176)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2$1.invoke(TestExecutor.kt)
	at io.kotest.core.runtime.ExecutorExecutionContext$executeWithTimeoutInterruption$$inlined$suspendCoroutine$lambda$2.invokeSuspend(ExecutorExecutionContext.kt:47)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:272)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at io.kotest.core.runtime.ExecutorExecutionContext.executeWithTimeoutInterruption-D5N0EJY(ExecutorExecutionContext.kt:46)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2.invokeSuspend(TestExecutor.kt:168)
	at io.kotest.core.runtime.TestExecutor$executeAndWait$2.invoke(TestExecutor.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
	at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:177)
	at io.kotest.core.runtime.TestExecutor.executeAndWait-xkB6VbI(TestExecutor.kt:165)
	at io.kotest.core.runtime.TestExecutor.invokeTestCase(TestExecutor.kt:149)
	at io.kotest.core.runtime.TestExecutor.executeActiveTest(TestExecutor.kt:116)
	at io.kotest.core.runtime.TestExecutor$intercept$2.invokeSuspend(TestExecutor.kt:73)
	at io.kotest.core.runtime.TestExecutor$intercept$2.invoke(TestExecutor.kt)
	at io.kotest.core.runtime.TestExecutor.executeIfActive(TestExecutor.kt:85)
	at io.kotest.core.runtime.TestExecutor.intercept(TestExecutor.kt:73)
	at io.kotest.core.runtime.TestExecutor.execute(TestExecutor.kt:54)
	at io.kotest.core.engine.SingleInstanceSpecRunner.runTest(SingleInstanceSpecRunner.kt:63)
	at io.kotest.core.engine.SingleInstanceSpecRunner$execute$2.invokeSuspend(SingleInstanceSpecRunner.kt:74)
	at io.kotest.core.engine.SingleInstanceSpecRunner$execute$2.invoke(SingleInstanceSpecRunner.kt)
	at io.kotest.core.engine.SingleInstanceSpecRunner$execute$3.invokeSuspend(SingleInstanceSpecRunner.kt:80)
	at io.kotest.core.engine.SingleInstanceSpecRunner$execute$3.invoke(SingleInstanceSpecRunner.kt)
	at kotlinx.coroutines.intrinsics.UndispatchedKt.startUndispatchedOrReturn(Undispatched.kt:91)
	at kotlinx.coroutines.CoroutineScopeKt.coroutineScope(CoroutineScope.kt:177)
	at io.kotest.core.engine.SingleInstanceSpecRunner.execute(SingleInstanceSpecRunner.kt:78)
	at io.kotest.core.engine.SpecExecutor$runTests$run$1.invokeSuspend(SpecExecutor.kt:105)
	at io.kotest.core.engine.SpecExecutor$runTests$run$1.invoke(SpecExecutor.kt)
	at io.kotest.core.engine.SpecExecutor.interceptSpec(SpecExecutor.kt:117)
	at io.kotest.core.engine.SpecExecutor.runTests(SpecExecutor.kt:108)
	at io.kotest.core.engine.SpecExecutor.execute(SpecExecutor.kt:36)
	at io.kotest.core.engine.KotestEngine$submitBatch$$inlined$forEach$lambda$1$1.invokeSuspend(KotestEngine.kt:78)
	at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
	at kotlinx.coroutines.DispatchedTask.run(DispatchedTask.kt:56)
	at kotlinx.coroutines.EventLoopImplBase.processNextEvent(EventLoop.common.kt:272)
	at kotlinx.coroutines.BlockingCoroutine.joinBlocking(Builders.kt:79)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking(Builders.kt:54)
	at kotlinx.coroutines.BuildersKt.runBlocking(Unknown Source)
	at kotlinx.coroutines.BuildersKt__BuildersKt.runBlocking$default(Builders.kt:36)
	at kotlinx.coroutines.BuildersKt.runBlocking$default(Unknown Source)
	at io.kotest.core.engine.KotestEngine$submitBatch$$inlined$forEach$lambda$1.run(KotestEngine.kt:77)
	at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515)
	at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264)
	at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1128)
	at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:628)
	at java.base/java.lang.Thread.run(Thread.java:834)

Could you investigate?

Missing documentation and weird usage

I really like this project, however, not only is documentation lacking, but the examples seem broken/half-baked (eg TestServer.kt's APIPrincipal) and some usages are far from understandable.

For example, I just (literally) got a headache trying to understand how the StatusPages syntax works.

Let's give the original usage a look:

exception<Throwable> { cause ->      // easy, when Throwable is caught, InternalServerError is returned
    call.respond(HttpStatusCode.InternalServerError)
}

And the new usage:

exception<ProperException, Error>(HttpStatusCode.BadRequest) {
     // ^ when ProperException is thrown (what's a ProperException???), and... Error??? (parameter "B"... very useful..) then we have hard-coded status code?
    it.printStackTrace()
    Error(it.id, it.localizedMessage)  // and for some reason, return an Error
}

Similarly, the "basic" example in the readme also falls short in explaining anything and is hard to grasp:

    //bare minimum, just like Ktor but strongly typed
    get<StringParam, StringResponse> { params ->               // ok... get what exactly? where are we?
        respond(StringResponse(params.a))
    }

    route("inine").get<StringParam, StringResponse>(      // what's "inline"??
        info("String Param Endpoint", "This is a String Param Endpoint"), // A Route module that describes an endpoint, it is optional
        example = StringResponse("Hi")
    ) { params ->
        respond(StringResponse(params.a))   //  what's params and why do we use params.a???
    }

If writing proper examples and tests is too much effort, at least using better named symbols helps.

(to put some context, I spent half a day trying to convert two of my existing endpoints and spent most of the time digging in examples and internal source code trying to figure things out)

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.