Skip to content
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ jobs:
- name: Check formatting
run: sbt scalafmtCheckAll
- name: Check binary compat
run: sbt ducktape/mimaReportBinaryIssues
run: sbt versionPolicyCheck
- name: Run unit tests
run: sbt test
- name: Check docs
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ jobs:
with:
distribution: temurin
java-version: 17
- run: sbt ci-release
- run: sbt versionCheck ci-release
env:
PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }}
PGP_SECRET: ${{ secrets.PGP_SECRET }}
Expand Down
9 changes: 8 additions & 1 deletion build.sbt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import com.typesafe.tools.mima.core._
import xerial.sbt.Sonatype._

Global / onChangedBuildSource := ReloadOnSourceChanges
Expand All @@ -16,6 +17,11 @@ ThisBuild / developers := List(
ThisBuild / scalaVersion := "3.2.1"
ThisBuild / scalafixDependencies += "com.github.liancheng" %% "organize-imports" % "0.6.0"

ThisBuild / versionPolicyIntention := Compatibility.BinaryCompatible
ThisBuild / mimaBinaryIssueFilters ++= Seq(
ProblemFilters.exclude[Problem]("io.github.arainko.ducktape.internal.*")
)

name := "ducktape"
sonatypeRepository := "https://s01.oss.sonatype.org/service/local"
sonatypeCredentialHost := "s01.oss.sonatype.org"
Expand All @@ -31,13 +37,14 @@ lazy val ducktape =
.in(file("ducktape"))
.settings(
scalacOptions ++= List("-Xcheck-macros", "-no-indent", "-old-syntax", "-Xfatal-warnings"),
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0-M6" % Test,
libraryDependencies += "org.scalameta" %% "munit" % "1.0.0-M7" % Test,
mimaPreviousArtifacts := Set("io.github.arainko" %% "ducktape" % "0.1.0")
)

lazy val docs =
project
.in(file("documentation"))
.settings(publish / skip := true)
.settings(mdocVariables := Map("VERSION" -> version.value))
.dependsOn(ducktape)
.enablePlugins(MdocPlugin)
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ object Transformer {

def define[Source, Dest]: DefinitionBuilder[Source, Dest] = DefinitionBuilder[Source, Dest]

def defineVia[A]: DefinitionViaBuilder.PartiallyApplied[A] = DefinitionViaBuilder.create[A]
def defineVia[Source]: DefinitionViaBuilder.PartiallyApplied[Source] = DefinitionViaBuilder.create[Source]

final class Identity[Source, Dest >: Source] private[Transformer] extends Transformer[Source, Dest] {
def transform(from: Source): Dest = from
Expand All @@ -26,46 +26,70 @@ object Transformer {
sealed trait ForProduct[Source, Dest] extends Transformer[Source, Dest]

object ForProduct {
@deprecated(message = "Use the variant with a Transformer instead", since = "0.1.1")
private[ducktape] def make[Source, Dest](f: Source => Dest): ForProduct[Source, Dest] =
new {
def transform(from: Source): Dest = f(from)
}

private[ducktape] def make[Source, Dest](transfomer: Transformer[Source, Dest]): ForProduct[Source, Dest] =
new {
def transform(from: Source): Dest = transfomer.transform(from)
}
}

sealed trait ForCoproduct[Source, Dest] extends Transformer[Source, Dest]

object ForCoproduct {
@deprecated(message = "Use the variant with a Transformer instead", since = "0.1.1")
private[ducktape] def make[Source, Dest](f: Source => Dest): ForCoproduct[Source, Dest] =
new {
def transform(from: Source): Dest = f(from)
}

private[ducktape] def make[Source, Dest](transformer: Transformer[Source, Dest]): ForCoproduct[Source, Dest] =
new {
def transform(from: Source): Dest = transformer.transform(from)
}
}

sealed trait FromAnyVal[Source <: AnyVal, Dest] extends Transformer[Source, Dest]

object FromAnyVal {
@deprecated(message = "Use the variant with a Transformer instead", since = "0.1.1")
private[ducktape] def make[Source <: AnyVal, Dest](f: Source => Dest): FromAnyVal[Source, Dest] =
new {
def transform(from: Source): Dest = f(from)
}

private[ducktape] def make[Source <: AnyVal, Dest](transformer: Transformer[Source, Dest]): FromAnyVal[Source, Dest] =
new {
def transform(from: Source): Dest = transformer.transform(from)
}
}

sealed trait ToAnyVal[Source, Dest <: AnyVal] extends Transformer[Source, Dest]

object ToAnyVal {
@deprecated(message = "Use the variant with a Transformer instead", since = "0.1.1")
private[ducktape] def make[Source, Dest <: AnyVal](f: Source => Dest): ToAnyVal[Source, Dest] =
new {
def transform(from: Source): Dest = f(from)
}

private[ducktape] def make[Source, Dest <: AnyVal](transformer: Transformer[Source, Dest]): ToAnyVal[Source, Dest] =
new {
def transform(from: Source): Dest = transformer.transform(from)
}
}

given [Source, Dest >: Source]: Identity[Source, Dest] = Identity[Source, Dest]

inline given forProducts[Source, Dest](using Mirror.ProductOf[Source], Mirror.ProductOf[Dest]): ForProduct[Source, Dest] =
ForProduct.make(from => NormalizationMacros.normalize(ProductTransformerMacros.transform(from)))
ForProduct.make(DerivationMacros.deriveProductTransformer[Source, Dest])

inline given forCoproducts[Source, Dest](using Mirror.SumOf[Source], Mirror.SumOf[Dest]): ForCoproduct[Source, Dest] =
ForCoproduct.make(from => CoproductTransformerMacros.transform(from))
ForCoproduct.make(DerivationMacros.deriveCoproductTransformer[Source, Dest])

given [Source, Dest](using Transformer[Source, Dest]): Transformer[Source, Option[Dest]] =
from => Transformer[Source, Dest].transform.andThen(Some.apply)(from)
Expand All @@ -84,9 +108,9 @@ object Transformer {
): Transformer[SourceCollection[Source], DestCollection[Dest]] = from => from.map(trans.transform).to(factory)

inline given fromAnyVal[Source <: AnyVal, Dest]: FromAnyVal[Source, Dest] =
FromAnyVal.make(from => ProductTransformerMacros.transformFromAnyVal[Source, Dest](from))
FromAnyVal.make(DerivationMacros.deriveFromAnyValTransformer[Source, Dest])

inline given toAnyVal[Source, Dest <: AnyVal]: ToAnyVal[Source, Dest] =
ToAnyVal.make(from => ProductTransformerMacros.transfromToAnyVal[Source, Dest](from))
ToAnyVal.make(DerivationMacros.deriveToAnyValTransformer[Source, Dest])

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ import io.github.arainko.ducktape.internal.macros.*
final class AppliedBuilder[Source, Dest](appliedTo: Source) {

inline def transform(inline config: BuilderConfig[Source, Dest]*): Dest =
NormalizationMacros.normalize(
TransformerMacros.transformConfigured(appliedTo, config)
)
TransformerMacros.transformConfigured(appliedTo, config)

}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,8 @@ final class AppliedViaBuilder[Source, Dest, Func, ArgSelector <: FunctionArgumen
inline def transform(
inline config: ArgBuilderConfig[Source, Dest, ArgSelector]*
)(using Source: Mirror.ProductOf[Source]): Dest =
NormalizationMacros.normalize(
ProductTransformerMacros.viaConfigured[Source, Dest, Func, ArgSelector](source, function, config*)
)
ProductTransformerMacros.viaConfigured[Source, Dest, Func, ArgSelector](source, function, config*)

}

object AppliedViaBuilder {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,5 @@ import scala.deriving.Mirror

final class DefinitionBuilder[Source, Dest] {
inline def build(inline config: BuilderConfig[Source, Dest]*): Transformer[Source, Dest] =
from => NormalizationMacros.normalize(TransformerMacros.transformConfigured(from, config))
from => TransformerMacros.transformConfigured(from, config)
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ final class DefinitionViaBuilder[Source, Dest, Func, ArgSelector <: FunctionArgu
inline def build(
inline config: ArgBuilderConfig[Source, Dest, ArgSelector]*
)(using Mirror.ProductOf[Source]): Transformer[Source, Dest] =
from =>
NormalizationMacros.normalize(
ProductTransformerMacros.viaConfigured[Source, Dest, Func, ArgSelector](from, function, config*)
)
from => ProductTransformerMacros.viaConfigured[Source, Dest, Func, ArgSelector](from, function, config*)

}

object DefinitionViaBuilder {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.github.arainko.ducktape.internal.macros

import io.github.arainko.ducktape.*

import scala.deriving.*
import scala.quoted.*

private[ducktape] object DerivationMacros {
inline def deriveProductTransformer[Source, Dest](using
Source: Mirror.ProductOf[Source],
Dest: Mirror.ProductOf[Dest]
): Transformer[Source, Dest] = ${ deriveProductTransformerMacro('Source, 'Dest) }

def deriveProductTransformerMacro[Source: Type, Dest: Type](
Source: Expr[Mirror.ProductOf[Source]],
Dest: Expr[Mirror.ProductOf[Dest]]
)(using Quotes): Expr[Transformer[Source, Dest]] =
'{ source => ${ ProductTransformerMacros.transformMacro[Source, Dest]('source, Source, Dest) } }

inline def deriveCoproductTransformer[Source, Dest](using
Source: Mirror.SumOf[Source],
Dest: Mirror.SumOf[Dest]
): Transformer[Source, Dest] = ${ deriveCoproductTransformerMacro[Source, Dest]('Source, 'Dest) }

def deriveCoproductTransformerMacro[Source: Type, Dest: Type](
Source: Expr[Mirror.SumOf[Source]],
Dest: Expr[Mirror.SumOf[Dest]]
)(using Quotes): Expr[Transformer[Source, Dest]] =
'{ source => ${ CoproductTransformerMacros.transformMacro[Source, Dest]('source, Source, Dest) } }

inline def deriveToAnyValTransformer[Source, Dest <: AnyVal]: Transformer[Source, Dest] =
${ deriveToAnyValTransformerMacro[Source, Dest] }

def deriveToAnyValTransformerMacro[Source: Type, Dest <: AnyVal: Type](using Quotes): Expr[Transformer[Source, Dest]] =
'{ source => ${ ProductTransformerMacros.transformToAnyValMacro('source) } }

inline def deriveFromAnyValTransformer[Source <: AnyVal, Dest] =
${ deriveFromAnyValTransformerMacro[Source, Dest] }

def deriveFromAnyValTransformerMacro[Source <: AnyVal: Type, Dest: Type](using Quotes): Expr[Transformer[Source, Dest]] =
'{ source => ${ ProductTransformerMacros.transformFromAnyValMacro('source) } }
}

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import io.github.arainko.ducktape.*
import io.github.arainko.ducktape.function.*
import io.github.arainko.ducktape.internal.modules.*

import scala.collection.Factory
import scala.deriving.*
import scala.quoted.*

Expand All @@ -13,7 +14,8 @@ private[ducktape] class ProductTransformerMacros(using val quotes: Quotes)
CaseModule,
MirrorModule,
SelectorModule,
ConfigurationModule {
ConfigurationModule,
NormalizationModule {
import quotes.reflect.*
import MaterializedConfiguration.*

Expand Down Expand Up @@ -128,7 +130,7 @@ private[ducktape] class ProductTransformerMacros(using val quotes: Quotes)
.get(field.name)
.getOrElse(abort(Failure.NoFieldMapping(field.name, TypeRepr.of[Source])))
}.map { (dest, source) =>
val call = resolveTransformer(sourceValue, source, dest)
val call = resolveTransformation(sourceValue, source, dest)

NamedArg(dest.name, call)
}
Expand All @@ -153,17 +155,58 @@ private[ducktape] class ProductTransformerMacros(using val quotes: Quotes)
NamedArg(field.name, castedCall.asTerm)
}

private def resolveTransformer[Source: Type](sourceValue: Expr[Source], source: Field, destination: Field) =
private def resolveTransformation[Source: Type](sourceValue: Expr[Source], source: Field, destination: Field)(using Quotes) = {
source.transformerTo(destination) match {
case '{ $transformer: Transformer.Identity[?, ?] } => accessField(sourceValue, source.name)

case '{ $transformer: Transformer.ForProduct[source, dest] } =>
val field = accessField(sourceValue, source.name).asExprOf[source]
normalizeTransformer(transformer, field).asTerm

case '{ $transformer: Transformer.FromAnyVal[source, dest] } =>
val field = accessField(sourceValue, source.name).asExprOf[source]
normalizeTransformer(transformer, field).asTerm

case '{ $transformer: Transformer.ToAnyVal[source, dest] } =>
val field = accessField(sourceValue, source.name).asExprOf[source]
normalizeTransformer(transformer, field).asTerm

case '{ Transformer.given_Transformer_Source_Option[source, dest](using $transformer) } =>
val field = accessField(sourceValue, source.name).asExprOf[source]
val normalized = normalizeTransformer(transformer, field)
'{ Some($normalized) }.asTerm

case '{ Transformer.given_Transformer_Option_Option[source, dest](using $transformer) } =>
val field = accessField(sourceValue, source.name).asExprOf[Option[source]]
'{ $field.map(src => ${ normalizeTransformer(transformer, 'src) }) }.asTerm

// Seems like higher-kinded type quotes are not supported yet
// https://github.com/lampepfl/dotty-feature-requests/issues/208
// https://github.com/lampepfl/dotty/discussions/12446
// Because of that we need to do some more shenanigans to get the exact collection type we transform into
case '{
Transformer.given_Transformer_SourceCollection_DestCollection[
source,
dest,
Iterable,
Iterable
](using $transformer, $factory)
} =>
val field = accessField(sourceValue, source.name).asExprOf[Iterable[source]]
factory match {
case '{ $f: Factory[`dest`, destColl] } =>
'{ $field.map(src => ${ normalizeTransformer(transformer, 'src) }).to($f) }.asTerm
}

case '{ $transformer: Transformer[source, dest] } =>
val field = accessField(sourceValue, source.name).asExprOf[source]
'{ $transformer.transform($field) }.asTerm
}
}

private def accessField[Source](value: Expr[Source], fieldName: String) = Select.unique(value.asTerm, fieldName)
private def accessField(value: Expr[Any], fieldName: String)(using Quotes) = Select.unique(value.asTerm, fieldName)

private def constructor(tpe: TypeRepr): Term = {
private def constructor(tpe: TypeRepr)(using Quotes): Term = {
val (repr, constructor, tpeArgs) = tpe match {
case AppliedType(repr, reprArguments) => (repr, repr.typeSymbol.primaryConstructor, reprArguments)
case notApplied => (tpe, tpe.typeSymbol.primaryConstructor, Nil)
Expand All @@ -177,11 +220,6 @@ private[ducktape] class ProductTransformerMacros(using val quotes: Quotes)
}

private[ducktape] object ProductTransformerMacros {
inline def transform[Source, Dest](source: Source)(using
Source: Mirror.ProductOf[Source],
Dest: Mirror.ProductOf[Dest]
): Dest = ${ transformMacro[Source, Dest]('source, 'Source, 'Dest) }

def transformMacro[Source: Type, Dest: Type](
source: Expr[Source],
Source: Expr[Mirror.ProductOf[Source]],
Expand Down Expand Up @@ -229,15 +267,9 @@ private[ducktape] object ProductTransformerMacros {
)(using Quotes) =
ProductTransformerMacros().transformConfigured(sourceValue, config, Source, Dest)

inline def transformFromAnyVal[Source <: AnyVal, Dest](sourceValue: Source): Dest =
${ transformFromAnyValMacro[Source, Dest]('sourceValue) }

def transformFromAnyValMacro[Source <: AnyVal: Type, Dest: Type](sourceValue: Expr[Source])(using Quotes): Expr[Dest] =
ProductTransformerMacros().transformFromAnyVal[Source, Dest](sourceValue)

inline def transfromToAnyVal[Source, Dest <: AnyVal](sourceValue: Source) =
${ transformToAnyValMacro[Source, Dest]('sourceValue) }

def transformToAnyValMacro[Source: Type, Dest <: AnyVal: Type](sourceValue: Expr[Source])(using Quotes): Expr[Dest] =
ProductTransformerMacros().transformToAnyVal[Source, Dest](sourceValue)

Expand Down
Loading