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

Add ability to provie a custom constructor #439

Merged
merged 22 commits into from
Dec 18, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
d7dc39a
Create types for storing method/function arguments in type-level
MateuszKubuszok Nov 21, 2023
8e5715c
Parse and store constructor in TransformerCfg within macros
MateuszKubuszok Nov 21, 2023
e4426ab
Wire (not yet implemented) custom constructor from config into Produc…
MateuszKubuszok Nov 21, 2023
0fd61ff
Stub DSL methods and custom custructor generation
MateuszKubuszok Nov 22, 2023
f809d0e
Prototype an interface to recognize function types
MateuszKubuszok Nov 23, 2023
082209c
Prototype how method calling would work
MateuszKubuszok Nov 23, 2023
77cf34b
Create IsFunction.Of (instead of .Aux)
MateuszKubuszok Nov 24, 2023
3396898
Start drafting DSL macros for withConstructor
MateuszKubuszok Dec 15, 2023
b8271cc
Initial macro implementations, for starters with Scala 2 macros
MateuszKubuszok Dec 15, 2023
df67bb0
Initial implementation for Scala 3 DSL and derivation
MateuszKubuszok Dec 15, 2023
35a6faa
Fix Scala 2 implementation after refactor and let it handle multiple …
MateuszKubuszok Dec 16, 2023
acc512e
Fix 2.12 build
MateuszKubuszok Dec 16, 2023
bae8651
Fix Scala 2.12 support for Eta-expansion in withConstructor
MateuszKubuszok Dec 16, 2023
1a45fbd
Allow creating 2.13+ tests (since 2.12 syntax can be limitting)
MateuszKubuszok Dec 16, 2023
8abba04
Test withConstructor and withConstructorPartial in its most basic use…
MateuszKubuszok Dec 18, 2023
9067dd2
Small cleanups and IsFunction tests
MateuszKubuszok Dec 18, 2023
29709da
Rename tests in CustomConstructor suites, since it doesn't make sense…
MateuszKubuszok Dec 18, 2023
cb045aa
Docs and opaque types tests
MateuszKubuszok Dec 18, 2023
90bd4ed
Fix formatting of the see section in scaladocs, added missing links i…
MateuszKubuszok Dec 18, 2023
6197111
Provide documentation for custom constructors
MateuszKubuszok Dec 18, 2023
2c1f444
Add warnings and notes
MateuszKubuszok Dec 18, 2023
f8524e9
Improve custom constructor's exmples and tests a bit
MateuszKubuszok Dec 18, 2023
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
12 changes: 11 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ val only1VersionInIDE =
.Configure(_.settings(ideSkipProject := true, bspEnabled := false, scalafmtOnCompile := false))
}

val non212tests =
MatrixAction
.ForScala(v => (v.value == versions.scala213) || v.isScala3)
.Configure(
_.settings(
// Compile / unmanagedSourceDirectories += sourceDirectory.value.toPath.resolve("./main/scala-2.13+").toFile,
Test / unmanagedSourceDirectories += sourceDirectory.value.toPath.resolve("test/scala-2.13+").toFile
)
)

val settings = Seq(
git.useGitDescribe := true,
git.uncommittedSignifier := None,
Expand Down Expand Up @@ -364,7 +374,7 @@ lazy val chimneyMacroCommons = projectMatrix

lazy val chimney = projectMatrix
.in(file("chimney"))
.someVariations(versions.scalas, versions.platforms)(only1VersionInIDE*)
.someVariations(versions.scalas, versions.platforms)((non212tests +: only1VersionInIDE)*)
.enablePlugins(GitVersioning, GitBranchPrompt)
.disablePlugins(WelcomePlugin, ProtocPlugin)
.settings(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,37 @@
} else None
}

def exprAsInstanceOfMethod[A: Type](args: List[ListMap[String, ??]])(expr: Expr[Any]): Product.Constructor[A] = {
val parameters: Product.Parameters = ListMap.from(for {
list <- args
pair <- list.toList
(paramName, paramType) = pair
} yield {
import paramType.Underlying as ParamType
paramName -> Existential[Product.Parameter, ParamType](
Product.Parameter(Product.Parameter.TargetType.ConstructorParameter, None)
)
})

val constructor: Product.Arguments => Expr[A] = arguments => {
val (constructorArguments, _) = checkArguments[A](parameters, arguments)

val methodType: ?? = args.foldRight[??](Type[A].as_??) { (paramList, resultType) =>
val paramTypes = paramList.values.map(_.Underlying.tpe).toList
// tq returns c.Tree, to turn it to c.Type we need .tpe, which without a .typecheck is null
fromUntyped(c.typecheck(tq"(..$paramTypes) => ${resultType.Underlying.tpe}", mode = c.TYPEmode).tpe).as_??
}

import methodType.Underlying as MethodType
val tree = expr.asInstanceOfExpr[MethodType].tree
c.Expr[A](q"$tree(...${(args.map(_.map { case (paramName, _) =>
constructorArguments(paramName).value.tree
}))})")
}

Product.Constructor[A](parameters, constructor)
}

Check warning on line 248 in chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala

View check run for this annotation

Codecov / codecov/patch

chimney-macro-commons/src/main/scala-2/io/scalaland/chimney/internal/compiletime/datatypes/ProductTypesPlatform.scala#L248

Added line #L248 was not covered by tests

private val getDecodedName = (s: Symbol) => s.name.decodedName.toString

private val isGarbageSymbol = getDecodedName andThen isGarbage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -242,6 +242,77 @@ trait ProductTypesPlatform extends ProductTypes { this: DefinitionsPlatform =>
Some(Product.Constructor(parameters, constructor))
} else None

def exprAsInstanceOfMethod[A: Type](args: List[ListMap[String, ??]])(expr: Expr[Any]): Product.Constructor[A] = {
val parameters: Product.Parameters = ListMap.from(for {
list <- args
pair <- list.toList
(paramName, paramType) = pair
} yield {
import paramType.Underlying as ParamType
paramName -> Existential[Product.Parameter, ParamType](
Product.Parameter(Product.Parameter.TargetType.ConstructorParameter, None)
)
})

val constructor: Product.Arguments => Expr[A] = arguments => {
val (constructorArguments, _) = checkArguments[A](parameters, arguments)

val methodType: ?? = args.foldRight[??](Type[A].as_??) { (paramList, resultType) =>
val fnType = fnTypeByArity.getOrElse(
paramList.size,
// TODO: handle FunctionXXL
assertionFailed(s"Expected arity between 0 and 22 into ${Type.prettyPrint[A]}, got: ${paramList.size}")
)
val paramTypes = paramList.view.values.map(p => TypeRepr.of(using p.Underlying)).toVector

fromUntyped(
fnType.appliedTo((paramTypes :+ TypeRepr.of(using resultType.Underlying)).toList).dealias.simplified
).as_??
}

import methodType.Underlying as MethodType
val tree = expr.asInstanceOfExpr[MethodType].asTerm
args
.foldLeft(tree) { (result, list) =>
val method: Symbol = result.tpe.typeSymbol.methodMember("apply").head
result
.select(method)
.appliedToArgs(list.map { (paramName, _) =>
constructorArguments(paramName).value.asTerm
}.toList)
}
.asExprOf[A]
}

Product.Constructor[A](parameters, constructor)
}

private lazy val fnTypeByArity = Map(
0 -> TypeRepr.of[scala.Function0],
1 -> TypeRepr.of[scala.Function1],
2 -> TypeRepr.of[scala.Function2],
3 -> TypeRepr.of[scala.Function3],
4 -> TypeRepr.of[scala.Function4],
5 -> TypeRepr.of[scala.Function5],
6 -> TypeRepr.of[scala.Function6],
7 -> TypeRepr.of[scala.Function7],
8 -> TypeRepr.of[scala.Function8],
9 -> TypeRepr.of[scala.Function9],
10 -> TypeRepr.of[scala.Function10],
11 -> TypeRepr.of[scala.Function11],
12 -> TypeRepr.of[scala.Function12],
13 -> TypeRepr.of[scala.Function13],
14 -> TypeRepr.of[scala.Function14],
15 -> TypeRepr.of[scala.Function15],
16 -> TypeRepr.of[scala.Function16],
17 -> TypeRepr.of[scala.Function17],
18 -> TypeRepr.of[scala.Function18],
19 -> TypeRepr.of[scala.Function19],
20 -> TypeRepr.of[scala.Function20],
21 -> TypeRepr.of[scala.Function21],
22 -> TypeRepr.of[scala.Function22]
)

private val isGarbageSymbol = ((s: Symbol) => s.name) andThen isGarbage
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,9 @@ trait ProductTypes { this: Definitions =>
object Constructor {
def unapply[To](To: Type[To]): Option[(Parameters, Arguments => Expr[To])] =
ProductType.parseConstructor(To).map(constructor => constructor.parameters -> constructor.constructor)

def exprAsInstanceOfMethod[To: Type](args: List[ListMap[String, ??]])(expr: Expr[Any]): Constructor[To] =
ProductType.exprAsInstanceOfMethod[To](args)(expr)
}
}

Expand All @@ -92,6 +95,8 @@ trait ProductTypes { this: Definitions =>
}
final def unapply[A](tpe: Type[A]): Option[Product[A]] = parse(tpe)

def exprAsInstanceOfMethod[A: Type](args: List[ListMap[String, ??]])(expr: Expr[Any]): Product.Constructor[A]

// cached in companion (regexps are expensive to initialize)
def areNamesMatching(fromName: String, toName: String): Boolean = ProductTypes.areNamesMatching(fromName, toName)
def isGarbage(name: String): Boolean = ProductTypes.isGarbage(name)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.scalaland.chimney.dsl
import io.scalaland.chimney.{partial, PartialTransformer}
import io.scalaland.chimney.internal.compiletime.derivation.transformer.TransformerMacros
import io.scalaland.chimney.internal.compiletime.dsl.PartialTransformerDefinitionMacros
import io.scalaland.chimney.internal.runtime.{TransformerCfg, TransformerFlags, WithRuntimeDataStore}
import io.scalaland.chimney.internal.runtime.{IsFunction, TransformerCfg, TransformerFlags, WithRuntimeDataStore}

import scala.language.experimental.macros

Expand Down Expand Up @@ -130,7 +130,7 @@ final class PartialTransformerDefinition[From, To, Cfg <: TransformerCfg, Flags
* in `To` field's type there is matching component in `From` type. If some component is missing
* it fails compilation unless provided replacement with this operation.
*
* @see [[]] for more details
* @see [[https://chimney.readthedocs.io/supported-transformations/#handling-a-specific-sealed-subtype-with-a-computed-value]] for more details
*
* @tparam Inst type of coproduct instance
* @param f function to calculate values of components that cannot be mapped automatically
Expand All @@ -148,7 +148,7 @@ final class PartialTransformerDefinition[From, To, Cfg <: TransformerCfg, Flags
* in `To` field's type there is matching component in `From` type. If some component is missing
* it fails compilation unless provided replacement with this operation.
*
* @see [[]] for more details
* @see [[https://chimney.readthedocs.io/supported-transformations/#handling-a-specific-sealed-subtype-with-a-computed-value]] for more details
*
* @tparam Inst type of coproduct instance
* @param f function to calculate values of components that cannot be mapped automatically
Expand All @@ -161,6 +161,46 @@ final class PartialTransformerDefinition[From, To, Cfg <: TransformerCfg, Flags
): PartialTransformerDefinition[From, To, ? <: TransformerCfg, Flags] =
macro PartialTransformerDefinitionMacros.withCoproductInstancePartialImpl[From, To, Cfg, Flags, Inst]

/** Use `f` instead of the primary constructor to construct the `To` value.
*
* Macro will read the names of Eta-expanded method's/lambda's parameters and try to match them with `From` getters.
*
* Values for each parameter can be provided the same way as if they were normal constructor's arguments.
*
* @see [[https://chimney.readthedocs.io/supported-transformations/#types-with-manually-provided-constructors]] for more details
*
* @tparam Ctor type of the Eta-expanded method/lambda which should return `To`
* @param f method name or lambda which constructs `To`
* @return [[io.scalaland.chimney.dsl.PartialTransformerDefinition]]
*
* @since 0.8.4
*/
def withConstructor[Ctor](
f: Ctor
)(implicit ev: IsFunction.Of[Ctor, To]): PartialTransformerDefinition[From, To, ? <: TransformerCfg, Flags] =
macro PartialTransformerDefinitionMacros.withConstructorImpl[From, To, Cfg, Flags]

/** Use `f` instead of the primary constructor to parse into `partial.Result[To]` value.
*
* Macro will read the names of Eta-expanded method's/lambda's parameters and try to match them with `From` getters.
*
* Values for each parameter can be provided the same way as if they were normal constructor's arguments.
*
* @see [[https://chimney.readthedocs.io/supported-transformations/#types-with-manually-provided-constructors]] for more details
*
* @tparam Ctor type of the Eta-expanded method/lambda which should return `partial.Result[To]`
* @param f method name or lambda which constructs `partial.Result[To]`
* @return [[io.scalaland.chimney.dsl.PartialTransformerDefinition]]
*
* @since 0.8.4
*/
def withConstructorPartial[Ctor](
f: Ctor
)(implicit
ev: IsFunction.Of[Ctor, partial.Result[To]]
): PartialTransformerDefinition[From, To, ? <: TransformerCfg, Flags] =
macro PartialTransformerDefinitionMacros.withConstructorPartialImpl[From, To, Cfg, Flags]

/** Build Partial Transformer using current configuration.
*
* It runs macro that tries to derive instance of `PartialTransformer[From, To]`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.scalaland.chimney.dsl
import io.scalaland.chimney.partial
import io.scalaland.chimney.internal.compiletime.derivation.transformer.TransformerMacros
import io.scalaland.chimney.internal.compiletime.dsl.PartialTransformerIntoMacros
import io.scalaland.chimney.internal.runtime.{TransformerCfg, TransformerFlags, WithRuntimeDataStore}
import io.scalaland.chimney.internal.runtime.{IsFunction, TransformerCfg, TransformerFlags, WithRuntimeDataStore}

import scala.language.experimental.macros

Expand Down Expand Up @@ -133,7 +133,7 @@ final class PartialTransformerInto[From, To, Cfg <: TransformerCfg, Flags <: Tra
* in `To` field's type there is matching component in `From` type. If some component is missing
* it fails compilation unless provided replacement with this operation.
*
* @see [[]] for more details
* @see [[https://chimney.readthedocs.io/supported-transformations/#handling-a-specific-sealed-subtype-with-a-computed-value]] for more details
*
* @tparam Inst type of coproduct instance
* @param f function to calculate values of components that cannot be mapped automatically
Expand All @@ -151,7 +151,7 @@ final class PartialTransformerInto[From, To, Cfg <: TransformerCfg, Flags <: Tra
* in `To` field's type there is matching component in `From` type. If some component is missing
* it fails compilation unless provided replacement with this operation.
*
* @see [[]] for more details
* @see [[https://chimney.readthedocs.io/supported-transformations/#handling-a-specific-sealed-subtype-with-a-computed-value]] for more details
*
* @tparam Inst type of coproduct instance
* @param f function to calculate values of components that cannot be mapped automatically
Expand All @@ -164,6 +164,46 @@ final class PartialTransformerInto[From, To, Cfg <: TransformerCfg, Flags <: Tra
): PartialTransformerInto[From, To, ? <: TransformerCfg, Flags] =
macro PartialTransformerIntoMacros.withCoproductInstancePartialImpl[From, To, Cfg, Flags, Inst]

/** Use `f` instead of the primary constructor to construct the `To` value.
*
* Macro will read the names of Eta-expanded method's/lambda's parameters and try to match them with `From` getters.
*
* Values for each parameter can be provided the same way as if they were normal constructor's arguments.
*
* @see [[https://chimney.readthedocs.io/supported-transformations/#types-with-manually-provided-constructors]] for more details
*
* @tparam Ctor type of the Eta-expanded method/lambda which should return `To`
* @param f method name or lambda which constructs `To`
* @return [[io.scalaland.chimney.dsl.PartialTransformerInto]]
*
* @since 0.8.4
*/
def withConstructor[Ctor](
f: Ctor
)(implicit ev: IsFunction.Of[Ctor, To]): PartialTransformerInto[From, To, ? <: TransformerCfg, Flags] =
macro PartialTransformerIntoMacros.withConstructorImpl[From, To, Cfg, Flags]

/** Use `f` instead of the primary constructor to parse into `partial.Result[To]` value.
*
* Macro will read the names of Eta-expanded method's/lambda's parameters and try to match them with `From` getters.
*
* Values for each parameter can be provided the same way as if they were normal constructor's arguments.
*
* @see [[https://chimney.readthedocs.io/supported-transformations/#types-with-manually-provided-constructors]] for more details
*
* @tparam Ctor type of the Eta-expanded method/lambda which should return `partial.Result[To]`
* @param f method name or lambda which constructs `partial.Result[To]`
* @return [[io.scalaland.chimney.dsl.PartialTransformerInto]]
*
* @since 0.8.4
*/
def withConstructorPartial[Ctor](
f: Ctor
)(implicit
ev: IsFunction.Of[Ctor, partial.Result[To]]
): PartialTransformerInto[From, To, ? <: TransformerCfg, Flags] =
macro PartialTransformerIntoMacros.withConstructorPartialImpl[From, To, Cfg, Flags]

/** Apply configured partial transformation in-place.
*
* It runs macro that tries to derive instance of `PartialTransformer[From, To]`
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ package io.scalaland.chimney.dsl
import io.scalaland.chimney.Transformer
import io.scalaland.chimney.internal.compiletime.derivation.transformer.TransformerMacros
import io.scalaland.chimney.internal.compiletime.dsl.TransformerDefinitionMacros
import io.scalaland.chimney.internal.runtime.{TransformerCfg, TransformerFlags, WithRuntimeDataStore}
import io.scalaland.chimney.internal.runtime.{IsFunction, TransformerCfg, TransformerFlags, WithRuntimeDataStore}

import scala.language.experimental.macros

Expand Down Expand Up @@ -100,7 +100,7 @@ final class TransformerDefinition[From, To, Cfg <: TransformerCfg, Flags <: Tran
* in `To` field's type there is matching component in `From` type. If some component is missing
* it fails compilation unless provided replacement with this operation.
*
* @see [[]] for more details
* @see [[https://chimney.readthedocs.io/supported-transformations/#handling-a-specific-sealed-subtype-with-a-computed-value]] for more details
*
* @tparam Inst type of coproduct instance
* @param f function to calculate values of components that cannot be mapped automatically
Expand All @@ -111,6 +111,25 @@ final class TransformerDefinition[From, To, Cfg <: TransformerCfg, Flags <: Tran
def withCoproductInstance[Inst](f: Inst => To): TransformerDefinition[From, To, ? <: TransformerCfg, Flags] =
macro TransformerDefinitionMacros.withCoproductInstanceImpl[From, To, Cfg, Flags, Inst]

/** Use `f` instead of the primary constructor to construct the `To` value.
*
* Macro will read the names of Eta-expanded method's/lambda's parameters and try to match them with `From` getters.
*
* Values for each parameter can be provided the same way as if they were normal constructor's arguments.
*
* @see [[https://chimney.readthedocs.io/supported-transformations/#types-with-manually-provided-constructors]] for more details
*
* @tparam Ctor type of the Eta-expanded method/lambda which should return `To`
* @param f method name or lambda which constructs `To`
* @return [[io.scalaland.chimney.dsl.TransformerDefinition]]
*
* @since 0.8.4
*/
def withConstructor[Ctor](
f: Ctor
)(implicit ev: IsFunction.Of[Ctor, To]): TransformerDefinition[From, To, ? <: TransformerCfg, Flags] =
macro TransformerDefinitionMacros.withConstructorImpl[From, To, Cfg, Flags]

/** Build Transformer using current configuration.
*
* It runs macro that tries to derive instance of `Transformer[From, To]`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package io.scalaland.chimney.dsl

import io.scalaland.chimney.internal.compiletime.derivation.transformer.TransformerMacros
import io.scalaland.chimney.internal.compiletime.dsl.TransformerIntoMacros
import io.scalaland.chimney.internal.runtime.{TransformerCfg, TransformerFlags, WithRuntimeDataStore}
import io.scalaland.chimney.internal.runtime.{IsFunction, TransformerCfg, TransformerFlags, WithRuntimeDataStore}

import scala.language.experimental.macros

Expand Down Expand Up @@ -96,7 +96,7 @@ final class TransformerInto[From, To, Cfg <: TransformerCfg, Flags <: Transforme
* in `To` field's type there is matching component in `From` type. If some component is missing
* it will fail.
*
* @see [[]] for more details
* @see [[https://chimney.readthedocs.io/supported-transformations/#handling-a-specific-sealed-subtype-with-a-computed-value]] for more details
*
* @tparam Inst type of coproduct instance@param f function to calculate values of components that cannot be mapped automatically
* @return [[io.scalaland.chimney.dsl.TransformerInto]]
Expand All @@ -106,6 +106,25 @@ final class TransformerInto[From, To, Cfg <: TransformerCfg, Flags <: Transforme
def withCoproductInstance[Inst](f: Inst => To): TransformerInto[From, To, ? <: TransformerCfg, Flags] =
macro TransformerIntoMacros.withCoproductInstanceImpl[From, To, Cfg, Flags, Inst]

/** Use `f` instead of the primary constructor to construct the `To` value.
*
* Macro will read the names of Eta-expanded method's/lambda's parameters and try to match them with `From` getters.
*
* Values for each parameter can be provided the same way as if they were normal constructor's arguments.
*
* @see [[https://chimney.readthedocs.io/supported-transformations/#types-with-manually-provided-constructors]] for more details
*
* @tparam Ctor type of the Eta-expanded method/lambda which should return `To`
* @param f method name or lambda which constructs `To`
* @return [[io.scalaland.chimney.dsl.TransformerInto]]
*
* @since 0.8.4
*/
def withConstructor[Ctor](
f: Ctor
)(implicit ev: IsFunction.Of[Ctor, To]): TransformerInto[From, To, ? <: TransformerCfg, Flags] =
macro TransformerIntoMacros.withConstructorImpl[From, To, Cfg, Flags]

/** Apply configured transformation in-place.
*
* It runs macro that tries to derive instance of `Transformer[From, To]`
Expand Down
Loading