Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

More examples of .derived and .define (semi auto), show using overrides with options/eithers/collections, minor docs fixes #693

Merged
merged 12 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ import io.scalaland.chimney.internal.runtime.{TransformerFlags, TransformerOverr
/** Type class expressing partial transformation between source type `From` and target type `To`, with the ability of
* reporting path-annotated transformation error(s).
*
* @note
* You should not need to instantiate this class manually, if you can derive it - take a look at
* [[io.scalaland.chimney.PartialTransformer.derive]] and [[io.scalaland.chimney.PartialTransformer.define]] methods
* for that. Manual intantiation is only necessary if you want to add support for the transformation that is not
* supported out of the box. Even then consult [[https://chimney.readthedocs.io/cookbook/#integrations]] first!
*
* @see
* [[https://chimney.readthedocs.io/supported-transformations/]]
* @see
Expand Down Expand Up @@ -177,7 +183,7 @@ object PartialTransformer extends PartialTransformerCompanionPlatform {
// extended by PartialTransformerCompanionPlatform
private[chimney] trait PartialTransformerLowPriorityImplicits1 { this: PartialTransformer.type =>

/** Extracts [[io.scalaland.chimney.PartialTransformer]] from existing [[io.scalaland.chimney.Codec#decode]].
/** Extracts [[io.scalaland.chimney.PartialTransformer]] from existing [[io.scalaland.chimney.Codec.decode]].
*
* @tparam Domain
* type of domain value
Expand Down
6 changes: 6 additions & 0 deletions chimney/src/main/scala/io/scalaland/chimney/Patcher.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import io.scalaland.chimney.dsl.{PatcherDefinition, PatcherDefinitionCommons}
import io.scalaland.chimney.internal.runtime.{PatcherFlags, PatcherOverrides}

/** Type class definition that wraps patching behavior.
*
* @note
* You should not need to instantiate this class manually, if you can derive it - take a look at
* [[io.scalaland.chimney.Patcher.derive]] and [[io.scalaland.chimney.Patcher.define]] methods for that. Manual
* intantiation is only necessary if you want to add support for the transformation that is not supported out of the
* box. Even then consult [[https://chimney.readthedocs.io/cookbook/#integrations]] first!
*
* @see
* [[https://chimney.readthedocs.io/supported-patching/]]
Expand Down
12 changes: 9 additions & 3 deletions chimney/src/main/scala/io/scalaland/chimney/Transformer.scala
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,12 @@ import io.scalaland.chimney.dsl.{PartialTransformerDefinition, TransformerDefini
import io.scalaland.chimney.internal.runtime.{TransformerFlags, TransformerOverrides}

/** Type class expressing total transformation between source type `From` and target type `To`.
*
* @note
* You should not need to instantiate this class manually, if you can derive it - take a look at
* [[io.scalaland.chimney.Transformer.derive]] and [[io.scalaland.chimney.Transformer.define]] methods for that.
* Manual intantiation is only necessary if you want to add support for the transformation that is not supported out
* of the box. Even then consult [[https://chimney.readthedocs.io/cookbook/#integrations]] first!
*
* @see
* [[https://chimney.readthedocs.io/supported-transformations/]]
Expand Down Expand Up @@ -107,7 +113,7 @@ object Transformer extends TransformerCompanionPlatform {
private[chimney] trait TransformerLowPriorityImplicits1 extends TransformerLowPriorityImplicits2 {
this: Transformer.type =>

/** Extracts [[io.scalaland.chimney.Transformer]] from existing [[io.scalaland.chimney.Iso#left]].
/** Extracts [[io.scalaland.chimney.Transformer]] from existing [[io.scalaland.chimney.Iso.first]].
*
* @tparam First
* input type of the first conversion, output type of the second conversion
Expand All @@ -122,7 +128,7 @@ private[chimney] trait TransformerLowPriorityImplicits1 extends TransformerLowPr
private[chimney] trait TransformerLowPriorityImplicits2 extends TransformerLowPriorityImplicits3 {
this: Transformer.type =>

/** Extracts [[io.scalaland.chimney.Transformer]] from existing [[io.scalaland.chimney.Iso#right]].
/** Extracts [[io.scalaland.chimney.Transformer]] from existing [[io.scalaland.chimney.Iso.second]].
*
* @tparam First
* input type of the first conversion, output type of the second conversion
Expand All @@ -136,7 +142,7 @@ private[chimney] trait TransformerLowPriorityImplicits2 extends TransformerLowPr
}
private[chimney] trait TransformerLowPriorityImplicits3 { this: Transformer.type =>

/** Extracts [[io.scalaland.chimney.Transformer]] from existing [[io.scalaland.chimney.Codec#encode]].
/** Extracts [[io.scalaland.chimney.Transformer]] from existing [[io.scalaland.chimney.Codec.encode]].
*
* @tparam Domain
* type of domain value
Expand Down
109 changes: 107 additions & 2 deletions docs/docs/cookbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -261,7 +261,7 @@ These can be enabled with `UnusedFieldPolicy`:
// User2(id = 1, name = "Adam")

locally {
// All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3)
// All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3!).
implicit val cfg = TransformerConfiguration.default.enableUnusedFieldPolicyCheck(FailOnIgnoredSourceVal)

pprint.pprintln(
Expand Down Expand Up @@ -345,7 +345,7 @@ and `UnmatchedSubtypePolicy`:
// Green

locally {
// All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3)
// All transformations derived in this scope will see these new flags (Scala 2-only syntax, see cookbook for Scala 3!).
implicit val cfg = TransformerConfiguration.default.enableUnmatchedSubtypePolicyCheck(FailOnUnmatchedTargetSubtype)

pprint.pprintln(
Expand Down Expand Up @@ -2948,6 +2948,111 @@ fields from the patching:
// Foo(a = "a", b = "d")
```

## Patching optional field with value decoded from JSON

JSON cannot define a nested optional values - since there is no wrapper like `Some` there is no way to represent difference between
`Some(None)` and `None` using build-in JSON semantics. If during `POST` request one want to always use `Some` values to **update**,
and `None` values to always indicate *keep old* semantics **or** always indicate *clear value* semantics (if the modified value is `Option` as well),
this is enough.

The problem, arises when one wantes to express 3 possible outcomes for modifying an `Option` value: *update value*/*keep old*/*clear value*.

The only solution in such case is to express in the API the 3 possible outcomes somwhow without resorting to nested `Option`s. As long as it can
be done, the type can be converted to nested `Option`s which have unambguous semantics:

!!! example

```scala
//> using dep io.scalaland::chimney::{{ chimney_version() }}
//> using dep com.lihaoyi::pprint::{{ libraries.pprint }}
//> using dep io.circe::circe-generic-extras::0.14.4
//> using dep io.circe::circe-parser::0.14.10
import io.circe.{Encoder, Decoder}
import io.circe.generic.extras.Configuration
import io.circe.generic.extras.auto._
import io.circe.generic.extras.semiauto._
import io.circe.parser.decode
import io.circe.syntax._

// An example of representing set-clean-keep operations in a way that cooperates with JSONs.
sealed trait OptionalUpdate[+A] extends Product with Serializable {

def toOption: Option[Option[A]] = this match {
case OptionalUpdate.Set(value) => Some(Some(value))
case OptionalUpdate.Clear => Some(None)
case OptionalUpdate.Keep => None
}
}
object OptionalUpdate {

case class Set[A](value: A) extends OptionalUpdate[A]
case object Clear extends OptionalUpdate[Nothing]
case object Keep extends OptionalUpdate[Nothing]

private implicit val customConfig: Configuration =
Configuration.default
.withDiscriminator("action")
.withSnakeCaseConstructorNames

implicit def encoder[A: Encoder]: Encoder[OptionalUpdate[A]] =
deriveConfiguredEncoder
implicit def decoder[A: Decoder]: Decoder[OptionalUpdate[A]] =
deriveConfiguredDecoder
}

case class Foo(field: Option[String], anotherField: String)

case class FooUpdate(field: OptionalUpdate[String])
object FooUpdate {

private implicit val customConfig: Configuration = Configuration.default
implicit val encoder: Encoder[FooUpdate] = deriveConfiguredEncoder
implicit val decoder: Decoder[FooUpdate] = deriveConfiguredDecoder
}

import io.scalaland.chimney.Patcher
import io.scalaland.chimney.dsl._

// This utility allows to automatically handle Option patching with OptionalUpdate values.
implicit def patchWithOptionalUpdate[A, Patch](implicit
inner: Patcher.AutoDerived[Option[A], Option[Option[A]]]
): Patcher[Option[A], OptionalUpdate[A]] = (obj, patch) =>
obj.patchUsing(patch.toOption)

pprint.pprintln(
decode[FooUpdate](
"""{ "field": { "action": "set", "value": "new-value" } }"""
) match {
case Left(error) => println(error)
case Right(patch) => Foo(Some("old-value"), "another-value").patchUsing(patch)
}
)
// expected output:
// Foo(field = Some(value = "new-value"), anotherField = "another-value")
pprint.pprintln(
decode[FooUpdate](
"""{ "field": { "action": "clear" } }"""
) match {
case Left(error) => println(error)
case Right(patch) => Foo(Some("old-value"), "another-value").patchUsing(patch)
}
)
// expected output:
// Foo(field = None, anotherField = "another-value")
pprint.pprintln(
decode[FooUpdate](
"""{ "field": { "action": "keep" } }"""
) match {
case Left(error) => println(error)
case Right(patch) => Foo(Some("old-value"), "another-value").patchUsing(patch)
}
)
// expected output:
// Foo(field = Some(value = "old-value"), anotherField = "another-value")
```

If we cannot modify our API, we have to [choose one semantics for `None` values](supported-patching.md#treating-none-as-no-update-instead-of-set-to-none).

## Mixing Scala 2.13 and Scala 3 types

[Scala 2.13 project can use Scala 3 artifacts and vice versa](https://docs.scala-lang.org/scala3/guides/migration/compatibility-classpath.html).
Expand Down
17 changes: 14 additions & 3 deletions docs/docs/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -55,11 +55,22 @@ case class ApiUser(name: String, surname: String)
val userID: UUID = ...
val user: User = ...

// Use .transformInto[Type], when don't need to customize anything...
// Use .transformInto[Type], when don't need to customize anything...:
val apiUser: ApiUser = user.transformInto[ApiUser]

// ...and .into[Type].customization.transform when you do.
// ...and .into[Type].customization.transform when you do:
val user2: User = apiUser.into[User].withFieldConst(_.id, userID).transform

// If yout want to reuse some Transformation (and you don't want to write it by hand)
// you can generate it with .derive:
implicit val transformer: Transformer[ApiUser, User] = Transformer.derive[ApiUser, User]

// ...or with .define.customization.buildTransformer:
implicit val transformerWithOverrides: Transformer[User, ApiUser] = Transformer.define[User, ApiUser]
.withFieldConst(_.id, userID)
.buildTransformer

// It works the same way with PartialTransformers and Patchers.
```

Chimney will take care of generating the boring transformation code, and if it finds something non-obvious, it will give
Expand All @@ -78,7 +89,7 @@ apiUser.transformInto[User]
But don't you worry! Usually Chimney only needs your help if there is no field in the source value with a matching name
or whe the targeted type has a private constructor. Out of the box, it supports:

* conversions [between `case class`es](supported-transformations.md#into-a-case-class)
* conversions [between `case class`es](supported-transformations.md#into-a-case-class-or-pojo)
* actually, a conversion between *any* `class` and *another `class` with a public constructor*
* with [an opt-in support for Java Beans](supported-transformations.md#reading-from-bean-getters)
* conversions [between `sealed trait`s, Scala 3 `enum`s, Java `enum`s](supported-transformations.md#between-sealedenums)
Expand Down
Loading