
contextual.interpolator.scala Maven / Gradle / Ivy
The newest version!
/* Contextual, version 1.0.0. Copyright 2016 Jon Pretty, Propensive Ltd.
*
* The primary distribution site is: http://co.ntextu.al/
*
* 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 contextual
import scala.reflect._, macros._
import language.implicitConversions
/** An [[Interpolator]] defines the compile-time and runtime behavior when interpreting an
* interpolated string. */
trait Interpolator { interpolator =>
/** The type, which is typically sealed, of all [[Context]]s which may exist for
* substitutions into this [[Interpolator]]. */
type ContextType <: Context
/** The common type that substitutions of any supported type will be converted to for
* processing at runtime, in the `evaluate` method. This is necessary as embeddings may
* be specified using typeclasses for ad-hoc types which are not known when defining the
* [[Interpolator]]. For most purposes, `String` is a reasonable choice, but there may
* be the need to attach additional metadata to substitutions (which depends on the
* original type being substituted), in which case a case class wrapping a `String` with
* additional fields may be a better choice. */
type Input
/** The type that will be used for refining return type of the evaluated result of the
* [[contextual]] macro. This type should be equal to the return type of the `evaluate`
* method. If the `evaluate` method isn't defined the default value should be `Any`.
*/
type Output
/** The [[RuntimeInterpolation]] type is a representation of the known runtime information
* about an interpolated string. Most importantly, this includes the literal parts of the
* interpolated string; the constant parts which surround the variables parts that are
* substituted into it. The [[RuntimeInterpolation]] type also provides details about the
* substituted values, in particular its context (which was determined at compile time).
*
* @param literals the literal parts of the interpolated string
* @param substitutions the substituted values, evaluated to a common [[Input]] type */
class RuntimeInterpolation(val literals: Seq[String],
val substitutions: Seq[Substitution]) {
/** A string representation of this [[RuntimeInterpolation]] */
override def toString = Seq("" +: substitutions, literals).transpose.flatten.mkString
/** Provides the sequence of [[Literal]]s and [[Hole]]s in this interpolated string. */
def parts: Seq[RuntimePart] = {
val literalsHead +: literalsTail = literals.zipWithIndex.map { case (lit, idx) =>
Literal(idx, lit)
}
literalsHead +: Seq(substitutions, literalsTail).transpose.flatten
}
}
/** The [[StaticInterpolation]] type is a representation of the known compile-time information
* about an interpolated string. Most importantly, this includes the literal parts of the
* interpolated string; the constant parts which surround the variables parts that are
* substituted into it. The [[StaticInterpolation]] type also provides details about these
* holes, specifically the possible set of contexts in which the substituted value may be
* interpreted. */
trait StaticInterpolation {
val macroContext: whitebox.Context
def literals: Seq[String]
def holes: Seq[Hole]
def literalTrees: Seq[macroContext.Tree]
def holeTrees: Seq[macroContext.Tree]
def interpolatorTerm: macroContext.Symbol
/** A string representation of this [[StaticInterpolation]] */
override def toString = Seq("" +: holes, literals).transpose.flatten.mkString
/** The universe of the whitebox macro context, should it be required. */
lazy val universe: macroContext.universe.type = macroContext.universe
/** Provides the sequence of [[Literal]]s and [[Hole]]s in this interpolated string. */
def parts: Seq[StaticPart] = {
val literalsHead +: literalsTail = literals.zipWithIndex.map { case (lit, idx) =>
Literal(idx, lit)
}
literalsHead +: Seq(holes, literalsTail).transpose.flatten
}
private def position(part: StaticPart, offset: Int): macroContext.Position = {
val errorTree: macroContext.Tree = part match {
case Hole(index, _) => holeTrees(index)
case Literal(index, _) => literalTrees(index)
}
errorTree.pos.withPoint(errorTree.pos.start + offset)
}
/** Report a compile error `message`, at the index `offset` within the [[Literal]] `part`,
* and continue evaluating the macro, whilst compilation will ultimately fail.
*
* @param part the [[Literal]] part containing the error
* @param offset the index of the error within the [[Literal]] part
* @param message the error message to report */
def error(part: Literal, offset: Int, message: String): Unit =
macroContext.error(position(part, offset), message)
/** Report a compile error `message`, at the index `offset` within the [[Literal]] `part`,
* and stop further evaluation of the macro.
*
* @param part the [[Literal]] part containing the error
* @param offset the index of the error within the [[Literal]] part
* @param message the error message to report */
def abort(part: Literal, offset: Int, message: String): Nothing =
macroContext.abort(position(part, offset), message)
/** Report a compile warning `message`, at the index `offset` within the [[Literal]] `part`,
* and continue evaluating the macro, potentially succeeding despite the warning.
*
* @param part the [[Literal]] part containing the warning
* @param offset the index of the warning within the [[Literal]] part
* @param message the warning message to report */
def warn(part: Literal, offset: Int, message: String): Unit =
macroContext.warning(position(part, offset), message)
/** Report a compile error `message`, at the [[Hole]] `part`, and continue evaluating the
* macro, whilst compilation will ultimately fail.
*
* @param part the [[Hole]] part containing the error
* @param message the error message to report */
def error(part: Hole, message: String): Unit =
macroContext.error(position(part, 0), message)
/** Report a compile error `message`, at the [[Hole]] `part`, and stop evaluating the macro.
*
* @param part the [[Hole]] part containing the error
* @param message the error message to report */
def abort(part: Hole, message: String): Nothing =
macroContext.abort(position(part, 0), message)
/** Report a compile warning `message`, at the [[Hole]] `part`, and continue evaluating the
* macro, potentially succeeding despite the warning.
*
* @param part the [[Hole]] part containing the warning
* @param message the warning message to report */
def warn(part: Hole, message: String): Unit =
macroContext.warning(position(part, 0), message)
}
/** Validates the interpolated string, and returns a sequence of contexts for each hole in the
* string.
*
* Each element of the sequence corresponds to a hole in the interpolated string, and
* determines how substitutions should be interpreted for that hole. Typically, the
* [[Context]] for a particular hole will be calculated by parsing the [[Literal]] part(s) of
* the interpolated string before the hole.
*
* For example, when interpolating the JSON interpolated string
* `json"""{ "key": \$value }"""`, parsing the literal `"""{ "key": """` should reveal that
* the hole has a "context" where any other JSON value could be substituted. For other
* interpolated strings, such as `json"""{ "id": "id-\$str" }"""`, parsing the first literal
* would determine that the first hole (where `str` is inserted) is suitable for
* substituting any string-like value, but not, say, a JSON object or array.
*
* These different contexts are represented by objects which subtype [[Context]], a sequence
* of which should be returned from this method.
*
* @param interpolation the context of the interpolated string
* @return the sequence of [[Context]]s corresponding to the holes
*/
def contextualize(interpolation: StaticInterpolation): Seq[ContextType]
/** The macro evaluator that defines what code will be generated for this [[Interpolator]].
* The default implementation constructs a new [[RuntimeInterpolation]] object, and invokes
* a user-defined method called `evaluate` on the [[Interpolator]].
*
* Note that the `evaluate` method is not part of the explicit [[Interpolator]] interface,
* and can be defined with type parameters or implicit parameters, as desired. It must only
* conform to a shape such that it may be invoked (in macro-generated code) with
*
*
* interpolator.evaluate(interpolation)
*
*
* @param contexts the sequence of contexts corresponding to each hole in the interpolated
* string, as the result of the compile-time invocation of [[contextualize]].
* @param interpolation the static context in which evaluation is done
*/
def evaluator(contexts: Seq[ContextType], interpolation: StaticInterpolation):
interpolation.macroContext.Tree = {
import interpolation.macroContext.universe._
val substitutions = contexts.zip(interpolation.holeTrees).zipWithIndex.map {
case ((ctx, Apply(Apply(_, List(value)), List(embedder))), idx) =>
val cls = ctx.getClass
val init :+ last = cls.getName.dropRight(1).split("\\.").to[Vector]
val elements = init ++ last.split("\\$").to[Vector]
val selector = elements.foldLeft(q"_root_": Tree) { case (t, p) =>
Select(t, TermName(p))
}
q"""${interpolation.interpolatorTerm}.Substitution(
$idx,
$embedder($selector).apply($value)
)"""
}
q"""${interpolation.interpolatorTerm}.evaluate(
new ${interpolation.interpolatorTerm}.RuntimeInterpolation(
_root_.scala.collection.Seq(..${interpolation.literals}),
_root_.scala.collection.Seq(..$substitutions)
)
)"""
}
/** Factory for creating [[Embedder]]s.
*
* @tparam Value the type for which this [[Embedding]] will create [[Embedder]]s for */
class Embedding[Value, Input] private[Interpolator]() {
/** Factory method for creating [[Embedder]]s for embedding values of type `Value` for
* an [[Interpolator]] of type `InterpolatorType`, typically inferring the type parameters.
*
* @param cases the functions for converting the `Value` type to `Input` for each supported
* [[Context]]
* @tparam ContextPair the intersection of "before" and "after" contexts, inferred from the
* least upper-bound of the [[Case]]s
* @tparam Input the common input type for this [[Interpolator]]
* @return a new [[Embedder]] which handles the type `Value` for a number of [[Context]]s
*/
def apply[ContextPair <: (Context, Context)]
(cases: Case[ContextPair, Value, Input]*):
Embedder[ContextPair, Value, Input, interpolator.type] =
new Embedder(cases)
}
/** Intermediate factory method for making new [[Embedder]] typeclasses, via the
* [[Embedding]] class, which only exists as a half-way house for inferring most type
* parameters, while having the type `Value` specified explicitly.
*
* @tparam Value
* */
def embed[Value]: Embedding[Value, Input] = new Embedding()
/** The common supertype of runtime and compile-time (static) parts of an interpolated
* string, namely [[Literal]]s (common to both runtime and compile-time contexts),
* [[Hole]]s (compile-time only) and [[Substitution]]s (runtime only). */
sealed trait Part extends Product with Serializable
/** Sealed trait of parts that are known at compile-time. This is only [[Literal]] and
* [[Hole]] values. Note that [[Literal]]s are also available at runtime. */
sealed trait StaticPart extends Part with Product with Serializable { def index: Int }
/** Sealed trait of parts that are known at runtime. This is only [[Literal]] and
* [[Substitution]] values. Note that [[Literal]]s are also available at compile-time. */
sealed trait RuntimePart extends Part with Product with Serializable { def index: Int }
/** A [[Hole]] represents all that is known at compile-time about a substitution into an
* interpolated string. */
case class Hole(index: Int, input: Map[ContextType, ContextType]) extends StaticPart {
/** A string representation of a hole, indicating its possible contexts */
override def toString: String = input.keys.mkString("[", "|", "]")
/** Gets the post-substitution [[Context]], provided the pre-substitution [[Context]] is
* defined for this [[Hole]].
*
* @param context the pre-substitution [[Context]]
* @return the post-substitution [[Context]], or `None` if it is not defined */
def apply(context: ContextType): Option[ContextType] = input.get(context)
}
/** Represents a known value (at runtime) that is substituted into an interpolated string.
*
* @param index the integer index of this substitution within the interpolated string
* @param value the substituted value, converted to the common input type */
case class Substitution(index: Int, value: Input) extends RuntimePart {
/** Gets the substituted value.
*
* @return the substituted value, converted to the common input type */
def apply(): Input = value
/** The string representation of the substituted value */
override def toString = value.toString
}
/** Represents a fixed, constant part of an interpolated string, known at compile-time.
*
* @param index the integer index of this literal within the interpolated string
* @param string the actual string literal */
case class Literal(index: Int, string: String) extends StaticPart with RuntimePart {
/** The string literal */
override def toString: String = string
}
}
/** Factory object for creating [[Case]]s. */
object Case {
/** Creates a new [[Case]] for instances of type `Value`, specifying the `context`
* in which that type may be substituted, and `after` context.
*
* For example, the case defined by,
*
*
* Case(Param, AfterParam) { (p: Int) => s"'\$p'" }
*
*
* handles substitutions at a hole with a hypothetical `Param` [[Context]] of integer
* values, converting them to a string by wrapping them in single-quotes, and specifying the
* hypothetical `AfterParam` [[Context]] for the continuation of parsing, following the
* substitution.
*
* For any [[Interpolator]] which parses the interpolated string, allows substitutions, and
* uses multiple [[Context]]s, the `after` value is important in determining how making the
* substitution changes the parse context. For example, considering the hole in,
*
*
* json"""{ "key": \$value }"""
*
*
* prior to the substitution of `value`, the [[Context]] would be a value representing a
* position where any JSON value may be substituted, but following the substitution, i.e.
* after the JSON value has been substituted, the parsing context has changed, as we would
* only permit a comma (followed by more key/value pairs), or a closing brace; nothing else
* is acceptable. We would therefore specify an `after` [[Context]] which represents this
* state.
*
* @param context the [[Context]] before the substitution
* @param after the [[Context]] after the substitution
* @param conversion the conversion function to the [[Interpolator]]'s common input type
* @tparam Before the type (typically inferred) of the [[Context]] before the transition
* @tparam After the type (typically inferred) of the [[Context]] after the transition
* @tparam Value the type for which this [[Case]] handles conversion
* @tparam Input the common input type to which all substitutions are converted, often (but
* not always) `String`
* @return a new [[Case]] for handling embedding a `Value` in the specified [[Context]] */
def apply[Before <: Context, After <: Context, Value, Input](context: Before, after: After)
(conversion: Value => Input): Case[(Before, After), Value, Input] =
new Case(context, after, conversion)
}
/** A [[Case]] specifies for a particular [[Context]] how a value of type `Value` should
* be converted into the appropriate `Input` type to an [[Interpolator]], and how the
* application of the value should change the [[Context]] in the interpolated string.
*
* @param context the [[Context]] before the substitution
* @param after the [[Context]] after the substitution is made
* @tparam ContextPair the "before" and "after" contexts, represented as a pair
* @tparam Value the type for which this [[Case]] handles conversion
* @tparam Input the common input type to which all substitutions are converted for this
* [[Interpolator]], often (but not always) `String`
* */
class Case[-ContextPair <: (Context, Context), -Value, +Input] private[contextual]
(val context: Context, val after: Context, val conversion: Value => Input)
/** An [[Embedder]] defines, for an [[Interpolator]], `InterpolatorType`, a type `Value` should
* be converted to the common input type `Input`, when substituted into different context
* positions. These should be created using the [[Interpolator#embed]] method.
*
* @param cases the functions for converting the `Value` type to `Input` for each supported
* [[Context]]
* @tparam ContextPair the intersection of "before" and "after" contexts, inferred from the
* least upper-bound of the [[Case]]s
* @tparam Value the type this [[Embedder]] is defining substitution functions for
* @tparam Input the common input type to which all substitutions are converted for this
* [[Interpolator]]
* @tparam InterpolatorType the [[Interpolator]] singleton type for which this [[Embedder]] is
* defining substitution functions */
class Embedder[ContextPair <: (Context, Context), Value, Input,
InterpolatorType <: Interpolator](val cases: Seq[Case[ContextPair, Value, Input]]) {
/** Retrieve the conversion function from the `Value` type to the `Input` type for the
* specified [[Context]].
*
* @param holeContext the context of the hole for which to retrieve the conversion function
* @tparam HoleContext the singleton type corresponding to the context of the hole
* @return the function for converting the `Value` type to an `Input` for the specified
* Context
*/
def apply[HoleContext](holeContext: HoleContext)
(implicit evidence: ContextPair <:< (HoleContext, Context)): Value => Input =
cases.find(_.context == holeContext).get.conversion
}
/** Companion object for the [[Interpolator]], containing related definitions. */
object Interpolator {
/** The [[embed]] implicit method which automatically converts acceptable types to the
* [[Embedded]] type, binding them with their corresponding [[Embedder]], which defines how
* that type should be converted to a common input type in different contexts.
*
* @param value the value of type `Value` being embedded
* @param embedder the implicit [[Embedder]] for values of type `Value`, whose existence (or
* not) determines whether embedding of the `Value` type is possible
* @tparam ContextPair the inferred intersection of "before" and "after" contexts for the
* embedding, inferred by the implicit [[Embedder]] instance
* @tparam Value the type of the value being embedded, inferred from the type being
* substituted into the interpolated string
* @tparam Input the common type to which all substitutions are converted for this
* [[Interpolator]]
* @tparam InterpolatorType the singleton type of the [[Interpolator]], inferred from the
* expected type of the [[Embedded]] parameter
* @return an [[Embedded]] instance, matching the required type of the holes in the
* interpolated string
*/
implicit def embed[ContextPair <: (Context, Context), Value, Input,
InterpolatorType <: Interpolator](value: Value)
(implicit embedder: Embedder[ContextPair, Value, Input, InterpolatorType]):
Embedded[Input, InterpolatorType] =
new Embedded[Input, InterpolatorType] {
def apply(context: Context): Input = embedder(context).apply(value)
}
/** A value which has been embedded as a substitution into an interpolated string, using the
* implicit [[embed]] method.
*
* @tparam Input the common input type for all substitutions for this [[Interpolator]]
* @tparam InterpolatorType the type of [[Interpolator]] for which this value is embedded */
abstract class Embedded[Input, InterpolatorType <: Interpolator] private[contextual] {
/** Apply the conversion function for the specified `context`
*
* @param context the [[Context]] to lookup for this embedding
* @return the common `Input` type for this interpolator */
def apply(context: Context): Input
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy