diff --git a/.flake8 b/.flake8 new file mode 100644 index 0000000..05e00da --- /dev/null +++ b/.flake8 @@ -0,0 +1,4 @@ +[flake8] +# 79 chars is too strict and we don't have 80-char terminals nowadays, +# 160 chars is too much since it doesn't let us use split view efficiently: +max-line-length = 120 diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 73aaadd..c94db12 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,5 +1,5 @@ -### JSONPath2 version -[Version of the JSONPath2 software where you are encountering the issue] +### jsonpath2 version +[Version of jsonpath2 where you are encountering the issue] ### Platform Details [Operating system distribution and release version. Cloud provider if running in the cloud] diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bffd74f..f6f42f9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,3 +1,4 @@ +exclude: 'jsonpath2/parser/JSONPath.*' repos: - repo: git://github.com/pre-commit/pre-commit-hooks rev: v1.2.3 diff --git a/.travis.yml b/.travis.yml index a11d46b..af726a3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,26 +1,24 @@ language: python -python: -- 2.7 -- 3.6 +python: 3.6 stages: - lint - test - deploy install: pip install -r requirements-dev.txt -script: -- coverage run --include='jsonpath2/*' -m pytest -v -- coverage report -m --fail-under 100 -- pip install . -- python setup.py bdist_wheel -- python setup.py sdist jobs: include: - stage: lint python: 3.6 script: pre-commit run -a - - python: 2.7 - script: pre-commit run -a + - stage: test + python: 3.6 + script: + - coverage run --include='jsonpath2/*' --omit='jsonpath2/parser/JSONPath*' -m pytest -v + - coverage report -m --fail-under 100 + - pip install . + - python setup.py bdist_wheel + - python setup.py sdist - stage: deploy python: 3.6 script: skip @@ -33,14 +31,3 @@ jobs: distributions: "sdist bdist_wheel" on: tags: true - - python: 2.7 - script: skip - deploy: - skip_cleanup: true - provider: pypi - user: dmlb2000 - password: - secure: MeskzH+TIGe4iboe/VL0+3dSJ5yL/0f8CVH7AcLBmToEArWAOdx5v42fDbOGxSio9ezYdlGyP1fTeBZybIhCvnv44W43iXol2DSkNILdIkfPhsp/RWvZh+qylSldfwiS+gKRtWRCnMpItpmIDMpbBBf/malDLgo41JrhUMeJ2EgvAlRAIDN58VcgZFCyq/cYpo8aRnqvjAmHKwNwEVZP9fFttpys7JXnxxXgP66Yr7WZIVp1v3wv5KwJdqdLlWAL/ZDftTy61ad23sZn0sv3DWYRJ8eJxb2UXQssLyhoZDvAKFoymFhBWoNINpwYDkTZeSQkRPuf1BHgSYRe3nT+71IpXIBF0H7kbmStOttk2Z2kPrlprkZhoZlUwYhRmwgTKWPR2BCyzepDfNKFGoGLz1a98ymb/iqJbBhtuo2ZHH6xsodfKmjVRS8Cx6xCXYyUG5ZW9NK/luMYSNmM78vL6HNcY+yGZ1GS6kXtjUVLPh9CSXld6fuDY/sWWzpXWuhutbfM8+TKNXNF/JOnolJVAgpseDLW3rlNM8jKFLYv1ut/MR/qyoTeMzGe03BgMxX4o5LesVHaWQfvlDubCnXmeRdgYWuxGmFCmRphIu7d3+NwI/ZWWV6dhlqdID1YbdmQJcfz/NPslAn3sXvgLpsmiuSyr2FIuXBbhQozc+xstsQ= - distributions: "bdist_wheel" - on: - tags: true diff --git a/README.md b/README.md index ad4828f..12641f9 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,142 @@ -# Python JSONPath2 +# jsonpath2 [![Build Status](https://travis-ci.org/pacifica/python-jsonpath2.svg?branch=master)](https://travis-ci.org/pacifica/python-jsonpath2) -A JSONPath implementation for Python (but better than jsonpath). + +This repository contains an implementation of [JSONPath](http://goessner.net/articles/JsonPath/) ([XPath](https://www.w3.org/TR/xpath/all/) for [JSON](https://www.json.org/)) for the Python programming language. + +## API + +### `Path` class + +The `jsonpath2.Path.Path` class represents a JSONPath. + +```python +>>> s = '{"hello":"Hello, world!"}' +'{"hello":"Hello, world!"}' +>>> import json +>>> d = json.loads(s) +{'hello':'Hello, world!'} +>>> from jsonpath2.path import Path +>>> p = Path.parse_str('$["hello"]') + +>>> list(map(lambda match_data: match_data.current_value, p.match(d))) +['Hello, world!'] +>>> list(map(lambda match_data: match_data.node.tojsonpath(), p.match(d))) +['$["hello"]'] +``` + +This class is constructed with respect to the given instance of the `jsonpath2.Path.RootNode` class (viz., the `root_node` property). + +#### `parse_str(strdata)` class method + +Parse the given string and return a new instance of this class. + +#### `parse_file(fileName, encoding='ascii')` class method + +Parse the contents of the given file and return a new instance of this class. + +#### `match(root_value)` instance method + +Match the given JSON data structure against this instance. +For each match, yield an instance of the `jsonpath2.Node.MatchData` class. + +#### `__eq__(other)` instance method + +Tests if two instances are equal. + +#### `__str__()` instance method + +Returns the string representation of this instance. + +#### `root_node` property + +The root node of the abstract syntax tree for this instance. + +### `Node` abstract class + +The `jsonpath2.Node.Node` class represents the abstract syntax tree for a JSONPath. + +#### `__eq__(other)` instance method + +Tests if two instances are equal. + +#### `__jsonpath__()` instance method + +Yields the lexer tokens for the string representation of this instance. + +#### `match(root_value, current_value)` instance method + +Match the given root and current JSON data structures against this instance. +For each match, yield an instance of the `jsonpath2.Node.MatchData` class. + +#### `tojsonpath()` instance method + +Returns the string representation of this instance. + +### `MatchData` class + +The `jsonpath2.Node.MatchData` class represents the JSON value and context for a JSONPath match. + +This class is constructed with respect to a root JSON value, a current JSON value, and an abstract syntax tree node. + +#### `__eq__(other)` instance method + +Tests if two instances are equal. + +#### `root_value` property + +The root JSON value. + +#### `current_value` property + +The current JSON value (i.e., the matching JSON value). + +#### `node` property + +The abstract syntax tree node. + +## Syntax + +| XPath | JSONPath | Description | +| - | - | - | +| `/` | `$` | the root JSON value | +| `.` | `@` | the current JSON value | +| `/` | `.` or `[]` | child operator | +| `//` | `..` | recursive descent (depth-first search) | +| `*` | `*` | wildcard (all elements of a JSON array; all values of a JSON object; otherwise none) | +| `[]` | `[]` | subscript operator | +| | | `[,]` | union operator (for two or more subscript operators) | +| n/a | `[start:end:step]` | slice operator (subset of elements of a JSON array) | +| `[]` | `?()` | filter expression (for use with subscript operator) | + +| JSONPath Filter Expression | Description | +| - | - | +| `$` or `@` | nested JSONPath (returns `true` if any match exists; otherwise, returns `false`) | +| `=`, `!=`, `>`, `>=`, `<`, `<=` | binary operator, where left-hand operand is a nested JSONPath and right-right operand is a JSON value (returns `true` if any match exists; otherwise, returns `false`) | +| `and`, `or`, `not` | Boolean operator, where operands are JSONPath filter expressions | +| `(` ... `)` | parentheses | + +## Grammar and parser + +The [ANTLR v4](https://github.com/antlr/antlr4) grammar for JSONPath is available at `jsonpath2/parser/JSONPath.g4`. + +### Installing ANTLR v4 + +Adapted from https://github.com/antlr/antlr4/blob/master/doc/getting-started.md. + +```bash +cd /usr/local/lib +curl -O http://www.antlr.org/download/antlr-4.7.1-complete.jar + +export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" + +alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool' +alias grun='java org.antlr.v4.gui.TestRig' +``` + +### Building the parser for the grammar + +Adapted from https://github.com/antlr/antlr4/blob/master/doc/python-target.md. + +```bash +antlr4 -Dlanguage=Python3 -o . -lib . jsonpath2/parser/JSONPath.g4 +``` diff --git a/jsonpath2/__init__.py b/jsonpath2/__init__.py index 7f286c7..a3668d7 100644 --- a/jsonpath2/__init__.py +++ b/jsonpath2/__init__.py @@ -1,17 +1,3 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -"""Example Module.""" - - -class Example(object): - """This is an example class in the example module.""" - - @staticmethod - def add(thing1, thing2): - """Add thing one and thing two together.""" - return thing1 + thing2 - - @staticmethod - def mul(thing1, thing2): - """Multiply thing one and thing two together.""" - return thing1 * thing2 +"""The jsonpath2 module.""" diff --git a/jsonpath2/expression.py b/jsonpath2/expression.py new file mode 100644 index 0000000..4b6eb3c --- /dev/null +++ b/jsonpath2/expression.py @@ -0,0 +1,18 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Expression module.""" +from abc import abstractmethod +from jsonpath2.tojsonpath import ToJSONPath + + +class Expression(ToJSONPath): + """Add the expression methods to the jsonpath object.""" + + def __eq__(self, other: object) -> bool: + """Test self the same as the other object.""" + return isinstance(other, self.__class__) and (self.__dict__ == other.__dict__) + + @abstractmethod + def evaluate(self, root_value: object, current_value: object) -> bool: # pragma: no cover abstract method + """Abstract method to evaluate the expression.""" + raise NotImplementedError() diff --git a/jsonpath2/expressions/__init__.py b/jsonpath2/expressions/__init__.py new file mode 100644 index 0000000..2ad25b0 --- /dev/null +++ b/jsonpath2/expressions/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Expressions used in jsonpath module.""" diff --git a/jsonpath2/expressions/operator.py b/jsonpath2/expressions/operator.py new file mode 100644 index 0000000..85ac2c0 --- /dev/null +++ b/jsonpath2/expressions/operator.py @@ -0,0 +1,243 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The operator expression module.""" +import json +from typing import Callable, Generator, List +from jsonpath2.expression import Expression +from jsonpath2.node import Node + + +class OperatorExpression(Expression): + """Basic operator expression object.""" + + def __jsonpath__(self) -> Generator[str, None, None]: # pragma: no cover abstract method + """Abstract method to return the jsonpath.""" + pass + + def evaluate(self, root_value: object, current_value: object) -> bool: # pragma: no cover abstract method + """Abstract method to evaluate the expression.""" + pass + + +class BinaryOperatorExpression(OperatorExpression): + """Binary operator expression.""" + + def __init__(self, token: str, callback: Callable[[object, object], bool], left_node: Node, right_value: object): + """Constructor save the left right and token.""" + super(BinaryOperatorExpression, self).__init__() + self.token = token + self.callback = callback + self.left_node = left_node + self.right_value = right_value + + def __jsonpath__(self) -> Generator[str, None, None]: + """Return the string json path of this expression.""" + for left_node_token in self.left_node.__jsonpath__(): + yield left_node_token + yield ' ' + yield self.token + yield ' ' + yield json.dumps(self.right_value) + + def evaluate(self, root_value: object, current_value: object) -> bool: + """Evaluate the left and right values given the token.""" + return any( + map( + lambda left_node_match_data: self.callback( + left_node_match_data.current_value, + self.right_value + ), + self.left_node.match(root_value, current_value) + ) + ) + + +class EqualBinaryOperatorExpression(BinaryOperatorExpression): + """Binary Equal operator expression.""" + + def __init__(self, *args, **kwargs): + """Constructor with the right function.""" + super(EqualBinaryOperatorExpression, self).__init__( + '=', EqualBinaryOperatorExpression.__evaluate__, + *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj, y_obj): + """Perform an equal on int or float.""" + return x_obj == y_obj + + +class NotEqualBinaryOperatorExpression(BinaryOperatorExpression): + """Binary Equal operator expression.""" + + def __init__(self, *args, **kwargs): + """Constructor with the right function.""" + super(NotEqualBinaryOperatorExpression, self).__init__( + '!=', NotEqualBinaryOperatorExpression.__evaluate__, + *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj, y_obj): + """Perform a not equal on int or float.""" + return x_obj != y_obj + + +class LessThanBinaryOperatorExpression(BinaryOperatorExpression): + """Expression to handle less than.""" + + def __init__(self, *args, **kwargs): + """Construct the binary operator with appropriate method.""" + super(LessThanBinaryOperatorExpression, self).__init__( + '<', LessThanBinaryOperatorExpression.__evaluate__, *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj, y_obj): + """Perform a less than on int or float.""" + if isinstance(x_obj, (float, int)) and isinstance(y_obj, (float, int)): + return x_obj < y_obj + return False + + +class LessThanOrEqualToBinaryOperatorExpression(BinaryOperatorExpression): + """Expression to handle less than or equal.""" + + def __init__(self, *args, **kwargs): + """Construct the binary operator with appropriate method.""" + super(LessThanOrEqualToBinaryOperatorExpression, self).__init__( + '<=', LessThanOrEqualToBinaryOperatorExpression.__evaluate__, *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj, y_obj): + """Perform a less than or equal to on int or float.""" + if isinstance(x_obj, (float, int)) and isinstance(y_obj, (float, int)): + return x_obj <= y_obj + return False + + +class GreaterThanBinaryOperatorExpression(BinaryOperatorExpression): + """Expression to handle greater than.""" + + def __init__(self, *args, **kwargs): + """Construct the binary operator with appropriate method.""" + super(GreaterThanBinaryOperatorExpression, self).__init__( + '>', GreaterThanBinaryOperatorExpression.__evaluate__, *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj, y_obj): + """Perform a greater than on int or float.""" + if isinstance(x_obj, (float, int)) and isinstance(y_obj, (float, int)): + return x_obj > y_obj + return False + + +class GreaterThanOrEqualToBinaryOperatorExpression(BinaryOperatorExpression): + """Expression to handle greater than or equal.""" + + def __init__(self, *args, **kwargs): + """Construct the binary operator with appropriate method.""" + super(GreaterThanOrEqualToBinaryOperatorExpression, self).__init__( + '>=', GreaterThanOrEqualToBinaryOperatorExpression.__evaluate__, *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj, y_obj): + """Perform a greater than or equal to on int or float.""" + if isinstance(x_obj, (float, int)) and isinstance(y_obj, (float, int)): + return x_obj >= y_obj + return False + + +class UnaryOperatorExpression(OperatorExpression): + """Unary operator expression base class.""" + + def __init__(self, token: str, callback: Callable[[bool], bool], expression: Expression): + """Save the callback operator the token and expression.""" + super(UnaryOperatorExpression, self).__init__() + self.token = token + self.callback = callback + self.expression = expression + + def __jsonpath__(self) -> Generator[str, None, None]: + """Generate the jsonpath for a unary operator.""" + yield self.token + yield ' ' + if isinstance(self.expression, (UnaryOperatorExpression, VariadicOperatorExpression)): + yield '(' + for expression_token in self.expression.__jsonpath__(): + yield expression_token + if isinstance(self.expression, (UnaryOperatorExpression, VariadicOperatorExpression)): + yield ')' + + def evaluate(self, root_value: object, current_value: object) -> bool: + """Evaluate the unary expression.""" + return self.callback(self.expression.evaluate(root_value, current_value)) + + +class NotUnaryOperatorExpression(UnaryOperatorExpression): + """Unary class to handle the 'not' expression.""" + + def __init__(self, *args, **kwargs): + """Call the unary operator expression with the right method.""" + super(NotUnaryOperatorExpression, self).__init__( + 'not', NotUnaryOperatorExpression.__evaluate__, *args, **kwargs) + + @staticmethod + def __evaluate__(x_obj): + """The unary not function.""" + return not x_obj + + +class VariadicOperatorExpression(OperatorExpression): + """Base class to handle boolean expressions of variadic type.""" + + def __init__(self, token: str, callback: Callable[[List[bool]], bool], expressions: List[Expression] = None): + """Save the operator token, callback and the list of expressions.""" + super(VariadicOperatorExpression, self).__init__() + self.token = token + self.callback = callback + self.expressions = expressions if expressions else [] + + def __jsonpath__(self) -> Generator[str, None, None]: + """Yield the string of the expression.""" + expressions_count = len(self.expressions) + if expressions_count == 0: + pass + elif expressions_count == 1: + for expression_token in self.expressions[0].__jsonpath__(): + yield expression_token + else: + for expression_index, expression in enumerate(self.expressions): + if expression_index > 0: + yield ' ' + yield self.token + yield ' ' + + if isinstance(expression, VariadicOperatorExpression): + yield '(' + + for expression_token in expression.__jsonpath__(): + yield expression_token + + if isinstance(expression, VariadicOperatorExpression): + yield ')' + + def evaluate(self, root_value: object, current_value: object) -> bool: + """Evaluate the expressions against the boolean callback.""" + return self.callback(map(lambda expression: expression.evaluate(root_value, current_value), self.expressions)) + + +class AndVariadicOperatorExpression(VariadicOperatorExpression): + """The boolean 'and' operator expression.""" + + def __init__(self, *args, **kwargs): + """Call the super with the 'and' boolean method.""" + super(AndVariadicOperatorExpression, self).__init__( + 'and', all, *args, **kwargs) + + +class OrVariadicOperatorExpression(VariadicOperatorExpression): + """The boolean 'or' operator expression.""" + + def __init__(self, *args, **kwargs): + """Call the super with the 'or' boolean method.""" + super(OrVariadicOperatorExpression, self).__init__( + 'or', any, *args, **kwargs) diff --git a/jsonpath2/expressions/some.py b/jsonpath2/expressions/some.py new file mode 100644 index 0000000..c128718 --- /dev/null +++ b/jsonpath2/expressions/some.py @@ -0,0 +1,25 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Some expression module.""" +from typing import Generator +from jsonpath2.expression import Expression +from jsonpath2.node import Node + + +class SomeExpression(Expression): + """The some expression class.""" + + def __init__(self, next_node: Node): + """Save the next node.""" + super(SomeExpression, self).__init__() + self.next_node = next_node + + def __jsonpath__(self) -> Generator[str, None, None]: + """Return the next nodes jsonpath.""" + return self.next_node.__jsonpath__() + + def evaluate(self, root_value: object, current_value: object) -> bool: + """Evaluate the next node.""" + for _next_node_match_data in self.next_node.match(root_value, current_value): + return True + return False diff --git a/jsonpath2/node.py b/jsonpath2/node.py new file mode 100644 index 0000000..d22a826 --- /dev/null +++ b/jsonpath2/node.py @@ -0,0 +1,39 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The parse tree node module.""" +from abc import abstractmethod +from typing import Generator +from jsonpath2.tojsonpath import ToJSONPath + + +# pylint: disable=too-few-public-methods +class MatchData(object): + """Match data object for storing node values.""" + + def __init__(self, node, root_value, current_value): + """Constructor to save root and current node values.""" + super(MatchData, self).__init__() + self.node = node + self.root_value = root_value + self.current_value = current_value + + def __eq__(self, other: object) -> bool: + """Test if two MatchData objects are the same.""" + return isinstance(other, self.__class__) and (self.__dict__ == other.__dict__) +# pylint: enable=too-few-public-methods + + +class Node(ToJSONPath): + """Node object for the jsonpath parsetree.""" + + def __eq__(self, other: object) -> bool: + """Determine if two Nodes are the same.""" + return isinstance(other, self.__class__) and (self.__dict__ == other.__dict__) + + @abstractmethod + def match( + self, + root_value: object, + current_value: object) -> Generator[MatchData, None, None]: # pragma: no cover abstract method. + """Abstract method to determine a node match.""" + raise NotImplementedError() diff --git a/jsonpath2/nodes/__init__.py b/jsonpath2/nodes/__init__.py new file mode 100644 index 0000000..378a3bd --- /dev/null +++ b/jsonpath2/nodes/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Nodes module contains all the node definitions.""" diff --git a/jsonpath2/nodes/current.py b/jsonpath2/nodes/current.py new file mode 100644 index 0000000..97621cf --- /dev/null +++ b/jsonpath2/nodes/current.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The current node module.""" +from typing import Generator +from jsonpath2.node import MatchData, Node + + +class CurrentNode(Node): + """Current node class to store current node info.""" + + def __init__(self, next_node: Node): + """Save the current node.""" + super(CurrentNode, self).__init__() + self.next_node = next_node + + def __jsonpath__(self) -> Generator[str, None, None]: + """Return the current node string.""" + yield '@' + for next_node_token in self.next_node.__jsonpath__(): + yield next_node_token + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the current value and root value.""" + return map( + lambda next_node_match_data: MatchData( + CurrentNode(next_node_match_data.node), + next_node_match_data.root_value, + next_node_match_data.current_value + ), + self.next_node.match(root_value, current_value) + ) diff --git a/jsonpath2/nodes/recursivedescent.py b/jsonpath2/nodes/recursivedescent.py new file mode 100644 index 0000000..140d733 --- /dev/null +++ b/jsonpath2/nodes/recursivedescent.py @@ -0,0 +1,32 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Recursive descent module.""" +import itertools +from typing import Generator +from jsonpath2.node import MatchData, Node +from jsonpath2.nodes.subscript import SubscriptNode +from jsonpath2.subscripts.wildcard import WildcardSubscript + + +class RecursiveDescentNode(Node): + """Recursive descent node class.""" + + def __init__(self, next_node: Node): + """Save the next node.""" + super(RecursiveDescentNode, self).__init__() + self.next_node = next_node + + def __jsonpath__(self) -> Generator[str, None, None]: + """Dump the string for the previous node.""" + yield '..' + for next_node_token in self.next_node.__jsonpath__(): + yield next_node_token + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the root value with the current value.""" + # NOTE Depth-first + return itertools.chain( + self.next_node.match(root_value, current_value), + SubscriptNode(self, [WildcardSubscript()]).match( + root_value, current_value) + ) diff --git a/jsonpath2/nodes/root.py b/jsonpath2/nodes/root.py new file mode 100644 index 0000000..729a49a --- /dev/null +++ b/jsonpath2/nodes/root.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Root node type.""" +from typing import Generator +from jsonpath2.node import MatchData, Node + + +class RootNode(Node): + """Root node to start the process.""" + + def __init__(self, next_node: Node): + """Save the next node object.""" + super(RootNode, self).__init__() + self.next_node = next_node + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the root value with the current value.""" + return map( + lambda next_node_match_data: MatchData( + RootNode(next_node_match_data.node), + next_node_match_data.root_value, + next_node_match_data.current_value + ), + self.next_node.match(root_value, root_value) + ) + + def __jsonpath__(self) -> Generator[str, None, None]: + """Yield the start character string and the next node.""" + yield '$' + for next_node_token in self.next_node.__jsonpath__(): + yield next_node_token diff --git a/jsonpath2/nodes/subscript.py b/jsonpath2/nodes/subscript.py new file mode 100644 index 0000000..099def4 --- /dev/null +++ b/jsonpath2/nodes/subscript.py @@ -0,0 +1,49 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The subscript module.""" +from typing import Generator, List +from jsonpath2.node import MatchData, Node +from jsonpath2.subscript import Subscript +from jsonpath2.nodes.terminal import TerminalNode + + +class SubscriptNode(Node): + """The subscript node class to handle '[]'.""" + + def __init__(self, next_node: Node, subscripts: List[Subscript] = None): + """Save the next node and subscripts.""" + super(SubscriptNode, self).__init__() + self.next_node = next_node + self.subscripts = subscripts if subscripts else [] + + def __jsonpath__(self) -> Generator[str, None, None]: + """Yield the subscript characters and subscript classes.""" + yield '[' + for subscript_index, subscript in enumerate(self.subscripts): + if subscript_index > 0: + yield ',' + for subscript_token in subscript.__jsonpath__(): + yield subscript_token + yield ']' + for next_node_token in self.next_node.__jsonpath__(): + yield next_node_token + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match root value and current value for subscripts.""" + for subscript in self.subscripts: + for subscript_match_data in subscript.match(root_value, current_value): + for next_node_match_data in self.next_node.match( + subscript_match_data.root_value, subscript_match_data.current_value): + if isinstance(subscript_match_data.node, TerminalNode): + yield next_node_match_data + elif isinstance(subscript_match_data.node, SubscriptNode): + if isinstance(subscript_match_data.node.next_node, TerminalNode): + yield MatchData( + SubscriptNode( + next_node_match_data.node, subscript_match_data.node.subscripts), + next_node_match_data.root_value, next_node_match_data.current_value + ) + else: + raise ValueError() + else: + raise ValueError() diff --git a/jsonpath2/nodes/terminal.py b/jsonpath2/nodes/terminal.py new file mode 100644 index 0000000..eade3aa --- /dev/null +++ b/jsonpath2/nodes/terminal.py @@ -0,0 +1,17 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Terminal node object.""" +from typing import Generator +from jsonpath2.node import MatchData, Node + + +class TerminalNode(Node): + """Terminal node class.""" + + def __jsonpath__(self) -> Generator[str, None, None]: + """Return the empty array not yield.""" + return [] + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match a termainal node.""" + return [MatchData(self, root_value, current_value)] diff --git a/jsonpath2/parser/JSONPath.g4 b/jsonpath2/parser/JSONPath.g4 new file mode 100644 index 0000000..f422697 --- /dev/null +++ b/jsonpath2/parser/JSONPath.g4 @@ -0,0 +1,157 @@ +grammar JSONPath; + +CURRENT_VALUE : '@' ; +RECURSIVE_DESCENT : '..' ; +ROOT_VALUE : '$' ; +SUBSCRIPT : '.' ; +WILDCARD_SUBSCRIPT : '*' ; + +AND : 'and' ; +EQ : '=' ; +GE : '>=' ; +GT : '>' ; +LE : '<=' ; +LT : '<' ; +NE : '!=' ; +NOT : 'not' ; +OR : 'or' ; + +TRUE : 'true' ; +FALSE : 'false' ; +NULL : 'null' ; + +BRACE_LEFT : '{' ; +BRACE_RIGHT : '}' ; +BRACKET_LEFT : '[' ; +BRACKET_RIGHT : ']' ; +COLON : ':' ; +COMMA : ',' ; +PAREN_LEFT : '(' ; +PAREN_RIGHT : ')' ; +QUESTION : '?' ; + +jsonpath + : ROOT_VALUE subscript? EOF + ; + +subscript + : RECURSIVE_DESCENT ( subscriptableBareword | subscriptables ) subscript? + | SUBSCRIPT subscriptableBareword subscript? + | subscriptables subscript? + ; + +subscriptables + : BRACKET_LEFT subscriptable ( COMMA subscriptable )* BRACKET_RIGHT + ; + +subscriptableBareword + : ID + | WILDCARD_SUBSCRIPT + ; + +subscriptable + : STRING + | NUMBER{self.tryCast(int)}? sliceable? + | sliceable + | WILDCARD_SUBSCRIPT + | QUESTION PAREN_LEFT expression PAREN_RIGHT + ; + +sliceable + : COLON ( NUMBER{self.tryCast(int)}? )? ( COLON NUMBER{self.tryCast(int)}? )? + ; + +expression + : andExpression + ; + +andExpression + : orExpression ( AND andExpression )? + ; + +orExpression + : notExpression ( OR orExpression )? + ; + +notExpression + : NOT notExpression + | PAREN_LEFT expression PAREN_RIGHT + | ( ROOT_VALUE | CURRENT_VALUE ) subscript? ( ( EQ | NE | LT | LE | GT | GE ) value )? + ; + + +ID + : [_A-Za-z] [_A-Za-z0-9]* + ; + + +/* c.f., https://github.com/antlr/grammars-v4/blob/master/json/JSON.g4 */ + +json + : value + ; + +obj + : BRACE_LEFT pair ( COMMA pair )* BRACE_RIGHT + | BRACE_LEFT BRACE_RIGHT + ; + +pair + : STRING COLON value + ; + +array + : BRACKET_LEFT value ( COMMA value )* BRACKET_RIGHT + | BRACKET_LEFT BRACKET_RIGHT + ; + +value + : STRING + | NUMBER + | obj + | array + | TRUE + | FALSE + | NULL + ; + + +STRING + : '"' (ESC | SAFECODEPOINT)* '"' + ; + + +fragment ESC + : '\\' (["\\/bfnrt] | UNICODE) + ; +fragment UNICODE + : 'u' HEX HEX HEX HEX + ; +fragment HEX + : [0-9a-fA-F] + ; +fragment SAFECODEPOINT + : ~ ["\\\u0000-\u001F] + ; + + +NUMBER + : '-'? INT ('.' [0-9] +)? EXP? + ; + + +fragment INT + : '0' | [1-9] [0-9]* + ; + +// no leading zeros + +fragment EXP + : [Ee] [+\-]? INT + ; + +// \- since - means "range" inside [...] + +WS + : [ \t\n\r] + -> skip + ; diff --git a/jsonpath2/parser/JSONPath.interp b/jsonpath2/parser/JSONPath.interp new file mode 100644 index 0000000..35da05e --- /dev/null +++ b/jsonpath2/parser/JSONPath.interp @@ -0,0 +1,86 @@ +token literal names: +null +'@' +'..' +'$' +'.' +'*' +'and' +'=' +'>=' +'>' +'<=' +'<' +'!=' +'not' +'or' +'true' +'false' +'null' +'{' +'}' +'[' +']' +':' +',' +'(' +')' +'?' +null +null +null +null + +token symbolic names: +null +CURRENT_VALUE +RECURSIVE_DESCENT +ROOT_VALUE +SUBSCRIPT +WILDCARD_SUBSCRIPT +AND +EQ +GE +GT +LE +LT +NE +NOT +OR +TRUE +FALSE +NULL +BRACE_LEFT +BRACE_RIGHT +BRACKET_LEFT +BRACKET_RIGHT +COLON +COMMA +PAREN_LEFT +PAREN_RIGHT +QUESTION +ID +STRING +NUMBER +WS + +rule names: +jsonpath +subscript +subscriptables +subscriptableBareword +subscriptable +sliceable +expression +andExpression +orExpression +notExpression +json +obj +pair +array +value + + +atn: +[3, 24715, 42794, 33075, 47597, 16764, 15335, 30598, 22884, 3, 32, 169, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 3, 2, 3, 2, 5, 2, 35, 10, 2, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 5, 3, 42, 10, 3, 3, 3, 5, 3, 45, 10, 3, 3, 3, 3, 3, 3, 3, 5, 3, 50, 10, 3, 3, 3, 3, 3, 5, 3, 54, 10, 3, 5, 3, 56, 10, 3, 3, 4, 3, 4, 3, 4, 3, 4, 7, 4, 62, 10, 4, 12, 4, 14, 4, 65, 11, 4, 3, 4, 3, 4, 3, 5, 3, 5, 3, 6, 3, 6, 3, 6, 3, 6, 5, 6, 75, 10, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 3, 6, 5, 6, 84, 10, 6, 3, 7, 3, 7, 3, 7, 5, 7, 89, 10, 7, 3, 7, 3, 7, 3, 7, 5, 7, 94, 10, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 5, 9, 101, 10, 9, 3, 10, 3, 10, 3, 10, 5, 10, 106, 10, 10, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 3, 11, 5, 11, 116, 10, 11, 3, 11, 3, 11, 5, 11, 120, 10, 11, 5, 11, 122, 10, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 13, 7, 13, 130, 10, 13, 12, 13, 14, 13, 133, 11, 13, 3, 13, 3, 13, 3, 13, 3, 13, 5, 13, 139, 10, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 15, 7, 15, 149, 10, 15, 12, 15, 14, 15, 152, 11, 15, 3, 15, 3, 15, 3, 15, 3, 15, 5, 15, 158, 10, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 5, 16, 167, 10, 16, 3, 16, 2, 2, 17, 2, 4, 6, 8, 10, 12, 14, 16, 18, 20, 22, 24, 26, 28, 30, 2, 5, 4, 2, 7, 7, 29, 29, 4, 2, 3, 3, 5, 5, 3, 2, 9, 14, 2, 184, 2, 32, 3, 2, 2, 2, 4, 55, 3, 2, 2, 2, 6, 57, 3, 2, 2, 2, 8, 68, 3, 2, 2, 2, 10, 83, 3, 2, 2, 2, 12, 85, 3, 2, 2, 2, 14, 95, 3, 2, 2, 2, 16, 97, 3, 2, 2, 2, 18, 102, 3, 2, 2, 2, 20, 121, 3, 2, 2, 2, 22, 123, 3, 2, 2, 2, 24, 138, 3, 2, 2, 2, 26, 140, 3, 2, 2, 2, 28, 157, 3, 2, 2, 2, 30, 166, 3, 2, 2, 2, 32, 34, 7, 5, 2, 2, 33, 35, 5, 4, 3, 2, 34, 33, 3, 2, 2, 2, 34, 35, 3, 2, 2, 2, 35, 36, 3, 2, 2, 2, 36, 37, 7, 2, 2, 3, 37, 3, 3, 2, 2, 2, 38, 41, 7, 4, 2, 2, 39, 42, 5, 8, 5, 2, 40, 42, 5, 6, 4, 2, 41, 39, 3, 2, 2, 2, 41, 40, 3, 2, 2, 2, 42, 44, 3, 2, 2, 2, 43, 45, 5, 4, 3, 2, 44, 43, 3, 2, 2, 2, 44, 45, 3, 2, 2, 2, 45, 56, 3, 2, 2, 2, 46, 47, 7, 6, 2, 2, 47, 49, 5, 8, 5, 2, 48, 50, 5, 4, 3, 2, 49, 48, 3, 2, 2, 2, 49, 50, 3, 2, 2, 2, 50, 56, 3, 2, 2, 2, 51, 53, 5, 6, 4, 2, 52, 54, 5, 4, 3, 2, 53, 52, 3, 2, 2, 2, 53, 54, 3, 2, 2, 2, 54, 56, 3, 2, 2, 2, 55, 38, 3, 2, 2, 2, 55, 46, 3, 2, 2, 2, 55, 51, 3, 2, 2, 2, 56, 5, 3, 2, 2, 2, 57, 58, 7, 22, 2, 2, 58, 63, 5, 10, 6, 2, 59, 60, 7, 25, 2, 2, 60, 62, 5, 10, 6, 2, 61, 59, 3, 2, 2, 2, 62, 65, 3, 2, 2, 2, 63, 61, 3, 2, 2, 2, 63, 64, 3, 2, 2, 2, 64, 66, 3, 2, 2, 2, 65, 63, 3, 2, 2, 2, 66, 67, 7, 23, 2, 2, 67, 7, 3, 2, 2, 2, 68, 69, 9, 2, 2, 2, 69, 9, 3, 2, 2, 2, 70, 84, 7, 30, 2, 2, 71, 72, 7, 31, 2, 2, 72, 74, 6, 6, 2, 2, 73, 75, 5, 12, 7, 2, 74, 73, 3, 2, 2, 2, 74, 75, 3, 2, 2, 2, 75, 84, 3, 2, 2, 2, 76, 84, 5, 12, 7, 2, 77, 84, 7, 7, 2, 2, 78, 79, 7, 28, 2, 2, 79, 80, 7, 26, 2, 2, 80, 81, 5, 14, 8, 2, 81, 82, 7, 27, 2, 2, 82, 84, 3, 2, 2, 2, 83, 70, 3, 2, 2, 2, 83, 71, 3, 2, 2, 2, 83, 76, 3, 2, 2, 2, 83, 77, 3, 2, 2, 2, 83, 78, 3, 2, 2, 2, 84, 11, 3, 2, 2, 2, 85, 88, 7, 24, 2, 2, 86, 87, 7, 31, 2, 2, 87, 89, 6, 7, 3, 2, 88, 86, 3, 2, 2, 2, 88, 89, 3, 2, 2, 2, 89, 93, 3, 2, 2, 2, 90, 91, 7, 24, 2, 2, 91, 92, 7, 31, 2, 2, 92, 94, 6, 7, 4, 2, 93, 90, 3, 2, 2, 2, 93, 94, 3, 2, 2, 2, 94, 13, 3, 2, 2, 2, 95, 96, 5, 16, 9, 2, 96, 15, 3, 2, 2, 2, 97, 100, 5, 18, 10, 2, 98, 99, 7, 8, 2, 2, 99, 101, 5, 16, 9, 2, 100, 98, 3, 2, 2, 2, 100, 101, 3, 2, 2, 2, 101, 17, 3, 2, 2, 2, 102, 105, 5, 20, 11, 2, 103, 104, 7, 16, 2, 2, 104, 106, 5, 18, 10, 2, 105, 103, 3, 2, 2, 2, 105, 106, 3, 2, 2, 2, 106, 19, 3, 2, 2, 2, 107, 108, 7, 15, 2, 2, 108, 122, 5, 20, 11, 2, 109, 110, 7, 26, 2, 2, 110, 111, 5, 14, 8, 2, 111, 112, 7, 27, 2, 2, 112, 122, 3, 2, 2, 2, 113, 115, 9, 3, 2, 2, 114, 116, 5, 4, 3, 2, 115, 114, 3, 2, 2, 2, 115, 116, 3, 2, 2, 2, 116, 119, 3, 2, 2, 2, 117, 118, 9, 4, 2, 2, 118, 120, 5, 30, 16, 2, 119, 117, 3, 2, 2, 2, 119, 120, 3, 2, 2, 2, 120, 122, 3, 2, 2, 2, 121, 107, 3, 2, 2, 2, 121, 109, 3, 2, 2, 2, 121, 113, 3, 2, 2, 2, 122, 21, 3, 2, 2, 2, 123, 124, 5, 30, 16, 2, 124, 23, 3, 2, 2, 2, 125, 126, 7, 20, 2, 2, 126, 131, 5, 26, 14, 2, 127, 128, 7, 25, 2, 2, 128, 130, 5, 26, 14, 2, 129, 127, 3, 2, 2, 2, 130, 133, 3, 2, 2, 2, 131, 129, 3, 2, 2, 2, 131, 132, 3, 2, 2, 2, 132, 134, 3, 2, 2, 2, 133, 131, 3, 2, 2, 2, 134, 135, 7, 21, 2, 2, 135, 139, 3, 2, 2, 2, 136, 137, 7, 20, 2, 2, 137, 139, 7, 21, 2, 2, 138, 125, 3, 2, 2, 2, 138, 136, 3, 2, 2, 2, 139, 25, 3, 2, 2, 2, 140, 141, 7, 30, 2, 2, 141, 142, 7, 24, 2, 2, 142, 143, 5, 30, 16, 2, 143, 27, 3, 2, 2, 2, 144, 145, 7, 22, 2, 2, 145, 150, 5, 30, 16, 2, 146, 147, 7, 25, 2, 2, 147, 149, 5, 30, 16, 2, 148, 146, 3, 2, 2, 2, 149, 152, 3, 2, 2, 2, 150, 148, 3, 2, 2, 2, 150, 151, 3, 2, 2, 2, 151, 153, 3, 2, 2, 2, 152, 150, 3, 2, 2, 2, 153, 154, 7, 23, 2, 2, 154, 158, 3, 2, 2, 2, 155, 156, 7, 22, 2, 2, 156, 158, 7, 23, 2, 2, 157, 144, 3, 2, 2, 2, 157, 155, 3, 2, 2, 2, 158, 29, 3, 2, 2, 2, 159, 167, 7, 30, 2, 2, 160, 167, 7, 31, 2, 2, 161, 167, 5, 24, 13, 2, 162, 167, 5, 28, 15, 2, 163, 167, 7, 17, 2, 2, 164, 167, 7, 18, 2, 2, 165, 167, 7, 19, 2, 2, 166, 159, 3, 2, 2, 2, 166, 160, 3, 2, 2, 2, 166, 161, 3, 2, 2, 2, 166, 162, 3, 2, 2, 2, 166, 163, 3, 2, 2, 2, 166, 164, 3, 2, 2, 2, 166, 165, 3, 2, 2, 2, 167, 31, 3, 2, 2, 2, 23, 34, 41, 44, 49, 53, 55, 63, 74, 83, 88, 93, 100, 105, 115, 119, 121, 131, 138, 150, 157, 166] \ No newline at end of file diff --git a/jsonpath2/parser/JSONPath.tokens b/jsonpath2/parser/JSONPath.tokens new file mode 100644 index 0000000..37a8444 --- /dev/null +++ b/jsonpath2/parser/JSONPath.tokens @@ -0,0 +1,56 @@ +CURRENT_VALUE=1 +RECURSIVE_DESCENT=2 +ROOT_VALUE=3 +SUBSCRIPT=4 +WILDCARD_SUBSCRIPT=5 +AND=6 +EQ=7 +GE=8 +GT=9 +LE=10 +LT=11 +NE=12 +NOT=13 +OR=14 +TRUE=15 +FALSE=16 +NULL=17 +BRACE_LEFT=18 +BRACE_RIGHT=19 +BRACKET_LEFT=20 +BRACKET_RIGHT=21 +COLON=22 +COMMA=23 +PAREN_LEFT=24 +PAREN_RIGHT=25 +QUESTION=26 +ID=27 +STRING=28 +NUMBER=29 +WS=30 +'@'=1 +'..'=2 +'$'=3 +'.'=4 +'*'=5 +'and'=6 +'='=7 +'>='=8 +'>'=9 +'<='=10 +'<'=11 +'!='=12 +'not'=13 +'or'=14 +'true'=15 +'false'=16 +'null'=17 +'{'=18 +'}'=19 +'['=20 +']'=21 +':'=22 +','=23 +'('=24 +')'=25 +'?'=26 diff --git a/jsonpath2/parser/JSONPathLexer.interp b/jsonpath2/parser/JSONPathLexer.interp new file mode 100644 index 0000000..6fbc9ab --- /dev/null +++ b/jsonpath2/parser/JSONPathLexer.interp @@ -0,0 +1,113 @@ +token literal names: +null +'@' +'..' +'$' +'.' +'*' +'and' +'=' +'>=' +'>' +'<=' +'<' +'!=' +'not' +'or' +'true' +'false' +'null' +'{' +'}' +'[' +']' +':' +',' +'(' +')' +'?' +null +null +null +null + +token symbolic names: +null +CURRENT_VALUE +RECURSIVE_DESCENT +ROOT_VALUE +SUBSCRIPT +WILDCARD_SUBSCRIPT +AND +EQ +GE +GT +LE +LT +NE +NOT +OR +TRUE +FALSE +NULL +BRACE_LEFT +BRACE_RIGHT +BRACKET_LEFT +BRACKET_RIGHT +COLON +COMMA +PAREN_LEFT +PAREN_RIGHT +QUESTION +ID +STRING +NUMBER +WS + +rule names: +CURRENT_VALUE +RECURSIVE_DESCENT +ROOT_VALUE +SUBSCRIPT +WILDCARD_SUBSCRIPT +AND +EQ +GE +GT +LE +LT +NE +NOT +OR +TRUE +FALSE +NULL +BRACE_LEFT +BRACE_RIGHT +BRACKET_LEFT +BRACKET_RIGHT +COLON +COMMA +PAREN_LEFT +PAREN_RIGHT +QUESTION +ID +STRING +ESC +UNICODE +HEX +SAFECODEPOINT +NUMBER +INT +EXP +WS + +channel names: +DEFAULT_TOKEN_CHANNEL +HIDDEN + +mode names: +DEFAULT_MODE + +atn: +[3, 24715, 42794, 33075, 47597, 16764, 15335, 30598, 22884, 2, 32, 216, 8, 1, 4, 2, 9, 2, 4, 3, 9, 3, 4, 4, 9, 4, 4, 5, 9, 5, 4, 6, 9, 6, 4, 7, 9, 7, 4, 8, 9, 8, 4, 9, 9, 9, 4, 10, 9, 10, 4, 11, 9, 11, 4, 12, 9, 12, 4, 13, 9, 13, 4, 14, 9, 14, 4, 15, 9, 15, 4, 16, 9, 16, 4, 17, 9, 17, 4, 18, 9, 18, 4, 19, 9, 19, 4, 20, 9, 20, 4, 21, 9, 21, 4, 22, 9, 22, 4, 23, 9, 23, 4, 24, 9, 24, 4, 25, 9, 25, 4, 26, 9, 26, 4, 27, 9, 27, 4, 28, 9, 28, 4, 29, 9, 29, 4, 30, 9, 30, 4, 31, 9, 31, 4, 32, 9, 32, 4, 33, 9, 33, 4, 34, 9, 34, 4, 35, 9, 35, 4, 36, 9, 36, 4, 37, 9, 37, 3, 2, 3, 2, 3, 3, 3, 3, 3, 3, 3, 4, 3, 4, 3, 5, 3, 5, 3, 6, 3, 6, 3, 7, 3, 7, 3, 7, 3, 7, 3, 8, 3, 8, 3, 9, 3, 9, 3, 9, 3, 10, 3, 10, 3, 11, 3, 11, 3, 11, 3, 12, 3, 12, 3, 13, 3, 13, 3, 13, 3, 14, 3, 14, 3, 14, 3, 14, 3, 15, 3, 15, 3, 15, 3, 16, 3, 16, 3, 16, 3, 16, 3, 16, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 17, 3, 18, 3, 18, 3, 18, 3, 18, 3, 18, 3, 19, 3, 19, 3, 20, 3, 20, 3, 21, 3, 21, 3, 22, 3, 22, 3, 23, 3, 23, 3, 24, 3, 24, 3, 25, 3, 25, 3, 26, 3, 26, 3, 27, 3, 27, 3, 28, 3, 28, 7, 28, 149, 10, 28, 12, 28, 14, 28, 152, 11, 28, 3, 29, 3, 29, 3, 29, 7, 29, 157, 10, 29, 12, 29, 14, 29, 160, 11, 29, 3, 29, 3, 29, 3, 30, 3, 30, 3, 30, 5, 30, 167, 10, 30, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 31, 3, 32, 3, 32, 3, 33, 3, 33, 3, 34, 5, 34, 180, 10, 34, 3, 34, 3, 34, 3, 34, 6, 34, 185, 10, 34, 13, 34, 14, 34, 186, 5, 34, 189, 10, 34, 3, 34, 5, 34, 192, 10, 34, 3, 35, 3, 35, 3, 35, 7, 35, 197, 10, 35, 12, 35, 14, 35, 200, 11, 35, 5, 35, 202, 10, 35, 3, 36, 3, 36, 5, 36, 206, 10, 36, 3, 36, 3, 36, 3, 37, 6, 37, 211, 10, 37, 13, 37, 14, 37, 212, 3, 37, 3, 37, 2, 2, 38, 3, 3, 5, 4, 7, 5, 9, 6, 11, 7, 13, 8, 15, 9, 17, 10, 19, 11, 21, 12, 23, 13, 25, 14, 27, 15, 29, 16, 31, 17, 33, 18, 35, 19, 37, 20, 39, 21, 41, 22, 43, 23, 45, 24, 47, 25, 49, 26, 51, 27, 53, 28, 55, 29, 57, 30, 59, 2, 61, 2, 63, 2, 65, 2, 67, 31, 69, 2, 71, 2, 73, 32, 3, 2, 12, 5, 2, 67, 92, 97, 97, 99, 124, 6, 2, 50, 59, 67, 92, 97, 97, 99, 124, 10, 2, 36, 36, 49, 49, 94, 94, 100, 100, 104, 104, 112, 112, 116, 116, 118, 118, 5, 2, 50, 59, 67, 72, 99, 104, 5, 2, 2, 33, 36, 36, 94, 94, 3, 2, 50, 59, 3, 2, 51, 59, 4, 2, 71, 71, 103, 103, 4, 2, 45, 45, 47, 47, 5, 2, 11, 12, 15, 15, 34, 34, 2, 221, 2, 3, 3, 2, 2, 2, 2, 5, 3, 2, 2, 2, 2, 7, 3, 2, 2, 2, 2, 9, 3, 2, 2, 2, 2, 11, 3, 2, 2, 2, 2, 13, 3, 2, 2, 2, 2, 15, 3, 2, 2, 2, 2, 17, 3, 2, 2, 2, 2, 19, 3, 2, 2, 2, 2, 21, 3, 2, 2, 2, 2, 23, 3, 2, 2, 2, 2, 25, 3, 2, 2, 2, 2, 27, 3, 2, 2, 2, 2, 29, 3, 2, 2, 2, 2, 31, 3, 2, 2, 2, 2, 33, 3, 2, 2, 2, 2, 35, 3, 2, 2, 2, 2, 37, 3, 2, 2, 2, 2, 39, 3, 2, 2, 2, 2, 41, 3, 2, 2, 2, 2, 43, 3, 2, 2, 2, 2, 45, 3, 2, 2, 2, 2, 47, 3, 2, 2, 2, 2, 49, 3, 2, 2, 2, 2, 51, 3, 2, 2, 2, 2, 53, 3, 2, 2, 2, 2, 55, 3, 2, 2, 2, 2, 57, 3, 2, 2, 2, 2, 67, 3, 2, 2, 2, 2, 73, 3, 2, 2, 2, 3, 75, 3, 2, 2, 2, 5, 77, 3, 2, 2, 2, 7, 80, 3, 2, 2, 2, 9, 82, 3, 2, 2, 2, 11, 84, 3, 2, 2, 2, 13, 86, 3, 2, 2, 2, 15, 90, 3, 2, 2, 2, 17, 92, 3, 2, 2, 2, 19, 95, 3, 2, 2, 2, 21, 97, 3, 2, 2, 2, 23, 100, 3, 2, 2, 2, 25, 102, 3, 2, 2, 2, 27, 105, 3, 2, 2, 2, 29, 109, 3, 2, 2, 2, 31, 112, 3, 2, 2, 2, 33, 117, 3, 2, 2, 2, 35, 123, 3, 2, 2, 2, 37, 128, 3, 2, 2, 2, 39, 130, 3, 2, 2, 2, 41, 132, 3, 2, 2, 2, 43, 134, 3, 2, 2, 2, 45, 136, 3, 2, 2, 2, 47, 138, 3, 2, 2, 2, 49, 140, 3, 2, 2, 2, 51, 142, 3, 2, 2, 2, 53, 144, 3, 2, 2, 2, 55, 146, 3, 2, 2, 2, 57, 153, 3, 2, 2, 2, 59, 163, 3, 2, 2, 2, 61, 168, 3, 2, 2, 2, 63, 174, 3, 2, 2, 2, 65, 176, 3, 2, 2, 2, 67, 179, 3, 2, 2, 2, 69, 201, 3, 2, 2, 2, 71, 203, 3, 2, 2, 2, 73, 210, 3, 2, 2, 2, 75, 76, 7, 66, 2, 2, 76, 4, 3, 2, 2, 2, 77, 78, 7, 48, 2, 2, 78, 79, 7, 48, 2, 2, 79, 6, 3, 2, 2, 2, 80, 81, 7, 38, 2, 2, 81, 8, 3, 2, 2, 2, 82, 83, 7, 48, 2, 2, 83, 10, 3, 2, 2, 2, 84, 85, 7, 44, 2, 2, 85, 12, 3, 2, 2, 2, 86, 87, 7, 99, 2, 2, 87, 88, 7, 112, 2, 2, 88, 89, 7, 102, 2, 2, 89, 14, 3, 2, 2, 2, 90, 91, 7, 63, 2, 2, 91, 16, 3, 2, 2, 2, 92, 93, 7, 64, 2, 2, 93, 94, 7, 63, 2, 2, 94, 18, 3, 2, 2, 2, 95, 96, 7, 64, 2, 2, 96, 20, 3, 2, 2, 2, 97, 98, 7, 62, 2, 2, 98, 99, 7, 63, 2, 2, 99, 22, 3, 2, 2, 2, 100, 101, 7, 62, 2, 2, 101, 24, 3, 2, 2, 2, 102, 103, 7, 35, 2, 2, 103, 104, 7, 63, 2, 2, 104, 26, 3, 2, 2, 2, 105, 106, 7, 112, 2, 2, 106, 107, 7, 113, 2, 2, 107, 108, 7, 118, 2, 2, 108, 28, 3, 2, 2, 2, 109, 110, 7, 113, 2, 2, 110, 111, 7, 116, 2, 2, 111, 30, 3, 2, 2, 2, 112, 113, 7, 118, 2, 2, 113, 114, 7, 116, 2, 2, 114, 115, 7, 119, 2, 2, 115, 116, 7, 103, 2, 2, 116, 32, 3, 2, 2, 2, 117, 118, 7, 104, 2, 2, 118, 119, 7, 99, 2, 2, 119, 120, 7, 110, 2, 2, 120, 121, 7, 117, 2, 2, 121, 122, 7, 103, 2, 2, 122, 34, 3, 2, 2, 2, 123, 124, 7, 112, 2, 2, 124, 125, 7, 119, 2, 2, 125, 126, 7, 110, 2, 2, 126, 127, 7, 110, 2, 2, 127, 36, 3, 2, 2, 2, 128, 129, 7, 125, 2, 2, 129, 38, 3, 2, 2, 2, 130, 131, 7, 127, 2, 2, 131, 40, 3, 2, 2, 2, 132, 133, 7, 93, 2, 2, 133, 42, 3, 2, 2, 2, 134, 135, 7, 95, 2, 2, 135, 44, 3, 2, 2, 2, 136, 137, 7, 60, 2, 2, 137, 46, 3, 2, 2, 2, 138, 139, 7, 46, 2, 2, 139, 48, 3, 2, 2, 2, 140, 141, 7, 42, 2, 2, 141, 50, 3, 2, 2, 2, 142, 143, 7, 43, 2, 2, 143, 52, 3, 2, 2, 2, 144, 145, 7, 65, 2, 2, 145, 54, 3, 2, 2, 2, 146, 150, 9, 2, 2, 2, 147, 149, 9, 3, 2, 2, 148, 147, 3, 2, 2, 2, 149, 152, 3, 2, 2, 2, 150, 148, 3, 2, 2, 2, 150, 151, 3, 2, 2, 2, 151, 56, 3, 2, 2, 2, 152, 150, 3, 2, 2, 2, 153, 158, 7, 36, 2, 2, 154, 157, 5, 59, 30, 2, 155, 157, 5, 65, 33, 2, 156, 154, 3, 2, 2, 2, 156, 155, 3, 2, 2, 2, 157, 160, 3, 2, 2, 2, 158, 156, 3, 2, 2, 2, 158, 159, 3, 2, 2, 2, 159, 161, 3, 2, 2, 2, 160, 158, 3, 2, 2, 2, 161, 162, 7, 36, 2, 2, 162, 58, 3, 2, 2, 2, 163, 166, 7, 94, 2, 2, 164, 167, 9, 4, 2, 2, 165, 167, 5, 61, 31, 2, 166, 164, 3, 2, 2, 2, 166, 165, 3, 2, 2, 2, 167, 60, 3, 2, 2, 2, 168, 169, 7, 119, 2, 2, 169, 170, 5, 63, 32, 2, 170, 171, 5, 63, 32, 2, 171, 172, 5, 63, 32, 2, 172, 173, 5, 63, 32, 2, 173, 62, 3, 2, 2, 2, 174, 175, 9, 5, 2, 2, 175, 64, 3, 2, 2, 2, 176, 177, 10, 6, 2, 2, 177, 66, 3, 2, 2, 2, 178, 180, 7, 47, 2, 2, 179, 178, 3, 2, 2, 2, 179, 180, 3, 2, 2, 2, 180, 181, 3, 2, 2, 2, 181, 188, 5, 69, 35, 2, 182, 184, 7, 48, 2, 2, 183, 185, 9, 7, 2, 2, 184, 183, 3, 2, 2, 2, 185, 186, 3, 2, 2, 2, 186, 184, 3, 2, 2, 2, 186, 187, 3, 2, 2, 2, 187, 189, 3, 2, 2, 2, 188, 182, 3, 2, 2, 2, 188, 189, 3, 2, 2, 2, 189, 191, 3, 2, 2, 2, 190, 192, 5, 71, 36, 2, 191, 190, 3, 2, 2, 2, 191, 192, 3, 2, 2, 2, 192, 68, 3, 2, 2, 2, 193, 202, 7, 50, 2, 2, 194, 198, 9, 8, 2, 2, 195, 197, 9, 7, 2, 2, 196, 195, 3, 2, 2, 2, 197, 200, 3, 2, 2, 2, 198, 196, 3, 2, 2, 2, 198, 199, 3, 2, 2, 2, 199, 202, 3, 2, 2, 2, 200, 198, 3, 2, 2, 2, 201, 193, 3, 2, 2, 2, 201, 194, 3, 2, 2, 2, 202, 70, 3, 2, 2, 2, 203, 205, 9, 9, 2, 2, 204, 206, 9, 10, 2, 2, 205, 204, 3, 2, 2, 2, 205, 206, 3, 2, 2, 2, 206, 207, 3, 2, 2, 2, 207, 208, 5, 69, 35, 2, 208, 72, 3, 2, 2, 2, 209, 211, 9, 11, 2, 2, 210, 209, 3, 2, 2, 2, 211, 212, 3, 2, 2, 2, 212, 210, 3, 2, 2, 2, 212, 213, 3, 2, 2, 2, 213, 214, 3, 2, 2, 2, 214, 215, 8, 37, 2, 2, 215, 74, 3, 2, 2, 2, 15, 2, 150, 156, 158, 166, 179, 186, 188, 191, 198, 201, 205, 212, 3, 8, 2, 2] \ No newline at end of file diff --git a/jsonpath2/parser/JSONPathLexer.py b/jsonpath2/parser/JSONPathLexer.py new file mode 100644 index 0000000..8cea2f0 --- /dev/null +++ b/jsonpath2/parser/JSONPathLexer.py @@ -0,0 +1,172 @@ +# Generated from jsonpath2/parser/JSONPath.g4 by ANTLR 4.7.1 +from antlr4 import * +from io import StringIO +from typing.io import TextIO +import sys + + +def serializedATN(): + with StringIO() as buf: + buf.write("\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\2 ") + buf.write("\u00d8\b\1\4\2\t\2\4\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7") + buf.write("\t\7\4\b\t\b\4\t\t\t\4\n\t\n\4\13\t\13\4\f\t\f\4\r\t\r") + buf.write("\4\16\t\16\4\17\t\17\4\20\t\20\4\21\t\21\4\22\t\22\4\23") + buf.write("\t\23\4\24\t\24\4\25\t\25\4\26\t\26\4\27\t\27\4\30\t\30") + buf.write("\4\31\t\31\4\32\t\32\4\33\t\33\4\34\t\34\4\35\t\35\4\36") + buf.write("\t\36\4\37\t\37\4 \t \4!\t!\4\"\t\"\4#\t#\4$\t$\4%\t%") + buf.write("\3\2\3\2\3\3\3\3\3\3\3\4\3\4\3\5\3\5\3\6\3\6\3\7\3\7\3") + buf.write("\7\3\7\3\b\3\b\3\t\3\t\3\t\3\n\3\n\3\13\3\13\3\13\3\f") + buf.write("\3\f\3\r\3\r\3\r\3\16\3\16\3\16\3\16\3\17\3\17\3\17\3") + buf.write("\20\3\20\3\20\3\20\3\20\3\21\3\21\3\21\3\21\3\21\3\21") + buf.write("\3\22\3\22\3\22\3\22\3\22\3\23\3\23\3\24\3\24\3\25\3\25") + buf.write("\3\26\3\26\3\27\3\27\3\30\3\30\3\31\3\31\3\32\3\32\3\33") + buf.write("\3\33\3\34\3\34\7\34\u0095\n\34\f\34\16\34\u0098\13\34") + buf.write("\3\35\3\35\3\35\7\35\u009d\n\35\f\35\16\35\u00a0\13\35") + buf.write("\3\35\3\35\3\36\3\36\3\36\5\36\u00a7\n\36\3\37\3\37\3") + buf.write("\37\3\37\3\37\3\37\3 \3 \3!\3!\3\"\5\"\u00b4\n\"\3\"\3") + buf.write("\"\3\"\6\"\u00b9\n\"\r\"\16\"\u00ba\5\"\u00bd\n\"\3\"") + buf.write("\5\"\u00c0\n\"\3#\3#\3#\7#\u00c5\n#\f#\16#\u00c8\13#\5") + buf.write("#\u00ca\n#\3$\3$\5$\u00ce\n$\3$\3$\3%\6%\u00d3\n%\r%\16") + buf.write("%\u00d4\3%\3%\2\2&\3\3\5\4\7\5\t\6\13\7\r\b\17\t\21\n") + buf.write("\23\13\25\f\27\r\31\16\33\17\35\20\37\21!\22#\23%\24\'") + buf.write("\25)\26+\27-\30/\31\61\32\63\33\65\34\67\359\36;\2=\2") + buf.write("?\2A\2C\37E\2G\2I \3\2\f\5\2C\\aac|\6\2\62;C\\aac|\n\2") + buf.write("$$\61\61^^ddhhppttvv\5\2\62;CHch\5\2\2!$$^^\3\2\62;\3") + buf.write("\2\63;\4\2GGgg\4\2--//\5\2\13\f\17\17\"\"\2\u00dd\2\3") + buf.write("\3\2\2\2\2\5\3\2\2\2\2\7\3\2\2\2\2\t\3\2\2\2\2\13\3\2") + buf.write("\2\2\2\r\3\2\2\2\2\17\3\2\2\2\2\21\3\2\2\2\2\23\3\2\2") + buf.write("\2\2\25\3\2\2\2\2\27\3\2\2\2\2\31\3\2\2\2\2\33\3\2\2\2") + buf.write("\2\35\3\2\2\2\2\37\3\2\2\2\2!\3\2\2\2\2#\3\2\2\2\2%\3") + buf.write("\2\2\2\2\'\3\2\2\2\2)\3\2\2\2\2+\3\2\2\2\2-\3\2\2\2\2") + buf.write("/\3\2\2\2\2\61\3\2\2\2\2\63\3\2\2\2\2\65\3\2\2\2\2\67") + buf.write("\3\2\2\2\29\3\2\2\2\2C\3\2\2\2\2I\3\2\2\2\3K\3\2\2\2\5") + buf.write("M\3\2\2\2\7P\3\2\2\2\tR\3\2\2\2\13T\3\2\2\2\rV\3\2\2\2") + buf.write("\17Z\3\2\2\2\21\\\3\2\2\2\23_\3\2\2\2\25a\3\2\2\2\27d") + buf.write("\3\2\2\2\31f\3\2\2\2\33i\3\2\2\2\35m\3\2\2\2\37p\3\2\2") + buf.write("\2!u\3\2\2\2#{\3\2\2\2%\u0080\3\2\2\2\'\u0082\3\2\2\2") + buf.write(")\u0084\3\2\2\2+\u0086\3\2\2\2-\u0088\3\2\2\2/\u008a\3") + buf.write("\2\2\2\61\u008c\3\2\2\2\63\u008e\3\2\2\2\65\u0090\3\2") + buf.write("\2\2\67\u0092\3\2\2\29\u0099\3\2\2\2;\u00a3\3\2\2\2=\u00a8") + buf.write("\3\2\2\2?\u00ae\3\2\2\2A\u00b0\3\2\2\2C\u00b3\3\2\2\2") + buf.write("E\u00c9\3\2\2\2G\u00cb\3\2\2\2I\u00d2\3\2\2\2KL\7B\2\2") + buf.write("L\4\3\2\2\2MN\7\60\2\2NO\7\60\2\2O\6\3\2\2\2PQ\7&\2\2") + buf.write("Q\b\3\2\2\2RS\7\60\2\2S\n\3\2\2\2TU\7,\2\2U\f\3\2\2\2") + buf.write("VW\7c\2\2WX\7p\2\2XY\7f\2\2Y\16\3\2\2\2Z[\7?\2\2[\20\3") + buf.write("\2\2\2\\]\7@\2\2]^\7?\2\2^\22\3\2\2\2_`\7@\2\2`\24\3\2") + buf.write("\2\2ab\7>\2\2bc\7?\2\2c\26\3\2\2\2de\7>\2\2e\30\3\2\2") + buf.write("\2fg\7#\2\2gh\7?\2\2h\32\3\2\2\2ij\7p\2\2jk\7q\2\2kl\7") + buf.write("v\2\2l\34\3\2\2\2mn\7q\2\2no\7t\2\2o\36\3\2\2\2pq\7v\2") + buf.write("\2qr\7t\2\2rs\7w\2\2st\7g\2\2t \3\2\2\2uv\7h\2\2vw\7c") + buf.write("\2\2wx\7n\2\2xy\7u\2\2yz\7g\2\2z\"\3\2\2\2{|\7p\2\2|}") + buf.write("\7w\2\2}~\7n\2\2~\177\7n\2\2\177$\3\2\2\2\u0080\u0081") + buf.write("\7}\2\2\u0081&\3\2\2\2\u0082\u0083\7\177\2\2\u0083(\3") + buf.write("\2\2\2\u0084\u0085\7]\2\2\u0085*\3\2\2\2\u0086\u0087\7") + buf.write("_\2\2\u0087,\3\2\2\2\u0088\u0089\7<\2\2\u0089.\3\2\2\2") + buf.write("\u008a\u008b\7.\2\2\u008b\60\3\2\2\2\u008c\u008d\7*\2") + buf.write("\2\u008d\62\3\2\2\2\u008e\u008f\7+\2\2\u008f\64\3\2\2") + buf.write("\2\u0090\u0091\7A\2\2\u0091\66\3\2\2\2\u0092\u0096\t\2") + buf.write("\2\2\u0093\u0095\t\3\2\2\u0094\u0093\3\2\2\2\u0095\u0098") + buf.write("\3\2\2\2\u0096\u0094\3\2\2\2\u0096\u0097\3\2\2\2\u0097") + buf.write("8\3\2\2\2\u0098\u0096\3\2\2\2\u0099\u009e\7$\2\2\u009a") + buf.write("\u009d\5;\36\2\u009b\u009d\5A!\2\u009c\u009a\3\2\2\2\u009c") + buf.write("\u009b\3\2\2\2\u009d\u00a0\3\2\2\2\u009e\u009c\3\2\2\2") + buf.write("\u009e\u009f\3\2\2\2\u009f\u00a1\3\2\2\2\u00a0\u009e\3") + buf.write("\2\2\2\u00a1\u00a2\7$\2\2\u00a2:\3\2\2\2\u00a3\u00a6\7") + buf.write("^\2\2\u00a4\u00a7\t\4\2\2\u00a5\u00a7\5=\37\2\u00a6\u00a4") + buf.write("\3\2\2\2\u00a6\u00a5\3\2\2\2\u00a7<\3\2\2\2\u00a8\u00a9") + buf.write("\7w\2\2\u00a9\u00aa\5? \2\u00aa\u00ab\5? \2\u00ab\u00ac") + buf.write("\5? \2\u00ac\u00ad\5? \2\u00ad>\3\2\2\2\u00ae\u00af\t") + buf.write("\5\2\2\u00af@\3\2\2\2\u00b0\u00b1\n\6\2\2\u00b1B\3\2\2") + buf.write("\2\u00b2\u00b4\7/\2\2\u00b3\u00b2\3\2\2\2\u00b3\u00b4") + buf.write("\3\2\2\2\u00b4\u00b5\3\2\2\2\u00b5\u00bc\5E#\2\u00b6\u00b8") + buf.write("\7\60\2\2\u00b7\u00b9\t\7\2\2\u00b8\u00b7\3\2\2\2\u00b9") + buf.write("\u00ba\3\2\2\2\u00ba\u00b8\3\2\2\2\u00ba\u00bb\3\2\2\2") + buf.write("\u00bb\u00bd\3\2\2\2\u00bc\u00b6\3\2\2\2\u00bc\u00bd\3") + buf.write("\2\2\2\u00bd\u00bf\3\2\2\2\u00be\u00c0\5G$\2\u00bf\u00be") + buf.write("\3\2\2\2\u00bf\u00c0\3\2\2\2\u00c0D\3\2\2\2\u00c1\u00ca") + buf.write("\7\62\2\2\u00c2\u00c6\t\b\2\2\u00c3\u00c5\t\7\2\2\u00c4") + buf.write("\u00c3\3\2\2\2\u00c5\u00c8\3\2\2\2\u00c6\u00c4\3\2\2\2") + buf.write("\u00c6\u00c7\3\2\2\2\u00c7\u00ca\3\2\2\2\u00c8\u00c6\3") + buf.write("\2\2\2\u00c9\u00c1\3\2\2\2\u00c9\u00c2\3\2\2\2\u00caF") + buf.write("\3\2\2\2\u00cb\u00cd\t\t\2\2\u00cc\u00ce\t\n\2\2\u00cd") + buf.write("\u00cc\3\2\2\2\u00cd\u00ce\3\2\2\2\u00ce\u00cf\3\2\2\2") + buf.write("\u00cf\u00d0\5E#\2\u00d0H\3\2\2\2\u00d1\u00d3\t\13\2\2") + buf.write("\u00d2\u00d1\3\2\2\2\u00d3\u00d4\3\2\2\2\u00d4\u00d2\3") + buf.write("\2\2\2\u00d4\u00d5\3\2\2\2\u00d5\u00d6\3\2\2\2\u00d6\u00d7") + buf.write("\b%\2\2\u00d7J\3\2\2\2\17\2\u0096\u009c\u009e\u00a6\u00b3") + buf.write("\u00ba\u00bc\u00bf\u00c6\u00c9\u00cd\u00d4\3\b\2\2") + return buf.getvalue() + + +class JSONPathLexer(Lexer): + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + CURRENT_VALUE = 1 + RECURSIVE_DESCENT = 2 + ROOT_VALUE = 3 + SUBSCRIPT = 4 + WILDCARD_SUBSCRIPT = 5 + AND = 6 + EQ = 7 + GE = 8 + GT = 9 + LE = 10 + LT = 11 + NE = 12 + NOT = 13 + OR = 14 + TRUE = 15 + FALSE = 16 + NULL = 17 + BRACE_LEFT = 18 + BRACE_RIGHT = 19 + BRACKET_LEFT = 20 + BRACKET_RIGHT = 21 + COLON = 22 + COMMA = 23 + PAREN_LEFT = 24 + PAREN_RIGHT = 25 + QUESTION = 26 + ID = 27 + STRING = 28 + NUMBER = 29 + WS = 30 + + channelNames = [ u"DEFAULT_TOKEN_CHANNEL", u"HIDDEN" ] + + modeNames = [ "DEFAULT_MODE" ] + + literalNames = [ "", + "'@'", "'..'", "'$'", "'.'", "'*'", "'and'", "'='", "'>='", + "'>'", "'<='", "'<'", "'!='", "'not'", "'or'", "'true'", "'false'", + "'null'", "'{'", "'}'", "'['", "']'", "':'", "','", "'('", "')'", + "'?'" ] + + symbolicNames = [ "", + "CURRENT_VALUE", "RECURSIVE_DESCENT", "ROOT_VALUE", "SUBSCRIPT", + "WILDCARD_SUBSCRIPT", "AND", "EQ", "GE", "GT", "LE", "LT", "NE", + "NOT", "OR", "TRUE", "FALSE", "NULL", "BRACE_LEFT", "BRACE_RIGHT", + "BRACKET_LEFT", "BRACKET_RIGHT", "COLON", "COMMA", "PAREN_LEFT", + "PAREN_RIGHT", "QUESTION", "ID", "STRING", "NUMBER", "WS" ] + + ruleNames = [ "CURRENT_VALUE", "RECURSIVE_DESCENT", "ROOT_VALUE", "SUBSCRIPT", + "WILDCARD_SUBSCRIPT", "AND", "EQ", "GE", "GT", "LE", "LT", + "NE", "NOT", "OR", "TRUE", "FALSE", "NULL", "BRACE_LEFT", + "BRACE_RIGHT", "BRACKET_LEFT", "BRACKET_RIGHT", "COLON", + "COMMA", "PAREN_LEFT", "PAREN_RIGHT", "QUESTION", "ID", + "STRING", "ESC", "UNICODE", "HEX", "SAFECODEPOINT", "NUMBER", + "INT", "EXP", "WS" ] + + grammarFileName = "JSONPath.g4" + + def __init__(self, input=None, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.7.1") + self._interp = LexerATNSimulator(self, self.atn, self.decisionsToDFA, PredictionContextCache()) + self._actions = None + self._predicates = None + + diff --git a/jsonpath2/parser/JSONPathLexer.tokens b/jsonpath2/parser/JSONPathLexer.tokens new file mode 100644 index 0000000..37a8444 --- /dev/null +++ b/jsonpath2/parser/JSONPathLexer.tokens @@ -0,0 +1,56 @@ +CURRENT_VALUE=1 +RECURSIVE_DESCENT=2 +ROOT_VALUE=3 +SUBSCRIPT=4 +WILDCARD_SUBSCRIPT=5 +AND=6 +EQ=7 +GE=8 +GT=9 +LE=10 +LT=11 +NE=12 +NOT=13 +OR=14 +TRUE=15 +FALSE=16 +NULL=17 +BRACE_LEFT=18 +BRACE_RIGHT=19 +BRACKET_LEFT=20 +BRACKET_RIGHT=21 +COLON=22 +COMMA=23 +PAREN_LEFT=24 +PAREN_RIGHT=25 +QUESTION=26 +ID=27 +STRING=28 +NUMBER=29 +WS=30 +'@'=1 +'..'=2 +'$'=3 +'.'=4 +'*'=5 +'and'=6 +'='=7 +'>='=8 +'>'=9 +'<='=10 +'<'=11 +'!='=12 +'not'=13 +'or'=14 +'true'=15 +'false'=16 +'null'=17 +'{'=18 +'}'=19 +'['=20 +']'=21 +':'=22 +','=23 +'('=24 +')'=25 +'?'=26 diff --git a/jsonpath2/parser/JSONPathListener.py b/jsonpath2/parser/JSONPathListener.py new file mode 100644 index 0000000..1795d21 --- /dev/null +++ b/jsonpath2/parser/JSONPathListener.py @@ -0,0 +1,145 @@ +# Generated from jsonpath2/parser/JSONPath.g4 by ANTLR 4.7.1 +from antlr4 import * +if __name__ is not None and "." in __name__: + from .JSONPathParser import JSONPathParser +else: + from JSONPathParser import JSONPathParser + +# This class defines a complete listener for a parse tree produced by JSONPathParser. +class JSONPathListener(ParseTreeListener): + + # Enter a parse tree produced by JSONPathParser#jsonpath. + def enterJsonpath(self, ctx:JSONPathParser.JsonpathContext): + pass + + # Exit a parse tree produced by JSONPathParser#jsonpath. + def exitJsonpath(self, ctx:JSONPathParser.JsonpathContext): + pass + + + # Enter a parse tree produced by JSONPathParser#subscript. + def enterSubscript(self, ctx:JSONPathParser.SubscriptContext): + pass + + # Exit a parse tree produced by JSONPathParser#subscript. + def exitSubscript(self, ctx:JSONPathParser.SubscriptContext): + pass + + + # Enter a parse tree produced by JSONPathParser#subscriptables. + def enterSubscriptables(self, ctx:JSONPathParser.SubscriptablesContext): + pass + + # Exit a parse tree produced by JSONPathParser#subscriptables. + def exitSubscriptables(self, ctx:JSONPathParser.SubscriptablesContext): + pass + + + # Enter a parse tree produced by JSONPathParser#subscriptableBareword. + def enterSubscriptableBareword(self, ctx:JSONPathParser.SubscriptableBarewordContext): + pass + + # Exit a parse tree produced by JSONPathParser#subscriptableBareword. + def exitSubscriptableBareword(self, ctx:JSONPathParser.SubscriptableBarewordContext): + pass + + + # Enter a parse tree produced by JSONPathParser#subscriptable. + def enterSubscriptable(self, ctx:JSONPathParser.SubscriptableContext): + pass + + # Exit a parse tree produced by JSONPathParser#subscriptable. + def exitSubscriptable(self, ctx:JSONPathParser.SubscriptableContext): + pass + + + # Enter a parse tree produced by JSONPathParser#sliceable. + def enterSliceable(self, ctx:JSONPathParser.SliceableContext): + pass + + # Exit a parse tree produced by JSONPathParser#sliceable. + def exitSliceable(self, ctx:JSONPathParser.SliceableContext): + pass + + + # Enter a parse tree produced by JSONPathParser#expression. + def enterExpression(self, ctx:JSONPathParser.ExpressionContext): + pass + + # Exit a parse tree produced by JSONPathParser#expression. + def exitExpression(self, ctx:JSONPathParser.ExpressionContext): + pass + + + # Enter a parse tree produced by JSONPathParser#andExpression. + def enterAndExpression(self, ctx:JSONPathParser.AndExpressionContext): + pass + + # Exit a parse tree produced by JSONPathParser#andExpression. + def exitAndExpression(self, ctx:JSONPathParser.AndExpressionContext): + pass + + + # Enter a parse tree produced by JSONPathParser#orExpression. + def enterOrExpression(self, ctx:JSONPathParser.OrExpressionContext): + pass + + # Exit a parse tree produced by JSONPathParser#orExpression. + def exitOrExpression(self, ctx:JSONPathParser.OrExpressionContext): + pass + + + # Enter a parse tree produced by JSONPathParser#notExpression. + def enterNotExpression(self, ctx:JSONPathParser.NotExpressionContext): + pass + + # Exit a parse tree produced by JSONPathParser#notExpression. + def exitNotExpression(self, ctx:JSONPathParser.NotExpressionContext): + pass + + + # Enter a parse tree produced by JSONPathParser#json. + def enterJson(self, ctx:JSONPathParser.JsonContext): + pass + + # Exit a parse tree produced by JSONPathParser#json. + def exitJson(self, ctx:JSONPathParser.JsonContext): + pass + + + # Enter a parse tree produced by JSONPathParser#obj. + def enterObj(self, ctx:JSONPathParser.ObjContext): + pass + + # Exit a parse tree produced by JSONPathParser#obj. + def exitObj(self, ctx:JSONPathParser.ObjContext): + pass + + + # Enter a parse tree produced by JSONPathParser#pair. + def enterPair(self, ctx:JSONPathParser.PairContext): + pass + + # Exit a parse tree produced by JSONPathParser#pair. + def exitPair(self, ctx:JSONPathParser.PairContext): + pass + + + # Enter a parse tree produced by JSONPathParser#array. + def enterArray(self, ctx:JSONPathParser.ArrayContext): + pass + + # Exit a parse tree produced by JSONPathParser#array. + def exitArray(self, ctx:JSONPathParser.ArrayContext): + pass + + + # Enter a parse tree produced by JSONPathParser#value. + def enterValue(self, ctx:JSONPathParser.ValueContext): + pass + + # Exit a parse tree produced by JSONPathParser#value. + def exitValue(self, ctx:JSONPathParser.ValueContext): + pass + + diff --git a/jsonpath2/parser/JSONPathParser.py b/jsonpath2/parser/JSONPathParser.py new file mode 100644 index 0000000..e8c9163 --- /dev/null +++ b/jsonpath2/parser/JSONPathParser.py @@ -0,0 +1,1319 @@ +# Generated from jsonpath2/parser/JSONPath.g4 by ANTLR 4.7.1 +# encoding: utf-8 +from antlr4 import * +from io import StringIO +from typing.io import TextIO +import sys + +def serializedATN(): + with StringIO() as buf: + buf.write("\3\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964\3 ") + buf.write("\u00a9\4\2\t\2\4\3\t\3\4\4\t\4\4\5\t\5\4\6\t\6\4\7\t\7") + buf.write("\4\b\t\b\4\t\t\t\4\n\t\n\4\13\t\13\4\f\t\f\4\r\t\r\4\16") + buf.write("\t\16\4\17\t\17\4\20\t\20\3\2\3\2\5\2#\n\2\3\2\3\2\3\3") + buf.write("\3\3\3\3\5\3*\n\3\3\3\5\3-\n\3\3\3\3\3\3\3\5\3\62\n\3") + buf.write("\3\3\3\3\5\3\66\n\3\5\38\n\3\3\4\3\4\3\4\3\4\7\4>\n\4") + buf.write("\f\4\16\4A\13\4\3\4\3\4\3\5\3\5\3\6\3\6\3\6\3\6\5\6K\n") + buf.write("\6\3\6\3\6\3\6\3\6\3\6\3\6\3\6\5\6T\n\6\3\7\3\7\3\7\5") + buf.write("\7Y\n\7\3\7\3\7\3\7\5\7^\n\7\3\b\3\b\3\t\3\t\3\t\5\te") + buf.write("\n\t\3\n\3\n\3\n\5\nj\n\n\3\13\3\13\3\13\3\13\3\13\3\13") + buf.write("\3\13\3\13\5\13t\n\13\3\13\3\13\5\13x\n\13\5\13z\n\13") + buf.write("\3\f\3\f\3\r\3\r\3\r\3\r\7\r\u0082\n\r\f\r\16\r\u0085") + buf.write("\13\r\3\r\3\r\3\r\3\r\5\r\u008b\n\r\3\16\3\16\3\16\3\16") + buf.write("\3\17\3\17\3\17\3\17\7\17\u0095\n\17\f\17\16\17\u0098") + buf.write("\13\17\3\17\3\17\3\17\3\17\5\17\u009e\n\17\3\20\3\20\3") + buf.write("\20\3\20\3\20\3\20\3\20\5\20\u00a7\n\20\3\20\2\2\21\2") + buf.write("\4\6\b\n\f\16\20\22\24\26\30\32\34\36\2\5\4\2\7\7\35\35") + buf.write("\4\2\3\3\5\5\3\2\t\16\2\u00b8\2 \3\2\2\2\4\67\3\2\2\2") + buf.write("\69\3\2\2\2\bD\3\2\2\2\nS\3\2\2\2\fU\3\2\2\2\16_\3\2\2") + buf.write("\2\20a\3\2\2\2\22f\3\2\2\2\24y\3\2\2\2\26{\3\2\2\2\30") + buf.write("\u008a\3\2\2\2\32\u008c\3\2\2\2\34\u009d\3\2\2\2\36\u00a6") + buf.write("\3\2\2\2 \"\7\5\2\2!#\5\4\3\2\"!\3\2\2\2\"#\3\2\2\2#$") + buf.write("\3\2\2\2$%\7\2\2\3%\3\3\2\2\2&)\7\4\2\2\'*\5\b\5\2(*\5") + buf.write("\6\4\2)\'\3\2\2\2)(\3\2\2\2*,\3\2\2\2+-\5\4\3\2,+\3\2") + buf.write("\2\2,-\3\2\2\2-8\3\2\2\2./\7\6\2\2/\61\5\b\5\2\60\62\5") + buf.write("\4\3\2\61\60\3\2\2\2\61\62\3\2\2\2\628\3\2\2\2\63\65\5") + buf.write("\6\4\2\64\66\5\4\3\2\65\64\3\2\2\2\65\66\3\2\2\2\668\3") + buf.write("\2\2\2\67&\3\2\2\2\67.\3\2\2\2\67\63\3\2\2\28\5\3\2\2") + buf.write("\29:\7\26\2\2:?\5\n\6\2;<\7\31\2\2<>\5\n\6\2=;\3\2\2\2") + buf.write(">A\3\2\2\2?=\3\2\2\2?@\3\2\2\2@B\3\2\2\2A?\3\2\2\2BC\7") + buf.write("\27\2\2C\7\3\2\2\2DE\t\2\2\2E\t\3\2\2\2FT\7\36\2\2GH\7") + buf.write("\37\2\2HJ\6\6\2\2IK\5\f\7\2JI\3\2\2\2JK\3\2\2\2KT\3\2") + buf.write("\2\2LT\5\f\7\2MT\7\7\2\2NO\7\34\2\2OP\7\32\2\2PQ\5\16") + buf.write("\b\2QR\7\33\2\2RT\3\2\2\2SF\3\2\2\2SG\3\2\2\2SL\3\2\2") + buf.write("\2SM\3\2\2\2SN\3\2\2\2T\13\3\2\2\2UX\7\30\2\2VW\7\37\2") + buf.write("\2WY\6\7\3\2XV\3\2\2\2XY\3\2\2\2Y]\3\2\2\2Z[\7\30\2\2") + buf.write("[\\\7\37\2\2\\^\6\7\4\2]Z\3\2\2\2]^\3\2\2\2^\r\3\2\2\2") + buf.write("_`\5\20\t\2`\17\3\2\2\2ad\5\22\n\2bc\7\b\2\2ce\5\20\t") + buf.write("\2db\3\2\2\2de\3\2\2\2e\21\3\2\2\2fi\5\24\13\2gh\7\20") + buf.write("\2\2hj\5\22\n\2ig\3\2\2\2ij\3\2\2\2j\23\3\2\2\2kl\7\17") + buf.write("\2\2lz\5\24\13\2mn\7\32\2\2no\5\16\b\2op\7\33\2\2pz\3") + buf.write("\2\2\2qs\t\3\2\2rt\5\4\3\2sr\3\2\2\2st\3\2\2\2tw\3\2\2") + buf.write("\2uv\t\4\2\2vx\5\36\20\2wu\3\2\2\2wx\3\2\2\2xz\3\2\2\2") + buf.write("yk\3\2\2\2ym\3\2\2\2yq\3\2\2\2z\25\3\2\2\2{|\5\36\20\2") + buf.write("|\27\3\2\2\2}~\7\24\2\2~\u0083\5\32\16\2\177\u0080\7\31") + buf.write("\2\2\u0080\u0082\5\32\16\2\u0081\177\3\2\2\2\u0082\u0085") + buf.write("\3\2\2\2\u0083\u0081\3\2\2\2\u0083\u0084\3\2\2\2\u0084") + buf.write("\u0086\3\2\2\2\u0085\u0083\3\2\2\2\u0086\u0087\7\25\2") + buf.write("\2\u0087\u008b\3\2\2\2\u0088\u0089\7\24\2\2\u0089\u008b") + buf.write("\7\25\2\2\u008a}\3\2\2\2\u008a\u0088\3\2\2\2\u008b\31") + buf.write("\3\2\2\2\u008c\u008d\7\36\2\2\u008d\u008e\7\30\2\2\u008e") + buf.write("\u008f\5\36\20\2\u008f\33\3\2\2\2\u0090\u0091\7\26\2\2") + buf.write("\u0091\u0096\5\36\20\2\u0092\u0093\7\31\2\2\u0093\u0095") + buf.write("\5\36\20\2\u0094\u0092\3\2\2\2\u0095\u0098\3\2\2\2\u0096") + buf.write("\u0094\3\2\2\2\u0096\u0097\3\2\2\2\u0097\u0099\3\2\2\2") + buf.write("\u0098\u0096\3\2\2\2\u0099\u009a\7\27\2\2\u009a\u009e") + buf.write("\3\2\2\2\u009b\u009c\7\26\2\2\u009c\u009e\7\27\2\2\u009d") + buf.write("\u0090\3\2\2\2\u009d\u009b\3\2\2\2\u009e\35\3\2\2\2\u009f") + buf.write("\u00a7\7\36\2\2\u00a0\u00a7\7\37\2\2\u00a1\u00a7\5\30") + buf.write("\r\2\u00a2\u00a7\5\34\17\2\u00a3\u00a7\7\21\2\2\u00a4") + buf.write("\u00a7\7\22\2\2\u00a5\u00a7\7\23\2\2\u00a6\u009f\3\2\2") + buf.write("\2\u00a6\u00a0\3\2\2\2\u00a6\u00a1\3\2\2\2\u00a6\u00a2") + buf.write("\3\2\2\2\u00a6\u00a3\3\2\2\2\u00a6\u00a4\3\2\2\2\u00a6") + buf.write("\u00a5\3\2\2\2\u00a7\37\3\2\2\2\27\"),\61\65\67?JSX]d") + buf.write("iswy\u0083\u008a\u0096\u009d\u00a6") + return buf.getvalue() + + +class JSONPathParser ( Parser ): + + grammarFileName = "JSONPath.g4" + + atn = ATNDeserializer().deserialize(serializedATN()) + + decisionsToDFA = [ DFA(ds, i) for i, ds in enumerate(atn.decisionToState) ] + + sharedContextCache = PredictionContextCache() + + literalNames = [ "", "'@'", "'..'", "'$'", "'.'", "'*'", "'and'", + "'='", "'>='", "'>'", "'<='", "'<'", "'!='", "'not'", + "'or'", "'true'", "'false'", "'null'", "'{'", "'}'", + "'['", "']'", "':'", "','", "'('", "')'", "'?'" ] + + symbolicNames = [ "", "CURRENT_VALUE", "RECURSIVE_DESCENT", + "ROOT_VALUE", "SUBSCRIPT", "WILDCARD_SUBSCRIPT", "AND", + "EQ", "GE", "GT", "LE", "LT", "NE", "NOT", "OR", "TRUE", + "FALSE", "NULL", "BRACE_LEFT", "BRACE_RIGHT", "BRACKET_LEFT", + "BRACKET_RIGHT", "COLON", "COMMA", "PAREN_LEFT", "PAREN_RIGHT", + "QUESTION", "ID", "STRING", "NUMBER", "WS" ] + + RULE_jsonpath = 0 + RULE_subscript = 1 + RULE_subscriptables = 2 + RULE_subscriptableBareword = 3 + RULE_subscriptable = 4 + RULE_sliceable = 5 + RULE_expression = 6 + RULE_andExpression = 7 + RULE_orExpression = 8 + RULE_notExpression = 9 + RULE_json = 10 + RULE_obj = 11 + RULE_pair = 12 + RULE_array = 13 + RULE_value = 14 + + ruleNames = [ "jsonpath", "subscript", "subscriptables", "subscriptableBareword", + "subscriptable", "sliceable", "expression", "andExpression", + "orExpression", "notExpression", "json", "obj", "pair", + "array", "value" ] + + EOF = Token.EOF + CURRENT_VALUE=1 + RECURSIVE_DESCENT=2 + ROOT_VALUE=3 + SUBSCRIPT=4 + WILDCARD_SUBSCRIPT=5 + AND=6 + EQ=7 + GE=8 + GT=9 + LE=10 + LT=11 + NE=12 + NOT=13 + OR=14 + TRUE=15 + FALSE=16 + NULL=17 + BRACE_LEFT=18 + BRACE_RIGHT=19 + BRACKET_LEFT=20 + BRACKET_RIGHT=21 + COLON=22 + COMMA=23 + PAREN_LEFT=24 + PAREN_RIGHT=25 + QUESTION=26 + ID=27 + STRING=28 + NUMBER=29 + WS=30 + + def __init__(self, input:TokenStream, output:TextIO = sys.stdout): + super().__init__(input, output) + self.checkVersion("4.7.1") + self._interp = ParserATNSimulator(self, self.atn, self.decisionsToDFA, self.sharedContextCache) + self._predicates = None + + + + class JsonpathContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ROOT_VALUE(self): + return self.getToken(JSONPathParser.ROOT_VALUE, 0) + + def EOF(self): + return self.getToken(JSONPathParser.EOF, 0) + + def subscript(self): + return self.getTypedRuleContext(JSONPathParser.SubscriptContext,0) + + + def getRuleIndex(self): + return JSONPathParser.RULE_jsonpath + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterJsonpath" ): + listener.enterJsonpath(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitJsonpath" ): + listener.exitJsonpath(self) + + + + + def jsonpath(self): + + localctx = JSONPathParser.JsonpathContext(self, self._ctx, self.state) + self.enterRule(localctx, 0, self.RULE_jsonpath) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 30 + self.match(JSONPathParser.ROOT_VALUE) + self.state = 32 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.RECURSIVE_DESCENT) | (1 << JSONPathParser.SUBSCRIPT) | (1 << JSONPathParser.BRACKET_LEFT))) != 0): + self.state = 31 + self.subscript() + + + self.state = 34 + self.match(JSONPathParser.EOF) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class SubscriptContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def RECURSIVE_DESCENT(self): + return self.getToken(JSONPathParser.RECURSIVE_DESCENT, 0) + + def subscriptableBareword(self): + return self.getTypedRuleContext(JSONPathParser.SubscriptableBarewordContext,0) + + + def subscriptables(self): + return self.getTypedRuleContext(JSONPathParser.SubscriptablesContext,0) + + + def subscript(self): + return self.getTypedRuleContext(JSONPathParser.SubscriptContext,0) + + + def SUBSCRIPT(self): + return self.getToken(JSONPathParser.SUBSCRIPT, 0) + + def getRuleIndex(self): + return JSONPathParser.RULE_subscript + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubscript" ): + listener.enterSubscript(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubscript" ): + listener.exitSubscript(self) + + + + + def subscript(self): + + localctx = JSONPathParser.SubscriptContext(self, self._ctx, self.state) + self.enterRule(localctx, 2, self.RULE_subscript) + self._la = 0 # Token type + try: + self.state = 53 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [JSONPathParser.RECURSIVE_DESCENT]: + self.enterOuterAlt(localctx, 1) + self.state = 36 + self.match(JSONPathParser.RECURSIVE_DESCENT) + self.state = 39 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [JSONPathParser.WILDCARD_SUBSCRIPT, JSONPathParser.ID]: + self.state = 37 + self.subscriptableBareword() + pass + elif token in [JSONPathParser.BRACKET_LEFT]: + self.state = 38 + self.subscriptables() + pass + else: + raise NoViableAltException(self) + + self.state = 42 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.RECURSIVE_DESCENT) | (1 << JSONPathParser.SUBSCRIPT) | (1 << JSONPathParser.BRACKET_LEFT))) != 0): + self.state = 41 + self.subscript() + + + pass + elif token in [JSONPathParser.SUBSCRIPT]: + self.enterOuterAlt(localctx, 2) + self.state = 44 + self.match(JSONPathParser.SUBSCRIPT) + self.state = 45 + self.subscriptableBareword() + self.state = 47 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.RECURSIVE_DESCENT) | (1 << JSONPathParser.SUBSCRIPT) | (1 << JSONPathParser.BRACKET_LEFT))) != 0): + self.state = 46 + self.subscript() + + + pass + elif token in [JSONPathParser.BRACKET_LEFT]: + self.enterOuterAlt(localctx, 3) + self.state = 49 + self.subscriptables() + self.state = 51 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.RECURSIVE_DESCENT) | (1 << JSONPathParser.SUBSCRIPT) | (1 << JSONPathParser.BRACKET_LEFT))) != 0): + self.state = 50 + self.subscript() + + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class SubscriptablesContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BRACKET_LEFT(self): + return self.getToken(JSONPathParser.BRACKET_LEFT, 0) + + def subscriptable(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(JSONPathParser.SubscriptableContext) + else: + return self.getTypedRuleContext(JSONPathParser.SubscriptableContext,i) + + + def BRACKET_RIGHT(self): + return self.getToken(JSONPathParser.BRACKET_RIGHT, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(JSONPathParser.COMMA) + else: + return self.getToken(JSONPathParser.COMMA, i) + + def getRuleIndex(self): + return JSONPathParser.RULE_subscriptables + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubscriptables" ): + listener.enterSubscriptables(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubscriptables" ): + listener.exitSubscriptables(self) + + + + + def subscriptables(self): + + localctx = JSONPathParser.SubscriptablesContext(self, self._ctx, self.state) + self.enterRule(localctx, 4, self.RULE_subscriptables) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 55 + self.match(JSONPathParser.BRACKET_LEFT) + self.state = 56 + self.subscriptable() + self.state = 61 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==JSONPathParser.COMMA: + self.state = 57 + self.match(JSONPathParser.COMMA) + self.state = 58 + self.subscriptable() + self.state = 63 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 64 + self.match(JSONPathParser.BRACKET_RIGHT) + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class SubscriptableBarewordContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def ID(self): + return self.getToken(JSONPathParser.ID, 0) + + def WILDCARD_SUBSCRIPT(self): + return self.getToken(JSONPathParser.WILDCARD_SUBSCRIPT, 0) + + def getRuleIndex(self): + return JSONPathParser.RULE_subscriptableBareword + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubscriptableBareword" ): + listener.enterSubscriptableBareword(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubscriptableBareword" ): + listener.exitSubscriptableBareword(self) + + + + + def subscriptableBareword(self): + + localctx = JSONPathParser.SubscriptableBarewordContext(self, self._ctx, self.state) + self.enterRule(localctx, 6, self.RULE_subscriptableBareword) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 66 + _la = self._input.LA(1) + if not(_la==JSONPathParser.WILDCARD_SUBSCRIPT or _la==JSONPathParser.ID): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class SubscriptableContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def STRING(self): + return self.getToken(JSONPathParser.STRING, 0) + + def NUMBER(self): + return self.getToken(JSONPathParser.NUMBER, 0) + + def sliceable(self): + return self.getTypedRuleContext(JSONPathParser.SliceableContext,0) + + + def WILDCARD_SUBSCRIPT(self): + return self.getToken(JSONPathParser.WILDCARD_SUBSCRIPT, 0) + + def QUESTION(self): + return self.getToken(JSONPathParser.QUESTION, 0) + + def PAREN_LEFT(self): + return self.getToken(JSONPathParser.PAREN_LEFT, 0) + + def expression(self): + return self.getTypedRuleContext(JSONPathParser.ExpressionContext,0) + + + def PAREN_RIGHT(self): + return self.getToken(JSONPathParser.PAREN_RIGHT, 0) + + def getRuleIndex(self): + return JSONPathParser.RULE_subscriptable + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSubscriptable" ): + listener.enterSubscriptable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSubscriptable" ): + listener.exitSubscriptable(self) + + + + + def subscriptable(self): + + localctx = JSONPathParser.SubscriptableContext(self, self._ctx, self.state) + self.enterRule(localctx, 8, self.RULE_subscriptable) + self._la = 0 # Token type + try: + self.state = 81 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [JSONPathParser.STRING]: + self.enterOuterAlt(localctx, 1) + self.state = 68 + self.match(JSONPathParser.STRING) + pass + elif token in [JSONPathParser.NUMBER]: + self.enterOuterAlt(localctx, 2) + self.state = 69 + self.match(JSONPathParser.NUMBER) + self.state = 70 + if not self.tryCast(int): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.tryCast(int)") + self.state = 72 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==JSONPathParser.COLON: + self.state = 71 + self.sliceable() + + + pass + elif token in [JSONPathParser.COLON]: + self.enterOuterAlt(localctx, 3) + self.state = 74 + self.sliceable() + pass + elif token in [JSONPathParser.WILDCARD_SUBSCRIPT]: + self.enterOuterAlt(localctx, 4) + self.state = 75 + self.match(JSONPathParser.WILDCARD_SUBSCRIPT) + pass + elif token in [JSONPathParser.QUESTION]: + self.enterOuterAlt(localctx, 5) + self.state = 76 + self.match(JSONPathParser.QUESTION) + self.state = 77 + self.match(JSONPathParser.PAREN_LEFT) + self.state = 78 + self.expression() + self.state = 79 + self.match(JSONPathParser.PAREN_RIGHT) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class SliceableContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def COLON(self, i:int=None): + if i is None: + return self.getTokens(JSONPathParser.COLON) + else: + return self.getToken(JSONPathParser.COLON, i) + + def NUMBER(self, i:int=None): + if i is None: + return self.getTokens(JSONPathParser.NUMBER) + else: + return self.getToken(JSONPathParser.NUMBER, i) + + def getRuleIndex(self): + return JSONPathParser.RULE_sliceable + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterSliceable" ): + listener.enterSliceable(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitSliceable" ): + listener.exitSliceable(self) + + + + + def sliceable(self): + + localctx = JSONPathParser.SliceableContext(self, self._ctx, self.state) + self.enterRule(localctx, 10, self.RULE_sliceable) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 83 + self.match(JSONPathParser.COLON) + self.state = 86 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==JSONPathParser.NUMBER: + self.state = 84 + self.match(JSONPathParser.NUMBER) + self.state = 85 + if not self.tryCast(int): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.tryCast(int)") + + + self.state = 91 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==JSONPathParser.COLON: + self.state = 88 + self.match(JSONPathParser.COLON) + self.state = 89 + self.match(JSONPathParser.NUMBER) + self.state = 90 + if not self.tryCast(int): + from antlr4.error.Errors import FailedPredicateException + raise FailedPredicateException(self, "self.tryCast(int)") + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class ExpressionContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def andExpression(self): + return self.getTypedRuleContext(JSONPathParser.AndExpressionContext,0) + + + def getRuleIndex(self): + return JSONPathParser.RULE_expression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterExpression" ): + listener.enterExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitExpression" ): + listener.exitExpression(self) + + + + + def expression(self): + + localctx = JSONPathParser.ExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 12, self.RULE_expression) + try: + self.enterOuterAlt(localctx, 1) + self.state = 93 + self.andExpression() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class AndExpressionContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def orExpression(self): + return self.getTypedRuleContext(JSONPathParser.OrExpressionContext,0) + + + def AND(self): + return self.getToken(JSONPathParser.AND, 0) + + def andExpression(self): + return self.getTypedRuleContext(JSONPathParser.AndExpressionContext,0) + + + def getRuleIndex(self): + return JSONPathParser.RULE_andExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterAndExpression" ): + listener.enterAndExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitAndExpression" ): + listener.exitAndExpression(self) + + + + + def andExpression(self): + + localctx = JSONPathParser.AndExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 14, self.RULE_andExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 95 + self.orExpression() + self.state = 98 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==JSONPathParser.AND: + self.state = 96 + self.match(JSONPathParser.AND) + self.state = 97 + self.andExpression() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class OrExpressionContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def notExpression(self): + return self.getTypedRuleContext(JSONPathParser.NotExpressionContext,0) + + + def OR(self): + return self.getToken(JSONPathParser.OR, 0) + + def orExpression(self): + return self.getTypedRuleContext(JSONPathParser.OrExpressionContext,0) + + + def getRuleIndex(self): + return JSONPathParser.RULE_orExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterOrExpression" ): + listener.enterOrExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitOrExpression" ): + listener.exitOrExpression(self) + + + + + def orExpression(self): + + localctx = JSONPathParser.OrExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 16, self.RULE_orExpression) + self._la = 0 # Token type + try: + self.enterOuterAlt(localctx, 1) + self.state = 100 + self.notExpression() + self.state = 103 + self._errHandler.sync(self) + _la = self._input.LA(1) + if _la==JSONPathParser.OR: + self.state = 101 + self.match(JSONPathParser.OR) + self.state = 102 + self.orExpression() + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class NotExpressionContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def NOT(self): + return self.getToken(JSONPathParser.NOT, 0) + + def notExpression(self): + return self.getTypedRuleContext(JSONPathParser.NotExpressionContext,0) + + + def PAREN_LEFT(self): + return self.getToken(JSONPathParser.PAREN_LEFT, 0) + + def expression(self): + return self.getTypedRuleContext(JSONPathParser.ExpressionContext,0) + + + def PAREN_RIGHT(self): + return self.getToken(JSONPathParser.PAREN_RIGHT, 0) + + def ROOT_VALUE(self): + return self.getToken(JSONPathParser.ROOT_VALUE, 0) + + def CURRENT_VALUE(self): + return self.getToken(JSONPathParser.CURRENT_VALUE, 0) + + def subscript(self): + return self.getTypedRuleContext(JSONPathParser.SubscriptContext,0) + + + def value(self): + return self.getTypedRuleContext(JSONPathParser.ValueContext,0) + + + def EQ(self): + return self.getToken(JSONPathParser.EQ, 0) + + def NE(self): + return self.getToken(JSONPathParser.NE, 0) + + def LT(self): + return self.getToken(JSONPathParser.LT, 0) + + def LE(self): + return self.getToken(JSONPathParser.LE, 0) + + def GT(self): + return self.getToken(JSONPathParser.GT, 0) + + def GE(self): + return self.getToken(JSONPathParser.GE, 0) + + def getRuleIndex(self): + return JSONPathParser.RULE_notExpression + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterNotExpression" ): + listener.enterNotExpression(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitNotExpression" ): + listener.exitNotExpression(self) + + + + + def notExpression(self): + + localctx = JSONPathParser.NotExpressionContext(self, self._ctx, self.state) + self.enterRule(localctx, 18, self.RULE_notExpression) + self._la = 0 # Token type + try: + self.state = 119 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [JSONPathParser.NOT]: + self.enterOuterAlt(localctx, 1) + self.state = 105 + self.match(JSONPathParser.NOT) + self.state = 106 + self.notExpression() + pass + elif token in [JSONPathParser.PAREN_LEFT]: + self.enterOuterAlt(localctx, 2) + self.state = 107 + self.match(JSONPathParser.PAREN_LEFT) + self.state = 108 + self.expression() + self.state = 109 + self.match(JSONPathParser.PAREN_RIGHT) + pass + elif token in [JSONPathParser.CURRENT_VALUE, JSONPathParser.ROOT_VALUE]: + self.enterOuterAlt(localctx, 3) + self.state = 111 + _la = self._input.LA(1) + if not(_la==JSONPathParser.CURRENT_VALUE or _la==JSONPathParser.ROOT_VALUE): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 113 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.RECURSIVE_DESCENT) | (1 << JSONPathParser.SUBSCRIPT) | (1 << JSONPathParser.BRACKET_LEFT))) != 0): + self.state = 112 + self.subscript() + + + self.state = 117 + self._errHandler.sync(self) + _la = self._input.LA(1) + if (((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.EQ) | (1 << JSONPathParser.GE) | (1 << JSONPathParser.GT) | (1 << JSONPathParser.LE) | (1 << JSONPathParser.LT) | (1 << JSONPathParser.NE))) != 0): + self.state = 115 + _la = self._input.LA(1) + if not((((_la) & ~0x3f) == 0 and ((1 << _la) & ((1 << JSONPathParser.EQ) | (1 << JSONPathParser.GE) | (1 << JSONPathParser.GT) | (1 << JSONPathParser.LE) | (1 << JSONPathParser.LT) | (1 << JSONPathParser.NE))) != 0)): + self._errHandler.recoverInline(self) + else: + self._errHandler.reportMatch(self) + self.consume() + self.state = 116 + self.value() + + + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class JsonContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def value(self): + return self.getTypedRuleContext(JSONPathParser.ValueContext,0) + + + def getRuleIndex(self): + return JSONPathParser.RULE_json + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterJson" ): + listener.enterJson(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitJson" ): + listener.exitJson(self) + + + + + def json(self): + + localctx = JSONPathParser.JsonContext(self, self._ctx, self.state) + self.enterRule(localctx, 20, self.RULE_json) + try: + self.enterOuterAlt(localctx, 1) + self.state = 121 + self.value() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class ObjContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BRACE_LEFT(self): + return self.getToken(JSONPathParser.BRACE_LEFT, 0) + + def pair(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(JSONPathParser.PairContext) + else: + return self.getTypedRuleContext(JSONPathParser.PairContext,i) + + + def BRACE_RIGHT(self): + return self.getToken(JSONPathParser.BRACE_RIGHT, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(JSONPathParser.COMMA) + else: + return self.getToken(JSONPathParser.COMMA, i) + + def getRuleIndex(self): + return JSONPathParser.RULE_obj + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterObj" ): + listener.enterObj(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitObj" ): + listener.exitObj(self) + + + + + def obj(self): + + localctx = JSONPathParser.ObjContext(self, self._ctx, self.state) + self.enterRule(localctx, 22, self.RULE_obj) + self._la = 0 # Token type + try: + self.state = 136 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,17,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 123 + self.match(JSONPathParser.BRACE_LEFT) + self.state = 124 + self.pair() + self.state = 129 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==JSONPathParser.COMMA: + self.state = 125 + self.match(JSONPathParser.COMMA) + self.state = 126 + self.pair() + self.state = 131 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 132 + self.match(JSONPathParser.BRACE_RIGHT) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 134 + self.match(JSONPathParser.BRACE_LEFT) + self.state = 135 + self.match(JSONPathParser.BRACE_RIGHT) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class PairContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def STRING(self): + return self.getToken(JSONPathParser.STRING, 0) + + def COLON(self): + return self.getToken(JSONPathParser.COLON, 0) + + def value(self): + return self.getTypedRuleContext(JSONPathParser.ValueContext,0) + + + def getRuleIndex(self): + return JSONPathParser.RULE_pair + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterPair" ): + listener.enterPair(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitPair" ): + listener.exitPair(self) + + + + + def pair(self): + + localctx = JSONPathParser.PairContext(self, self._ctx, self.state) + self.enterRule(localctx, 24, self.RULE_pair) + try: + self.enterOuterAlt(localctx, 1) + self.state = 138 + self.match(JSONPathParser.STRING) + self.state = 139 + self.match(JSONPathParser.COLON) + self.state = 140 + self.value() + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class ArrayContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def BRACKET_LEFT(self): + return self.getToken(JSONPathParser.BRACKET_LEFT, 0) + + def value(self, i:int=None): + if i is None: + return self.getTypedRuleContexts(JSONPathParser.ValueContext) + else: + return self.getTypedRuleContext(JSONPathParser.ValueContext,i) + + + def BRACKET_RIGHT(self): + return self.getToken(JSONPathParser.BRACKET_RIGHT, 0) + + def COMMA(self, i:int=None): + if i is None: + return self.getTokens(JSONPathParser.COMMA) + else: + return self.getToken(JSONPathParser.COMMA, i) + + def getRuleIndex(self): + return JSONPathParser.RULE_array + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterArray" ): + listener.enterArray(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitArray" ): + listener.exitArray(self) + + + + + def array(self): + + localctx = JSONPathParser.ArrayContext(self, self._ctx, self.state) + self.enterRule(localctx, 26, self.RULE_array) + self._la = 0 # Token type + try: + self.state = 155 + self._errHandler.sync(self) + la_ = self._interp.adaptivePredict(self._input,19,self._ctx) + if la_ == 1: + self.enterOuterAlt(localctx, 1) + self.state = 142 + self.match(JSONPathParser.BRACKET_LEFT) + self.state = 143 + self.value() + self.state = 148 + self._errHandler.sync(self) + _la = self._input.LA(1) + while _la==JSONPathParser.COMMA: + self.state = 144 + self.match(JSONPathParser.COMMA) + self.state = 145 + self.value() + self.state = 150 + self._errHandler.sync(self) + _la = self._input.LA(1) + + self.state = 151 + self.match(JSONPathParser.BRACKET_RIGHT) + pass + + elif la_ == 2: + self.enterOuterAlt(localctx, 2) + self.state = 153 + self.match(JSONPathParser.BRACKET_LEFT) + self.state = 154 + self.match(JSONPathParser.BRACKET_RIGHT) + pass + + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + class ValueContext(ParserRuleContext): + + def __init__(self, parser, parent:ParserRuleContext=None, invokingState:int=-1): + super().__init__(parent, invokingState) + self.parser = parser + + def STRING(self): + return self.getToken(JSONPathParser.STRING, 0) + + def NUMBER(self): + return self.getToken(JSONPathParser.NUMBER, 0) + + def obj(self): + return self.getTypedRuleContext(JSONPathParser.ObjContext,0) + + + def array(self): + return self.getTypedRuleContext(JSONPathParser.ArrayContext,0) + + + def TRUE(self): + return self.getToken(JSONPathParser.TRUE, 0) + + def FALSE(self): + return self.getToken(JSONPathParser.FALSE, 0) + + def NULL(self): + return self.getToken(JSONPathParser.NULL, 0) + + def getRuleIndex(self): + return JSONPathParser.RULE_value + + def enterRule(self, listener:ParseTreeListener): + if hasattr( listener, "enterValue" ): + listener.enterValue(self) + + def exitRule(self, listener:ParseTreeListener): + if hasattr( listener, "exitValue" ): + listener.exitValue(self) + + + + + def value(self): + + localctx = JSONPathParser.ValueContext(self, self._ctx, self.state) + self.enterRule(localctx, 28, self.RULE_value) + try: + self.state = 164 + self._errHandler.sync(self) + token = self._input.LA(1) + if token in [JSONPathParser.STRING]: + self.enterOuterAlt(localctx, 1) + self.state = 157 + self.match(JSONPathParser.STRING) + pass + elif token in [JSONPathParser.NUMBER]: + self.enterOuterAlt(localctx, 2) + self.state = 158 + self.match(JSONPathParser.NUMBER) + pass + elif token in [JSONPathParser.BRACE_LEFT]: + self.enterOuterAlt(localctx, 3) + self.state = 159 + self.obj() + pass + elif token in [JSONPathParser.BRACKET_LEFT]: + self.enterOuterAlt(localctx, 4) + self.state = 160 + self.array() + pass + elif token in [JSONPathParser.TRUE]: + self.enterOuterAlt(localctx, 5) + self.state = 161 + self.match(JSONPathParser.TRUE) + pass + elif token in [JSONPathParser.FALSE]: + self.enterOuterAlt(localctx, 6) + self.state = 162 + self.match(JSONPathParser.FALSE) + pass + elif token in [JSONPathParser.NULL]: + self.enterOuterAlt(localctx, 7) + self.state = 163 + self.match(JSONPathParser.NULL) + pass + else: + raise NoViableAltException(self) + + except RecognitionException as re: + localctx.exception = re + self._errHandler.reportError(self, re) + self._errHandler.recover(self, re) + finally: + self.exitRule() + return localctx + + + + def sempred(self, localctx:RuleContext, ruleIndex:int, predIndex:int): + if self._predicates == None: + self._predicates = dict() + self._predicates[4] = self.subscriptable_sempred + self._predicates[5] = self.sliceable_sempred + pred = self._predicates.get(ruleIndex, None) + if pred is None: + raise Exception("No predicate with index:" + str(ruleIndex)) + else: + return pred(localctx, predIndex) + + def subscriptable_sempred(self, localctx:SubscriptableContext, predIndex:int): + if predIndex == 0: + return self.tryCast(int) + + + def sliceable_sempred(self, localctx:SliceableContext, predIndex:int): + if predIndex == 1: + return self.tryCast(int) + + + if predIndex == 2: + return self.tryCast(int) + + + + + diff --git a/jsonpath2/parser/__init__.py b/jsonpath2/parser/__init__.py new file mode 100644 index 0000000..8c592c1 --- /dev/null +++ b/jsonpath2/parser/__init__.py @@ -0,0 +1,363 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The jsonpath parser module.""" +import antlr4 +from jsonpath2.expressions.operator import AndVariadicOperatorExpression, EqualBinaryOperatorExpression, \ + GreaterThanBinaryOperatorExpression, GreaterThanOrEqualToBinaryOperatorExpression, \ + LessThanBinaryOperatorExpression, LessThanOrEqualToBinaryOperatorExpression, \ + NotEqualBinaryOperatorExpression, NotUnaryOperatorExpression, OrVariadicOperatorExpression +from jsonpath2.expressions.some import SomeExpression +from jsonpath2.nodes.current import CurrentNode +from jsonpath2.nodes.recursivedescent import RecursiveDescentNode +from jsonpath2.nodes.root import RootNode +from jsonpath2.nodes.subscript import SubscriptNode +from jsonpath2.nodes.terminal import TerminalNode + +from jsonpath2.parser.JSONPathLexer import JSONPathLexer +from jsonpath2.parser.JSONPathListener import JSONPathListener +from jsonpath2.parser.JSONPathParser import JSONPathParser + +from jsonpath2.subscripts.arrayindex import ArrayIndexSubscript +from jsonpath2.subscripts.arrayslice import ArraySliceSubscript +from jsonpath2.subscripts.filter import FilterSubscript +from jsonpath2.subscripts.objectindex import ObjectIndexSubscript +from jsonpath2.subscripts.wildcard import WildcardSubscript + + +class _ConsoleErrorListener(antlr4.error.ErrorListener.ConsoleErrorListener): + # pylint: disable=too-many-arguments + # this is an error handling issue with antlr + def syntaxError(self, recognizer, offendingSymbol, line, column, msg, e): + raise ValueError('line {}:{} {}'.format(line, column, msg)) + # pylint: enable=too-many-arguments + + +class _JSONPathListener(JSONPathListener): + def __init__(self, _stack=None): + super(_JSONPathListener, self).__init__() + self._stack = _stack if _stack else [] + + def exitJsonpath(self, ctx: JSONPathParser.JsonpathContext): + if ctx.getToken(JSONPathParser.ROOT_VALUE, 0) is not None: + if bool(ctx.subscript()): + next_node = self._stack.pop() + else: + next_node = TerminalNode() + self._stack.append(RootNode(next_node)) + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + + # pylint: disable=too-many-branches + # It would sure be nice if we had a case statement. + def exitSubscript(self, ctx: JSONPathParser.SubscriptContext): + if ctx.getToken(JSONPathParser.RECURSIVE_DESCENT, 0) is not None: + if bool(ctx.subscript()): + next_node = self._stack.pop() + else: + next_node = TerminalNode() + if bool(ctx.subscriptableBareword()): + subscriptable_nodes = [self._stack.pop()] + elif bool(ctx.subscriptables()): + subscriptable_nodes = self._stack.pop() + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + self._stack.append(RecursiveDescentNode( + SubscriptNode(next_node, subscriptable_nodes))) + elif ctx.getToken(JSONPathParser.SUBSCRIPT, 0) is not None: + if bool(ctx.subscript()): + next_node = self._stack.pop() + else: + next_node = TerminalNode() + if bool(ctx.subscriptableBareword()): + subscriptable_nodes = [self._stack.pop()] + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + self._stack.append(SubscriptNode(next_node, subscriptable_nodes)) + else: + if bool(ctx.subscript()): + next_node = self._stack.pop() + else: + next_node = TerminalNode() + if bool(ctx.subscriptables()): + subscriptable_nodes = self._stack.pop() + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + self._stack.append(SubscriptNode(next_node, subscriptable_nodes)) + # pylint: enable=too-many-branches + + def exitSubscriptables(self, ctx: JSONPathParser.SubscriptablesContext): + subscriptable_nodes = [] + for _subscriptable_ctx in ctx.getTypedRuleContexts(JSONPathParser.SubscriptableContext): + subscriptable_node = self._stack.pop() + subscriptable_nodes.insert(0, subscriptable_node) + self._stack.append(subscriptable_nodes) + + def exitSubscriptableBareword(self, ctx: JSONPathParser.SubscriptableBarewordContext): + if bool(ctx.ID()): + text = ctx.ID().getText() + self._stack.append(ObjectIndexSubscript(text)) + elif ctx.getToken(JSONPathParser.WILDCARD_SUBSCRIPT, 0) is not None: + self._stack.append(WildcardSubscript()) + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + + def exitSubscriptable(self, ctx: JSONPathParser.SubscriptableContext): + if bool(ctx.STRING()): + text = ctx.STRING().getText()[1:-1] + + self._stack.append(ObjectIndexSubscript(text)) + elif bool(ctx.NUMBER()): + if bool(ctx.sliceable()): + func = self._stack.pop() + + start = int(ctx.NUMBER().getText()) if bool( + ctx.NUMBER()) else None + + self._stack.append(func(start)) + else: + index = int(ctx.NUMBER().getText()) + + self._stack.append(ArrayIndexSubscript(index)) + elif bool(ctx.sliceable()): + func = self._stack.pop() + + start = None + + self._stack.append(func(start)) + elif ctx.getToken(JSONPathParser.WILDCARD_SUBSCRIPT, 0) is not None: + self._stack.append(WildcardSubscript()) + elif ctx.getToken(JSONPathParser.QUESTION, 0) is not None: + expression = self._stack.pop() + + self._stack.append(FilterSubscript(expression)) + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + + def exitSliceable(self, ctx: JSONPathParser.SliceableContext): + end = int(ctx.NUMBER(0).getText()) if bool( + ctx.NUMBER(0)) else None + + step = int(ctx.NUMBER(1).getText()) if bool( + ctx.NUMBER(1)) else None + + self._stack.append(lambda start: ArraySliceSubscript(start, end, step)) + + def exitAndExpression(self, ctx: JSONPathParser.AndExpressionContext): + expressions = [] + + if bool(ctx.andExpression()): + expression = self._stack.pop() + + if isinstance(expression, AndVariadicOperatorExpression): + expressions = expression.expressions + expressions + else: + expressions.insert(0, expression) + + expression = self._stack.pop() + + if isinstance(expression, AndVariadicOperatorExpression): + expressions = expression.expressions + expressions + else: + expressions.insert(0, expression) + + if not expressions: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + if len(expressions) == 1: + self._stack.append(expressions[0]) + else: + self._stack.append(AndVariadicOperatorExpression(expressions)) + + def exitOrExpression(self, ctx: JSONPathParser.OrExpressionContext): + expressions = [] + + if bool(ctx.orExpression()): + expression = self._stack.pop() + + if isinstance(expression, OrVariadicOperatorExpression): + expressions = expression.expressions + expressions + else: + expressions.insert(0, expression) + + expression = self._stack.pop() + + if isinstance(expression, OrVariadicOperatorExpression): + expressions = expression.expressions + expressions + else: + expressions.insert(0, expression) + + if not expressions: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + if len(expressions) == 1: + self._stack.append(expressions[0]) + else: + self._stack.append(OrVariadicOperatorExpression(expressions)) + + # pylint: disable=too-many-branches + def exitNotExpression(self, ctx: JSONPathParser.NotExpressionContext): + if ctx.getToken(JSONPathParser.NOT, 0) is not None: + expression = self._stack.pop() + + if isinstance(expression, NotUnaryOperatorExpression): + self._stack.append(expression.expression) + else: + self._stack.append(NotUnaryOperatorExpression(expression)) + elif (ctx.getToken(JSONPathParser.ROOT_VALUE, 0) is not None) or \ + (ctx.getToken(JSONPathParser.CURRENT_VALUE, 0) is not None): + if bool(ctx.value()): + right_value = self._stack.pop() + + if bool(ctx.subscript()): + next_node = self._stack.pop() + else: + next_node = TerminalNode() + + if ctx.getToken(JSONPathParser.ROOT_VALUE, 0) is not None: + left_node = RootNode(next_node) + elif ctx.getToken(JSONPathParser.CURRENT_VALUE, 0) is not None: + left_node = CurrentNode(next_node) + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + + if ctx.getToken(JSONPathParser.EQ, 0) is not None: + self._stack.append( + EqualBinaryOperatorExpression(left_node, right_value)) + elif ctx.getToken(JSONPathParser.NE, 0) is not None: + self._stack.append( + NotEqualBinaryOperatorExpression(left_node, right_value)) + elif ctx.getToken(JSONPathParser.LT, 0) is not None: + self._stack.append( + LessThanBinaryOperatorExpression(left_node, right_value)) + elif ctx.getToken(JSONPathParser.LE, 0) is not None: + self._stack.append( + LessThanOrEqualToBinaryOperatorExpression(left_node, right_value)) + elif ctx.getToken(JSONPathParser.GT, 0) is not None: + self._stack.append( + GreaterThanBinaryOperatorExpression(left_node, right_value)) + elif ctx.getToken(JSONPathParser.GE, 0) is not None: + self._stack.append( + GreaterThanOrEqualToBinaryOperatorExpression(left_node, right_value)) + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + else: + if bool(ctx.subscript()): + next_node = self._stack.pop() + else: + next_node = TerminalNode() + + self._stack.append(SomeExpression(CurrentNode(next_node))) + else: + pass + # pylint: enable=too-many-branches + + def exitObj(self, ctx: JSONPathParser.ObjContext): + values = [] + + for pair_ctx in ctx.getTypedRuleContexts(JSONPathParser.PairContext): + value = self._stack.pop() + + values.insert(0, value) + + obj = {} + + for index, pair_ctx in enumerate(ctx.getTypedRuleContexts(JSONPathParser.PairContext)): + key = pair_ctx.STRING().getText()[1:-1] + + obj[key] = values[index] + + self._stack.append(obj) + + def exitArray(self, ctx: JSONPathParser.ArrayContext): + array = [] + + for _value_ctx in ctx.getTypedRuleContexts(JSONPathParser.ValueContext): + value = self._stack.pop() + + array.insert(0, value) + + self._stack.append(array) + + def exitValue(self, ctx: JSONPathParser.ValueContext): + if bool(ctx.STRING()): + text = ctx.STRING().getText()[1:-1] + + self._stack.append(text) + elif bool(ctx.NUMBER()): + text = ctx.NUMBER().getText() + + if ('.' in text) or ('E' in text) or ('e' in text): + self._stack.append(float(text)) + else: + self._stack.append(int(text)) + elif bool(ctx.obj()): + pass + elif bool(ctx.array()): + pass + elif ctx.getToken(JSONPathParser.TRUE, 0) is not None: + self._stack.append(True) + elif ctx.getToken(JSONPathParser.FALSE, 0) is not None: + self._stack.append(False) + elif ctx.getToken(JSONPathParser.NULL, 0) is not None: + self._stack.append(None) + else: + # NOTE Unreachable when listener is used as tree walker. + raise ValueError() # pragma: no cover + + +class _JSONPathParser(JSONPathParser): + # pylint: disable=invalid-name + # this is a antlr ism... + def tryCast(self, cls): + """Override the antlr tryCast method.""" + try: + cls(self._input.LT(-1).text) + return True + except ValueError: + return False + # pylint: enable=invalid-name + + +def _parse_input_stream(input_stream: antlr4.InputStream) -> RootNode: + error_listener = _ConsoleErrorListener() + + lexer = JSONPathLexer(input_stream) + + lexer.addErrorListener(error_listener) + + token_stream = antlr4.CommonTokenStream(lexer) + + parser = _JSONPathParser(token_stream) + + parser.addErrorListener(error_listener) + + tree = parser.jsonpath() + + listener = _JSONPathListener(_stack=[]) + + walker = antlr4.ParseTreeWalker() + walker.walk(listener, tree) + + # pylint: disable=protected-access + return listener._stack.pop() + # pylint: enable=protected-access + + +def parse_file(*args, **kwargs) -> RootNode: + """Parse a json path from a file.""" + file_stream = antlr4.FileStream(*args, **kwargs) + return _parse_input_stream(file_stream) + + +def parse_str(*args, **kwargs) -> RootNode: + """Parse a json path from a string.""" + input_stream = antlr4.InputStream(*args, **kwargs) + return _parse_input_stream(input_stream) diff --git a/jsonpath2/path.py b/jsonpath2/path.py new file mode 100644 index 0000000..7f771eb --- /dev/null +++ b/jsonpath2/path.py @@ -0,0 +1,41 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The path module.""" +from typing import Generator +from jsonpath2.node import MatchData +from jsonpath2.nodes.root import RootNode +import jsonpath2.parser as _parser + + +class Path(object): + """Path parsetree object.""" + + def __init__(self, root_node: RootNode): + """Constructor saving the root node.""" + super(Path, self).__init__() + if isinstance(root_node, RootNode): + self.root_node = root_node + else: + raise ValueError() + + def __eq__(self, other: object) -> bool: + """Check to see if two paths are the same.""" + return isinstance(other, self.__class__) and (self.__dict__ == other.__dict__) + + def __str__(self) -> str: + """Stringify the path object.""" + return self.root_node.tojsonpath() + + def match(self, root_value: object) -> Generator[MatchData, None, None]: + """Match root value of the path.""" + return self.root_node.match(root_value, root_value) + + @classmethod + def parse_file(cls, *args, **kwargs): + """A handler to parse a file.""" + return cls(_parser.parse_file(*args, **kwargs)) + + @classmethod + def parse_str(cls, *args, **kwargs): + """A handler to parse a string.""" + return cls(_parser.parse_str(*args, **kwargs)) diff --git a/jsonpath2/subscript.py b/jsonpath2/subscript.py new file mode 100644 index 0000000..59e1c9e --- /dev/null +++ b/jsonpath2/subscript.py @@ -0,0 +1,20 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""The Subscript module.""" +from typing import Generator +from jsonpath2.node import Node, MatchData + + +class Subscript(Node): + """Subscript has no value beyond a node other than type.""" + + def __jsonpath__(self) -> Generator[str, None, None]: # pragma: no cover abstract method + """Abstract method to return the jsonpath.""" + pass + + def match( + self, + root_value: object, + current_value: object) -> Generator[MatchData, None, None]: # pragma: no cover abstract method. + """Abstract method to determine a node match.""" + pass diff --git a/jsonpath2/subscripts/__init__.py b/jsonpath2/subscripts/__init__.py new file mode 100644 index 0000000..001373a --- /dev/null +++ b/jsonpath2/subscripts/__init__.py @@ -0,0 +1,3 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Subscripts module contains the various subscripting classes.""" diff --git a/jsonpath2/subscripts/arrayindex.py b/jsonpath2/subscripts/arrayindex.py new file mode 100644 index 0000000..90971c6 --- /dev/null +++ b/jsonpath2/subscripts/arrayindex.py @@ -0,0 +1,34 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Array Index subscript of the parse tree.""" +import json +from typing import Generator +from jsonpath2.node import MatchData +from jsonpath2.subscript import Subscript +from jsonpath2.nodes.subscript import SubscriptNode +from jsonpath2.nodes.terminal import TerminalNode + + +class ArrayIndexSubscript(Subscript): + """Array index subscript object.""" + + def __init__(self, index: int): + """Save the index of the subscript.""" + super(ArrayIndexSubscript, self).__init__() + self.index = index + + def __jsonpath__(self) -> Generator[str, None, None]: + """Dump the json index when rendering jsonpath.""" + yield json.dumps(self.index) + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the root value against the current value.""" + if isinstance(current_value, list): + if self.index < 0: + new_index = self.index + len(current_value) + + if (new_index >= 0) and (new_index < len(current_value)): + return [MatchData(SubscriptNode(TerminalNode(), [self]), root_value, current_value[new_index])] + elif self.index < len(current_value): + return [MatchData(SubscriptNode(TerminalNode(), [self]), root_value, current_value[self.index])] + return [] diff --git a/jsonpath2/subscripts/arrayslice.py b/jsonpath2/subscripts/arrayslice.py new file mode 100644 index 0000000..b2ef2f0 --- /dev/null +++ b/jsonpath2/subscripts/arrayslice.py @@ -0,0 +1,69 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Array slicing module.""" +import itertools +import json +from typing import Generator +from jsonpath2.node import MatchData +from jsonpath2.subscript import Subscript +from jsonpath2.subscripts.arrayindex import ArrayIndexSubscript + + +class ArraySliceSubscript(Subscript): + """Array slice class for the parse tree.""" + + def __init__(self, start: int = None, end: int = None, step: int = None): + """Save the start end and step in the array slice.""" + super(ArraySliceSubscript, self).__init__() + self.start = start + self.end = end + self.step = step + + def __jsonpath__(self) -> Generator[str, None, None]: + """Return the jsonpath of the array slice.""" + if self.start is not None: + yield json.dumps(self.start) + yield ':' + if self.end is not None: + yield json.dumps(self.end) + if self.step is not None: + yield ':' + yield json.dumps(self.step) + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match an array slice between values.""" + if isinstance(current_value, list): + start = None if (self.start is None) else ( + self.start + (len(current_value) if (self.start < 0) else 0)) + + end = None if (self.end is None) else ( + self.end + (len(current_value) if (self.end < 0) else 0)) + + if start is None: + if end is None: + if self.step is None: + indices = range(0, len(current_value)) + else: + indices = range(0, len(current_value), self.step) + else: + if self.step is None: + indices = range(0, end) + else: + indices = range(0, end, self.step) + else: + if end is None: + if self.step is None: + indices = range(start, len(current_value)) + else: + indices = range(start, len(current_value), self.step) + else: + if self.step is None: + indices = range(start, end) + else: + indices = range(start, end, self.step) + + return itertools.chain(*map( + lambda index: ArrayIndexSubscript( + index).match(root_value, current_value), + indices)) + return [] diff --git a/jsonpath2/subscripts/filter.py b/jsonpath2/subscripts/filter.py new file mode 100644 index 0000000..6ba85a6 --- /dev/null +++ b/jsonpath2/subscripts/filter.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Filter parse tree.""" +from typing import Generator +from jsonpath2.expression import Expression +from jsonpath2.node import MatchData +from jsonpath2.subscript import Subscript +from jsonpath2.nodes.terminal import TerminalNode + + +class FilterSubscript(Subscript): + """Filter subscript in the parse tree.""" + + def __init__(self, expression: Expression): + """Save the filter expression.""" + super(FilterSubscript, self).__init__() + self.expression = expression + + def __jsonpath__(self) -> Generator[str, None, None]: + """generate the jsonpath for the filter.""" + yield '?' + yield '(' + for expression_token in self.expression.__jsonpath__(): + yield expression_token + yield ')' + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the filter subscript against the current value.""" + if self.expression.evaluate(root_value, current_value): + return [MatchData(TerminalNode(), root_value, current_value)] + return [] diff --git a/jsonpath2/subscripts/objectindex.py b/jsonpath2/subscripts/objectindex.py new file mode 100644 index 0000000..a4dd71a --- /dev/null +++ b/jsonpath2/subscripts/objectindex.py @@ -0,0 +1,28 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Object index subscript module.""" +import json +from typing import Generator +from jsonpath2.node import MatchData +from jsonpath2.subscript import Subscript +from jsonpath2.nodes.subscript import SubscriptNode +from jsonpath2.nodes.terminal import TerminalNode + + +class ObjectIndexSubscript(Subscript): + """Object index subscript part of the jsonpath parse tree.""" + + def __init__(self, index: str): + """Save the string index into the json object.""" + super(ObjectIndexSubscript, self).__init__() + self.index = index + + def __jsonpath__(self) -> Generator[str, None, None]: + """Yield the dumps of the index.""" + yield json.dumps(self.index) + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the current value against the root value.""" + if isinstance(current_value, dict) and (self.index in current_value): + return [MatchData(SubscriptNode(TerminalNode(), [self]), root_value, current_value[self.index])] + return [] diff --git a/jsonpath2/subscripts/wildcard.py b/jsonpath2/subscripts/wildcard.py new file mode 100644 index 0000000..704dcca --- /dev/null +++ b/jsonpath2/subscripts/wildcard.py @@ -0,0 +1,31 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Wild cart subscript module.""" +import itertools +from typing import Generator +from jsonpath2.node import MatchData +from jsonpath2.subscript import Subscript +from jsonpath2.subscripts.arrayindex import ArrayIndexSubscript +from jsonpath2.subscripts.objectindex import ObjectIndexSubscript + + +class WildcardSubscript(Subscript): + """Wild card subscript part of the parse tree.""" + + def __jsonpath__(self) -> Generator[str, None, None]: + """Yield the '*' wild card character.""" + yield '*' + + def match(self, root_value: object, current_value: object) -> Generator[MatchData, None, None]: + """Match the root value against the current value.""" + if isinstance(current_value, dict): + return itertools.chain(*map( + lambda index: ObjectIndexSubscript( + index).match(root_value, current_value), + current_value.keys())) + elif isinstance(current_value, list): + return itertools.chain(*map( + lambda index: ArrayIndexSubscript( + index).match(root_value, current_value), + range(len(current_value)))) + return [] diff --git a/jsonpath2/test/arrayslice_subscript_test.py b/jsonpath2/test/arrayslice_subscript_test.py new file mode 100644 index 0000000..fed8319 --- /dev/null +++ b/jsonpath2/test/arrayslice_subscript_test.py @@ -0,0 +1,77 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Test the arrayslice object.""" +from unittest import TestCase +from jsonpath2.subscripts.arrayslice import ArraySliceSubscript + + +class TestArraySliceSubscript(TestCase): + """Test the arrayslice base class.""" + + def setUp(self): + """Setup the class.""" + self.root_value = None + self.current_value = list(range(10)) + + def test_arrayslice0(self): + """Test the arrayslice with configuration 000 (base 2).""" + subscript = ArraySliceSubscript(None, None, None) + self.assertEqual(':', subscript.tojsonpath()) + self.assertEqual([0, 1, 2, 3, 4, 5, 6, 7, 8, 9], list(map( + lambda match_data: match_data.current_value, subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice1(self): + """Test the arrayslice with configuration 001 (base 2).""" + subscript = ArraySliceSubscript(None, None, 2) + self.assertEqual('::2', subscript.tojsonpath()) + self.assertEqual([0, 2, 4, 6, 8], list(map( + lambda match_data: match_data.current_value, subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice2(self): + """Test the arrayslice with configuration 010 (base 2).""" + subscript = ArraySliceSubscript(None, 7, None) + self.assertEqual(':7', subscript.tojsonpath()) + self.assertEqual([0, 1, 2, 3, 4, 5, 6], list(map( + lambda match_data: match_data.current_value, subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice3(self): + """Test the arrayslice with configuration 011 (base 2).""" + subscript = ArraySliceSubscript(None, 7, 2) + self.assertEqual(':7:2', subscript.tojsonpath()) + self.assertEqual([0, 2, 4, 6], list(map( + lambda match_data: match_data.current_value, subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice4(self): + """Test the arrayslice with configuration 100 (base 2).""" + subscript = ArraySliceSubscript(5, None, None) + self.assertEqual('5:', subscript.tojsonpath()) + self.assertEqual([5, 6, 7, 8, 9], list(map( + lambda match_data: match_data.current_value, subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice5(self): + """Test the arrayslice with configuration 101 (base 2).""" + subscript = ArraySliceSubscript(5, None, 2) + self.assertEqual('5::2', subscript.tojsonpath()) + self.assertEqual([5, 7, 9], list(map(lambda match_data: match_data.current_value, + subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice6(self): + """Test the arrayslice with configuration 110 (base 2).""" + subscript = ArraySliceSubscript(5, 7, None) + self.assertEqual('5:7', subscript.tojsonpath()) + self.assertEqual([5, 6], list(map(lambda match_data: match_data.current_value, + subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice7(self): + """Test the arrayslice with configuration 111 (base 2).""" + subscript = ArraySliceSubscript(5, 7, 2) + self.assertEqual('5:7:2', subscript.tojsonpath()) + self.assertEqual([5], list(map(lambda match_data: match_data.current_value, + subscript.match(self.root_value, self.current_value)))) + + def test_arrayslice_not_list(self): + """Test the arrayslice with non-list input.""" + subscript = ArraySliceSubscript() + root_value = None + self.assertTrue(not isinstance(root_value, list)) + self.assertEqual([], list(subscript.match(root_value, root_value))) diff --git a/jsonpath2/test/bookstore_test.py b/jsonpath2/test/bookstore_test.py new file mode 100644 index 0000000..f2e5487 --- /dev/null +++ b/jsonpath2/test/bookstore_test.py @@ -0,0 +1,192 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Test the jsonpath module.""" +from unittest import TestCase +from jsonpath2.path import Path + + +class TestBookStore(TestCase): + """ + Test the bookstore from original jsonpath article. + + http://goessner.net/articles/JsonPath/ + """ + + def setUp(self): + """Setup the class.""" + self.root_value = { + 'store': { + 'book': [ + { + 'category': 'reference', + 'author': 'Nigel Rees', + '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 + } + } + } + + def test_bookstore_examples_1(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$.store.book[*].author') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + for auth in ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']: + self.assertTrue(auth in matches) + + def test_bookstore_examples_2(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$..author') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + for auth in ['Nigel Rees', 'Evelyn Waugh', 'Herman Melville', 'J. R. R. Tolkien']: + self.assertTrue(auth in matches) + + def test_bookstore_examples_3(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$.store.*') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertTrue(isinstance(matches[0], list)) + self.assertTrue(isinstance(matches[1], dict)) + self.assertEqual(matches[0][0]['author'], 'Nigel Rees') + self.assertEqual(matches[1]['color'], 'red') + + def test_bookstore_examples_4(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$.store..price') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + for price in [8.95, 12.99, 8.99, 22.99, 19.95]: + self.assertTrue(price in matches) + + def test_bookstore_examples_5(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$..book[2]') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'fiction') + self.assertEqual(matches[0]['author'], 'Herman Melville') + self.assertEqual(matches[0]['title'], 'Moby Dick') + self.assertEqual(matches[0]['isbn'], '0-553-21311-3') + self.assertEqual(matches[0]['price'], 8.99) + + def test_bookstore_examples_6(self): + """Test the bookstore examples.""" + for path in ['$..book[-1:]', '$..book[-1]', '$..book[3:4:1]']: + expr = Path.parse_str(path) + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'fiction') + self.assertEqual(matches[0]['author'], 'J. R. R. Tolkien') + self.assertEqual(matches[0]['title'], 'The Lord of the Rings') + self.assertEqual(matches[0]['isbn'], '0-395-19395-8') + self.assertEqual(matches[0]['price'], 22.99) + + def test_bookstore_examples_7(self): + """Test the bookstore examples.""" + for path in ['$..book[0,1]', '$..book[:2]', '$..book[:2:1]']: + expr = Path.parse_str(path) + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'reference') + self.assertEqual(matches[0]['author'], 'Nigel Rees') + self.assertEqual(matches[0]['title'], 'Sayings of the Century') + self.assertEqual(matches[0]['price'], 8.95) + self.assertEqual(matches[1]['category'], 'fiction') + self.assertEqual(matches[1]['author'], 'Evelyn Waugh') + self.assertEqual(matches[1]['title'], 'Sword of Honour') + self.assertEqual(matches[1]['price'], 12.99) + + def test_bookstore_examples_8(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$..book[*][?(@.isbn)]') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'fiction') + self.assertEqual(matches[0]['author'], 'Herman Melville') + self.assertEqual(matches[0]['title'], 'Moby Dick') + self.assertEqual(matches[0]['isbn'], '0-553-21311-3') + self.assertEqual(matches[0]['price'], 8.99) + self.assertEqual(matches[1]['category'], 'fiction') + self.assertEqual(matches[1]['author'], 'J. R. R. Tolkien') + self.assertEqual(matches[1]['title'], 'The Lord of the Rings') + self.assertEqual(matches[1]['isbn'], '0-395-19395-8') + self.assertEqual(matches[1]['price'], 22.99) + + def test_bookstore_examples_9(self): + """Test the bookstore examples.""" + for path in ['$..book[*][?(@.price<10)]', '$..book[*][?(@.price<=10)]']: + expr = Path.parse_str(path) + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'reference') + self.assertEqual(matches[0]['author'], 'Nigel Rees') + self.assertEqual(matches[0]['title'], 'Sayings of the Century') + self.assertEqual(matches[0]['price'], 8.95) + self.assertEqual(matches[1]['category'], 'fiction') + self.assertEqual(matches[1]['author'], 'Herman Melville') + self.assertEqual(matches[1]['title'], 'Moby Dick') + self.assertEqual(matches[1]['isbn'], '0-553-21311-3') + self.assertEqual(matches[1]['price'], 8.99) + + def test_bookstore_examples_10(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$..book[*][?(@.author = "Nigel Rees")]') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'reference') + self.assertEqual(matches[0]['author'], 'Nigel Rees') + self.assertEqual(matches[0]['title'], 'Sayings of the Century') + self.assertEqual(matches[0]['price'], 8.95) + + def test_bookstore_examples_11(self): + """Test the bookstore examples.""" + expr = Path.parse_str('$..book[*][?(@.category != "fiction")]') + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'reference') + self.assertEqual(matches[0]['author'], 'Nigel Rees') + self.assertEqual(matches[0]['title'], 'Sayings of the Century') + self.assertEqual(matches[0]['price'], 8.95) + + def test_bookstore_examples_12(self): + """Test the bookstore examples.""" + for path in ['$..book[*][?(@.price>10)]', '$..book[*][?(@.price>=10)]']: + expr = Path.parse_str(path) + self.assertEqual(Path.parse_str(str(expr)), expr) + matches = [x.current_value for x in expr.match(self.root_value)] + self.assertEqual(matches[0]['category'], 'fiction') + self.assertEqual(matches[0]['author'], 'Evelyn Waugh') + self.assertEqual(matches[0]['title'], 'Sword of Honour') + self.assertEqual(matches[0]['price'], 12.99) + self.assertEqual(matches[1]['category'], 'fiction') + self.assertEqual(matches[1]['author'], 'J. R. R. Tolkien') + self.assertEqual(matches[1]['title'], 'The Lord of the Rings') + self.assertEqual(matches[1]['isbn'], '0-395-19395-8') + self.assertEqual(matches[1]['price'], 22.99) diff --git a/jsonpath2/test/example_test.py b/jsonpath2/test/example_test.py deleted file mode 100644 index 6a9e063..0000000 --- a/jsonpath2/test/example_test.py +++ /dev/null @@ -1,23 +0,0 @@ -#!/usr/bin/python -# -*- coding: utf-8 -*- -"""Test the example module.""" -from unittest import TestCase -from jsonpath2 import Example - - -class TestExample(TestCase): - """Test the example class.""" - - def test_add(self): - """Test the add method in example class.""" - self.assertEqual(Example().add('123', 'abc'), - '123abc', 'sum of strings should work') - self.assertEqual(Example().add(123, 456), 579, - 'sum of integers should work') - - def test_mul(self): - """Test the mul method in example class.""" - self.assertEqual(Example().mul('a', 4), 'aaaa', - 'multiply of string and number should work') - self.assertEqual(Example().mul(2, 3), 6, - 'multiply of two integers should work') diff --git a/jsonpath2/test/expression_test.py b/jsonpath2/test/expression_test.py new file mode 100644 index 0000000..2406ddc --- /dev/null +++ b/jsonpath2/test/expression_test.py @@ -0,0 +1,84 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Test the expression object.""" +from unittest import TestCase +from jsonpath2.expressions.operator import OperatorExpression, AndVariadicOperatorExpression +from jsonpath2.expressions.some import SomeExpression +from jsonpath2.nodes.current import CurrentNode +from jsonpath2.nodes.terminal import TerminalNode +from jsonpath2.path import Path + + +class TestExpression(TestCase): + """Test the expression base class.""" + + def test_expression(self): + """Test the base expression equal.""" + obj_a = OperatorExpression() + obj_b = OperatorExpression() + self.assertTrue(obj_a == obj_b) + + def test_failure_expressions(self): + """Test expressions that make no sense.""" + data = {'hello': 'Hello, World!'} + for path in [ + '$[?(@.hello < "string")]', + '$[?(@.hello <= "string")]', + '$[?(@.hello > "string")]', + '$[?(@.hello >= "string")]', + '$[?(@.hello = {"bar": "baz"})]', + '$[?(@.hello = ["bar", "baz"])]', + '$[?(@.hello = 42)]', + '$[?(@.hello = 3.14159)]', + '$[?(@.hello = false)]', + '$[?(@.hello = true)]', + '$[?(@.hello = null)]' + ]: + print(path) + expr = Path.parse_str(path) + self.assertFalse([x for x in expr.match(data)]) + + def test_unary_operator(self): + """Test the unary not in a path.""" + data = [ + { + 'hello': 'Hello, World!', + 'bar': False + } + ] + expr = Path.parse_str('$[?(not @.bar)]') + self.assertEqual(Path.parse_str(str(expr)), expr) + self.assertTrue([x for x in expr.match(data)]) + + def test_unary_boolean_operator(self): + """Test the unary not in a path.""" + data = [ + { + 'hello': 'Hello, World!', + 'bar': False + } + ] + expr = Path.parse_str('$[?(not (@.bar or (@.fiz > 2 and @.biz > 2)))]') + self.assertEqual(Path.parse_str(str(expr)), expr) + self.assertTrue([x for x in expr.match(data)]) + + def test_variadic_operator(self): + """Test a variadic operator.""" + expr = AndVariadicOperatorExpression([]) + self.assertEqual('', expr.tojsonpath()) + expr = AndVariadicOperatorExpression( + [SomeExpression(CurrentNode(TerminalNode()))]) + self.assertEqual('@', expr.tojsonpath()) + + def test_binary_operator(self): + """Test a binary operator.""" + expr = Path.parse_str('$[?(@ and (@ and @))]') + self.assertEqual('$[?(@ and @ and @)]', str(expr)) + expr = Path.parse_str('$[?(@ or (@ or @))]') + self.assertEqual('$[?(@ or @ or @)]', str(expr)) + expr = Path.parse_str('$[?(not not @)]') + self.assertEqual('$[?(@)]', str(expr)) + expr = Path.parse_str('$[?($ = null)]') + self.assertEqual('$[?($ = null)]', str(expr)) + with self.assertRaises(ValueError): + Path.parse_str('$[3.14156:]') diff --git a/jsonpath2/test/jsonpath2_test.py b/jsonpath2/test/jsonpath2_test.py new file mode 100644 index 0000000..52421af --- /dev/null +++ b/jsonpath2/test/jsonpath2_test.py @@ -0,0 +1,366 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Test the jsonpath module.""" +from tempfile import NamedTemporaryFile +from unittest import TestCase +from json import loads +from jsonpath2.node import MatchData +from jsonpath2.expressions.some import SomeExpression +from jsonpath2.nodes.current import CurrentNode +from jsonpath2.nodes.recursivedescent import RecursiveDescentNode +from jsonpath2.nodes.root import RootNode +from jsonpath2.nodes.subscript import SubscriptNode +from jsonpath2.nodes.terminal import TerminalNode +from jsonpath2.path import Path +from jsonpath2.subscripts.arrayindex import ArrayIndexSubscript +from jsonpath2.subscripts.filter import FilterSubscript +from jsonpath2.subscripts.objectindex import ObjectIndexSubscript +from jsonpath2.subscripts.wildcard import WildcardSubscript + + +class TestNode(TestCase): + """Test the node object.""" + + def setUp(self): + """Setup the class.""" + root_value = { + 'hello': 'Hello, world!', + 'languages': [ + 'en-GB', + 'en-US', + ], + } + current_value = root_value['hello'] + + self._state = [ + { + '__jsonpath__': '', + 'node': TerminalNode(), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [ + MatchData(TerminalNode(), root_value, current_value), + ], + }, + { + '__jsonpath__': '$', + 'node': RootNode(TerminalNode()), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [ + MatchData(RootNode(TerminalNode()), + root_value, root_value), + ], + }, + { + '__jsonpath__': '@', + 'node': CurrentNode(TerminalNode()), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [ + MatchData(CurrentNode(TerminalNode()), + root_value, current_value), + ], + }, + { + '__jsonpath__': '[]', + 'node': SubscriptNode(TerminalNode(), []), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [], + }, + { + '__jsonpath__': '[?(@)]', + 'node': SubscriptNode(TerminalNode(), [FilterSubscript(SomeExpression(CurrentNode(TerminalNode())))]), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [ + MatchData(TerminalNode(), root_value, current_value), + ], + }, + { + '__jsonpath__': '[?(@),?(@)]', + 'node': SubscriptNode( + TerminalNode(), + [ + FilterSubscript(SomeExpression( + CurrentNode(TerminalNode()))), + FilterSubscript(SomeExpression( + CurrentNode(TerminalNode()))) + ] + ), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [ + MatchData(TerminalNode(), root_value, current_value), + MatchData(TerminalNode(), root_value, current_value), + ], + }, + { + '__jsonpath__': '[*]', + 'node': SubscriptNode(TerminalNode(), [WildcardSubscript()]), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [], + }, + { + '__jsonpath__': '[*]', + 'node': SubscriptNode(TerminalNode(), [WildcardSubscript()]), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'hello')]), root_value, root_value['hello']), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'languages')]), root_value, root_value['languages']), + ], + }, + { + '__jsonpath__': '["languages"][*]', + 'node': SubscriptNode( + SubscriptNode(TerminalNode(), [WildcardSubscript()]), + [ObjectIndexSubscript('languages')] + ), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(0)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][0] + ), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(1)]), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][1] + ), + ], + }, + { + '__jsonpath__': '["hello","languages"]', + 'node': SubscriptNode( + TerminalNode(), + [ObjectIndexSubscript('hello'), + ObjectIndexSubscript('languages')] + ), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'hello')]), root_value, root_value['hello']), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'languages')]), root_value, root_value['languages']), + ], + }, + { + '__jsonpath__': '..', + 'node': RecursiveDescentNode(TerminalNode()), + 'root_value': root_value, + 'current_value': current_value, + 'match_data_list': [ + MatchData(TerminalNode(), root_value, current_value), + ], + }, + { + '__jsonpath__': '..', + 'node': RecursiveDescentNode(TerminalNode()), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData(TerminalNode(), root_value, root_value), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'hello')]), root_value, root_value['hello']), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'languages')]), root_value, root_value['languages']), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(0)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][0] + ), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(1)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][1] + ), + ], + }, + { + '__jsonpath__': '..[?(@)]', + 'node': RecursiveDescentNode( + SubscriptNode( + TerminalNode(), + [ + FilterSubscript(SomeExpression( + CurrentNode(TerminalNode()))) + ] + ) + ), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData(TerminalNode(), root_value, root_value), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'hello')]), root_value, root_value['hello']), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'languages')]), root_value, root_value['languages']), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(0)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][0] + ), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(1)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][1] + ), + ], + }, + { + '__jsonpath__': '..[*]', + 'node': RecursiveDescentNode(SubscriptNode(TerminalNode(), [WildcardSubscript()])), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'hello')]), root_value, root_value['hello']), + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'languages')]), root_value, root_value['languages']), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(0)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][0] + ), + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(1)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][1] + ), + ], + }, + { + '__jsonpath__': '..["hello"]', + 'node': RecursiveDescentNode(SubscriptNode(TerminalNode(), [ObjectIndexSubscript('hello')])), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData(SubscriptNode(TerminalNode(), [ObjectIndexSubscript( + 'hello')]), root_value, root_value['hello']), + ], + }, + { + '__jsonpath__': '..[0]', + 'node': RecursiveDescentNode(SubscriptNode(TerminalNode(), [ArrayIndexSubscript(0)])), + 'root_value': root_value, + 'current_value': root_value, + 'match_data_list': [ + MatchData( + SubscriptNode( + SubscriptNode( + TerminalNode(), + [ArrayIndexSubscript(0)] + ), + [ObjectIndexSubscript('languages')] + ), + root_value, + root_value['languages'][0] + ), + ], + }, + ] + + def _assert_node_test_case(self, **kwargs): + self.assertEqual(kwargs['__jsonpath__'], kwargs['node'].tojsonpath()) + + if isinstance(kwargs['node'], RootNode): + self.assertEqual(kwargs['node'], Path.parse_str( + kwargs['__jsonpath__']).root_node) + + with NamedTemporaryFile() as temp_file: + temp_file.write(bytes(kwargs['__jsonpath__'], 'utf-8')) + temp_file.seek(0) + + self.assertEqual(kwargs['node'], Path.parse_file( + temp_file.name).root_node) + + else: + with self.assertRaises(ValueError): + Path(kwargs['node']) + + with self.assertRaises(ValueError): + Path.parse_str('__jsonpath__') + + match_data_list = list(kwargs['node'].match( + kwargs['root_value'], kwargs['current_value'])) + + for index, value in enumerate(match_data_list): + self.assertEqual(kwargs['match_data_list'][index], value) + + for match_data in match_data_list: + new_match_data_list = list(match_data.node.match( + kwargs['root_value'], kwargs['current_value'])) + + self.assertEqual([match_data], new_match_data_list) + + def test_state(self): + """Test the state.""" + for kwargs in self._state: + self._assert_node_test_case(**kwargs) + + def test_example(self): + """Test the example from the README.md.""" + test_string = '{"hello":"Hello, world!"}' + test_json = loads(test_string) + path_expr = Path.parse_str('$["hello"]') + result = list( + map(lambda match_data: match_data.current_value, path_expr.match(test_json))) + self.assertEqual(result[0], 'Hello, world!') + result = list( + map(lambda match_data: match_data.node.tojsonpath(), path_expr.match(test_json))) + self.assertEqual(result[0], '$["hello"]') diff --git a/jsonpath2/test/subscript_node_test.py b/jsonpath2/test/subscript_node_test.py new file mode 100644 index 0000000..b03fb12 --- /dev/null +++ b/jsonpath2/test/subscript_node_test.py @@ -0,0 +1,56 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""Test the subscript object.""" +from unittest import TestCase +from jsonpath2.node import MatchData +from jsonpath2.nodes.subscript import SubscriptNode +from jsonpath2.nodes.terminal import TerminalNode +from jsonpath2.subscript import Subscript + + +class TestSubscriptNode(TestCase): + """Test the subscript base class.""" + + def test_badsubscript1(self): + """Test subscript that does not provide terminal or other subscript.""" + class BadSubscript1(Subscript): + """Subscript that does not provide terminal or other subscript.""" + + def __jsonpath__(self): + """Empty.""" + return [] + + def match(self, root_value, current_value): + """One.""" + yield MatchData(None, root_value, current_value) + + self.assertEqual('', BadSubscript1().tojsonpath()) + + with self.assertRaises(ValueError): + # NOTE Use 'list' to force the computation to occur, raising any exceptions. + list(SubscriptNode( + TerminalNode(), + [BadSubscript1()] + ).match(None, None)) + + def test_badsubscript2(self): + """Test subscript that provides other subscript but not subscripted-terminal.""" + class BadSubscript2(Subscript): + """Subscript that provides other subscript but not subscripted-terminal.""" + + def __jsonpath__(self): + """Empty.""" + return [] + + def match(self, root_value, current_value): + """One.""" + yield MatchData(SubscriptNode(None), root_value, current_value) + + self.assertEqual('', BadSubscript2().tojsonpath()) + + with self.assertRaises(ValueError): + # NOTE Use 'list' to force the computation to occur, raising any exceptions. + list(SubscriptNode( + TerminalNode(), + [BadSubscript2()] + ).match(None, None)) diff --git a/jsonpath2/tojsonpath.py b/jsonpath2/tojsonpath.py new file mode 100644 index 0000000..345f913 --- /dev/null +++ b/jsonpath2/tojsonpath.py @@ -0,0 +1,18 @@ +#!/usr/bin/python +# -*- coding: utf-8 -*- +"""A JSONPath abstract class.""" +from abc import ABC, abstractmethod +from typing import Generator + + +class ToJSONPath(ABC): + """Abstract class which calls internal method.""" + + def tojsonpath(self) -> str: + """Get the json path from self and return it.""" + return ''.join(list(self.__jsonpath__())) + + @abstractmethod + def __jsonpath__(self) -> Generator[str, None, None]: # pragma: no cover abstract method + """Abstract method to return the jsonpath.""" + raise NotImplementedError() diff --git a/requirements-dev.txt b/requirements-dev.txt index f698807..57c100e 100644 --- a/requirements-dev.txt +++ b/requirements-dev.txt @@ -1,6 +1,8 @@ +antlr4-python3-runtime coverage pep257 pre-commit pylint<2.0 pytest setuptools +six diff --git a/requirements.txt b/requirements.txt index e69de29..b58d864 100644 --- a/requirements.txt +++ b/requirements.txt @@ -0,0 +1,3 @@ +antlr4-python3-runtime +setuptools +six diff --git a/setup.py b/setup.py index a67eed3..ecbc3e8 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,7 @@ #!/usr/bin/python # -*- coding: utf-8 -*- -"""Setup and install JSONPath2 library.""" +"""Setup the python package for jsonpath2.""" +from os import path try: # pip version 9 from pip.req import parse_requirements except ImportError: @@ -16,9 +17,13 @@ setup_requires=['setuptools_scm'], license='LGPLv3', url='https://pypi.python.org/pypi/jsonpath2/', - description='JSONPath Parser', - author='David Brown', - author_email='dmlb2000@gmail.com', + description='JSONPath implementation for Python', + long_description=open(path.join( + path.abspath(path.dirname(__file__)), + 'README.md'), encoding='utf-8').read(), + long_description_content_type='text/markdown', + author='Mark Borkum', + author_email='mark.borkum@pnnl.gov', packages=find_packages(), install_requires=[str(ir.req) for ir in INSTALL_REQS] )