jql_ruby 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +21 -0
- data/LICENSE +21 -0
- data/README.md +231 -0
- data/lib/jql_ruby/adapters/active_record.rb +75 -0
- data/lib/jql_ruby/adapters/base.rb +154 -0
- data/lib/jql_ruby/adapters/configuration.rb +50 -0
- data/lib/jql_ruby/adapters.rb +5 -0
- data/lib/jql_ruby/ast/compound_clause.rb +31 -0
- data/lib/jql_ruby/ast/field.rb +47 -0
- data/lib/jql_ruby/ast/node.rb +25 -0
- data/lib/jql_ruby/ast/not_clause.rb +19 -0
- data/lib/jql_ruby/ast/operand.rb +58 -0
- data/lib/jql_ruby/ast/operator.rb +28 -0
- data/lib/jql_ruby/ast/order_by.rb +32 -0
- data/lib/jql_ruby/ast/predicate.rb +21 -0
- data/lib/jql_ruby/ast/query.rb +19 -0
- data/lib/jql_ruby/ast/terminal_clause.rb +21 -0
- data/lib/jql_ruby/ast.rb +12 -0
- data/lib/jql_ruby/errors.rb +26 -0
- data/lib/jql_ruby/lexer.rb +245 -0
- data/lib/jql_ruby/parser.rb +576 -0
- data/lib/jql_ruby/result.rb +16 -0
- data/lib/jql_ruby/token.rb +66 -0
- data/lib/jql_ruby/version.rb +5 -0
- data/lib/jql_ruby.rb +19 -0
- metadata +125 -0
|
@@ -0,0 +1,576 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
class Parser
|
|
5
|
+
attr_reader :errors
|
|
6
|
+
|
|
7
|
+
def initialize(tokens)
|
|
8
|
+
@tokens = tokens
|
|
9
|
+
@pos = 0
|
|
10
|
+
@errors = []
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def parse
|
|
14
|
+
query = parse_query
|
|
15
|
+
expect(TokenType::EOF)
|
|
16
|
+
query
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
# query = where? orderBy? EOF
|
|
22
|
+
def parse_query
|
|
23
|
+
where_clause = nil
|
|
24
|
+
order_by = nil
|
|
25
|
+
|
|
26
|
+
unless current_type == TokenType::EOF
|
|
27
|
+
if current_type == TokenType::ORDER
|
|
28
|
+
order_by = parse_order_by
|
|
29
|
+
else
|
|
30
|
+
where_clause = parse_where
|
|
31
|
+
order_by = parse_order_by if current_type == TokenType::ORDER
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
Ast::Query.new(where_clause: where_clause, order_by: order_by, position: 0)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# where = orClause
|
|
39
|
+
def parse_where
|
|
40
|
+
parse_or_clause
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# orClause = andClause (OR andClause)*
|
|
44
|
+
def parse_or_clause
|
|
45
|
+
pos = current_position
|
|
46
|
+
left = parse_and_clause
|
|
47
|
+
clauses = [left]
|
|
48
|
+
|
|
49
|
+
while current_type == TokenType::OR
|
|
50
|
+
advance
|
|
51
|
+
clauses << parse_and_clause
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
return clauses.first if clauses.size == 1
|
|
55
|
+
|
|
56
|
+
Ast::OrClause.new(clauses: clauses, position: pos)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
# andClause = notClause (AND notClause)*
|
|
60
|
+
def parse_and_clause
|
|
61
|
+
pos = current_position
|
|
62
|
+
left = parse_not_clause
|
|
63
|
+
clauses = [left]
|
|
64
|
+
|
|
65
|
+
while current_type == TokenType::AND
|
|
66
|
+
advance
|
|
67
|
+
clauses << parse_not_clause
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
return clauses.first if clauses.size == 1
|
|
71
|
+
|
|
72
|
+
Ast::AndClause.new(clauses: clauses, position: pos)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# notClause = NOT notClause | '!' notClause | subClause | terminalClause
|
|
76
|
+
def parse_not_clause
|
|
77
|
+
pos = current_position
|
|
78
|
+
|
|
79
|
+
if current_type == TokenType::NOT
|
|
80
|
+
advance
|
|
81
|
+
clause = parse_not_clause
|
|
82
|
+
return Ast::NotClause.new(clause: clause, operator: :not, position: pos)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
if current_type == TokenType::BANG
|
|
86
|
+
advance
|
|
87
|
+
clause = parse_not_clause
|
|
88
|
+
return Ast::NotClause.new(clause: clause, operator: :bang, position: pos)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if current_type == TokenType::LPAREN
|
|
92
|
+
return parse_sub_clause
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
parse_terminal_clause
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
# subClause = '(' orClause ')'
|
|
99
|
+
def parse_sub_clause
|
|
100
|
+
expect(TokenType::LPAREN)
|
|
101
|
+
clause = parse_or_clause
|
|
102
|
+
expect(TokenType::RPAREN)
|
|
103
|
+
clause
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# terminalClause = field terminalClauseRhs
|
|
107
|
+
def parse_terminal_clause
|
|
108
|
+
pos = current_position
|
|
109
|
+
field = parse_field
|
|
110
|
+
|
|
111
|
+
parse_terminal_clause_rhs(field, pos)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# terminalClauseRhs dispatches based on operator lookahead
|
|
115
|
+
def parse_terminal_clause_rhs(field, pos)
|
|
116
|
+
case current_type
|
|
117
|
+
when TokenType::EQUALS, TokenType::NOT_EQUALS
|
|
118
|
+
parse_equals_clause(field, pos)
|
|
119
|
+
when TokenType::LT, TokenType::GT, TokenType::LTEQ, TokenType::GTEQ
|
|
120
|
+
parse_comparison_clause(field, pos)
|
|
121
|
+
when TokenType::LIKE, TokenType::NOT_LIKE
|
|
122
|
+
parse_like_clause(field, pos)
|
|
123
|
+
when TokenType::NOT
|
|
124
|
+
# NOT IN
|
|
125
|
+
if peek_type == TokenType::IN
|
|
126
|
+
parse_in_clause(field, pos)
|
|
127
|
+
else
|
|
128
|
+
error("unexpected NOT after field")
|
|
129
|
+
end
|
|
130
|
+
when TokenType::IN
|
|
131
|
+
parse_in_clause(field, pos)
|
|
132
|
+
when TokenType::IS
|
|
133
|
+
parse_is_clause(field, pos)
|
|
134
|
+
when TokenType::WAS
|
|
135
|
+
parse_was_clause(field, pos)
|
|
136
|
+
when TokenType::CHANGED
|
|
137
|
+
parse_changed_clause(field, pos)
|
|
138
|
+
else
|
|
139
|
+
error("expected operator after field '#{field_name(field)}'")
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
# equalsClause = ('=' | '!=') (value | function | empty)
|
|
144
|
+
def parse_equals_clause(field, pos)
|
|
145
|
+
op_value = current_type == TokenType::EQUALS ? :eq : :not_eq
|
|
146
|
+
advance
|
|
147
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
148
|
+
|
|
149
|
+
operand = if current_type == TokenType::EMPTY || current_type == TokenType::NULL
|
|
150
|
+
parse_keyword_operand
|
|
151
|
+
else
|
|
152
|
+
parse_value_or_function
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
Ast::TerminalClause.new(field: field, operator: operator, operand: operand, position: pos)
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
# comparisonClause = ('<' | '>' | '<=' | '>=') (value | function)
|
|
159
|
+
def parse_comparison_clause(field, pos)
|
|
160
|
+
op_map = {
|
|
161
|
+
TokenType::LT => :lt,
|
|
162
|
+
TokenType::GT => :gt,
|
|
163
|
+
TokenType::LTEQ => :lteq,
|
|
164
|
+
TokenType::GTEQ => :gteq,
|
|
165
|
+
}
|
|
166
|
+
op_value = op_map[current_type]
|
|
167
|
+
advance
|
|
168
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
169
|
+
operand = parse_value_or_function
|
|
170
|
+
|
|
171
|
+
Ast::TerminalClause.new(field: field, operator: operator, operand: operand, position: pos)
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
# likeClause = ('~' | '!~') (value | function | empty)
|
|
175
|
+
def parse_like_clause(field, pos)
|
|
176
|
+
op_value = current_type == TokenType::LIKE ? :like : :not_like
|
|
177
|
+
advance
|
|
178
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
179
|
+
|
|
180
|
+
operand = if current_type == TokenType::EMPTY || current_type == TokenType::NULL
|
|
181
|
+
parse_keyword_operand
|
|
182
|
+
else
|
|
183
|
+
parse_value_or_function
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
Ast::TerminalClause.new(field: field, operator: operator, operand: operand, position: pos)
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# inClause = [NOT] IN (list | function)
|
|
190
|
+
def parse_in_clause(field, pos)
|
|
191
|
+
negated = false
|
|
192
|
+
if current_type == TokenType::NOT
|
|
193
|
+
negated = true
|
|
194
|
+
advance
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
expect(TokenType::IN)
|
|
198
|
+
op_value = negated ? :not_in : :in
|
|
199
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
200
|
+
|
|
201
|
+
operand = if current_type == TokenType::LPAREN
|
|
202
|
+
parse_list_operand
|
|
203
|
+
else
|
|
204
|
+
parse_function
|
|
205
|
+
end
|
|
206
|
+
|
|
207
|
+
Ast::TerminalClause.new(field: field, operator: operator, operand: operand, position: pos)
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
# isClause = IS [NOT] empty
|
|
211
|
+
def parse_is_clause(field, pos)
|
|
212
|
+
advance
|
|
213
|
+
negated = false
|
|
214
|
+
if current_type == TokenType::NOT
|
|
215
|
+
negated = true
|
|
216
|
+
advance
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
op_value = negated ? :is_not : :is
|
|
220
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
221
|
+
operand = parse_keyword_operand
|
|
222
|
+
|
|
223
|
+
Ast::TerminalClause.new(field: field, operator: operator, operand: operand, position: pos)
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# wasClause / wasInClause
|
|
227
|
+
def parse_was_clause(field, pos)
|
|
228
|
+
advance
|
|
229
|
+
|
|
230
|
+
negated = false
|
|
231
|
+
if current_type == TokenType::NOT
|
|
232
|
+
negated = true
|
|
233
|
+
advance
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# WAS [NOT] IN => wasInClause
|
|
237
|
+
if current_type == TokenType::IN
|
|
238
|
+
return parse_was_in_clause(field, pos, negated)
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
op_value = negated ? :was_not : :was
|
|
242
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
243
|
+
operand = parse_value_or_function
|
|
244
|
+
predicates = parse_predicates
|
|
245
|
+
|
|
246
|
+
Ast::TerminalClause.new(
|
|
247
|
+
field: field, operator: operator, operand: operand,
|
|
248
|
+
predicates: predicates, position: pos
|
|
249
|
+
)
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def parse_was_in_clause(field, pos, negated)
|
|
253
|
+
advance
|
|
254
|
+
op_value = negated ? :was_not_in : :was_in
|
|
255
|
+
operator = Ast::Operator.new(value: op_value, position: pos)
|
|
256
|
+
|
|
257
|
+
operand = if current_type == TokenType::LPAREN
|
|
258
|
+
parse_list_operand
|
|
259
|
+
else
|
|
260
|
+
parse_function
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
predicates = parse_predicates
|
|
264
|
+
|
|
265
|
+
Ast::TerminalClause.new(
|
|
266
|
+
field: field, operator: operator, operand: operand,
|
|
267
|
+
predicates: predicates, position: pos
|
|
268
|
+
)
|
|
269
|
+
end
|
|
270
|
+
|
|
271
|
+
# changedClause = CHANGED changedPredicate*
|
|
272
|
+
def parse_changed_clause(field, pos)
|
|
273
|
+
advance
|
|
274
|
+
operator = Ast::Operator.new(value: :changed, position: pos)
|
|
275
|
+
predicates = parse_predicates
|
|
276
|
+
|
|
277
|
+
Ast::TerminalClause.new(
|
|
278
|
+
field: field, operator: operator,
|
|
279
|
+
predicates: predicates, position: pos
|
|
280
|
+
)
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
PREDICATE_KEYWORDS = Set.new([
|
|
284
|
+
TokenType::AFTER, TokenType::BEFORE, TokenType::ON,
|
|
285
|
+
TokenType::DURING, TokenType::BY,
|
|
286
|
+
TokenType::FROM, TokenType::TO,
|
|
287
|
+
]).freeze
|
|
288
|
+
|
|
289
|
+
def parse_predicates
|
|
290
|
+
predicates = []
|
|
291
|
+
while PREDICATE_KEYWORDS.include?(current_type)
|
|
292
|
+
predicates << parse_predicate
|
|
293
|
+
end
|
|
294
|
+
predicates
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def parse_predicate
|
|
298
|
+
pos = current_position
|
|
299
|
+
op_map = {
|
|
300
|
+
TokenType::AFTER => :after,
|
|
301
|
+
TokenType::BEFORE => :before,
|
|
302
|
+
TokenType::ON => :on,
|
|
303
|
+
TokenType::DURING => :during,
|
|
304
|
+
TokenType::BY => :by,
|
|
305
|
+
TokenType::FROM => :from,
|
|
306
|
+
TokenType::TO => :to,
|
|
307
|
+
}
|
|
308
|
+
op = op_map[current_type]
|
|
309
|
+
advance
|
|
310
|
+
|
|
311
|
+
operand = if current_type == TokenType::LPAREN
|
|
312
|
+
parse_list_operand
|
|
313
|
+
else
|
|
314
|
+
parse_value_or_function
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
Ast::Predicate.new(operator: op, operand: operand, position: pos)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
# field = string fieldProperty* | number | customField fieldProperty*
|
|
321
|
+
def parse_field
|
|
322
|
+
pos = current_position
|
|
323
|
+
|
|
324
|
+
case current_type
|
|
325
|
+
when TokenType::NUMBER
|
|
326
|
+
name = current_value
|
|
327
|
+
advance
|
|
328
|
+
Ast::Field.new(name: name, position: pos)
|
|
329
|
+
when TokenType::STRING, TokenType::QUOTED_STRING
|
|
330
|
+
value = current_value
|
|
331
|
+
advance
|
|
332
|
+
|
|
333
|
+
# check for custom field: cf[12345]
|
|
334
|
+
if value.downcase == "cf" && current_type == TokenType::LBRACKET
|
|
335
|
+
return parse_custom_field(pos)
|
|
336
|
+
end
|
|
337
|
+
|
|
338
|
+
properties = parse_field_properties
|
|
339
|
+
Ast::Field.new(name: value, properties: properties, position: pos)
|
|
340
|
+
else
|
|
341
|
+
error("expected field name")
|
|
342
|
+
# return a placeholder to allow parser to continue
|
|
343
|
+
advance
|
|
344
|
+
Ast::Field.new(name: "", position: pos)
|
|
345
|
+
end
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def parse_custom_field(pos)
|
|
349
|
+
expect(TokenType::LBRACKET)
|
|
350
|
+
id_str = current_value
|
|
351
|
+
expect(TokenType::NUMBER) || expect(TokenType::STRING)
|
|
352
|
+
expect(TokenType::RBRACKET)
|
|
353
|
+
properties = parse_field_properties
|
|
354
|
+
Ast::CustomField.new(id: id_str.to_i, properties: properties, position: pos)
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def parse_field_properties
|
|
358
|
+
properties = []
|
|
359
|
+
while current_type == TokenType::DOT
|
|
360
|
+
properties << parse_field_property
|
|
361
|
+
end
|
|
362
|
+
properties
|
|
363
|
+
end
|
|
364
|
+
|
|
365
|
+
# fieldProperty = '.' propertyArgument+ ('[' argument ']')?
|
|
366
|
+
def parse_field_property
|
|
367
|
+
pos = current_position
|
|
368
|
+
advance
|
|
369
|
+
keys = []
|
|
370
|
+
|
|
371
|
+
while current_type == TokenType::STRING || current_type == TokenType::QUOTED_STRING || current_type == TokenType::NUMBER
|
|
372
|
+
keys << current_value
|
|
373
|
+
advance
|
|
374
|
+
break unless current_type == TokenType::DOT && peek_type != TokenType::LPAREN
|
|
375
|
+
advance if current_type == TokenType::DOT
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
argument = nil
|
|
379
|
+
if current_type == TokenType::LBRACKET
|
|
380
|
+
advance
|
|
381
|
+
argument = current_value if current_type == TokenType::STRING || current_type == TokenType::QUOTED_STRING || current_type == TokenType::NUMBER
|
|
382
|
+
advance
|
|
383
|
+
expect(TokenType::RBRACKET)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
Ast::FieldProperty.new(keys: keys, argument: argument, position: pos)
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# value = string | number
|
|
390
|
+
def parse_value
|
|
391
|
+
pos = current_position
|
|
392
|
+
case current_type
|
|
393
|
+
when TokenType::STRING, TokenType::QUOTED_STRING
|
|
394
|
+
val = current_value
|
|
395
|
+
advance
|
|
396
|
+
Ast::ValueOperand.new(value: val, position: pos)
|
|
397
|
+
when TokenType::NUMBER
|
|
398
|
+
val = current_value
|
|
399
|
+
advance
|
|
400
|
+
Ast::ValueOperand.new(value: val, position: pos)
|
|
401
|
+
else
|
|
402
|
+
error("expected value")
|
|
403
|
+
advance
|
|
404
|
+
Ast::ValueOperand.new(value: "", position: pos)
|
|
405
|
+
end
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
# parse value or function, checking for '(' after a name to distinguish
|
|
409
|
+
def parse_value_or_function
|
|
410
|
+
pos = current_position
|
|
411
|
+
if (current_type == TokenType::STRING || current_type == TokenType::QUOTED_STRING || current_type == TokenType::NUMBER) && peek_type == TokenType::LPAREN
|
|
412
|
+
return parse_function
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
parse_value
|
|
416
|
+
end
|
|
417
|
+
|
|
418
|
+
# function = functionName '(' argumentList? ')'
|
|
419
|
+
def parse_function
|
|
420
|
+
pos = current_position
|
|
421
|
+
name = current_value
|
|
422
|
+
advance
|
|
423
|
+
expect(TokenType::LPAREN)
|
|
424
|
+
|
|
425
|
+
arguments = []
|
|
426
|
+
unless current_type == TokenType::RPAREN
|
|
427
|
+
arguments << parse_argument
|
|
428
|
+
while current_type == TokenType::COMMA
|
|
429
|
+
advance
|
|
430
|
+
arguments << parse_argument
|
|
431
|
+
end
|
|
432
|
+
end
|
|
433
|
+
|
|
434
|
+
expect(TokenType::RPAREN)
|
|
435
|
+
Ast::FunctionOperand.new(name: name, arguments: arguments, position: pos)
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
def parse_argument
|
|
439
|
+
pos = current_position
|
|
440
|
+
case current_type
|
|
441
|
+
when TokenType::STRING, TokenType::QUOTED_STRING, TokenType::NUMBER
|
|
442
|
+
val = current_value
|
|
443
|
+
advance
|
|
444
|
+
Ast::ValueOperand.new(value: val, position: pos)
|
|
445
|
+
else
|
|
446
|
+
error("expected function argument")
|
|
447
|
+
advance
|
|
448
|
+
Ast::ValueOperand.new(value: "", position: pos)
|
|
449
|
+
end
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# list = '(' operand (',' operand)* ')'
|
|
453
|
+
def parse_list_operand
|
|
454
|
+
pos = current_position
|
|
455
|
+
expect(TokenType::LPAREN)
|
|
456
|
+
|
|
457
|
+
values = []
|
|
458
|
+
values << parse_operand
|
|
459
|
+
while current_type == TokenType::COMMA
|
|
460
|
+
advance
|
|
461
|
+
values << parse_operand
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
expect(TokenType::RPAREN)
|
|
465
|
+
Ast::ListOperand.new(values: values, position: pos)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
# operand = value | function | list | empty
|
|
469
|
+
def parse_operand
|
|
470
|
+
if current_type == TokenType::EMPTY || current_type == TokenType::NULL
|
|
471
|
+
return parse_keyword_operand
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
if current_type == TokenType::LPAREN
|
|
475
|
+
return parse_list_operand
|
|
476
|
+
end
|
|
477
|
+
|
|
478
|
+
parse_value_or_function
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def parse_keyword_operand
|
|
482
|
+
pos = current_position
|
|
483
|
+
val = current_type == TokenType::EMPTY ? :empty : :null
|
|
484
|
+
advance
|
|
485
|
+
Ast::KeywordOperand.new(value: val, position: pos)
|
|
486
|
+
end
|
|
487
|
+
|
|
488
|
+
# orderBy = ORDER BY searchSort (',' searchSort)*
|
|
489
|
+
def parse_order_by
|
|
490
|
+
pos = current_position
|
|
491
|
+
expect(TokenType::ORDER)
|
|
492
|
+
expect(TokenType::BY)
|
|
493
|
+
|
|
494
|
+
sorts = []
|
|
495
|
+
sorts << parse_search_sort
|
|
496
|
+
while current_type == TokenType::COMMA
|
|
497
|
+
advance
|
|
498
|
+
sorts << parse_search_sort
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
Ast::OrderBy.new(fields: sorts, position: pos)
|
|
502
|
+
end
|
|
503
|
+
|
|
504
|
+
# searchSort = field (ASC | DESC)?
|
|
505
|
+
def parse_search_sort
|
|
506
|
+
pos = current_position
|
|
507
|
+
field = parse_field
|
|
508
|
+
|
|
509
|
+
direction = nil
|
|
510
|
+
if current_type == TokenType::ASC
|
|
511
|
+
direction = :asc
|
|
512
|
+
advance
|
|
513
|
+
elsif current_type == TokenType::DESC
|
|
514
|
+
direction = :desc
|
|
515
|
+
advance
|
|
516
|
+
end
|
|
517
|
+
|
|
518
|
+
Ast::SearchSort.new(field: field, direction: direction, position: pos)
|
|
519
|
+
end
|
|
520
|
+
|
|
521
|
+
# token helpers
|
|
522
|
+
|
|
523
|
+
def current_token
|
|
524
|
+
@tokens[@pos] || Token.new(type: TokenType::EOF, value: nil, position: @pos)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def current_type
|
|
528
|
+
current_token.type
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def current_value
|
|
532
|
+
current_token.value
|
|
533
|
+
end
|
|
534
|
+
|
|
535
|
+
def current_position
|
|
536
|
+
current_token.position || @pos
|
|
537
|
+
end
|
|
538
|
+
|
|
539
|
+
def peek_token
|
|
540
|
+
@tokens[@pos + 1] || Token.new(type: TokenType::EOF, value: nil, position: @pos)
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def peek_type
|
|
544
|
+
peek_token.type
|
|
545
|
+
end
|
|
546
|
+
|
|
547
|
+
def advance
|
|
548
|
+
@pos += 1
|
|
549
|
+
end
|
|
550
|
+
|
|
551
|
+
def expect(type)
|
|
552
|
+
if current_type == type
|
|
553
|
+
token = current_token
|
|
554
|
+
advance
|
|
555
|
+
token
|
|
556
|
+
else
|
|
557
|
+
error("expected #{type}, got #{current_type} (#{current_value.inspect})")
|
|
558
|
+
nil
|
|
559
|
+
end
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def error(message)
|
|
563
|
+
err = ParseError.new(message, position: current_position)
|
|
564
|
+
@errors << err
|
|
565
|
+
err
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def field_name(field)
|
|
569
|
+
case field
|
|
570
|
+
when Ast::Field then field.name
|
|
571
|
+
when Ast::CustomField then "cf[#{field.id}]"
|
|
572
|
+
else ""
|
|
573
|
+
end
|
|
574
|
+
end
|
|
575
|
+
end
|
|
576
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
Token = Struct.new(:type, :value, :position, keyword_init: true) do
|
|
5
|
+
def keyword?
|
|
6
|
+
KEYWORD_TYPES.include?(type)
|
|
7
|
+
end
|
|
8
|
+
|
|
9
|
+
KEYWORD_TYPES = %i[
|
|
10
|
+
and or not in is was changed
|
|
11
|
+
order by asc desc
|
|
12
|
+
empty null
|
|
13
|
+
after before during on from to
|
|
14
|
+
].freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
module TokenType
|
|
18
|
+
# delimiters
|
|
19
|
+
LPAREN = :lparen
|
|
20
|
+
RPAREN = :rparen
|
|
21
|
+
COMMA = :comma
|
|
22
|
+
LBRACKET = :lbracket
|
|
23
|
+
RBRACKET = :rbracket
|
|
24
|
+
DOT = :dot
|
|
25
|
+
|
|
26
|
+
# operators
|
|
27
|
+
EQUALS = :equals
|
|
28
|
+
NOT_EQUALS = :not_equals
|
|
29
|
+
LT = :lt
|
|
30
|
+
GT = :gt
|
|
31
|
+
LTEQ = :lteq
|
|
32
|
+
GTEQ = :gteq
|
|
33
|
+
LIKE = :like
|
|
34
|
+
NOT_LIKE = :not_like
|
|
35
|
+
BANG = :bang
|
|
36
|
+
|
|
37
|
+
# keywords
|
|
38
|
+
AND = :and
|
|
39
|
+
OR = :or
|
|
40
|
+
NOT = :not
|
|
41
|
+
IN = :in
|
|
42
|
+
IS = :is
|
|
43
|
+
WAS = :was
|
|
44
|
+
CHANGED = :changed
|
|
45
|
+
ORDER = :order
|
|
46
|
+
BY = :by
|
|
47
|
+
ASC = :asc
|
|
48
|
+
DESC = :desc
|
|
49
|
+
EMPTY = :empty
|
|
50
|
+
NULL = :null
|
|
51
|
+
AFTER = :after
|
|
52
|
+
BEFORE = :before
|
|
53
|
+
DURING = :during
|
|
54
|
+
ON = :on
|
|
55
|
+
FROM = :from
|
|
56
|
+
TO = :to
|
|
57
|
+
|
|
58
|
+
# literals
|
|
59
|
+
STRING = :string
|
|
60
|
+
QUOTED_STRING = :quoted_string
|
|
61
|
+
NUMBER = :number
|
|
62
|
+
|
|
63
|
+
# special
|
|
64
|
+
EOF = :eof
|
|
65
|
+
end
|
|
66
|
+
end
|
data/lib/jql_ruby.rb
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "jql_ruby/version"
|
|
4
|
+
require_relative "jql_ruby/errors"
|
|
5
|
+
require_relative "jql_ruby/token"
|
|
6
|
+
require_relative "jql_ruby/lexer"
|
|
7
|
+
require_relative "jql_ruby/ast"
|
|
8
|
+
require_relative "jql_ruby/parser"
|
|
9
|
+
require_relative "jql_ruby/result"
|
|
10
|
+
require_relative "jql_ruby/adapters"
|
|
11
|
+
|
|
12
|
+
module JqlRuby
|
|
13
|
+
def self.parse(input)
|
|
14
|
+
tokens = Lexer.new(input).tokenize
|
|
15
|
+
parser = Parser.new(tokens)
|
|
16
|
+
query = parser.parse
|
|
17
|
+
Result.new(query: query, errors: parser.errors)
|
|
18
|
+
end
|
|
19
|
+
end
|