kql 1.0.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,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"