-
Notifications
You must be signed in to change notification settings - Fork 348
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implement static operator for splicing constants e.g. static(Constant…
…s.Foo) (#16)
- Loading branch information
1 parent
1b2870b
commit 8345399
Showing
13 changed files
with
589 additions
and
42 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
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 |
---|---|---|
@@ -1,3 +1,4 @@ | ||
package io.getquill | ||
|
||
export Dsl._ | ||
export StaticSplice._ |
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,72 @@ | ||
package io.getquill | ||
|
||
import scala.quoted._ | ||
import java.time.format.DateTimeFormatter | ||
import io.getquill.util.Format | ||
import io.getquill.util.LoadModule | ||
import scala.util.Try | ||
import scala.util.Failure | ||
import scala.util.Success | ||
import io.getquill.util.CommonExtensions.Either._ | ||
|
||
/** | ||
* Trait that allows usage of 'static' block. Can declared one of these and use similar to encoders | ||
* but it needs to be compiled in a previous compilation unit and a global static. | ||
* TODO More explanation | ||
*/ | ||
trait StaticSplice[T]: | ||
def apply(value: T): String | ||
|
||
object StaticSplice: | ||
import io.getquill.metaprog.Extractors._ | ||
|
||
object Summon: | ||
def apply[T: Type](using Quotes): Either[String, StaticSplice[T]] = | ||
import quotes.reflect.{ Try => TTry, _} | ||
for { | ||
summonValue <- Expr.summon[StaticSplice[T]].toEitherOr(s"a StaticSplice[${Format.TypeOf[T]}] cannot be summoned") | ||
// Summoning StaticSplice[T] will given (SpliceString: StaticSplice[String]) | ||
// (a.k.a. Typed(Ident(SpliceString), TypeTree(StaticSplice[String])) usually with an outer inline surrounding it all) | ||
// so then we need to use Untype to just get SpliceString which is a module that we can load | ||
staticSpliceType = Untype(summonValue.asTerm.underlyingArgument).tpe.widen | ||
|
||
untypedModule <- LoadModule.TypeRepr(staticSpliceType).toEither.mapLeft(_.getMessage) | ||
module <- Try(untypedModule.asInstanceOf[StaticSplice[T]]).toEither.mapLeft(_.getMessage) | ||
} yield (module) | ||
|
||
object SpliceString extends StaticSplice[String]: | ||
def apply(value: String) = s"'${value}'" | ||
inline given StaticSplice[String] = SpliceString | ||
|
||
object SpliceInt extends StaticSplice[Int]: | ||
def apply(value: Int) = s"${value}" | ||
inline given StaticSplice[Int] = SpliceInt | ||
|
||
object SpliceShort extends StaticSplice[Short]: | ||
def apply(value: Short) = s"${value}" | ||
inline given StaticSplice[Short] = SpliceShort | ||
|
||
object SpliceLong extends StaticSplice[Long]: | ||
def apply(value: Long) = s"${value}" | ||
inline given StaticSplice[Long] = SpliceLong | ||
|
||
object SpliceFloat extends StaticSplice[Float]: | ||
def apply(value: Float) = s"${value}" | ||
inline given StaticSplice[Float] = SpliceFloat | ||
|
||
object SpliceDouble extends StaticSplice[Double]: | ||
def apply(value: Double) = s"${value}" | ||
inline given StaticSplice[Double] = SpliceDouble | ||
|
||
object SpliceDate extends StaticSplice[java.sql.Date]: | ||
def apply(value: java.sql.Date) = | ||
value.toLocalDate.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | ||
inline given StaticSplice[java.sql.Date] = SpliceDate | ||
|
||
object SpliceLocalDate extends StaticSplice[java.time.LocalDate]: | ||
def apply(value: java.time.LocalDate) = | ||
value.format(DateTimeFormatter.ofPattern("yyyyMMdd")) | ||
inline given StaticSplice[java.time.LocalDate] = SpliceLocalDate | ||
|
||
|
||
end StaticSplice |
131 changes: 131 additions & 0 deletions
131
quill-sql/src/main/scala/io/getquill/context/ReflectiveChainLookup.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,131 @@ | ||
package io.getquill.context | ||
|
||
import scala.quoted._ | ||
import io.getquill.StaticSplice | ||
import io.getquill.util.LoadModule | ||
import io.getquill.metaprog.Extractors | ||
import scala.util.Success | ||
import scala.util.Failure | ||
import scala.util.Try | ||
import scala.util.Either | ||
import io.getquill.util.Format | ||
|
||
private[getquill] object ReflectivePathChainLookup: | ||
sealed trait LookupElement { def cls: Class[_]; def current: Object } | ||
object LookupElement: | ||
// For a module class the lookup-object is actualy a class. For example | ||
// for: object Foo { object Bar { ... } } you would do: | ||
// val submod: Class[Bar] = Class[Foo].getDeclaredClasses.find(_.name endsWith "Bar$") | ||
// submod.getField("MODULE$").get(submod /*Object is passed into here*/) | ||
// (note that the Class[---] things are typed above but in reality when dealing with them in the reflection they won't be) | ||
case class ModuleClass(cls: Class[_]) extends LookupElement { val current: Object = cls } | ||
// For a regular value reference, we can simply lookup the class from the object | ||
case class Value(current: Object) extends LookupElement { val cls = current.getClass } | ||
end LookupElement | ||
|
||
case class LookupPath(element: LookupElement, path: String): | ||
def cls = element.cls | ||
def current = element.current | ||
|
||
extension (elem: Option[Object]) | ||
def nullCheck(path: String, cls: Class[_], lookupType: String) = | ||
elem match | ||
case Some(null) => | ||
println(s"The ${lookupType} ${path} can be looked up from ${cls} but the value is null") | ||
None | ||
case other => | ||
other | ||
|
||
object Lookup: | ||
|
||
|
||
|
||
// If it's a method on a regular (i.e. dynamic) class. Try to that up | ||
object Method: | ||
def unapply(lookup: LookupPath): Option[LookupElement.Value] = | ||
lookupFirstMethod(lookup.path)(lookup.cls, lookup.current)("method").map(LookupElement.Value(_)) | ||
|
||
// if it's a field on a regular (i.e. dynamic) class. Try to look that up | ||
object Field: | ||
def unapply(lookup: LookupPath): Option[LookupElement.Value] = | ||
lookupFirstMethod(lookup.path)(lookup.cls, lookup.current)("field").map(LookupElement.Value(_)) | ||
|
||
// Scala object-in-object e.g. object Foo { object Bar { ... } }. Lookup the `Bar` | ||
// it will be represented in java as: path.to.Foo$Bar. | ||
object Submodule: | ||
def unapply(lookup: LookupPath): Option[LookupElement.ModuleClass] = | ||
val submod = lookup.cls.getDeclaredClasses.find(c => c.getName.endsWith(lookup.path + "$")) | ||
submod.orElse { | ||
// Odd pattern for top level object: object Foo { object Bar } | ||
// there won't be a Bar in getDeclaredClasses but instead a Bar field on Foo whose value is null | ||
lookup.cls.getFields.find(_.getName == lookup.path).map(_.getType) | ||
}.map(LookupElement.ModuleClass(_)) | ||
|
||
object HelperObjectField: | ||
def unapply(lookup: LookupPath): Option[LookupElement.Value] = | ||
// Get Foo.MODULE$ | ||
val submodOpt: Option[Object] = lookupModuleObject(lookup.current)(lookup.cls) | ||
// Get Foo.MODULE$.fields. The `Field` unapply can be recycled for this purpose | ||
submodOpt.map(submod => | ||
// I.e. lookup MODULE$.field | ||
lookupFirstMethod(lookup.path)(lookup.cls, submod)("$MODULE.field").map(LookupElement.Value(_)) | ||
).flatten | ||
|
||
object HelperObjectMethod: | ||
def unapply(lookup: LookupPath): Option[LookupElement.Value] = | ||
// Get Foo.MODULE$ | ||
val submodOpt: Option[Object] = lookupModuleObject(lookup.current)(lookup.cls) | ||
// Get Foo.MODULE$.methods. The `Method` unapply can be recycled for this purpose | ||
submodOpt.map(submod => | ||
// I.e. lookup MODULE$.method | ||
lookupFirstMethod(lookup.path)(lookup.cls, submod)("$MODULE.method").map(LookupElement.Value(_)) | ||
).flatten | ||
|
||
// Lookup object Foo { ... } element MODULE$ which is the singleton instance in the Java representation | ||
def lookupModuleObject(obj: Object)(cls: Class[_] = obj.getClass): Option[Object] = | ||
cls.getFields.find(_.getName == "MODULE$").map(m => Try(m.get(obj)).toOption).flatten | ||
|
||
def lookupFirstMethod(path: String)(cls: Class[_], instance: Object)(label: String) = | ||
val methodOpt = cls.getMethods.find(m => m.getName == path && m.getParameterCount == 0) | ||
methodOpt.map(m => Try(m.invoke(instance)).toOption).flatten.nullCheck(path, cls, label) | ||
|
||
def lookupFirstField(path: String)(cls: Class[_], instance: Object)(label: String) = | ||
val fieldOpt = cls.getFields.find(_.getName == path) | ||
fieldOpt.map(f => Try(f.get(instance)).toOption).flatten.nullCheck(path, cls, label) | ||
|
||
end Lookup | ||
|
||
import java.lang.reflect.{ Method, Field } | ||
def singleLookup(elem: LookupElement, path: String): Option[LookupElement] = | ||
LookupPath(elem, path) match | ||
case Lookup.Submodule(elem) => Some(elem) | ||
case Lookup.Method(elem) => Some(elem) | ||
case Lookup.Field(elem) => Some(elem) | ||
case Lookup.HelperObjectMethod(elem) => Some(elem) | ||
case Lookup.HelperObjectField(elem) => Some(elem) | ||
case _ => None | ||
|
||
def chainLookup(element: LookupElement, paths: List[String])(pathsSeen: List[String] = List()): Either[String, LookupElement] = | ||
import StringOps._ | ||
paths match | ||
case Nil => Right(element) | ||
case head :: tail => | ||
val nextElementOpt = singleLookup(element, head) | ||
nextElementOpt match | ||
case Some(nextElement) => | ||
chainLookup(nextElement, tail)(pathsSeen :+ head) | ||
case None => | ||
Left( | ||
s"Could not look up the path `${pathsSeen.dots}[$head]` from the `${element.cls.getName}` ${element.current}.\n" + | ||
s"Remaining path: ${paths.mkString(".")}" | ||
) | ||
|
||
def apply(obj: Object, paths: List[String]) = | ||
chainLookup(LookupElement.Value(obj), paths)() | ||
|
||
object StringOps: | ||
extension (strList: List[String]) | ||
def dots = | ||
if (!strList.isEmpty) strList.mkString("", ".", ".") else "" | ||
|
||
end ReflectivePathChainLookup |
147 changes: 147 additions & 0 deletions
147
quill-sql/src/main/scala/io/getquill/context/StaticSpliceMacro.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,147 @@ | ||
package io.getquill.context | ||
|
||
import scala.quoted._ | ||
import io.getquill.StaticSplice | ||
import io.getquill.util.LoadModule | ||
import io.getquill.metaprog.Extractors | ||
import scala.util.Success | ||
import scala.util.Failure | ||
import scala.util.Try | ||
import scala.util.Right | ||
import scala.util.Left | ||
import io.getquill.util.Format | ||
import io.getquill.Quoted | ||
import io.getquill.quat.QuatMaking | ||
import io.getquill.parser.Lifter | ||
import scala.util.Try | ||
import io.getquill.StaticSplice | ||
import io.getquill.util.CommonExtensions.Either._ | ||
import io.getquill.util.CommonExtensions.Throwable._ | ||
import io.getquill.util.CommonExtensions.For._ | ||
|
||
object StaticSpliceMacro { | ||
import Extractors._ | ||
|
||
private[getquill] object SelectPath: | ||
def recurseInto(using Quotes)(term: quotes.reflect.Term, accum: List[String] = List()): Option[(quotes.reflect.Term, List[String])] = | ||
import quotes.reflect._ | ||
term match | ||
// Recurses through a series of selects do the core identifier e.g: | ||
// Select(Select(Ident("core"), "foo"), "bar") => recurseInto( {Select(Ident("core"), "foo")}, "bar" +: List("baz") ) | ||
case IgnoreApplyNoargs(Select(inner, pathNode)) => recurseInto(inner, pathNode +: accum) | ||
case id: Ident => Some((id, accum)) | ||
// If at the core of the nested selects is not a Ident, this does not match | ||
case other => None | ||
|
||
def unapply(using Quotes)(term: quotes.reflect.Term): Option[(quotes.reflect.Term, List[String])] = | ||
import quotes.reflect._ | ||
term match | ||
// recurse on Module.Something | ||
case select: Select => recurseInto(select) | ||
// recurse on Module.SomethingAply() from which the empty-args apply i.e. `()` needs to be ignored | ||
case select @ IgnoreApplyNoargs(_:Select) => recurseInto(select) | ||
case id: Ident => Some((id, List())) | ||
case _ => None | ||
end SelectPath | ||
|
||
extension [T](opt: Option[T]) | ||
def nullAsNone = | ||
opt match | ||
case Some(null) => None | ||
case _ => opt | ||
|
||
object DefTerm: | ||
def unapply(using Quotes)(term: quotes.reflect.Term): Option[quotes.reflect.Term] = | ||
import quotes.reflect._ | ||
if (term.tpe.termSymbol.isValDef || term.tpe.termSymbol.isDefDef) Some(term) | ||
else None | ||
|
||
def isModule(using Quotes)(sym: quotes.reflect.Symbol) = | ||
import quotes.reflect._ | ||
val f = sym.flags | ||
f.is(Flags.Module) && !f.is(Flags.Package) && !f.is(Flags.Param) && !f.is(Flags.ParamAccessor) && !f.is(Flags.Method) | ||
|
||
object TermIsModule: | ||
def unapply(using Quotes)(value: quotes.reflect.Term): Boolean = | ||
import quotes.reflect.{Try => _, _} | ||
val tpe = value.tpe.widen | ||
if (isModule(tpe.typeSymbol)) | ||
true | ||
else | ||
false | ||
|
||
/** The term is a static module but not a package */ | ||
object TermOwnerIsModule: | ||
def unapply(using Quotes)(value: quotes.reflect.Term): Option[quotes.reflect.TypeRepr] = | ||
import quotes.reflect.{Try => _, _} | ||
Try(value.tpe.termSymbol.owner).toOption.flatMap { owner => | ||
val memberType = value.tpe.memberType(owner) | ||
if (isModule(memberType.typeSymbol)) | ||
Some(memberType) | ||
else | ||
None | ||
} | ||
|
||
def apply[T: Type](valueRaw: Expr[T])(using Quotes): Expr[T] = | ||
import quotes.reflect.{ Try => _, _ } | ||
import ReflectivePathChainLookup.StringOps._ | ||
import io.getquill.ast._ | ||
|
||
val value = valueRaw.asTerm.underlyingArgument | ||
|
||
// TODO summon a Expr[StaticSplicer] using the T type passed originally. | ||
// Then use use LoadModule to get the value of that thing during runtime so we can use it | ||
// (i.e. see io.getquill.metaprog.SummonParser on how to do that) | ||
// for primitive types e.g. String, Int, Float etc... rather then making summonable splicers | ||
// it is easier to just splice them directly, since otherwise those StaticSplicer modules themselves | ||
// need to be compiled in a previous compilation unit, and we want to define them here. | ||
// Technically that should be fine because they will only actually be used in the testing code though | ||
// should think about this more later. For now just do toString to check that stuff from the main return works | ||
|
||
val (pathRoot, selectPath) = | ||
Untype(value) match | ||
case SelectPath(pathRoot, selectPath) => (pathRoot, selectPath) | ||
case other => | ||
// TODO Long explanatory message about how it has to some value inside object foo inside object bar... and it needs to be a thing compiled in a previous compilation unit | ||
report.throwError(s"Could not load a static value `${Format.Term(value)}` from ${Printer.TreeStructure.show(other)}") | ||
|
||
|
||
val (ownerTpe, path) = | ||
pathRoot match | ||
case term @ DefTerm(TermIsModule()) => | ||
(pathRoot.tpe, selectPath) | ||
// TODO Maybe only check module owner if Path is Nil? | ||
case term @ DefTerm(TermOwnerIsModule(owner)) => | ||
(owner, pathRoot.symbol.name +: selectPath) | ||
case _ => | ||
report.throwError(s"Cannot evaluate the static path ${Format.Term(value)}. Neither it's type ${Format.TypeRepr(pathRoot.tpe)} nor the owner of this type is a static module.") | ||
|
||
val module = LoadModule.TypeRepr(ownerTpe).toEither.discardLeft(e => | ||
// TODO Long explanatory message about how it has to some value inside object foo inside object bar... and it needs to be a thing compiled in a previous compilation unit | ||
report.throwError(s"Could not look up {${(ownerTpe)}}.${path.mkString(".")} from the object.\nStatic load failed due to: ${e.stackTraceToString}") | ||
) | ||
|
||
val splicedValue = | ||
ReflectivePathChainLookup(module, path).discardLeft(msg => | ||
report.throwError(s"Could not look up {${(ownerTpe)}}.${path.mkString(".")}. Failed because:\n${msg}") | ||
) | ||
|
||
val quat = Lifter.quat(QuatMaking.ofType[T]) | ||
|
||
def errorMsg(error: String) = | ||
s"Could not statically splice ${Format.Term(value)} because ${error}" | ||
|
||
val spliceEither = | ||
for { | ||
castSplice <- Try(splicedValue.current.asInstanceOf[T]).toEither.mapLeft(e => errorMsg(e.getMessage)) | ||
splicer <- StaticSplice.Summon[T].mapLeft(str => errorMsg(str)) | ||
splice <- Try(splicer(castSplice)).toEither.mapLeft(e => errorMsg(e.getMessage)) | ||
} yield splice | ||
|
||
val spliceStr = | ||
spliceEither match | ||
case Left(msg) => report.throwError(msg, valueRaw) | ||
case Right(value) => value | ||
|
||
UnquoteMacro('{ Quoted[T](Infix(List(${Expr(spliceStr)}), List(), true, $quat),Nil, Nil) }) | ||
} |
Oops, something went wrong.