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.
@@ -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
@@ -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