olon.json.JsonAST.scala Maven / Gradle / Ivy
The newest version!
package olon
package json
import izumi.reflect.Tag
import java.lang.StringBuilder
/** This object contains the abstract syntax tree (or AST) for working with JSON
* objects in lift-json.
*
* The purpose of the JSON AST is to represent and manipulate JSON by
* leveraging Scala language features like types, case classes, etc. The AST
* should allow you to represent anything you could imagine from JSON land
* using the Scala type system.
*
* Everything in the AST has a single root: JValue. A JValue could, quite
* literally, be anything. It could be an an object (represented by
* `[[JObject]]`), a string (`[[JString]]`), a null (`[[JNull]]`), and so on.
* So, when constructing a JSON object with the AST directly you might
* construct something like the following:
*
* {{{
* JObject(JField("bacon", JBool(true)) :: JField("spinach", JBool(false)))
* }}}
*
* Once serialized to the string representation of JSON you would end up with
* the following:
*
* {{{
* {
* "bacon":true,
* "spinach":false
* }
* }}}
*/
object JsonAST {
/** Concatenate a sequence of `[[JValue]]`s together.
*
* This would be useful in the event that you have a handful of `JValue`
* instances that need to be smacked together into one unit.
*
* For example:
*
* {{{
* concat(JInt(1), JInt(2)) == JArray(List(JInt(1), JInt(2)))
* }}}
*/
def concat(values: JValue[?]*): JValue[?] = { // Scala 2 workaround
values.foldLeft(JNothing: JValue[None.type])(
((_: JValue[None.type]) ++ (_: JValue[None.type]))
.asInstanceOf[(JValue[None.type], JValue[?]) => JValue[None.type]]
)
}
object JValue extends Merge.Mergeable
/** The base type for all things that represent distinct JSON entities in the
* AST.
*
* Most members of the AST will extend this class. The one exception is
* `[[JField]]` which does not extend this class because it really ''can't''
* properly exist as a first-class citizen of JSON.
*/
sealed abstract class JValue[+Values] extends Diff.Diffable {
/** An XPath-like expression to find a child of a `[[JObject]]` or a
* `[[JArray]]` of `JObject` by name. If you call this method on anything
* other than a `JObject` or `JArray` of `JObject`s you'll get a
* `[[JNothing]]`.
*
* This method is most useful if you have an object that you need to dig
* into in order to retrieve a specific value. So, let's say that you had a
* JSON object that looked something like this:
*
* {{{
* {
* "name": "Joe",
* "profession": "Software Engineer",
* "catchphrase": {
* "name": "Alabama Cheer",
* "value": "Roll tide"
* }
* }
* }}}
*
* If for some reason you're interested in taking a look at Joe's
* catchphrase, you can query it using the `\` method to find it like so:
*
* Example:
*
* {{{
* scala> json \ "catchphrase"
* res0: JValue = JObject(List(JField("name", JString("Alabama Cheer")), JField("value", JString("Roll tide"))))
* }}}
*
* Likewise, if you wanted to find Joe's name you could do the following:
*
* {{{
* scala> json \ "name"
* res0: JValue = JString("Joe")
* }}}
*
* The result could be any subclass of `JValue`. In the event that the
* `JValue` you're operating on is actually an array of objects, you'll get
* back a `JArray` of the result of executing `\` on each object in the
* array. In the event nothing is found, you'll get a `JNothing`.
*/
def \(nameToFind: String): JValue[?] = {
// Use :: instead of List() to avoid the extra array allocation for the variable arguments
findDirectByName(this :: Nil, nameToFind) match {
case Nil => JNothing
case x :: Nil => x
case x => JArray(x)
}
}
private def findDirectByName(
xs: List[JValue[?]],
name: String
): List[JValue[?]] =
xs.flatMap {
case JObject(l) =>
l.collect {
case JField(n, value) if n == name => value
}
case JArray(l) => findDirectByName(l, name)
case _ => Nil
}
private def findDirect(
xs: List[JValue[?]],
p: JValue[?] => Boolean
): List[JValue[?]] = xs.flatMap {
case JObject(l) =>
l.collect {
case JField(_, x) if p(x) => x
}
case JArray(l) => findDirect(l, p)
case x if p(x) => x :: Nil
case _ => Nil
}
/** Find all children of a `[[JObject]]` with the matching name, returning
* an empty `JObject` if no matches are found.
*
* For example given this example JSON:
*
* {{{
* {
* "name": "Joe",
* "profession": "Software Engineer",
* "catchphrase": {
* "name": "Alabama Cheer",
* "value": "Roll tide"
* }
* }
* }}}
*
* We might do the following:
*
* {{{
* scala> json \\ "name"
* res2: JValue = JObject(List(JField(name,JString(Joe)), JField(name,JString(Alabama Cheer))))
* }}}
*/
def \\(nameToFind: String): JObject = {
def find(json: JValue[?]): List[JField] = json match {
case JObject(fields) =>
fields.foldLeft(List[JField]()) {
case (matchingFields, JField(name, value)) =>
matchingFields :::
List(JField(name, value)).filter(_.name == nameToFind) :::
find(value)
}
case JArray(fields) =>
fields.foldLeft(List[JField]()) { (matchingFields, children) =>
matchingFields ::: find(children)
}
case _ =>
Nil
}
JObject(find(this))
}
/** Find immediate children of this `[[JValue]]` that match a specific
* `JValue` subclass.
*
* This methid will search a `[[JObject]]` or `[[JArray]]` for values of a
* specific type and return a `List` of those values if they any are found.
*
* So given some JSON like so:
*
* {{{
* [
* {
* "thinga":1,
* "thingb":"bacon"
* },{
* "thingc":3,
* "thingd":"Wakka"
* },{
* "thinge":{
* "thingf":4
* },
* "thingg":true
* }
* ]
* }}}
*
* You would use this method like so:
*
* {{{
* scala> json \ classOf[JInt]
* res0: List[olon.json.JInt#Values] = List(1, 3)
* }}}
*
* This method does require that whatever type you're searching for is
* subtype of `JValue`.
*/
def \[B, A <: JValue[B]](clazz: Class[A]): List[B] =
findDirect(children, typePredicate(clazz)).asInstanceOf[List[A]] map {
_.values
}
/** Find all descendants of this `JValue` that match a specific `JValue`
* subclass.
*
* Unlike its cousin `\`, this method will recurse down into all children
* looking for type matches searching a `[[JObject]]` or `[[JArray]]` for
* values of a specific type and return a `List` of those values if they
* are found.
*
* So given some JSON like so:
*
* {{{
* [
* {
* "thinga":1,
* "thingb":"bacon"
* },{
* "thingc":3,
* "thingd":"Wakka"
* },{
* "thinge":{
* "thingf":4
* },
* "thingg":true
* }
* ]
* }}}
*
* You would use this method like so:
*
* {{{
* scala> json \\ classOf[JInt]
* res0: List[olon.json.JInt#Values] = List(1, 3, 4)
* }}}
*/
def \\[B, A <: JValue[B]](clazz: Class[A]): List[B] =
(this.filter(typePredicate(clazz))).asInstanceOf[List[A]] map {
_.values
}
private def typePredicate[A <: JValue[?]](
clazz: Class[A]
)(json: JValue[?]) =
json match {
case x if x.getClass == clazz => true
case _ => false
}
/** Return the element in the `i`-th position from a `[[JArray]]`. Will
* return `JNothing` when invoked on any other kind of `JValue`.
*
* For example:
*
* {{{
* scala> val array = JArray(JInt(1) :: JInt(2) :: Nil)
* array: olon.json.JsonAST.JArray = JArray(List(JInt(1), JInt(2)))
*
* scala> array(1)
* res0: olon.json.JsonAST.JValue = JInt(2)
* }}}
*/
def apply(i: Int): JValue[?] = JNothing
/** Return a representation of the values in this `[[JValue]]` in a native
* Scala structure.
*
* For example, you might invoke this on a `[[JObject]]` to have its fields
* returned as a `Map`.
*
* {{{
* scala> JObject(JField("name", JString("joe")) :: Nil).values
* res0: scala.collection.immutable.Map[String,Any] = Map(name -> joe)
* }}}
*/
def values: Values
/** Return direct child elements of this `JValue`, if this `JValue` is a
* `[[JObject]]` or `[[JArray]]`.
*
* This method is useful for getting all the values of a `JObject` or
* `JArray` and will return them as a `List[JValue]`. If the `JValue` you
* invoke this method on is not a `JObject` or `JArray` you will instead
* get `Nil`.
*
* Example:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: Nil).children
* List(JInt(1), JInt(2))
* }}}
*
* @return
* Direct children of this `JValue` if it is a `[[JObject]]` or
* `[[JArray]]`, or `[[JNothing]]` otherwise.
*/
def children: List[JValue[?]] = {
if (this.isInstanceOf[JObject])
this.asInstanceOf[JObject].obj map (_.value)
else if (this.isInstanceOf[JArray]) this.asInstanceOf[JArray].arr
else Nil
// this match {
// case JObject(l) => l map (_.value)
// case JArray(l) => l
// case _ => Nil
}
/** Fold over `JValue`s by applying a function to each element.
*
* @param f
* The function to apply, which takes an accumulator and the next item as
* paramaters.
* @param z
* The initial value for the fold.
*/
def fold[A](z: A)(f: (A, JValue[?]) => A): A = {
def rec(acc: A, v: JValue[?]) = {
val newAcc = f(acc, v)
v match {
case JObject(l) =>
l.foldLeft(newAcc) { case (a, JField(_, value)) =>
value.fold(a)(f)
}
case JArray(l) =>
l.foldLeft(newAcc) { (a, e) =>
e.fold(a)(f)
}
case _ => newAcc
}
}
rec(z, this)
}
/** Fold over a series of `JField`s applying a function to each one.
*
* @param z
* The initial value for the fold.
* @param f
* The function to apply, which takes an accumulator as its first
* parameter and the next field as its second.
*/
def foldField[A](z: A)(f: (A, JField) => A): A = {
def rec(acc: A, v: JValue[?]) = {
v match {
case JObject(l) =>
l.foldLeft(acc) { case (a, field @ JField(_, value)) =>
value.foldField(f(a, field))(f)
}
case JArray(l) => l.foldLeft(acc)((a, e) => e.foldField(a)(f))
case _ => acc
}
}
rec(z, this)
}
/** Return a new `JValue` resulting from applying the given function to each
* value, recursively.
*
* If this function is invoked on a `[[JObject]]`, it will iterate over the
* field values of that `JObject`. If this function is invoked on a
* `[[JArray]]`, it will iterate over the values of that `JArray`. If this
* function is invoked on any other kind of `JValue` it will simply pass
* that instance into the function you have provided.
*
* Example:
*
* {{{
* JArray(JInt(1) :: JInt(2) :: Nil) map {
* case JInt(x) => JInt(x+1)
* case x => x
* }
* }}}
*/
def map(f: JValue[?] => JValue[?]): JValue[?] = {
def rec(v: JValue[?]): JValue[?] = v match {
case JObject(l) =>
f(JObject(l.map { field => field.copy(value = rec(field.value)) }))
case JArray(l) => f(JArray(l.map(rec)))
case x => f(x)
}
rec(this)
}
/** Return a new `JValue` resulting from applying the given function to each
* `[[JField]]` in a `[[JObject]]` or a `[[JArray]]` of `JObject`,
* recursively.
*
* Example:
*
* {{{
* JObject(("age", JInt(10)) :: Nil) map {
* case ("age", JInt(x)) => ("age", JInt(x+1))
* case x => x
* }
* }}}
*
* @see
* transformField
*/
def mapField(f: JField => JField): JValue[?] = {
def rec(v: JValue[?]): JValue[?] = v match {
case JObject(l) =>
JObject(l.map { field => f(field.copy(value = rec(field.value))) })
case JArray(l) => JArray(l.map(rec))
case x => x
}
rec(this)
}
/** Return a new `JValue` resulting from applying the given partial function
* `f`` to each field in JSON.
*
* Example:
* {{{
* JObject(("age", JInt(10)) :: Nil) transformField {
* case ("age", JInt(x)) => ("age", JInt(x+1))
* }
* }}}
*/
def transformField(f: PartialFunction[JField, JField]): JValue[?] =
mapField { x =>
if (f.isDefinedAt(x)) f(x) else x
}
/** Return a new `JValue` resulting from applying the given partial function
* to each value within this `JValue`.
*
* If this is a `JArray`, this means we will transform each value in the
* array and return an updated array.
*
* If this is a `JObject`, this means we will transform the value of each
* field of the object and the object in turn and return an updated object.
*
* If this is another type of `JValue`, the value is transformed directly.
*
* Note that this happens recursively, so you will receive both each value
* in an array ''and'' the array itself, or each field value in an object
* ''and'' the object itself. If an array contains arrays, we will recurse
* into them in turn.
*
* Examples:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: Nil) transform {
* case JInt(x) =>
* JInt(x+1)
* }
* res0: olon.json.JsonAST.JValue = JArray(List(JInt(2), JInt(3)))
* }}}
*
* Without type matching, notice that we get the result of the transform
* replacing the array:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: Nil) transform {
* case _ =>
* JString("hello")
* }
* res0: olon.json.JsonAST.JValue = JString("hello")
* }}}
*
* @return
* This `JValue` with its child values recursively transformed by the
* given `PartialFunction`, when defined. If the `PartialFunction` is
* undefined, leaves the child values untouched.
*/
def transform(f: PartialFunction[JValue[?], JValue[?]]): JValue[?] = map {
x =>
if (f.isDefinedAt(x)) f(x) else x
}
/** Return a new `JValue` resulting from replacing the value at the
* specified field path with the replacement value provided. This has no
* effect if the path is empty or if the value is not a `[[JObject]]`
* instance.
*
* Example:
*
* {{{
* > JObject(List(JField("foo", JObject(List(JField("bar", JInt(1))))))).replace("foo" :: "bar" :: Nil, JString("baz"))
* JObject(List(JField("foo", JObject(List(JField("bar", JString("baz")))))))
* }}}
*/
def replace(l: List[String], replacement: JValue[?]): JValue[?] = {
def rep(l: List[String], in: JValue[?]): JValue[?] = {
l match {
case x :: xs =>
in match {
case JObject(fields) =>
JObject(
fields.map {
case JField(`x`, value) =>
JField(x, if (xs == Nil) replacement else rep(xs, value))
case field => field
}
)
case other => other
}
case Nil => in
}
}
rep(l, this)
}
/** Return the first field from this `JValue` which matches the given
* predicate.
*
* When invoked on a `[[JObject]]` it will first attempt to see if the
* `JObject` has the field defined on it. Not finding the field defined,
* this method will recurse into the fields of that object and search for
* the value there. When invoked on or encountering a `[[JArray]]` during
* recursion this method will run its search on each member of the
* `JArray`.
*
* Example:
*
* {{{
* > JObject(JField("age", JInt(2))) findField {
* case JField(n, v) =>
* n == "age"
* }
* res0: Option[olon.json.JsonAST.JField] = Some(JField(age,JInt(2))
* }}}
*/
def findField(p: JField => Boolean): Option[JField] = {
def find(json: JValue[?]): Option[JField] = json match {
case JObject(fs) if (fs.find(p)).isDefined => return fs.find(p)
case JObject(fs) =>
fs.flatMap { case JField(_, v) => find(v) }.headOption
case JArray(l) => l.flatMap(find).headOption
case _ => None
}
find(this)
}
/** Return the first element from a `JValue` which matches the given
* predicate.
*
* Example:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: Nil) find { _ == JInt(2) }
* res0: Option[olon.json.JsonAST.JValue] = Some(JInt(2))
* }}}
*/
def find(p: JValue[?] => Boolean): Option[JValue[?]] = {
def find(json: JValue[?]): Option[JValue[?]] = {
json match {
case _ if p(json) => Some(json)
case JObject(fs) =>
fs.flatMap { case JField(_, v) => find(v) }.headOption
case JArray(l) => l.flatMap(find).headOption
case _ => None
}
}
find(this)
}
/** Return a `List` of all fields that match the given predicate. Does not
* recurse into child elements, so this will only check a `JObject`'s field
* values.
*
* Example:
*
* {{{
* > JObject(JField("age", JInt(10))) filterField {
* case JField("age", JInt(x)) if x > 18 =>
* true
*
* case _ =>
* false
* }
* res0: List[olon.json.JsonAST.JField] = List()
* > JObject(JField("age", JInt(10))) filterField {
* case JField("age", JInt(x)) if x < 18 =>
* true
*
* case _ =>
* false
* }
* res1: List[olon.json.JsonAST.JField] = List(JField(age,JInt(10)))
* }}}
*
* @return
* A `List` of `JField`s that match the given predicate `p`, or `Nil` if
* this `JValue` is not a `JObject`.
*/
def filterField(p: JField => Boolean): List[JField] =
foldField(List[JField]())((acc, e) => if (p(e)) e :: acc else acc).reverse
/** Return a List of all values which matches the given predicate,
* recursively.
*
* Example:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: Nil) filter {
* case JInt(x) => x > 1
* case _ => false
* }
* res0: List[olon.json.JsonAST.JValue] = List(JInt(2))
* }}}
*
* This operates recursively, so nested objects work too:
* {{{
* > ((("boom" -> ("slam" -> "hello")) ~ ("shap" -> 3)): JObject) filter {
* case JString("hello") => true
* case _ => false
* }
* res0: List[olon.json.JsonAST.JValue] = List(JString(hello))
* }}}
*/
def filter(p: JValue[?] => Boolean): List[JValue[?]] =
fold(List[JValue[?]]())((acc, e) => if (p(e)) e :: acc else acc).reverse
/** Create a new instance of `[[WithFilter]]` for Scala to use when using
* this `JValue` in a for comprehension.
*/
def withFilter(p: JValue[?] => Boolean) = new WithFilter(this, p)
final class WithFilter(self: JValue[?], p: JValue[?] => Boolean) {
def map[A](f: JValue[?] => A): List[A] = self.filter(p).map(f)
def flatMap[A](f: JValue[?] => List[A]) = self.filter(p).flatMap(f)
def withFilter(q: JValue[?] => Boolean): WithFilter =
new WithFilter(self, x => p(x) && q(x))
def foreach[U](f: JValue[?] => U): Unit = self.filter(p).foreach(f)
}
/** Concatenate this `JValue` with another `JValue`.
*
* Example:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: Nil) ++ JArray(JInt(3) :: Nil)
* res0: JArray(List(JInt(1), JInt(2), JInt(3)))
* }}}
*/
def ++[T](other: JValue[T]) = {
def append(value1: JValue[?], value2: JValue[?]): JValue[?] =
(value1, value2) match {
case (JNothing, x) => x
case (x, JNothing) => x
case (JArray(xs), JArray(ys)) => JArray(xs ::: ys)
case (JArray(xs), v: JValue[?]) => JArray(xs ::: List(v))
case (v: JValue[?], JArray(xs)) => JArray(v :: xs)
case (x, y) => JArray(x :: y :: Nil)
}
append(this, other)
}
/** Return a `JValue` where all fields matching the given predicate are
* removed.
*
* Example:
*
* {{{
* > JObject(JField("age", JInt(10))) removeField {
* case JField("age", _) => true
* case _ => false
* }
* }}}
*/
def removeField(p: JField => Boolean): JValue[?] = this mapField {
case x if p(x) => JField(x.name, JNothing)
case x => x
}
/** Return a JSON where all values matching the given predicate are removed.
*
* Example:
*
* {{{
* > JArray(JInt(1) :: JInt(2) :: JNull :: Nil).remove(_ == JNull)
* res0: olon.json.JsonAST.JValue = JArray(List(JInt(1), JInt(2), JNothing))
* }}}
*/
def remove(p: JValue[?] => Boolean): JValue[?] = this map {
case x if p(x) => JNothing
case x => x
}
/** Extract a value into a concrete Scala instance from its `JValue`
* representation.
*
* Value can be:
* - a case class
* - a primitive (String, Boolean, Date, etc.)>
* - any type which has a configured [[TypeHints custom deserializer]]
* - a supported collection type of any of the above (List, Seq,
* Map[String, _], Set)
*
* Example:
*
* {{{
* > case class Person(name: String)
* > JObject(JField("name", JString("joe")) :: Nil).extract[Person]
* res0: Person("joe")
* }}}
*/
// SCALA3 Using `ClassTag` instead of `Manifest`
def extract[A](implicit
formats: Formats,
mf: Tag[A]
): A =
Extraction.extract(this)(formats, mf)
/** Optionally extract a value into a concrete Scala instance from its
* `JValue` representation.
*
* This method will attempt to extract a concrete Scala instance of type
* `A`, but if it fails it will return a `[[scala.None]]` instead of
* throwing an exception as `[[extract]]` would.
*
* Value can be:
* - a case class
* - a primitive (String, Boolean, Date, etc.)>
* - any type which has a configured [[TypeHints custom deserializer]]
* - a supported collection type of any of the above (List, Seq,
* Map[String, _], Set)
*
* Example:
*
* {{{
* scala> case class Person(name: String)
* defined class Person
*
* scala> implicit val formats = DefaultFormats
* formats: olon.json.DefaultFormats.type = olon.json.DefaultFormats$@39afbb7c
*
* scala> JObject(JField("name", JString("joe")) :: Nil).extractOpt[Person]
* res1: Option[Person] = Some(Person(joe))
* }}}
*/
// SCALA3 Using `ClassTag` instead of `Manifest`
def extractOpt[A](implicit
formats: Formats,
mf: Tag[A]
): Option[A] =
Extraction.extractOpt(this)(formats, mf)
/** Attempt to extract a concrete Scala instance of type `A` from this
* `JValue` and, on failing to do so, return the default value instead.
*
* Value can be:
* - a case class
* - a primitive (String, Boolean, Date, etc.)>
* - any type which has a configured [[TypeHints custom deserializer]]
* - a supported collection type of any of the above (List, Seq,
* Map[String, _], Set)
*
* Example:
*
* {{{
* > case class Person(name: String)
* > JNothing.extractOrElse(Person("joe"))
* res0: Person("joe")
* }}}
*/
// SCALA3 Using `ClassTag` instead of `Manifest`
def extractOrElse[A](
default: => A
)(implicit formats: Formats, mf: Tag[A]): A =
Extraction.extractOpt(this)(formats, mf).getOrElse(default)
def toOpt: Option[JValue[?]] = {
if (this.isInstanceOf[JNothing.type]) None
else Some(this)
}
// this match {
// case JNothing => None
// case json => Some(json)
// }
}
case object JNothing extends JValue[None.type] {
def values = None
}
case object JNull extends JValue[Null] {
def values = null
}
case class JString(s: String) extends JValue[String] {
def values = s
}
case class JDouble(num: Double) extends JValue[Double] {
def values = num
}
case class JInt(num: BigInt) extends JValue[BigInt] {
def values = num
}
case class JBool(value: Boolean) extends JValue[Boolean] {
def values = value
}
case class JObject(obj: List[JField]) extends JValue[Map[String, Any]] {
def values = {
obj.map { case JField(name, value) =>
(name, value.values): (String, Any)
}.toMap
}
override def equals(that: Any): Boolean = that match {
case o: JObject => obj.toSet == o.obj.toSet
case _ => false
}
override def hashCode = obj.toSet[JField].hashCode
}
case object JObject {
def apply(fs: JField*): JObject = JObject(fs.toList)
}
case class JArray(arr: List[JValue[?]]) extends JValue[List[Any]] {
def values = arr.map(_.values)
override def apply(i: Int): JValue[?] = arr(i)
}
case class JField(name: String, value: JValue[?])
private[json] def quote(s: String): String = {
val buf = new StringBuilder
appendEscapedString(buf, s, RenderSettings.compact)
buf.toString
}
private def appendEscapedString(
buf: Appendable,
s: String,
settings: RenderSettings
): Unit = {
s.foreach { c =>
val strReplacement = c match {
case '"' => "\\\""
case '\\' => "\\\\"
case '\b' => "\\b"
case '\f' => "\\f"
case '\n' => "\\n"
case '\r' => "\\r"
case '\t' => "\\t"
// Set.contains will cause boxing of c to Character, try and avoid this
case c
if ((c >= '\u0000' && c < '\u0020')) || (settings.escapeChars.nonEmpty && settings.escapeChars
.contains(c)) =>
"\\u%04x".format(c: Int)
case _ => ""
}
// Use Char version of append if we can, as it's cheaper.
if (strReplacement.isEmpty) {
buf.append(c)
} else {
buf.append(strReplacement)
}
}
}
object RenderSettings {
/** Pretty-print JSON with 2-space indentation.
*/
val pretty = RenderSettings(2)
/** Compact print JSON on one line.
*/
val compact = RenderSettings(0)
/** Ranges of chars that should be escaped if this JSON is to be evaluated
* directly as JavaScript (rather than by a valid JSON parser).
*/
val jsEscapeChars =
List(
('\u00ad', '\u00ad'),
('\u0600', '\u0604'),
('\u070f', '\u070f'),
('\u17b4', '\u17b5'),
('\u200c', '\u200f'),
('\u2028', '\u202f'),
('\u2060', '\u206f'),
('\ufeff', '\ufeff'),
('\ufff0', '\uffff')
)
.foldLeft(Set[Char]()) { case (set, (start, end)) =>
set ++ (start to end).toSet
}
/** Pretty-print JSON with 2-space indentation and escape all JS-sensitive
* characters.
*/
val prettyJs = RenderSettings(2, jsEscapeChars)
/** Compact print JSON on one line and escape all JS-sensitive characters.
*/
val compactJs = RenderSettings(0, jsEscapeChars)
}
/** Parent trait for double renderers, which decide how doubles contained in a
* JDouble are rendered to JSON string.
*/
sealed trait DoubleRenderer extends Function1[Double, String] {
def apply(double: Double): String
}
/** A `DoubleRenderer` that renders special values `NaN`, `-Infinity`, and
* `Infinity` as-is using `toString`. This is not valid JSON, meaning JSON
* libraries generally won't be able to parse it (including lift-json!), but
* JavaScript can eval it. Other double values are also rendered the same
* way.
*
* Usage is not recommended.
*/
case object RenderSpecialDoubleValuesAsIs extends DoubleRenderer {
def apply(double: Double): String = {
double.toString
}
}
/** A `DoubleRenderer` that renders special values `NaN`, `-Infinity`, and
* `Infinity` as `null`. Other doubles are rendered normally using
* `toString`.
*/
case object RenderSpecialDoubleValuesAsNull extends DoubleRenderer {
def apply(double: Double): String = {
if (double.isNaN || double.isInfinity) {
"null"
} else {
double.toString
}
}
}
/** A `DoubleRenderer` that throws an `IllegalArgumentException` when the
* special values `NaN`, `-Infinity`, and `Infinity` are encountered. Other
* doubles are rendered normally using `toString`.
*/
case object FailToRenderSpecialDoubleValues extends DoubleRenderer {
def apply(double: Double): String = {
if (double.isNaN || double.isInfinity) {
throw new IllegalArgumentException(
s"Double value $double cannot be rendered to JSON with the current DoubleRenderer."
)
} else {
double.toString
}
}
}
/** RenderSettings allows for customizing how JSON is rendered to a String. At
* the moment, you can customize the indentation (if 0, all the JSON is
* printed on one line), the characters that should be escaped (in addition
* to a base set that will always be escaped for valid JSON), and whether or
* not a space should be included after a field name.
*
* @param doubleRendering
* Before Lift 3.1.0, the three special double values NaN, Infinity, and
* -Infinity were serialized as-is. This is invalid JSON, but valid
* JavaScript. We now default special double values to serialize as null,
* but provide both the old behavior and a new behavior that throws an
* exception upon finding these values. See `[[DoubleRenderer]]` and its
* subclasses for more.
*/
case class RenderSettings(
indent: Int,
escapeChars: Set[Char] = Set.empty,
spaceAfterFieldName: Boolean = false,
doubleRenderer: DoubleRenderer = RenderSpecialDoubleValuesAsNull
) {
val lineBreaks_? = indent > 0
}
/** Render `value` using `[[RenderSettings.pretty]]`.
*/
def prettyRender(value: JValue[?]): String = {
render(value, RenderSettings.pretty)
}
/** Render `value` to the given `appendable` using
* `[[RenderSettings.pretty]]`.
*/
def prettyRender(value: JValue[?], appendable: Appendable): String = {
render(value, RenderSettings.pretty, appendable)
}
/** Renders JSON directly to string in compact format. This is an optimized
* version of compact(render(value)) when the intermediate Document is not
* needed.
*/
def compactRender(value: JValue[?]): String = {
render(value, RenderSettings.compact)
}
/** Render `value` to the given `appendable` using
* `[[RenderSettings.compact]]`.
*/
def compactRender(value: JValue[?], appendable: Appendable): String = {
render(value, RenderSettings.compact, appendable)
}
/** Render `value` to the given `appendable` (a `StringBuilder`, by default)
* using the given `settings`. The appendable's `toString` will be called and
* the result will be returned.
*/
def render(
value: JValue[?],
settings: RenderSettings,
appendable: Appendable = new StringBuilder()
): String = {
bufRender(value, appendable, settings).toString()
}
case class RenderIntermediaryDocument(value: JValue[?])
def render(value: JValue[?]) = RenderIntermediaryDocument(value)
/** @param value
* the JSON to render
* @param buf
* the buffer to render the JSON into. may not be empty
*/
private def bufRender(
value: JValue[?],
buf: Appendable,
settings: RenderSettings,
indentLevel: Int = 0
): Appendable = value match {
case null => buf.append("null")
case JBool(true) => buf.append("true")
case JBool(false) => buf.append("false")
case JDouble(n) => buf.append(settings.doubleRenderer(n))
case JInt(n) => buf.append(n.toString)
case JNull => buf.append("null")
case JString(null) => buf.append("null")
case JString(s) => bufQuote(s, buf, settings)
case JArray(arr) => bufRenderArr(arr, buf, settings, indentLevel)
case JObject(obj) => bufRenderObj(obj, buf, settings, indentLevel)
case JNothing =>
sys.error(
"can't render 'nothing'"
) // TODO: this should not throw an exception
}
private def bufRenderArr(
values: List[JValue[?]],
buf: Appendable,
settings: RenderSettings,
indentLevel: Int
): Appendable = {
var firstEntry = true
val currentIndent = indentLevel + settings.indent
buf.append('[') // open array
if (!values.isEmpty) {
if (settings.lineBreaks_?) {
buf.append('\n')
}
values.foreach { elem =>
if (elem != JNothing) {
if (firstEntry) {
firstEntry = false
} else {
buf.append(',')
if (settings.lineBreaks_?) {
buf.append('\n')
}
}
(0 until currentIndent).foreach(_ => buf.append(' '))
bufRender(elem, buf, settings, currentIndent)
}
}
if (settings.lineBreaks_?) {
buf.append('\n')
}
(0 until indentLevel).foreach(_ => buf.append(' '))
}
buf.append(']')
buf
}
private def bufRenderObj(
fields: List[JField],
buf: Appendable,
settings: RenderSettings,
indentLevel: Int
): Appendable = {
var firstEntry = true
val currentIndent = indentLevel + settings.indent
buf.append('{') // open bracket
if (!fields.isEmpty) {
if (settings.lineBreaks_?) {
buf.append('\n')
}
fields.foreach {
case JField(name, value) if value != JNothing =>
if (firstEntry) {
firstEntry = false
} else {
buf.append(',')
if (settings.lineBreaks_?) {
buf.append('\n')
}
}
(0 until currentIndent).foreach(_ => buf.append(' '))
bufQuote(name, buf, settings)
buf.append(':')
if (settings.spaceAfterFieldName) {
buf.append(' ')
}
bufRender(value, buf, settings, currentIndent)
case _ => // omit fields with value of JNothing
}
if (settings.lineBreaks_?) {
buf.append('\n')
}
(0 until indentLevel).foreach(_ => buf.append(' '))
}
buf.append('}') // close bracket
buf
}
private def bufQuote(
s: String,
buf: Appendable,
settings: RenderSettings
): Appendable = {
buf.append('"') // open quote
appendEscapedString(buf, s, settings)
buf.append('"') // close quote
buf
}
}
/** Basic implicit conversions from primitive types into JSON. Example:
* import olon.json.Implicits._ JObject(JField("name", "joe") :: Nil) ==
* JObject(JField("name", JString("joe")) :: Nil)
*/
object Implicits extends Implicits
trait Implicits {
implicit def int2jvalue(x: Int): JInt = JInt(x)
implicit def long2jvalue(x: Long): JInt = JInt(x)
implicit def bigint2jvalue(x: BigInt): JInt = JInt(x)
implicit def double2jvalue(x: Double): JDouble = JDouble(x)
implicit def float2jvalue(x: Float): JDouble = JDouble(x)
implicit def bigdecimal2jvalue(x: BigDecimal): JDouble = JDouble(
x.doubleValue
)
implicit def boolean2jvalue(x: Boolean): JBool = JBool(x)
implicit def string2jvalue(x: String): JString = JString(x)
}
/** A DSL to produce valid JSON. Example: import olon.json.JsonDSL._
* ("name", "joe") ~ ("age", 15) == JObject(JField("name",JString("joe")) ::
* JField("age",JInt(15)) :: Nil)
*/
object JsonDSL extends JsonDSL
trait JsonDSL extends Implicits {
implicit def seq2jvalue[A](s: Iterable[A])(implicit
ev: A => JValue
): JArray =
JArray(s.toList.map { a =>
val v: JValue = ev(a); v
})
implicit def map2jvalue[A](m: Map[String, A])(implicit
ev: A => JValue
): JObject =
JObject(m.toList.map { case (k, v) => JField(k, ev(v)) })
implicit def option2jvalue[A](
opt: Option[A]
)(implicit ev: A => JValue): JValue = opt match {
case Some(x) => ev(x)
case None => JNothing
}
implicit def symbol2jvalue(x: Symbol): JString = JString(x.name)
implicit def pair2jvalue[A](t: (String, A))(implicit
ev: A => JValue
): JObject =
JObject(List(JField(t._1, ev(t._2))))
implicit def list2jvalue(l: List[JField]): JObject = JObject(l)
implicit def jobject2assoc(o: JObject): JsonListAssoc = new JsonListAssoc(
o.obj
)
implicit def pair2Assoc[A](t: (String, A))(implicit
ev: A => JValue
): JsonAssoc[A] =
new JsonAssoc(t)
class JsonAssoc[A](left: (String, A))(implicit ev: A => JValue) {
def ~[B <: JValue](right: (String, B)) = {
val l: JValue = ev(left._2)
val r: JValue = right._2
JObject(JField(left._1, l) :: JField(right._1, r) :: Nil)
}
def ~(right: JObject) = {
val l: JValue = ev(left._2)
JObject(JField(left._1, l) :: right.obj)
}
}
class JsonListAssoc(left: List[JField]) {
def ~(right: (String, JValue)) = JObject(
left ::: List(JField(right._1, right._2))
)
def ~(right: JObject) = JObject(left ::: right.obj)
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy