diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/BuilderConfig.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/BuilderConfig.scala index b765cca0..3cef4161 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/BuilderConfig.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/BuilderConfig.scala @@ -41,7 +41,7 @@ object Field { ): BuilderConfig[Source, Dest] = throw NotQuotedException("Field.renamed") @compileTimeOnly("'Field.default' needs to be erased from the AST with a macro.") - def default[Source, Dest, FieldType](selector: Dest => FieldType)(using + def default[Source, Dest, FieldType](selector: Dest => FieldType)(using @implicitNotFound("Field.default is supported for product types only, but ${Source} is not a product type.") ev1: Mirror.ProductOf[Source], @implicitNotFound("Field.default is supported for product types only, but ${Dest} is not a product type.") diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala index 4c14828d..6a2b1f4e 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/Failure.scala @@ -125,12 +125,17 @@ private[ducktape] object Failure { override final def render(using Quotes): String = s"No field named '$fieldName' found in ${sourceType.show}" } - final case class DefaultMissing(fieldName: String, destType: Type[?]) extends Failure { + final case class DefaultMissing(fieldName: String, destType: Type[?], config: Expr[Any]) extends Failure { + override def position(using Quotes): quotes.reflect.Position = config.pos + override final def render(using Quotes): String = s"No default value for '$fieldName' found in ${destType.show}" } - final case class InvalidDefaultType(defaultField: Field, destType: Type[?]) extends Failure { - override final def render(using Quotes): String = s"The default value of '${destType.show}.${defaultField.name}' is not a subtype of ${defaultField.tpe.show}" + final case class InvalidDefaultType(defaultField: Field, destType: Type[?], config: Expr[Any]) extends Failure { + override def position(using Quotes): quotes.reflect.Position = config.pos + + override final def render(using Quotes): String = + s"The default value of '${destType.show}.${defaultField.name}' is not a subtype of ${defaultField.tpe.show}" } final case class NoChildMapping(childName: String, destinationType: Type[?]) extends Failure { diff --git a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedConfiguration.scala b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedConfiguration.scala index 0ea47bea..e03e4a76 100644 --- a/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedConfiguration.scala +++ b/ducktape/src/main/scala/io/github/arainko/ducktape/internal/modules/MaterializedConfiguration.scala @@ -62,16 +62,16 @@ private[ducktape] object MaterializedConfiguration { val destFieldName = Selectors.fieldName(Fields.dest, destSelector) val sourceFieldName = Selectors.fieldName(Fields.source, sourceSelector) Product.Renamed(destFieldName, sourceFieldName)(Pos.fromExpr(config)) :: Nil - + case '{ FieldConfig.default[source, dest, destFieldType]($destSelector)(using $ev1, $ev2) } => val destFieldName = Selectors.fieldName(Fields.dest, destSelector) val field = Fields.dest.unsafeGet(destFieldName) - val default = field.default.getOrElse(Failure.emit(Failure.DefaultMissing(field.name, Type.of[dest]))) + val default = field.default.getOrElse(Failure.emit(Failure.DefaultMissing(field.name, Type.of[dest], config))) Failure.cond( successCondition = default.asTerm.tpe <:< TypeRepr.of(using field.tpe), value = Product.Const(field.name, default)(Pos.fromExpr(config)) :: Nil, - failure = Failure.InvalidDefaultType(field, Type.of[dest]) + failure = Failure.InvalidDefaultType(field, Type.of[dest], config) ) case config @ '{ diff --git a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue38Spec.scala b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue38Spec.scala index 033921d5..9fe86adf 100644 --- a/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue38Spec.scala +++ b/ducktape/src/test/scala/io/github/arainko/ducktape/issues/Issue38Spec.scala @@ -17,17 +17,41 @@ class Issue38Spec extends DucktapeSuite { List( testClass .into[TestClassWithAdditionalGenericArg[String]] - .transform( - Field.default(_.additionalArg) - ), + .transform(Field.default(_.additionalArg)), Transformer .define[TestClass, TestClassWithAdditionalGenericArg[String]] - .build( - Field.default(_.additionalArg) - ) + .build(Field.default(_.additionalArg)) .transform(testClass) ) actual.foreach(actual => assertEquals(actual, expected)) } + + test("Field.default fails when a field doesn't have a default value") { + val testClass = TestClass("str", 1) + + assertFailsToCompileWith { + """ + testClass + .into[TestClassWithAdditionalGenericArg[String]] + .transform( + Field.default(_.int) + ) + """ + }("No default value for 'int' found in TestClassWithAdditionalGenericArg[String]") + } + + test("Field.default fails when the default doesn't match the expected type") { + val testClass = TestClass("str", 1) + + assertFailsToCompileWith { + """ + testClass + .into[TestClassWithAdditionalGenericArg[Int]] + .transform( + Field.default(_.additionalArg) + ) + """ + }("The default value of 'TestClassWithAdditionalGenericArg[Int].additionalArg' is not a subtype of Int") + } }