-
Notifications
You must be signed in to change notification settings - Fork 2
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
4 changed files
with
146 additions
and
2 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
89 changes: 89 additions & 0 deletions
89
modules/generic/src/main/scala-3/erules/generic/RuleMacros.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,89 @@ | ||
package erules.generic | ||
|
||
import erules.core.Rule | ||
|
||
import scala.annotation.tailrec | ||
|
||
private[generic] trait RuleMacros: | ||
|
||
extension [F[_], T](rule: Rule[F, T]) | ||
/** Contramap `Rule` and add target info invoking `targetInfo` and passing the expression of the | ||
* map function `f` | ||
* | ||
* For example | ||
* {{{ | ||
* case class Foo(bar: Bar) | ||
* case class Bar(test: Test) | ||
* case class Test(value: Int) | ||
* | ||
* val rule: Rule[Int] = Rule("RULE").const(RuleVerdict.Ignore.withoutReasons) | ||
* val fooRule: Rule[Foo] = rule.contramapTarget[Foo](_.bar.test.value) | ||
* | ||
* fooRule.targetInfo | ||
* scala> val res0: Option[String] = Some(bar.test.value) | ||
* }}} | ||
* | ||
* @see | ||
* [[Rule.contramap()]] and [[Rule.targetInfo()]] for further information | ||
*/ | ||
inline def contramapTarget[U](inline path: U => T): Rule[F, U] = | ||
${ RuleImplMacros.contramapTargetImpl[F, U, T]('rule, 'path) } | ||
|
||
private[generic] object RuleMacros extends RuleMacros | ||
|
||
private object RuleImplMacros { | ||
|
||
import scala.quoted.* | ||
|
||
def contramapTargetImpl[F[_]: Type, U: Type, T: Type]( | ||
rule: Expr[Rule[F, T]], | ||
path: Expr[U => T] | ||
)(using Quotes): Expr[Rule[F, U]] = | ||
'{ | ||
$rule.contramap($path).targetInfo(${ extractTargetInfoFromFunctionCall(path) }) | ||
} | ||
|
||
def extractTargetInfoFromFunctionCall[T: Type, U: Type]( | ||
path: Expr[T => U] | ||
)(using Quotes): Expr[String] = { | ||
|
||
import quotes.reflect.* | ||
|
||
val expectedShapeInfo = "Path must have shape: _.field1.field2.each.field3.(...)" | ||
|
||
enum PathElement { | ||
case TermPathElement(term: String, xargs: String*) extends PathElement | ||
case FunctorPathElement(functor: String, method: String, xargs: String*) extends PathElement | ||
} | ||
|
||
def toPath(tree: Tree, acc: List[PathElement]): Seq[PathElement] = { | ||
tree match { | ||
/** Field access */ | ||
case Select(deep, ident) => | ||
toPath(deep, PathElement.TermPathElement(ident) :: acc) | ||
/** The first segment from path (e.g. `_.age` -> `_`) */ | ||
case i: Ident => | ||
acc | ||
case t => | ||
report.errorAndAbort(s"Unsupported path element $t") | ||
} | ||
} | ||
|
||
val pathElements: Seq[PathElement] = path.asTerm match { | ||
/** Single inlined path */ | ||
case Inlined(_, _, Block(List(DefDef(_, _, _, Some(p))), _)) => | ||
toPath(p, List.empty) | ||
case _ => | ||
report.errorAndAbort(s"Unsupported path [$path]") | ||
} | ||
|
||
Expr( | ||
pathElements | ||
.map { | ||
case PathElement.TermPathElement(c, _ @_*) => c | ||
case PathElement.FunctorPathElement(_, method, _ @_*) => method | ||
} | ||
.mkString(".") | ||
) | ||
} | ||
} |
3 changes: 3 additions & 0 deletions
3
modules/generic/src/main/scala-3/erules/generic/implicits.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
package erules.generic | ||
|
||
object implicits extends RuleMacros |
37 changes: 37 additions & 0 deletions
37
modules/generic/src/test/scala-3/erules/generic/RuleMacrosTest.scala
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,37 @@ | ||
package erules.generic | ||
|
||
import cats.Id | ||
import erules.core.{PureRule, Rule, RuleVerdict} | ||
import org.scalatest.funsuite.AnyFunSuite | ||
import org.scalatest.matchers.should.Matchers | ||
|
||
class RuleMacrosTest extends AnyFunSuite with Matchers { | ||
|
||
import erules.generic.implicits.* | ||
|
||
test("contramapTarget should contramap and add target info") { | ||
|
||
case class Foo(bar: Bar) | ||
case class Bar(test: Test) | ||
case class Test(value: Int) | ||
|
||
val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons) | ||
val fooRule: PureRule[Foo] = rule.contramapTarget[Foo](_.bar.test.value) | ||
|
||
fooRule.targetInfo shouldBe Some("bar.test.value") | ||
} | ||
|
||
test("contramapTarget should not compile with monadic values") { | ||
""" | ||
import cats.Id | ||
case class Foo(b: Option[Bar]) | ||
case class Bar(t: Option[Test]) | ||
case class Test(value: Int) | ||
val rule: PureRule[Int] = Rule("RULE").const[Id, Int](RuleVerdict.Ignore.withoutReasons) | ||
rule.contramapTarget[Foo](_.b.flatMap(_.t.map(_.value)).get) | ||
""" shouldNot compile | ||
} | ||
} |