GithubHelp home page GithubHelp logo

derveloper / kotlin-openapi3-dsl Goto Github PK

View Code? Open in Web Editor NEW
65.0 65.0 6.0 567 KB

Build your OpenApi3 spec in kotlin!

License: Apache License 2.0

Kotlin 100.00%
dsl java kotlin openapi3 rest-api swagger

kotlin-openapi3-dsl's Introduction

Hi there πŸ‘‹

I'm Tristan and I'm working as a IT Security & Site Reliability Lead @sipgate.

  • πŸ’¬ Ask me about IaC and DevOps in general
  • 🌱 I’m currently learning rust
  • πŸ˜„ Pronouns: he/him
  • πŸ“« How to reach me: 🐦 @derveloper

kotlin-openapi3-dsl's People

Contributors

derveloper avatar renovate-bot avatar renovate[bot] avatar richardskg avatar travisagengler 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

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar

kotlin-openapi3-dsl's Issues

How do we provide descriptions for parameters?

For a data class defined as follows,

import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.media.Schema

// ...

@Schema(description = "Test DateWrapper Description")
   data class DateWrapper(
       @Parameter(description = "test description")
       val startDate: LocalDate,
       val endDate: LocalDate? = null,
)

the output of the json is as follows.

"DateWrapper": {
        "description": "Test DateWrapper Description",
        "type": "object",
        "properties": {
          "endDate": {
            "format": "date",
            "type": "string"
          },
          "startDate": {
            "format": "date",
            "type": "string"
          },
        }
      },

I am trying to provide a description for the Property (startDate) in the Schema. Could you provide some guidance on how to do achieve this? I am looking for an output as follows:

"DateWrapper": {
        "description": "Test DateWrapper Description",
        "type": "object",
        "properties": {
          "endDate": {
            "format": "date",
            "type": "string"
          },
          "startDate": {
            "format": "date",
            "type": "string",
            "description" : "test description"
          },
        }
      },

Support ByteArray in findSchema

Currently the "ByteArray" is not supported by the "findSchema" function.

I propose the add the following mapping

when (clazz) {
  // ...
  ByteArray::class-> StringSchema().format("binary")
  // ...
}

This would be compliant with the documentation.

I could create a PR for this if the solution is accepted.

[Feature] Configure required properties based on Kotlin null safety.

I am currently using the following to define required properties from Kotlin data classes:

data class CarView(
    @field:Schema(required = true) val label: String,
    val description: String?,
    )

It should be great if we can avoid all this boilerplate by expressing the requirement with Kotlin Null Safety. So in previous code we do not need to annotate every field.

I think it could be expressed in the function findSchema

Alternative to inline functions

Problem

The inline functions have the disadvantage that they can just be called from other inline functions. If you have class as a variable (for example from a java interopt) you currently cannot use a lot of the DSL.

Example:

fun myFunction(kClass: KClass<*>) {
  findSchema<kClass>(); // This is not possible
}

Proposal

For every inline function create an alternative which gets the class as a value.

As an example:

// Current
inline fun <reified T> findSchema(): Schema<*>? {
    return getEnumSchema<T>() ?: when (T::class) {
        String::class -> StringSchema()
        Boolean::class -> BooleanSchema()
        java.lang.Boolean::class -> BooleanSchema()
        Int::class -> IntegerSchema()
        Integer::class -> IntegerSchema()
        List::class -> ArraySchema()
        Long::class -> IntegerSchema().format("int64")
        BigDecimal::class -> IntegerSchema().format("")
        Date::class -> DateSchema()
        LocalDate::class -> DateSchema()
        LocalDateTime::class -> DateTimeSchema()
        else -> ModelConverters.getInstance().read(T::class.java)[T::class.java.simpleName]
    }
}
// Proposal
inline fun <reified T> findSchema(): Schema<*>? = findSchema(T::class)

fun findSchema(kClass: KClass<*>): Schema<*>? {
    return getEnumSchema(kClass) ?: when (kClass) {
        String::class -> StringSchema()
        Boolean::class -> BooleanSchema()
        java.lang.Boolean::class -> BooleanSchema()
        Int::class -> IntegerSchema()
        Integer::class -> IntegerSchema()
        List::class -> ArraySchema()
        Long::class -> IntegerSchema().format("int64")
        BigDecimal::class -> IntegerSchema().format("")
        Date::class -> DateSchema()
        LocalDate::class -> DateSchema()
        LocalDateTime::class -> DateTimeSchema()
        else -> ModelConverters.getInstance().read(kClass.java)[kClass.java.simpleName]
    }
}

If you accept the proposal I could create a PR for this.

Components.securityScheme uses type as key

The existing Components.securityScheme method registers the scheme using SecurityScheme.type. It think it might be better to use a construct like:

fun Components.securityScheme(name: String, init: SecurityScheme.() -> Unit) {
    val security = SecurityScheme()
    security.init()
    securitySchemes = securitySchemes ?: mutableMapOf()
    securitySchemes[name] = security
}

and invoke it like:

securityScheme("bearerAuth") {
    type = SecurityScheme.Type.HTTP
    scheme = "bearer"
    bearerFormat = "JWT"
 }
 securityScheme("apiKeyAuth") {
    type = SecurityScheme.Type.APIKEY
    `in` = SecurityScheme.In.HEADER
    name = "X-API-Key"
}

This would produce json that is valid according to https://editor.swagger.io/:

   "securitySchemes": {
      "apiKeyAuth": {
        "in": "header",
        "name": "X-API-Key",
        "type": "apiKey"
      },
      "bearerAuth": {
        "bearerFormat": "JWT",
        "scheme": "bearer",
        "type": "http"
      }
    }

With regard to a comment in the code base, I donΒ΄t think it is correct to use SecurityScheme.name since it has another meaning.

Dependency Dashboard

This issue lists Renovate updates and detected dependencies. Read the Dependency Dashboard docs to learn more.

This repository currently has no open or pending branches.

Detected dependencies

github-actions
.github/workflows/gradle.yml
  • actions/checkout v4@a5ac7e51b41094c92402da3b24376905380afc29
  • actions/setup-java v4
.github/workflows/publish.yml
  • actions/checkout v4@a5ac7e51b41094c92402da3b24376905380afc29
  • actions/setup-java v4
gradle
settings.gradle
build.gradle
  • org.jetbrains.kotlin:kotlin-gradle-plugin 2.0.0
  • io.github.gradle-nexus.publish-plugin 2.0.0
  • org.jetbrains.kotlin:kotlin-stdlib 2.0.0
  • com.fasterxml.jackson.core:jackson-core 2.17.1
  • com.fasterxml.jackson.core:jackson-databind 2.17.1
  • com.fasterxml.jackson.module:jackson-module-jsonSchema 2.17.1
  • com.fasterxml.jackson.datatype:jackson-datatype-json-org 2.17.1
  • io.swagger.core.v3:swagger-core 2.2.22
  • io.swagger.parser.v3:swagger-parser 2.1.22
  • org.jetbrains.kotlin:kotlin-reflect 2.0.0
  • io.kotlintest:kotlintest-runner-junit5 3.4.2
  • org.slf4j:slf4j-simple 2.0.13
gradle-wrapper
gradle/wrapper/gradle-wrapper.properties
  • gradle 8.8

  • Check this box to trigger a request for Renovate to run again on this repository

swagger libraries at runtime

With this updated version, my application is no longer compiling the swagger classes because they are being used at runtime now. I see in your gradle file that you changed them from 'compile' to 'implementation'. In my maven dependency tree it used to be 'io.swagger.core.v3:swagger-core...:compile'. Now it says 'io.swagger.core.v3:swagger-core...:runtime. Would you be wiling to change libraries back to compile scope instead of runtime scope. If not then you can go ahead and close this issue.

kotlin-openapi3-dsl plugin doesn't work with Javalin

I tried to use kotlin-openapi3-dsl plugin with Javalin according to this documentation: https://javalin.io/plugins/openapi

In my sample class here: https://github.com/pwittchen/money-transfer-api/blob/master/src/main/java/com/pwittchen/money/transfer/api/controller/AccountController.java

I added annotations as follows:

public class AccountController {

  private ContextWrapper contextWrapper;
  private AccountRepository accountRepository;

  @Inject
  public AccountController(
      final ContextWrapper contextWrapper,
      final AccountRepository accountRepository) {
    this.contextWrapper = contextWrapper;
    this.accountRepository = accountRepository;
  }

  @OpenApi(
      method = HttpMethod.GET,
      path = "/account",
      pathParams = @OpenApiParam(name = "id", type = Integer.class),
      responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Account.class))
  )
  public void getOne(Context context) {
    Optional<Account> account = accountRepository.get(contextWrapper.pathParam(context, "id"));

    if (account.isPresent()) {
      contextWrapper.json(context, account.get());
    } else {
      String message = String.format(
          "account with id %s does not exist",
          contextWrapper.pathParam(context, "id")
      );

      Response response = Response.builder()
          .message(message)
          .build();

      contextWrapper.json(context, response, 404);
    }
  }

  @OpenApi(
      method = HttpMethod.GET,
      path = "/account",
      responses = @OpenApiResponse(
          status = "200",
          content = @OpenApiContent(from = Account.class, isArray = true)
      )
  )
  public void getAll(final Context context) {
    contextWrapper.json(context, accountRepository.get());
  }

  @OpenApi(
      method = HttpMethod.POST,
      path = "/account",
      pathParams = {
          @OpenApiParam(name = "name"),
          @OpenApiParam(name = "surname"),
          @OpenApiParam(name = "currency"),
          @OpenApiParam(name = "money")
      },
      responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Response.class))
  )
  public void create(final Context context) {
    User user = createUser(context);
    Optional<Account> account = createAccount(context, user);

    if (!account.isPresent()) {
      Response response = Response.builder().message("Invalid money format").build();
      contextWrapper.json(context, response);
      return;
    }

    try {
      accountRepository.create(account.get());
      Response response = Response.builder()
          .message("account created")
          .object(account.get())
          .build();

      contextWrapper.json(context, response);
    } catch (Exception exception) {
      Response response = Response.builder().message(exception.getMessage()).build();
      contextWrapper.json(context, response);
    }
  }

  private User createUser(Context context) {
    return User.builder()
        .id(UUID.randomUUID().toString())
        .name(contextWrapper.formParam(context, "name"))
        .surname(contextWrapper.formParam(context, "surname"))
        .build();
  }

  private Optional<Account> createAccount(Context context, User user) {
    return parseMoney(context)
        .map(money -> Account.builder()
            .number(UUID.randomUUID().toString())
            .user(user)
            .money(money)
            .createdAt(LocalDateTime.now())
            .build()
        );
  }

  private Optional<Money> parseMoney(Context context) {
    try {
      return Optional.of(Money.parse(String.format("%s %s",
          contextWrapper.formParam(context, "currency"),
          contextWrapper.formParam(context, "money"))
      ));
    } catch (Exception exception) {
      return Optional.empty();
    }
  }

  @OpenApi(
      method = HttpMethod.DELETE,
      path = "/account",
      pathParams = @OpenApiParam(name = "id", type = Integer.class),
      responses = @OpenApiResponse(status = "200", content = @OpenApiContent(from = Response.class))
  )
  public void delete(Context context) {
    try {
      accountRepository.delete(contextWrapper.pathParam(context, "id"));

      String message = String.format(
          "account with number %s deleted",
          contextWrapper.pathParam(context, "id")
      );

      Response response = Response.builder()
          .message(message)
          .build();

      contextWrapper.json(context, response);
    } catch (Exception exception) {
      Response response = Response.builder().message(exception.getMessage()).build();
      contextWrapper.json(context, response);
    }
  }

and I registered plugin:

              config.registerPlugin(new OpenApiPlugin(
                      new OpenApiOptions(new Info()
                          .version("1.0")
                          .description("Money Transfer API"))
                          .path("/openapi")
                          .activateAnnotationScanningFor(
                              "com.github.pwittchen.money.transfer.api.controller"
                          )
                  )
              );

Then, when I open ReDoc, I see the following errors in the console:

[main] INFO com.pwittchen.money.transfer.api.Application - server has started
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
Aug 14, 2019 11:02:53 PM io.javalin.plugin.openapi.dsl.ExtractDocumentationKt getMethodReferenceOfNonStaticJavaHandler
WARNING: Unfortunately it is not possible to match the @OpenApi annotations to the handler in com.pwittchen.money.transfer.api.controller.AccountController. Please add the `path` and the `me
thod` information to the annotation, so the handler can be matched.
[qtp728885526-17] INFO com.pwittchen.money.transfer.api.Application - 463.79935 ms       GET   /openapi 

It says, I should add path and method to the annotations, but I already did it!

I did everything what documentation says, but I didn't get expected result. I tried to use this without annotations, but it didn't work either.

Documentation with ReDoc works in general, but I'm not able to document exact response values and types and other details.

Consider using a DSLMarker annotation

I think your DSL could benefit greatly from using a DSLMarker annotation.

Consider defining an annotation like:

@Target(AnnotationTarget.CLASS, AnnotationTarget.TYPE, AnnotationTarget.FUNCTION)
@DslMarker
annotation class OpenApiDsl

Then annotate the API receiver lambdas like:

fun OpenAPI.paths(init: @OpenApiDsl Paths.() -> Unit) {
    paths = paths ?: Paths()
    paths.init()
}

Sadly, this could potentially break existing code.

Output properties order are not consistent.

I have found an issue while creating an OpenAPI definition with this library.

I define my operation responses as:

               responses {
                    response("204") {
                        description = "..."
                    }
                    responses.putAll(standardResponses)
                }

Where standardResponses is a collection of common responses. The responses method is a builder for ApiResponses class from swagger that implement LinkedHashMap so the order should be preserved.

Everything should work but the output does not keep the same order I used while inserting the responses, the cuprit seems to be the JSONObject used for formatting:

internal fun validatedJson(api: OpenAPI): JSONObject {
val json = Json.mapper().writeValueAsString(api)
OpenAPIV3Parser().read(toFile(json).absolutePath)
return JSONObject(json)
}

Looks like there is a debate about this problem at the moment, see issue

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.