json_mend 0.3.5 → 0.3.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7dcc378c148dc0514753b966693e0afe286bf145c283d069f01c0b9c74f59a75
4
- data.tar.gz: 37fa52116d57bc80b168c20feba1fd7b33edab73d1f157759bc71f4a3362802b
3
+ metadata.gz: 0db6ff9aaecf22de21a04b136e235fa04d12d43f3c1db614f96eec2c881ce7b9
4
+ data.tar.gz: 00aa675a9392fdcd9d528f788144ad7a3064c689e9a6a698132c15ff0a9b4130
5
5
  SHA512:
6
- metadata.gz: c94ff3c9d0c2e3602b6a125e74fdc72896709b311221eeceeecd3fbf96b0aef8dc82edfed136512443b5ccc403d512d7beb3476c59732f1a04280a71bbf7de5b
7
- data.tar.gz: f3603aa1d247686329ec0effe5d1819f4feec47cb43adbff520ed7695831cc30649fa99b4e21a51d4e460754af3d825480335cea10ffbb79feea773fabfa1cd9
6
+ metadata.gz: 22e7132bcfe8f2109be0684afc22bcd21ac8d358dfb0f3b04ad41b14bba5853d82a9d6f1a54d996f8c4ecd9ab0fded3675c147b98fa218513336d7f1856c2a6b
7
+ data.tar.gz: f347eb625959326c68c2ffb283ba0d7a170a981927540d16c1b9aa526cde70fb1755cd40e8587af308ed17c83adccddca4a0ea49f986c8536419cfaf99af88bb
data/.rubocop.yml CHANGED
@@ -1,9 +1,18 @@
1
+ inherit_mode:
2
+ merge:
3
+ - Exclude
4
+
1
5
  plugins:
2
6
  - rubocop-performance
3
7
  - rubocop-rspec
8
+ - rubocop-rake
4
9
 
5
10
  AllCops:
6
11
  NewCops: enable
12
+ ParserEngine: parser_prism
13
+ CacheRootDirectory: .rubocop_cache
14
+ MaxFilesInCache: 8000
15
+ UseCache: false
7
16
  SuggestExtensions: false
8
17
 
9
18
  Metrics/AbcSize:
data/.tool-versions CHANGED
@@ -1 +1 @@
1
- ruby 4.0.2
1
+ ruby 4.0.6
@@ -42,6 +42,7 @@ module JsonMend
42
42
  INVALID_NUMBER_TRAILERS = ['-', 'e', 'E', ','].freeze
43
43
 
44
44
  # Pre-compile regexes for performance
45
+ STRING_START_REGEX = /[\p{L}$_]/
45
46
  NUMBER_REGEX = /[#{Regexp.escape(NUMBER_CHARS.to_a.join)}]+/
46
47
  NUMBER_NO_COMMA_REGEX = /[#{Regexp.escape(NUMBER_CHARS.dup.tap { |s| s.delete(',') }.to_a.join)}]+/
47
48
  INVALID_NUMBER_TRAILERS_REGEX = /[#{Regexp.union(*INVALID_NUMBER_TRAILERS)}]+\z/
@@ -53,6 +54,7 @@ module JsonMend
53
54
  def initialize(json_string)
54
55
  @scanner = StringScanner.new(json_string)
55
56
  @context = []
57
+ @current_context = nil
56
58
  @depth = 0
57
59
  end
58
60
 
@@ -178,7 +180,7 @@ module JsonMend
178
180
  with_depth_check do
179
181
  object = {}
180
182
 
181
- @context.push(:object)
183
+ push_context(:object)
182
184
 
183
185
  loop do
184
186
  skip_whitespaces
@@ -222,7 +224,7 @@ module JsonMend
222
224
  object[key] = value
223
225
  end
224
226
 
225
- @context.pop
227
+ pop_context
226
228
 
227
229
  object
228
230
  end
@@ -299,7 +301,7 @@ module JsonMend
299
301
  end
300
302
 
301
303
  # If no merge happened, proceed with standard key parsing.
302
- @context.push(:object_key)
304
+ push_context(:object_key)
303
305
  is_bracketed = false
304
306
 
305
307
  if char == '['
@@ -310,7 +312,7 @@ module JsonMend
310
312
  else
311
313
  key = parse_string.to_s
312
314
  end
313
- @context.pop
315
+ pop_context
314
316
 
315
317
  # If the key is empty, consume any stray characters to prevent infinite loops.
316
318
  @scanner.getch if key.empty? && !@scanner.check(/[:{\[}\]]/) && !@scanner.eos?
@@ -320,18 +322,18 @@ module JsonMend
320
322
 
321
323
  # Parses the value part of a key-value pair.
322
324
  def parse_object_value(colon_found: true)
323
- @context.push(:object_value)
325
+ push_context(:object_value)
324
326
  skip_whitespaces
325
327
 
326
328
  # Handle cases where the value is missing (e.g. "key": } or "key": ,)
327
329
  if @scanner.eos? || @scanner.check(/[,}]/)
328
- @context.pop
330
+ pop_context
329
331
  return colon_found ? '' : :inferred_true
330
332
  end
331
333
 
332
334
  # Delegate to the main JSON value parser.
333
335
  value = parse_json
334
- @context.pop
336
+ pop_context
335
337
 
336
338
  # If parse_json returned JSON_STOP_TOKEN (nothing found due to garbage->terminator),
337
339
  # treat it as empty string for object values to be safe.
@@ -342,7 +344,7 @@ module JsonMend
342
344
  def try_to_merge_dangling_array(object)
343
345
  return false unless peek_char == '['
344
346
 
345
- prev_key = object.keys.last
347
+ prev_key = object.keys[-1]
346
348
  return false unless prev_key && object[prev_key].is_a?(Array)
347
349
 
348
350
  @scanner.getch # Consume '['
@@ -365,7 +367,7 @@ module JsonMend
365
367
  def parse_array
366
368
  with_depth_check do
367
369
  arr = []
368
- @context.push(:array)
370
+ push_context(:array)
369
371
  char = peek_char
370
372
  # Stop when you find the closing bracket or an invalid character like '}'
371
373
  while !@scanner.eos? && !TERMINATORS_ARRAY.include?(char)
@@ -409,7 +411,7 @@ module JsonMend
409
411
  unless @scanner.scan(']')
410
412
  @scanner.scan('}') # Consume } if it was the closer
411
413
  end
412
- @context.pop
414
+ pop_context
413
415
 
414
416
  arr
415
417
  end
@@ -447,7 +449,7 @@ module JsonMend
447
449
 
448
450
  doubled_quotes = rest.first
449
451
 
450
- string_parts = []
452
+ string_parts = +''
451
453
 
452
454
  # Here things get a bit hairy because a string missing the final quote can also be a key or a value in an object
453
455
  # In that case we need to use the ":|,|}" characters as terminators of the string
@@ -576,12 +578,12 @@ module JsonMend
576
578
  end
577
579
 
578
580
  break if context_termination_reached?(
579
- char:,
580
- missing_quotes:
581
+ char,
582
+ missing_quotes
581
583
  )
582
584
 
583
585
  if current_context?(:object_value) && TERMINATORS_OBJECT_VALUE.include?(char) &&
584
- (string_parts.empty? || string_parts.last != rstring_delimiter)
586
+ (string_parts.empty? || string_parts[-1] != rstring_delimiter)
585
587
 
586
588
  is_break = check_rstring_delimiter_missing(
587
589
  string_parts:,
@@ -592,7 +594,7 @@ module JsonMend
592
594
  break if is_break
593
595
  end
594
596
 
595
- if char == ']' && context_contain?(:array) && string_parts.last != rstring_delimiter
597
+ if char == ']' && context_contain?(:array) && string_parts[-1] != rstring_delimiter
596
598
  i = skip_to_character(rstring_delimiter)
597
599
  # No delimiter found
598
600
  break unless peek_char(i)
@@ -610,7 +612,7 @@ module JsonMend
610
612
  @scanner.getch # Consume the character
611
613
  char = peek_char
612
614
 
613
- if !@scanner.eos? && string_parts.last == '\\'
615
+ if !@scanner.eos? && string_parts[-1] == '\\'
614
616
  # This is a special case, if people use real strings this might happen
615
617
  is_next, string_parts, char = parse_escape_sequence(
616
618
  string_parts:,
@@ -629,7 +631,7 @@ module JsonMend
629
631
  break if is_break
630
632
  end
631
633
 
632
- if char == rstring_delimiter && string_parts.last != '\\'
634
+ if char == rstring_delimiter && string_parts[-1] != '\\'
633
635
  if check_doubled_quotes(doubled_quotes, rstring_delimiter)
634
636
  # Consumed in helper
635
637
  elsif check_missing_quotes_in_object_value(missing_quotes, lstring_delimiter, rstring_delimiter)
@@ -934,18 +936,18 @@ module JsonMend
934
936
  char:,
935
937
  rstring_delimiter:
936
938
  )
937
- if !@scanner.eos? && string_parts.last == '\\'
939
+ if !@scanner.eos? && string_parts[-1] == '\\'
938
940
  # This is a special case, if people use real strings this might happen
939
941
  if char == rstring_delimiter || ESCAPE_START_CHARS.include?(char)
940
- string_parts.pop
942
+ string_parts.chop!
941
943
  string_parts << ESCAPE_MAPPING.fetch(char, char)
942
944
 
943
945
  @scanner.getch # Consume the character
944
946
  char = peek_char
945
- while !@scanner.eos? && string_parts.last == '\\' && (char == rstring_delimiter || char == '\\')
947
+ while !@scanner.eos? && string_parts[-1] == '\\' && (char == rstring_delimiter || char == '\\')
946
948
  # this is a bit of a special case, if I don't do this it will close the loop or create a train of \\
947
949
  # I don't love it though
948
- string_parts.pop
950
+ string_parts.chop!
949
951
  string_parts << char
950
952
  @scanner.getch # Consume the character
951
953
  char = peek_char
@@ -959,7 +961,7 @@ module JsonMend
959
961
 
960
962
  # Validate valid hex digits
961
963
  if (hex_str = @scanner.scan(hex_regex))
962
- string_parts.pop
964
+ string_parts.chop!
963
965
  hex_val = hex_str.to_i(16)
964
966
 
965
967
  if char == 'u' && hex_val.between?(0xD800, 0xDBFF)
@@ -1000,7 +1002,7 @@ module JsonMend
1000
1002
  @scanner.pos = entry_pos
1001
1003
  end
1002
1004
  elsif STRING_DELIMITERS.include?(char) && char != rstring_delimiter
1003
- string_parts.pop
1005
+ string_parts.chop!
1004
1006
  string_parts << char
1005
1007
  @scanner.getch # Consume the character
1006
1008
  char = peek_char
@@ -1045,19 +1047,19 @@ module JsonMend
1045
1047
  # we need to update the index only if we had a closing quote
1046
1048
  if char == rstring_delimiter
1047
1049
  @scanner.getch
1048
- elsif missing_quotes && current_context?(:object_key) && string_parts.last == ','
1049
- string_parts.pop
1050
+ elsif missing_quotes && current_context?(:object_key) && string_parts[-1] == ','
1051
+ string_parts.chop!
1050
1052
  end
1051
1053
 
1052
- final_str = string_parts.join
1054
+ final_str = string_parts
1053
1055
  final_str = final_str.rstrip if missing_quotes || final_str.end_with?("\n")
1054
1056
 
1055
1057
  final_str
1056
1058
  end
1057
1059
 
1058
1060
  def context_termination_reached?(
1059
- char:,
1060
- missing_quotes:
1061
+ char,
1062
+ missing_quotes
1061
1063
  )
1062
1064
  return false unless missing_quotes
1063
1065
 
@@ -1302,12 +1304,13 @@ module JsonMend
1302
1304
  def peek_char(offset = 0)
1303
1305
  # Handle the common 0-offset case
1304
1306
  if offset.zero?
1305
- # peek(1) returns the next BYTE, not character
1306
- byte_str = @scanner.peek(1)
1307
- return nil if byte_str.empty?
1307
+ pos = @scanner.pos
1308
+ str = @scanner.string
1309
+ return nil if pos >= str.bytesize
1308
1310
 
1311
+ byte = str.getbyte(pos)
1309
1312
  # Fast path: If it's a standard ASCII char (0-127), return it directly.
1310
- return byte_str if byte_str.getbyte(0) < 128
1313
+ return byte.chr if byte < 128
1311
1314
 
1312
1315
  # Slow path: If it's a multibyte char (e.g. “), use regex to match the full character.
1313
1316
  return @scanner.check(/./m)
@@ -1324,8 +1327,18 @@ module JsonMend
1324
1327
  res
1325
1328
  end
1326
1329
 
1330
+ def push_context(value)
1331
+ @context.push(value)
1332
+ @current_context = value
1333
+ end
1334
+
1335
+ def pop_context
1336
+ @context.pop
1337
+ @current_context = @context.last
1338
+ end
1339
+
1327
1340
  def current_context?(value)
1328
- @context&.last == value
1341
+ @current_context == value
1329
1342
  end
1330
1343
 
1331
1344
  def context_contain?(value)
@@ -1334,7 +1347,7 @@ module JsonMend
1334
1347
 
1335
1348
  # Checks if the character signifies the start of a string or literal
1336
1349
  def string_start?(char)
1337
- STRING_DELIMITERS.include?(char) || char&.match?(/[\p{L}$_]/)
1350
+ STRING_DELIMITERS.include?(char) || (char && STRING_START_REGEX.match?(char))
1338
1351
  end
1339
1352
 
1340
1353
  # Checks if the character signifies the start of a number
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module JsonMend
4
- VERSION = '0.3.5'
4
+ VERSION = '0.3.6'
5
5
  end
data/lib/json_mend.rb CHANGED
@@ -15,21 +15,22 @@ module JsonMend
15
15
  # @return [Object, String] The repaired JSON object or string.
16
16
  def repair(json_string, return_objects: false)
17
17
  # First, attempt to parse the string with the standard library.
18
- repaired_json = begin
18
+ begin
19
19
  parsed = JSON.parse(
20
20
  json_string,
21
21
  allow_trailing_comma: true,
22
22
  allow_control_characters: true
23
23
  )
24
24
 
25
- # Verify the native parser didn't produce invalid UTF-8 (like unpaired surrogates)
26
- # by ensuring it can safely dump its own output.
27
- JSON.generate(parsed)
25
+ # If the user wants objects, return them immediately
26
+ return parsed if return_objects
28
27
 
29
- parsed
28
+ # Otherwise, generate the string once. This acts as both the
29
+ # UTF-8 verification step AND the final string output.
30
+ return JSON.generate(parsed)
30
31
  rescue JSON::ParserError, JSON::GeneratorError
31
32
  parser = Parser.new(json_string)
32
- parser.parse
33
+ repaired_json = parser.parse
33
34
  end
34
35
 
35
36
  # Avoids returning `null` for empty results, returns the object directly
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json_mend
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.5
4
+ version: 0.3.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Oleksii Vasyliev
@@ -77,14 +77,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
77
77
  requirements:
78
78
  - - ">="
79
79
  - !ruby/object:Gem::Version
80
- version: 3.2.0
80
+ version: 3.3.0
81
81
  required_rubygems_version: !ruby/object:Gem::Requirement
82
82
  requirements:
83
83
  - - ">="
84
84
  - !ruby/object:Gem::Version
85
85
  version: '0'
86
86
  requirements: []
87
- rubygems_version: 4.0.6
87
+ rubygems_version: 4.0.16
88
88
  specification_version: 4
89
89
  summary: Repair broken JSON
90
90
  test_files: []