ksp, kapt or reflection? I am looking to add OpenA...
# ksp
v
ksp, kapt or reflection? I am looking to add OpenAPI specs to my custom API routing library. I'd like to generate the yaml representation of the classes used in request and response bodies -
schemas
in OpenAPI parlance. The user of the library would annotate a data class with
@APISchema
and my library would generate the appropriate yaml. I've tried this before, using KSP, but it was very hacky and it wasn't a separate library. So I am not sure what is the best way - a kapt plugin to generate the yaml, or ksp, or maybe just do runtime reflection? (I'm running this in AWS lambda and reflection seems to be a bad idea). Some sample code in thread...
Users of my library would declare HTTP routes in a similar fashion to ktor, spark, etc:
Copy code
override val router: Router = lambdaRouter {
        get("/warm") { _: Request<Unit> ->
            println("NEW API: Ping received; warming"); Response.ok(
            "Warmed"
        )
        }.supplies(MimeType.plainText)

        put("/wibble") { request: Request<Wibble> -> { println("Wibble: ${request.body}") }; Response.ok(
                    "Wibble"
                ) }.expects(MimeType.json)

        group("/project") {
            auth(cognitoJWTAuthorizer) {
                get("/load/{projectKey}", projectController::getProject)
             }
        }
}
Where
Wibble
would be a data class with my
APISchema
annotation.
So ultimately, at runtime, I'd like to be able to have a route
get("/openAPI") { request<Unit> -> Response.ok(router.openAPI()) }.supplies(MimeType.YAML)
which would, at run time, provide a complete OpenAPI Yaml specification for all the routes defined in the
lambdaRouter {...}
block.
The final output of the schema object would be something like:
Copy code
schemas:
  Wibble:
    type: object
    properties:
      id:
        type: integer
      name:
        type: string
j
it looks like you need to get the value of
Wibble
inside a lambda body? If that’s the case, KSP won’t work, I am afraid kapt won’t work either.
v
Ah, bother. And now I am having flashbacks to when I tried this before a year ago. Seems a weakness of ksp and kapt not to support one of Kotlin's key features. Could I do it with runtime reflection?
j
I am not sure reflection can help either.
v
(I don't need the value, I just need to know the kclass of the Request and Response declarations in the lambda).
y
i actually did something very similar, for an open-ai action endpoint 😄. I ended up doing reflection, way easier when you are running on the server side (especially you can also cache it so the cost is not significant)
👍 1
this is all generated: https://iracing-openapi.wl.r.appspot.com/actionApi/schema though i stopped playing w/ it and I don’t have access to my personal machine to share more details. basically, even if your API is lambdas, it ends up in a data structure inside your runtime so it is rather easy to build a schema out of it (then reflection for documentation etc)
v
I can totally see that KSP cannot help me with the lambda function like:
Copy code
put("/wibble") { request: Request<Wibble> -> Response.OK("Wibble created") }
And I've accepted that. However, most of the time I won't be using trailing lambdas to declare my API routes - instead, I'd be passing a named function:
Copy code
put("/wibble", wibbleController::createNewWibble)
That function would be declared separately, something like this:
Copy code
class WibbleController {
@OpenAPIRoute 
fun createNewWibble(request: Request<Wibble>): Response<String> {
  // create a new Wibble in the database...
  return Response("Wibble created")
 }
}
And with that
OpenAPIRoute
annotation on that function body, KSP can get me the information I need about the
Request<Wibble>
and the
Response<String>
. tl;dr - I can get KSP to do what I want for the more realistic scenario, but it cannot and well never help with the trailing lambda scenario. I think I can live with that.
👍 2
y
i checked, this is how my API methods look like:
Copy code
@Get("/driver/profile/{id}")
    suspend fun searchDrivers(
        @Doc("The customer id of the driver")
        id: Int
    ): IRDriverProfile? {
        return repo.getDriverProfile(id)
    }
@Doc
is a custom annotation to adds documentation to the open-api schema. the response types all have the same annotation in their properties. to generate the schema, i simply reflect through all methods and traverse them. and the same code also adds them as routes to ktor
h
I write a similar library for OpenAPI generation and I use KSP due multiplatform support. But it’s a multiple step generator: 1. you write a function 2. You annotate your function parameters with the body/request parameters 3. KSP extracts the parameters, return type, kdoc (if it’s in the same module), error type using Throws 4. then you compose multiple functions into one service (using a custom dsl), and all parameters are merged into one endpoint 5. This endpoint is defined in the OpenAPI document 6. based on the open api document, the client will be created 7. With the information of the custom dsl, the server will be generated calling the original function from step 1