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 .scalafmt.conf
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
version = 3.0.4
version = 3.6.1
runner.dialect = scala3
continuationIndent.defnSite = 2
docstrings.style = Asterisk
Expand Down
173 changes: 167 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ If this project interests you, please drop a 🌟 - these things are worthless b

### Installation
```scala
libraryDependencies += "io.github.arainko" %% "ducktape" % "0.1.0"
libraryDependencies += "io.github.arainko" %% "ducktape" % "0.1.1"
```

### Examples
Expand Down Expand Up @@ -44,8 +44,8 @@ person.to[PersonButMoreFields]

// error:
// No field named 'socialSecurityNo' found in Person
// .define[TestClass, TestClassWithAdditionalList]
//
// .into[Person2]
// ^
```

#### 2. *Enum to enum*
Expand Down Expand Up @@ -322,13 +322,13 @@ val definedViaTransformer =
Transformer
.defineVia[TestClass](method)
.build(Arg.const(_.additionalArg, List("const")))
// definedViaTransformer: Transformer[TestClass, TestClassWithAdditionalList] = repl.MdocSession$MdocApp6$$Lambda$41195/0x000000010897d840@6ee2238f
// definedViaTransformer: Transformer[TestClass, TestClassWithAdditionalList] = repl.MdocSession$MdocApp6$$Lambda$91804/0x00000001037e0c40@3fcb542c

val definedTransformer =
Transformer
.define[TestClass, TestClassWithAdditionalList]
.build(Field.const(_.additionalArg, List("const")))
// definedTransformer: Transformer[TestClass, TestClassWithAdditionalList] = repl.MdocSession$MdocApp6$$Lambda$41196/0x000000010897dc40@3eb47bb1
// definedTransformer: Transformer[TestClass, TestClassWithAdditionalList] = repl.MdocSession$MdocApp6$$Lambda$91805/0x00000001037e6440@538893e6

val transformedVia = definedViaTransformer.transform(testClass)
// transformedVia: TestClassWithAdditionalList = TestClassWithAdditionalList(
Expand All @@ -348,4 +348,165 @@ val transformed = definedTransformer.transform(testClass)

### A look at the generated code

#### -- TODO --
To inspect the code that is generated you can use `Transformer.Debug.showCode`, this method will print
the generated code at compile time for you to analyze and see if there's something funny going on after the macro expands.

For the sake of documentation let's also give some examples of what should be the expected output for some basic usages of `ducktape`.

#### Generated code - product transformations
Given a structure of case classes like the ones below let's examine the output that `ducktape` splices into your code:

```scala
import io.github.arainko.ducktape.*

final case class Wrapped[A](value: A) extends AnyVal

case class Person(int: Int, str: Option[String], inside: Inside, collectionOfNumbers: Vector[Float])
case class Person2(int: Wrapped[Int], str: Option[Wrapped[String]], inside: Inside2, collectionOfNumbers: List[Wrapped[Float]])

case class Inside(str: String, int: Int, inside: EvenMoreInside)
case class Inside2(int: Int, str: String, inside: Option[EvenMoreInside2])

case class EvenMoreInside(str: String, int: Int)
case class EvenMoreInside2(str: String, int: Int)

val person = Person(23, Some("str"), Inside("insideStr", 24, EvenMoreInside("evenMoreInsideStr", 25)), Vector.empty)
```
#### Generated code - expansion of `.to`
Calling the `.to` method
```scala
person.to[Person2]
```
expands to:
``` scala
to[Person](person)[Person2](
inline$make$i1[Person, Person2](ForProduct)(
(
(source: Person) =>
new Person2(
int = new Wrapped[Int](source.int),
str = source.str.map[Wrapped[String]]((src: String) => new Wrapped[String](src)),
inside = new Inside2(
int = source.inside.int,
str = source.inside.str,
inside =
Some.apply[EvenMoreInside2](new EvenMoreInside2(str = source.inside.inside.str, int = source.inside.inside.int))
),
collectionOfNumbers = source.collectionOfNumbers
.map[Wrapped[Float]]((`src₂`: Float) => new Wrapped[Float](`src₂`))
.to[List[Wrapped[Float]] & Iterable[Wrapped[Float]]](iterableFactory[Wrapped[Float]])
)
): Transformer[Person, Person2]
): ForProduct[Person, Person2]
)
```

#### Generated code - expansion of `.into`
Calling the `.into` method
```scala
person
.into[Person2]
.transform(
Field.const(_.str, Some(Wrapped("ConstString!"))),
Field.computed(_.int, person => Wrapped(person.int + 100)),
)
```
expands to:
``` scala
{
val AppliedBuilder_this: AppliedBuilder[Person, Person2] = into[Person](person)[Person2]

{
val sourceValue$proxy9: Person = AppliedBuilder_this.inline$appliedTo

{
val inside$2: Inside2 = new Inside2(
int = sourceValue$proxy9.inside.int,
str = sourceValue$proxy9.inside.str,
inside = Some.apply[EvenMoreInside2](
new EvenMoreInside2(str = sourceValue$proxy9.inside.inside.str, int = sourceValue$proxy9.inside.inside.int)
)
)
val collectionOfNumbers$2: List[Wrapped[Float]] = sourceValue$proxy9.collectionOfNumbers
.map[Wrapped[Float]]((src: Float) => new Wrapped[Float](src))
.to[List[Wrapped[Float]] & Iterable[Wrapped[Float]]](iterableFactory[Wrapped[Float]])
val str$2: Some[Wrapped[String]] = Some.apply[Wrapped[String]](Wrapped.apply[String]("ConstString!"))
val int$2: Wrapped[Int] = Wrapped.apply[Int](sourceValue$proxy9.int.+(100))
new Person2(int = int$2, str = str$2, inside = inside$2, collectionOfNumbers = collectionOfNumbers$2)
}: Person2
}: Person2
}
```

#### Generated code - expansion of `.via`
Calling the `.via` method
```scala
person.via(Person2.apply)
```

expands to:
``` scala
{
val Func$proxy4: FunctionMirror[Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2]] {
type Return >: Person2 <: Person2
} = FunctionMirror.asInstanceOf[
FunctionMirror[Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2]] {
type Return >: Person2 <: Person2
}
]

({
val int$proxy2: Wrapped[Int] = new Wrapped[Int](person.int)
val str$proxy2: Option[Wrapped[String]] = person.str.map[Wrapped[String]]((src: String) => new Wrapped[String](src))
val inside$proxy2: Inside2 = new Inside2(
int = person.inside.int,
str = person.inside.str,
inside = Some.apply[EvenMoreInside2](new EvenMoreInside2(str = person.inside.inside.str, int = person.inside.inside.int))
)
val collectionOfNumbers$proxy2: List[Wrapped[Float]] = person.collectionOfNumbers
.map[Wrapped[Float]]((`src₂`: Float) => new Wrapped[Float](`src₂`))
.to[List[Wrapped[Float]] & Iterable[Wrapped[Float]]](iterableFactory[Wrapped[Float]])
Person2.apply(int$proxy2, str$proxy2, inside$proxy2, collectionOfNumbers$proxy2)
}: Return): Return
}
```

#### Generated code - expansion of `.intoVia`
Calling the `.intoVia` method with subsequent transformation customizations
```scala
person
.intoVia(Person2.apply)
.transform(
Arg.const(_.str, Some(Wrapped("ConstStr!"))),
Arg.computed(_.int, person => Wrapped(person.int + 100))
)
```

expands to:
``` scala
{
val x$4$proxy5: FunctionMirror[Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2]] {
type Return >: Person2 <: Person2
} = FunctionMirror.asInstanceOf[FunctionMirror[Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2]] {
type Return >: Person2 <: Person2
}]
val builder: AppliedViaBuilder[Person, Return, Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2], Nothing] = inline$instance[Person, x$4$proxy5.Return, Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2], Nothing](person, ((int: Wrapped[Int], str: Option[Wrapped[String]], inside: Inside2, collectionOfNumbers: List[Wrapped[Float]]) => Person2.apply(int, str, inside, collectionOfNumbers)))
val AppliedViaBuilder_this: AppliedViaBuilder[Person, Person2, Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2], FunctionArguments {
val int: Wrapped[Int]
val str: Option[Wrapped[String]]
val inside: Inside2
val collectionOfNumbers: List[Wrapped[Float]]
}] = builder.asInstanceOf[[ArgSelector >: Nothing <: FunctionArguments] => AppliedViaBuilder[Person, Return, Function4[Wrapped[Int], Option[Wrapped[String]], Inside2, List[Wrapped[Float]], Person2], ArgSelector][FunctionArguments {
val int: Wrapped[Int]
val str: Option[Wrapped[String]]
val inside: Inside2
val collectionOfNumbers: List[Wrapped[Float]]
}]]

({
val source$proxy5: Person = AppliedViaBuilder_this.inline$source

(AppliedViaBuilder_this.inline$function.apply(Wrapped.apply[Int](source$proxy5.int.+(100)), Some.apply[Wrapped[String]](Wrapped.apply[String]("ConstStr!")), new Inside2(int = source$proxy5.inside.int, str = source$proxy5.inside.str, inside = Some.apply[EvenMoreInside2](new EvenMoreInside2(str = source$proxy5.inside.inside.str, int = source$proxy5.inside.inside.int))), source$proxy5.collectionOfNumbers.map[Wrapped[Float]](((src: Float) => new Wrapped[Float](src))).to[List[Wrapped[Float]] & Iterable[Wrapped[Float]]](iterableFactory[Wrapped[Float]])): Person2)
}: Person2)
}
```
9 changes: 6 additions & 3 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -36,15 +36,18 @@ lazy val ducktape =
project
.in(file("ducktape"))
.settings(
scalacOptions ++= List("-Xcheck-macros", "-no-indent", "-old-syntax", "-Xfatal-warnings"),
scalacOptions ++= List("-Xcheck-macros", "-no-indent", "-old-syntax", "-Xfatal-warnings", "-deprecation"),
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))
.settings(
mdocVariables := Map("VERSION" -> version.value),
libraryDependencies += ("org.scalameta" %% "scalafmt-dynamic" % "3.6.1").cross(CrossVersion.for3Use2_13),
publish / skip := true
)
.dependsOn(ducktape)
.enablePlugins(MdocPlugin)
99 changes: 98 additions & 1 deletion docs/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -277,4 +277,101 @@ val transformed = definedTransformer.transform(testClass)

### A look at the generated code

#### -- TODO --
To inspect the code that is generated you can use `Transformer.Debug.showCode`, this method will print
the generated code at compile time for you to analyze and see if there's something funny going on after the macro expands.

For the sake of documentation let's also give some examples of what should be the expected output for some basic usages of `ducktape`.

#### Generated code - product transformations
Given a structure of case classes like the ones below let's examine the output that `ducktape` splices into your code:

```scala mdoc:reset-object:silent
import io.github.arainko.ducktape.*

final case class Wrapped[A](value: A) extends AnyVal

case class Person(int: Int, str: Option[String], inside: Inside, collectionOfNumbers: Vector[Float])
case class Person2(int: Wrapped[Int], str: Option[Wrapped[String]], inside: Inside2, collectionOfNumbers: List[Wrapped[Float]])

case class Inside(str: String, int: Int, inside: EvenMoreInside)
case class Inside2(int: Int, str: String, inside: Option[EvenMoreInside2])

case class EvenMoreInside(str: String, int: Int)
case class EvenMoreInside2(str: String, int: Int)

val person = Person(23, Some("str"), Inside("insideStr", 24, EvenMoreInside("evenMoreInsideStr", 25)), Vector.empty)
```
#### Generated code - expansion of `.to`
Calling the `.to` method
```scala mdoc:silent
person.to[Person2]
```
expands to:
```scala mdoc:passthrough
import io.github.arainko.ducktape.docs.*

Docs.printCode(person.to[Person2])
```

#### Generated code - expansion of `.into`
Calling the `.into` method
```scala mdoc:silent
person
.into[Person2]
.transform(
Field.const(_.str, Some(Wrapped("ConstString!"))),
Field.computed(_.int, person => Wrapped(person.int + 100)),
)
```
expands to:
```scala mdoc:passthrough
import io.github.arainko.ducktape.docs.*

Docs.printCode(
person
.into[Person2]
.transform(
Field.const(_.str, Some(Wrapped("ConstString!"))),
Field.computed(_.int, person => Wrapped(person.int + 100)),
)
)
```

#### Generated code - expansion of `.via`
Calling the `.via` method
```scala mdoc:silent
person.via(Person2.apply)
```

expands to:
```scala mdoc:passthrough
import io.github.arainko.ducktape.docs.*

Docs.printCode(person.via(Person2.apply))
```

#### Generated code - expansion of `.intoVia`
Calling the `.intoVia` method with subsequent transformation customizations
```scala mdoc:silent
person
.intoVia(Person2.apply)
.transform(
Arg.const(_.str, Some(Wrapped("ConstStr!"))),
Arg.computed(_.int, person => Wrapped(person.int + 100))
)
```

expands to:
```scala mdoc:passthrough
import io.github.arainko.ducktape.docs.*

Docs.printCode(
person
.intoVia(Person2.apply)
.transform(
Arg.const(_.str, Some(Wrapped("ConstStr!"))),
Arg.computed(_.int, person => Wrapped(person.int + 100))
)
)

```
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package io.github.arainko.ducktape.docs

import scala.quoted.*
import org.scalafmt.interfaces.Scalafmt
import java.nio.file.Path
import org.scalafmt.dynamic.ConsoleScalafmtReporter
import java.io.PrintStream
import java.io.OutputStream

/**
* Sometimes the code printed with `Printer.TreeShortCode` is not fully legal (at least in scalafmt terms)
* so we create an error reporter that doesn't report anything to not bother ourselves with weird stacktraces when the docs are compiling
*/
object SilentReporter extends ConsoleScalafmtReporter(PrintStream(OutputStream.nullOutputStream()))

object Docs {
val scalafmt = Scalafmt.create(this.getClass.getClassLoader()).withReporter(SilentReporter)
val config = Path.of(".scalafmt.conf")

inline def printCode[A](inline value: A): A = ${ printCodeMacro[A]('value) }

def printCodeMacro[A: Type](value: Expr[A])(using Quotes): Expr[A] = {
import quotes.reflect.*

val struct = Printer.TreeShortCode.show(value.asTerm)

// we need to enclose it inside a class/object, otherwise scalafmt doesn't run
val enclosedCode =
s"""object Code {
| $struct
|}""".stripMargin

val formatted =
scalafmt
.format(config, Path.of("Code.scala"), enclosedCode)
.linesWithSeparators
.toVector
.drop(1) // strip 'object Code {'
.dropRight(1) // strip the block-closing '}'
.prepended("``` scala \n") // enclose it in a Scala markdown block
.appended("```")
.mkString

'{
println(${ Expr(formatted) })
$value
}
}
}
Loading