rjq 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.
data/lib/rjq/path.rb ADDED
@@ -0,0 +1,250 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ module Path
5
+ module_function
6
+
7
+ def get(value, path)
8
+ assert_path(path)
9
+ path.reduce(value) { |current, key| read_index(current, key) }
10
+ end
11
+
12
+ def read_index(value, index)
13
+ return read_slice(value, index) if slice_component?(index)
14
+
15
+ case value
16
+ when NilClass
17
+ raise TypeError, cannot_index_message(value, index) unless index.is_a?(String) || index.is_a?(Numeric)
18
+
19
+ nil
20
+ when Array
21
+ raise TypeError, cannot_index_message(value, index) unless index.is_a?(Numeric)
22
+
23
+ read_array(value, index)
24
+ when Hash
25
+ raise TypeError, cannot_index_message(value, index) unless index.is_a?(String)
26
+
27
+ value[index]
28
+ when String
29
+ raise TypeError, cannot_index_message(value, index)
30
+ else
31
+ raise TypeError, cannot_index_message(value, index)
32
+ end
33
+ end
34
+
35
+ def set(value, path, new_value)
36
+ assert_path(path)
37
+ return new_value if path.empty?
38
+
39
+ if value.nil?
40
+ read_index(nil, path.first)
41
+ value = container_for(path.first)
42
+ end
43
+ parent = ensure_parent(value, path)
44
+ key = path.last
45
+ if slice_component?(key)
46
+ replace_slice(parent, key, new_value)
47
+ return value
48
+ end
49
+ key = normalize_array_key(parent, key)
50
+ case parent
51
+ when Array
52
+ parent[key] = new_value
53
+ when Hash
54
+ raise TypeError, cannot_index_message(parent, key) unless key.is_a?(String)
55
+
56
+ parent[key] = new_value
57
+ else
58
+ raise TypeError, cannot_index_message(parent, key)
59
+ end
60
+ value
61
+ end
62
+
63
+ def delete(value, path)
64
+ assert_path(path)
65
+ return nil if path.empty?
66
+
67
+ parent = get(value, path[0...-1])
68
+ key = path.last
69
+ if slice_component?(key)
70
+ replacement = parent.is_a?(String) ? '' : []
71
+ replace_slice(parent, key, replacement)
72
+ return value
73
+ end
74
+ case parent
75
+ when Array
76
+ raise TypeError, cannot_index_message(parent, key) unless key.is_a?(Numeric)
77
+ return value if key.respond_to?(:finite?) && !key.finite?
78
+
79
+ key = key.to_i
80
+ return value if key.zero? && path.last.negative?
81
+
82
+ key = parent.length + key if key.negative?
83
+ parent.delete_at(key) unless key.negative?
84
+ when Hash
85
+ raise TypeError, cannot_index_message(parent, key) unless key.is_a?(String)
86
+
87
+ parent.delete(key)
88
+ end
89
+ value
90
+ end
91
+
92
+ def paths(value, leaves_only: false)
93
+ out = []
94
+ stack = [[value, nil]]
95
+ until stack.empty?
96
+ current, path_node = stack.pop
97
+ scalar = !(current.is_a?(Array) || current.is_a?(Hash))
98
+ out << materialize_path(path_node) if path_node.nil? || !leaves_only || scalar
99
+ children = if current.is_a?(Array)
100
+ current.each_with_index.map { |item, index| [item, [path_node, index]] }
101
+ elsif current.is_a?(Hash)
102
+ current.map { |key, item| [item, [path_node, key]] }
103
+ else
104
+ []
105
+ end
106
+ stack.concat(children.reverse)
107
+ end
108
+ out
109
+ end
110
+
111
+ def assert_path(path)
112
+ raise TypeError, 'Path must be specified as an array' unless path.is_a?(Array)
113
+
114
+ path
115
+ end
116
+
117
+ def materialize_path(node)
118
+ path = []
119
+ while node
120
+ node, key = node
121
+ path << key
122
+ end
123
+ path.reverse
124
+ end
125
+ private_class_method :materialize_path
126
+
127
+ def ensure_parent(value, path)
128
+ current = value
129
+ path[0...-1].each_with_index do |key, index|
130
+ next_key = path[index + 1]
131
+ case current
132
+ when Array
133
+ key = normalize_array_key(current, key)
134
+ if current[key].nil?
135
+ read_index(nil, next_key)
136
+ current[key] = container_for(next_key)
137
+ end
138
+ current = current[key]
139
+ when Hash
140
+ raise TypeError, cannot_index_message(current, key) unless key.is_a?(String)
141
+
142
+ if current[key].nil?
143
+ read_index(nil, next_key)
144
+ current[key] = container_for(next_key)
145
+ end
146
+ current = current[key]
147
+ else
148
+ raise TypeError, cannot_index_message(current, key)
149
+ end
150
+ end
151
+ current
152
+ end
153
+ private_class_method :ensure_parent
154
+
155
+ def container_for(next_key)
156
+ next_key.is_a?(Numeric) ? [] : {}
157
+ end
158
+ private_class_method :container_for
159
+
160
+ def normalize_array_key(parent, key)
161
+ return key unless parent.is_a?(Array)
162
+ raise TypeError, 'array index must be an integer' unless key.is_a?(Numeric)
163
+ raise TypeError, 'Cannot set array element at NaN index' if key.respond_to?(:nan?) && key.nan?
164
+ raise TypeError, 'Cannot set array element at non-finite index' if key.respond_to?(:finite?) && !key.finite?
165
+
166
+ key = key.to_i
167
+ return key unless key.negative?
168
+
169
+ normalized = parent.length + key
170
+ raise RuntimeError, 'Out of bounds negative array index' if normalized.negative?
171
+
172
+ normalized
173
+ end
174
+ private_class_method :normalize_array_key
175
+
176
+ def read_array(array, key)
177
+ return nil if (key.respond_to?(:nan?) && key.nan?) || (key.respond_to?(:finite?) && !key.finite?)
178
+
179
+ index = key.to_i
180
+ index = array.length + index if index.negative?
181
+ return nil if index.negative? || index >= array.length
182
+
183
+ array[index]
184
+ end
185
+ private_class_method :read_array
186
+
187
+ def slice_component?(key)
188
+ key.is_a?(Hash) && key.keys.all? { |name| %w[start end].include?(name) } &&
189
+ (key.key?('start') || key.key?('end'))
190
+ end
191
+ private_class_method :slice_component?
192
+
193
+ def read_slice(value, component)
194
+ return nil if value.nil?
195
+
196
+ case value
197
+ when Array
198
+ value[slice_range(value.length, component)] || []
199
+ when String
200
+ characters = value.each_char.to_a
201
+ characters[slice_range(characters.length, component)].to_a.join
202
+ else
203
+ raise TypeError, "cannot slice #{Value.type_of(value)}"
204
+ end
205
+ end
206
+ private_class_method :read_slice
207
+
208
+ def replace_slice(value, component, replacement)
209
+ case value
210
+ when Array
211
+ raise TypeError, 'can only assign an array to an array slice' unless replacement.is_a?(Array)
212
+
213
+ value[slice_range(value.length, component)] = Value.deep_copy(replacement)
214
+ when String
215
+ raise TypeError, 'can only assign a string to a string slice' unless replacement.is_a?(String)
216
+
217
+ characters = value.each_char.to_a
218
+ characters[slice_range(characters.length, component)] = replacement.each_char.to_a
219
+ value.replace(characters.join)
220
+ else
221
+ raise TypeError, "cannot slice #{Value.type_of(value)}"
222
+ end
223
+ end
224
+ private_class_method :replace_slice
225
+
226
+ def slice_range(length, component)
227
+ from = slice_boundary(component['start'], length, :floor, 0)
228
+ to = slice_boundary(component['end'], length, :ceil, length)
229
+ from...to
230
+ end
231
+ private_class_method :slice_range
232
+
233
+ def slice_boundary(value, length, rounding, default)
234
+ return default if value.nil? || (value.respond_to?(:nan?) && value.nan?)
235
+ raise TypeError, 'slice index must be a number' unless value.is_a?(Numeric)
236
+ return value.positive? ? length : 0 if value.respond_to?(:finite?) && !value.finite?
237
+
238
+ index = rounding == :ceil ? value.ceil : value.floor
239
+ index = length + index if value.negative?
240
+ [[index, 0].max, length].min
241
+ end
242
+ private_class_method :slice_boundary
243
+
244
+ def cannot_index_message(value, key)
245
+ key_text = key.is_a?(String) ? "string #{key.inspect}" : Value.type_of(key)
246
+ "Cannot index #{Value.type_of(value)} with #{key_text}"
247
+ end
248
+ private_class_method :cannot_index_message
249
+ end
250
+ end
@@ -0,0 +1,377 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ class Runtime
5
+ DEFAULT_OPTIONS = {
6
+ compact: false, raw_output: false, raw_output0: false, join_output: false,
7
+ null_input: false, raw_input: false, slurp: false, ascii: false,
8
+ sort_keys: false, tab: false, indent: 2, seq: false, stream: false,
9
+ stream_errors: false, unbuffered: false, max_call_depth: nil, max_instructions: nil,
10
+ max_outputs: nil, variables: {}
11
+ }.freeze
12
+ OPTION_KEYS = (DEFAULT_OPTIONS.keys + %i[
13
+ allow_comments color current_filename current_line exit_status input_chunk_size input_max_depth input_queue
14
+ jq_origin library_path max_filter_depth max_number_digits max_string_bytes module_resolver regexp_timeout
15
+ max_replay_cache remaining_inputs runtime_error_handler source_path stderr
16
+ ]).freeze
17
+ BOOLEAN_OPTIONS = %i[
18
+ allow_comments ascii compact exit_status join_output null_input raw_input raw_output raw_output0 seq slurp
19
+ sort_keys stream stream_errors tab unbuffered
20
+ ].freeze
21
+
22
+ class << self
23
+ def validate_options!(opts)
24
+ raise ArgumentError, 'options must be a Hash' unless opts.is_a?(Hash)
25
+
26
+ unknown = opts.keys - OPTION_KEYS
27
+ raise ArgumentError, "unknown runtime option: #{unknown.first.inspect}" unless unknown.empty?
28
+
29
+ BOOLEAN_OPTIONS.each { |key| validate_boolean!(opts, key) }
30
+ validate_color!(opts)
31
+ validate_integer!(opts, :indent, minimum: 0, maximum: 7)
32
+ validate_integer!(opts, :input_chunk_size, minimum: 1)
33
+ validate_integer!(opts, :input_max_depth, minimum: 0)
34
+ validate_integer!(opts, :max_call_depth, minimum: 1, optional: true)
35
+ validate_integer!(opts, :max_instructions, minimum: 0, optional: true)
36
+ validate_integer!(opts, :max_replay_cache, minimum: 0, optional: true)
37
+ validate_integer!(opts, :current_line, minimum: 1, optional: true)
38
+ validate_integer!(opts, :max_number_digits, minimum: 0, optional: true)
39
+ validate_integer!(opts, :max_outputs, minimum: 0, optional: true)
40
+ validate_integer!(opts, :max_string_bytes, minimum: 0, optional: true)
41
+ validate_regexp_timeout!(opts)
42
+ validate_stderr!(opts)
43
+ validate_runtime_error_handler!(opts)
44
+ validate_variables!(opts)
45
+ Compiler.validate_options!(Compiler.options_from(opts))
46
+ opts
47
+ end
48
+
49
+ def normalize_options(opts)
50
+ validate_options!(opts)
51
+ normalized = opts.dup
52
+ normalized[:variables] = normalized[:variables].dup.freeze if normalized[:variables]
53
+ normalized[:library_path] = normalized[:library_path].dup.freeze if normalized[:library_path]
54
+ normalized.freeze
55
+ end
56
+
57
+ private
58
+
59
+ def validate_boolean!(opts, key)
60
+ return unless opts.key?(key)
61
+ return if opts[key] == true || opts[key] == false
62
+
63
+ raise ArgumentError, "#{key} must be true or false"
64
+ end
65
+
66
+ def validate_color!(opts)
67
+ return unless opts.key?(:color)
68
+ return if opts[:color].nil? || opts[:color] == true || opts[:color] == false
69
+
70
+ raise ArgumentError, 'color must be true, false, or nil'
71
+ end
72
+
73
+ def validate_integer!(opts, key, minimum:, maximum: nil, optional: false)
74
+ return unless opts.key?(key)
75
+ return if optional && opts[key].nil?
76
+
77
+ value = opts[key]
78
+ valid = value.is_a?(Integer) && value >= minimum && (!maximum || value <= maximum)
79
+ return if valid
80
+
81
+ range = maximum ? "between #{minimum} and #{maximum}" : "at least #{minimum}"
82
+ raise ArgumentError, "#{key} must be an Integer #{range}"
83
+ end
84
+
85
+ def validate_regexp_timeout!(opts)
86
+ return unless opts.key?(:regexp_timeout)
87
+
88
+ timeout = opts[:regexp_timeout]
89
+ return if timeout.nil? || (timeout.is_a?(Numeric) && timeout.respond_to?(:finite?) && timeout.finite? &&
90
+ timeout.respond_to?(:positive?) && timeout.positive?)
91
+
92
+ raise ArgumentError, 'regexp_timeout must be a finite positive number or nil'
93
+ end
94
+
95
+ def validate_stderr!(opts)
96
+ return unless opts.key?(:stderr)
97
+ return if opts[:stderr].nil? || opts[:stderr].respond_to?(:puts)
98
+
99
+ raise ArgumentError, 'stderr must be nil or respond to puts'
100
+ end
101
+
102
+ def validate_runtime_error_handler!(opts)
103
+ return unless opts.key?(:runtime_error_handler)
104
+ return if opts[:runtime_error_handler].nil? || opts[:runtime_error_handler].respond_to?(:call)
105
+
106
+ raise ArgumentError, 'runtime_error_handler must be nil or respond to call'
107
+ end
108
+
109
+ def validate_variables!(opts)
110
+ return unless opts.key?(:variables)
111
+ return if opts[:variables].is_a?(Hash)
112
+
113
+ raise ArgumentError, 'variables must be a Hash'
114
+ end
115
+
116
+ end
117
+
118
+ InputRecord = Struct.new(:value, :filename, :line, keyword_init: true)
119
+
120
+ class ResultStream
121
+ include Enumerable
122
+
123
+ def initialize(on_close: nil, &producer)
124
+ @producer = producer
125
+ @on_close = on_close
126
+ @closed = false
127
+ end
128
+
129
+ def each
130
+ return enum_for(:each) unless block_given?
131
+
132
+ begin
133
+ @producer.call(->(value) { yield value })
134
+ ensure
135
+ close
136
+ end
137
+ end
138
+
139
+ def close
140
+ return if @closed
141
+
142
+ @closed = true
143
+ @on_close&.call
144
+ end
145
+ end
146
+
147
+ class InputQueue
148
+ END_OF_INPUT = Object.new.freeze
149
+
150
+ attr_reader :current_record
151
+
152
+ def initialize(records)
153
+ @records = records.to_enum
154
+ @next_record = nil
155
+ @current_record = nil
156
+ end
157
+
158
+ def empty?
159
+ peek.equal?(END_OF_INPUT)
160
+ end
161
+
162
+ def shift_record
163
+ record = peek
164
+ return if record.equal?(END_OF_INPUT)
165
+
166
+ @next_record = nil
167
+ @current_record = record
168
+ end
169
+
170
+ def shift
171
+ shift_record&.value
172
+ end
173
+
174
+ def each_remaining
175
+ return enum_for(:each_remaining) unless block_given?
176
+
177
+ yield shift until empty?
178
+ end
179
+
180
+ def remaining_values
181
+ each_remaining.to_a
182
+ end
183
+
184
+ private
185
+
186
+ def peek
187
+ return @next_record if @next_record
188
+
189
+ @next_record = @records.next
190
+ rescue StopIteration
191
+ @next_record = END_OF_INPUT
192
+ end
193
+ end
194
+
195
+ def initialize(filter_string, opts = {})
196
+ @filter_string = filter_string || '.'
197
+ @opts = self.class.normalize_options(DEFAULT_OPTIONS.merge(opts))
198
+ @program = Rjq.compile(@filter_string, Compiler.options_from(@opts))
199
+ end
200
+
201
+ def run_values(values, input_queue: nil)
202
+ records = values.lazy.map { |value| InputRecord.new(value: value, filename: nil, line: 1) }
203
+ queue = InputQueue.new(records)
204
+ builtin_queue = input_queue.is_a?(InputQueue) ? input_queue : queue
205
+ run_queue(queue, builtin_queue)
206
+ end
207
+
208
+ def run_stream(io, &block)
209
+ enum = run_io_streams([[io, nil, false]])
210
+ return enum unless block
211
+
212
+ enum.each(&block)
213
+ end
214
+
215
+ def run_io_streams(streams)
216
+ owned_streams = []
217
+ records = records_from_streams(streams, owned_streams)
218
+ close_streams = lambda do
219
+ owned_streams.each { |io| io.close unless io.closed? }
220
+ end
221
+ if @opts[:null_input]
222
+ main = InputQueue.new([InputRecord.new(value: nil, filename: nil, line: 1)])
223
+ return run_queue(main, InputQueue.new(records), on_close: close_streams)
224
+ end
225
+
226
+ run_queue(InputQueue.new(records), on_close: close_streams)
227
+ end
228
+
229
+ def format_output(value)
230
+ return value if @opts[:raw_output] && value.is_a?(String)
231
+
232
+ indent = @opts[:compact] ? nil : @opts[:indent]
233
+ dumped = JSON::Dumper.dump(value, indent: indent, sort_keys: @opts[:sort_keys], ascii: @opts[:ascii],
234
+ tab: @opts[:tab])
235
+ @opts[:color] ? Color.colorize(dumped) : dumped
236
+ end
237
+
238
+ def write_output(value, io)
239
+ if @opts[:raw_output] && value.is_a?(String)
240
+ io << value
241
+ elsif @opts[:color]
242
+ io << format_output(value)
243
+ else
244
+ indent = @opts[:compact] ? nil : @opts[:indent]
245
+ JSON::Dumper.dump(value, indent: indent, sort_keys: @opts[:sort_keys], ascii: @opts[:ascii],
246
+ tab: @opts[:tab], io: io)
247
+ end
248
+ end
249
+
250
+ private
251
+
252
+ def run_queue(queue, builtin_queue = queue, on_close: nil)
253
+ ResultStream.new(on_close: on_close) do |emit|
254
+ output_count = 0
255
+ instruction_budget = VM::InstructionBudget.new(@opts[:max_instructions])
256
+ until queue.empty?
257
+ record = queue.shift_record
258
+ run_opts = @opts.merge(
259
+ variables: @opts.fetch(:variables, {}), input_queue: builtin_queue,
260
+ remaining_inputs: builtin_queue, current_filename: display_filename(record.filename),
261
+ current_line: record.line
262
+ )
263
+ begin
264
+ @program.run_with_instruction_budget(record.value, run_opts, instruction_budget).each do |result|
265
+ output_count += 1
266
+ max_outputs = @opts[:max_outputs]
267
+ raise RuntimeError, "output limit exceeded (#{max_outputs})" if max_outputs && output_count > max_outputs
268
+
269
+ emit.call(result)
270
+ end
271
+ rescue ResourceLimitError
272
+ raise
273
+ rescue Rjq::RuntimeError => e
274
+ handler = @opts[:runtime_error_handler]
275
+ raise unless handler
276
+
277
+ handler.call(e, record)
278
+ end
279
+ end
280
+ end
281
+ end
282
+
283
+ def records_from_streams(streams, owned_streams)
284
+ return raw_slurp_record(streams, owned_streams) if @opts[:slurp] && @opts[:raw_input]
285
+
286
+ records = uncollected_records(streams, owned_streams)
287
+ return records unless @opts[:slurp]
288
+
289
+ Enumerator.new do |yielder|
290
+ collected = records.to_a
291
+ yielder << InputRecord.new(value: collected.map(&:value), filename: collected.last&.filename, line: 1)
292
+ end
293
+ end
294
+
295
+ def raw_slurp_record(streams, owned_streams)
296
+ Enumerator.new do |yielder|
297
+ content = +''.b
298
+ last_filename = nil
299
+ streams.each do |io, filename, close_after|
300
+ owned_streams << io if close_after && !owned_streams.include?(io)
301
+ begin
302
+ while (chunk = io.read(input_chunk_size))
303
+ break if chunk.empty?
304
+
305
+ content << chunk.b
306
+ end
307
+ last_filename = filename
308
+ ensure
309
+ io.close if close_after && !io.closed?
310
+ end
311
+ end
312
+ content.force_encoding(Encoding::UTF_8)
313
+ yielder << InputRecord.new(value: content, filename: last_filename, line: 1)
314
+ end
315
+ end
316
+
317
+ def uncollected_records(streams, owned_streams)
318
+ Enumerator.new do |yielder|
319
+ streams.each do |io, filename, close_after|
320
+ owned_streams << io if close_after && !owned_streams.include?(io)
321
+ begin
322
+ records_for_io(io, filename).each { |record| yielder << record }
323
+ ensure
324
+ io.close if close_after && !io.closed?
325
+ end
326
+ end
327
+ end
328
+ end
329
+
330
+ def records_for_io(io, filename)
331
+ return raw_records(io, filename) if @opts[:raw_input]
332
+ return stream_records(io, filename) if @opts[:stream]
333
+
334
+ JSON::Parser.parse_records(io, seq: @opts[:seq], chunk_size: input_chunk_size,
335
+ on_error: method(:warn_ignored_parse_error),
336
+ max_depth: input_max_depth, max_number_digits: @opts[:max_number_digits],
337
+ max_string_bytes: @opts[:max_string_bytes]).lazy.map do |parsed|
338
+ InputRecord.new(value: parsed.value, filename: filename, line: parsed.line)
339
+ end
340
+ end
341
+
342
+ def raw_records(io, filename)
343
+ Enumerator.new do |yielder|
344
+ io.each_line.with_index(1) do |line, line_number|
345
+ value = line.delete_suffix("\n").delete_suffix("\r")
346
+ yielder << InputRecord.new(value: value, filename: filename, line: line_number)
347
+ end
348
+ end
349
+ end
350
+
351
+ def stream_records(io, filename)
352
+ JSON::StreamParser.parse(io, seq: @opts[:seq], stream_errors: @opts[:stream_errors],
353
+ chunk_size: input_chunk_size,
354
+ on_error: method(:warn_ignored_parse_error),
355
+ max_depth: input_max_depth, max_number_digits: @opts[:max_number_digits],
356
+ max_string_bytes: @opts[:max_string_bytes], locations: true).lazy.map do |parsed|
357
+ InputRecord.new(value: parsed.value, filename: filename, line: parsed.line)
358
+ end
359
+ end
360
+
361
+ def input_chunk_size
362
+ @opts.fetch(:input_chunk_size, JSON::InputBuffer::DEFAULT_CHUNK_SIZE)
363
+ end
364
+
365
+ def input_max_depth
366
+ @opts.fetch(:input_max_depth, JSON::Parser::DEFAULT_MAX_DEPTH)
367
+ end
368
+
369
+ def display_filename(filename)
370
+ filename || '<stdin>'
371
+ end
372
+
373
+ def warn_ignored_parse_error(message)
374
+ (@opts[:stderr] || $stderr).puts("rjq: ignoring parse error: #{message}")
375
+ end
376
+ end
377
+ end