json5-ruby 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,599 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON5
4
+ class Lexer
5
+ SIMPLE_ESCAPES = {
6
+ 0x27 => 0x27,
7
+ 0x22 => 0x22,
8
+ 0x5c => 0x5c,
9
+ 0x62 => 0x08,
10
+ 0x66 => 0x0c,
11
+ 0x6e => 0x0a,
12
+ 0x72 => 0x0d,
13
+ 0x74 => 0x09,
14
+ 0x76 => 0x0b
15
+ }.freeze
16
+ KEYWORDS = {
17
+ "true" => :TRUE,
18
+ "false" => :FALSE,
19
+ "null" => :NULL,
20
+ "Infinity" => :INFINITY,
21
+ "NaN" => :NAN
22
+ }.freeze
23
+ PUNCTUATORS = {
24
+ 0x7b => "{",
25
+ 0x7d => "}",
26
+ 0x5b => "[",
27
+ 0x5d => "]",
28
+ 0x3a => ":",
29
+ 0x2c => ","
30
+ }.freeze
31
+ ASCII = ASCIIClassification::TABLE
32
+ ASCII_WHITESPACE = ASCIIClassification::WHITESPACE
33
+ ASCII_DIGIT = ASCIIClassification::DIGIT
34
+ ASCII_HEX_DIGIT = ASCIIClassification::HEX_DIGIT
35
+ ASCII_IDENTIFIER_START = ASCIIClassification::IDENTIFIER_START
36
+ ASCII_IDENTIFIER_PART = ASCIIClassification::IDENTIFIER_PART
37
+ ASCII_QUOTE = ASCIIClassification::QUOTE
38
+ ASCII_PUNCTUATOR = ASCIIClassification::PUNCTUATOR
39
+ ASCII_SLASH = ASCIIClassification::SLASH
40
+ ASCII_BACKSLASH = ASCIIClassification::BACKSLASH
41
+ ASCII_LINE_TERMINATOR = ASCIIClassification::LINE_TERMINATOR
42
+ ASCII_STRING_STOP = ASCII_BACKSLASH | ASCII_LINE_TERMINATOR
43
+ EMPTY_TRIVIA = [].freeze
44
+
45
+ attr_reader :input, :last_token, :nesting_depth,
46
+ :last_token_container, :last_token_nesting_depth
47
+
48
+ def initialize(input, limits: Limits.default, diagnostics: nil, filename: nil, preserve_trivia: false)
49
+ @input = input
50
+ @limits = limits
51
+ @diagnostics = DiagnosticSink.build(diagnostics, filename: filename)
52
+ @filename = filename
53
+ @preserve_trivia = preserve_trivia
54
+ @leading_trivia = []
55
+ @container_stack = []
56
+ @nesting_depth = 0
57
+ end
58
+
59
+ def next_token
60
+ skip_trivia
61
+ @last_token_container = current_container
62
+ @last_token_nesting_depth = @nesting_depth
63
+ start_byte = input.index
64
+ start_line = input.line
65
+ start_column = input.column
66
+ kind, value = scan_token
67
+ finish_token(kind, value, start_byte, start_line, start_column)
68
+ end
69
+
70
+ private
71
+
72
+ def scan_token
73
+ return [:EOF, nil] if input.eof?
74
+ byte = input.peek_byte
75
+ flags = ASCII.fetch(byte)
76
+ return [PUNCTUATORS.fetch(byte), consume_punctuation] unless (flags & ASCII_PUNCTUATOR).zero?
77
+ return [:STRING, scan_string] unless (flags & ASCII_QUOTE).zero?
78
+ return [:NUMBER, scan_number] if number_start?(byte)
79
+ return scan_identifier if identifier_start_byte?(byte) || !(flags & ASCII_BACKSLASH).zero?
80
+
81
+ raise_error("unexpected_character", "unexpected character", input.mark, LexError)
82
+ end
83
+
84
+ def finish_token(kind, value, start_byte, start_line, start_column)
85
+ token = Token.new(
86
+ kind,
87
+ value,
88
+ start_byte,
89
+ input.index,
90
+ start_line,
91
+ start_column,
92
+ nil,
93
+ (@preserve_trivia ? @leading_trivia.freeze : EMPTY_TRIVIA),
94
+ source: input.source
95
+ )
96
+ @last_token = token
97
+ @leading_trivia = []
98
+ token
99
+ end
100
+
101
+ def consume_punctuation
102
+ byte = input.peek_byte
103
+ if byte == 0x7b || byte == 0x5b
104
+ @container_stack << byte
105
+ @nesting_depth = @container_stack.length
106
+ if @limits.max_depth && @nesting_depth > @limits.max_depth
107
+ raise_error("maximum_depth_exceeded", "nested value exceeds max_depth", input.mark, DepthError)
108
+ end
109
+ elsif byte == 0x7d || byte == 0x5d
110
+ matching_open = byte == 0x7d ? 0x7b : 0x5b
111
+ @container_stack.pop if @container_stack.last == matching_open
112
+ @nesting_depth = @container_stack.length
113
+ end
114
+ input.consume_byte
115
+ nil
116
+ end
117
+
118
+ public
119
+
120
+ def current_container
121
+ case @container_stack.last
122
+ when 0x7b then "{"
123
+ when 0x5b then "["
124
+ end
125
+ end
126
+
127
+ private
128
+
129
+ def skip_trivia
130
+ loop do
131
+ if whitespace?
132
+ start = input.mark
133
+ input.consume_codepoint
134
+ record_trivia(:whitespace, start)
135
+ next
136
+ end
137
+ unless (ascii_flags(input.peek_byte) & ASCII_SLASH).zero?
138
+ if input.peek_byte(1) == 0x2f
139
+ start = input.mark
140
+ scan_line_comment(start)
141
+ next
142
+ end
143
+ if input.peek_byte(1) == 0x2a
144
+ start = input.mark
145
+ scan_block_comment(start)
146
+ next
147
+ end
148
+ end
149
+ break
150
+ end
151
+ end
152
+
153
+ def whitespace?
154
+ byte = input.peek_byte
155
+ return false unless byte
156
+ return !(ASCII.fetch(byte) & ASCII_WHITESPACE).zero? if byte < 0x80
157
+
158
+ codepoint, = input.peek_codepoint
159
+ codepoint == 0xa0 || codepoint == 0x2028 || codepoint == 0x2029 || codepoint == 0xfeff ||
160
+ UnicodeTables.space_separator?(codepoint)
161
+ end
162
+
163
+ def scan_line_comment(start)
164
+ input.consume_ascii(0x2f)
165
+ input.consume_ascii(0x2f)
166
+ enforce_comment_limit(start)
167
+ while !input.eof?
168
+ codepoint, = input.peek_codepoint
169
+ break if input.line_terminator?(codepoint)
170
+ input.consume_codepoint
171
+ enforce_comment_limit(start)
172
+ end
173
+ record_trivia(:line_comment, start)
174
+ end
175
+
176
+ def scan_block_comment(start)
177
+ input.consume_ascii(0x2f)
178
+ input.consume_ascii(0x2a)
179
+ enforce_comment_limit(start)
180
+ until input.eof?
181
+ if input.peek_byte == 0x2a && input.peek_byte(1) == 0x2f
182
+ input.consume_byte
183
+ input.consume_byte
184
+ enforce_comment_limit(start)
185
+ record_trivia(:block_comment, start)
186
+ return
187
+ end
188
+ input.consume_codepoint
189
+ enforce_comment_limit(start)
190
+ end
191
+ raise_error("unterminated_comment", "unterminated block comment", start, LexError)
192
+ end
193
+
194
+ def enforce_comment_limit(start)
195
+ return unless @limits.max_comment_bytes && input.index - start.byte_offset > @limits.max_comment_bytes
196
+ raise_error("maximum_comment_size_exceeded", "comment exceeds max_comment_bytes", start, LimitError)
197
+ end
198
+
199
+ def record_trivia(kind, start)
200
+ return unless @preserve_trivia
201
+ raw = input.byteslice(start.byte_offset).freeze
202
+ @leading_trivia << Trivia.new(kind, start.byte_offset, input.index, raw)
203
+ end
204
+
205
+ def scan_identifier
206
+ start = input.mark
207
+ units = []
208
+ escaped = consume_identifier_character(units, first: true)
209
+ while identifier_continuation?
210
+ escaped = consume_identifier_character(units, first: false) || escaped
211
+ end
212
+ name = ECMAString.new(units)
213
+ kind = keyword_kind(start.byte_offset, input.index) unless escaped
214
+ if kind
215
+ enforce_number_limit(start) if %i[INFINITY NAN].include?(kind)
216
+ value = if %i[INFINITY NAN].include?(kind)
217
+ NumberValue.new(input.byteslice(start.byte_offset))
218
+ else
219
+ name
220
+ end
221
+ return [kind, value]
222
+ end
223
+ [:IDENTIFIER_NAME, name]
224
+ end
225
+
226
+ def consume_identifier_character(units, first:)
227
+ start = input.mark
228
+ if input.peek_byte == 0x5c
229
+ unit = consume_identifier_escape
230
+ valid = first ? UnicodeTables.identifier_start?(unit) : UnicodeTables.identifier_part?(unit)
231
+ raise_error("invalid_identifier_character", "escaped character is not valid in IdentifierName", start, LexError) unless valid
232
+ units << unit
233
+ return true
234
+ end
235
+ codepoint, = input.peek_codepoint
236
+ input.consume_codepoint
237
+ if codepoint > 0xffff
238
+ raise_error("invalid_identifier_character", "non-BMP identifier characters are not supported", start, LexError)
239
+ end
240
+ valid = first ? identifier_start_codepoint?(codepoint) : identifier_part_codepoint?(codepoint)
241
+ raise_error("invalid_identifier_character", "character is not valid in IdentifierName", start, LexError) unless valid
242
+ units << codepoint
243
+ false
244
+ end
245
+
246
+ def keyword_kind(start_byte, end_byte)
247
+ length = end_byte - start_byte
248
+ KEYWORDS.each do |word, kind|
249
+ next unless word.bytesize == length
250
+ return kind if word.bytes.each_with_index.all? { |byte, index| input.source.getbyte(start_byte + index) == byte }
251
+ end
252
+ nil
253
+ end
254
+
255
+ def consume_identifier_escape
256
+ start = input.mark
257
+ input.consume_ascii(0x5c)
258
+ unless input.peek_byte == 0x75
259
+ raise_error("invalid_identifier_escape", "IdentifierName escape must use \\uHHHH", start, LexError)
260
+ end
261
+ input.consume_byte
262
+ value = 0
263
+ 4.times do
264
+ byte = input.peek_byte
265
+ unless hex_digit_byte?(byte)
266
+ raise_error("invalid_identifier_escape", "IdentifierName escape requires four hex digits", start, LexError)
267
+ end
268
+ input.consume_byte
269
+ value = (value << 4) | hex_digit_value(byte)
270
+ end
271
+ value
272
+ end
273
+
274
+ def identifier_continuation?
275
+ return false if input.eof?
276
+ return true if input.peek_byte == 0x5c
277
+ byte = input.peek_byte
278
+ return !(ASCII.fetch(byte) & ASCII_IDENTIFIER_PART).zero? if byte < 0x80
279
+
280
+ codepoint, = input.peek_codepoint
281
+ identifier_part_codepoint?(codepoint)
282
+ end
283
+
284
+ def identifier_start_byte?(byte)
285
+ byte >= 0x80 || !(ASCII.fetch(byte) & ASCII_IDENTIFIER_START).zero?
286
+ end
287
+
288
+ def identifier_start_codepoint?(codepoint)
289
+ return false unless codepoint
290
+ return !(ASCII.fetch(codepoint) & ASCII_IDENTIFIER_START).zero? if codepoint < 0x80
291
+
292
+ UnicodeTables.identifier_start?(codepoint)
293
+ end
294
+
295
+ def identifier_part_codepoint?(codepoint)
296
+ return false unless codepoint
297
+ return !(ASCII.fetch(codepoint) & ASCII_IDENTIFIER_PART).zero? if codepoint < 0x80
298
+
299
+ UnicodeTables.identifier_part?(codepoint)
300
+ end
301
+
302
+ def scan_string
303
+ quote = input.consume_byte
304
+ content_start = input.index
305
+ stop = ascii_string_stop(content_start, quote)
306
+ units = consume_ascii_string_prefix(content_start, stop)
307
+
308
+ if input.peek_byte == quote
309
+ input.consume_byte
310
+ return ECMAString.new(units)
311
+ end
312
+
313
+ loop do
314
+ raise_error("unterminated_string", "unterminated string", input.mark, LexError) if input.eof?
315
+ codepoint, = input.peek_codepoint
316
+ if codepoint == quote
317
+ input.consume_byte
318
+ return ECMAString.new(units)
319
+ end
320
+ if codepoint == 0x5c
321
+ scan_escape(units)
322
+ next
323
+ end
324
+ if codepoint == 0x0a || codepoint == 0x0d
325
+ raise_error("unterminated_string", "line terminator in string", input.mark, LexError)
326
+ end
327
+ if codepoint == 0x2028 || codepoint == 0x2029
328
+ position = input.mark
329
+ @diagnostics.emit(
330
+ code: codepoint == 0x2028 ? "unescaped_line_separator_in_string" : "unescaped_paragraph_separator_in_string",
331
+ message: "unescaped Unicode line separator in string",
332
+ byte_offset: input.index,
333
+ end_byte_offset: input.index + 3,
334
+ line: position.line,
335
+ column: position.column
336
+ )
337
+ end
338
+ input.consume_codepoint
339
+ append_codepoint(units, codepoint)
340
+ enforce_string_limit(units)
341
+ end
342
+ end
343
+
344
+ def ascii_string_stop(start, quote)
345
+ index = start
346
+ while (byte = input.source.getbyte(index))
347
+ return index if byte >= 0x80 || byte == quote || !(ASCII.fetch(byte) & ASCII_STRING_STOP).zero?
348
+ index += 1
349
+ end
350
+ index
351
+ end
352
+
353
+ def consume_ascii_string_prefix(start, finish)
354
+ length = finish - start
355
+ limit = @limits.max_string_code_units
356
+ if limit && length > limit
357
+ input.consume_ascii_bytes(limit + 1)
358
+ raise_string_limit
359
+ end
360
+
361
+ input.consume_ascii_bytes(length)
362
+ input.source.byteslice(start, length).bytes
363
+ end
364
+
365
+ def scan_escape(units)
366
+ start = input.mark
367
+ input.consume_byte
368
+ raise_error("invalid_escape", "backslash at end of string", start, LexError) if input.eof?
369
+ codepoint, = input.peek_codepoint
370
+ if input.line_terminator?(codepoint)
371
+ input.consume_codepoint
372
+ return
373
+ end
374
+ if SIMPLE_ESCAPES.key?(codepoint)
375
+ input.consume_byte
376
+ units << SIMPLE_ESCAPES.fetch(codepoint)
377
+ elsif codepoint == 0x30
378
+ input.consume_byte
379
+ if decimal_digit_byte?(input.peek_byte)
380
+ raise_error("invalid_escape", "\\0 cannot be followed by a decimal digit", start, LexError)
381
+ end
382
+ units << 0
383
+ elsif codepoint.between?(0x31, 0x39)
384
+ raise_error("invalid_escape", "decimal and octal escapes are not allowed", start, LexError)
385
+ elsif codepoint == 0x78
386
+ units << read_hex_escape(2, start, "x")
387
+ elsif codepoint == 0x75
388
+ units << read_hex_escape(4, start, "u")
389
+ else
390
+ input.consume_codepoint
391
+ append_codepoint(units, codepoint)
392
+ end
393
+ enforce_string_limit(units)
394
+ end
395
+
396
+ def read_hex_escape(length, start, marker)
397
+ input.consume_byte
398
+ value = 0
399
+ length.times do
400
+ byte = input.peek_byte
401
+ unless hex_digit_byte?(byte)
402
+ raise_error("invalid_escape", "\\#{marker} escape requires #{length} hex digits", start, LexError)
403
+ end
404
+ input.consume_byte
405
+ value = (value << 4) | hex_digit_value(byte)
406
+ end
407
+ value
408
+ end
409
+
410
+ def append_codepoint(units, codepoint)
411
+ if codepoint <= 0xffff
412
+ units << codepoint
413
+ else
414
+ codepoint -= 0x10000
415
+ units << (0xd800 + (codepoint >> 10))
416
+ units << (0xdc00 + (codepoint & 0x3ff))
417
+ end
418
+ end
419
+
420
+ def enforce_string_limit(units)
421
+ return unless @limits.max_string_code_units && units.length > @limits.max_string_code_units
422
+ raise_string_limit
423
+ end
424
+
425
+ def raise_string_limit
426
+ raise_error("maximum_string_size_exceeded", "string exceeds max_string_code_units", input.mark, LimitError)
427
+ end
428
+
429
+ def scan_number
430
+ start = input.mark
431
+ if [0x2b, 0x2d].include?(input.peek_byte)
432
+ consume_number_byte(start)
433
+ raw = scan_signed_number_body(start)
434
+ return raw
435
+ end
436
+ if input.peek_byte == 0x2e
437
+ consume_number_byte(start)
438
+ require_decimal_digits(start)
439
+ consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
440
+ scan_decimal_tail(start)
441
+ check_number_boundary(start)
442
+ enforce_number_limit(start)
443
+ return input.byteslice(start.byte_offset)
444
+ end
445
+ scan_unsigned_number(start)
446
+ end
447
+
448
+ def scan_signed_number_body(start)
449
+ if match_ascii_word?("Infinity") || match_ascii_word?("NaN")
450
+ consume_number_bytes(input.peek_byte == 0x49 ? 8 : 3, start)
451
+ check_number_boundary(start)
452
+ enforce_number_limit(start)
453
+ return input.byteslice(start.byte_offset)
454
+ end
455
+ if input.peek_byte == 0x30 && [0x78, 0x58].include?(input.peek_byte(1))
456
+ scan_hex_body(start)
457
+ elsif input.peek_byte == 0x2e
458
+ consume_number_byte(start)
459
+ require_decimal_digits(start)
460
+ consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
461
+ scan_decimal_tail(start)
462
+ check_number_boundary(start)
463
+ enforce_number_limit(start)
464
+ input.byteslice(start.byte_offset)
465
+ elsif input.peek_byte == 0x30
466
+ consume_number_byte(start)
467
+ raise_error("leading_zero", "leading zero is not allowed", start, LexError) if decimal_digit_byte?(input.peek_byte)
468
+ scan_decimal_tail(start)
469
+ raw = input.byteslice(start.byte_offset)
470
+ check_number_boundary(start)
471
+ enforce_number_limit(start)
472
+ raw
473
+ elsif decimal_digit_byte?(input.peek_byte)
474
+ scan_decimal_body(start)
475
+ else
476
+ raise_error("invalid_number", "sign must be followed by a JSON5 number", start, LexError)
477
+ end
478
+ end
479
+
480
+ def scan_unsigned_number(start)
481
+ if input.peek_byte == 0x30 && [0x78, 0x58].include?(input.peek_byte(1))
482
+ scan_hex_body(start)
483
+ elsif input.peek_byte == 0x30
484
+ consume_number_byte(start)
485
+ raise_error("leading_zero", "leading zero is not allowed", start, LexError) if decimal_digit_byte?(input.peek_byte)
486
+ scan_decimal_tail(start)
487
+ raw = input.byteslice(start.byte_offset)
488
+ check_number_boundary(start)
489
+ enforce_number_limit(start)
490
+ raw
491
+ elsif input.peek_byte&.between?(0x31, 0x39)
492
+ scan_decimal_body(start)
493
+ else
494
+ raise_error("invalid_number", "invalid number", start, LexError)
495
+ end
496
+ end
497
+
498
+ def scan_decimal_body(start)
499
+ consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
500
+ scan_decimal_tail(start)
501
+ raw = input.byteslice(start.byte_offset)
502
+ check_number_boundary(start)
503
+ enforce_number_limit(start)
504
+ raw
505
+ end
506
+
507
+ def scan_decimal_tail(start)
508
+ if input.peek_byte == 0x2e
509
+ consume_number_byte(start)
510
+ consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
511
+ end
512
+ if [0x65, 0x45].include?(input.peek_byte)
513
+ consume_number_byte(start)
514
+ consume_number_byte(start) if [0x2b, 0x2d].include?(input.peek_byte)
515
+ require_decimal_digits(input.mark)
516
+ consume_number_byte(start) while decimal_digit_byte?(input.peek_byte)
517
+ end
518
+ end
519
+
520
+ def scan_hex_body(start)
521
+ consume_number_byte(start)
522
+ consume_number_byte(start)
523
+ unless hex_digit_byte?(input.peek_byte)
524
+ raise_error("invalid_number", "hexadecimal number requires at least one digit", start, LexError)
525
+ end
526
+ consume_number_byte(start) while hex_digit_byte?(input.peek_byte)
527
+ raw = input.byteslice(start.byte_offset)
528
+ check_number_boundary(start)
529
+ enforce_number_limit(start)
530
+ raw
531
+ end
532
+
533
+ def require_decimal_digits(start)
534
+ raise_error("invalid_number", "number requires a decimal digit", start, LexError) unless decimal_digit_byte?(input.peek_byte)
535
+ end
536
+
537
+ def check_number_boundary(start)
538
+ return if input.eof?
539
+ codepoint, = input.peek_codepoint
540
+ return unless identifier_start_codepoint?(codepoint) || decimal_digit_byte?(codepoint) || input.peek_byte == 0x5c
541
+ raise_error("invalid_number", "number is followed by an identifier character", start, LexError)
542
+ end
543
+
544
+ def enforce_number_limit(start)
545
+ return unless @limits.max_number_bytes && input.index - start.byte_offset > @limits.max_number_bytes
546
+ raise_error("maximum_number_size_exceeded", "number exceeds max_number_bytes", start, LimitError)
547
+ end
548
+
549
+ def consume_number_byte(start)
550
+ input.consume_byte
551
+ enforce_number_limit(start)
552
+ end
553
+
554
+ def consume_number_bytes(count, start)
555
+ count.times { consume_number_byte(start) }
556
+ end
557
+
558
+ def match_ascii_word?(word)
559
+ input.source.byteslice(input.index, word.bytesize) == word
560
+ end
561
+
562
+ def number_start?(byte)
563
+ byte == 0x2b || byte == 0x2d || byte == 0x2e || decimal_digit_byte?(byte)
564
+ end
565
+
566
+ def decimal_digit_byte?(byte)
567
+ !(ascii_flags(byte) & ASCII_DIGIT).zero?
568
+ end
569
+
570
+ def hex_digit_byte?(byte)
571
+ !(ascii_flags(byte) & ASCII_HEX_DIGIT).zero?
572
+ end
573
+
574
+ def hex_digit_value(byte)
575
+ return byte - 0x30 if byte <= 0x39
576
+ return byte - 0x37 if byte <= 0x46
577
+
578
+ byte - 0x57
579
+ end
580
+
581
+ def ascii_flags(byte)
582
+ byte && byte < ASCII.length ? ASCII.fetch(byte) : 0
583
+ end
584
+
585
+ def raise_error(code, message, position, klass)
586
+ offset = position.byte_offset
587
+ raise klass.new(
588
+ message,
589
+ code: code,
590
+ filename: @filename,
591
+ byte_offset: offset,
592
+ end_byte_offset: [[input.index, offset + 1].max, input.source.bytesize].min,
593
+ line: position.line,
594
+ column: position.column,
595
+ excerpt: input.excerpt_at(offset)
596
+ )
597
+ end
598
+ end
599
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module JSON5
4
+ class Limits
5
+ ATTRIBUTES = %i[
6
+ max_input_bytes
7
+ max_depth
8
+ max_string_code_units
9
+ max_number_bytes
10
+ max_comment_bytes
11
+ max_container_entries
12
+ ].freeze
13
+
14
+ attr_reader(*ATTRIBUTES)
15
+
16
+ def initialize(max_input_bytes: 64 * 1024 * 1024, max_depth: 512,
17
+ max_string_code_units: 16 * 1024 * 1024,
18
+ max_number_bytes: 1 * 1024 * 1024,
19
+ max_comment_bytes: 16 * 1024 * 1024,
20
+ max_container_entries: 10_000_000)
21
+ values = {
22
+ max_input_bytes: max_input_bytes,
23
+ max_depth: max_depth,
24
+ max_string_code_units: max_string_code_units,
25
+ max_number_bytes: max_number_bytes,
26
+ max_comment_bytes: max_comment_bytes,
27
+ max_container_entries: max_container_entries
28
+ }
29
+ values.each do |name, value|
30
+ next if value.nil?
31
+ unless value.is_a?(Integer) && value >= 0
32
+ raise ArgumentError, "#{name} must be non-negative"
33
+ end
34
+ end
35
+ values.each { |name, value| instance_variable_set("@#{name}", value) }
36
+ freeze
37
+ end
38
+
39
+ def self.default
40
+ @default ||= new
41
+ end
42
+
43
+ def self.unbounded
44
+ @unbounded ||= new(**ATTRIBUTES.to_h { |name| [name, nil] })
45
+ end
46
+ end
47
+ end