Custom Frameworks

The framework layer of genhttp-kt allows application frameworks (such as webservices or controllers) to host a set of operations (put simply a route with a piece of Kotlin code to be executed). This way the application framework itself is only a very thin layer - it analyzes the given scope (such as a controller class) and derives operations from it which are then handled by the generic framework part. This way, all application frameworks share the same feature level and can support things like parameter binding, serialization, suspend functions or execution modes. To give an example, the handler which provides the whole webservice framework consists of about 60 lines of code only - everything else is done by the underlying framework layer.

In essence, the framework layer allows you to bind a dynamic bunch of Kotlin functions to HTTP endpoints with some granularity to configure how they are exposed. Examples could be a framework mapping database access to REST or a framework to emulate file-based endpoints as we have with PHP.

The reflection module is currently considered an internal dependency of the webservice frameworks, so we do not guarantee compatibility between versions as we do with the public API surface of genhttp-kt.

Implementing a Custom Framework

To give an example, we will create a simple handler that creates some operations and delegates their execution to a MethodCollection handler.

Kotlin
import org.genhttp.api.content.Handler
import org.genhttp.api.infrastructure.Server
import org.genhttp.api.protocol.Request
import org.genhttp.api.protocol.Response
import org.genhttp.modules.conversion.Formatting
import org.genhttp.modules.conversion.Serialization
import org.genhttp.modules.reflection.ExecutionMode
import org.genhttp.modules.reflection.ExecutionSettings
import org.genhttp.modules.reflection.Injection
import org.genhttp.modules.reflection.Method
import org.genhttp.modules.reflection.MethodAttribute
import org.genhttp.modules.reflection.MethodCollection
import org.genhttp.modules.reflection.MethodHandler
import org.genhttp.modules.reflection.MethodRegistry
import org.genhttp.modules.reflection.ServiceMethodProvider
import org.genhttp.modules.reflection.operations.OperationBuilder

class CustomFrameworkHandler : Handler, ServiceMethodProvider {

    private var collection: MethodCollection? = null

    override val methods: MethodCollection
        get() = collection ?: throw IllegalStateException("Handler is not prepared yet")

    override suspend fun prepare(server: Server) {
        // this snippet exposes a given function as an operation.
        // your framework will typically analyze a scope given by the
        // user to dynamically collect a bunch of operations

        val list = mutableListOf<MethodHandler>()

        // specify the supported methods of the operation we are exposing
        val supportedMethods = MethodAttribute(Method.Get)

        // the actual piece of code to be executed - a function or a suspend lambda
        val function = CustomFrameworkHandler::exposedMethod

        // auto passes the arguments by position, reflection by name
        val executionSettings = ExecutionSettings(ExecutionMode.Auto)

        // configures the behavior for serialization, injection and formatting
        val registry = MethodRegistry(
            Serialization.default().build(),
            Injection.default().build(),
            Formatting.default().build()
        )

        // build the operation we would like to provide
        val operation = OperationBuilder.create(server, "/path/:id", function, null, executionSettings, supportedMethods, registry)

        // create a method handler from the operation which will serve it as an HTTP endpoint
        // this handler requires an instance provider which tells the framework on which
        // object to invoke the given function
        list.add(MethodHandler(operation, { this }, registry))

        // build a method collection handler from all collected operations
        // this handler is responsible for routing
        val methods = MethodCollection(list)

        methods.prepare(server)

        collection = methods
    }

    override suspend fun handle(request: Request): Response? = methods.handle(request)

    fun exposedMethod(id: Int): String = id.toString()

}

Hosting this handler (e.g. with Layout.create().add(CustomFrameworkHandler())) will expose /path/:id on our server, where :id is automatically mapped as a path argument and converted into an integer. The signature of the function is analyzed with kotlin-reflect when the operation is built; with ExecutionMode.Auto, the MethodHandler passes the arguments by position (callSuspend), while ExecutionMode.Reflection always passes them by name (callSuspendBy).

By implementing ServiceMethodProvider, our custom framework seamlessly integrates into functionality that discovers available operations, such as the Open API handlers.