diff --git a/README.rst b/README.rst index 1dcdecc..a1652a5 100644 --- a/README.rst +++ b/README.rst @@ -40,14 +40,16 @@ Or, if you have virtualenvwrapper installed:: Extensions ---------- -+--------------+-------------------------------+ -| name | Example | -+==============+===============================+ -| len | $.objects.`len` | -+--------------+-------------------------------+ -| sorted | $.objects.`sorted` | -+--------------+-------------------------------+ -| filter | $.objects[?(@some_field > 5)] | -+--------------+-------------------------------+ ++--------------+--------------------------------------------+ +| name | Example | ++==============+============================================+ +| len | $.objects.`len` | ++--------------+--------------------------------------------+ +| sorted | $.objects.`sorted` | +| | $.objects[\some_field] | +| | $.objects[\some_field,/other_field] | ++--------------+--------------------------------------------+ +| filter | $.objects[?(@some_field > 5)] | ++--------------+--------------------------------------------+ diff --git a/jsonpath_rw_ext/_iterable.py b/jsonpath_rw_ext/_iterable.py index 3b56ed2..0b13a18 100644 --- a/jsonpath_rw_ext/_iterable.py +++ b/jsonpath_rw_ext/_iterable.py @@ -11,22 +11,59 @@ # License for the specific language governing permissions and limitations # under the License. +import functools import jsonpath_rw class SortedThis(jsonpath_rw.This): """The JSONPath referring to the sorted version of the current object. - Concrete syntax is '`sorted`'. + Concrete syntax is '`sorted`' or [\\field,/field]. """ + def __init__(self, expressions=None): + self.expressions = expressions + + def _compare(self, left, right): + left = jsonpath_rw.DatumInContext.wrap(left) + right = jsonpath_rw.DatumInContext.wrap(right) + + for expr in self.expressions: + field, reverse = expr + l_datum = field.find(left) + r_datum = field.find(right) + if (not l_datum or not r_datum or + len(l_datum) > 1 or len(r_datum) > 1 or + l_datum[0].value == r_datum[0].value): + # NOTE(sileht): should we do something if the expression + # match multiple fields, for now ignore them + continue + elif l_datum[0].value < r_datum[0].value: + return 1 if reverse else -1 + else: + return -1 if reverse else 1 + return 0 def find(self, datum): """Return sorted value of This if list or dict.""" + if isinstance(datum.value, dict) and self.expressions: + return datum + if isinstance(datum.value, dict) or isinstance(datum.value, list): + key = (functools.cmp_to_key(self._compare) + if self.expressions else None) return [jsonpath_rw.DatumInContext.wrap(value) - for value in sorted(datum.value)] + for value in sorted(datum.value, key=key)] return datum + def __eq__(self, other): + return isinstance(other, Len) + + def __repr__(self): + return '%s(%r)' % (self.__class__.__name__, self.expressions) + + def __str__(self): + return '[?%s]' % self.expressions + class Len(jsonpath_rw.JSONPath): """The JSONPath referring to the len of the current object. diff --git a/jsonpath_rw_ext/parser.py b/jsonpath_rw_ext/parser.py index 642df53..b9fbc25 100644 --- a/jsonpath_rw_ext/parser.py +++ b/jsonpath_rw_ext/parser.py @@ -28,10 +28,11 @@ class ExtendedJsonPathLexer(lexer.JsonPathLexer): """Custom LALR-lexer for JsonPath""" literals = lexer.JsonPathLexer.literals + ['?', '@'] tokens = (parser.JsonPathLexer.tokens + - ['FILTER_OP', 'FILTER_VALUE']) + ['FILTER_OP', 'FILTER_VALUE', 'SORT_DIRECTION']) t_FILTER_VALUE = r'\w+' t_FILTER_OP = r'(==?|<=|>=|!=|<|>)' + t_SORT_DIRECTION = r'(/|\\)' def t_ID(self, t): r'@?[a-zA-Z_][a-zA-Z0-9_@\-]*' @@ -92,6 +93,24 @@ class ExtentedJsonPathParser(parser.JsonPathParser): "jsonpath : jsonpath '[' filter ']'" p[0] = jsonpath_rw.Child(p[1], p[3]) + def p_sort(self, p): + "sort : SORT_DIRECTION ID" + field = jsonpath_rw.Fields(p[2]) + p[0] = (field, p[1] != '/') + + def p_sorts_sort(self, p): + "sorts : sort" + p[0] = [p[1]] + + def p_sorts_comma(self, p): + "sorts : sorts ',' sorts" + p[0] = p[1] + p[3] + + def p_jsonpath_sort(self, p): + "jsonpath : jsonpath '[' sorts ']'" + sort = _iterable.SortedThis(p[3]) + p[0] = jsonpath_rw.Child(p[1], sort) + def p_jsonpath_this(self, p): "jsonpath : '@'" p[0] = jsonpath_rw.This() diff --git a/jsonpath_rw_ext/tests/test_jsonpath_rw_ext.py b/jsonpath_rw_ext/tests/test_jsonpath_rw_ext.py index b52fc91..453812a 100644 --- a/jsonpath_rw_ext/tests/test_jsonpath_rw_ext.py +++ b/jsonpath_rw_ext/tests/test_jsonpath_rw_ext.py @@ -36,6 +36,7 @@ class TestJsonpath_rw_ext(testscenarios.WithScenarios, data={'objects': {'cow': 'moo', 'horse': 'neigh', 'cat': 'meow'}}, target=['cat', 'cow', 'horse'])), + ('len_list', dict(string='objects.`len`', data={'objects': ['alpha', 'gamma', 'beta']}, target=3)), @@ -45,6 +46,7 @@ class TestJsonpath_rw_ext(testscenarios.WithScenarios, ('len_str', dict(string='objects[0].`len`', data={'objects': ['alpha', 'gamma']}, target=5)), + ('filter_exists_syntax1', dict(string='objects[?cow]', data={'objects': [{'cow': 'moo'}, {'cat': 'neigh'}]}, @@ -85,6 +87,27 @@ class TestJsonpath_rw_ext(testscenarios.WithScenarios, {'cow': 8, 'cat': 3}]}, target=[{'cow': 8, 'cat': 2}, {'cow': 7, 'cat': 2}])), + + ('sort1', dict(string='objects[/cow]', + data={'objects': [{'cat': 1, 'cow': 2}, + {'cat': 2, 'cow': 1}, + {'cat': 3, 'cow': 3}]}, + target=[{'cat': 2, 'cow': 1}, + {'cat': 1, 'cow': 2}, + {'cat': 3, 'cow': 3}])), + ('sort2', dict(string='objects[\\cat]', + data={'objects': [{'cat': 2}, {'cat': 1}, {'cat': 3}]}, + target=[{'cat': 3}, {'cat': 2}, {'cat': 1}])), + ('sort3', dict(string='objects[/cow,\\cat]', + data={'objects': [{'cat': 1, 'cow': 2}, + {'cat': 2, 'cow': 1}, + {'cat': 3, 'cow': 1}, + {'cat': 3, 'cow': 3}]}, + target=[{'cat': 3, 'cow': 1}, + {'cat': 2, 'cow': 1}, + {'cat': 1, 'cow': 2}, + {'cat': 3, 'cow': 3}])), + ('real_life_example1', dict( string="payload.metrics[?(@.name='cpu.frequency')].value", data={'payload': {'metrics': [