All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.http4k.contract.ContractRoute.kt Maven / Gradle / Ivy

There is a newer version: 5.31.0.0
Show newest version
package org.http4k.contract

import org.http4k.contract.PreFlightExtraction.Companion
import org.http4k.contract.openapi.operationId
import org.http4k.core.Filter
import org.http4k.core.HttpHandler
import org.http4k.core.Method
import org.http4k.core.Method.OPTIONS
import org.http4k.core.NoOp
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.BAD_REQUEST
import org.http4k.core.Status.Companion.METHOD_NOT_ALLOWED
import org.http4k.core.Status.Companion.NOT_FOUND
import org.http4k.core.Status.Companion.OK
import org.http4k.core.Uri
import org.http4k.core.then
import org.http4k.core.toPathSegmentDecoded
import org.http4k.filter.ServerFilters
import org.http4k.lens.LensFailure
import org.http4k.lens.PathLens
import org.http4k.routing.Router
import org.http4k.routing.RouterDescription
import org.http4k.routing.RouterMatch
import org.http4k.routing.RouterMatch.MatchedWithoutHandler
import org.http4k.routing.RouterMatch.MatchingHandler
import org.http4k.routing.RouterMatch.MethodNotMatched
import org.http4k.routing.RouterMatch.Unmatched

class ContractRoute internal constructor(
    val method: Method,
    val spec: ContractRouteSpec,
    val meta: RouteMeta,
    internal val toHandler: (ExtractedParts) -> HttpHandler
) : HttpHandler {
    val nonBodyParams = meta.requestParams.plus(spec.pathLenses).flatten()

    val tags = meta.tags.toSet().sortedBy { it.name }

    fun newRequest(baseUri: Uri) = Request(method, "").uri(baseUri.path(spec.describe(Root)))

    fun toRouter(contractRoot: PathSegments) = object : Router {

        override fun toString() = description.description

        override val description = RouterDescription(spec.describe(contractRoot))

        override fun match(request: Request): RouterMatch =
            if ((request.method == OPTIONS || request.method == method) && request.pathSegments().startsWith(spec.pathFn(contractRoot))) {
                try {
                    request.without(spec.pathFn(contractRoot))
                        .extract(spec.pathLenses.toList())
                        ?.let {
                            MatchingHandler(
                                if (request.method == OPTIONS) {
                                    { Response(OK) }
                                } else toHandler(it), description)
                        } ?: Unmatched(description)
                } catch (e: LensFailure) {
                    Unmatched(description)
                }
            } else Unmatched(description)
    }

    fun describeFor(contractRoot: PathSegments) = spec.describe(contractRoot)

    /**
     * ContractRoutes are chiefly designed to operate within a contract {} block and not directly as an HttpHandler,
     * but this function exists to enable the testing of the ContractRoute logic outside of a wider contract context.
     * This means that certain behaviour is defaulted - chiefly the generation of NOT_FOUND and BAD_REQUEST responses.
     */
    override fun invoke(request: Request): Response {
        return when (val matchResult = toRouter(Root).match(request)) {
            is MatchingHandler -> {
                (meta.security?.filter ?: Filter.NoOp)
                    .then(ServerFilters.CatchLensFailure { _ -> Response(BAD_REQUEST) })
                    .then(PreFlightExtractionFilter(meta, Companion.All))
                    .then(matchResult)(request)
            }
            is MethodNotMatched -> Response(METHOD_NOT_ALLOWED)
            is Unmatched -> Response(NOT_FOUND)
            is MatchedWithoutHandler -> Response(NOT_FOUND)
        }
    }

    internal fun operationId(contractRoot: PathSegments) =
        operationId(meta, method, describeFor(contractRoot))

    override fun toString() = "${method.name}: ${spec.describe(Root)}"
}

internal class ExtractedParts(private val mapping: Map, *>) {
    @Suppress("UNCHECKED_CAST")
    operator fun  get(lens: PathLens): T = mapping[lens] as T
}

private operator fun  PathSegments.invoke(index: Int, fn: (String) -> T): T? = toList().let { if (it.size > index) fn(it[index]) else null }

private fun PathSegments.extract(lenses: List>): ExtractedParts? =
    when (toList().size) {
        lenses.size -> ExtractedParts(
            lenses.mapIndexed { i, lens -> lens to this(i) { lens(it.toPathSegmentDecoded()) } }.toMap()
        )
        else -> null
    }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy