Skip to content

Instantly share code, notes, and snippets.

@OlegIlyenko
Last active March 20, 2018 00:29
Show Gist options
  • Save OlegIlyenko/5f77fc6ffd0283b80d5e8f109c874fdb to your computer and use it in GitHub Desktop.
Save OlegIlyenko/5f77fc6ffd0283b80d5e8f109c874fdb to your computer and use it in GitHub Desktop.
Stefan Goessner JsonPath (http://goessner.net/articles/JsonPath/) implementation for Circe
// For build.sbt:
//
// libraryDependencies ++= Seq(
// "io.circe" %% "circe-core" % "0.9.2",
// "io.circe" %% "circe-parser" % "0.9.2",
// "com.jayway.jsonpath" % "json-path" % "2.3.0")
import com.jayway.jsonpath.{InvalidJsonException, JsonPathException, Configuration, JsonPath, TypeRef}
import com.jayway.jsonpath.spi.json.JsonProvider
import com.jayway.jsonpath.spi.mapper.MappingProvider
import io.circe._
import java.io.InputStream
import scala.collection.JavaConverters._
import scala.io.Source
object CirceJsonPath {
private lazy val config =
Configuration.builder()
.jsonProvider(new CirceJsonProvider)
.mappingProvider(new CirceMappingProvider)
.build()
def query(json: Json, jsonPath: String): Json =
JsonPathValueWrapper.toJson(JsonPath.using(config).parse(json).read(jsonPath))
}
class CirceJsonProvider extends JsonProvider {
override def createArray(): AnyRef = JsonPathValueWrapper.emptyArray
override def setArrayIndex(array: Any, idx: Int, newValue: Any): Unit =
array match {
case arr: java.util.ArrayList[Any] @unchecked ⇒ if (idx == arr.size) arr.add(newValue) else arr.set(idx, newValue)
case _ ⇒ error("setArrayIndex is only available on new objects")
}
override def length(obj: Any): Int = {
obj match {
case json: Json if json.isArray ⇒
json.asArray.get.size
case json: Json if json.isObject ⇒
json.asObject.get.size
case json: Json if json.isString ⇒
json.asString.get.length
case arr: java.util.ArrayList[_] ⇒
arr.size
case obj: java.util.LinkedHashMap[_, _] ⇒
obj.size
case s: String ⇒
s.length
case _ ⇒ throw new JsonPathException(s"Length operation cannot be applied to ${if (obj != null) obj.getClass.getName else "null"}")
}
}
override def getArrayIndex(obj: Any, idx: Int): AnyRef = obj match {
case arr: java.util.ArrayList[AnyRef @unchecked] ⇒ arr.get(idx)
case json: Json if json.isArray ⇒ json.asArray.get(idx)
case o ⇒ notJson(o)
}
override def getArrayIndex(obj: Any, idx: Int, unwrap: Boolean): AnyRef =
getArrayIndex(obj, idx)
override def createMap(): AnyRef = JsonPathValueWrapper.emptyMap
override def setProperty(obj: Any, key: Any, value: Any): Unit = obj match {
case obj: java.util.LinkedHashMap[String @unchecked, Any @unchecked] ⇒ obj.put(key.asInstanceOf[String], value)
case _ ⇒ error("setProperty is only available on new objects")
}
override def getPropertyKeys(obj: Any) =
obj match {
case obj: java.util.LinkedHashMap[String, Any] @unchecked ⇒ obj.keySet
case obj: Json if obj.isObject ⇒ obj.asObject.get.keys.asJavaCollection
case o ⇒ Vector.empty.asJavaCollection
}
override def removeProperty(obj: Any, key: Any): Unit =
obj match {
case obj: java.util.LinkedHashMap[_, _] ⇒ obj.remove(key)
case _ ⇒ error("removeProperty is only available on new objects")
}
override def getMapValue(obj: Any, key: String): AnyRef =
obj match {
case obj: java.util.LinkedHashMap[String, AnyRef] @unchecked if obj.containsKey(key) ⇒ obj.get(key)
case json: Json if json.isObject && json.asObject.get.contains(key) ⇒ json.asObject.get(key).get
case _ ⇒ JsonProvider.UNDEFINED
}
override def toIterable(obj: Any) =
if (isArray(obj)) obj.asInstanceOf[Json].asArray.get.asJava
else error(s"Cannot iterate over ${if (obj != null) obj.getClass.getName else "null"}")
override def unwrap(obj: Any): AnyRef =
obj match {
case json: Json ⇒
json.fold(
jsonNull = null,
jsonBoolean = b ⇒ b: java.lang.Boolean,
jsonNumber = _.toBigDecimal.get,
jsonString = identity,
jsonArray = JsonPathValueWrapper.array(_),
jsonObject = JsonPathValueWrapper.map(_))
case obj ⇒ obj.asInstanceOf[AnyRef]
}
override def isMap(obj: Any): Boolean = obj match {
case obj: java.util.HashMap[_, _] ⇒ true
case obj: Json if obj.isObject ⇒ true
case _ ⇒ false
}
override def isArray(obj: Any): Boolean = obj match {
case obj: java.util.ArrayList[_] ⇒ true
case obj: Json if obj.isArray ⇒ true
case _ ⇒ false
}
override def toJson(obj: Any): String =
obj match {
case json: Json ⇒ json.spaces2
case obj ⇒ JsonPathValueWrapper.toJson(obj).spaces2
}
override def parse(json: String): AnyRef =
io.circe.parser.parse(json).fold(e ⇒ throw new InvalidJsonException(e, json), identity)
override def parse(jsonStream: InputStream, charset: String): AnyRef = {
val json = Source.fromInputStream(jsonStream, charset).getLines.mkString("\n")
io.circe.parser.parse(json).fold(e ⇒ throw new InvalidJsonException(e, json), identity)
}
private def notJson(obj: Any) = error("Not a JSON value")
private def error(message: String) = throw new JsonPathException(message)
}
object JsonPathValueWrapper {
def emptyMap: java.util.LinkedHashMap[String, Any] =
new java.util.LinkedHashMap[String, Any]
def map(obj: JsonObject): java.util.LinkedHashMap[String, Any] = {
val values = new java.util.LinkedHashMap[String, Any](obj.size)
obj.keys.foreach { key ⇒
values.put(key, obj(key).get)
}
values
}
def emptyArray = new java.util.ArrayList[Any]
def array(obj: Vector[Any]): java.util.ArrayList[Any] = {
val values = new java.util.ArrayList[Any](obj.size)
obj.foreach { key ⇒
values.add(key)
}
values
}
def toJson(obj: Any): Json = obj match {
case arr: java.util.ArrayList[_] ⇒
Json.arr(arr.asScala.map(toJson): _*)
case obj: java.util.LinkedHashMap[String, Any] @unchecked ⇒
Json.obj(obj.asScala.toSeq.map{case (k, v) ⇒ k → toJson(v)}: _*)
case json: Json ⇒
json
case v: String ⇒
Json.fromString(v)
case v: Boolean ⇒
Json.fromBoolean(v)
case v: Int ⇒
Json.fromInt(v)
case v: Float ⇒
Json.fromFloat(v).get
case v: Double ⇒
Json.fromDouble(v).get
case v: BigInt ⇒
Json.fromBigInt(v)
case v: BigDecimal ⇒
Json.fromBigDecimal(v)
case null ⇒
Json.Null
case v ⇒
throw new JsonPathException("Unsupported value: " + v)
}
}
class CirceMappingProvider extends MappingProvider {
override def map[T](source: Any, targetType: Class[T], configuration: Configuration): T =
throw new UnsupportedOperationException("Circe JSON provider does not support mapping!")
override def map[T](source: Any, targetType: TypeRef[T], configuration: Configuration): T =
throw new UnsupportedOperationException("Circe JSON provider does not support TypeRef mapping!")
}
import io.circe.Json
import io.circe.parser._
object Test extends App {
val json = parse("""
{
"store": {
"book": [
{
"category": "reference",
"author": "Nigel Rees",
"comment": {"text": "hello"},
"title": "Sayings of the Century",
"price": 8.95
},
{
"category": "fiction",
"author": "Evelyn Waugh",
"title": "Sword of Honour",
"price": 12.99
},
{
"category": "fiction",
"author": "Herman Melville",
"title": "Moby Dick",
"isbn": "0-553-21311-3",
"price": 8.99
},
{
"category": "fiction",
"author": "J. R. R. Tolkien",
"title": "The Lord of the Rings",
"isbn": "0-395-19395-8",
"price": 22.99
}
],
"bicycle": {
"color": "red",
"price": 19.95
}
},
"expensive": 10
}
""").right.get
val result: Json = CirceJsonPath.query(json, "$.store.book[?(@.price < 10)]")
println(result)
// Prints following JSON:
//
// [
// {
// "category" : "reference",
// "author" : "Nigel Rees",
// "title" : "Sayings of the Century",
// "price" : 8.95
// },
// {
// "category" : "fiction",
// "author" : "Herman Melville",
// "title" : "Moby Dick",
// "isbn" : "0-553-21311-3",
// "price" : 8.99
// }
// ]
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment