HttpClient.kt

package com.hexagonkt.http.client

import com.hexagonkt.http.handlers.HttpContext
import com.hexagonkt.http.handlers.HttpHandler
import com.hexagonkt.http.handlers.OnHandler
import com.hexagonkt.http.handlers.path
import com.hexagonkt.http.model.HttpRequest
import com.hexagonkt.http.model.HttpResponsePort
import com.hexagonkt.http.model.*
import com.hexagonkt.http.model.HttpMethod.*
import com.hexagonkt.http.model.ws.WsSession
import java.io.Closeable
import java.util.concurrent.Flow.Publisher

/**
 * Client to use other REST services.
 */
class HttpClient(
    private val adapter: HttpClientPort,
    val settings: HttpClientSettings = HttpClientSettings(),
    val handler: HttpHandler? = null,
) : Closeable {

    private val rootHandler: HttpHandler? =
        handler?.let {
            val sendHandler = OnHandler("*") { send(adapter.send(request)) }
            path("*", listOf(it, sendHandler))
        }

    private val noRequestSettings =
        settings.contentType == null
            && settings.authorization == null
            && settings.accept.isEmpty()
            && settings.headers.isEmpty()

    var cookies: List<Cookie> = emptyList()

    override fun close() {
        stop()
    }

    fun cookiesMap(): Map<String, Cookie> =
        cookies.associateBy { it.name }

    fun start() {
        check(!started()) { "HTTP client is already started" }
        adapter.startUp(this)
    }

    fun stop() {
        check(started()) { "HTTP client *MUST BE STARTED* before shut-down" }
        adapter.shutDown()
    }

    fun started(): Boolean =
        adapter.started()

    /**
     * Synchronous execution.
     */
    fun send(request: HttpRequest, attributes: Map<String, Any> = emptyMap()): HttpResponsePort =
        if (!started())
            error("HTTP client *MUST BE STARTED* before sending requests")
        else
            rootHandler
                ?.process(request.setUp(), attributes)
                ?.let {
                    if (it.exception != null) throw it.exception as Exception
                    else it.response
                }
                ?: adapter.send(request.setUp())

    private fun HttpRequest.setUp(): HttpRequest {
        return if (noRequestSettings)
            this
        else
            copy(
                contentType = contentType ?: settings.contentType,
                accept = accept.ifEmpty(settings::accept),
                headers = settings.headers + headers,
                authorization = authorization ?: settings.authorization,
            )
    }

    fun sse(request: HttpRequest): Publisher<ServerEvent> =
        if (!started()) error("HTTP client *MUST BE STARTED* before sending requests")
        else adapter.sse(request)

    fun sse(path: String): Publisher<ServerEvent> =
        sse(HttpRequest(path = path))

    fun ws(
        path: String,
        onConnect: WsSession.() -> Unit = {},
        onBinary: WsSession.(data: ByteArray) -> Unit = {},
        onText: WsSession.(text: String) -> Unit = {},
        onPing: WsSession.(data: ByteArray) -> Unit = {},
        onPong: WsSession.(data: ByteArray) -> Unit = {},
        onClose: WsSession.(status: Int, reason: String) -> Unit = { _, _ -> },
    ): WsSession =
        if (!started()) error("HTTP client *MUST BE STARTED* before connecting to WS")
        else adapter.ws(path, onConnect, onBinary, onText, onPing, onPong, onClose)

    fun request (block: HttpClient.() -> Unit) {
        if (!started())
            start()

        use(block)
    }

    fun get(
        path: String = "",
        headers: Headers = Headers(),
        body: Any? = null,
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
            send(
                HttpRequest(
                    method = GET,
                    path = path,
                    body = body ?: "",
                    headers = headers,
                    contentType = contentType,
                    accept = accept,
                )
            )

    fun head(path: String = "", headers: Headers = Headers()): HttpResponsePort =
        send(HttpRequest(HEAD, path = path, body = ByteArray(0), headers = headers))

    fun post(
        path: String = "",
        body: Any? = null,
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            HttpRequest(
                method = POST,
                path = path,
                body = body ?: "",
                contentType = contentType,
                accept = accept,
            )
        )

    fun put(
        path: String = "",
        body: Any? = null,
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            HttpRequest(
                method = PUT,
                path = path,
                body = body ?: "",
                contentType = contentType,
                accept = accept,
            )
        )

    fun delete(
        path: String = "",
        body: Any? = null,
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            HttpRequest(
                method = DELETE,
                path = path,
                body = body ?: "",
                contentType = contentType,
                accept = accept,
            )
        )

    fun trace(
        path: String = "",
        body: Any? = null,
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            HttpRequest(
                method = TRACE,
                path = path,
                body = body ?: "",
                contentType = contentType,
                accept = accept,
            )
        )

    fun options(
        path: String = "",
        body: Any? = null,
        headers: Headers = Headers(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            HttpRequest(
                method = OPTIONS,
                path = path,
                body = body ?: "",
                headers = headers,
                contentType = contentType,
                accept = accept,
            )
        )

    fun patch(
        path: String = "",
        body: Any? = null,
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            HttpRequest(
                method = PATCH,
                path = path,
                body = body ?: "",
                contentType = contentType,
                accept = accept,
            )
        )

    private fun HttpHandler.process(
        request: HttpRequestPort, attributes: Map<String, Any>
    ): HttpContext =
        processHttp(
            HttpContext(HttpCall(request = request), handlerPredicate, attributes = attributes)
        )
}