// , : case class MyClass(field: String) // spray.json: import spray.json._ import DefaultJsonProtocol._ implicit val myClassFormat = jsonFormat1(MyClass) val json = """{ "field\": "value" }""" val obj = json.parseJson.convertTo[MyClass] // ok
abstract sealed class Message() case class SimpleMessage() extends Message case class FieldMessage(field: String) extends Message case class NestedMessage(nested: Message) extends Message case class MultiMessage(field: Int, nested: Message) extends Message
type
field to a JSON object, and when deserializing, dispatch over this field. The idea can be expressed by the following pseudocode: json.type match { case "SimpleMessage" => SimpleMessage() case "FieldMessage" => FieldMessage(json.field) // ... }
RootJsonFormat
. It sounds quite like what we need. The core of our formatter should look like this: val typeName = ... typeName match { case "FieldMessage" => map.getFields("field") match { case Seq(field) => new FieldMessage(field.convertTo[String]) } case "NestedMessage" => map.getFields("nested") match { case Seq(nested) => new NestedMessage(nested.convertTo[Message]) } case "MultiMessage" => map.getFields("field", "nested") match { case Seq(field, nested) => new MultiMessage(field.convertTo[Int], nested.convertTo[Message]) } case "SimpleMessage" => map.getFields() match { case Seq() => new SimpleMessage() } }
Message
type as a starting point.project/Build.sbt
. In the code accompanying the article, these preparations have already been made, here are links to the resulting files:project/Build.sbt
file project/Build.sbt
;macro/build.sbt
;main/build.sbt
.-Ymacro-debug-lite
compiler option helps you to display the results of deploying all macros in a project to the console (these results are very similar to Scala code, and can often be compiled manually without any changes when passed to the compiler, which can help debugging non-trivial cases).Context
class. An instance of this class is always passed to the macro when expanded. Then you can import the internals of the Universe
object from it and use them in the same way as in runtime reflection — request from there descriptors of types, methods, properties, etc. The same context allows you to create syntax trees using classes like Literal
, Constant
, List
, etc. import scala.language.experimental.macros import scala.reflect.macros.blackbox.Context import spray.json._ object Parsers { def impl[T: c.WeakTypeTag](c: Context)(typeName: c.Expr[String], map: c.Expr[JsObject]): c.Expr[T] = { import c.universe._ val cls = weakTypeOf[T].typeSymbol.asClass val tree = ??? // c.Expr[T](tree) } def parseMessage[T](typeName: String, map: JsObject): T = macro Parsers.impl[T] }
parseMessage[T]
macro takes type T
, which is the base for the hierarchy of classes being deserialized, and a syntax tree to get the type of the map
deserialized, and returns a syntax tree to get the deserialized object converted to the base type T
T
described in a special way: it is indicated that the compiler must attach an implicitly generated object of type c.WeakTypeTag
. Generally speaking, the implicit TypeTag
argument TypeTag
used in Scala to work with generic argument types that are usually not available at runtime due to type erasure . For macro arguments, the compiler requires using not just TypeTag
, but WeakTypeTag
, which, as far as I understand, is related to the features of the compiler's work (it does not have a “full-fledged” TypeTag
for a type that may not yet be fully generated during macro expansion). The type associated with a TypeTag
can be obtained using the typeOf[T]
method of the Universe
object; accordingly, for WeakTypeTag
there is a weakTypeOf[T]
method.2 + 2
should look like Apply(Select(Literal(Constant(2)), TermName("$plus")), List(Literal(Constant(2))))
when generated; even more serious cases begin when we need to present larger pieces of code with pattern substitution. Naturally, we do not like this complexity and we will overcome it.2 + 2
, in the form of quasiquotes will look just like q"2 + 2"
, which is very convenient. In general, quasiquotes in Scala are a set of string interpolators that are located in the Universe
object. After importing these interpolators in the current scope, it is possible to use a series of characters before the string constant, which determine its processing by the compiler. In particular, the interpolators pq
for patterns, cq
for branches of the match
expression, and q
for complete expressions of the language will be useful to us when implementing the problem in question.2 + 2
you can use the following code: val a = 2 q"$a + $a"
TermName
object.match
typeName
variable with case
branches corresponding to each type of hierarchy;map.getFields
method;match
expression) on variables and passing these variables to the type constructor.match
expression. To do this, you have to use interpolation of variables in the context of quasiquotes: val clauses: Set[Tree] = ??? // . val tree = q"$typeName match { case ..$clauses }"
case ..$clauses
expression case ..$clauses
inside the match
block will be expanded as a list of case
branches. As we remember, each branch should look like this: case "FieldMessage" => map.getFields("field") match { case Seq(field) => new FieldMessage(field.convertTo[String]) }
val tpe: Type // val constructorParameters: List[Symbol] // val parameterNames = constructorParameters.map(_.name) val parameterNameStrings = parameterNames.map(_.toString) // pq: val parameterBindings = parameterNames.map(name => pq"$name") // , : val args = constructorParameters.map { param => val parameterName = TermName(param.name.toString) val parameterType = param.typeSignature q"$parameterName.convertTo[$parameterType]" } // case: val typeName = tpe.typeSymbol val typeNameString = typeName.name.toString cq"""$typeNameString => $map.getFields(..$parameterNameStrings) match { case Seq(..$parameterBindings) => new $typeName(..$args) }"""
pq"$name"
expression creates a set of patterns, which are subsequently substituted into the Seq(...)
expression. Each of these expressions is of type JsValue
, which must be converted to the appropriate type before passing to the constructor; for this, a quasiquote is used that generates a call to the convertTo
method. Note that this method can recursively call our formatter if necessary (that is, you can nest Message
objects into each other.match
expression with the case
branches generated by us, can also be constructed using interpolation: val tree = q"$typeName match { case ..$clauses }"
Source: https://habr.com/ru/post/224229/
All Articles