diff --git a/chimney/src/main/scala/io/scalaland/chimney/partial/Path.scala b/chimney/src/main/scala/io/scalaland/chimney/partial/Path.scala index 83147c498..66c5a8bb0 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/partial/Path.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/partial/Path.scala @@ -19,9 +19,22 @@ final case class Path(private val elements: List[PathElement]) extends AnyVal { * @since 0.7.0 */ def prepend(pathElement: PathElement): Path = elements match { + // Const shouldn't be modified (or we can overwrite several nested Consts with outer one, which is useless) case (_: PathElement.Const) :: _ => this - case (h: PathElement.Computed) :: t => if (h.sealPath) this else Path(h :: pathElement :: t) - case _ => Path(pathElement :: elements) + case (h: PathElement.Computed) :: t => + // sealed Computed should not be modified + if (h.sealPath || pathElement.isInstanceOf[PathElement.Computed]) this + // Computed inside Const should stay Computed - but it should be sealed again + else if (pathElement.isInstanceOf[PathElement.Const]) { + h.sealPath = true + this + } + // Unsealed Computed can be prepended + else Path(h :: pathElement :: t) + case _ => + // If something prepends Const it should replace the content + if (pathElement.isInstanceOf[PathElement.Const]) Path(pathElement :: Nil) + else Path(pathElement :: elements) } /** Unseals the [[io.scalaland.chimney.partial.Path]] of current [[io.scalaland.chimney.partial.Error]]. @@ -54,7 +67,7 @@ final case class Path(private val elements: List[PathElement]) extends AnyVal { var computedSuffix: String = null while (it.hasNext) { val curr = it.next() - if (computedSuffix == null && curr.isInstanceOf[PathElement.Computed]) { + if (computedSuffix == null && (curr.isInstanceOf[PathElement.Computed])) { computedSuffix = curr.asString } else { if (sb.nonEmpty && PathElement.shouldPrependWithDot(curr)) { diff --git a/chimney/src/main/scala/io/scalaland/chimney/partial/PathElement.scala b/chimney/src/main/scala/io/scalaland/chimney/partial/PathElement.scala index 6141e9628..0acdde6b3 100644 --- a/chimney/src/main/scala/io/scalaland/chimney/partial/PathElement.scala +++ b/chimney/src/main/scala/io/scalaland/chimney/partial/PathElement.scala @@ -88,9 +88,10 @@ object PathElement { * @since 1.6.0 */ final case class Computed(targetPath: String) extends PathElement { - // TODO: description - var sealPath: Boolean = true override def asString: String = s"" + + /** Flag preventing appending when the whole path was already precomputed. */ + var sealPath: Boolean = true } /** Specifies if path element in conventional string representation should be prepended with a dot. @@ -105,7 +106,9 @@ object PathElement { case _: Index => false case _: MapValue => false case _: MapKey => true + // $COVERAGE-OFF$Required by exhaustive check but never really used in runtime case _: Const => false case _: Computed => false + // $COVERAGE-ON$ } } diff --git a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerErrorPathSpec.scala b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerErrorPathSpec.scala index 2fb51bb22..d25ad9449 100644 --- a/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerErrorPathSpec.scala +++ b/chimney/src/test/scala/io/scalaland/chimney/PartialTransformerErrorPathSpec.scala @@ -61,29 +61,138 @@ class PartialTransformerErrorPathSpec extends ChimneySpec { case class Bar(inner: InnerBar, b: Int) case class InnerBar(int1: Int, int2: Int, double: Double) - implicit val innerT: PartialTransformer[InnerFoo, InnerBar] = PartialTransformer - .define[InnerFoo, InnerBar] - .withFieldRenamed(_.str, _.int1) - .withFieldConstPartial(_.int2, intParserOpt.transform("notint")) - .withFieldComputedPartial(_.double, foo => partial.Result.fromOption(foo.str.parseDouble)) - .buildTransformer - - val result = Foo(InnerFoo("aaa")) - .intoPartial[Bar] - .withFieldConstPartial(_.b, intParserOpt.transform("bbb")) - .transform - result.asErrorPathMessages ==> Iterable( - "inner.str" -> partial.ErrorMessage.EmptyValue, - "" -> partial.ErrorMessage.EmptyValue, - "inner => " -> partial.ErrorMessage.EmptyValue, - "" -> partial.ErrorMessage.EmptyValue - ) - result.asErrorPathMessageStrings ==> Iterable( - "inner.str" -> "empty value", - "" -> "empty value", - "inner => " -> "empty value", - "" -> "empty value" - ) + locally { + // withFieldComputedPartial + implicit val innerT: PartialTransformer[InnerFoo, InnerBar] = PartialTransformer + .define[InnerFoo, InnerBar] + .withFieldRenamed(_.str, _.int1) + .withFieldConstPartial(_.int2, intParserOpt.transform("notint")) + .withFieldComputedPartial(_.double, foo => partial.Result.fromOption(foo.str.parseDouble)) + .buildTransformer + + val result = Foo(InnerFoo("aaa")) + .intoPartial[Bar] + .withFieldConstPartial(_.b, intParserOpt.transform("bbb")) + .transform + result.asErrorPathMessages ==> Iterable( + "inner.str" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "inner => " -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue + ) + result.asErrorPathMessageStrings ==> Iterable( + "inner.str" -> "empty value", + "" -> "empty value", + "inner => " -> "empty value", + "" -> "empty value" + ) + + val result2 = List(Map("value" -> Foo(InnerFoo("aaa")))) + .intoPartial[List[Map[String, Bar]]] + .withFieldConstPartial(_.everyItem.everyMapValue.b, intParserOpt.transform("bbb")) + .transform + result2.asErrorPathMessages ==> Iterable( + "(0)(value).inner.str" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "(0)(value).inner => " -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue + ) + result2.asErrorPathMessageStrings ==> Iterable( + "(0)(value).inner.str" -> "empty value", + "" -> "empty value", + "(0)(value).inner => " -> "empty value", + "" -> "empty value" + ) + + val result3 = List(Map("value" -> Foo(InnerFoo("aaa")))) + .intoPartial[List[Map[String, Bar]]] + .withFieldConstPartial(_.everyItem.everyMapValue.b, InnerFoo("aaa").transformIntoPartial[InnerBar].map(_ => 0)) + .transform + result3.asErrorPathMessages ==> Iterable( + "(0)(value).inner.str" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "(0)(value).inner => " -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "(0)(value) => " -> partial.ErrorMessage.EmptyValue + ) + result3.asErrorPathMessageStrings ==> Iterable( + "(0)(value).inner.str" -> "empty value", + "" -> "empty value", + "(0)(value).inner => " -> "empty value", + "" -> "empty value", + "" -> "empty value", + "(0)(value) => " -> "empty value" + ) + } + + locally { + // withFieldComputedPartialFrom + implicit val innerT: PartialTransformer[InnerFoo, InnerBar] = PartialTransformer + .define[InnerFoo, InnerBar] + .withFieldRenamed(_.str, _.int1) + .withFieldConstPartial(_.int2, intParserOpt.transform("notint")) + .withFieldComputedPartialFrom(_.str)(_.double, str => partial.Result.fromOption(str.parseDouble)) + .buildTransformer + + val result = Foo(InnerFoo("aaa")) + .intoPartial[Bar] + .withFieldConstPartial(_.b, intParserOpt.transform("bbb")) + .transform + result.asErrorPathMessages ==> Iterable( + "inner.str" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "inner.str => " -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue + ) + result.asErrorPathMessageStrings ==> Iterable( + "inner.str" -> "empty value", + "" -> "empty value", + "inner.str => " -> "empty value", + "" -> "empty value" + ) + + val result2 = List(Map("value" -> Foo(InnerFoo("aaa")))) + .intoPartial[List[Map[String, Bar]]] + .withFieldConstPartial(_.everyItem.everyMapValue.b, intParserOpt.transform("bbb")) + .transform + result2.asErrorPathMessages ==> Iterable( + "(0)(value).inner.str" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "(0)(value).inner.str => " -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue + ) + result2.asErrorPathMessageStrings ==> Iterable( + "(0)(value).inner.str" -> "empty value", + "" -> "empty value", + "(0)(value).inner.str => " -> "empty value", + "" -> "empty value" + ) + + val result3 = List(Map("value" -> Foo(InnerFoo("aaa")))) + .intoPartial[List[Map[String, Bar]]] + .withFieldComputedPartialFrom(_.everyItem.everyMapValue.inner)( + _.everyItem.everyMapValue.b, + _.transformIntoPartial[InnerBar].map(_ => 0) + ) + .transform + result3.asErrorPathMessages ==> Iterable( + "(0)(value).inner.str" -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "(0)(value).inner.str => " -> partial.ErrorMessage.EmptyValue, + "(0)(value).str => " -> partial.ErrorMessage.EmptyValue, + "" -> partial.ErrorMessage.EmptyValue, + "(0)(value).str => " -> partial.ErrorMessage.EmptyValue + ) + result3.asErrorPathMessageStrings ==> Iterable( + "(0)(value).inner.str" -> "empty value", + "" -> "empty value", + "(0)(value).inner.str => " -> "empty value", + "(0)(value).str => " -> "empty value", + "" -> "empty value", + "(0)(value).str => " -> "empty value" + ) + } } test("Java Bean accessors error should contain path to the failed getter") {