HttpPredicate.kt

package com.hexagonkt.http.handlers

import com.hexagonkt.handlers.Context
import com.hexagonkt.core.logging.Logger
import com.hexagonkt.http.patterns.LiteralPathPattern
import com.hexagonkt.http.model.HttpMethod
import com.hexagonkt.http.model.HttpStatus
import com.hexagonkt.http.patterns.PathPattern
import com.hexagonkt.http.patterns.createPathPattern
import com.hexagonkt.http.model.HttpCall
import kotlin.reflect.KClass

data class HttpPredicate(
    val methods: Set<HttpMethod> = emptySet(),
    val pathPattern: PathPattern = LiteralPathPattern(),
    val exception: KClass<out Exception>? = null,
    val status: HttpStatus? = null,
) : (Context<HttpCall>) -> Boolean {

    private companion object {
        val logger: Logger = Logger(HttpPredicate::class)
    }

    private fun PathPattern.isEmpty(): Boolean =
        pattern.isEmpty()

    val predicate: (Context<HttpCall>) -> Boolean =
        if (methods.isEmpty()) log(::filterWithoutMethod)
        else log(::filterWithMethod)

    constructor(
        methods: Set<HttpMethod> = emptySet(),
        pattern: String = "",
        exception: KClass<out Exception>? = null,
        status: HttpStatus? = null,
        prefix: Boolean = false,
    ) :
        this(methods, createPathPattern(pattern, prefix), exception, status)

    override fun invoke(context: Context<HttpCall>): Boolean =
        predicate(context)

    fun clearMethods(): HttpPredicate =
        copy(methods = emptySet())

    private fun log(
        predicate: (Context<HttpCall>) -> Boolean
    ): (Context<HttpCall>) -> Boolean = {
        val allowed = predicate(it)
        logger.debug { "${describe()} -> ${if (allowed) "ALLOWED" else "DENIED"}" }
        allowed
    }

    private fun filterMethod(context: Context<HttpCall>): Boolean =
        context.event.request.method in methods

    private fun filterPattern(context: Context<HttpCall>): Boolean =
        if (pathPattern.isEmpty() && context.event.request.path == "/") true
        else pathPattern.matches(context.event.request.path)

    private fun filterException(context: Context<HttpCall>): Boolean {
        val exceptionClass = context.exception?.javaClass ?: return false
        return exception?.java?.isAssignableFrom(exceptionClass) ?: false
    }

    private fun filterStatus(context: Context<HttpCall>): Boolean =
        status == context.event.response.status

    private fun filterWithoutMethod(context: Context<HttpCall>): Boolean =
        filterPattern(context)
            && (exception == null || filterException(context))
            && (status == null || filterStatus(context))

    private fun filterWithMethod(context: Context<HttpCall>): Boolean =
        filterMethod(context) && filterWithoutMethod(context)

    fun addPrefix(prefix: String): HttpPredicate =
        copy(pathPattern = pathPattern.addPrefix(prefix))

    fun describe(): String =
        methods
            .map { it.name }
            .ifEmpty { listOf("ANY") }
            .joinToString(
                separator = ", ",
                postfix = pathPattern.describe().prependIndent(" "),
                transform = { it }
            )
}