Skip to content

Instantly share code, notes, and snippets.

@DylanLukes
Last active October 25, 2018 06:38
Show Gist options
  • Save DylanLukes/6789799a177839abf7d5db9f203a71bf to your computer and use it in GitHub Desktop.
Save DylanLukes/6789799a177839abf7d5db9f203a71bf to your computer and use it in GitHub Desktop.
package com.emg.slidedeck.js.macros
import scala.collection.immutable.Seq
import scala.meta._
import scala.meta.dialects.Paradise211
class ScalaJSDefinedApply extends scala.annotation.StaticAnnotation {
inline def apply(defn: Any): Any = meta {
def extract(trt: Defn.Trait): Seq[(Term.Name, Option[String], Type, Term)] = {
trt.collect {
case dv @ Defn.Val(mods, Seq(Pat.Var.Term(name)), Some(tpe), rhs @ q"js.undefined") =>
val jsName = mods.collectFirst {
case mod"@JSName(${lit: Lit})" =>
lit.value match {
case s: String => s
case _ => abort(s"@ScalaJSDefinedApply: @JSName must be called with a string literal.")
}
// todo: add case of js.Symbol
}
(name, jsName, tpe, rhs)
case dv @ Defn.Val(_, Seq(_, _, _*), None, _) =>
abort(s"@ScalaJSDefinedApply: $dv must only declare one left-hand side.")
case dv @ Defn.Val(_, _, None, _) =>
abort(s"@ScalaJSDefinedApply: $dv must have an explicit type declaration.")
case dv @ Defn.Val(_, _, _, _) =>
abort(s"@ScalaJSDefinedApply: $dv must be initialized to js.undefined.")
}
}
def generate(tname: Type.Name, schema: Seq[(Term.Name, Option[String], Type, Term)]): Defn.Def = {
val params = schema.map {
case (name, _, tpe, default) =>
param"$name: $tpe = $default"
}
val args = schema.map {
case (name, jsName, _, _) =>
val namestr = Lit.String(jsName.getOrElse(name.value))
arg"($namestr, $name.asInstanceOf[UndefOr[js.Any]])"
}
q"""def apply(..$params): $tname = {
import js.JSConverters._
val args = Map[String, js.UndefOr[js.Any]](..$args).collect {
case (k, v) if !js.isUndefined(v) => (k, v.asInstanceOf[js.Any])
};
args.toJSDictionary.asInstanceOf[$tname]
}"""
}
defn match {
// A companion Object already exists in scope. Modify it.
case Term.Block(Seq(
dt @ Defn.Trait(_, tname, _, _, _),
companion: Defn.Object
)) =>
val fields = extract(dt)
val applyMethod = generate(tname, fields)
Term.Block(Seq(dt, companion.copy(
templ = companion.templ.copy(
stats = companion.templ.stats.map { applyMethod +: _ }
))
))
// A companion Object does not yet exist.
case dt @ Defn.Trait(_, tname, _, _, _) =>
val schema = extract(dt)
val apply = generate(tname, schema)
val companion = q"object ${Term.Name(tname.value)} { $apply }"
Term.Block(Seq(dt, companion))
case _ =>
abort("@ScalaJSDefinedApply must annotate a trait.")
}
}
}
@glmars
Copy link

glmars commented Oct 16, 2018

Hi! Is this the latest version of your @ScalaJSDefinedApply macro?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment