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.
@@ -0,0 +1,526 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rjq
4
+ module JSON
5
+ class StreamParser
6
+ DEFAULT_MAX_DEPTH = 256
7
+ Result = Struct.new(:events, :close_path, keyword_init: true)
8
+ ParsedEvent = Struct.new(:value, :line, keyword_init: true)
9
+ class StreamError < StandardError
10
+ attr_reader :path
11
+
12
+ def initialize(message:, path:)
13
+ @path = path
14
+ super(message)
15
+ end
16
+ end
17
+
18
+ class << self
19
+ def parse(io_or_string, seq: false, stream_errors: false, chunk_size: InputBuffer::DEFAULT_CHUNK_SIZE,
20
+ on_error: nil, max_depth: DEFAULT_MAX_DEPTH, max_number_digits: nil, max_string_bytes: nil,
21
+ locations: false)
22
+ new(io_or_string, seq: seq, stream_errors: stream_errors, chunk_size: chunk_size,
23
+ on_error: on_error, max_depth: max_depth, max_number_digits: max_number_digits,
24
+ max_string_bytes: max_string_bytes, locations: locations).parse
25
+ end
26
+ end
27
+
28
+ def initialize(input, seq: false, stream_errors: false, chunk_size: InputBuffer::DEFAULT_CHUNK_SIZE,
29
+ on_error: nil, max_depth: DEFAULT_MAX_DEPTH, max_number_digits: nil, max_string_bytes: nil,
30
+ locations: false)
31
+ validate_options!(chunk_size, max_depth, max_number_digits, max_string_bytes)
32
+ @input = InputBuffer.new(input, chunk_size: chunk_size)
33
+ @seq = seq
34
+ @stream_errors = stream_errors
35
+ @on_error = on_error
36
+ @max_depth = max_depth
37
+ @max_number_digits = max_number_digits
38
+ @max_string_bytes = max_string_bytes
39
+ @locations = locations
40
+ @depth = 0
41
+ @index = 0
42
+ @line = 1
43
+ @column = 1
44
+ advance if current == "\uFEFF"
45
+ end
46
+
47
+ def parse
48
+ Enumerator.new do |yielder|
49
+ @events = yielder
50
+ parse_values
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def parse_values
57
+ until eof?
58
+ skip_separators
59
+ break if eof?
60
+
61
+ begin
62
+ value_line = @line
63
+ value_column = @column
64
+ result = parse_value([])
65
+ skip_whitespace
66
+ commit(result)
67
+ @unmatched_error_location = [value_line, value_column] if @unmatched_error_location
68
+ raise_error('expected record separator', []) if @seq && !eof? && current != "\x1e"
69
+ @input.discard_before(@index)
70
+ rescue StreamError => e
71
+ if @stream_errors
72
+ emit([e.message, e.path])
73
+ elsif @seq && @on_error
74
+ @on_error.call(e.message)
75
+ else
76
+ raise JSONParseError, e.message
77
+ end
78
+ @unmatched_error_location = [@last_error_line, @last_error_column + 1]
79
+ if @seq
80
+ resync_to_record_separator
81
+ elsif @stream_errors
82
+ resync_to_next_line
83
+ else
84
+ break
85
+ end
86
+ end
87
+ end
88
+ @events
89
+ end
90
+
91
+ def parse_value(path)
92
+ skip_whitespace
93
+ raise_error('Unfinished JSON term at EOF', path) if eof?
94
+
95
+ case current
96
+ when '{'
97
+ parse_object(path)
98
+ when '['
99
+ parse_array(path)
100
+ when '"'
101
+ Result.new(events: [pending_event(path, parse_string(path))], close_path: path)
102
+ when 't'
103
+ consume_literal('true', path)
104
+ Result.new(events: [pending_event(path, true)], close_path: path)
105
+ when 'f'
106
+ consume_literal('false', path)
107
+ Result.new(events: [pending_event(path, false)], close_path: path)
108
+ when 'n'
109
+ consume_literal('null', path)
110
+ Result.new(events: [pending_event(path, nil)], close_path: path)
111
+ when ']', '}'
112
+ delimiter = current
113
+ if @unmatched_error_location
114
+ line, column = @unmatched_error_location
115
+ @unmatched_error_location = nil
116
+ raise_error_at("Unmatched '#{delimiter}' at the top-level", path, line, column)
117
+ end
118
+ raise_error("Unmatched '#{delimiter}' at the top-level", path)
119
+ else
120
+ Result.new(events: [pending_event(path, parse_number(path))], close_path: path)
121
+ end
122
+ end
123
+
124
+ def parse_object(path)
125
+ with_container_depth(path) { parse_object_body(path) }
126
+ end
127
+
128
+ def parse_object_body(path)
129
+ advance
130
+ skip_whitespace
131
+ if consume?('}')
132
+ emit([path, {}])
133
+ return Result.new(events: [], close_path: path)
134
+ end
135
+
136
+ last_path = path
137
+ loop do
138
+ skip_whitespace
139
+ raise_error('Unfinished JSON term at EOF', path + [nil]) if eof?
140
+ raise_error('Expected another key:value pair', path + [nil]) if current == '}'
141
+ unless current == '"'
142
+ advance while current&.match?(/[0-9A-Za-z_+-]/)
143
+ raise_error('Invalid numeric literal', path + [nil])
144
+ end
145
+
146
+ key = parse_string(path + [nil])
147
+ skip_whitespace
148
+ unless consume?(':')
149
+ raise_error('Unfinished JSON term at EOF', path + [nil]) if eof?
150
+
151
+ scan_unexpected_value
152
+ raise_error('Expected separator between values', path + [nil])
153
+ end
154
+ skip_whitespace
155
+ raise_error('Missing value in key:value pair', path + [key]) if ['}', ']'].include?(current)
156
+
157
+ result = parse_value(path + [key])
158
+ skip_whitespace
159
+
160
+ if consume?(',')
161
+ commit(result)
162
+ last_path = result.close_path
163
+ next
164
+ end
165
+
166
+ if consume?('}')
167
+ commit(result)
168
+ last_path = result.close_path
169
+ emit([last_path])
170
+ return Result.new(events: [], close_path: path)
171
+ end
172
+
173
+ raise_error('Unfinished JSON term at EOF', result.close_path) if eof?
174
+
175
+ scan_unexpected_value
176
+ raise_error('Expected separator between values', result.close_path)
177
+ end
178
+ end
179
+
180
+ def parse_array(path)
181
+ with_container_depth(path) { parse_array_body(path) }
182
+ end
183
+
184
+ def parse_array_body(path)
185
+ advance
186
+ skip_whitespace
187
+ if consume?(']')
188
+ emit([path, []])
189
+ return Result.new(events: [], close_path: path)
190
+ end
191
+
192
+ index = 0
193
+ last_path = path
194
+ loop do
195
+ skip_whitespace
196
+ raise_error('Expected another array element', path + [index]) if current == ']'
197
+
198
+ result = parse_value(path + [index])
199
+ skip_whitespace
200
+
201
+ if consume?(',')
202
+ commit(result)
203
+ last_path = result.close_path
204
+ index += 1
205
+ next
206
+ end
207
+
208
+ if consume?(']')
209
+ commit(result)
210
+ last_path = result.close_path
211
+ emit([last_path])
212
+ return Result.new(events: [], close_path: path)
213
+ end
214
+
215
+ raise_error('Unfinished JSON term at EOF', result.close_path) if eof?
216
+
217
+ scan_unexpected_value
218
+ raise_error('Expected separator between values', result.close_path)
219
+ end
220
+ end
221
+
222
+ def commit(result)
223
+ result.events.each { |event| emit(event) }
224
+ end
225
+
226
+ def emit(event)
227
+ parsed = event.is_a?(ParsedEvent) ? event : located_event(event)
228
+ @events << (@locations ? parsed : parsed.value)
229
+ end
230
+
231
+ def located_event(value)
232
+ ParsedEvent.new(value: value, line: @line)
233
+ end
234
+
235
+ def pending_event(path, value)
236
+ event = [path, value]
237
+ path.empty? ? located_event(event) : event
238
+ end
239
+
240
+ def parse_string(path = [])
241
+ expect('"', [])
242
+ out = +''
243
+ bytes = 0
244
+ until eof?
245
+ line = @line
246
+ column = @column
247
+ char = advance
248
+ return out.force_encoding(Encoding::UTF_8) if char == '"'
249
+
250
+ piece = if char == '\\'
251
+ parse_escape
252
+ else
253
+ raise_error('unescaped control character in string', path) if char.ord < 0x20
254
+
255
+ char
256
+ end
257
+ bytes += piece.bytesize
258
+ if @max_string_bytes && bytes > @max_string_bytes
259
+ raise_error_at("string exceeds #{@max_string_bytes} byte limit", path, line, column)
260
+ end
261
+
262
+ out << piece
263
+ end
264
+ raise_error('Unfinished JSON term at EOF', [])
265
+ end
266
+
267
+ def parse_escape
268
+ raise_error('Unfinished JSON term at EOF', []) if eof?
269
+
270
+ char = advance
271
+ case char
272
+ when '"', '\\', '/'
273
+ char
274
+ when 'b'
275
+ "\b"
276
+ when 'f'
277
+ "\f"
278
+ when 'n'
279
+ "\n"
280
+ when 'r'
281
+ "\r"
282
+ when 't'
283
+ "\t"
284
+ when 'u'
285
+ parse_unicode_escape
286
+ else
287
+ raise_error("invalid escape: \\#{char}", [])
288
+ end
289
+ end
290
+
291
+ def parse_unicode_escape
292
+ codepoint = read_hex4
293
+ if high_surrogate?(codepoint)
294
+ raise_error('missing low surrogate', []) unless @input[@index, 2] == '\\u'
295
+
296
+ 2.times { advance }
297
+ low = read_hex4
298
+ raise_error('invalid low surrogate', []) unless low_surrogate?(low)
299
+
300
+ codepoint = 0x10000 + ((codepoint - 0xD800) << 10) + (low - 0xDC00)
301
+ elsif low_surrogate?(codepoint)
302
+ raise_error('unexpected low surrogate', [])
303
+ end
304
+ [codepoint].pack('U')
305
+ end
306
+
307
+ def parse_number(path)
308
+ start = @index
309
+ digits = 0
310
+ consume?('-')
311
+ if current == '0'
312
+ digits = consume_number_digit(digits, path)
313
+ invalid_numeric_literal(path) if digit?(current)
314
+ else
315
+ invalid_numeric_literal(path) unless digit_1_9?(current)
316
+ digits = consume_number_digit(digits, path) while digit?(current)
317
+ end
318
+
319
+ if consume?('.')
320
+ invalid_numeric_literal(path) unless digit?(current)
321
+ digits = consume_number_digit(digits, path) while digit?(current)
322
+ end
323
+
324
+ if %w[e E].include?(current)
325
+ advance
326
+ advance if ['+', '-'].include?(current)
327
+ invalid_numeric_literal(path) unless digit?(current)
328
+ digits = consume_number_digit(digits, path) while digit?(current)
329
+ end
330
+
331
+ literal = @input[start...@index]
332
+ invalid_numeric_literal(path) unless number_delimiter?(current)
333
+
334
+ Number.parse(literal)
335
+ rescue ArgumentError
336
+ invalid_numeric_literal(path)
337
+ end
338
+
339
+ def number_delimiter?(char)
340
+ char.nil? || char.match?(/[\s,\]}\x1e]/)
341
+ end
342
+
343
+ def consume_literal(literal, path)
344
+ unless @input[@index, literal.length] == literal
345
+ scan_invalid_token
346
+ raise_error(eof? ? 'Invalid literal at EOF' : 'Invalid literal', path)
347
+ end
348
+
349
+ literal.length.times { advance }
350
+ if atom_char?(current)
351
+ scan_invalid_token
352
+ raise_error(eof? ? 'Invalid literal at EOF' : 'Invalid literal', path)
353
+ end
354
+ end
355
+
356
+ def atom_char?(char)
357
+ char&.match?(/[0-9A-Za-z_]/)
358
+ end
359
+
360
+ def read_hex4
361
+ chars = @input[@index, 4]
362
+ raise_error('invalid unicode escape', []) unless chars&.match?(/\A[0-9a-fA-F]{4}\z/)
363
+
364
+ 4.times { advance }
365
+ chars.to_i(16)
366
+ end
367
+
368
+ def high_surrogate?(codepoint)
369
+ codepoint.between?(0xD800, 0xDBFF)
370
+ end
371
+
372
+ def low_surrogate?(codepoint)
373
+ codepoint.between?(0xDC00, 0xDFFF)
374
+ end
375
+
376
+ def skip_separators
377
+ loop do
378
+ skip_whitespace
379
+ break unless @seq && current == "\x1e"
380
+
381
+ advance
382
+ end
383
+ end
384
+
385
+ def resync_to_record_separator
386
+ advance until eof? || current == "\x1e"
387
+ @input.discard_before(@index)
388
+ end
389
+
390
+ def resync_to_next_line
391
+ advance until eof? || current == "\n"
392
+ advance if current == "\n"
393
+ @input.discard_before(@index)
394
+ end
395
+
396
+ def with_container_depth(path)
397
+ @depth += 1
398
+ raise_error('Exceeds depth limit for parsing', path) if @depth > @max_depth
399
+
400
+ yield
401
+ ensure
402
+ @depth -= 1
403
+ end
404
+
405
+ def consume_number_digit(count, path)
406
+ count += 1
407
+ if @max_number_digits && count > @max_number_digits
408
+ raise_error("Number exceeds #{@max_number_digits} digit limit", path)
409
+ end
410
+
411
+ advance
412
+ count
413
+ end
414
+
415
+ def validate_options!(chunk_size, max_depth, max_number_digits, max_string_bytes)
416
+ validate_nonnegative_integer!(:max_depth, max_depth)
417
+ validate_nonnegative_integer!(:max_number_digits, max_number_digits, optional: true)
418
+ validate_nonnegative_integer!(:max_string_bytes, max_string_bytes, optional: true)
419
+ return if chunk_size.is_a?(Integer) && chunk_size.positive?
420
+
421
+ raise ArgumentError, 'chunk_size must be a positive Integer'
422
+ end
423
+
424
+ def validate_nonnegative_integer!(name, value, optional: false)
425
+ return if optional && value.nil?
426
+ return if value.is_a?(Integer) && value >= 0
427
+
428
+ raise ArgumentError, "#{name} must be a non-negative Integer#{' or nil' if optional}"
429
+ end
430
+
431
+ def skip_whitespace
432
+ advance while current&.match?(/[ \t\r\n]/)
433
+ end
434
+
435
+ def scan_unexpected_value
436
+ skip_whitespace
437
+ return if eof? || current == ',' || current == ']' || current == '}'
438
+
439
+ if current == '"'
440
+ begin
441
+ parse_string
442
+ rewind_one unless @index.zero?
443
+ rescue StreamError
444
+ nil
445
+ end
446
+ else
447
+ scan_invalid_token
448
+ end
449
+ skip_whitespace
450
+ end
451
+
452
+ def scan_invalid_token
453
+ advance while current && !current.match?(/[,\]\s}\x1e]/)
454
+ end
455
+
456
+ def invalid_numeric_literal(path)
457
+ scan_invalid_token
458
+ raise_error(eof? ? 'Invalid numeric literal at EOF' : 'Invalid numeric literal', path)
459
+ end
460
+
461
+ def expect(char, path)
462
+ raise_error("expected #{char}", path) unless consume?(char)
463
+ end
464
+
465
+ def consume?(char)
466
+ return false unless current == char
467
+
468
+ advance
469
+ true
470
+ end
471
+
472
+ def advance
473
+ char = current
474
+ @index += char.length
475
+ if char == "\n"
476
+ @line += 1
477
+ @column = 1
478
+ else
479
+ @column += 1
480
+ end
481
+ char
482
+ end
483
+
484
+ def rewind_one
485
+ @index -= 1
486
+ @column -= 1
487
+ end
488
+
489
+ def current
490
+ @input[@index]
491
+ end
492
+
493
+ def eof?
494
+ current.nil?
495
+ end
496
+
497
+ def digit?(char)
498
+ !char.nil? && char >= '0' && char <= '9'
499
+ end
500
+
501
+ def digit_1_9?(char)
502
+ !char.nil? && char >= '1' && char <= '9'
503
+ end
504
+
505
+ def raise_error(message, path)
506
+ @last_error_line = line_number
507
+ @last_error_column = column_number
508
+ raise StreamError.new(message: "#{message} at line #{line_number}, column #{column_number}", path: path)
509
+ end
510
+
511
+ def raise_error_at(message, path, line, column)
512
+ @last_error_line = line
513
+ @last_error_column = column
514
+ raise StreamError.new(message: "#{message} at line #{line}, column #{column}", path: path)
515
+ end
516
+
517
+ def line_number
518
+ @line
519
+ end
520
+
521
+ def column_number
522
+ eof? ? [@column - 1, 1].max : @column
523
+ end
524
+ end
525
+ end
526
+ end
data/lib/rjq/json.rb ADDED
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'json/parser'
4
+ require_relative 'json/dumper'