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.
- checksums.yaml +7 -0
- data/ARCHITECTURE.md +38 -0
- data/CHANGELOG.md +7 -0
- data/COMPATIBILITY.md +48 -0
- data/CONTRIBUTING.md +30 -0
- data/LICENSE.txt +21 -0
- data/README.md +192 -0
- data/SECURITY.md +10 -0
- data/bin/rjq +9 -0
- data/lib/rjq/ast.rb +1475 -0
- data/lib/rjq/builtins/array.rb +3 -0
- data/lib/rjq/builtins/core.rb +3 -0
- data/lib/rjq/builtins/date.rb +3 -0
- data/lib/rjq/builtins/format.rb +3 -0
- data/lib/rjq/builtins/io.rb +3 -0
- data/lib/rjq/builtins/math.rb +3 -0
- data/lib/rjq/builtins/regex.rb +3 -0
- data/lib/rjq/builtins/sql.rb +3 -0
- data/lib/rjq/builtins/stream.rb +3 -0
- data/lib/rjq/builtins/string.rb +3 -0
- data/lib/rjq/builtins.rb +2103 -0
- data/lib/rjq/cli.rb +459 -0
- data/lib/rjq/color.rb +36 -0
- data/lib/rjq/compiler.rb +392 -0
- data/lib/rjq/errors.rb +76 -0
- data/lib/rjq/json/dumper.rb +191 -0
- data/lib/rjq/json/input_buffer.rb +99 -0
- data/lib/rjq/json/parser.rb +405 -0
- data/lib/rjq/json/stream_parser.rb +526 -0
- data/lib/rjq/json.rb +4 -0
- data/lib/rjq/lexer.rb +344 -0
- data/lib/rjq/math_functions.rb +168 -0
- data/lib/rjq/module_loader.rb +178 -0
- data/lib/rjq/modules.rb +88 -0
- data/lib/rjq/number.rb +189 -0
- data/lib/rjq/opcodes.rb +130 -0
- data/lib/rjq/parser.rb +755 -0
- data/lib/rjq/path.rb +250 -0
- data/lib/rjq/runtime.rb +377 -0
- data/lib/rjq/semantic_analyzer.rb +209 -0
- data/lib/rjq/value.rb +287 -0
- data/lib/rjq/version.rb +5 -0
- data/lib/rjq/vm.rb +1359 -0
- data/lib/rjq.rb +41 -0
- metadata +106 -0
data/lib/rjq/compiler.rb
ADDED
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
class CompiledProgram
|
|
5
|
+
attr_reader :ast, :program
|
|
6
|
+
|
|
7
|
+
def initialize(ast, program:)
|
|
8
|
+
@ast = ast
|
|
9
|
+
@program = program
|
|
10
|
+
freeze
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def instructions
|
|
14
|
+
program.instructions
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def run(input_value, opts = {})
|
|
18
|
+
VM.new(self, Runtime.normalize_options(opts)).run(input_value)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def run_with_instruction_budget(input_value, opts, budget)
|
|
22
|
+
normalized = Runtime.normalize_options(opts)
|
|
23
|
+
unless budget.is_a?(VM::InstructionBudget) && budget.maximum == normalized[:max_instructions]
|
|
24
|
+
raise ArgumentError, 'instruction budget must match max_instructions'
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
VM.new(self, normalized, instruction_budget: budget).run(input_value)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def disasm
|
|
31
|
+
program.disasm
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
class BytecodeCompiler
|
|
36
|
+
def initialize(allow_comments: true, max_filter_depth: Parser::DEFAULT_MAX_FILTER_DEPTH)
|
|
37
|
+
@constants = []
|
|
38
|
+
@constant_indices = {}
|
|
39
|
+
@current_nodes = []
|
|
40
|
+
@allow_comments = allow_comments
|
|
41
|
+
@max_filter_depth = max_filter_depth
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def compile(ast, module_metadata: {}, module_variables: {})
|
|
45
|
+
instructions = compile_node(ast.body)
|
|
46
|
+
Rjq::Program.new(
|
|
47
|
+
instructions: instructions,
|
|
48
|
+
constants: @constants,
|
|
49
|
+
definitions: ast.definitions.map { |definition| compile_definition(definition) },
|
|
50
|
+
module_metadata: module_metadata,
|
|
51
|
+
module_variables: module_variables
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
private
|
|
56
|
+
|
|
57
|
+
def compile_node(node)
|
|
58
|
+
@current_nodes << node
|
|
59
|
+
case node
|
|
60
|
+
when AST::Identity
|
|
61
|
+
[instruction(:load_input)]
|
|
62
|
+
when AST::Literal
|
|
63
|
+
[instruction(:load_const, const(node.value))]
|
|
64
|
+
when AST::StringLiteral
|
|
65
|
+
compile_string(node)
|
|
66
|
+
when AST::Format
|
|
67
|
+
compile_format(node)
|
|
68
|
+
when AST::Variable
|
|
69
|
+
[instruction(:variable, node.name, nil, loc: node.source_span)]
|
|
70
|
+
when AST::Field
|
|
71
|
+
compile_node(node.base) + [instruction(:field, node.name)]
|
|
72
|
+
when AST::Index
|
|
73
|
+
compile_index(node)
|
|
74
|
+
when AST::Slice
|
|
75
|
+
compile_slice(node)
|
|
76
|
+
when AST::Iterate
|
|
77
|
+
compile_node(node.base) + [instruction(:each)]
|
|
78
|
+
when AST::Optional
|
|
79
|
+
[instruction(:optional, block_for(node.node))]
|
|
80
|
+
when AST::Pipe
|
|
81
|
+
compile_node(node.left) + [instruction(:pipe, block_for(node.right))]
|
|
82
|
+
when AST::Comma
|
|
83
|
+
compile_node(node.left) + [instruction(:append, block_for(node.right))]
|
|
84
|
+
when AST::Binding
|
|
85
|
+
[instruction(:binding, {
|
|
86
|
+
source: block_for(node.source),
|
|
87
|
+
pattern: node.pattern,
|
|
88
|
+
body: block_for(node.body)
|
|
89
|
+
})]
|
|
90
|
+
when AST::ArrayLiteral
|
|
91
|
+
expression = node.expression
|
|
92
|
+
[instruction(:array, expression ? block_for(expression) : nil)]
|
|
93
|
+
when AST::ObjectLiteral
|
|
94
|
+
[instruction(:object, compile_object_pairs(node.pairs))]
|
|
95
|
+
when AST::FunctionCall
|
|
96
|
+
compile_call(node)
|
|
97
|
+
when AST::BinaryOp
|
|
98
|
+
compile_binary(node)
|
|
99
|
+
when AST::UnaryOp
|
|
100
|
+
[instruction(:unary, node.op, block_for(node.expression))]
|
|
101
|
+
when AST::If
|
|
102
|
+
compile_if(node)
|
|
103
|
+
when AST::Try
|
|
104
|
+
[instruction(:try, { body: block_for(node.body), handler: optional_block(node.handler) })]
|
|
105
|
+
when AST::Reduce
|
|
106
|
+
[instruction(:reduce, {
|
|
107
|
+
generator: block_for(node.generator),
|
|
108
|
+
pattern: node.variable,
|
|
109
|
+
initial: block_for(node.initial),
|
|
110
|
+
update: block_for(node.update)
|
|
111
|
+
})]
|
|
112
|
+
when AST::Foreach
|
|
113
|
+
[instruction(:foreach, {
|
|
114
|
+
generator: block_for(node.generator),
|
|
115
|
+
pattern: node.variable,
|
|
116
|
+
initial: block_for(node.initial),
|
|
117
|
+
update: block_for(node.update),
|
|
118
|
+
extract: optional_block(node.extract)
|
|
119
|
+
})]
|
|
120
|
+
when AST::Label
|
|
121
|
+
[instruction(:label, node.label, block_for(node.body))]
|
|
122
|
+
when AST::Break
|
|
123
|
+
[instruction(:break, node.label)]
|
|
124
|
+
when AST::Assignment
|
|
125
|
+
[instruction(:assign,
|
|
126
|
+
{ left: block_for(node.left), op: node.op, right: block_for(node.right) })]
|
|
127
|
+
when AST::ScopedDefinition
|
|
128
|
+
[instruction(:scoped_def, compile_definition(node.definition), block_for(node.body))]
|
|
129
|
+
when AST::Recurse
|
|
130
|
+
[instruction(:recurse)]
|
|
131
|
+
else
|
|
132
|
+
raise CompileError, "unsupported AST node #{node.class}"
|
|
133
|
+
end
|
|
134
|
+
ensure
|
|
135
|
+
@current_nodes.pop
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def compile_string(node)
|
|
139
|
+
value = node.value
|
|
140
|
+
return [instruction(:load_const, const(value))] if value.is_a?(String)
|
|
141
|
+
|
|
142
|
+
[instruction(:string_interp, value.map { |kind, segment| compile_string_segment(kind, segment) })]
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def compile_string_segment(kind, value)
|
|
146
|
+
return { kind: :text, value: value } if kind == :text
|
|
147
|
+
|
|
148
|
+
fragment = value.is_a?(SourceFragment) ? value : SourceFragment.new(
|
|
149
|
+
source: value, filename: '<top-level>', line: 1, column: 1, start_offset: 0
|
|
150
|
+
)
|
|
151
|
+
parsed = Parser.new(fragment.source, allow_comments: @allow_comments, source_name: fragment.filename,
|
|
152
|
+
initial_line: fragment.line, initial_column: fragment.column,
|
|
153
|
+
start_offset: fragment.start_offset,
|
|
154
|
+
max_filter_depth: @max_filter_depth).parse
|
|
155
|
+
{ kind: :expr, block: block_for(parsed.body), definitions: parsed.definitions.map do |definition|
|
|
156
|
+
compile_definition(definition)
|
|
157
|
+
end }
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def compile_format(node)
|
|
161
|
+
expression = node.expression
|
|
162
|
+
return [instruction(:format, node.name, nil)] unless expression
|
|
163
|
+
|
|
164
|
+
if expression.is_a?(AST::StringLiteral) && expression.value.is_a?(Array)
|
|
165
|
+
return [instruction(:format, node.name, { segments: expression.value.map do |kind, value|
|
|
166
|
+
compile_string_segment(kind, value)
|
|
167
|
+
end })]
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
[instruction(:format, node.name, { block: block_for(expression) })]
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
def compile_index(node)
|
|
174
|
+
base = compile_node(node.base)
|
|
175
|
+
index = node.index
|
|
176
|
+
return base + [instruction(:index_const, index.value)] if index.is_a?(AST::Literal)
|
|
177
|
+
|
|
178
|
+
base + [instruction(:index_filter, block_for(index))]
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def compile_slice(node)
|
|
182
|
+
start_node = node.start_node
|
|
183
|
+
finish_node = node.finish_node
|
|
184
|
+
base = compile_node(node.base)
|
|
185
|
+
if literal_or_nil?(start_node) && literal_or_nil?(finish_node)
|
|
186
|
+
return base + [instruction(:slice_const, literal_value(start_node), literal_value(finish_node))]
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
base + [instruction(:slice_filter, { start: optional_block(start_node), finish: optional_block(finish_node) })]
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
def compile_call(node)
|
|
193
|
+
arg_blocks = node.args.map { |arg| block_for(arg) }
|
|
194
|
+
return [instruction(:path, arg_blocks.first)] if node.name == 'path' && arg_blocks.length == 1
|
|
195
|
+
|
|
196
|
+
[instruction(:call, node.name, arg_blocks)]
|
|
197
|
+
end
|
|
198
|
+
|
|
199
|
+
def compile_binary(node)
|
|
200
|
+
[instruction(:binary, node.op, [block_for(node.left), block_for(node.right)])]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
def compile_if(node)
|
|
204
|
+
[instruction(
|
|
205
|
+
:branch,
|
|
206
|
+
block_for(node.condition),
|
|
207
|
+
[block_for(node.then_branch), block_for(node.else_branch)]
|
|
208
|
+
)]
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
def block_for(node)
|
|
212
|
+
BytecodeBlock.new(instructions: compile_node(node))
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
def optional_block(node)
|
|
216
|
+
node ? block_for(node) : nil
|
|
217
|
+
end
|
|
218
|
+
|
|
219
|
+
def compile_definition(definition)
|
|
220
|
+
compiled = BytecodeFunctionDefinition.new(
|
|
221
|
+
name: definition.name,
|
|
222
|
+
params: definition.params,
|
|
223
|
+
body: block_for(definition.body)
|
|
224
|
+
)
|
|
225
|
+
mark_tail_calls(compiled.body)
|
|
226
|
+
compiled
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
def mark_tail_calls(block)
|
|
230
|
+
instruction = block.instructions.last
|
|
231
|
+
return unless instruction
|
|
232
|
+
|
|
233
|
+
case instruction.op
|
|
234
|
+
when :call
|
|
235
|
+
instruction.op = :tail_call
|
|
236
|
+
when :pipe
|
|
237
|
+
mark_tail_calls(instruction.arg1)
|
|
238
|
+
when :branch
|
|
239
|
+
instruction.arg2.each { |branch| mark_tail_calls(branch) }
|
|
240
|
+
when :binding
|
|
241
|
+
mark_tail_calls(instruction.arg1.fetch(:body))
|
|
242
|
+
when :append
|
|
243
|
+
mark_tail_calls(instruction.arg1)
|
|
244
|
+
when :scoped_def
|
|
245
|
+
mark_tail_calls(instruction.arg2)
|
|
246
|
+
end
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def compile_object_pairs(pairs)
|
|
250
|
+
pairs.map do |pair|
|
|
251
|
+
{
|
|
252
|
+
key: compile_object_key(pair.key),
|
|
253
|
+
value: block_for(pair.value)
|
|
254
|
+
}
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
def compile_object_key(key)
|
|
259
|
+
return { type: :filter, block: block_for(key) } if key.is_a?(AST::Node)
|
|
260
|
+
return { type: :filter, block: block_for(AST::StringLiteral.new(key)) } if key.is_a?(Array)
|
|
261
|
+
|
|
262
|
+
{ type: :literal, value: key }
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def literal_or_nil?(node)
|
|
266
|
+
node.nil? || node.is_a?(AST::Literal)
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def literal_value(node)
|
|
270
|
+
node&.value
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
def const(value)
|
|
274
|
+
copy = Value.deep_copy(value)
|
|
275
|
+
key = JSON::Dumper.dump(copy, indent: nil, sort_keys: true)
|
|
276
|
+
return @constant_indices.fetch(key) if @constant_indices.key?(key)
|
|
277
|
+
|
|
278
|
+
@constants << copy
|
|
279
|
+
@constant_indices[key] = @constants.length - 1
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def instruction(op, arg1 = nil, arg2 = nil, loc: nil)
|
|
283
|
+
inherited_location = @current_nodes.reverse_each.lazy.filter_map(&:source_span).first
|
|
284
|
+
location = loc || inherited_location || AST::SourceSpan.new(
|
|
285
|
+
filename: '<top-level>', line: 1, column: 1, start_offset: 0, end_offset: 0
|
|
286
|
+
)
|
|
287
|
+
Instruction.new(op: op, arg1: arg1, arg2: arg2, loc: location)
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
|
|
291
|
+
class Compiler
|
|
292
|
+
OPTION_KEYS = %i[allow_comments library_path max_filter_depth module_resolver source_path].freeze
|
|
293
|
+
|
|
294
|
+
class << self
|
|
295
|
+
def options_from(opts)
|
|
296
|
+
opts.slice(*OPTION_KEYS)
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
def validate_options!(opts)
|
|
300
|
+
raise ArgumentError, 'options must be a Hash' unless opts.is_a?(Hash)
|
|
301
|
+
|
|
302
|
+
unknown = opts.keys - OPTION_KEYS
|
|
303
|
+
raise ArgumentError, "unknown compiler option: #{unknown.first.inspect}" unless unknown.empty?
|
|
304
|
+
|
|
305
|
+
validate_boolean!(opts, :allow_comments)
|
|
306
|
+
validate_optional_string!(opts, :source_path)
|
|
307
|
+
validate_positive_integer!(opts, :max_filter_depth)
|
|
308
|
+
validate_library_path!(opts[:library_path]) if opts.key?(:library_path)
|
|
309
|
+
validate_module_resolver!(opts[:module_resolver]) if opts.key?(:module_resolver)
|
|
310
|
+
opts
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def normalize_options(opts)
|
|
314
|
+
validate_options!(opts)
|
|
315
|
+
normalized = opts.dup
|
|
316
|
+
normalized[:library_path] = normalized[:library_path].dup.freeze if normalized[:library_path]
|
|
317
|
+
normalized.freeze
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
private
|
|
321
|
+
|
|
322
|
+
def validate_boolean!(opts, key)
|
|
323
|
+
return unless opts.key?(key)
|
|
324
|
+
return if opts[key] == true || opts[key] == false
|
|
325
|
+
|
|
326
|
+
raise ArgumentError, "#{key} must be true or false"
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def validate_optional_string!(opts, key)
|
|
330
|
+
return unless opts.key?(key)
|
|
331
|
+
return if opts[key].nil? || opts[key].is_a?(String)
|
|
332
|
+
|
|
333
|
+
raise ArgumentError, "#{key} must be a String or nil"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def validate_positive_integer!(opts, key)
|
|
337
|
+
return unless opts.key?(key)
|
|
338
|
+
return if opts[key].is_a?(Integer) && opts[key].positive?
|
|
339
|
+
|
|
340
|
+
raise ArgumentError, "#{key} must be a positive Integer"
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def validate_library_path!(paths)
|
|
344
|
+
return if paths.is_a?(Array) && paths.all? { |path| path.is_a?(String) }
|
|
345
|
+
|
|
346
|
+
raise ArgumentError, 'library_path must be an Array of Strings'
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def validate_module_resolver!(resolver)
|
|
350
|
+
return if resolver.nil?
|
|
351
|
+
return if resolver.respond_to?(:resolve) && resolver.respond_to?(:initial_metadata)
|
|
352
|
+
|
|
353
|
+
raise ArgumentError, 'module_resolver must be nil or respond to resolve and initial_metadata'
|
|
354
|
+
end
|
|
355
|
+
end
|
|
356
|
+
|
|
357
|
+
def initialize(opts = {})
|
|
358
|
+
@opts = self.class.normalize_options(opts)
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
def compile(filter_string)
|
|
362
|
+
max_filter_depth = @opts.fetch(:max_filter_depth, Parser::DEFAULT_MAX_FILTER_DEPTH)
|
|
363
|
+
source_path = @opts[:source_path] || '<top-level>'
|
|
364
|
+
parsed = Parser.new(filter_string, allow_comments: @opts.fetch(:allow_comments, true),
|
|
365
|
+
source_name: source_path, max_filter_depth: max_filter_depth).parse
|
|
366
|
+
resolver = @opts[:module_resolver] || default_module_resolver
|
|
367
|
+
allow_comments = @opts.fetch(:allow_comments, true)
|
|
368
|
+
loaded = ModuleLoader.new(resolver, allow_comments: allow_comments,
|
|
369
|
+
max_filter_depth: max_filter_depth).load(parsed,
|
|
370
|
+
source_path: @opts[:source_path])
|
|
371
|
+
ast = loaded.program
|
|
372
|
+
program = BytecodeCompiler.new(allow_comments: allow_comments, max_filter_depth: max_filter_depth).compile(
|
|
373
|
+
ast,
|
|
374
|
+
module_metadata: loaded.metadata,
|
|
375
|
+
module_variables: loaded.variables
|
|
376
|
+
)
|
|
377
|
+
SemanticAnalyzer.new(program).validate!
|
|
378
|
+
program.finalize!
|
|
379
|
+
CompiledProgram.new(ast, program: program)
|
|
380
|
+
rescue SystemStackError
|
|
381
|
+
raise CompileError, "filter nesting exceeds safe parser/compiler depth in #{source_path}"
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
private
|
|
385
|
+
|
|
386
|
+
def default_module_resolver
|
|
387
|
+
paths = @opts.fetch(:library_path, [])
|
|
388
|
+
ModuleResolver.new(paths: paths, use_default_paths: paths.empty?)
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
end
|
|
392
|
+
end
|
data/lib/rjq/errors.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
|
|
6
|
+
class ParseError < Error; end
|
|
7
|
+
|
|
8
|
+
class CompileError < Error; end
|
|
9
|
+
|
|
10
|
+
class RuntimeError < Error
|
|
11
|
+
attr_reader :outputs
|
|
12
|
+
|
|
13
|
+
def prepend_outputs(values)
|
|
14
|
+
@outputs = Array(values) + Array(@outputs)
|
|
15
|
+
self
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def take_outputs
|
|
19
|
+
values = Array(@outputs)
|
|
20
|
+
@outputs = nil
|
|
21
|
+
values
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Execution budgets are process-safety boundaries and cannot be caught by
|
|
26
|
+
# jq filters such as `try` or `?`.
|
|
27
|
+
class ResourceLimitError < RuntimeError; end
|
|
28
|
+
|
|
29
|
+
class TypeError < RuntimeError; end
|
|
30
|
+
|
|
31
|
+
class InvalidPathError < TypeError
|
|
32
|
+
attr_reader :result
|
|
33
|
+
|
|
34
|
+
def initialize(message, result, outputs: nil)
|
|
35
|
+
@result = result
|
|
36
|
+
prepend_outputs(outputs)
|
|
37
|
+
super(message)
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class ErrorValue < RuntimeError
|
|
42
|
+
attr_reader :value
|
|
43
|
+
|
|
44
|
+
def initialize(value, outputs: nil)
|
|
45
|
+
@value = value
|
|
46
|
+
super(value.to_s)
|
|
47
|
+
prepend_outputs(outputs)
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# Halting terminates the whole jq program. It is deliberately not a
|
|
52
|
+
# RuntimeError: try/catch and the optional operator only catch ordinary
|
|
53
|
+
# filter failures.
|
|
54
|
+
class HaltError < Error
|
|
55
|
+
attr_reader :value, :status
|
|
56
|
+
|
|
57
|
+
def initialize(value = nil, status = 0)
|
|
58
|
+
@value = value
|
|
59
|
+
@status = status
|
|
60
|
+
super(value.nil? ? 'halt_error' : value.to_s)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
class JSONParseError < Error; end
|
|
65
|
+
|
|
66
|
+
class BreakSignal < RuntimeError
|
|
67
|
+
attr_reader :label, :value, :outputs
|
|
68
|
+
|
|
69
|
+
def initialize(label, value = nil, outputs: nil)
|
|
70
|
+
@label = label
|
|
71
|
+
@value = value
|
|
72
|
+
@outputs = outputs
|
|
73
|
+
super("break #{label}")
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Rjq
|
|
4
|
+
module JSON
|
|
5
|
+
module Dumper
|
|
6
|
+
module_function
|
|
7
|
+
|
|
8
|
+
def dump(value, indent: 2, sort_keys: false, ascii: false, tab: false, io: nil)
|
|
9
|
+
destination = io || +''
|
|
10
|
+
step = tab ? "\t" : (' ' * Integer(indent || 0))
|
|
11
|
+
write(value, destination, indent.nil? ? nil : step, sort_keys, ascii)
|
|
12
|
+
io || destination
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def write(value, destination, indent, sort_keys, ascii)
|
|
16
|
+
active = {}
|
|
17
|
+
stack = [[:value, value, 0]]
|
|
18
|
+
until stack.empty?
|
|
19
|
+
type, item, depth = stack.pop
|
|
20
|
+
case type
|
|
21
|
+
when :text
|
|
22
|
+
destination << item
|
|
23
|
+
when :leave
|
|
24
|
+
active.delete(item)
|
|
25
|
+
when :array_items
|
|
26
|
+
write_array_item(item, depth, destination, stack, indent)
|
|
27
|
+
when :object_items
|
|
28
|
+
write_object_item(item, depth, destination, stack, indent, ascii)
|
|
29
|
+
when :value
|
|
30
|
+
write_value(item, depth, destination, stack, active, indent, sort_keys, ascii)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
private_class_method :write
|
|
35
|
+
|
|
36
|
+
def write_value(value, depth, destination, stack, active, indent, sort_keys, ascii)
|
|
37
|
+
case value
|
|
38
|
+
when NilClass
|
|
39
|
+
destination << 'null'
|
|
40
|
+
when TrueClass
|
|
41
|
+
destination << 'true'
|
|
42
|
+
when FalseClass
|
|
43
|
+
destination << 'false'
|
|
44
|
+
when Number
|
|
45
|
+
destination << value.dump
|
|
46
|
+
when Integer
|
|
47
|
+
destination << value.to_s
|
|
48
|
+
when Float
|
|
49
|
+
destination << dump_float(value)
|
|
50
|
+
when String
|
|
51
|
+
destination << dump_string(value, ascii)
|
|
52
|
+
when Array
|
|
53
|
+
write_array(value, depth, destination, stack, active, indent)
|
|
54
|
+
when Hash
|
|
55
|
+
write_object(value, depth, destination, stack, active, indent, sort_keys, ascii)
|
|
56
|
+
else
|
|
57
|
+
raise TypeError, "unsupported value type: #{value.class}"
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
private_class_method :write_value
|
|
61
|
+
|
|
62
|
+
def write_array(value, depth, destination, stack, active, indent)
|
|
63
|
+
return destination << '[]' if value.empty?
|
|
64
|
+
|
|
65
|
+
enter_container(value, active)
|
|
66
|
+
destination << (indent ? "[\n" : '[')
|
|
67
|
+
stack << [:leave, value.object_id, nil]
|
|
68
|
+
stack << [:text, indent ? "\n#{indent * depth}]" : ']', nil]
|
|
69
|
+
stack << [:array_items, [value, 0], depth]
|
|
70
|
+
end
|
|
71
|
+
private_class_method :write_array
|
|
72
|
+
|
|
73
|
+
def write_array_item(payload, depth, destination, stack, indent)
|
|
74
|
+
value, index = payload
|
|
75
|
+
return if index >= value.length
|
|
76
|
+
|
|
77
|
+
destination << if index.zero?
|
|
78
|
+
indent ? indent * (depth + 1) : ''
|
|
79
|
+
else
|
|
80
|
+
indent ? ",\n#{indent * (depth + 1)}" : ','
|
|
81
|
+
end
|
|
82
|
+
stack << [:array_items, [value, index + 1], depth]
|
|
83
|
+
stack << [:value, value[index], depth + 1]
|
|
84
|
+
end
|
|
85
|
+
private_class_method :write_array_item
|
|
86
|
+
|
|
87
|
+
def write_object(value, depth, destination, stack, active, indent, sort_keys, ascii)
|
|
88
|
+
return destination << '{}' if value.empty?
|
|
89
|
+
|
|
90
|
+
validate_object_keys!(value)
|
|
91
|
+
enter_container(value, active)
|
|
92
|
+
destination << (indent ? "{\n" : '{')
|
|
93
|
+
keys = sort_keys ? value.keys.sort : value.keys
|
|
94
|
+
stack << [:leave, value.object_id, nil]
|
|
95
|
+
stack << [:text, indent ? "\n#{indent * depth}}" : '}', nil]
|
|
96
|
+
stack << [:object_items, [value, keys, 0], depth]
|
|
97
|
+
end
|
|
98
|
+
private_class_method :write_object
|
|
99
|
+
|
|
100
|
+
def write_object_item(payload, depth, destination, stack, indent, ascii)
|
|
101
|
+
value, keys, index = payload
|
|
102
|
+
return if index >= keys.length
|
|
103
|
+
|
|
104
|
+
destination << if index.zero?
|
|
105
|
+
indent ? indent * (depth + 1) : ''
|
|
106
|
+
else
|
|
107
|
+
indent ? ",\n#{indent * (depth + 1)}" : ','
|
|
108
|
+
end
|
|
109
|
+
key = keys[index]
|
|
110
|
+
destination << dump_string(key, ascii)
|
|
111
|
+
destination << (indent ? ': ' : ':')
|
|
112
|
+
stack << [:object_items, [value, keys, index + 1], depth]
|
|
113
|
+
stack << [:value, value.fetch(key), depth + 1]
|
|
114
|
+
end
|
|
115
|
+
private_class_method :write_object_item
|
|
116
|
+
|
|
117
|
+
def enter_container(value, active)
|
|
118
|
+
raise TypeError, 'cannot dump a cyclic JSON value' if active[value.object_id]
|
|
119
|
+
|
|
120
|
+
active[value.object_id] = true
|
|
121
|
+
end
|
|
122
|
+
private_class_method :enter_container
|
|
123
|
+
|
|
124
|
+
def validate_object_keys!(value)
|
|
125
|
+
invalid = value.keys.find { |key| !key.is_a?(String) }
|
|
126
|
+
raise TypeError, "object key must be a string, got #{invalid.class}" if invalid
|
|
127
|
+
end
|
|
128
|
+
private_class_method :validate_object_keys!
|
|
129
|
+
|
|
130
|
+
def dump_float(value)
|
|
131
|
+
return 'null' if value.nan?
|
|
132
|
+
return Float::MAX.to_s if value.infinite? == 1
|
|
133
|
+
return "-#{Float::MAX}" if value.infinite? == -1
|
|
134
|
+
return '-0' if value.zero? && (1.0 / value).infinite? == -1
|
|
135
|
+
|
|
136
|
+
text = value.to_s.sub(/\.0(?=e)/, '')
|
|
137
|
+
if value == value.to_i
|
|
138
|
+
integer_text = value.to_i.to_s
|
|
139
|
+
return integer_text if value.abs < 1e16 || integer_text.length <= text.length
|
|
140
|
+
end
|
|
141
|
+
text.include?('.') || text.match?(/[eE]/) ? text : "#{text}.0"
|
|
142
|
+
end
|
|
143
|
+
private_class_method :dump_float
|
|
144
|
+
|
|
145
|
+
def dump_string(value, ascii)
|
|
146
|
+
string = value.encode(Encoding::UTF_8)
|
|
147
|
+
raise TypeError, 'invalid UTF-8 string' unless string.valid_encoding?
|
|
148
|
+
|
|
149
|
+
escaped = string.each_codepoint.map { |codepoint| escape_codepoint(codepoint, ascii) }.join
|
|
150
|
+
"\"#{escaped}\""
|
|
151
|
+
rescue EncodingError
|
|
152
|
+
raise TypeError, 'invalid UTF-8 string'
|
|
153
|
+
end
|
|
154
|
+
private_class_method :dump_string
|
|
155
|
+
|
|
156
|
+
def escape_codepoint(codepoint, ascii)
|
|
157
|
+
case codepoint
|
|
158
|
+
when 0x22
|
|
159
|
+
'\"'
|
|
160
|
+
when 0x5C
|
|
161
|
+
'\\\\'
|
|
162
|
+
when 0x08
|
|
163
|
+
'\\b'
|
|
164
|
+
when 0x0C
|
|
165
|
+
'\\f'
|
|
166
|
+
when 0x0A
|
|
167
|
+
'\\n'
|
|
168
|
+
when 0x0D
|
|
169
|
+
'\\r'
|
|
170
|
+
when 0x09
|
|
171
|
+
'\\t'
|
|
172
|
+
when 0x00..0x1F
|
|
173
|
+
'\\u%04x' % codepoint
|
|
174
|
+
else
|
|
175
|
+
ascii && codepoint > 0x7F ? unicode_escape(codepoint) : [codepoint].pack('U')
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
private_class_method :escape_codepoint
|
|
179
|
+
|
|
180
|
+
def unicode_escape(codepoint)
|
|
181
|
+
return '\\u%04x' % codepoint if codepoint <= 0xFFFF
|
|
182
|
+
|
|
183
|
+
n = codepoint - 0x10000
|
|
184
|
+
high = 0xD800 + (n >> 10)
|
|
185
|
+
low = 0xDC00 + (n & 0x3FF)
|
|
186
|
+
format('\\u%04x\\u%04x', high, low)
|
|
187
|
+
end
|
|
188
|
+
private_class_method :unicode_escape
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|