kql 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,23 @@
1
+ require_relative './query'
2
+
3
+ module KQL
4
+ class Mapping < Query
5
+ attr_accessor :mapping
6
+
7
+ def initialize(alternatives, mapping)
8
+ super(alternatives)
9
+ @mapping = mapping
10
+ end
11
+
12
+ def execute(document)
13
+ nodes = super
14
+ nodes.map { |node| mapping.execute(node) }
15
+ end
16
+
17
+ def ==(other)
18
+ return false unless other.is_a?(Mapping)
19
+
20
+ super(other) && other.mapping = mapping
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,86 @@
1
+ module KQL
2
+ class Matcher
3
+ singleton :Any, Matcher do
4
+ def match?(node)
5
+ true
6
+ end
7
+ end
8
+
9
+ singleton :AnyTag, Matcher do
10
+ def match?(node)
11
+ !node.type.nil?
12
+ end
13
+ end
14
+
15
+ class Tag < Matcher
16
+ attr_reader :tag
17
+ alias value tag
18
+
19
+ def initialize(tag)
20
+ @tag = tag
21
+ end
22
+
23
+ def match?(node)
24
+ node.type == tag
25
+ end
26
+
27
+ def ==(other)
28
+ return false unless other.is_a?(Tag)
29
+
30
+ other.tag == tag
31
+ end
32
+
33
+ def coerce(a)
34
+ case a
35
+ when ::KDL::Node, ::KDL::Value then a.type
36
+ else a
37
+ end
38
+ end
39
+ end
40
+
41
+ class Value < Matcher
42
+ attr_reader :value
43
+
44
+ def initialize(value)
45
+ @value = value
46
+ end
47
+
48
+ def ==(other)
49
+ return false unless other.is_a?(Value)
50
+
51
+ other.value == value
52
+ end
53
+
54
+ def coerce(a)
55
+ case a
56
+ when ::KDL::Value then a.value
57
+ else a
58
+ end
59
+ end
60
+ end
61
+
62
+ class Comparison < Matcher
63
+ attr_reader :accessor, :operator, :value
64
+
65
+ def initialize(accessor, operator, value)
66
+ @accessor = accessor
67
+ @operator = operator
68
+ @value = value
69
+ end
70
+
71
+ def match?(node)
72
+ return false unless accessor.match?(node)
73
+
74
+ operator.execute(value.coerce(accessor.execute(node)), value.value)
75
+ end
76
+
77
+ def ==(other)
78
+ return false unless other.is_a?(Comparison)
79
+
80
+ other.accessor == accessor &&
81
+ other.operator == operator &&
82
+ other.value == value
83
+ end
84
+ end
85
+ end
86
+ end
@@ -0,0 +1,57 @@
1
+ module KQL
2
+ class Operator
3
+ singleton :Equals, Operator do
4
+ def execute(a, b)
5
+ a == b
6
+ end
7
+ end
8
+
9
+ singleton :NotEquals, Operator do
10
+ def execute(a, b)
11
+ a != b
12
+ end
13
+ end
14
+
15
+ singleton :GreaterThanOrEqual, Operator do
16
+ def execute(a, b)
17
+ a >= b
18
+ end
19
+ end
20
+
21
+ singleton :GreaterThan, Operator do
22
+ def execute(a, b)
23
+ a > b
24
+ end
25
+ end
26
+
27
+ singleton :LessThanOrEqual, Operator do
28
+ def execute(a, b)
29
+ a <= b
30
+ end
31
+ end
32
+
33
+ singleton :LessThan, Operator do
34
+ def execute(a, b)
35
+ a < b
36
+ end
37
+ end
38
+
39
+ singleton :StartsWith, Operator do
40
+ def execute(a, b)
41
+ a.start_with?(b)
42
+ end
43
+ end
44
+
45
+ singleton :EndsWith, Operator do
46
+ def execute(a, b)
47
+ a.end_with?(b)
48
+ end
49
+ end
50
+
51
+ singleton :Includes, Operator do
52
+ def execute(a, b)
53
+ a.include?(b)
54
+ end
55
+ end
56
+ end
57
+ end
data/lib/kql/query.rb ADDED
@@ -0,0 +1,77 @@
1
+ module KQL
2
+ class Query
3
+ attr_reader :alternatives
4
+
5
+ def initialize(alternatives)
6
+ @alternatives = alternatives
7
+ end
8
+
9
+ def ==(other)
10
+ return false unless other.is_a?(Query)
11
+
12
+ other.alternatives == alternatives
13
+ end
14
+
15
+ def execute(document)
16
+ alternatives.flat_map do |alt|
17
+ alt.execute(TopContext.new(document))
18
+ .nodes
19
+ .uniq { |n| n.__id__ }
20
+ end
21
+ end
22
+
23
+ private
24
+
25
+ class Context
26
+ attr_accessor :selected_nodes
27
+
28
+ def initialize(selected_nodes)
29
+ @selected_nodes = selected_nodes
30
+ end
31
+
32
+ def nodes
33
+ selected_nodes.map(&:node)
34
+ end
35
+
36
+ def children(**kwargs)
37
+ nodes.flat_map do |node|
38
+ node.children
39
+ .each_with_index
40
+ .map { |n, i| Query::SelectedNode.new(n, node.children, i, **kwargs) }
41
+ end
42
+ end
43
+
44
+ def top?
45
+ false
46
+ end
47
+ end
48
+
49
+ class TopContext < Context
50
+ attr_accessor :document
51
+
52
+ def initialize(document)
53
+ @document = document
54
+ super(children)
55
+ end
56
+
57
+ def children(**kwargs)
58
+ document.nodes.each_with_index.map { |n, i| Query::SelectedNode.new(n, document.nodes, i, **kwargs) }
59
+ end
60
+
61
+ def top?
62
+ true
63
+ end
64
+ end
65
+
66
+ class SelectedNode
67
+ attr_accessor :node, :siblings, :index, :stop
68
+
69
+ def initialize(node, siblings, index, stop: false)
70
+ @node = node
71
+ @siblings = siblings
72
+ @index = index
73
+ @stop = stop
74
+ end
75
+ end
76
+ end
77
+ end
@@ -0,0 +1,41 @@
1
+ module KQL
2
+ class Selector
3
+ attr_reader :filter
4
+
5
+ def initialize(filter)
6
+ @filter = filter
7
+ end
8
+
9
+ def ==(other)
10
+ return false unless other.class == Selector
11
+
12
+ other.filter == filter
13
+ end
14
+
15
+ def execute(context)
16
+ filter.execute(context)
17
+ end
18
+
19
+ class Combined < Selector
20
+ attr_reader :combinator, :selector
21
+
22
+ def initialize(filter, combinator, selector)
23
+ super(filter)
24
+ @combinator = combinator
25
+ @selector = selector
26
+ end
27
+
28
+ def ==(other)
29
+ return false unless other.is_a?(Combined)
30
+
31
+ other.filter == filter &&
32
+ other.combinator == combinator &&
33
+ other.selector == selector
34
+ end
35
+
36
+ def execute(context)
37
+ combinator.execute(super, selector)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -0,0 +1,302 @@
1
+ module KQL
2
+ class Tokenizer
3
+ class Error < StandardError
4
+ def initialize(message, line, column)
5
+ super("#{message} (#{line}:#{column})")
6
+ end
7
+ end
8
+
9
+ class Token
10
+ attr_reader :type, :value, :line, :column
11
+
12
+ def initialize(type, value, line, column)
13
+ @type = type
14
+ @value = value
15
+ @line = line
16
+ @column = column
17
+ end
18
+
19
+ def ==(other )
20
+ return false unless other.is_a?(Token)
21
+ return false unless type == other.type && value == other.value
22
+
23
+ if line && other.line
24
+ return false unless line == other.line
25
+ end
26
+ if column && other.column
27
+ return false unless column == other.column
28
+ end
29
+
30
+ true
31
+ end
32
+
33
+ def to_s
34
+ "#{value.inspect} (#{line || '?'}:#{column || '?'})"
35
+ end
36
+ alias inspect to_s
37
+ end
38
+
39
+ attr_reader :index
40
+
41
+ SYMBOLS = {
42
+ '(' => :LPAREN,
43
+ ')' => :RPAREN,
44
+ '[' => :LBRACKET,
45
+ ']' => :RBRACKET,
46
+ ',' => :COMMA
47
+ }
48
+
49
+ WHITESPACE = ["\u0009", "\u0020", "\u00A0", "\u1680",
50
+ "\u2000", "\u2001", "\u2002", "\u2003",
51
+ "\u2004", "\u2005", "\u2006", "\u2007",
52
+ "\u2008", "\u2009", "\u200A", "\u202F",
53
+ "\u205F", "\u3000", ' ']
54
+
55
+ NEWLINES = ["\u000A", "\u0085", "\u000C", "\u2028", "\u2029"]
56
+
57
+ NON_IDENTIFIER_CHARS = Regexp.escape "#{SYMBOLS.keys.join('')}()/\\<>[]\","
58
+ IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}\x0-\x20]/
59
+ INITIAL_IDENTIFIER_CHARS = /[^#{NON_IDENTIFIER_CHARS}0-9\x0-\x20]/
60
+
61
+ def initialize(str, start = 0)
62
+ @str = str
63
+ @context = nil
64
+ @index = start
65
+ @buffer = ''
66
+ @previous_context = nil
67
+ @line = 1
68
+ @column = 1
69
+ end
70
+
71
+ def next_token
72
+ @context = nil
73
+ @previous_context = nil
74
+ @line_at_start = @line
75
+ @column_at_start = @column
76
+ loop do
77
+ c = @str[@index]
78
+ n = @str[@index + 1]
79
+ case @context
80
+ when nil
81
+ case c
82
+ when '"'
83
+ self.context = :string
84
+ @buffer = ''
85
+ traverse(1)
86
+ when /[0-9\-]/
87
+ self.context = :number
88
+ traverse(1)
89
+ @buffer = c
90
+ when '='
91
+ if n == '>'
92
+ return token(:MAP, '=>').tap { traverse(2) }
93
+ else
94
+ return token(:EQUALS, c).tap { traverse(1) }
95
+ end
96
+ when '>'
97
+ if n == '='
98
+ return token(:GTE, '>=').tap { traverse(2) }
99
+ else
100
+ return token(:GT, c).tap { traverse(1) }
101
+ end
102
+ when '<'
103
+ if n == '='
104
+ return token(:LTE, '<=').tap { traverse(2) }
105
+ else
106
+ return token(:LT, c).tap { traverse(1) }
107
+ end
108
+ when '|'
109
+ if n == '|'
110
+ return token(:OR, '||').tap { traverse(2) }
111
+ else
112
+ self.context = :ident
113
+ @buffer = c
114
+ traverse(1)
115
+ end
116
+ when '^'
117
+ if n == '='
118
+ return token(:STARTS_WITH, '^=').tap { traverse(2) }
119
+ else
120
+ self.context = :ident
121
+ @buffer = c
122
+ traverse(1)
123
+ end
124
+ when '$'
125
+ if n == '='
126
+ return token(:ENDS_WITH, '$=').tap { traverse(2) }
127
+ else
128
+ self.context = :ident
129
+ @buffer = c
130
+ traverse(1)
131
+ end
132
+ when '*'
133
+ if n == '='
134
+ return token(:INCLUDES, '*=').tap { traverse(2) }
135
+ else
136
+ self.context = :ident
137
+ @buffer = c
138
+ traverse(1)
139
+ end
140
+ when '+'
141
+ case n
142
+ when /[0-9]/
143
+ self.context = :number
144
+ traverse(1)
145
+ @buffer = c
146
+ when IDENTIFIER_CHARS
147
+ self.context = :ident
148
+ @buffer = c
149
+ traverse(1)
150
+ else
151
+ return token(:PLUS, '+').tap { traverse(1) }
152
+ end
153
+ when '~'
154
+ case n
155
+ when IDENTIFIER_CHARS
156
+ self.context = :ident
157
+ @buffer = c
158
+ traverse(1)
159
+ else
160
+ return token(:TILDE, '~').tap { traverse(1) }
161
+ end
162
+ when '!'
163
+ case n
164
+ when '='
165
+ return token(:NOT_EQUALS, '!=').tap { traverse(2) }
166
+ else
167
+ self.context = :ident
168
+ @buffer = c
169
+ traverse(1)
170
+ end
171
+ when *SYMBOLS.keys
172
+ return token(SYMBOLS[c], c).tap { traverse(1) }
173
+ when *WHITESPACE
174
+ traverse(1)
175
+ when *NEWLINES
176
+ traverse(1)
177
+ new_line
178
+ when INITIAL_IDENTIFIER_CHARS
179
+ self.context = :ident
180
+ @buffer = c
181
+ traverse(1)
182
+ when nil
183
+ return [false, nil]
184
+ else
185
+ raise_error "Unexpected `#{c}'"
186
+ end
187
+ when :ident
188
+ case c
189
+ when IDENTIFIER_CHARS
190
+ traverse(1)
191
+ @buffer += c
192
+ else
193
+ case @buffer
194
+ when 'true' then return token(:TRUE, true)
195
+ when 'false' then return token(:FALSE, false)
196
+ when 'null' then return token(:NULL, nil)
197
+ when 'top', 'name', 'tag', 'props', 'values'
198
+ if c == '(' && n == ')'
199
+ return token(@buffer.upcase.to_sym, "#{@buffer}()").tap { traverse(2) }
200
+ end
201
+ when 'val'
202
+ return token(:VAL, @buffer) if c == '('
203
+ when 'prop'
204
+ return token(:PROP, @buffer) if c == '('
205
+ end
206
+ return token(:IDENT, @buffer)
207
+ end
208
+ when :string
209
+ case c
210
+ when '\\'
211
+ @buffer += c
212
+ @buffer += @str[@index + 1]
213
+ traverse(2)
214
+ when '"'
215
+ return token(:STRING, convert_escapes(@buffer)).tap { traverse(1) }
216
+ when nil
217
+ raise_error 'Unterminated string literal'
218
+ else
219
+ @buffer += c
220
+ traverse(1)
221
+ end
222
+ when :number
223
+ case c
224
+ when /[0-9.\-+_eE]/
225
+ traverse(1)
226
+ @buffer += c
227
+ else
228
+ return parse_number(@buffer)
229
+ end
230
+ end
231
+ end
232
+ end
233
+
234
+ private
235
+
236
+ def token(type, value)
237
+ [type, Token.new(type, value, @line_at_start, @column_at_start)]
238
+ end
239
+
240
+ def traverse(count = 1)
241
+ @column += count
242
+ @index += count
243
+ end
244
+
245
+ def raise_error(message)
246
+ raise Error.new(message, @line, @column)
247
+ end
248
+
249
+ def new_line
250
+ @column = 1
251
+ @line += 1
252
+ end
253
+
254
+ def context=(new_context)
255
+ @previous_context = @context
256
+ @context = new_context
257
+ end
258
+
259
+ def parse_number(string)
260
+ return parse_float(string) if string =~ /[.E]/i
261
+
262
+ token(:INTEGER, Integer(munch_underscores(string), 10))
263
+ end
264
+
265
+ def parse_float(string)
266
+ string = munch_underscores(string)
267
+
268
+ value = Float(string)
269
+ if value.infinite? || (value.zero? && exponent.to_i < 0)
270
+ token(:FLOAT, BigDecimal(string))
271
+ else
272
+ token(:FLOAT, value)
273
+ end
274
+ end
275
+
276
+ def munch_underscores(string)
277
+ string.chomp('_').squeeze('_')
278
+ end
279
+
280
+ def convert_escapes(string)
281
+ string.gsub(/\\[^u]/) do |m|
282
+ case m
283
+ when '\n' then "\n"
284
+ when '\r' then "\r"
285
+ when '\t' then "\t"
286
+ when '\\\\' then '\\'
287
+ when '\"' then '"'
288
+ when '\b' then "\b"
289
+ when '\f' then "\f"
290
+ when '\/' then '/'
291
+ else raise_error "Unexpected escape #{m.inspect}"
292
+ end
293
+ end.gsub(/\\u\{[0-9a-fA-F]{0,6}\}/) do |m|
294
+ i = Integer(m[3..-2], 16)
295
+ if i < 0 || i > 0x10FFFF
296
+ raise_error "Invalid code point #{u}"
297
+ end
298
+ i.chr(Encoding::UTF_8)
299
+ end
300
+ end
301
+ end
302
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module KQL
4
+ VERSION = "1.0.0"
5
+ end
data/lib/kql.rb ADDED
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ Class.module_eval do
4
+ def singleton(classname, superclass = nil, &block)
5
+ klass = Class.new(superclass, &block)
6
+ const_set(:"#{classname}Impl", klass)
7
+ const_set(classname, klass.new)
8
+ end
9
+ end
10
+
11
+ module KQL
12
+ def self.parse_query(query)
13
+ Parser.new.parse(query)
14
+ end
15
+
16
+ def self.query_document(document, query)
17
+ parse_query(query).execute(document)
18
+ end
19
+ end
20
+
21
+ require_relative "kql/version"
22
+ require_relative "kql/tokenizer"
23
+ require_relative "kql/query"
24
+ require_relative "kql/filter"
25
+ require_relative "kql/combinator"
26
+ require_relative "kql/selector"
27
+ require_relative "kql/matcher"
28
+ require_relative "kql/accessor"
29
+ require_relative "kql/operator"
30
+ require_relative "kql/mapping"
31
+ require_relative "kql/kql.tab"