All Downloads are FREE. Search and download functionalities are using the official Maven repository.

io.circe.literal.JsonLiteralMacros.scala Maven / Gradle / Ivy

The newest version!
/*
 * Copyright 2024 circe
 *
 * 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 io.circe.literal

import java.io.{ PrintWriter, StringWriter }
import java.lang.reflect.{ InvocationHandler, Method, Proxy }
import java.util.UUID
import scala.Predef.classOf
import scala.reflect.macros.blackbox
import scala.util.control.NonFatal

class JsonLiteralMacros(val c: blackbox.Context) {
  import c.universe._

  /**
   * Represents an interpolated expression that we've replaced with a unique string during parsing.
   */
  private[this] class Replacement(val placeholder: String, argument: Tree) {
    private[this] val argumentType = c.typecheck(argument).tpe

    def asJson: Tree = q"_root_.io.circe.Encoder[${argumentType.widen}].apply($argument)"
    def asKey: Tree = q"_root_.io.circe.KeyEncoder[${argumentType.widen}].apply($argument)"
  }

  private[this] object Replacement {
    private[this] final def generatePlaceholder(): String = UUID.randomUUID().toString

    def apply(stringParts: Seq[String], argument: Tree): Replacement = {

      /**
       * Generate a unique string that doesn't appear in the JSON literal.
       */
      val placeholder =
        Stream.continually(generatePlaceholder()).distinct.dropWhile(s => stringParts.exists(_.contains(s))).head

      new Replacement(placeholder, argument)
    }
  }

  private[this] abstract class Handler(replacements: Seq[Replacement]) extends InvocationHandler {
    protected[this] def invokeWithoutArgs: String => Object
    protected[this] def invokeWithArgs: (String, Array[Class[_]], Array[Object]) => Object

    final def invoke(proxy: Object, method: Method, args: Array[Object]): Object =
      if (args.eq(null)) {
        invokeWithoutArgs(method.getName)
      } else {
        invokeWithArgs(method.getName, method.getParameterTypes, args)
      }
    protected[this] final def toJsonKey(s: String): Tree =
      replacements.find(_.placeholder == s).fold(q"$s")(_.asKey)

    protected[this] final def toJsonString(s: CharSequence): Tree =
      replacements.find(_.placeholder == s).fold(q"_root_.io.circe.Json.fromString(${s.toString})")(_.asJson)

    final def asProxy(cls: Class[_]): Object = Proxy.newProxyInstance(getClass.getClassLoader, Array(cls), this)
  }

  private[this] class SingleContextHandler(replacements: Seq[Replacement]) extends Handler(replacements) {
    private[this] var value: Tree = null

    protected[this] val invokeWithoutArgs: String => Object = {
      case "finish" => value
      case "isObj"  => java.lang.Boolean.FALSE
    }

    protected[this] val invokeWithArgs: (String, Array[Class[_]], Array[Object]) => Object = {
      case ("finish", _, _) => value
      case ("isObj", _, _)  => java.lang.Boolean.FALSE
      case ("add", Array(cls), Array(arg: CharSequence)) if cls == classOf[CharSequence] =>
        value = toJsonString(arg.toString)
        null
      case ("add", Array(cls, _), Array(arg: CharSequence, _)) if cls == classOf[CharSequence] =>
        value = toJsonString(arg.toString)
        null
      case ("add", Array(cls, _, _), Array(arg: CharSequence, _, _)) if cls == classOf[CharSequence] =>
        value = toJsonString(arg.toString)
        null
      case ("add", Array(_), Array(arg: Tree)) =>
        value = arg
        null
      case ("add", Array(_, _), Array(arg: Tree, _)) =>
        value = arg
        null
    }
  }

  private[this] class ArrayContextHandler(replacements: Seq[Replacement]) extends Handler(replacements) {
    private[this] var values: List[Tree] = Nil

    protected[this] val invokeWithoutArgs: String => Object = {
      case "finish" => q"_root_.io.circe.Json.arr(..$values)"
      case "isObj"  => java.lang.Boolean.FALSE
    }

    protected[this] val invokeWithArgs: (String, Array[Class[_]], Array[Object]) => Object = {
      case ("finish", _, _) => q"_root_.io.circe.Json.arr(..$values)"
      case ("isObj", _, _)  => java.lang.Boolean.FALSE
      case ("add", Array(cls), Array(arg: CharSequence)) if cls == classOf[CharSequence] =>
        values = values :+ toJsonString(arg.toString)
        null
      case ("add", Array(cls, _), Array(arg: CharSequence, _)) if cls == classOf[CharSequence] =>
        values = values :+ toJsonString(arg.toString)
        null
      case ("add", Array(cls, _, _), Array(arg: CharSequence, _, _)) if cls == classOf[CharSequence] =>
        values = values :+ toJsonString(arg.toString)
        null
      case ("add", Array(_), Array(arg: Tree)) =>
        values = values :+ arg
        null
      case ("add", Array(_, _), Array(arg: Tree, _)) =>
        values = values :+ arg
        null
    }
  }

  private[this] class ObjectContextHandler(replacements: Seq[Replacement]) extends Handler(replacements) {
    private[this] var key: String = null
    private[this] var fields: List[Tree] = Nil

    protected[this] val invokeWithoutArgs: String => Object = {
      case "finish" => q"_root_.io.circe.Json.obj(..$fields)"
      case "isObj"  => java.lang.Boolean.TRUE
    }

    protected[this] val invokeWithArgs: (String, Array[Class[_]], Array[Object]) => Object = {
      case ("finish", _, _) => q"_root_.io.circe.Json.obj(..$fields)"
      case ("isObj", _, _)  => java.lang.Boolean.TRUE
      case ("add", Array(cls), Array(arg: CharSequence)) if cls == classOf[CharSequence] =>
        if (key.eq(null)) {
          key = arg.toString
        } else {
          fields = fields :+ q"(${toJsonKey(key)}, ${toJsonString(arg)})"
          key = null
        }
        null
      case ("add", Array(cls, _), Array(arg: CharSequence, _)) if cls == classOf[CharSequence] =>
        if (key.eq(null)) {
          key = arg.toString
        } else {
          fields = fields :+ q"(${toJsonKey(key)}, ${toJsonString(arg)})"
          key = null
        }
        null
      case ("add", Array(cls, _, _), Array(arg: CharSequence, _, _)) if cls == classOf[CharSequence] =>
        if (key.eq(null)) {
          key = arg.toString
        } else {
          fields = fields :+ q"(${toJsonKey(key)}, ${toJsonString(arg)})"
          key = null
        }
        null
      case ("add", Array(_), Array(arg: Tree)) =>
        fields = fields :+ q"(${toJsonKey(key)}, $arg)"
        key = null
        null
      case ("add", Array(_, _), Array(arg: Tree, _)) =>
        fields = fields :+ q"(${toJsonKey(key)}, $arg)"
        key = null
        null
    }
  }

  private[this] def jawnFContextClass = Class.forName("org.typelevel.jawn.FContext")
  private[this] def jawnParserClass = Class.forName("org.typelevel.jawn.Parser$")
  private[this] def jawnParser = jawnParserClass.getField("MODULE$").get(jawnParserClass)
  private[this] def jawnFacadeClass = Class.forName("org.typelevel.jawn.Facade")
  private[this] def parseMethod = jawnParserClass.getMethod("parseUnsafe", classOf[String], jawnFacadeClass)

  private[this] class TreeFacadeHandler(replacements: Seq[Replacement]) extends Handler(replacements) {
    protected[this] val invokeWithoutArgs: String => Object = {
      case "jnull"         => q"_root_.io.circe.Json.Null"
      case "jfalse"        => q"_root_.io.circe.Json.False"
      case "jtrue"         => q"_root_.io.circe.Json.True"
      case "singleContext" => new SingleContextHandler(replacements).asProxy(jawnFContextClass)
      case "arrayContext"  => new ArrayContextHandler(replacements).asProxy(jawnFContextClass)
      case "objectContext" => new ObjectContextHandler(replacements).asProxy(jawnFContextClass)
    }

    protected[this] val invokeWithArgs: (String, Array[Class[_]], Array[Object]) => Object = {
      // format: off
      case ("jnull", _, _)         => q"_root_.io.circe.Json.Null"
      case ("jfalse", _, _)        => q"_root_.io.circe.Json.False"
      case ("jtrue", _, _)         => q"_root_.io.circe.Json.True"
      case ("singleContext", _, _) => new SingleContextHandler(replacements).asProxy(jawnFContextClass)
      case ("arrayContext", _, _)  => new ArrayContextHandler(replacements).asProxy(jawnFContextClass)
      case ("objectContext", _, _) => new ObjectContextHandler(replacements).asProxy(jawnFContextClass)
      case ("jstring", Array(cls), Array(arg: CharSequence)) if cls == classOf[CharSequence]       => toJsonString(arg)
      case ("jstring", Array(cls, _), Array(arg: CharSequence, _)) if cls == classOf[CharSequence] => toJsonString(arg)
      // format: on
      case ("jnum", Array(clsS, clsDecIndex, clsExpIndex), Array(s: CharSequence, decIndex, expIndex))
          if clsS == classOf[CharSequence] && clsDecIndex == classOf[Int] && clsExpIndex == classOf[Int] =>
        if (decIndex.asInstanceOf[Int] < 0 && expIndex.asInstanceOf[Int] < 0) {
          q"_root_.io.circe.Json.fromJsonNumber(_root_.io.circe.JsonNumber.fromIntegralStringUnsafe(${s.toString}))"
        } else {
          q"_root_.io.circe.Json.fromJsonNumber(_root_.io.circe.JsonNumber.fromDecimalStringUnsafe(${s.toString}))"
        }
      case ("jnum", Array(clsS, clsDecIndex, clsExpIndex, _), Array(s: CharSequence, decIndex, expIndex, _))
          if clsS == classOf[CharSequence] && clsDecIndex == classOf[Int] && clsExpIndex == classOf[Int] =>
        if (decIndex.asInstanceOf[Int] < 0 && expIndex.asInstanceOf[Int] < 0) {
          q"_root_.io.circe.Json.fromJsonNumber(_root_.io.circe.JsonNumber.fromIntegralStringUnsafe(${s.toString}))"
        } else {
          q"_root_.io.circe.Json.fromJsonNumber(_root_.io.circe.JsonNumber.fromDecimalStringUnsafe(${s.toString}))"
        }
    }
  }

  private[this] final def parse(jsonString: String, replacements: Seq[Replacement]): Either[Throwable, Tree] =
    try
      Right(
        parseMethod
          .invoke(
            jawnParser,
            jsonString,
            new TreeFacadeHandler(replacements).asProxy(jawnFacadeClass)
          )
          .asInstanceOf[Tree]
      )
    catch {
      case NonFatal(e) => Left(e)
    }

  final def jsonStringContext(args: c.Expr[Any]*): Tree = c.prefix.tree match {
    case Apply(_, Apply(_, parts) :: Nil) =>
      val stringParts = parts.map {
        case Literal(Constant(part: String)) => part
        case _ =>
          c.abort(
            c.enclosingPosition,
            "A StringContext part for the json interpolator is not a string"
          )
      }

      val replacements: Seq[Replacement] = args.map(argument => Replacement(stringParts, argument.tree))

      if (stringParts.size != replacements.size + 1)
        c.abort(
          c.enclosingPosition,
          "Invalid arguments for the json interpolator"
        )
      else {
        val jsonString = stringParts.zip(replacements.map(_.placeholder)).foldLeft("") {
          case (acc, (part, placeholder)) =>
            val qm = "\""

            s"$acc$part$qm$placeholder$qm"
        } + stringParts.last

        parse(jsonString, replacements) match {
          case Right(tree) => tree
          case Left(_: ClassNotFoundException) =>
            c.abort(
              c.enclosingPosition,
              "The json interpolator requires jawn to be available at compile time"
            )
          case Left(t: Throwable) =>
            val sw = new StringWriter
            t.printStackTrace(new PrintWriter(sw))

            c.abort(c.enclosingPosition, s"Invalid JSON in interpolated string, ${sw.toString}")
        }
      }
    case _ => c.abort(c.enclosingPosition, "Invalid use of the json interpolator")
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy