Skip to content
Open
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
/*
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.blocks.schema

import scala.language.experimental.macros

/**
* Scala 2 selector-based API for MigrationBuilder. Mixes in methods that accept
* selector lambdas (e.g. `_.name`) and convert them to DynamicOptic at compile
* time via blackbox macros.
*
* Each method here overloads the corresponding DynamicOptic-based method
* defined in MigrationBuilder itself. Overload resolution picks the selector
* variant when the argument is a lambda and the optic variant when a
* DynamicOptic is supplied directly.
*/
trait MigrationBuilderVersionSpecific[A, B, State <: BuilderState] {
this: MigrationBuilder[A, B, State] =>

def renameField[C, D](from: A => C, to: B => D): MigrationBuilder[A, B, State] =
macro MigrationMacros.renameFieldImpl

def addField[C](at: B => C, value: MigrationExpr): MigrationBuilder[A, B, State] =
macro MigrationMacros.addFieldImpl

def dropField[C](at: A => C, defaultForReverse: MigrationExpr): MigrationBuilder[A, B, State] =
macro MigrationMacros.dropFieldImpl

def transformValue[C](
at: A => C,
transform: MigrationExpr,
inverseTransform: MigrationExpr
): MigrationBuilder[A, B, State] = macro MigrationMacros.transformValueImpl

def optionalize[C](at: A => C, defaultForReverse: MigrationExpr): MigrationBuilder[A, B, State] =
macro MigrationMacros.optionalizeImpl

def mandate[C](at: A => C): MigrationBuilder[A, B, State] =
macro MigrationMacros.mandateImpl

def changeFieldType[C](
at: A => C,
converter: MigrationExpr,
inverseConverter: MigrationExpr
): MigrationBuilder[A, B, State] = macro MigrationMacros.changeFieldTypeImpl
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
/*
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.blocks.schema

import scala.reflect.NameTransformer
import scala.reflect.macros.blackbox

object MigrationMacros {

/**
* Converts a selector lambda like `_.field` or `_.a.b` into a DynamicOptic
* tree by walking the lambda body and collecting each field-select step.
*/
def selectorBodyToOptic(c: blackbox.Context)(selectorTree: c.Tree): c.Tree = {
import c.universe._

def extractFields(tree: c.Tree): List[String] = tree match {
case q"$parent.$field" => extractFields(parent) :+ NameTransformer.decode(field.toString)
case _: Ident => Nil
case _ =>
c.abort(
c.enclosingPosition,
s"Migration selector must be a chain of field accesses (e.g. _.field or _.a.b), got '$tree'"
)
}

val body = selectorTree match {
case Function(_, body) => body
case _ => c.abort(c.enclosingPosition, s"Expected a lambda expression, got '$selectorTree'")
}

val fields = extractFields(body)
fields.foldLeft[c.Tree](q"_root_.zio.blocks.schema.DynamicOptic.root") { (acc, name) =>
q"$acc.field($name)"
}
}

def renameFieldImpl(c: blackbox.Context)(from: c.Expr[Any], to: c.Expr[Any]): c.Tree = {
import c.universe._
val fromOptic = selectorBodyToOptic(c)(from.tree)
val toOptic = selectorBodyToOptic(c)(to.tree)
q"${c.prefix}.renameField($fromOptic, $toOptic)"
}

def addFieldImpl(c: blackbox.Context)(at: c.Expr[Any], value: c.Expr[Any]): c.Tree = {
import c.universe._
val atOptic = selectorBodyToOptic(c)(at.tree)
q"${c.prefix}.addField($atOptic, $value)"
}

def dropFieldImpl(c: blackbox.Context)(at: c.Expr[Any], defaultForReverse: c.Expr[Any]): c.Tree = {
import c.universe._
val atOptic = selectorBodyToOptic(c)(at.tree)
q"${c.prefix}.dropField($atOptic, $defaultForReverse)"
}

def transformValueImpl(
c: blackbox.Context
)(at: c.Expr[Any], transform: c.Expr[Any], inverseTransform: c.Expr[Any]): c.Tree = {
import c.universe._
val atOptic = selectorBodyToOptic(c)(at.tree)
q"${c.prefix}.transformValue($atOptic, $transform, $inverseTransform)"
}

def optionalizeImpl(c: blackbox.Context)(at: c.Expr[Any], defaultForReverse: c.Expr[Any]): c.Tree = {
import c.universe._
val atOptic = selectorBodyToOptic(c)(at.tree)
q"${c.prefix}.optionalize($atOptic, $defaultForReverse)"
}

def mandateImpl(c: blackbox.Context)(at: c.Expr[Any]): c.Tree = {
import c.universe._
val atOptic = selectorBodyToOptic(c)(at.tree)
q"${c.prefix}.mandate($atOptic)"
}

def changeFieldTypeImpl(
c: blackbox.Context
)(at: c.Expr[Any], converter: c.Expr[Any], inverseConverter: c.Expr[Any]): c.Tree = {
import c.universe._
val atOptic = selectorBodyToOptic(c)(at.tree)
q"${c.prefix}.changeFieldType($atOptic, $converter, $inverseConverter)"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
/*
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.blocks.schema

import scala.quoted.*

/**
* Scala 3 selector-based API for MigrationBuilder. Mixes in inline methods that
* accept selector lambdas (e.g. `_.name`) and convert them to DynamicOptic at
* compile time via MigrationMacros.selectorToOpticImpl.
*
* `opticOf` is the single-splice macro helper; all other methods are plain
* inline methods that delegate to the DynamicOptic-based overloads defined in
* MigrationBuilder once the selector has been materialised.
*/
trait MigrationBuilderVersionSpecific[A, B, State <: BuilderState] {
this: MigrationBuilder[A, B, State] =>

/** Materialises a selector lambda into a DynamicOptic at compile time. */
inline def opticOf[X](inline path: X => Any): DynamicOptic =
${ MigrationMacros.selectorToOpticImpl('path) }

inline def renameField[C, D](inline from: A => C, inline to: B => D): MigrationBuilder[A, B, State] =
renameField(opticOf(from), opticOf(to))

inline def addField[C](inline at: B => C, value: MigrationExpr): MigrationBuilder[A, B, State] =
addField(opticOf(at), value)

inline def dropField[C](
inline at: A => C,
defaultForReverse: MigrationExpr
): MigrationBuilder[A, B, State] =
dropField(opticOf(at), defaultForReverse)

inline def transformValue[C](
inline at: A => C,
transform: MigrationExpr,
inverseTransform: MigrationExpr
): MigrationBuilder[A, B, State] =
transformValue(opticOf(at), transform, inverseTransform)

inline def optionalize[C](
inline at: A => C,
defaultForReverse: MigrationExpr
): MigrationBuilder[A, B, State] =
optionalize(opticOf(at), defaultForReverse)

inline def mandate[C](inline at: A => C): MigrationBuilder[A, B, State] =
mandate(opticOf(at))

inline def changeFieldType[C](
inline at: A => C,
converter: MigrationExpr,
inverseConverter: MigrationExpr
): MigrationBuilder[A, B, State] =
changeFieldType(opticOf(at), converter, inverseConverter)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
/*
* Copyright 2024-2026 John A. De Goes and the ZIO Contributors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package zio.blocks.schema

import scala.annotation.tailrec
import scala.quoted.*

object MigrationMacros {

/**
* Converts a selector lambda like `_.field` or `_.a.b` into a `DynamicOptic`
* at compile time by walking the lambda body and collecting each field-select
* step.
*/
def selectorToOpticImpl[A: Type](path: Expr[A => Any])(using Quotes): Expr[DynamicOptic] = {
import quotes.reflect.*

@tailrec
def toLambdaBody(term: Term): Term = term match {
case Inlined(_, _, inner) => toLambdaBody(inner)
case Block(List(DefDef(_, _, _, Some(body))), _) => body
case _ =>
report.errorAndAbort(s"Expected a lambda expression, got '${term.show}'")
}

def extractFields(term: Term): List[String] = term match {
case Select(parent, fieldName) => extractFields(parent) :+ fieldName
case _: Ident => Nil
case Inlined(_, _, inner) => extractFields(inner)
case _ =>
report.errorAndAbort(
s"Migration selector must be a chain of field accesses (e.g. _.field or _.a.b), got '${term.show}'"
)
}

val body = toLambdaBody(path.asTerm)
val fields = extractFields(body)
fields.foldLeft('{ DynamicOptic.root }) { (acc, name) =>
val nameExpr = Expr(name)
'{ $acc.field($nameExpr) }
}
}
}
Loading
Loading