Concept: Lenses

Lenses provide typesafe parameter destructuring/construction of HTTP messages. Getting values from HTTP messages is one thing, but we want to ensure that those values are both present and valid. For this purpose, we can use a Lens.

A Lens is a bi-directional entity which can be used to either get or set a particular value from/onto an HTTP message. http4k provides a DSL to configure these lenses to target particular parts of the message, whilst at the same time specifying the requirement for those parts (i.e. mandatory or optional).

To utilise a lens, first you have to declare it with the form <Location>.<configuration and mapping operations>.<terminator>.

There is one “location” type for each part of the message, each with config/mapping operations which are specific to that location:

LocationStarting typeApplicable toMultiplicityRequirement terminatorExamples
QueryStringRequestSingular or multipleOptional or RequiredQuery.optional("name")
Query.required("name")
Query.int().required("name")
Query.localDate().multi.required("name")
Query.map(::CustomType, { it.value }).required("name")
HeaderStringRequest or ResponseSingular or multipleOptional or RequiredHeader.optional("name")
Header.required("name")
Header.int().required("name")
Header.localDate().multi.required("name")
Header.map(::CustomType, { it.value }).required("name")
PathStringRequestSingularRequiredPath.of("name")
Path.int().of("name")
Path.map(::CustomType, { it.value }).of("name")
FormFieldStringWebFormSingular or multipleOptional or RequiredFormField.optional("name")
FormField.required("name")
FormField.int().required("name")
FormField.localDate().multi.required("name")
FormField.map(::CustomType, { it.value }).required("name")
BodyByteBufferRequest or ResponseSingularRequiredBody.string(ContentType.TEXT_PLAIN).toLens()
Body.json().toLens()
Body.webForm(Validator.Strict, FormField.required("name")).toLens()

Once the lens is declared, you can use it on a target object to either get or set the value:

  • Retrieving a value: use <lens>.extract(<target>), or the more concise invoke form: <lens>(<target>)
  • Setting a value: use <lens>.inject(<value>, <target>), or the more concise invoke form: <lens>(<value>, <target>)

Code

package content.ecosystem.http4k.concepts.lens

import org.http4k.core.Body
import org.http4k.core.ContentType
import org.http4k.core.Method.GET
import org.http4k.core.Request
import org.http4k.core.Response
import org.http4k.core.Status.Companion.OK
import org.http4k.core.then
import org.http4k.core.with
import org.http4k.filter.ServerFilters
import org.http4k.lens.Header
import org.http4k.lens.Path
import org.http4k.lens.Query
import org.http4k.lens.int
import org.http4k.lens.localDate
import org.http4k.lens.nonEmptyString
import org.http4k.lens.string
import org.http4k.routing.RoutingHttpHandler
import org.http4k.routing.bind
import org.http4k.routing.routes
import java.time.LocalDate

val pathLocalDate = Path.localDate().of("date")
val requiredQuery = Query.required("myQueryName")
val nonEmptyQuery = Query.nonEmptyString().required("myNonEmptyQuery")
val optionalHeader = Header.int().optional("Content-Length")
val responseBody = Body.string(ContentType.TEXT_PLAIN).toLens()

// Most of the useful common JDK types are covered. However, if we want to use our own types, we can just use `map()`
data class CustomType(val value: String)

val requiredCustomQuery = Query.map(::CustomType, { it.value }).required("myCustomType")

//To use the Lens, simply `invoke() or extract()` it using an HTTP message to extract the value, or alternatively
// `invoke() or inject()` it with the value if we are modifying (via copy) the message:
val handler: RoutingHttpHandler = routes(
    "/hello/{date:.*}" bind GET to { request: Request ->
        val pathDate: LocalDate = pathLocalDate(request)
        // SAME AS:
        // val pathDate: LocalDate = pathLocalDate.extract(request)

        val customType: CustomType = requiredCustomQuery(request)
        val anIntHeader: Int? = optionalHeader(request)

        val baseResponse = Response(OK)
        val responseWithHeader = optionalHeader(anIntHeader, baseResponse)
        // SAME AS:
        // val responseWithHeader = optionalHeader.inject(anIntHeader, baseResponse)

        responseBody("you sent $pathDate and $customType", responseWithHeader)
    }
)

//With the addition of the `CatchLensFailure` filter, no other validation is required when using Lenses, as http4k
// will handle invalid requests by returning a BAD_REQUEST (400) response.
val app = ServerFilters.CatchLensFailure.then(handler)(
    Request(
        GET,
        "/hello/2000-01-01?myCustomType=someValue"
    )
)

//More conveniently for construction of HTTP messages, multiple lenses can be used at once to modify a message,
// which is useful for properly building both requests and responses in a typesafe way without resorting to string
// values (especially in URLs which should never be constructed using String concatenation):
val modifiedRequest: Request = Request(GET, "http://google.com/{pathLocalDate}").with(
    pathLocalDate of LocalDate.now(),
    requiredQuery of "myAmazingString",
    optionalHeader of 123
)