Skip to main content

Optics - Lenses and Prisms

Smithy4s has the ability to render optics (Lens/Prism) instances in the code it generates.

If you're using Smithy4s via mill or sbt, then you can enable this functionality with the following keys:

  • in mill, task: def smithy4sRenderOptics = true
  • in sbt, setting: smithy4sRenderOptics := true

If you are using Smithy4s via the CLI, then they way to utilize this feature is through your Smithy specifications. The simplest approach is to add a file with the following content to your CLI invocation:

$version: "2"

metadata smithy4sRenderOptics = true

Alternatively, if you want to generate optics for only select shapes in your model, you can accomplish this using the smithy4s.meta#generateOptics trait. This trait can be used on enum, intEnum, union, and structure shapes.

use smithy4s.meta#generateOptics

@generateOptics
structure MyStruct {
one: String
}

Optics Usage

Below is an example of using the lenses that smithy4s generates. By default, smithy4s will generate lenses for all structure shapes in your input smithy model(s).

import smithy4s.example._

val input = TestInput("test", TestBody(Some("test body")))
// input: TestInput = TestInput(
// pathParam = "test",
// body = TestBody(data = Some(value = "test body")),
// queryParam = None
// )
val lens = TestInput.optics.body.andThen(TestBody.optics.data).some
// lens: smithy4s.optics.Optional[TestInput, String] = smithy4s.optics.Optional$$anon$1@4fc3133d
val resultGet = lens.project(input)
// resultGet: Option[String] = Some(value = "test body")

resultGet == Option("test body") // true
// res1: Boolean = true // true

val resultSet =
lens.replace("new body")(input)
// resultSet: TestInput = TestInput(
// pathParam = "test",
// body = TestBody(data = Some(value = "new body")),
// queryParam = None
// )

val updatedInput = TestInput("test", TestBody(Some("new body")))
// updatedInput: TestInput = TestInput(
// pathParam = "test",
// body = TestBody(data = Some(value = "new body")),
// queryParam = None
// )

resultSet == updatedInput // true
// res2: Boolean = true

You can also compose prisms with lenses (and vice-versa) as in the example below:

import smithy4s.example._

val input = Podcast.Video(Some("Pod Title"))
// input: Podcast.Video = Video(
// title = Some(value = "Pod Title"),
// url = None,
// durationMillis = None
// )

val prism = Podcast.optics.video.andThen(Podcast.Video.optics.title).some
// prism: smithy4s.optics.Optional[Podcast, String] = smithy4s.optics.Optional$$anon$1@25bcb29d
val result = prism.replace("New Pod Title")(input)
// result: Podcast = Video(
// title = Some(value = "New Pod Title"),
// url = None,
// durationMillis = None
// )

Podcast.Video(Some("New Pod Title")) == result // true
// res4: Boolean = true

Smithy4s also provides a value function on Prisms and Lenses that can be used to abstract over NewTypes (similar to what .some does for Option types):

import smithy4s.example._

val input = GetCityInput(CityId("test"))
// input: GetCityInput = GetCityInput(cityId = "test")

val cityName: smithy4s.optics.Lens[GetCityInput, String] = GetCityInput.optics.cityId.value
// cityName: smithy4s.optics.Lens[GetCityInput, String] = smithy4s.optics.Lens$$anon$2@46770f11
val updated = cityName.replace("Fancy New Name")(input)
// updated: GetCityInput = GetCityInput(cityId = "Fancy New Name")

val result = cityName.project(updated)
// result: Option[String] = Some(value = "Fancy New Name")

Option("Fancy New Name") == result // true
// res6: Boolean = true

Using 3rd Party Optics Libraries

If you'd like to use a third party optics library for more functionality, you can accomplish this by adding an object with a few conversion functions. Here is an example using Monocle.

object MonocleConversions {

implicit def smithy4sToMonocleLens[S, A](
smithy4sLens: smithy4s.optics.Lens[S, A]
): monocle.Lens[S, A] =
monocle.Lens[S, A](smithy4sLens.get)(smithy4sLens.replace)

implicit def smithy4sToMonoclePrism[S, A](
smithy4sPrism: smithy4s.optics.Prism[S, A]
): monocle.Prism[S, A] =
monocle.Prism(smithy4sPrism.project)(smithy4sPrism.inject)

implicit def smithy4sToMonocleOptional[S, A](
smithy4sOptional: smithy4s.optics.Optional[S, A]
): monocle.Optional[S, A] =
monocle.Optional(smithy4sOptional.project)(smithy4sOptional.replace)

}

Then you can import MonocleConversions._ at the top of any file you need to seamlessly convert smithy4s optics over to Monocle ones.