json-repair 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 83ede681caac8ebbc294902937c8af11ebc90fe066beed41e15013782fbd0b96
4
- data.tar.gz: da309d72a1c564f05dd405c02115985572c38c8b2c28aad85f778b94cc2a68e9
3
+ metadata.gz: b4892b62960d0c4d27976577c506585ea8df11bfbf14dc1a9b40952947ad1f20
4
+ data.tar.gz: fe26e3da464e5356bd33021e5b08a7a1f1ad938e125f0ef384777ff64b13f813
5
5
  SHA512:
6
- metadata.gz: da63df705106d20701700dd55712ed06676cdb4e6615f36d915cae12f3bdf7ac085ea919a553c77e8e0876663e5ad7d0067b46b00fddd3a7cca837eb38e54fc8
7
- data.tar.gz: d4471c07b5a3a39505ec890b8afc359ca12fa3042c953143675af2d88bd8a4f56fe6225f00e0d4665eaf66465862f2b273929a2bb7fd54a81f79f1abf39ecfa1
6
+ metadata.gz: 4cf46eae9e05e718c978e7a503578bd867bacce1b71565764ec88af8b762f498ceceada6ce37d73663e6bcff56b85013746c7a82e84eda6eb55ed57e3535fe0d
7
+ data.tar.gz: f2fc802dd1bfbd26bb04f34ca2902189d11537d4bbbebc131d14ef7672bcd9f2dd01bcc2157182ba6801eea0f92693883086197013add4f86f69e6fcf32e5a3b
data/CHANGELOG.md CHANGED
@@ -1,5 +1,14 @@
1
1
  # Changes
2
2
 
3
+ ### 2026-05-12 (0.6.0)
4
+
5
+ * `JSON.repair` accepts a `return_objects:` keyword argument. Pass
6
+ `return_objects: true` to receive the parsed Ruby value (Hash, Array,
7
+ or scalar) instead of a serialized JSON string. Default is `false`,
8
+ preserving the existing return-a-string contract. Mirrors Python's
9
+ `return_objects` option on
10
+ [`json_repair`](https://github.com/mangiucugna/json_repair).
11
+
3
12
  ### 2026-05-12 (0.5.0)
4
13
 
5
14
  * `JSON::JSONRepairError#position` returns the input index at which the
data/CLAUDE.md CHANGED
@@ -5,13 +5,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co
5
5
  ## Commands
6
6
 
7
7
  - `bin/setup` — install dependencies via Bundler.
8
- - `bundle exec rake` — default task; runs both RSpec and RuboCop.
8
+ - `bundle exec rake` — default task; runs RSpec, RuboCop, RBS validate, and Steep.
9
9
  - `bundle exec rspec` — run the test suite.
10
10
  - `bundle exec rspec spec/json_spec.rb:42` — run a single example by line number; nearly all behavioral specs live in `spec/json_spec.rb`.
11
11
  - `bundle exec rubocop` — lint. Project-specific exclusions in `.rubocop.yml` deliberately disable several `Metrics/*` cops for `lib/json/repairer.rb` and `lib/json/repair/string_utils.rb` because the parser is long by design — don't try to "fix" it by chopping methods up.
12
12
  - `bin/console` — IRB with the gem preloaded.
13
13
  - `bundle exec rake install` / `bundle exec rake release` — local install / publish to rubygems.org.
14
- - Type checking: `Steepfile` checks `lib/` against `sig/`. Run `bundle exec steep check` if/when steep is installed (not in `Gemfile` by default).
14
+ - Type checking: `Steepfile` checks `lib/` against `sig/`. `bundle exec steep check` (typecheck) and `bundle exec rbs validate` (sig syntax) both run in CI and as part of the default rake task. `steep` and `rbs` are dev dependencies in the `Gemfile`.
15
15
 
16
16
  Ruby `>= 3.0.0` is required (per gemspec). CI runs against Ruby 3.3.1.
17
17
 
data/README.md CHANGED
@@ -31,6 +31,13 @@ puts repaired_json # Outputs: {"name": "Alice", "age": 25}
31
31
 
32
32
  The `repair` method takes a string containing JSON data and returns a corrected version of this string, ensuring it is valid JSON.
33
33
 
34
+ Pass `return_objects: true` to get the parsed Ruby value (Hash, Array, or scalar) instead of a string:
35
+
36
+ ```ruby
37
+ JSON.repair('{a: 1, b: [2, 3,]}', return_objects: true)
38
+ # => {"a" => 1, "b" => [2, 3]}
39
+ ```
40
+
34
41
  ## Command line
35
42
 
36
43
  The gem ships a `json-repair` executable. It reads from stdin or a file and writes to stdout, `--output FILE`, or back over the input file with `--overwrite`.
data/Rakefile CHANGED
@@ -9,4 +9,14 @@ require 'rubocop/rake_task'
9
9
 
10
10
  RuboCop::RakeTask.new
11
11
 
12
- task default: %i[spec rubocop]
12
+ desc 'Validate RBS signatures in sig/'
13
+ task :rbs do
14
+ sh 'bundle exec rbs validate'
15
+ end
16
+
17
+ desc 'Run Steep type check against sig/'
18
+ task :steep do
19
+ sh 'bundle exec steep check'
20
+ end
21
+
22
+ task default: %i[spec rubocop rbs steep]
data/Steepfile CHANGED
@@ -3,4 +3,9 @@
3
3
  target :lib do
4
4
  signature 'sig'
5
5
  check 'lib'
6
+
7
+ library 'optparse'
8
+ library 'tempfile'
9
+ library 'fileutils'
10
+ library 'json'
6
11
  end
@@ -34,7 +34,8 @@ module JSON
34
34
 
35
35
  def run(argv)
36
36
  positional = catch(:halt) { parser.parse(argv) }
37
- return @halt if @halt
37
+ halt = @halt
38
+ return halt if halt
38
39
 
39
40
  input_path = positional.first
40
41
  return 1 unless validate(positional, input_path)
@@ -61,7 +62,7 @@ module JSON
61
62
  end
62
63
 
63
64
  def read_input(input_path)
64
- raw = input_path ? File.read(input_path) : @stdin.read
65
+ raw = (input_path ? File.read(input_path) : @stdin.read).to_s
65
66
  raw.force_encoding(Encoding::UTF_8)
66
67
  raise JSON::JSONRepairError, 'input is not valid UTF-8' unless raw.valid_encoding?
67
68
 
@@ -69,10 +70,10 @@ module JSON
69
70
  end
70
71
 
71
72
  def write_output(repaired, input_path)
72
- if @overwrite
73
+ if @overwrite && input_path
73
74
  replace_in_place(input_path, repaired)
74
- elsif @output_path
75
- File.write(@output_path, repaired)
75
+ elsif (output_path = @output_path)
76
+ File.write(output_path, repaired)
76
77
  else
77
78
  @stdout.write(repaired)
78
79
  @stdout.write("\n") unless repaired.end_with?("\n")
@@ -2,6 +2,6 @@
2
2
 
3
3
  module JSON
4
4
  module Repair
5
- VERSION = '0.5.0'
5
+ VERSION = '0.6.0'
6
6
  end
7
7
  end
data/lib/json/repair.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'json'
4
+
3
5
  require_relative 'repair/version'
4
6
  require_relative 'repairer'
5
7
 
@@ -13,7 +15,8 @@ module JSON
13
15
  end
14
16
  end
15
17
 
16
- def self.repair(json)
17
- Repairer.new(json).repair
18
+ def self.repair(json, return_objects: false)
19
+ repaired = Repairer.new(json).repair
20
+ return_objects ? JSON.parse(repaired) : repaired
18
21
  end
19
22
  end
data/lib/json/repairer.rb CHANGED
@@ -227,9 +227,19 @@ module JSON
227
227
  if processed_colon || truncated_text
228
228
  # repair missing object value
229
229
  @output << 'null'
230
+ # :nocov:
230
231
  else
232
+ # Unreachable through JSON.repair: if we got here, the colon-repair
233
+ # branch above ran, which required start_of_value? to be true. Every
234
+ # char that satisfies start_of_value? (see REGEX_START_OF_VALUE plus
235
+ # quote chars) is consumable by some parse_* method, so parse_value
236
+ # cannot return false in this state. Preserved for parity with the
237
+ # upstream JS parser; if a future change to REGEX_START_OF_VALUE or
238
+ # parse_unquoted_string invalidates that invariant, this branch
239
+ # becomes live and the :nocov: will hide it.
231
240
  throw_colon_expected
232
241
  end
242
+ # :nocov:
233
243
  end
234
244
  end
235
245
 
@@ -725,10 +735,10 @@ module JSON
725
735
  processed_value = parse_value
726
736
  end
727
737
 
728
- unless processed_value
729
- # repair: remove trailing comma
730
- @output = strip_last_occurrence(@output, ',')
731
- end
738
+ # repair: remove trailing comma
739
+ # (the `while processed_value` loop above only exits when processed_value
740
+ # is falsy, so the upstream JS `if (!processedValue)` guard is redundant)
741
+ @output = strip_last_occurrence(@output, ',')
732
742
 
733
743
  # repair: wrap the output inside array brackets
734
744
  @output = "[\n#{@output}\n]"
@@ -1,16 +1,47 @@
1
1
  module JSON
2
2
  module Repair
3
3
  class CLI
4
- # `::IO | ::StringIO` because `::StringIO` is not an `::IO` subclass; the
5
- # specs inject `::StringIO` instances and any other duck-typed stream
6
- # that implements `#read` / `#write` / `#puts` would work too.
4
+ # `::IO | ::StringIO` because `::StringIO` is not an `::IO` subclass
5
+ # and the specs inject `::StringIO` instances.
7
6
  type stream = ::IO | ::StringIO
8
7
 
8
+ # Marked `private_constant` in `lib/json/repair/cli.rb`. RBS has no
9
+ # `private_constant` syntax, so the declaration is unavoidably public
10
+ # in the signature; do not rely on it from outside this class.
11
+ OVERWRITE_DESC: ::String
12
+
13
+ @stdin: stream
14
+ @stdout: stream
15
+ @stderr: stream
16
+ @output_path: ::String?
17
+ @halt: ::Integer?
18
+ @overwrite: bool
19
+
9
20
  def self.call: (::Array[::String] argv, ?stdin: stream, ?stdout: stream, ?stderr: stream) -> ::Integer
10
21
 
11
22
  def initialize: (?stdin: stream, ?stdout: stream, ?stderr: stream) -> void
12
23
 
13
24
  def call: (::Array[::String] argv) -> ::Integer
25
+
26
+ private
27
+
28
+ def run: (::Array[::String] argv) -> ::Integer
29
+
30
+ def validate: (::Array[::String] positional, ::String? input_path) -> bool
31
+
32
+ def validation_error: (::Array[::String] positional, ::String? input_path) -> ::String?
33
+
34
+ def read_input: (::String? input_path) -> ::String
35
+
36
+ def write_output: (::String repaired, ::String? input_path) -> void
37
+
38
+ def replace_in_place: (::String input_path, ::String repaired) -> void
39
+
40
+ def parser: () -> ::OptionParser
41
+
42
+ def define_options: (::OptionParser opts) -> void
43
+
44
+ def halt_with: (::String message) -> void
14
45
  end
15
46
  end
16
47
  end
data/sig/json/repair.rbs CHANGED
@@ -10,4 +10,6 @@ module JSON
10
10
  end
11
11
 
12
12
  def self.repair: (::String json) -> ::String
13
+ | (::String json, return_objects: false) -> ::String
14
+ | (::String json, return_objects: true) -> untyped
13
15
  end
@@ -1,6 +1,15 @@
1
1
  module JSON
2
2
  class Repairer
3
- @json: ::String
3
+ # `untyped` (not `::String`) because the parser idiomatically uses
4
+ # `@json[@index]` past EOF, relying on Ruby returning `nil` as a
5
+ # sentinel. Declaring `@json: ::String` makes steep infer `String?`
6
+ # from `String#[]` and rejects ~15 downstream call sites that pass
7
+ # the result to methods expecting `String`. Tightening would either
8
+ # require pervasive nil-extraction at every indexing site or a
9
+ # private accessor — both of which break parity with the upstream
10
+ # JS source (see CLAUDE.md and `.rubocop.yml` exceptions for
11
+ # `lib/json/repairer.rb`).
12
+ @json: untyped
4
13
 
5
14
  @index: Integer
6
15
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: json-repair
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aleksandr Zykov