1#
2# Licensed under the Apache License, Version 2.0 (the "License"); you may
3# not use this file except in compliance with the License. You may obtain
4# a copy of the License at
5#
6#      http://www.apache.org/licenses/LICENSE-2.0
7#
8# Unless required by applicable law or agreed to in writing, software
9# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
10# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
11# License for the specific language governing permissions and limitations
12# under the License.
13
14from .. import lexer
15from .. import parser
16from .. import Fields, This, Child
17
18from . import arithmetic as _arithmetic
19from . import filter as _filter
20from . import iterable as _iterable
21from . import string as _string
22
23
24class ExtendedJsonPathLexer(lexer.JsonPathLexer):
25    """Custom LALR-lexer for JsonPath"""
26    literals = lexer.JsonPathLexer.literals + ['?', '@', '+', '*', '/', '-']
27    tokens = (['BOOL'] +
28              parser.JsonPathLexer.tokens +
29              ['FILTER_OP', 'SORT_DIRECTION', 'FLOAT'])
30
31    t_FILTER_OP = r'=~|==?|<=|>=|!=|<|>'
32
33    def t_BOOL(self, t):
34        r'true|false'
35        t.value = True if t.value == 'true' else False
36        return t
37
38    def t_SORT_DIRECTION(self, t):
39        r',?\s*(/|\\)'
40        t.value = t.value[-1]
41        return t
42
43    def t_ID(self, t):
44        r'@?[a-zA-Z_][a-zA-Z0-9_@\-]*'
45        # NOTE(sileht): This fixes the ID expression to be
46        # able to use @ for `This` like any json query
47        t.type = self.reserved_words.get(t.value, 'ID')
48        return t
49
50    def t_FLOAT(self, t):
51        r'-?\d+\.\d+'
52        t.value = float(t.value)
53        return t
54
55
56class ExtentedJsonPathParser(parser.JsonPathParser):
57    """Custom LALR-parser for JsonPath"""
58
59    tokens = ExtendedJsonPathLexer.tokens
60
61    def __init__(self, debug=False, lexer_class=None):
62        lexer_class = lexer_class or ExtendedJsonPathLexer
63        super(ExtentedJsonPathParser, self).__init__(debug, lexer_class)
64
65    def p_jsonpath_operator_jsonpath(self, p):
66        """jsonpath : NUMBER operator NUMBER
67                    | FLOAT operator FLOAT
68                    | ID operator ID
69                    | NUMBER operator jsonpath
70                    | FLOAT operator jsonpath
71                    | jsonpath operator NUMBER
72                    | jsonpath operator FLOAT
73                    | jsonpath operator jsonpath
74        """
75
76        # NOTE(sileht): If we have choice between a field or a string we
77        # always choice string, because field can be full qualified
78        # like $.foo == foo and where string can't.
79        for i in [1, 3]:
80            if (isinstance(p[i], Fields) and len(p[i].fields) == 1):  # noqa
81                p[i] = p[i].fields[0]
82
83        p[0] = _arithmetic.Operation(p[1], p[2], p[3])
84
85    def p_operator(self, p):
86        """operator : '+'
87                    | '-'
88                    | '*'
89                    | '/'
90        """
91        p[0] = p[1]
92
93    def p_jsonpath_named_operator(self, p):
94        "jsonpath : NAMED_OPERATOR"
95        if p[1] == 'len':
96            p[0] = _iterable.Len()
97        elif p[1] == 'sorted':
98            p[0] = _iterable.SortedThis()
99        elif p[1].startswith("split("):
100            p[0] = _string.Split(p[1])
101        elif p[1].startswith("sub("):
102            p[0] = _string.Sub(p[1])
103        elif p[1].startswith("str("):
104            p[0] = _string.Str(p[1])
105        else:
106            super(ExtentedJsonPathParser, self).p_jsonpath_named_operator(p)
107
108    def p_expression(self, p):
109        """expression : jsonpath
110                      | jsonpath FILTER_OP ID
111                      | jsonpath FILTER_OP FLOAT
112                      | jsonpath FILTER_OP NUMBER
113                      | jsonpath FILTER_OP BOOL
114        """
115        if len(p) == 2:
116            left, op, right = p[1], None, None
117        else:
118            __, left, op, right = p
119        p[0] = _filter.Expression(left, op, right)
120
121    def p_expressions_expression(self, p):
122        "expressions : expression"
123        p[0] = [p[1]]
124
125    def p_expressions_and(self, p):
126        "expressions : expressions '&' expressions"
127        # TODO(sileht): implements '|'
128        p[0] = p[1] + p[3]
129
130    def p_expressions_parens(self, p):
131        "expressions : '(' expressions ')'"
132        p[0] = p[2]
133
134    def p_filter(self, p):
135        "filter : '?' expressions "
136        p[0] = _filter.Filter(p[2])
137
138    def p_jsonpath_filter(self, p):
139        "jsonpath : jsonpath '[' filter ']'"
140        p[0] = Child(p[1], p[3])
141
142    def p_sort(self, p):
143        "sort : SORT_DIRECTION jsonpath"
144        p[0] = (p[2], p[1] != "/")
145
146    def p_sorts_sort(self, p):
147        "sorts : sort"
148        p[0] = [p[1]]
149
150    def p_sorts_comma(self, p):
151        "sorts : sorts sorts"
152        p[0] = p[1] + p[2]
153
154    def p_jsonpath_sort(self, p):
155        "jsonpath : jsonpath '[' sorts ']'"
156        sort = _iterable.SortedThis(p[3])
157        p[0] = Child(p[1], sort)
158
159    def p_jsonpath_this(self, p):
160        "jsonpath : '@'"
161        p[0] = This()
162
163    precedence = [
164        ('left', '+', '-'),
165        ('left', '*', '/'),
166    ] + parser.JsonPathParser.precedence + [
167        ('nonassoc', 'ID'),
168    ]
169
170
171def parse(path, debug=False):
172    return ExtentedJsonPathParser(debug=debug).parse(path)
173