Skip to content
This repository has been archived by the owner on Apr 20, 2020. It is now read-only.

Universal approach for Jackson and Play JSON libraries #36

Closed
wants to merge 4 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
13 changes: 13 additions & 0 deletions build.sbt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,19 @@ import _root_.io.gatling.build.license._

enablePlugins(AutomateHeaderPlugin, SonatypeReleasePlugin)

name := "jsonpath"

lazy val global = (project in file("."))
.aggregate(core, jackson, play)

lazy val core = project

lazy val jackson = project
.dependsOn(core % "test->test; compile->compile")

lazy val play = project
.dependsOn(core % "test->test; compile->compile")

projectDevelopers := Seq(
GatlingDeveloper("slandelle@gatling.io", "Stéphane Landelle", isGatlingCorp = true),
GatlingDeveloper("nremond@gmail.com", "Nicolas Rémond", isGatlingCorp = false)
Expand Down
1 change: 1 addition & 0 deletions core/build.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
name := "jsonpath-core"
3 changes: 3 additions & 0 deletions core/dependencies.sbt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
libraryDependencies += "org.scala-lang.modules" %% "scala-parser-combinators" % "1.1.2"
libraryDependencies += "org.scalatest" %% "scalatest" % "3.0.5" % "test"
libraryDependencies += "org.scalacheck" %% "scalacheck" % "1.14.0" % "test"
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,6 @@

package io.gatling.jsonpath

import com.fasterxml.jackson.databind.JsonNode
import com.fasterxml.jackson.databind.node.{ BooleanNode, DoubleNode, LongNode, NullNode, TextNode }

object AST {
sealed trait AstToken
sealed trait PathToken extends AstToken
Expand All @@ -37,30 +34,24 @@ object AST {
* Slicing of an array, indices start at zero
*
* @param start is the first item that you want (of course)
* @param stop is the first item that you do not want
* @param step, being positive or negative, defines whether you are moving
* @param stop is the first item that you do not want
* @param step being positive or negative, defines whether you are moving
*/
case class ArraySlice(start: Option[Int], stop: Option[Int], step: Int = 1) extends ArrayAccessor

object ArraySlice {
val All = ArraySlice(None, None)
}

case class ArrayRandomAccess(indices: List[Int]) extends ArrayAccessor

// JsonPath Filter AST //////////////////////////////////////////////

case object CurrentNode extends PathToken
sealed trait FilterValue extends AstToken

object FilterDirectValue {
def long(value: Long) = FilterDirectValue(new LongNode(value))
def double(value: Double) = FilterDirectValue(new DoubleNode(value))
val True = FilterDirectValue(BooleanNode.TRUE)
val False = FilterDirectValue(BooleanNode.FALSE)
def string(value: String) = FilterDirectValue(new TextNode(value))
val Null = FilterDirectValue(NullNode.instance)
}
sealed trait FilterValue extends AstToken

case class FilterDirectValue(node: JsonNode) extends FilterValue
case class FilterDirectValue(node: JsonElement[_]) extends FilterValue

case class SubQuery(path: List[PathToken]) extends FilterValue

Expand All @@ -70,4 +61,5 @@ object AST {
case class BooleanFilter(operator: BinaryBooleanOperator, lhs: FilterToken, rhs: FilterToken) extends FilterToken

case class RecursiveFilterToken(filter: FilterToken) extends PathToken

}
193 changes: 193 additions & 0 deletions core/src/main/scala/io/gatling/jsonpath/BaseJsonPath.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,193 @@
/*
* Copyright 2011-2019 GatlingCorp (https://gatling.io)
*
* 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.gatling.jsonpath

import io.gatling.jsonpath.AST._

import scala.math.abs

case class JPError(reason: String)

abstract class BaseJsonPath[T] {
private val JsonPathParser = ThreadLocal.withInitial[Parser](() => new Parser)

def compile(query: String): Either[JPError, JsonPathQuery] =
JsonPathParser.get.compile(query) match {
case Parser.Success(q, _) => Right(new JsonPathQuery(q))
case ns: Parser.NoSuccess => Left(JPError(ns.msg))
}

protected def mapJsonObject(jsonObject: T): JsonElement[_]

def query(query: String, jsonObject: T): Either[JPError, Iterator[Any]] =
compile(query).right.map(_.query(mapJsonObject(jsonObject)))
}

class JsonPathQuery(path: List[PathToken]) extends Serializable {
private def getValue(element: JsonElement[_]): Any = element match {
case obj: ObjectElement => getObject(obj)
case array: ArrayElement => getArray(array)
case LongValue(value) => value
case DoubleValue(value) => value
case BooleanValue(value) => value
case StringValue(value) => value
case NullValue => null
}

private def getObject(obj: ObjectElement): Map[String, Any] =
obj.values.map { case (key, value) => key -> getValue(value) }.toMap

private def getArray(array: ArrayElement): Seq[Any] =
array.values.map(getValue).toList

def query(jsonNode: JsonElement[_]): Iterator[Any] =
new JsonPathWalker(jsonNode, path).walk().map(getValue)
}

class JsonPathWalker(rootNode: JsonElement[_], fullPath: List[PathToken]) {

def walk(): Iterator[JsonElement[_]] = walk(rootNode, fullPath)

private[this] def walk(node: JsonElement[_], path: List[PathToken]): Iterator[JsonElement[_]] =
path match {
case head :: tail => walk1(node, head).flatMap(walk(_, tail))
case _ => Iterator.single(node)
}

private[this] def walk1(node: JsonElement[_], query: PathToken): Iterator[JsonElement[_]] =
query match {
case RootNode => Iterator.single(rootNode)

case CurrentNode => Iterator.single(node)

case Field(name) => node match {
case obj: ObjectElement if obj.contains(name) =>
Iterator.single(obj(name))
case _ => Iterator.empty
}

case RecursiveField(name) => new RecursiveFieldIterator(node, name)

case MultiField(fieldNames) => node match {
case obj: ObjectElement =>
// don't use collect on iterator with filter causes (executed twice)
fieldNames.iterator.filter(obj.contains).map(obj(_))
case _ => Iterator.empty
}

case AnyField => node match {
case obj: ObjectElement => obj.values.map(_._2)
case _ => Iterator.empty
}

case ArraySlice(None, None, 1) => node match {
case array: ArrayElement => array.values
case _ => Iterator.empty
}

case ArraySlice(start, stop, step) => node match {
case array: ArrayElement => sliceArray(array, start, stop, step)
case _ => Iterator.empty
}

case ArrayRandomAccess(indices) => node match {
case array: ArrayElement => indices.iterator.collect {
case i if i >= 0 && i < array.size => array(i)
case i if i < 0 && i >= -array.size => array(i + array.size)
}
case _ => Iterator.empty
}

case RecursiveFilterToken(filterToken) => new RecursiveDataIterator(node).flatMap(applyFilter(_, filterToken))

case filterToken: FilterToken => applyFilter(node, filterToken)

case RecursiveAnyField => new RecursiveNodeIterator(node)
}

private[this] def applyFilter(currentNode: JsonElement[_], filterToken: FilterToken): Iterator[JsonElement[_]] = {

def resolveSubQuery(node: JsonElement[_], q: List[AST.PathToken], nextOp: JsonElement[_] => Boolean): Boolean = {
val it = walk(node, q)
it.hasNext && nextOp(it.next())
}

def applyBinaryOpWithResolvedLeft(node: JsonElement[_], op: ComparisonOperator, lhsNode: JsonElement[_], rhs: FilterValue): Boolean =
rhs match {
case FilterDirectValue(valueNode) => op(lhsNode, valueNode)
case SubQuery(q) => resolveSubQuery(node, q, op(lhsNode, _))
}

def applyBinaryOp(op: ComparisonOperator, lhs: FilterValue, rhs: FilterValue): JsonElement[_] => Boolean =
lhs match {
case FilterDirectValue(valueNode) => applyBinaryOpWithResolvedLeft(_, op, valueNode, rhs)
case SubQuery(q) => node => resolveSubQuery(node, q, applyBinaryOpWithResolvedLeft(node, op, _, rhs))
}

def elementsToFilter(node: JsonElement[_]): Iterator[JsonElement[_]] =
node match {
case array: ArrayElement => array.values
case obj: ObjectElement => Iterator.single(obj)
case _ => Iterator.empty
}

def evaluateFilter(filterToken: FilterToken): JsonElement[_] => Boolean =
filterToken match {
case HasFilter(subQuery) =>
walk(_, subQuery.path).hasNext

case ComparisonFilter(op, lhs, rhs) =>
applyBinaryOp(op, lhs, rhs)

case BooleanFilter(op, filter1, filter2) =>
val f1 = evaluateFilter(filter1)
val f2 = evaluateFilter(filter2)
node => op(f1(node), f2(node))
}

val filterFunction = evaluateFilter(filterToken)
elementsToFilter(currentNode).filter(filterFunction)
}

private[this] def sliceArray(array: ArrayElement, start: Option[Int], stop: Option[Int], step: Int): Iterator[JsonElement[_]] = {
val size = array.size

def lenRelative(x: Int) = if (x >= 0) x else size + x

def stepRelative(x: Int) = if (step >= 0) x else -1 - x

def relative(x: Int) = lenRelative(stepRelative(x))

val absStart = start match {
case Some(v) => relative(v)
case _ => 0
}
val absEnd = stop match {
case Some(v) => relative(v)
case _ => size
}
val absStep = abs(step)

val elements: Iterator[JsonElement[_]] = if (step < 0) Iterator.range(array.size - 1, -1, -1).map(array(_)) else array.values
val fromStartToEnd = elements.slice(absStart, absEnd)

if (absStep == 1)
fromStartToEnd
else
fromStartToEnd.grouped(absStep).map(_.head)
}
}
83 changes: 83 additions & 0 deletions core/src/main/scala/io/gatling/jsonpath/ComparisonOperators.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
/*
* Copyright 2011-2019 GatlingCorp (https://gatling.io)
*
* 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.gatling.jsonpath

sealed trait ComparisonOperator {
def apply(lhs: JsonElement[_], rhs: JsonElement[_]): Boolean
}

// Comparison operators
sealed trait ComparisonWithOrderingOperator extends ComparisonOperator {

protected def compare[T: Ordering](lhs: T, rhs: T): Boolean

def apply(lhs: JsonElement[_], rhs: JsonElement[_]): Boolean =
(lhs, rhs) match {
case (StringValue(left), StringValue(right)) => compare(left, right)
case (BooleanValue(left), BooleanValue(right)) => compare(left, right)
case (LongValue(left), LongValue(right)) => compare(left, right)
case (LongValue(left), DoubleValue(right)) => compare(left.doubleValue(), right)
case (DoubleValue(left), LongValue(right)) => compare(left, right.doubleValue())
case (DoubleValue(left), DoubleValue(right)) => compare(left, right)
case _ => false
}
}

case object EqWithOrderingOperator extends ComparisonWithOrderingOperator {
protected def compare[T: Ordering](lhs: T, rhs: T): Boolean = Ordering[T].equiv(lhs, rhs)
}

case object EqOperator extends ComparisonOperator {
override def apply(lhs: JsonElement[_], rhs: JsonElement[_]): Boolean =
(lhs, rhs) match {
case (NullValue, NullValue) => true
case _ => EqWithOrderingOperator(lhs, rhs)
}
}

case object NotEqOperator extends ComparisonOperator {
override def apply(lhs: JsonElement[_], rhs: JsonElement[_]): Boolean = !EqOperator(lhs, rhs)
}

case object LessOperator extends ComparisonWithOrderingOperator {
override protected def compare[T: Ordering](lhs: T, rhs: T): Boolean = Ordering[T].lt(lhs, rhs)
}

case object GreaterOperator extends ComparisonWithOrderingOperator {
override protected def compare[T: Ordering](lhs: T, rhs: T): Boolean = Ordering[T].gt(lhs, rhs)
}

case object LessOrEqOperator extends ComparisonWithOrderingOperator {
override protected def compare[T: Ordering](lhs: T, rhs: T): Boolean = Ordering[T].lteq(lhs, rhs)
}

case object GreaterOrEqOperator extends ComparisonWithOrderingOperator {
override protected def compare[T: Ordering](lhs: T, rhs: T): Boolean = Ordering[T].gteq(lhs, rhs)
}

// Binary boolean operators
sealed trait BinaryBooleanOperator {
def apply(lhs: Boolean, rhs: Boolean): Boolean
}

case object AndOperator extends BinaryBooleanOperator {
override def apply(lhs: Boolean, rhs: Boolean): Boolean = lhs && rhs
}

case object OrOperator extends BinaryBooleanOperator {
override def apply(lhs: Boolean, rhs: Boolean): Boolean = lhs || rhs
}
42 changes: 42 additions & 0 deletions core/src/main/scala/io/gatling/jsonpath/JsonElement.scala
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package io.gatling.jsonpath

sealed trait JsonElement[T]

abstract class ObjectElement extends JsonElement[ObjectElement] {
def contains(key: String): Boolean
def apply(key: String): JsonElement[_]
def values: Iterator[(String, JsonElement[_])]
def size: Int
def isEmpty: Boolean
def nonEmpty: Boolean = !isEmpty
}

abstract class ArrayElement extends JsonElement[ArrayElement] {
def values: Iterator[JsonElement[_]]
def apply(index: Int): JsonElement[_]
def size: Int
def isEmpty: Boolean
def nonEmpty: Boolean = !isEmpty
}

abstract class ValueElement[T <: ValueElement[T]] extends JsonElement[T] with Comparable[T]

case class LongValue(value: Long) extends ValueElement[LongValue] {
override def compareTo(o: LongValue): Int = value.compareTo(o.value)
}

case class DoubleValue(value: Double) extends ValueElement[DoubleValue] {
override def compareTo(o: DoubleValue): Int = value.compareTo(o.value)
}

case class BooleanValue(value: Boolean) extends ValueElement[BooleanValue] {
override def compareTo(o: BooleanValue): Int = value.compareTo(o.value)
}

case class StringValue(value: String) extends ValueElement[StringValue] {
override def compareTo(o: StringValue): Int = value.compareTo(o.value)
}

object NullValue extends ValueElement[Nothing] {
override def compareTo(o: Nothing): Int = 0
}