diff --git a/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/TypesPlatform.scala b/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/TypesPlatform.scala index 6ece56bc6..5dce163ba 100644 --- a/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/TypesPlatform.scala +++ b/chimney-macro-commons/src/main/scala-3/io/scalaland/chimney/internal/compiletime/TypesPlatform.scala @@ -61,8 +61,8 @@ private[compiletime] trait TypesPlatform extends Types { this: DefinitionsPlatfo .toMap typeArgumentByName // unknown + // $COVERAGE-OFF$should never happen unless we messed up case out => - // $COVERAGE-OFF$should never happen unless we messed up assertionFailed( s"Constructor of ${Type.prettyPrint(fromUntyped[Any](tpe))} has unrecognized/unsupported format of type: $out" ) diff --git a/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/SingletonTypes.scala b/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/SingletonTypes.scala index f9bde7c75..6f20fc162 100644 --- a/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/SingletonTypes.scala +++ b/chimney-macro-commons/src/main/scala/io/scalaland/chimney/internal/compiletime/datatypes/SingletonTypes.scala @@ -37,8 +37,8 @@ trait SingletonTypes { this: (Definitions & ProductTypes) => case _ if ProductType.isCaseObject[A] || ProductType.isCaseVal[A] || ProductType.isJavaEnumValue[A] => Type[A] match { case Product.Constructor(params, ctor) if params.isEmpty => found(ctor(Map.empty)) - case _ => - // $COVERAGE-OFF$should never happen unless we messed up + // $COVERAGE-OFF$should never happen unless we messed up + case _ => assertionFailed( s"Expected case object/case with no params/Java enum of ${Type.prettyPrint[A]} to have a nullary constructor" ) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Configurations.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Configurations.scala index 27b0dc1bb..601cc7a1e 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Configurations.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Configurations.scala @@ -90,8 +90,8 @@ private[compiletime] trait Configurations { this: Derivation => case ChimneyType.PatcherFlags.Disable(flag, flags) => import flag.Underlying as Flag, flags.Underlying as Flags2 extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = false) + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => - // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation reportError(s"Invalid internal PatcherFlags type shape: ${Type.prettyPrint[Flags]}!") // $COVERAGE-ON$ } @@ -99,8 +99,8 @@ private[compiletime] trait Configurations { this: Derivation => private def extractPatcherConfig[Tail <: runtime.PatcherOverrides: Type](): PatcherConfiguration = Type[Tail] match { case empty if empty =:= ChimneyType.PatcherOverrides.Empty => PatcherConfiguration() - case _ => - // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation + case _ => reportError(s"Invalid internal PatcherOverrides type shape: ${Type.prettyPrint[Tail]}!!") // $COVERAGE-ON$ } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Derivation.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Derivation.scala index b9bd88270..48e35737f 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Derivation.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/patcher/Derivation.scala @@ -104,8 +104,8 @@ private[compiletime] trait Derivation transformedExpr.orElse(targetGetter.value.get(ctx.obj).upcastToExprOf[Option[TargetInner]]).as_?? ) } + // $COVERAGE-OFF$should never happen unless we messed up case _ => - // $COVERAGE-OFF$should never happen unless we messed up assertionFailed( s"Expected both types to be options, got ${Type.prettyPrint[PatchGetter]} and ${Type.prettyPrint[TargetParam]}" ) diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala index 275a8b61f..f90e36a32 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/Configurations.scala @@ -9,6 +9,8 @@ import scala.collection.immutable.{ListMap, ListSet} private[compiletime] trait Configurations { this: Derivation => + import Type.Implicits.* + final protected case class TransformerFlags( inheritedAccessors: Boolean = false, methodAccessors: Boolean = false, @@ -308,6 +310,7 @@ private[compiletime] trait Configurations { this: Derivation => runtimeOverrides.isEmpty def areLocalFlagsAndOverridesEmpty: Boolean = areLocalFlagsEmpty && areOverridesEmpty + def filterCurrentOverridesForField(nameFilter: String => Boolean): Map[String, TransformerOverride.ForField] = ListMap.from( runtimeOverridesForCurrent.collect { @@ -318,14 +321,52 @@ private[compiletime] trait Configurations { this: Derivation => def filterCurrentOverridesForSubtype( sourceTypeFilter: ?? => Boolean, @scala.annotation.unused targetTypeFilter: ?? => Boolean - ): Map[Option[??], TransformerOverride.ForSubtype] = - ListMap.from( - runtimeOverridesForCurrent.collect { - case (Path.AtSourceSubtype(tpe, _), runtimeCoproductOverride: TransformerOverride.ForSubtype) - if sourceTypeFilter(tpe) => - Some(tpe) -> runtimeCoproductOverride - } - ) + ): Map[Option[??], TransformerOverride.ForSubtype] = ListMap.from( + runtimeOverridesForCurrent.collect { + case (Path.AtSourceSubtype(tpe, _), runtimeCoproductOverride: TransformerOverride.ForSubtype) + if sourceTypeFilter(tpe) => + Some(tpe) -> runtimeCoproductOverride + } + ) + def filterCurrentOverridesForSome: Set[TransformerOverride.ForField] = ListSet.from( + runtimeOverrides.collect { + case (Path.AtSubtype(tpe, path), runtimeFieldOverride: TransformerOverride.ForField) + if path == Path.Root && tpe.Underlying <:< Type[Some[Any]] => + runtimeFieldOverride + } + ) + def filterCurrentOverridesForLeft: Set[TransformerOverride.ForField] = ListSet.from( + runtimeOverrides.collect { + case (Path.AtSubtype(tpe, path), runtimeFieldOverride: TransformerOverride.ForField) + if path == Path.Root && tpe.Underlying <:< Type[Left[Any, Any]] => + runtimeFieldOverride + } + ) + def filterCurrentOverridesForRight: Set[TransformerOverride.ForField] = ListSet.from( + runtimeOverrides.collect { + case (Path.AtSubtype(tpe, path), runtimeFieldOverride: TransformerOverride.ForField) + if path == Path.Root && tpe.Underlying <:< Type[Right[Any, Any]] => + runtimeFieldOverride + } + ) + def filterCurrentOverridesForEveryItem: Set[TransformerOverride.ForField] = ListSet.from( + runtimeOverrides.collect { + case (Path.AtItem(path), runtimeFieldOverride: TransformerOverride.ForField) if path == Path.Root => + runtimeFieldOverride + } + ) + def filterCurrentOverridesForEveryMapKey: Set[TransformerOverride.ForField] = ListSet.from( + runtimeOverrides.collect { + case (Path.AtMapKey(path), runtimeFieldOverride: TransformerOverride.ForField) if path == Path.Root => + runtimeFieldOverride + } + ) + def filterCurrentOverridesForEveryMapValue: Set[TransformerOverride.ForField] = ListSet.from( + runtimeOverrides.collect { + case (Path.AtMapValue(path), runtimeFieldOverride: TransformerOverride.ForField) if path == Path.Root => + runtimeFieldOverride + } + ) def currentOverrideForConstructor: Option[TransformerOverride.ForConstructor] = runtimeOverridesForCurrent.collectFirst { case (_, runtimeConstructorOverride: TransformerOverride.ForConstructor) => runtimeConstructorOverride @@ -446,8 +487,8 @@ private[compiletime] trait Configurations { this: Derivation => case _ => extractTransformerFlags[Flags2](defaultFlags).setBoolFlag[Flag](value = false) } + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => - // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation reportError(s"Invalid internal TransformerFlags type shape: ${Type.prettyPrint[Flags]}!") // $COVERAGE-ON$ } @@ -527,8 +568,8 @@ private[compiletime] trait Configurations { this: Derivation => extractPath[FromPath], TransformerOverride.RenamedTo(extractPath[ToPath]) ) + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => - // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation reportError(s"Invalid internal TransformerOverrides type shape: ${Type.prettyPrint[Tail]}!!") // $COVERAGE-ON$ } @@ -554,8 +595,8 @@ private[compiletime] trait Configurations { this: Derivation => case ChimneyType.Path.EveryMapValue(init) => import init.Underlying as PathType2 extractPath[PathType2].everyMapValue + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => - // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation reportError(s"Invalid internal Path shape: ${Type.prettyPrint[PathType]}!!") // $COVERAGE-ON$ } diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformEitherToEitherRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformEitherToEitherRuleModule.scala index 3d6829d42..9e33a5fae 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformEitherToEitherRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformEitherToEitherRuleModule.scala @@ -4,9 +4,10 @@ import io.scalaland.chimney.internal.compiletime.DerivationResult import io.scalaland.chimney.internal.compiletime.derivation.transformer.Derivation import io.scalaland.chimney.partial -private[compiletime] trait TransformEitherToEitherRuleModule { this: Derivation => +private[compiletime] trait TransformEitherToEitherRuleModule { + this: Derivation & TransformProductToProductRuleModule => - import Type.Implicits.*, ChimneyType.Implicits.* + import Type.Implicits.*, ChimneyType.Implicits.*, TransformProductToProductRule.useOverrideIfPresentOr protected object TransformEitherToEitherRule extends Rule("EitherToEither") { @@ -27,26 +28,32 @@ private[compiletime] trait TransformEitherToEitherRuleModule { this: Derivation private def mapLeft[From, To, FromL: Type, FromR: Type, ToL: Type, ToR: Type](implicit ctx: TransformationContext[From, To] ): DerivationResult[Rule.ExpansionResult[To]] = - deriveRecursiveTransformationExpr[FromL, ToL]( - ctx.src.upcastToExprOf[Left[FromL, FromR]].value, - Path.Root.matching[Left[ToL, ToR]] - ).flatMap { (derivedToL: TransformationExpr[ToL]) => - // We're constructing: - // '{ Left( ${ derivedToL } ) /* from ${ src }.value */ } - DerivationResult.expanded(derivedToL.map(Expr.Either.Left[ToL, ToR](_).upcastToExprOf[To])) + useOverrideIfPresentOr("matchingLeft", ctx.config.filterCurrentOverridesForLeft) { + deriveRecursiveTransformationExpr[FromL, ToL]( + ctx.src.upcastToExprOf[Left[FromL, FromR]].value, + Path.Root.matching[Left[ToL, ToR]] + ) } + .flatMap { (derivedToL: TransformationExpr[ToL]) => + // We're constructing: + // '{ Left( ${ derivedToL } ) /* from ${ src }.value */ } + DerivationResult.expanded(derivedToL.map(Expr.Either.Left[ToL, ToR](_).upcastToExprOf[To])) + } private def mapRight[From, To, FromL: Type, FromR: Type, ToL: Type, ToR: Type](implicit ctx: TransformationContext[From, To] ): DerivationResult[Rule.ExpansionResult[To]] = - deriveRecursiveTransformationExpr[FromR, ToR]( - ctx.src.upcastToExprOf[Right[FromL, FromR]].value, - Path.Root.matching[Right[ToL, ToR]] - ).flatMap { (derivedToR: TransformationExpr[ToR]) => - // We're constructing: - // '{ Right( ${ derivedToR } ) /* from ${ src }.value */ } - DerivationResult.expanded(derivedToR.map(Expr.Either.Right[ToL, ToR](_).upcastToExprOf[To])) + useOverrideIfPresentOr("matchingRight", ctx.config.filterCurrentOverridesForRight) { + deriveRecursiveTransformationExpr[FromR, ToR]( + ctx.src.upcastToExprOf[Right[FromL, FromR]].value, + Path.Root.matching[Right[ToL, ToR]] + ) } + .flatMap { (derivedToR: TransformationExpr[ToR]) => + // We're constructing: + // '{ Right( ${ derivedToR } ) /* from ${ src }.value */ } + DerivationResult.expanded(derivedToR.map(Expr.Either.Right[ToL, ToR](_).upcastToExprOf[To])) + } private def mapEither[From, To, FromL: Type, FromR: Type, ToL: Type, ToR: Type](implicit ctx: TransformationContext[From, To] @@ -54,13 +61,17 @@ private[compiletime] trait TransformEitherToEitherRuleModule { this: Derivation val toLeftResult = ExprPromise .promise[FromL](ExprPromise.NameGenerationStrategy.FromPrefix("left")) .traverse { (leftExpr: Expr[FromL]) => - deriveRecursiveTransformationExpr[FromL, ToL](leftExpr, Path.Root.matching[Left[ToL, ToR]]) + useOverrideIfPresentOr("matchingLeft", ctx.config.filterCurrentOverridesForLeft) { + deriveRecursiveTransformationExpr[FromL, ToL](leftExpr, Path.Root.matching[Left[ToL, ToR]]) + } } val toRightResult = ExprPromise .promise[FromR](ExprPromise.NameGenerationStrategy.FromPrefix("right")) .traverse { (rightExpr: Expr[FromR]) => - deriveRecursiveTransformationExpr[FromR, ToR](rightExpr, Path.Root.matching[Right[ToL, ToR]]) + useOverrideIfPresentOr("matchingRight", ctx.config.filterCurrentOverridesForRight) { + deriveRecursiveTransformationExpr[FromR, ToR](rightExpr, Path.Root.matching[Right[ToL, ToR]]) + } } val inLeft = diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformIterableToIterableRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformIterableToIterableRuleModule.scala index 94e7651ba..0e88d5708 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformIterableToIterableRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformIterableToIterableRuleModule.scala @@ -6,9 +6,10 @@ import io.scalaland.chimney.partial import scala.collection.compat.Factory -private[compiletime] trait TransformIterableToIterableRuleModule { this: Derivation => +private[compiletime] trait TransformIterableToIterableRuleModule { + this: Derivation & TransformProductToProductRuleModule => - import Type.Implicits.*, ChimneyType.Implicits.* + import Type.Implicits.*, ChimneyType.Implicits.*, TransformProductToProductRule.useOverrideIfPresentOr protected object TransformIterableToIterableRule extends Rule("IterableToIterable") { @@ -55,12 +56,16 @@ private[compiletime] trait TransformIterableToIterableRuleModule { this: Derivat val toKeyResult = ExprPromise .promise[FromK](ExprPromise.NameGenerationStrategy.FromPrefix("key")) .traverse { key => - deriveRecursiveTransformationExpr[FromK, ToK](key, Path.Root.everyMapKey).map(_.ensurePartial -> key) + useOverrideIfPresentOr("everyMapKey", ctx.config.filterCurrentOverridesForEveryMapKey) { + deriveRecursiveTransformationExpr[FromK, ToK](key, Path.Root.everyMapKey) + }.map(_.ensurePartial -> key) } val toValueResult = ExprPromise .promise[FromV](ExprPromise.NameGenerationStrategy.FromPrefix("value")) .traverse { value => - deriveRecursiveTransformationExpr[FromV, ToV](value, Path.Root.everyMapValue).map(_.ensurePartial) + useOverrideIfPresentOr("everyMapValue", ctx.config.filterCurrentOverridesForEveryMapValue) { + deriveRecursiveTransformationExpr[FromV, ToV](value, Path.Root.everyMapValue) + }.map(_.ensurePartial) } toKeyResult.parTuple(toValueResult).flatMap { case (toKeyP, toValueP) => @@ -113,12 +118,14 @@ private[compiletime] trait TransformIterableToIterableRuleModule { this: Derivat ExprPromise .promise[InnerFrom](ExprPromise.NameGenerationStrategy.FromExpr(ctx.src)) .traverse { (newFromSrc: Expr[InnerFrom]) => - deriveRecursiveTransformationExpr[InnerFrom, InnerTo](newFromSrc, Path.Root.everyItem) + useOverrideIfPresentOr("everyItem", ctx.config.filterCurrentOverridesForEveryItem) { + deriveRecursiveTransformationExpr[InnerFrom, InnerTo](newFromSrc, Path.Root.everyItem) + } } .flatMap { (to2P: ExprPromise[InnerFrom, TransformationExpr[InnerTo]]) => to2P.foldTransformationExpr { (totalP: ExprPromise[InnerFrom, Expr[InnerTo]]) => // TODO: restore .map implementation - if (Type[InnerFrom] =:= Type[InnerTo]) { + if (Type[InnerFrom] =:= Type[InnerTo] && ctx.config.areOverridesEmpty) { def srcToFactory[ToOrPartialTo: Type]( factory: Expr[Factory[InnerTo, ToOrPartialTo]] ): Expr[ToOrPartialTo] = diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformMapToMapRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformMapToMapRuleModule.scala index eed85baf5..91a1dea09 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformMapToMapRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformMapToMapRuleModule.scala @@ -6,9 +6,10 @@ import io.scalaland.chimney.partial import scala.collection.compat.Factory -private[compiletime] trait TransformMapToMapRuleModule { this: Derivation with TransformIterableToIterableRuleModule => +private[compiletime] trait TransformMapToMapRuleModule { + this: Derivation & TransformIterableToIterableRuleModule & TransformProductToProductRuleModule => - import Type.Implicits.*, ChimneyType.Implicits.* + import Type.Implicits.*, ChimneyType.Implicits.*, TransformProductToProductRule.useOverrideIfPresentOr protected object TransformMapToMapRule extends Rule("MapToMap") { @@ -74,12 +75,16 @@ private[compiletime] trait TransformMapToMapRuleModule { this: Derivation with T val toKeyResult = ExprPromise .promise[FromK](ExprPromise.NameGenerationStrategy.FromPrefix("key")) .traverse { key => - deriveRecursiveTransformationExpr[FromK, ToK](key, Path.Root.everyMapKey).map(_.ensureTotal) + useOverrideIfPresentOr("everyMapKey", ctx.config.filterCurrentOverridesForEveryMapKey) { + deriveRecursiveTransformationExpr[FromK, ToK](key, Path.Root.everyMapKey) + }.map(_.ensureTotal) } val toValueResult = ExprPromise .promise[FromV](ExprPromise.NameGenerationStrategy.FromPrefix("value")) .traverse { value => - deriveRecursiveTransformationExpr[FromV, ToV](value, Path.Root.everyMapValue).map(_.ensureTotal) + useOverrideIfPresentOr("everyMapValue", ctx.config.filterCurrentOverridesForEveryMapValue) { + deriveRecursiveTransformationExpr[FromV, ToV](value, Path.Root.everyMapValue) + }.map(_.ensureTotal) } toKeyResult.parTuple(toValueResult).flatMap { case (toKeyP, toValueP) => @@ -117,12 +122,16 @@ private[compiletime] trait TransformMapToMapRuleModule { this: Derivation with T val toKeyResult = ExprPromise .promise[FromK](ExprPromise.NameGenerationStrategy.FromPrefix("key")) .traverse { key => - deriveRecursiveTransformationExpr[FromK, ToK](key, Path.Root.everyMapKey).map(_.ensurePartial -> key) + useOverrideIfPresentOr("everyMapKey", ctx.config.filterCurrentOverridesForEveryMapKey) { + deriveRecursiveTransformationExpr[FromK, ToK](key, Path.Root.everyMapKey) + }.map(_.ensurePartial -> key) } val toValueResult = ExprPromise .promise[FromV](ExprPromise.NameGenerationStrategy.FromPrefix("value")) .traverse { value => - deriveRecursiveTransformationExpr[FromV, ToV](value, Path.Root.everyMapValue).map(_.ensurePartial -> value) + useOverrideIfPresentOr("everyMapValue", ctx.config.filterCurrentOverridesForEveryMapValue) { + deriveRecursiveTransformationExpr[FromV, ToV](value, Path.Root.everyMapValue) + }.map(_.ensurePartial -> value) } toKeyResult.parTuple(toValueResult).flatMap { case (toKeyP, toValueP) => diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformOptionToOptionRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformOptionToOptionRuleModule.scala index 7c0a85034..6b4da6c05 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformOptionToOptionRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformOptionToOptionRuleModule.scala @@ -4,9 +4,10 @@ import io.scalaland.chimney.internal.compiletime.DerivationResult import io.scalaland.chimney.internal.compiletime.derivation.transformer.Derivation import io.scalaland.chimney.partial -private[compiletime] trait TransformOptionToOptionRuleModule { this: Derivation => +private[compiletime] trait TransformOptionToOptionRuleModule { + this: Derivation & TransformProductToProductRuleModule => - import Type.Implicits.*, ChimneyType.Implicits.* + import Type.Implicits.*, ChimneyType.Implicits.*, TransformProductToProductRule.useOverrideIfPresentOr protected object TransformOptionToOptionRule extends Rule("OptionToOption") { @@ -36,7 +37,9 @@ private[compiletime] trait TransformOptionToOptionRuleModule { this: Derivation ExprPromise .promise[InnerFrom](ExprPromise.NameGenerationStrategy.FromType) .traverse { (newFromExpr: Expr[InnerFrom]) => - deriveRecursiveTransformationExpr[InnerFrom, InnerTo](newFromExpr, Path.Root.matching[Some[InnerTo]]) + useOverrideIfPresentOr("matchingSome", ctx.config.filterCurrentOverridesForSome) { + deriveRecursiveTransformationExpr[InnerFrom, InnerTo](newFromExpr, Path.Root.matching[Some[InnerTo]]) + } } .flatMap { (derivedToExprPromise: ExprPromise[InnerFrom, TransformationExpr[InnerTo]]) => derivedToExprPromise.foldTransformationExpr { (totalP: ExprPromise[InnerFrom, Expr[InnerTo]]) => diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala index aafac8805..4ebf800a3 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformProductToProductRuleModule.scala @@ -176,7 +176,10 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio foundOverrides, flags.getFieldNameComparison.toString ) - case (_, value) => useOverride[From, To, CtorParam](toName, value) + case (_, value) => + useOverride[From, To, CtorParam](toName, value).flatMap( + DerivationResult.existential[TransformationExpr, CtorParam](_) + ) } .orElse { val ambiguityOrPossibleSourceField = @@ -270,16 +273,16 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio } // TODO: perhaps we should NOT pass To's field name as From's field name when providing errors to overrides? - private def useOverride[From, To, CtorParam: Type]( + def useOverride[From, To, CtorParam: Type]( toName: String, runtimeFieldOverride: TransformerOverride.ForField )(implicit ctx: TransformationContext[From, To] - ): DerivationResult[Existential[TransformationExpr]] = runtimeFieldOverride match { + ): DerivationResult[TransformationExpr[CtorParam]] = runtimeFieldOverride match { case TransformerOverride.Const(runtimeData) => // We're constructing: // '{ ${ runtimeDataStore }(idx).asInstanceOf[$ctorParam] } - DerivationResult.existential[TransformationExpr, CtorParam]( + DerivationResult.pure( TransformationExpr.fromTotal( runtimeData.asInstanceOfExpr[CtorParam] ) @@ -291,7 +294,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // .asInstanceOf[partial.Result[$ctorParam]] // .prependErrorPath(PathElement.Accessor("toName")) // } - DerivationResult.existential[TransformationExpr, CtorParam]( + DerivationResult.pure( TransformationExpr.fromPartial( runtimeData .asInstanceOfExpr[partial.Result[CtorParam]] @@ -306,7 +309,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio case TransformationContext.ForTotal(_) => // We're constructing: // '{ ${ runtimeDataStore }(idx).asInstanceOf[$OriginalFrom => $CtorParam](${ originalSrc }) } - DerivationResult.existential[TransformationExpr, CtorParam]( + DerivationResult.pure( TransformationExpr.fromTotal( runtimeData.asInstanceOfExpr[OriginalFrom => CtorParam].apply(originalSrc) ) @@ -320,7 +323,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // .apply(${ originalSrc }) // .prependErrorPath(PathElement.Accessor("toName")) // } - DerivationResult.existential[TransformationExpr, CtorParam]( + DerivationResult.pure( TransformationExpr.fromPartial( ChimneyExpr.PartialResult .fromFunction(runtimeData.asInstanceOfExpr[OriginalFrom => CtorParam]) @@ -341,7 +344,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // .prependErrorPath(PathElement.Accessor("toName")) // } import ctx.originalSrc.{Underlying as OriginalFrom, value as originalSrc} - DerivationResult.existential[TransformationExpr, CtorParam]( + DerivationResult.pure( TransformationExpr.fromPartial( runtimeData .asInstanceOfExpr[OriginalFrom => partial.Result[CtorParam]] @@ -406,7 +409,7 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio .transformWith { expr => // If we derived partial.Result[$ctorParam] we are appending: // ${ derivedToElement }.prependErrorPath(...).prependErrorPath(...) // sourcePath - DerivationResult.existential[TransformationExpr, CtorParam](appendPath(expr, sourcePath)) + DerivationResult.pure(appendPath(expr, sourcePath)) } { errors => appendMissingTransformer[From, To, ExtractedSrc, CtorParam](errors, toName) } @@ -414,6 +417,23 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio } } + // Exposes logic for: OptionToOption, EitherToEither, ... + def useOverrideIfPresentOr[From, To, CtorParam: Type]( + toName: String, + runtimeFieldOverrides: Set[TransformerOverride.ForField] + )(whenAbsent: => DerivationResult[TransformationExpr[CtorParam]])(implicit + ctx: TransformationContext[From, To] + ): DerivationResult[TransformationExpr[CtorParam]] = runtimeFieldOverrides.toList match { + case Nil => + whenAbsent + case runtimeFieldOverride :: Nil => + TransformProductToProductRule.useOverride[From, To, CtorParam](toName, runtimeFieldOverride) + // $COVERAGE-OFF$Config parsing dedupliate values + case runtimeFieldOverrides => + DerivationResult.assertionError(s"Unexpected multiple overrides: ${runtimeFieldOverrides.mkString(", ")}") + // $COVERAGE-ON$ + } + private def useExtractor[From, To, CtorParam: Type]( ctorTargetType: Product.Parameter.TargetType, fromName: String, @@ -515,8 +535,8 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio // }, ${ failFast }) } import res1.{Underlying as Res1, value as result1Expr}, res2.{Underlying as Res2, value as result2Expr} ctx match { + // $COVERAGE-OFF$should never happen unless we messed up case TransformationContext.ForTotal(_) => - // $COVERAGE-OFF$should never happen unless we messed up assertionFailed("Expected partial while got total") // $COVERAGE-ON$ case TransformationContext.ForPartial(_, failFast) => @@ -668,8 +688,8 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio } ctx match { + // $COVERAGE-OFF$should never happen unless we messed up case TransformationContext.ForTotal(_) => - // $COVERAGE-OFF$should never happen unless we messed up assertionFailed("Expected partial, got total") // $COVERAGE-ON$ case TransformationContext.ForPartial(_, failFast) => @@ -711,16 +731,16 @@ private[compiletime] trait TransformProductToProductRuleModule { this: Derivatio private def appendMissingTransformer[From, To, SourceField: Type, TargetField: Type]( errors: DerivationErrors, toName: String - )(implicit ctx: TransformationContext[From, To]) = { + )(implicit ctx: TransformationContext[From, To]): DerivationResult[Nothing] = { val newError = DerivationResult.missingFieldTransformer[ From, To, SourceField, TargetField, - Existential[TransformationExpr] + TransformationExpr[TargetField] ](toName) val oldErrors = DerivationResult.fail(errors) - newError.parTuple(oldErrors).map[Existential[TransformationExpr]](_ => ???) + newError.parTuple(oldErrors).map[Nothing](_ => ???) } // Stub to use when the setter's return type is not Unit and nonUnitBeanSetters flag is off. diff --git a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala index 97c916f7c..a2f27672e 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/internal/compiletime/derivation/transformer/rules/TransformSealedHierarchyToSealedHierarchyRuleModule.scala @@ -115,8 +115,8 @@ private[compiletime] trait TransformSealedHierarchyToSealedHierarchyRuleModule { TransformationExpr.fromPartial(partialExpr.asInstanceOfExpr[partial.Result[To]]) } ) + // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation case _ => - // $COVERAGE-OFF$should never happen unless someone mess around with type-level representation DerivationResult.assertionError(s"Unexpected path: $targetPath") // $COVERAGE-ON$ }) diff --git a/chimney/src/test/scala/io/scalaland/chimney/IssuesSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/IssuesSpec.scala index 437eb03d1..162b034ee 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/IssuesSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/IssuesSpec.scala @@ -760,4 +760,13 @@ class IssuesSpec extends ChimneySpec { import Issue498.* (Foo.Sub1("test"): Foo).into[Bar].withFieldConst(_.b, 1).transform ==> Bar.Sub1("test", 1) } + + test("fix issue #579") { + import Issue579.* + Foo(Some(Bar(List(Baz(a = 1, b = "a", c = 10.0), Baz(a = 2, b = "b", c = 20.0))))) + .into[Foo] + .withFieldConst(_.bar.matchingSome.baz.everyItem.a, 10) + .withFieldConst(_.bar.matchingSome.baz.everyItem.b, "new") + .transform ==> Foo(Some(Bar(List(Baz(a = 10, b = "new", c = 10.0), Baz(a = 10, b = "new", c = 20.0))))) + } } diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerIntegrationsSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerIntegrationsSpec.scala index bc0c0c2b6..8a75bee32 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerIntegrationsSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerIntegrationsSpec.scala @@ -175,7 +175,14 @@ class PartialTransformerIntegrationsSpec extends ChimneySpec { } } - // TODO: matchingSome + test("transform into OptionalValue with an override") { + "abc".intoPartial[Possible[String]].withFieldConst(_.matchingSome, "def").transform.asOption ==> Some( + Possible("def") + ) + Option("abc").intoPartial[Possible[String]].withFieldConst(_.matchingSome, "def").transform.asOption ==> Some( + Possible("def") + ) + } test( "transform TotallyBuildIterable/PartiallyBuildIterable to TotallyBuildIterable/PartiallyBuildIterable, using Total Transformer for inner type transformation" diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerLensLikeSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerLensLikeSpec.scala new file mode 100644 index 000000000..ef4b4afbb --- /dev/null +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerLensLikeSpec.scala @@ -0,0 +1,113 @@ +package io.scalaland.chimney + +import io.scalaland.chimney.dsl.* +import io.scalaland.chimney.fixtures.lenslike.* + +class PartialTransformerLensLikeSpec extends ChimneySpec { + + test("update case class field") { + Foo(10, "example").intoPartial[Foo[Int]].withFieldConst(_.value, 20).transform.asOption ==> Some(Foo(20, "example")) + Foo(Foo(10, "example"), "example") + .intoPartial[Foo[Foo[Int]]] + .withFieldConst(_.value.value, 20) + .transform + .asOption ==> Some(Foo(Foo(20, "example"), "example")) + } + + test("update sealed hierarchy") { + Foo[Bar[Int]](Bar.Baz(10), "example") + .intoPartial[Foo[Bar[Int]]] + .withFieldConst(_.value.matching[Bar.Baz[Int]].value, 20) + .transform + .asOption ==> Some(Foo[Bar[Int]](Bar.Baz(20), "example")) + (Bar.Baz(Foo(10, "example")): Bar[Foo[Int]]) + .intoPartial[Bar[Foo[Int]]] + .withFieldConst(_.matching[Bar.Baz[Foo[Int]]].value.value, 20) + .transform + .asOption ==> Some(Bar.Baz(Foo(20, "example"))) + } + + test("update case class with Option") { + WithOption(Some(Foo(10, "example"))) + .intoPartial[WithOption[Foo[Int]]] + .withFieldConst(_.option.matchingSome.value, 20) + .transform + .asOption ==> Some(WithOption(Some(Foo(20, "example")))) + Foo(WithOption(Some(10)), "example") + .intoPartial[Foo[WithOption[Int]]] + .withFieldConst(_.value.option.matchingSome, 20) + .transform + .asOption ==> Some(Foo(WithOption(Some(20)), "example")) + } + + test("update case class with Either") { + WithEither[Foo[Int], Foo[Int]](Left(Foo(10, "example"))) + .intoPartial[WithEither[Foo[Int], Foo[Int]]] + .withFieldConst(_.either.matchingLeft.value, 20) + .withFieldConst(_.either.matchingRight.value, 30) + .transform + .asOption ==> Some(WithEither(Left(Foo(20, "example")))) + WithEither[Foo[Int], Foo[Int]](Right(Foo(10, "example"))) + .intoPartial[WithEither[Foo[Int], Foo[Int]]] + .withFieldConst(_.either.matchingLeft.value, 20) + .withFieldConst(_.either.matchingRight.value, 30) + .transform + .asOption ==> Some(WithEither(Right(Foo(30, "example")))) + Foo[WithEither[Int, Int]](WithEither(Left(10)), "example") + .intoPartial[Foo[WithEither[Int, Int]]] + .withFieldConst(_.value.either.matchingLeft, 20) + .withFieldConst(_.value.either.matchingRight, 30) + .transform + .asOption ==> Some(Foo(WithEither(Left(20)), "example")) + Foo[WithEither[Int, Int]](WithEither(Right(10)), "example") + .intoPartial[Foo[WithEither[Int, Int]]] + .withFieldConst(_.value.either.matchingLeft, 20) + .withFieldConst(_.value.either.matchingRight, 30) + .transform + .asOption ==> Some(Foo(WithEither(Right(30)), "example")) + } + + test("update case class with collection") { + WithList(List(Foo(10, "example"))) + .intoPartial[WithList[Foo[Int]]] + .withFieldConst(_.list.everyItem.value, 20) + .transform + .asOption ==> Some(WithList(List(Foo(20, "example")))) + Foo(WithList(List(10)), "example") + .intoPartial[Foo[WithList[Int]]] + .withFieldConst(_.value.list.everyItem, 20) + .transform + .asOption ==> Some(Foo(WithList(List(20)), "example")) + } + + test("update case class with Map") { + WithMap[Foo[Int], Foo[Int]](Map(Foo(10, "example") -> Foo(20, "example2"))) + .intoPartial[WithMap[Foo[Int], Foo[Int]]] + .withFieldConst(_.map.everyMapKey.value, 30) + .withFieldConst(_.map.everyMapValue.value, 40) + .transform + .asOption ==> Some(WithMap[Foo[Int], Foo[Int]](Map(Foo(30, "example") -> Foo(40, "example2")))) + Foo[WithMap[Int, Int]](WithMap(Map(10 -> 20)), "example") + .intoPartial[Foo[WithMap[Int, Int]]] + .withFieldConst(_.value.map.everyMapKey, 30) + .withFieldConst(_.value.map.everyMapValue, 40) + .transform + .asOption ==> Some(Foo[WithMap[Int, Int]](WithMap(Map(30 -> 40)), "example")) + } + + test("update deep complex nesting") { + Foo( + WithOption(Some(WithList(List(WithMap(Map(10 -> WithEither[Int, Foo[Int]](Right(Foo(10, "example2"))))))))), + "example" + ) + .intoPartial[Foo[WithOption[WithList[WithMap[Int, WithEither[Int, Foo[Int]]]]]]] + .withFieldConst(_.value.option.matchingSome.list.everyItem.map.everyMapValue.either.matchingRight.value, 20) + .transform + .asOption ==> Some( + Foo( + WithOption(Some(WithList(List(WithMap(Map(10 -> WithEither[Int, Foo[Int]](Right(Foo(20, "example2"))))))))), + "example" + ) + ) + } +} diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerStdLibTypesSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerStdLibTypesSpec.scala index 56b9d638e..445784ceb 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerStdLibTypesSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerStdLibTypesSpec.scala @@ -197,7 +197,12 @@ class PartialTransformerStdLibTypesSpec extends ChimneySpec { } } - // TODO: matchingSome + test("transform into Option-type with an override") { + "abc".intoPartial[Option[String]].withFieldConst(_.matchingSome, "def").transform.asOption ==> Some(Some("def")) + Option("abc").intoPartial[Option[String]].withFieldConst(_.matchingSome, "def").transform.asOption ==> Some( + Some("def") + ) + } test("transform from Either-type into Either-type, using Total Transformer for inner types transformation") { implicit val intPrinter: Transformer[Int, String] = _.toString @@ -229,7 +234,30 @@ class PartialTransformerStdLibTypesSpec extends ChimneySpec { Right("x").transformIntoPartial[Right[Int, Int]].asOption ==> None } - // TODO: matchingLeft, matchingRight + test("transform Either-type with an override") { + (Left("a"): Either[String, String]) + .intoPartial[Either[String, String]] + .withFieldConst(_.matchingLeft, "b") + .withFieldConst(_.matchingRight, "c") + .transform + .asOption ==> Some(Left("b")) + (Right("a"): Either[String, String]) + .intoPartial[Either[String, String]] + .withFieldConst(_.matchingLeft, "b") + .withFieldConst(_.matchingRight, "c") + .transform + .asOption ==> Some(Right("c")) + Left("a") + .intoPartial[Either[String, String]] + .withFieldConst(_.matchingLeft, "b") + .transform + .asOption ==> Some(Left("b")) + Right("a") + .intoPartial[Either[String, String]] + .withFieldConst(_.matchingRight, "c") + .transform + .asOption ==> Some(Right("c")) + } test("transform Iterable-type to Iterable-type, using Total Transformer for inner type transformation") { implicit val intPrinter: Transformer[Int, String] = _.toString diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerIntegrationsSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerIntegrationsSpec.scala index 35edb7cde..232d07a9d 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerIntegrationsSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerIntegrationsSpec.scala @@ -32,7 +32,12 @@ class TotalTransformerIntegrationsSpec extends ChimneySpec { (null: String).transformInto[Possible[String]] ==> Possible.Nope } - // TODO: matchingSome + test("transform into OptionalValue with an override") { + Foo("abc").into[Possible[Foo]].withFieldConst(_.matchingSome.value, "def").transform ==> Possible(Foo("def")) + Option(Foo("abc")).into[Possible[Foo]].withFieldConst(_.matchingSome.value, "def").transform ==> Possible( + Foo("def") + ) + } test("transform from TotallyBuildIterable to TotallyBuildIterable") { CustomCollection.of(Foo("a")).transformInto[Seq[Bar]] ==> Seq(Bar("a")) diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerLensLikeSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerLensLikeSpec.scala new file mode 100644 index 000000000..79ad7094a --- /dev/null +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerLensLikeSpec.scala @@ -0,0 +1,97 @@ +package io.scalaland.chimney + +import io.scalaland.chimney.dsl.* +import io.scalaland.chimney.fixtures.lenslike.* + +class TotalTransformerLensLikeSpec extends ChimneySpec { + + test("update case class field") { + Foo(10, "example").into[Foo[Int]].withFieldConst(_.value, 20).transform ==> Foo(20, "example") + Foo(Foo(10, "example"), "example") + .into[Foo[Foo[Int]]] + .withFieldConst(_.value.value, 20) + .transform ==> Foo(Foo(20, "example"), "example") + } + + test("update sealed hierarchy") { + Foo[Bar[Int]](Bar.Baz(10), "example") + .into[Foo[Bar[Int]]] + .withFieldConst(_.value.matching[Bar.Baz[Int]].value, 20) + .transform ==> Foo[Bar[Int]](Bar.Baz(20), "example") + (Bar.Baz(Foo(10, "example")): Bar[Foo[Int]]) + .into[Bar[Foo[Int]]] + .withFieldConst(_.matching[Bar.Baz[Foo[Int]]].value.value, 20) + .transform ==> Bar.Baz(Foo(20, "example")) + } + + test("update case class with Option") { + WithOption(Some(Foo(10, "example"))) + .into[WithOption[Foo[Int]]] + .withFieldConst(_.option.matchingSome.value, 20) + .transform ==> WithOption(Some(Foo(20, "example"))) + Foo(WithOption(Some(10)), "example") + .into[Foo[WithOption[Int]]] + .withFieldConst(_.value.option.matchingSome, 20) + .transform ==> Foo(WithOption(Some(20)), "example") + } + + test("update case class with Either") { + WithEither[Foo[Int], Foo[Int]](Left(Foo(10, "example"))) + .into[WithEither[Foo[Int], Foo[Int]]] + .withFieldConst(_.either.matchingLeft.value, 20) + .withFieldConst(_.either.matchingRight.value, 30) + .transform ==> WithEither(Left(Foo(20, "example"))) + WithEither[Foo[Int], Foo[Int]](Right(Foo(10, "example"))) + .into[WithEither[Foo[Int], Foo[Int]]] + .withFieldConst(_.either.matchingLeft.value, 20) + .withFieldConst(_.either.matchingRight.value, 30) + .transform ==> WithEither(Right(Foo(30, "example"))) + Foo[WithEither[Int, Int]](WithEither(Left(10)), "example") + .into[Foo[WithEither[Int, Int]]] + .withFieldConst(_.value.either.matchingLeft, 20) + .withFieldConst(_.value.either.matchingRight, 30) + .transform ==> Foo(WithEither(Left(20)), "example") + Foo[WithEither[Int, Int]](WithEither(Right(10)), "example") + .into[Foo[WithEither[Int, Int]]] + .withFieldConst(_.value.either.matchingLeft, 20) + .withFieldConst(_.value.either.matchingRight, 30) + .transform ==> Foo(WithEither(Right(30)), "example") + } + + test("update case class with collection") { + WithList(List(Foo(10, "example"))) + .into[WithList[Foo[Int]]] + .withFieldConst(_.list.everyItem.value, 20) + .transform ==> WithList(List(Foo(20, "example"))) + Foo(WithList(List(10)), "example") + .into[Foo[WithList[Int]]] + .withFieldConst(_.value.list.everyItem, 20) + .transform ==> Foo(WithList(List(20)), "example") + } + + test("update case class with Map") { + WithMap[Foo[Int], Foo[Int]](Map(Foo(10, "example") -> Foo(20, "example2"))) + .into[WithMap[Foo[Int], Foo[Int]]] + .withFieldConst(_.map.everyMapKey.value, 30) + .withFieldConst(_.map.everyMapValue.value, 40) + .transform ==> WithMap[Foo[Int], Foo[Int]](Map(Foo(30, "example") -> Foo(40, "example2"))) + Foo[WithMap[Int, Int]](WithMap(Map(10 -> 20)), "example") + .into[Foo[WithMap[Int, Int]]] + .withFieldConst(_.value.map.everyMapKey, 30) + .withFieldConst(_.value.map.everyMapValue, 40) + .transform ==> Foo[WithMap[Int, Int]](WithMap(Map(30 -> 40)), "example") + } + + test("update deep complex nesting") { + Foo( + WithOption(Some(WithList(List(WithMap(Map(10 -> WithEither[Int, Foo[Int]](Right(Foo(10, "example2"))))))))), + "example" + ) + .into[Foo[WithOption[WithList[WithMap[Int, WithEither[Int, Foo[Int]]]]]]] + .withFieldConst(_.value.option.matchingSome.list.everyItem.map.everyMapValue.either.matchingRight.value, 20) + .transform ==> Foo( + WithOption(Some(WithList(List(WithMap(Map(10 -> WithEither[Int, Foo[Int]](Right(Foo(20, "example2"))))))))), + "example" + ) + } +} diff --git a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerStdLibTypesSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerStdLibTypesSpec.scala index 12b4b721b..9e7c166a9 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerStdLibTypesSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/TotalTransformerStdLibTypesSpec.scala @@ -62,7 +62,10 @@ class TotalTransformerStdLibTypesSpec extends ChimneySpec { (null: String).transformInto[Option[String]] ==> None } - // TODO: matchingSome + test("transform into Option-type with an override") { + Foo("abc").into[Option[Foo]].withFieldConst(_.matchingSome.value, "def").transform ==> Some(Foo("def")) + Option(Foo("abc")).into[Option[Foo]].withFieldConst(_.matchingSome.value, "def").transform ==> Some(Foo("def")) + } test("transform from Either-type into Either-type") { (Left(Foo("a")): Either[Foo, Foo]).transformInto[Either[Bar, Bar]] ==> Left(Bar("a")) @@ -75,7 +78,20 @@ class TotalTransformerStdLibTypesSpec extends ChimneySpec { (Right("a"): Either[String, String]).transformInto[Either[String, String]] ==> Right("a") } - // TODO: matchingLeft, matchingRight + test("transform Either-type with an override") { + (Left(Foo("a")): Either[Foo, Foo]) + .into[Either[Bar, Bar]] + .withFieldConst(_.matchingLeft.value, "b") + .withFieldConst(_.matchingRight.value, "c") + .transform ==> Left(Bar("b")) + (Right(Foo("a")): Either[Foo, Foo]) + .into[Either[Bar, Bar]] + .withFieldConst(_.matchingLeft.value, "b") + .withFieldConst(_.matchingRight.value, "c") + .transform ==> Right(Bar("c")) + Left(Foo("a")).into[Either[Bar, Bar]].withFieldConst(_.matchingLeft.value, "b").transform ==> Left(Bar("b")) + Right(Foo("a")).into[Either[Bar, Bar]].withFieldConst(_.matchingRight.value, "c").transform ==> Right(Bar("c")) + } test("transform from Iterable-type to Iterable-type") { Seq(Foo("a")).transformInto[Seq[Bar]] ==> Seq(Bar("a")) diff --git a/chimney/src/test/scala/io/scalaland/chimney/fixtures/Issues.scala b/chimney/src/test/scala/io/scalaland/chimney/fixtures/Issues.scala index 3eb7d9521..8524314ce 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/fixtures/Issues.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/fixtures/Issues.scala @@ -208,3 +208,9 @@ object Issue498 { case class Sub2(a: String, b: Int) extends Bar } } + +object Issue579 { + case class Foo(bar: Option[Bar]) + case class Bar(baz: List[Baz]) + case class Baz(a: Int, b: String, c: Double) +} diff --git a/chimney/src/test/scala/io/scalaland/chimney/fixtures/lenslike/lenslike.scala b/chimney/src/test/scala/io/scalaland/chimney/fixtures/lenslike/lenslike.scala new file mode 100644 index 000000000..5c88db7f5 --- /dev/null +++ b/chimney/src/test/scala/io/scalaland/chimney/fixtures/lenslike/lenslike.scala @@ -0,0 +1,16 @@ +package io.scalaland.chimney.fixtures.lenslike + +case class Foo[A](value: A, meta: String) + +case class WithOption[A](option: Option[A]) + +case class WithEither[L, R](either: Either[L, R]) + +case class WithList[A](list: List[A]) + +case class WithMap[K, V](map: Map[K, V]) + +sealed trait Bar[A] +object Bar { + case class Baz[A](value: A) extends Bar[A] +} diff --git a/docs/docs/cookbook.md b/docs/docs/cookbook.md index a94de969e..efdecdbdb 100644 --- a/docs/docs/cookbook.md +++ b/docs/docs/cookbook.md @@ -1405,6 +1405,138 @@ Each of these transformations is provided by the same import: import io.scalaland.chimney.protobufs._ ``` +## Lens-like use cases + +Chimney can be used in some cases where optics/prisms are normally used. Let us demonstrate them by reimplementing +some [Quicklens](https://github.com/softwaremill/quicklens) example, where we would update a value of nested case +classes. + +!!! example + + Let' set we have a nested structure: + + ```scala + case class Foo(bar: Option[Bar]) + case class Bar(baz: List[Baz]) + case class Baz(a: Int, b: String, c: Double) + + val foo = Foo( + bar = Some( + Baz( + baz = List( + Baz(a = 1, b = "a", c = 10.0), + Baz(a = 2, b = "b", c = 20.0) + ) + ) + ) + ) + ``` + + Let's say we need to update all `a` in Baz to `10` and all `b` to `"new"`. With Quicklens we could implement it like + this: + + ```scala + //> using dep com.softwaremill.quicklens::quicklens::{{ libraries.quicklens }} + //> using dep com.lihaoyi::pprint::{{ libraries.pprint }} + + case class Foo(bar: Option[Bar]) + case class Bar(baz: List[Baz]) + case class Baz(a: Int, b: String, c: Double) + + val foo = Foo( + bar = Some( + Bar( + baz = List( + Baz(a = 1, b = "a", c = 10.0), + Baz(a = 2, b = "b", c = 20.0) + ) + ) + ) + ) + + import com.softwaremill.quicklens._ + + pprint.pprintln( + foo + .modify(_.bar.each.baz.each.a).setTo(10) + .modify(_.bar.each.baz.each.b).setTo("new") + ) + // expected output: + // Foo( + // bar = Some( + // value = Bar(baz = List(Baz(a = 10, b = "new", c = 10.0), Baz(a = 10, b = "new", c = 20.0))) + // ) + // ) + ``` + + It could be translated to Chimney like this: + + ```scala + //> using dep io.scalaland::chimney::{{ chimney_version() }} + //> using dep com.lihaoyi::pprint::{{ libraries.pprint }} + + case class Foo(bar: Option[Bar]) + case class Bar(baz: List[Baz]) + case class Baz(a: Int, b: String, c: Double) + + val foo = Foo( + bar = Some( + Bar( + baz = List( + Baz(a = 1, b = "a", c = 10.0), + Baz(a = 2, b = "b", c = 20.0) + ) + ) + ) + ) + + import io.scalaland.chimney.dsl._ + + pprint.pprintln( + foo + .into[Foo] + .withFieldConst(_.bar.matchingSome.baz.everyItem.a, 10) + .withFieldConst(_.bar.matchingSome.baz.everyItem.b, "new") + .enableMacrosLogging + .transform + ) + // expected output: + // Foo( + // bar = Some( + // value = Bar(baz = List(Baz(a = 10, b = "new", c = 10.0), Baz(a = 10, b = "new", c = 20.0))) + // ) + // ) + ``` + +Some comparison between the two could be found in the table below: + +| Quicklens | Chimney | +|----------------------------------------|--------------------------------------------------------------------| +| `value.modify(path).setTo(fieldValue)` | `value.into[ValueType].withFieldConst(path, fieldValue).transform` | +| `.fieldName` | `.fieldName` | +| `.each` (collection, non-`Map`) | `.everyItem` | +| `.each` (collection, `Map`) | `.everyMapValue` | +| `.each` (`Option`) | `.matchingSome` | +| `.eachLeft` | `.matchingLeft` | +| `.eachRight` | `.matchingRight` | +| `.when[Subtype]` | `.matching[Subtype]` | + +Additionally, Chimney defines `.everyMapKey`. + +There are no Chimney counterparts for Quicklens': + + * `.at(idx)`/`.at(key)` (update specific index/map key, throwing if absent) + * `.index(idx)` (update specific index/map key, ignoring if absent) + * `.atOrElse(idx, value)` (update specific index/map key, using `value` if absent) + * `.eachWhere(predicate)` (update all items fulfilling the predicate) + * `.setToIf(predicate)(value)` (updates if predicate is fulfilled) + * `.setToIfDefined(option)` (updates using `Option`) + * `.using(f)` (update the field with specific fun) + +For these cases, a proper optics library (like Quicklens) is recommended. As you can see method names in Chimney DSL +were selected in such way that there should be no conflicts with other libraries, so you don't have to choose one - you +can pick up both. + ## Libraries with smart constructors Any type that uses a smart constructor (returning parsed result rather than throwing an exception) would require diff --git a/docs/docs/troubleshooting.md b/docs/docs/troubleshooting.md index 16a6e1440..19c551d01 100644 --- a/docs/docs/troubleshooting.md +++ b/docs/docs/troubleshooting.md @@ -19,8 +19,9 @@ If you: Swagger or AsyncAPI. - However, if you parse raw data into some structured data, that structured data can be used by Chimney to convert into e.g. domain model - - want to update immutable data by passing some path to the updated field and then provide a value - you need a lens - library like Quicklens or Monocle + - want to update immutable data by passing some path to the updated field and then provide a value - you MAY need + a lens library like Quicklens or Monocle, although Chimney has + [a limited support some lens use case](cookbook.md#lens-like-use-cases) - want to limit the amount of tests written and are wondering if automatic generation of such an important code is safe - you need to ask yourself: would you have the same dilemma if you were asked about generating JSON codecs? Would you wonder if you need to test them? (In our experience, yes). Could you remove the need to test them if you diff --git a/docs/mkdocs.yml b/docs/mkdocs.yml index 6723f8ad1..fa413eaf1 100644 --- a/docs/mkdocs.yml +++ b/docs/mkdocs.yml @@ -99,5 +99,6 @@ extra: henkan: "0.6.5" ducktape: "0.2.0" pprint: "0.9.0" + quicklens: "1.9.7" local: tag: !ENV [CI_LATEST_TAG, 'latest'] # used as git.tag fallback in Docker container