diff --git a/README.md b/README.md index 55b311cd..57166f13 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,22 @@ Operators | `[start:end]` | Array slice operator | | `[?()]` | Filter expression. Expression must evaluate to a boolean value. | + +Functions +--------- + +Functions can be invoked at the tail end of a path - the input to a function is the output of the path expression. +The function output is dictated by the function itself. + +| Function | Description | Output | +| :------------------------ | :----------------------------------------------------------------- |-----------| +| %min() | Provides the min value of an array of numbers | Double | +| %max() | Provides the max value of an array of numbers | Double | +| %avg() | Provides the average value of an array of numbers | Double | +| %stddev() | Provides the standard deviation value of an array of numbers | Double | +| %length() | Provides the length of an array | Integer | + + Path Examples ------------- @@ -124,7 +140,7 @@ Given the json | $..book[?(@.price <= $['expensive'])] | All books in store that are not "expensive" | | $..book[?(@.author =~ /.*REES/i)] | All books matching regex (ignore case) | | $..* | Give me every thing | - +| $..book.%length() | The number of books | Reading a Document ------------------ diff --git a/json-path/src/main/java/com/jayway/jsonpath/Function.java b/json-path/src/main/java/com/jayway/jsonpath/Function.java new file mode 100644 index 00000000..4256f8cf --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/Function.java @@ -0,0 +1,33 @@ +package com.jayway.jsonpath; + +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; + +/** + * Defines the pattern by which a function can be executed over the result set in the particular path + * being grabbed. The Function's input is the content of the data from the json path selector and its output + * is defined via the functions behavior. Thus transformations in types can take place. Additionally, functions + * can accept multiple selectors in order to produce their output. + * + * Created by matt@mjgreenwood.net on 6/26/15. + */ +public interface Function { + + /** + * Invoke the function and output a JSON object (or scalar) value which will be the result of executing the path + * + * @param currentPath + * The current path location inclusive of the function name + * @param parent + * The path location above the current function + * + * @param model + * The JSON model as input to this particular function + * + * @param ctx + * Eval context, state bag used as the path is traversed, maintains the result of executing + * + * @return + */ + Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx); +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java index f423e2d0..1ea61c5b 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/JsonPath.java @@ -172,6 +172,9 @@ public class JsonPath { boolean optSuppressExceptions = configuration.containsOption(Option.SUPPRESS_EXCEPTIONS); try { + if(path.isFunctionPath()){ + return path.evaluate(jsonObject, jsonObject, configuration).getValue(true); + } if(optAsPathList){ return (T)path.evaluate(jsonObject, jsonObject, configuration).getPath(); } else { diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java b/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java index 4a2f9cfc..e2c074c7 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/CompiledPath.java @@ -16,7 +16,7 @@ package com.jayway.jsonpath.internal; import com.jayway.jsonpath.Configuration; import com.jayway.jsonpath.internal.token.EvaluationContextImpl; -import com.jayway.jsonpath.internal.token.PathToken; +import com.jayway.jsonpath.internal.token.RootPathToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -24,12 +24,12 @@ public class CompiledPath implements Path { private static final Logger logger = LoggerFactory.getLogger(CompiledPath.class); - private final PathToken root; + private final RootPathToken root; private final boolean isRootPath; - public CompiledPath(PathToken root, boolean isRootPath) { + public CompiledPath(RootPathToken root, boolean isRootPath) { this.root = root; this.isRootPath = isRootPath; } @@ -64,6 +64,11 @@ public class CompiledPath implements Path { return root.isPathDefinite(); } + @Override + public boolean isFunctionPath() { + return root.isFunctionPath(); + } + @Override public String toString() { return root.toString(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java b/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java index b87b7b55..3d99b1b1 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/Path.java @@ -49,6 +49,12 @@ public interface Path { */ boolean isDefinite(); + /** + * + * @return true id this path is a function + */ + boolean isFunctionPath(); + /** * * @return true id this path is starts with '$' and false if the path starts with '@' diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java index 88b6a107..4a3a36c0 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/PathCompiler.java @@ -5,6 +5,7 @@ import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Predicate; import com.jayway.jsonpath.internal.token.ArrayIndexOperation; import com.jayway.jsonpath.internal.token.ArraySliceOperation; +import com.jayway.jsonpath.internal.token.FunctionPathToken; import com.jayway.jsonpath.internal.token.PathTokenAppender; import com.jayway.jsonpath.internal.token.PathTokenFactory; import com.jayway.jsonpath.internal.token.RootPathToken; @@ -35,6 +36,7 @@ public class PathCompiler { private static final char MINUS = '-'; private static final char ESCAPE = '\\'; private static final char TICK = '\''; + private static final char FUNCTION = '%'; private static final Cache cache = new Cache(200); @@ -127,12 +129,42 @@ public class PathCompiler { case WILDCARD: return readWildCardToken(appender) || fail("Could not parse token at position " + path.position); + case FUNCTION: + return readFunctionToken(appender) || + fail("Could not parse token at position " + path.position); default: return readPropertyToken(appender) || fail("Could not parse token at position " + path.position); } } + // + // $function() + // + private boolean readFunctionToken(PathTokenAppender appender) { + if (path.currentCharIs(OPEN_SQUARE_BRACKET) || path.currentCharIs(WILDCARD) || path.currentCharIs(PERIOD) || path.currentCharIs(SPACE)) { + return false; + } + int startPosition = path.position; + int readPosition = startPosition; + int endPosition = 0; + while (path.inBounds(readPosition)) { + char c = path.charAt(readPosition); + if (c == OPEN_BRACKET && path.nextSignificantCharIs(readPosition, CLOSE_BRACKET)) { + endPosition = path.indexOfNextSignificantChar(readPosition, CLOSE_BRACKET); + break; + } + readPosition++; + } + path.setPosition(endPosition); + + String function = path.subSequence(startPosition, endPosition + 1).toString(); + + appender.appendPathToken(PathTokenFactory.createFunctionPathToken(function)); + + return path.currentIsTail(); + } + // // . // diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java new file mode 100644 index 00000000..35104ab3 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/FunctionFactory.java @@ -0,0 +1,71 @@ +package com.jayway.jsonpath.internal.function; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.internal.function.numeric.*; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +/** + * Implements a factory that given a name of the function will return the Function implementation, or null + * if the value is not obtained. + * + * Leverages the function's name in order to determine which function to execute which is maintained internally + * here via a static map + * + * Created by mattg on 6/27/15. + */ +public class FunctionFactory { + + public static final Map FUNCTIONS; + + static { + // New functions should be added here and ensure the name is not overridden + Map map = new HashMap(); + + // Math Functions + map.put("avg", Average.class); + map.put("stddev", StandardDeviation.class); + map.put("sum", Sum.class); + map.put("min", Min.class); + map.put("max", Max.class); + + // JSON Entity Functions + map.put("length", Length.class); + + FUNCTIONS = Collections.unmodifiableMap(map); + } + + /** + * Either provides a pass thru function when the function cannot be properly mapped or otherwise returns the function + * implementation based on the name using the internal FUNCTION map + * + * @see #FUNCTIONS + * @see Function + * + * @param name + * The name of the function + * + * @return + * The implementation of a function + * + * @throws InvalidPathException + */ + public static Function newFunction(String name) throws InvalidPathException { + Function result = new PassthruFunction(); + + if (null != name && FUNCTIONS.containsKey(name) && Function.class.isAssignableFrom(FUNCTIONS.get(name))) { + try { + result = (Function)FUNCTIONS.get(name).newInstance(); + } catch (InstantiationException e) { + throw new InvalidPathException("Function of name: " + name + " cannot be created", e); + } catch (IllegalAccessException e) { + throw new InvalidPathException("Function of name: " + name + " cannot be created", e); + } + } + return result; + + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java new file mode 100644 index 00000000..12188833 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/Length.java @@ -0,0 +1,26 @@ +package com.jayway.jsonpath.internal.function; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; +import net.minidev.json.JSONArray; + +import java.util.*; + +/** + * Provides the length of a JSONArray Object + * + * Created by mattg on 6/26/15. + */ +public class Length implements Function { + + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { + if(ctx.configuration().jsonProvider().isArray(model)){ + return ctx.configuration().jsonProvider().length(model); + } else if(ctx.configuration().jsonProvider().isMap(model)){ + return ctx.configuration().jsonProvider().length(model); + } + return null; + } +} \ No newline at end of file diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java new file mode 100644 index 00000000..fadda31d --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/PassthruFunction.java @@ -0,0 +1,18 @@ +package com.jayway.jsonpath.internal.function; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; + +/** + * Defines the default behavior which is to return the model that is provided as input as output + * + * Created by mattg on 6/26/15. + */ +public class PassthruFunction implements Function { + + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { + return model; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java new file mode 100644 index 00000000..d7bb97c1 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/AbstractAggregation.java @@ -0,0 +1,55 @@ +package com.jayway.jsonpath.internal.function.numeric; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.EvaluationContext; +import com.jayway.jsonpath.internal.PathRef; +import net.minidev.json.JSONArray; + +import java.util.Iterator; + +/** + * Defines the pattern for processing numerical values via an abstract implementation that iterates over the collection + * of JSONArray entities and verifies that each is a numerical value and then passes that along the abstract methods + * + * + * Created by mattg on 6/26/15. + */ +public abstract class AbstractAggregation implements Function { + + /** + * Defines the next value in the array to the mathmatical function + * + * @param value + * The numerical value to process next + */ + protected abstract void next(Number value); + + /** + * Obtains the value generated via the series of next value calls + * + * @return + * A numerical answer based on the input value provided + */ + protected abstract Number getValue(); + + @Override + public Object invoke(String currentPath, PathRef parent, Object model, EvaluationContext ctx) { + if(ctx.configuration().jsonProvider().isArray(model)){ + + Iterable objects = ctx.configuration().jsonProvider().toIterable(model); + for (Object obj : objects) { +// Object unwraped = ctx.configuration().jsonProvider().unwrap(obj); +// if (unwraped instanceof Number) { +// Number value = (Number) unwraped; +// next(value); +// } + if (obj instanceof Number) { + Number value = (Number) obj; + next(value); + } + } + return getValue(); + } + return null; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java new file mode 100644 index 00000000..f4c6788e --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Average.java @@ -0,0 +1,26 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Provides the average of a series of numbers in a JSONArray + * + * Created by mattg on 6/26/15. + */ +public class Average extends AbstractAggregation { + + private Double summation = 0d; + private Double count = 0d; + + @Override + protected void next(Number value) { + count++; + summation += value.doubleValue(); + } + + @Override + protected Number getValue() { + if (count != 0d) { + return summation / count; + } + return 0d; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java new file mode 100644 index 00000000..27570bf6 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Max.java @@ -0,0 +1,22 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Defines the summation of a series of JSONArray numerical values + * + * Created by mattg on 6/26/15. + */ +public class Max extends AbstractAggregation { + private Double max = Double.MIN_VALUE; + + @Override + protected void next(Number value) { + if (max < value.doubleValue()) { + max = value.doubleValue(); + } + } + + @Override + protected Number getValue() { + return max; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java new file mode 100644 index 00000000..3c57e5f2 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Min.java @@ -0,0 +1,22 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Defines the summation of a series of JSONArray numerical values + * + * Created by mattg on 6/26/15. + */ +public class Min extends AbstractAggregation { + private Double min = Double.MAX_VALUE; + + @Override + protected void next(Number value) { + if (min > value.doubleValue()) { + min = value.doubleValue(); + } + } + + @Override + protected Number getValue() { + return min; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java new file mode 100644 index 00000000..0a83d8a8 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/StandardDeviation.java @@ -0,0 +1,24 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Provides the standard deviation of a series of numbers + * + * Created by mattg on 6/27/15. + */ +public class StandardDeviation extends AbstractAggregation { + private Double sumSq = 0d; + private Double sum = 0d; + private Double count = 0d; + + @Override + protected void next(Number value) { + sum += value.doubleValue(); + sumSq += value.doubleValue() * value.doubleValue(); + count++; + } + + @Override + protected Number getValue() { + return Math.sqrt(sumSq/count - sum*sum/count/count); + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java new file mode 100644 index 00000000..3996bb43 --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/function/numeric/Sum.java @@ -0,0 +1,20 @@ +package com.jayway.jsonpath.internal.function.numeric; + +/** + * Defines the summation of a series of JSONArray numerical values + * + * Created by mattg on 6/26/15. + */ +public class Sum extends AbstractAggregation { + private Double summation = 0d; + + @Override + protected void next(Number value) { + summation += value.doubleValue(); + } + + @Override + protected Number getValue() { + return summation; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java new file mode 100644 index 00000000..68d15fcc --- /dev/null +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/FunctionPathToken.java @@ -0,0 +1,56 @@ +package com.jayway.jsonpath.internal.token; + +import com.jayway.jsonpath.Function; +import com.jayway.jsonpath.internal.PathRef; +import com.jayway.jsonpath.internal.function.FunctionFactory; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * Token representing a Function call to one of the functions produced via the FunctionFactory + * + * @see FunctionFactory + * + * Created by mattg on 6/27/15. + */ +public class FunctionPathToken extends PathToken { + + private final String functionName; + private final String pathFragment; + + public FunctionPathToken(String pathFragment) { + this.pathFragment = pathFragment; + Matcher matcher = Pattern.compile(".*?\\%(\\w+)\\(.*?").matcher(pathFragment); + if (matcher.matches()) { + functionName = matcher.group(1); + } + else { + // We'll end up throwing an error from the factory when we get that far + functionName = null; + } + } + + @Override + public void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + Function function = FunctionFactory.newFunction(functionName); + Object result = function.invoke(currentPath, parent, model, ctx); + ctx.addResult(currentPath, parent, result); + } + + /** + * Return the actual value by indicating true. If this return was false then we'd return the value in an array which + * isn't what is desired - true indicates the raw value is returned. + * + * @return + */ + @Override + public boolean isTokenDefinite() { + return true; + } + + @Override + public String getPathFragment() { + return pathFragment; + } +} diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java index 40760143..d80a8b93 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathToken.java @@ -14,11 +14,17 @@ */ package com.jayway.jsonpath.internal.token; +import com.jayway.jsonpath.Function; import com.jayway.jsonpath.InvalidPathException; import com.jayway.jsonpath.Option; import com.jayway.jsonpath.PathNotFoundException; import com.jayway.jsonpath.internal.PathRef; import com.jayway.jsonpath.internal.Utils; +import com.jayway.jsonpath.internal.function.FunctionFactory; +import com.jayway.jsonpath.internal.function.numeric.Average; +import com.jayway.jsonpath.internal.function.Length; +import com.jayway.jsonpath.internal.function.PassthruFunction; +import com.jayway.jsonpath.internal.function.numeric.Sum; import com.jayway.jsonpath.spi.json.JsonProvider; import java.util.List; @@ -67,7 +73,8 @@ public abstract class PathToken { PathRef pathRef = ctx.forUpdate() ? PathRef.create(model, property) : PathRef.NO_OP; if (isLeaf()) { ctx.addResult(evalPath, pathRef, propertyVal); - } else { + } + else { next().evaluate(evalPath, pathRef, propertyVal, ctx); } } else { @@ -201,6 +208,10 @@ public abstract class PathToken { return super.equals(obj); } + public void invoke(Function function, String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx) { + ctx.addResult(currentPath, parent, function.invoke(currentPath, parent, model, ctx)); + } + public abstract void evaluate(String currentPath, PathRef parent, Object model, EvaluationContextImpl ctx); public abstract boolean isTokenDefinite(); diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java index 0dd53bb5..b363fcb0 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/PathTokenFactory.java @@ -44,4 +44,8 @@ public class PathTokenFactory { public static PathToken createPredicatePathToken(Predicate predicate) { return new PredicatePathToken(predicate); } + + public static PathToken createFunctionPathToken(String function) { + return new FunctionPathToken((function)); + } } diff --git a/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java b/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java index 9495aa64..60f204ef 100644 --- a/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java +++ b/json-path/src/main/java/com/jayway/jsonpath/internal/token/RootPathToken.java @@ -73,5 +73,7 @@ public class RootPathToken extends PathToken { return true; } - + public boolean isFunctionPath() { + return (tail instanceof FunctionPathToken); + } } diff --git a/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java b/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java index ababb1e0..b387f43a 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/BaseTest.java @@ -4,9 +4,11 @@ import com.jayway.jsonpath.internal.Path; import com.jayway.jsonpath.spi.json.GsonJsonProvider; import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider; import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import com.jayway.jsonpath.internal.token.PredicateContextImpl; +import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider; import java.util.HashMap; @@ -30,7 +32,11 @@ public class BaseTest { .jsonProvider(new JacksonJsonNodeJsonProvider()) .build(); - public static final Configuration JSON_SMART_CONFIGURATION = Configuration.defaultConfiguration(); + public static final Configuration JSON_SMART_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JsonSmartMappingProvider()) + .jsonProvider(new JsonSmartJsonProvider()) + .build(); public static final String JSON_BOOK_DOCUMENT = "{ " + diff --git a/json-path/src/test/java/com/jayway/jsonpath/Configurations.java b/json-path/src/test/java/com/jayway/jsonpath/Configurations.java new file mode 100644 index 00000000..b0d01a57 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/Configurations.java @@ -0,0 +1,56 @@ +package com.jayway.jsonpath; + +import com.jayway.jsonpath.spi.json.GsonJsonProvider; +import com.jayway.jsonpath.spi.json.JacksonJsonNodeJsonProvider; +import com.jayway.jsonpath.spi.json.JacksonJsonProvider; +import com.jayway.jsonpath.spi.json.JsonSmartJsonProvider; +import com.jayway.jsonpath.spi.mapper.GsonMappingProvider; +import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; +import com.jayway.jsonpath.spi.mapper.JsonSmartMappingProvider; + +import java.util.Arrays; + +public class Configurations { + + public static final Configuration GSON_CONFIGURATION = Configuration + .builder() + .mappingProvider(new GsonMappingProvider()) + .jsonProvider(new GsonJsonProvider()) + .build(); + + public static final Configuration JACKSON_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonProvider()) + .build(); + + public static final Configuration JACKSON_JSON_NODE_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JacksonMappingProvider()) + .jsonProvider(new JacksonJsonNodeJsonProvider()) + .build(); + + public static final Configuration JSON_SMART_CONFIGURATION = Configuration + .builder() + .mappingProvider(new JsonSmartMappingProvider()) + .jsonProvider(new JsonSmartJsonProvider()) + .build(); + + public static Iterable configurations() { + return Arrays.asList( + JSON_SMART_CONFIGURATION + ,GSON_CONFIGURATION + ,JACKSON_CONFIGURATION + ,JACKSON_JSON_NODE_CONFIGURATION + ); + } + + + public static Iterable objectMappingConfigurations() { + return Arrays.asList( + GSON_CONFIGURATION + ,JACKSON_CONFIGURATION + ,JACKSON_JSON_NODE_CONFIGURATION + ); + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java index 9c80e56b..2949fcaa 100644 --- a/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTest.java @@ -1,6 +1,8 @@ package com.jayway.jsonpath; import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; import java.io.IOException; import java.util.List; @@ -8,84 +10,38 @@ import java.util.List; import static com.jayway.jsonpath.JsonPath.using; import static org.assertj.core.api.Assertions.assertThat; +@RunWith(Parameterized.class) public class JsonProviderTest extends BaseTest { - private static final String JSON = - "[" + - "{\n" + - " \"foo\" : \"foo0\",\n" + - " \"bar\" : 0,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"prop\" : \"yepp0\"}" + - "}," + - "{\n" + - " \"foo\" : \"foo1\",\n" + - " \"bar\" : 1,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"prop\" : \"yepp1\"}" + - "}," + - "{\n" + - " \"foo\" : \"foo2\",\n" + - " \"bar\" : 2,\n" + - " \"baz\" : true,\n" + - " \"gen\" : {\"prop\" : \"yepp2\"}" + - "}" + - "]"; + private final Configuration conf; - @Test - public void strings_are_unwrapped() { - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); - assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); + public JsonProviderTest(Configuration conf) { + this.conf = conf; } - @Test - public void integers_are_unwrapped() { - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); + @Parameterized.Parameters + public static Iterable configurations() { + return Configurations.configurations(); } @Test - public void ints_are_unwrapped() { - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(JSON_SMART_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); + public void strings_are_unwrapped() { + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.string-property", String.class)).isEqualTo("string-value"); } @Test - public void list_of_numbers() { - - TypeRef> typeRef = new TypeRef>() {}; - - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); - assertThat(using(JACKSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); - assertThat(using(GSON_CONFIGURATION).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + public void integers_are_unwrapped() { + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.int-max-property", Integer.class)).isEqualTo(Integer.MAX_VALUE); } @Test - public void test_type_ref() throws IOException { - TypeRef>> typeRef = new TypeRef>>() {}; - - assertThat(using(JACKSON_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); - assertThat(using(JACKSON_JSON_NODE_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); - assertThat(using(GSON_CONFIGURATION).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + public void ints_are_unwrapped() { + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.int-max-property", int.class)).isEqualTo(Integer.MAX_VALUE); } - public static class FooBarBaz { - public T gen; - public String foo; - public Long bar; - public boolean baz; - } - public static class Sub { - public String prop; - } + } diff --git a/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTestObjectMapping.java b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTestObjectMapping.java new file mode 100644 index 00000000..b0028c03 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/JsonProviderTestObjectMapping.java @@ -0,0 +1,75 @@ +package com.jayway.jsonpath; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.io.IOException; +import java.util.List; + +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; + +@RunWith(Parameterized.class) +public class JsonProviderTestObjectMapping extends BaseTest { + + private static final String JSON = + "[" + + "{\n" + + " \"foo\" : \"foo0\",\n" + + " \"bar\" : 0,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp0\"}" + + "}," + + "{\n" + + " \"foo\" : \"foo1\",\n" + + " \"bar\" : 1,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp1\"}" + + "}," + + "{\n" + + " \"foo\" : \"foo2\",\n" + + " \"bar\" : 2,\n" + + " \"baz\" : true,\n" + + " \"gen\" : {\"prop\" : \"yepp2\"}" + + "}" + + "]"; + + private final Configuration conf; + + public JsonProviderTestObjectMapping(Configuration conf) { + this.conf = conf; + } + + @Parameterized.Parameters + public static Iterable configurations() { + return Configurations.objectMappingConfigurations(); + } + + @Test + public void list_of_numbers() { + + TypeRef> typeRef = new TypeRef>() {}; + + assertThat(using(conf).parse(JSON_DOCUMENT).read("$.store.book[*].display-price", typeRef)).containsExactly(8.95D, 12.99D, 8.99D, 22.99D); + } + + @Test + public void test_type_ref() throws IOException { + TypeRef>> typeRef = new TypeRef>>() {}; + + assertThat(using(conf).parse(JSON).read("$", typeRef)).extracting("foo").containsExactly("foo0", "foo1", "foo2"); + } + + + public static class FooBarBaz { + public T gen; + public String foo; + public Long bar; + public boolean baz; + } + + public static class Sub { + public String prop; + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java new file mode 100644 index 00000000..fb2eaed8 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/BaseFunctionTest.java @@ -0,0 +1,45 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; + +import java.io.IOException; +import java.util.Scanner; + +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Created by mattg on 6/27/15. + */ +public class BaseFunctionTest { + protected static final String NUMBER_SERIES = "{\"numbers\" : [ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}"; + protected static final String TEXT_SERIES = "{\"text\" : [ \"a\", \"b\", \"c\", \"d\", \"e\", \"f\" ]}"; + + + + + /** + * Verify the function returns the correct result based on the input expectedValue + * + * @param pathExpr + * The path expression to execute + * + * @param json + * The json document (actual content) to parse + * + * @param expectedValue + * The expected value to be returned from the test + */ + protected void verifyFunction(Configuration conf, String pathExpr, String json, Object expectedValue) { + Object result = using(conf).parse(json).read(pathExpr); + assertThat(result).isEqualTo(expectedValue); + } + + protected void verifyMathFunction(Configuration conf, String pathExpr, Object expectedValue) { + verifyFunction(conf, pathExpr, NUMBER_SERIES, expectedValue); + } + + protected String getResourceAsText(String resourceName) throws IOException { + return new Scanner(BaseFunctionTest.class.getResourceAsStream(resourceName), "UTF-8").useDelimiter("\\A").next(); + } +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java new file mode 100644 index 00000000..f37ce18d --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/JSONEntityFunctionTest.java @@ -0,0 +1,105 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.Configurations; +import net.minidev.json.JSONArray; +import org.junit.Test; + +/** + * Verifies methods that are helper implementations of functions for manipulating JSON entities, i.e. + * length, etc. + * + * Created by mattg on 6/27/15. + */ +public class JSONEntityFunctionTest extends BaseFunctionTest { + + + private static final String BATCH_JSON = "{\n" + + " \"batches\": {\n" + + " \"minBatchSize\": 10,\n" + + " \"results\": [\n" + + " {\n" + + " \"productId\": 23,\n" + + " \"values\": [\n" + + " 2,\n" + + " 45,\n" + + " 34,\n" + + " 23,\n" + + " 3,\n" + + " 5,\n" + + " 4,\n" + + " 3,\n" + + " 2,\n" + + " 1,\n" + + " ]\n" + + " },\n" + + " {\n" + + " \"productId\": 23,\n" + + " \"values\": [\n" + + " 52,\n" + + " 3,\n" + + " 12,\n" + + " 11,\n" + + " 18,\n" + + " 22,\n" + + " 1\n" + + " ]\n" + + " }\n" + + " ]\n" + + " }\n" + + "}"; + + private Configuration conf = Configurations.JSON_SMART_CONFIGURATION; + + @Test + public void testLengthOfTextArray() { + // The length of JSONArray is an integer + System.out.println(TEXT_SERIES); + verifyFunction(conf, "$['text'].%length()", TEXT_SERIES, 6); + } + @Test + public void testLengthOfNumberArray() { + // The length of JSONArray is an integer + verifyFunction(conf, "$.numbers.%length()", NUMBER_SERIES, 10); + } + + + @Test + public void testLengthOfStructure() { + verifyFunction(conf, "$.batches.%length()", BATCH_JSON, 2); + } + + /** + * The fictitious use-case/story - is we have a collection of batches with values indicating some quality metric. + * We want to determine the average of the values for only the batch's values where the number of items in the batch + * is greater than the min batch size which is encoded in the JSON document. + * + * We use the length function in the predicate to determine the number of values in each batch and then for those + * batches where the count is greater than min we calculate the average batch value. + * + * Its completely contrived example, however, this test exercises functions within predicates. + */ + @Test + public void testPredicateWithFunctionCallSingleMatch() { + String path = "$.batches.results[?(@.values.%length() >= $.batches.minBatchSize)].values.%avg()"; + + // Its an array because in some use-cases the min size might match more than one batch and thus we'll get + // the average out for each collection + JSONArray values = new JSONArray(); + values.add(12.2d); + verifyFunction(conf, path, BATCH_JSON, values); + } + + @Test + public void testPredicateWithFunctionCallTwoMatches() { + String path = "$.batches.results[?(@.values.%length() >= 3)].values.%avg()"; + + // Its an array because in some use-cases the min size might match more than one batch and thus we'll get + // the average out for each collection + JSONArray values = new JSONArray(); + values.add(12.2d); + values.add(17d); + verifyFunction(conf, path, BATCH_JSON, values); + } + +} diff --git a/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java new file mode 100644 index 00000000..5baad6c6 --- /dev/null +++ b/json-path/src/test/java/com/jayway/jsonpath/functions/NumericFunctionTest.java @@ -0,0 +1,90 @@ +package com.jayway.jsonpath.functions; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.Configurations; +import com.jayway.jsonpath.JsonPath; +import net.minidev.json.JSONArray; +import org.junit.Ignore; +import org.junit.Test; +import org.junit.experimental.theories.DataPoints; +import org.junit.experimental.theories.Theories; +import org.junit.experimental.theories.Theory; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Arrays; + +import static com.jayway.jsonpath.Configurations.*; +import static com.jayway.jsonpath.JsonPath.using; +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.runners.Parameterized.Parameters; + +/** + * Defines functional tests around executing: + * + * - sum + * - avg + * - stddev + * + * for each of the above, executes the test and verifies that the results are as expected based on a static input + * and static output. + * + * Created by mattg on 6/26/15. + */ +@RunWith(Parameterized.class) +public class NumericFunctionTest extends BaseFunctionTest { + + private static final Logger logger = LoggerFactory.getLogger(NumericFunctionTest.class); + + private Configuration conf = Configurations.GSON_CONFIGURATION; + + public NumericFunctionTest(Configuration conf) { + logger.debug("Testing with configuration {}", conf.getClass().getName()); + this.conf = conf; + } + + @Parameters + public static Iterable configurations() { + return Configurations.configurations(); + } + + + @Test + public void testAverageOfDoubles() { + verifyMathFunction(conf, "$.numbers.%avg()", 5.5); + } + + @Test + public void testSumOfDouble() { + verifyMathFunction(conf, "$.numbers.%sum()", (10d * (10d + 1d)) / 2d); + } + + @Test + public void testMaxOfDouble() { + verifyMathFunction(conf, "$.numbers.%max()", 10d); + } + + @Test + public void testMinOfDouble() { + verifyMathFunction(conf, "$.numbers.%min()", 1d); + } + + @Test + public void testStdDevOfDouble() { + verifyMathFunction(conf, "$.numbers.%stddev()", 2.8722813232690143d); + } + + /** + * Expect that for an invalid function name we'll get back the original input to the function + */ +// @Test +// @Ignore +// public void testInvalidFunctionNameNegative() { +// JSONArray numberSeries = new JSONArray(); +// numberSeries.addAll(Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)); +// assertThat(using(conf).parse(NUMBER_SERIES).read("$.numbers.%foo()")).isEqualTo(numberSeries); +// } + +}