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

org.http4k.lens.body.kt Maven / Gradle / Ivy

package org.http4k.lens

import org.http4k.asString
import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.HttpMessage
import org.http4k.core.with
import org.http4k.lens.ContentNegotiation.Companion.None
import org.http4k.lens.Header.CONTENT_TYPE
import org.http4k.lens.ParamMeta.FileParam
import org.http4k.lens.ParamMeta.StringParam

/**
 * A BodyLens provides the uni-directional extraction of an entity from a target body.
 */
open class BodyLens(val metas: List, val contentType: ContentType, private val getLens: (HttpMessage) -> FINAL) : LensExtractor {

    override operator fun invoke(target: HttpMessage): FINAL = try {
        getLens(target)
    } catch (e: LensFailure) {
        throw e
    } catch (e: Exception) {
        throw LensFailure(metas.map(::Invalid), cause = e, target = target)
    }
}

/**
 * A BiDiBodyLens provides the bi-directional extraction of an entity from a target body, or the insertion of an entity
 * into a target body.
 */
class BiDiBodyLens(
    metas: List,
    contentType: ContentType,
    get: (HttpMessage) -> FINAL,
    private val setLens: (FINAL, HttpMessage) -> HttpMessage
) : LensInjector, BodyLens(metas, contentType, get) {

    @Suppress("UNCHECKED_CAST")
    override operator fun  invoke(value: FINAL, target: R): R = setLens(value, target) as R
}

/**
 * Represents a uni-directional extraction of an entity from a target Body.
 */
open class BodyLensSpec(internal val metas: List, internal val contentType: ContentType, internal val get: LensGet) {
    /**
     * Create a lens for this Spec
     */
    open fun toLens(): BodyLens = with(get("")) {
        BodyLens(metas, contentType) { this(it).firstOrNull() ?: throw LensFailure(metas.map(::Missing), target = it) }
    }

    /**
     * Create another BodyLensSpec which applies the uni-directional transformation to the result. Any resultant Lens can only be
     * used to extract the final type from a Body.
     */
    fun  map(nextIn: (OUT) -> NEXT): BodyLensSpec = BodyLensSpec(metas, contentType, get.map(nextIn))
}

/**
 * Represents a bi-directional extraction of an entity from a target Body, or an insertion into a target Body.
 */
open class BiDiBodyLensSpec(
    metas: List,
    contentType: ContentType,
    get: LensGet,
    private val set: LensSet
) : BodyLensSpec(metas, contentType, get) {

    /**
     * Create another BiDiBodyLensSpec which applies the bi-directional transformations to the result. Any resultant Lens can be
     * used to extract or insert the final type from/into a Body.
     */
    fun  map(nextIn: (OUT) -> NEXT, nextOut: (NEXT) -> OUT) = BiDiBodyLensSpec(metas, contentType, get.map(nextIn), set.map(nextOut))

    /**
     * Create a lens for this Spec
     */
    override fun toLens(): BiDiBodyLens {
        val getLens = get("")
        val setLens = set("")
        return BiDiBodyLens(metas, contentType,
            { getLens(it).let { if (it.isEmpty()) throw LensFailure(metas.map(::Missing), target = it) else it.first() } },
            { out: OUT, target: HttpMessage -> setLens(out?.let { listOf(it) } ?: emptyList(), target) }
        )
    }
}

fun httpBodyRoot(metas: List, acceptedContentType: ContentType, contentNegotiation: ContentNegotiation) =
    BiDiBodyLensSpec(metas, acceptedContentType,
        LensGet { _, target ->
            contentNegotiation(acceptedContentType, CONTENT_TYPE(target))
            listOf(target.body)
        },
        LensSet { _, values, target -> values.fold(target) { memo, next -> memo.body(next) }.with(CONTENT_TYPE of acceptedContentType) }
    )

/**
 * Modes for determining if a passed content type is acceptable.
 */
fun interface ContentNegotiation {

    @Throws(LensFailure::class)
    operator fun invoke(expected: ContentType, actual: ContentType?)

    companion object {
        /**
         * The received Content-type header passed back MUST equal the expected Content-type, including directive
         */
        val Strict = ContentNegotiation { expected, actual -> if (actual != expected) throw LensFailure(Unsupported(CONTENT_TYPE.meta), target = actual) }

        /**
         * The received Content-type header passed back MUST equal the expected Content-type, not including the directive
         */
        val StrictNoDirective = ContentNegotiation { expected, actual -> if (expected.value != actual?.value) throw LensFailure(Unsupported(CONTENT_TYPE.meta), target = actual) }

        /**
         * If present, the received Content-type header passed back MUST equal the expected Content-type, including directive
         */
        val NonStrict = ContentNegotiation { expected, actual -> if (actual != null && actual != expected) throw LensFailure(Unsupported(CONTENT_TYPE.meta), target = actual) }

        /**
         * No validation is done on the received content type at all
         */
        val None = ContentNegotiation { _, _ -> }
    }
}

fun Body.Companion.string(contentType: ContentType, description: String? = null, contentNegotiation: ContentNegotiation = None) = httpBodyRoot(listOf(Meta(true, "body", StringParam, "body", description)), contentType, contentNegotiation)
    .map({ it.payload.asString() }, { Body(it) })

fun Body.Companion.nonEmptyString(contentType: ContentType, description: String? = null, contentNegotiation: ContentNegotiation = None) = string(contentType, description, contentNegotiation).map(StringBiDiMappings.nonEmpty())

fun Body.Companion.binary(contentType: ContentType, description: String? = null, contentNegotiation: ContentNegotiation = None) = httpBodyRoot(listOf(Meta(true, "body", FileParam, "body", description)), contentType, contentNegotiation)
    .map({ it.stream }, { Body(it) })

fun Body.Companion.regex(pattern: String, group: Int = 1, contentType: ContentType = ContentType.TEXT_PLAIN, description: String? = null, contentNegotiation: ContentNegotiation = None) =
    StringBiDiMappings.regex(pattern, group).let { string(contentType, description, contentNegotiation).map(it) }

internal fun  BiDiBodyLensSpec.map(mapping: BiDiMapping) = map(mapping::invoke, mapping::invoke)




© 2015 - 2025 Weber Informatics LLC | Privacy Policy