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,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
module Ast
|
|
5
|
+
class ValueOperand < Node
|
|
6
|
+
attr_reader :value
|
|
7
|
+
|
|
8
|
+
def initialize(value:, **rest)
|
|
9
|
+
super(**rest)
|
|
10
|
+
@value = value
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def accept(visitor)
|
|
14
|
+
visitor.visit_value_operand(self)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class FunctionOperand < Node
|
|
19
|
+
attr_reader :name, :arguments
|
|
20
|
+
|
|
21
|
+
def initialize(name:, arguments: [], **rest)
|
|
22
|
+
super(**rest)
|
|
23
|
+
@name = name
|
|
24
|
+
@arguments = arguments
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def accept(visitor)
|
|
28
|
+
visitor.visit_function_operand(self)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
class ListOperand < Node
|
|
33
|
+
attr_reader :values
|
|
34
|
+
|
|
35
|
+
def initialize(values:, **rest)
|
|
36
|
+
super(**rest)
|
|
37
|
+
@values = values
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def accept(visitor)
|
|
41
|
+
visitor.visit_list_operand(self)
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
class KeywordOperand < Node
|
|
46
|
+
attr_reader :value
|
|
47
|
+
|
|
48
|
+
def initialize(value:, **rest)
|
|
49
|
+
super(**rest)
|
|
50
|
+
@value = value
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def accept(visitor)
|
|
54
|
+
visitor.visit_keyword_operand(self)
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
module Ast
|
|
5
|
+
class Operator < Node
|
|
6
|
+
attr_reader :value
|
|
7
|
+
|
|
8
|
+
TYPES = %i[
|
|
9
|
+
eq not_eq
|
|
10
|
+
lt gt lteq gteq
|
|
11
|
+
like not_like
|
|
12
|
+
in not_in
|
|
13
|
+
is is_not
|
|
14
|
+
was was_not was_in was_not_in
|
|
15
|
+
changed
|
|
16
|
+
].freeze
|
|
17
|
+
|
|
18
|
+
def initialize(value:, **rest)
|
|
19
|
+
super(**rest)
|
|
20
|
+
@value = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def accept(visitor)
|
|
24
|
+
visitor.visit_operator(self)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
module Ast
|
|
5
|
+
class OrderBy < Node
|
|
6
|
+
attr_reader :fields
|
|
7
|
+
|
|
8
|
+
def initialize(fields:, **rest)
|
|
9
|
+
super(**rest)
|
|
10
|
+
@fields = fields
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def accept(visitor)
|
|
14
|
+
visitor.visit_order_by(self)
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class SearchSort < Node
|
|
19
|
+
attr_reader :field, :direction
|
|
20
|
+
|
|
21
|
+
def initialize(field:, direction: nil, **rest)
|
|
22
|
+
super(**rest)
|
|
23
|
+
@field = field
|
|
24
|
+
@direction = direction
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def accept(visitor)
|
|
28
|
+
visitor.visit_search_sort(self)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
module Ast
|
|
5
|
+
class Predicate < Node
|
|
6
|
+
attr_reader :operator, :operand
|
|
7
|
+
|
|
8
|
+
OPERATORS = %i[after before during on by from to].freeze
|
|
9
|
+
|
|
10
|
+
def initialize(operator:, operand:, **rest)
|
|
11
|
+
super(**rest)
|
|
12
|
+
@operator = operator
|
|
13
|
+
@operand = operand
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def accept(visitor)
|
|
17
|
+
visitor.visit_predicate(self)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
module Ast
|
|
5
|
+
class Query < Node
|
|
6
|
+
attr_reader :where_clause, :order_by
|
|
7
|
+
|
|
8
|
+
def initialize(where_clause: nil, order_by: nil, **rest)
|
|
9
|
+
super(**rest)
|
|
10
|
+
@where_clause = where_clause
|
|
11
|
+
@order_by = order_by
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def accept(visitor)
|
|
15
|
+
visitor.visit_query(self)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
module Ast
|
|
5
|
+
class TerminalClause < Node
|
|
6
|
+
attr_reader :field, :operator, :operand, :predicates
|
|
7
|
+
|
|
8
|
+
def initialize(field:, operator:, operand: nil, predicates: [], **rest)
|
|
9
|
+
super(**rest)
|
|
10
|
+
@field = field
|
|
11
|
+
@operator = operator
|
|
12
|
+
@operand = operand
|
|
13
|
+
@predicates = predicates
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def accept(visitor)
|
|
17
|
+
visitor.visit_terminal_clause(self)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
data/lib/jql_ruby/ast.rb
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "ast/node"
|
|
4
|
+
require_relative "ast/query"
|
|
5
|
+
require_relative "ast/compound_clause"
|
|
6
|
+
require_relative "ast/not_clause"
|
|
7
|
+
require_relative "ast/terminal_clause"
|
|
8
|
+
require_relative "ast/field"
|
|
9
|
+
require_relative "ast/operator"
|
|
10
|
+
require_relative "ast/operand"
|
|
11
|
+
require_relative "ast/order_by"
|
|
12
|
+
require_relative "ast/predicate"
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ParseError < Error
|
|
7
|
+
attr_reader :position, :line, :column
|
|
8
|
+
|
|
9
|
+
def initialize(message, position: nil, line: nil, column: nil)
|
|
10
|
+
@position = position
|
|
11
|
+
@line = line
|
|
12
|
+
@column = column
|
|
13
|
+
|
|
14
|
+
parts = [message]
|
|
15
|
+
parts << "at position #{position}" if position
|
|
16
|
+
super(parts.join(" "))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class LexerError < ParseError; end
|
|
21
|
+
|
|
22
|
+
class AdapterError < Error; end
|
|
23
|
+
class UnknownFieldError < AdapterError; end
|
|
24
|
+
class UnknownFunctionError < AdapterError; end
|
|
25
|
+
class UnsupportedOperatorError < AdapterError; end
|
|
26
|
+
end
|
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module JqlRuby
|
|
4
|
+
class Lexer
|
|
5
|
+
KEYWORDS = {
|
|
6
|
+
"and" => TokenType::AND,
|
|
7
|
+
"or" => TokenType::OR,
|
|
8
|
+
"not" => TokenType::NOT,
|
|
9
|
+
"in" => TokenType::IN,
|
|
10
|
+
"is" => TokenType::IS,
|
|
11
|
+
"was" => TokenType::WAS,
|
|
12
|
+
"changed" => TokenType::CHANGED,
|
|
13
|
+
"order" => TokenType::ORDER,
|
|
14
|
+
"by" => TokenType::BY,
|
|
15
|
+
"asc" => TokenType::ASC,
|
|
16
|
+
"desc" => TokenType::DESC,
|
|
17
|
+
"empty" => TokenType::EMPTY,
|
|
18
|
+
"null" => TokenType::NULL,
|
|
19
|
+
"after" => TokenType::AFTER,
|
|
20
|
+
"before" => TokenType::BEFORE,
|
|
21
|
+
"during" => TokenType::DURING,
|
|
22
|
+
"on" => TokenType::ON,
|
|
23
|
+
"from" => TokenType::FROM,
|
|
24
|
+
"to" => TokenType::TO,
|
|
25
|
+
}.freeze
|
|
26
|
+
|
|
27
|
+
# characters that terminate an unquoted string
|
|
28
|
+
STRING_TERMINATORS = Set.new([
|
|
29
|
+
" ", "\t", "\n", "\r",
|
|
30
|
+
"(", ")", "[", "]", ",", ".",
|
|
31
|
+
"=", "!", "<", ">", "~",
|
|
32
|
+
nil,
|
|
33
|
+
]).freeze
|
|
34
|
+
|
|
35
|
+
def initialize(input)
|
|
36
|
+
@input = input
|
|
37
|
+
@pos = 0
|
|
38
|
+
@tokens = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def tokenize
|
|
42
|
+
@tokens = []
|
|
43
|
+
@pos = 0
|
|
44
|
+
|
|
45
|
+
while @pos < @input.length
|
|
46
|
+
skip_whitespace
|
|
47
|
+
break if @pos >= @input.length
|
|
48
|
+
|
|
49
|
+
token = read_next_token
|
|
50
|
+
@tokens << token if token
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
@tokens << Token.new(type: TokenType::EOF, value: nil, position: @pos)
|
|
54
|
+
@tokens
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
private
|
|
58
|
+
|
|
59
|
+
def current_char
|
|
60
|
+
@input[@pos]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def peek_char
|
|
64
|
+
@input[@pos + 1]
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def advance
|
|
68
|
+
@pos += 1
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def skip_whitespace
|
|
72
|
+
advance while @pos < @input.length && @input[@pos].match?(/\s/)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def read_next_token
|
|
76
|
+
start = @pos
|
|
77
|
+
|
|
78
|
+
case current_char
|
|
79
|
+
when "("
|
|
80
|
+
advance
|
|
81
|
+
Token.new(type: TokenType::LPAREN, value: "(", position: start)
|
|
82
|
+
when ")"
|
|
83
|
+
advance
|
|
84
|
+
Token.new(type: TokenType::RPAREN, value: ")", position: start)
|
|
85
|
+
when ","
|
|
86
|
+
advance
|
|
87
|
+
Token.new(type: TokenType::COMMA, value: ",", position: start)
|
|
88
|
+
when "["
|
|
89
|
+
advance
|
|
90
|
+
Token.new(type: TokenType::LBRACKET, value: "[", position: start)
|
|
91
|
+
when "]"
|
|
92
|
+
advance
|
|
93
|
+
Token.new(type: TokenType::RBRACKET, value: "]", position: start)
|
|
94
|
+
when "."
|
|
95
|
+
advance
|
|
96
|
+
Token.new(type: TokenType::DOT, value: ".", position: start)
|
|
97
|
+
when "="
|
|
98
|
+
advance
|
|
99
|
+
Token.new(type: TokenType::EQUALS, value: "=", position: start)
|
|
100
|
+
when "!"
|
|
101
|
+
advance
|
|
102
|
+
if current_char == "="
|
|
103
|
+
advance
|
|
104
|
+
Token.new(type: TokenType::NOT_EQUALS, value: "!=", position: start)
|
|
105
|
+
elsif current_char == "~"
|
|
106
|
+
advance
|
|
107
|
+
Token.new(type: TokenType::NOT_LIKE, value: "!~", position: start)
|
|
108
|
+
else
|
|
109
|
+
Token.new(type: TokenType::BANG, value: "!", position: start)
|
|
110
|
+
end
|
|
111
|
+
when "<"
|
|
112
|
+
advance
|
|
113
|
+
if current_char == "="
|
|
114
|
+
advance
|
|
115
|
+
Token.new(type: TokenType::LTEQ, value: "<=", position: start)
|
|
116
|
+
else
|
|
117
|
+
Token.new(type: TokenType::LT, value: "<", position: start)
|
|
118
|
+
end
|
|
119
|
+
when ">"
|
|
120
|
+
advance
|
|
121
|
+
if current_char == "="
|
|
122
|
+
advance
|
|
123
|
+
Token.new(type: TokenType::GTEQ, value: ">=", position: start)
|
|
124
|
+
else
|
|
125
|
+
Token.new(type: TokenType::GT, value: ">", position: start)
|
|
126
|
+
end
|
|
127
|
+
when "~"
|
|
128
|
+
advance
|
|
129
|
+
Token.new(type: TokenType::LIKE, value: "~", position: start)
|
|
130
|
+
when '"', "'"
|
|
131
|
+
read_quoted_string(start)
|
|
132
|
+
when "-"
|
|
133
|
+
if peek_char&.match?(/\d/)
|
|
134
|
+
read_number(start)
|
|
135
|
+
else
|
|
136
|
+
read_unquoted_string(start)
|
|
137
|
+
end
|
|
138
|
+
when /\d/
|
|
139
|
+
read_number(start)
|
|
140
|
+
else
|
|
141
|
+
read_unquoted_string(start)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def read_quoted_string(start)
|
|
146
|
+
quote = current_char
|
|
147
|
+
advance
|
|
148
|
+
value = +""
|
|
149
|
+
|
|
150
|
+
while @pos < @input.length && current_char != quote
|
|
151
|
+
if current_char == "\\"
|
|
152
|
+
advance
|
|
153
|
+
case current_char
|
|
154
|
+
when "\\", quote
|
|
155
|
+
value << current_char
|
|
156
|
+
when "n"
|
|
157
|
+
value << "\n"
|
|
158
|
+
when "t"
|
|
159
|
+
value << "\t"
|
|
160
|
+
when "'"
|
|
161
|
+
value << "'"
|
|
162
|
+
when '"'
|
|
163
|
+
value << '"'
|
|
164
|
+
when "u"
|
|
165
|
+
# unicode escape: \uXXXX
|
|
166
|
+
advance
|
|
167
|
+
hex = @input[@pos, 4]
|
|
168
|
+
if hex&.match?(/\A[0-9a-fA-F]{4}\z/)
|
|
169
|
+
value << hex.to_i(16).chr(Encoding::UTF_8)
|
|
170
|
+
@pos += 3
|
|
171
|
+
else
|
|
172
|
+
value << "u"
|
|
173
|
+
next
|
|
174
|
+
end
|
|
175
|
+
else
|
|
176
|
+
value << "\\" << (current_char || "")
|
|
177
|
+
end
|
|
178
|
+
else
|
|
179
|
+
value << current_char
|
|
180
|
+
end
|
|
181
|
+
advance
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
if @pos >= @input.length
|
|
185
|
+
raise LexerError.new("unterminated string", position: start)
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
advance
|
|
189
|
+
Token.new(type: TokenType::QUOTED_STRING, value: value, position: start)
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def read_number(start)
|
|
193
|
+
value = +""
|
|
194
|
+
if current_char == "-"
|
|
195
|
+
value << "-"
|
|
196
|
+
advance
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
while @pos < @input.length && current_char.match?(/\d/)
|
|
200
|
+
value << current_char
|
|
201
|
+
advance
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
if @pos < @input.length && current_char == "." && peek_char&.match?(/\d/)
|
|
205
|
+
value << "."
|
|
206
|
+
advance
|
|
207
|
+
while @pos < @input.length && current_char.match?(/\d/)
|
|
208
|
+
value << current_char
|
|
209
|
+
advance
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# if followed by a non-terminating char, this is actually a string (e.g. "3.14a")
|
|
214
|
+
unless STRING_TERMINATORS.include?(current_char)
|
|
215
|
+
value << read_remaining_unquoted_string
|
|
216
|
+
return classify_unquoted(value, start)
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
Token.new(type: TokenType::NUMBER, value: value, position: start)
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def read_unquoted_string(start)
|
|
223
|
+
value = read_remaining_unquoted_string
|
|
224
|
+
classify_unquoted(value, start)
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
def read_remaining_unquoted_string
|
|
228
|
+
value = +""
|
|
229
|
+
until STRING_TERMINATORS.include?(current_char)
|
|
230
|
+
value << current_char
|
|
231
|
+
advance
|
|
232
|
+
end
|
|
233
|
+
value
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
def classify_unquoted(value, start)
|
|
237
|
+
keyword_type = KEYWORDS[value.downcase]
|
|
238
|
+
if keyword_type
|
|
239
|
+
Token.new(type: keyword_type, value: value, position: start)
|
|
240
|
+
else
|
|
241
|
+
Token.new(type: TokenType::STRING, value: value, position: start)
|
|
242
|
+
end
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
end
|