Skip to content

Commit 2f7c608

Browse files
committed
Support json query filter
Related bug: kennknowles#8
1 parent e745706 commit 2f7c608

File tree

5 files changed

+150
-5
lines changed

5 files changed

+150
-5
lines changed

jsonpath_rw/jsonpath.py

+88
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
from six.moves import xrange
55
from itertools import *
66
import functools
7+
import operator
78

89
logger = logging.getLogger(__name__)
910

@@ -563,3 +564,90 @@ def __repr__(self):
563564

564565
def __str__(self):
565566
return '[?%s]' % self.expressions
567+
568+
569+
OPERATOR_MAP = {
570+
'!=': operator.ne,
571+
'==': operator.eq,
572+
'=': operator.eq,
573+
'<=': operator.le,
574+
'<': operator.lt,
575+
'>=': operator.ge,
576+
'>': operator.gt,
577+
}
578+
579+
580+
class Filter(JSONPath):
581+
"""The JSONQuery filter"""
582+
583+
def __init__(self, expressions):
584+
self.expressions = expressions
585+
586+
def find(self, datum):
587+
if not self.expressions:
588+
return []
589+
590+
datum = DatumInContext.wrap(datum)
591+
return [DatumInContext(datum.value[i],
592+
path=Index(i),
593+
context=datum)
594+
for i in xrange(0, len(datum.value))
595+
if (len(self.expressions) ==
596+
len(list(filter(lambda x: x.find(datum.value[i]),
597+
self.expressions))))]
598+
599+
def __repr__(self):
600+
return '%s(%r)' % (self.__class__.__name__, self.expressions)
601+
602+
def __str__(self):
603+
return '[?%s]' % self.expressions
604+
605+
606+
class FilterExpression(JSONPath):
607+
"""The JSONQuery expression"""
608+
609+
def __init__(self, target, op, value):
610+
self.target = target
611+
self.op = op
612+
self.value = value
613+
614+
def find(self, datum):
615+
datum = self.target.find(DatumInContext.wrap(datum))
616+
617+
if not datum:
618+
return []
619+
if self.op is None:
620+
return datum
621+
622+
found = []
623+
for data in datum:
624+
value = data.value
625+
if isinstance(self.value, int):
626+
try:
627+
value = int(value)
628+
except ValueError:
629+
continue
630+
631+
if OPERATOR_MAP[self.op](value, self.value):
632+
found.append(data)
633+
634+
return found
635+
636+
def __eq__(self, other):
637+
return (isinstance(other, Filter) and
638+
self.target == other.target and
639+
self.op == other.op and
640+
self.value == other.value)
641+
642+
def __repr__(self):
643+
if self.op is None:
644+
return '%s(%r)' % (self.__class__.__name__, self.target)
645+
else:
646+
return '%s(%r %s %r)' % (self.__class__.__name__,
647+
self.target, self.op, self.value)
648+
649+
def __str__(self):
650+
if self.op is None:
651+
return '%s' % self.target
652+
else:
653+
return '%s %s %s' % (self.target, self.op, self.value)

jsonpath_rw/lexer.py

+5-3
Original file line numberDiff line numberDiff line change
@@ -46,11 +46,12 @@ def tokenize(self, string):
4646
#
4747
# Anyhow, it is pythonic to give some rope to hang oneself with :-)
4848

49-
literals = ['*', '.', '[', ']', '(', ')', '$', ',', ':', '|', '&']
49+
literals = ['*', '.', '[', ']', '(', ')', '$', ',', ':', '|', '&', '@', '?']
5050

5151
reserved_words = { 'where': 'WHERE' }
5252

53-
tokens = ['DOUBLEDOT', 'NUMBER', 'ID', 'NAMED_OPERATOR', 'SORT_DIRECTION'] + list(reserved_words.values())
53+
tokens = ['DOUBLEDOT', 'NUMBER', 'ID', 'NAMED_OPERATOR', 'SORT_DIRECTION',
54+
'FILTER_OP'] + list(reserved_words.values())
5455

5556
states = [ ('singlequote', 'exclusive'),
5657
('doublequote', 'exclusive'),
@@ -59,9 +60,10 @@ def tokenize(self, string):
5960
# Normal lexing, rather easy
6061
t_DOUBLEDOT = r'\.\.'
6162
t_ignore = ' \t'
63+
t_FILTER_OP = r'(==?|<=|>=|!=|<|>)'
6264

6365
def t_ID(self, t):
64-
r'[a-zA-Z_@][a-zA-Z0-9_@\-]*'
66+
r'@?[a-zA-Z_][a-zA-Z0-9_@\-]*'
6567
t.type = self.reserved_words.get(t.value, 'ID')
6668
return t
6769

jsonpath_rw/parser.py

+35
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,41 @@ def p_jsonpath_sort(self, p):
187187
sort = SortedThis(p[3])
188188
p[0] = Child(p[1], sort)
189189

190+
def p_jsonpath_this(self, p):
191+
"jsonpath : '@'"
192+
p[0] = This()
193+
194+
def p_expression(self, p):
195+
"""expression : jsonpath
196+
| jsonpath FILTER_OP ID
197+
| jsonpath FILTER_OP NUMBER
198+
"""
199+
if len(p) == 2:
200+
left, op, right = p[1], None, None
201+
else:
202+
__, left, op, right = p
203+
p[0] = FilterExpression(left, op, right)
204+
205+
def p_expressions_expression(self, p):
206+
"expressions : expression"
207+
p[0] = [p[1]]
208+
209+
def p_expressions_and(self, p):
210+
"expressions : expressions '&' expressions"
211+
p[0] = p[1] + p[3]
212+
213+
def p_expressions_parens(self, p):
214+
"expressions : '(' expressions ')'"
215+
p[0] = p[2]
216+
217+
def p_filter(self, p):
218+
"filter : '?' expressions "
219+
p[0] = Filter(p[2])
220+
221+
def p_jsonpath_filter(self, p):
222+
"jsonpath : jsonpath '[' filter ']'"
223+
p[0] = Child(p[1], p[3])
224+
190225

191226
class IteratorToTokenStream(object):
192227
def __init__(self, iterator):

tests/test_jsonpath.py

+13
Original file line numberDiff line numberDiff line change
@@ -127,6 +127,19 @@ def test_sort_value(self):
127127
[{'cat': {'dog': 2, 'cow': 1}}, {'cat': {'dog': 2, 'bow': 2}}, {'cat': {'dog': 3, 'cow': 2}}, {'cat': {'dog': 1, 'bow': 3}}]),
128128
])
129129

130+
def test_filter_value(self):
131+
jsonpath.auto_id_field = None
132+
self.check_cases([
133+
('objects[?cow]', {'objects': [{'cow': 'moo'}, {'cat': 'neigh'}]}, [{'cow': 'moo'}]),
134+
('objects[[email protected]]', {'objects': [{'cow': 'moo'}, {'cat': 'neigh'}]}, [{'cow': 'moo'}]),
135+
('objects[?(@.cow)]', {'objects': [{'cow': 'moo'}, {'cat': 'neigh'}]}, [{'cow': 'moo'}]),
136+
('objects[?(@."cow!?cat")]', {'objects': [{'cow!?cat': 'moo'}, {'cat': 'neigh'}]}, [{'cow!?cat': 'moo'}]),
137+
('objects[?cow="moo"]', {'objects': [{'cow': 'moo'}, {'cow': 'neigh'}, {'cat': 'neigh'}]}, [{'cow': 'moo'}]),
138+
('objects[?(@.["cow"]="moo")]', {'objects': [{'cow': 'moo'}, {'cow': 'neigh'}, {'cat': 'neigh'}]}, [{'cow': 'moo'}]),
139+
('objects[?cow=="moo"]', {'objects': [{'cow': 'moo'}, {'cow': 'neigh'}, {'cat': 'neigh'}]}, [{'cow': 'moo'}]),
140+
('objects[?cow>5]', {'objects': [{'cow': 8}, {'cow': 7}, {'cow': 5}, {'cow': 'neigh'}]}, [{'cow': 8}, {'cow': 7}]),
141+
('objects[?cow>5&cat=2]', {'objects': [{'cow': 8, 'cat': 2}, {'cow': 7, 'cat': 2}, {'cow': 2, 'cat': 2}, {'cow': 5, 'cat': 3}, {'cow': 8, 'cat': 3}]}, [{'cow': 8, 'cat': 2}, {'cow': 7, 'cat': 2}]),
142+
])
130143

131144
def test_root_value(self):
132145
jsonpath.auto_id_field = None

tests/test_lexer.py

+9-2
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,20 @@ def test_simple_inputs(self):
4949
self.assert_lex_equiv('fuzz.*', [self.token('fuzz', 'ID'), self.token('.', '.'), self.token('*', '*')])
5050
self.assert_lex_equiv('fuzz..bang', [self.token('fuzz', 'ID'), self.token('..', 'DOUBLEDOT'), self.token('bang', 'ID')])
5151
self.assert_lex_equiv('&', [self.token('&', '&')])
52-
self.assert_lex_equiv('@', [self.token('@', 'ID')])
52+
self.assert_lex_equiv('@', [self.token('@', '@')])
53+
self.assert_lex_equiv('?', [self.token('?', '?')])
5354
self.assert_lex_equiv('`this`', [self.token('this', 'NAMED_OPERATOR')])
5455
self.assert_lex_equiv('|', [self.token('|', '|')])
5556
self.assert_lex_equiv('where', [self.token('where', 'WHERE')])
5657
self.assert_lex_equiv('/', [self.token('/', 'SORT_DIRECTION')])
5758
self.assert_lex_equiv('\\', [self.token('\\', 'SORT_DIRECTION')])
59+
self.assert_lex_equiv('==', [self.token('==', 'FILTER_OP')])
60+
self.assert_lex_equiv('=', [self.token('=', 'FILTER_OP')])
61+
self.assert_lex_equiv('<=', [self.token('<=', 'FILTER_OP')])
62+
self.assert_lex_equiv('<', [self.token('<', 'FILTER_OP')])
63+
self.assert_lex_equiv('>=', [self.token('>=', 'FILTER_OP')])
64+
self.assert_lex_equiv('>', [self.token('>', 'FILTER_OP')])
65+
self.assert_lex_equiv('!=', [self.token('!=', 'FILTER_OP')])
5866

5967
def test_basic_errors(self):
6068
def tokenize(s):
@@ -67,5 +75,4 @@ def tokenize(s):
6775
self.assertRaises(JsonPathLexerError, tokenize, "`'")
6876
self.assertRaises(JsonPathLexerError, tokenize, '"`')
6977
self.assertRaises(JsonPathLexerError, tokenize, "'`")
70-
self.assertRaises(JsonPathLexerError, tokenize, '?')
7178
self.assertRaises(JsonPathLexerError, tokenize, '$.foo.bar.#')

0 commit comments

Comments
 (0)