HttpClientTool.kt

package com.hexagontk.rest.tools

import com.hexagontk.core.media.MediaType
import com.hexagontk.http.SslSettings
import com.hexagontk.http.client.HttpClient
import com.hexagontk.http.client.HttpClientPort
import com.hexagontk.http.client.HttpClientSettings
import com.hexagontk.http.handlers.BeforeHandler
import com.hexagontk.http.handlers.HttpHandler
import com.hexagontk.http.model.HttpRequest
import com.hexagontk.http.model.*
import com.hexagontk.http.model.HttpMethod.*
import com.hexagontk.http.patterns.createPathPattern
import com.hexagontk.rest.SerializeRequestCallback
import java.net.URI

class HttpClientTool(
    adapter: HttpClientPort,
    url: String? = null,
    httpContentType: ContentType? = null,
    httpAccept: List<ContentType> = emptyList(),
    val httpHeaders: Map<String, *> = emptyMap<String, Any>(),
    sslSettings: SslSettings? = SslSettings(),
    val handler: HttpHandler? = serializeHandler,
    authorization: Authorization? = null,
    followRedirects: Boolean = false
) {
    companion object {
        // TODO Use SerializeRequestHandler when created
        val serializeHandler: HttpHandler = BeforeHandler("*", SerializeRequestCallback())
    }

    private val settings =
        HttpClientSettings(
            baseUri = url?.let(::URI),
            contentType = httpContentType,
            accept = httpAccept,
            useCookies = true,
            headers = toHeaders(httpHeaders),
            insecure = true,
            sslSettings = sslSettings,
            authorization = authorization,
            followRedirects = followRedirects
        )

    private val client = HttpClient(adapter, settings, handler = handler)

    private lateinit var lastRequest: HttpRequest
    private lateinit var lastResponse: HttpResponsePort

    val request: HttpRequest get() = lastRequest
    val response: HttpResponsePort get() = lastResponse
    val status: Int get() = lastResponse.status
    val body: Any get() = lastResponse.body
    val cookies: Map<String, Cookie> get() = lastResponse.cookiesMap()
    val headers: Headers get() = lastResponse.headers
    val contentType: ContentType? get() = lastResponse.contentType

    constructor(
        adapter: HttpClientPort,
        url: String? = null,
        mediaType: MediaType,
        accept: List<MediaType> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        sslSettings: SslSettings? = SslSettings(),
        handler: HttpHandler? = serializeHandler,
    ) : this(
        adapter,
        url,
        ContentType(mediaType),
        accept.map(::ContentType),
        headers,
        sslSettings,
        handler
    )

    fun start() {
        if (!client.started())
            client.start()
    }

    fun stop() {
        if (client.started())
            client.stop()
    }

    fun request(block: HttpClientTool.() -> Unit) {
        client.request { block.invoke(this@HttpClientTool) }
    }

    fun assertStatus(status: Int) {
        assert(status == lastResponse.status)
    }

    fun assertOk() {
        assertStatus(OK_200)
    }

    fun assertSuccess() {
        assert(lastResponse.status in SUCCESS)
    }

    fun assertContentType(contentType: ContentType) {
        assert(this.contentType?.text == contentType.text)
    }

    fun assertContentType(mediaType: MediaType) {
        assertContentType(ContentType(mediaType))
    }

    fun assertBody(body: Any) {
        assert(body == lastResponse.body)
    }

    fun assertBodyContains(vararg texts: String) {
        texts.forEach { assert(lastResponse.bodyString().contains(it)) }
    }

    private fun toHeaders(map: Map<String, *>): Headers = Headers(
        map.flatMap { (k, v) ->
            when (v) {
                is Collection<*> -> v.map { Header(k, it) }.toList()
                else -> listOf(Header(k, v))
            }
        }
    )

    private fun send(
        method: HttpMethod,
        path: String?,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
        pathPattern: String? = null,
        pathParameters: Map<String, Any> = emptyMap(),
    ): HttpResponsePort =
        client
            .apply {
                if (!started())
                    start()

                val (pattern, patternPath) = when {
                    pathPattern != null ->
                        createPathPattern(pathPattern)
                            .let { it to it.insertParameters(pathParameters) }

                    else -> null to (path ?: "")
                }

                lastRequest = HttpRequest(
                    method = method,
                    path = patternPath,
                    body = body,
                    headers = toHeaders(headers),
                    formParameters = Parameters(formParameters),
                    parts = parts,
                    contentType = contentType,
                    accept = accept,
                    pathPattern = pattern,
                    pathParameters = pathParameters,
                )
            }
            .send(lastRequest)
            .apply { lastResponse = this }

    private fun send(
        method: HttpMethod,
        pathPattern: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            method,
            null,
            body,
            formParameters,
            headers,
            parts,
            contentType,
            accept,
            pathPattern,
            pathParameters,
        )

    fun get(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(GET, path, body, formParameters, headers, parts, contentType, accept)

    fun put(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(PUT, path, body, formParameters, headers, parts, contentType, accept)

    fun put(
        path: String = "/",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
        body: () -> Any,
    ): HttpResponsePort =
        put(path, body(), formParameters, headers, parts, contentType, accept)

    fun post(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(POST, path, body, formParameters, headers, parts, contentType, accept)

    fun post(
        path: String = "/",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
        body: () -> Any,
    ): HttpResponsePort =
        post(path, body(), formParameters, headers, parts, contentType, accept)

    fun options(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(OPTIONS, path, body, formParameters, headers, parts, contentType, accept)

    fun delete(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(DELETE, path, body, formParameters, headers, parts, contentType, accept)

    fun patch(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(PATCH, path, body, formParameters, headers, parts, contentType, accept)

    fun trace(
        path: String = "/",
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(TRACE, path, body, formParameters, headers, parts, contentType, accept)

    fun get(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(GET, path, pathParameters, body, formParameters, headers, parts, contentType, accept)

    fun put(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(PUT, path, pathParameters, body, formParameters, headers, parts, contentType, accept)

    fun put(
        path: String,
        pathParameters: Map<String, Any>,
        formParameters: List<HttpField> = emptyList(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
        body: () -> Any,
    ): HttpResponsePort =
        put(
            path,
            pathParameters,
            body(),
            formParameters,
            mapOf<String, Any>(),
            parts,
            contentType,
            accept
        )

    fun post(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(POST, path, pathParameters, body, formParameters, headers, parts, contentType, accept)

    fun post(
        path: String,
        pathParameters: Map<String, Any>,
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
        body: () -> Any,
    ): HttpResponsePort =
        post(path, pathParameters, body(), formParameters, headers, parts, contentType, accept)

    fun options(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            OPTIONS, path, pathParameters, body, formParameters, headers, parts, contentType, accept
        )

    fun delete(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(
            DELETE, path, pathParameters, body, formParameters, headers, parts, contentType, accept
        )

    fun patch(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(PATCH, path, pathParameters, body, formParameters, headers, parts, contentType, accept)

    fun trace(
        path: String,
        pathParameters: Map<String, Any>,
        body: Any = "",
        formParameters: List<HttpField> = emptyList(),
        headers: Map<String, *> = emptyMap<String, Any>(),
        parts: List<HttpPart> = emptyList(),
        contentType: ContentType? = settings.contentType,
        accept: List<ContentType> = settings.accept,
    ): HttpResponsePort =
        send(TRACE, path, pathParameters, body, formParameters, headers, parts, contentType, accept)
}