smarter_json 0.6.0 → 0.7.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 +4 -4
- data/CHANGELOG.md +7 -2
- data/README.md +14 -1
- data/docs/basic_read_api.md +8 -7
- data/docs/options.md +9 -8
- data/ext/smarter_json/smarter_json.c +20 -18
- data/lib/smarter_json/parser.rb +13 -17
- data/lib/smarter_json/version.rb +1 -1
- data/lib/smarter_json/warning.rb +2 -2
- metadata +2 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 00a4ada7970ab645761573bd3b9704effda072064102ead760124716f0896289
|
|
4
|
+
data.tar.gz: a7232f3d8d06d3b7d770645b28a247774c83b8ac50603e4b7611006e852480d4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a4b4b8737f10ebf09408014419a9d5cf2203b706766ea5acc358ad595c5cc34104b3f658f2ca6cf6b130124c514d89d035dea2a0ea16e619ffaec7da16da4a23
|
|
7
|
+
data.tar.gz: a317f74e3399d6b95327fb10a7b65d2ca0f07062d9097303ab32aefb6721a46a78784addd0a937c0c8f433c4acb4e7758af3986fa741bd0d5b816141ee101fd4
|
data/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
|
|
2
2
|
# SmarterJSON Change Log
|
|
3
3
|
|
|
4
|
+
> 🚧 Getting ready for the 1.0.0 release - sorry for the interface changes - thank you for your patience! 🚧
|
|
5
|
+
|
|
6
|
+
## 0.7.0 (2026-06-02)
|
|
7
|
+
- **Breaking:** replaced the `warnings:` option (and its `[result, warnings]` tuple return) with an `on_warning:` callable. Pass `on_warning: ->(w) { ... }` to be handed each `SmarterJSON::Warning` as the parser applies a lenient fix; `process` / `process_file` now always return the bare value (nil / value / Array) on every path. Unlike the tuple, this also fires on the streaming block form. The default (no handler) records nothing and costs nothing.
|
|
8
|
+
|
|
4
9
|
## 0.6.0 (2026-06-02)
|
|
5
10
|
- Lenient comma handling: empty slots around / between commas are collapsed (`[1,,2]` → `[1,2]`, `[,1,]` → `[1]`, `{a:1,,b:2}` → `{a:1,b:2}`), on both the C and Ruby paths. No null is inserted for an empty slot.
|
|
6
11
|
- A key with a colon but no value reads as null: `{a:}` → `{"a"=>nil}` (both paths).
|
|
@@ -9,12 +14,12 @@
|
|
|
9
14
|
- Fixed a pure-Ruby bug where a `\u` escape whose next bytes split a multibyte character leaked `ArgumentError`; it now raises `SmarterJSON::ParseError`.
|
|
10
15
|
- Added a property/fuzz test suite that checks C/Ruby parity and round-tripping on generated, mutated, and random input.
|
|
11
16
|
|
|
12
|
-
## 0.5.2 (2026-06-01)
|
|
17
|
+
## 0.5.2 (2026-06-01) yanked
|
|
13
18
|
- `generate` now supports pretty-printing via the `indent:` option (spaces per nesting level; default `0` = compact). Empty objects/arrays stay inline; `indent:` combined with `format: :ndjson` raises `ArgumentError`.
|
|
14
19
|
- `generate` adds `sort_keys:` (emit object keys in sorted order), `ascii_only:` (escape non-ASCII as `\uXXXX`, astral chars as surrogate pairs), and `script_safe:` (escape `</` and U+2028/U+2029 for safe embedding in an HTML `<script>` tag).
|
|
15
20
|
- `generate` adds opt-in `coerce:` — when `true`, a value that isn't natively supported (e.g. `Time`, `Date`, app objects) is converted via its own `as_json` (result re-emitted) or `to_json` (spliced); strict-by-default still raises `GenerateError`.
|
|
16
21
|
|
|
17
|
-
## 0.5.1 (2026-06-01)
|
|
22
|
+
## 0.5.1 (2026-06-01) yanked
|
|
18
23
|
- Unified the error classes under a single `SmarterJSON::Error` base: `ParseError` and `EncodingError` now inherit from it, and `generate` raises a new `GenerateError`. `rescue SmarterJSON::Error` now catches everything the gem raises.
|
|
19
24
|
- Added a CI test matrix (Ruby 2.6–4.0 + head, on Ubuntu and macOS).
|
|
20
25
|
- Fixed the C extension build on Ruby 2.6 (declare `rb_hash_bulk_insert`, which 2.6 exports but does not declare in its headers); set the minimum Ruby to 2.6.
|
data/README.md
CHANGED
|
@@ -81,7 +81,20 @@ SmarterJSON.process_file("events.ndjson") { |event| EventJob.perform_async(event
|
|
|
81
81
|
| `bigdecimal_load` | `:auto` | `:auto` keeps high-precision decimals as `BigDecimal`; `:float` forces `Float`; `:bigdecimal` forces `BigDecimal` |
|
|
82
82
|
| `acceleration` | `true` | `true` uses the C extension when compiled and loadable; `false` forces pure Ruby (identical results) |
|
|
83
83
|
| `encoding` | `"UTF-8"` | labels the input's encoding (no transcoding pass; see below) |
|
|
84
|
-
| `
|
|
84
|
+
| `on_warning` | `nil` | a callable invoked once per lenient fix applied (`:empty_slot`, `:empty_value`, `:duplicate_key`), passed a `SmarterJSON::Warning`; the return value is never changed. See below. |
|
|
85
|
+
|
|
86
|
+
### Warnings (`on_warning`)
|
|
87
|
+
|
|
88
|
+
When the parser quietly fixes something lenient — collapses an empty comma slot, reads a key with no value as `null`, drops a duplicate key — it can tell you, without changing what `process` returns. Pass a callable as `on_warning:`; it is invoked once per fix with a `SmarterJSON::Warning` (`type`, `message`, `line`, `col`). It fires on every path, including the streaming block form. With no handler (the default) nothing is recorded and there is zero overhead.
|
|
89
|
+
|
|
90
|
+
```ruby
|
|
91
|
+
# Collect them all:
|
|
92
|
+
warns = []
|
|
93
|
+
data = SmarterJSON.process(input, on_warning: ->(w) { warns << w })
|
|
94
|
+
|
|
95
|
+
# Or route them — log, count, raise:
|
|
96
|
+
SmarterJSON.process(input, on_warning: ->(w) { Rails.logger.warn(w) })
|
|
97
|
+
```
|
|
85
98
|
|
|
86
99
|
## Performance
|
|
87
100
|
|
data/docs/basic_read_api.md
CHANGED
|
@@ -67,18 +67,19 @@ The streaming path reads the input as newline-delimited documents (NDJSON / JSON
|
|
|
67
67
|
|
|
68
68
|
By default (`acceleration: true`) the C extension is used when it is compiled and loadable (`SmarterJSON::HAS_ACCELERATION` is then `true`); otherwise the pure-Ruby parser runs and produces identical results. Pass `acceleration: false` to force the pure-Ruby path. See [Configuration Options](./options.md).
|
|
69
69
|
|
|
70
|
-
## Seeing what was fixed: `
|
|
70
|
+
## Seeing what was fixed: `on_warning:`
|
|
71
71
|
|
|
72
|
-
`process` and `process_file` are lenient — they salvage your data rather than reject a whole document over a stray comma. Pass
|
|
72
|
+
`process` and `process_file` are lenient — they salvage your data rather than reject a whole document over a stray comma. Pass an `on_warning:` callable to also get a record of what was adjusted, so the leniency is transparent instead of silent. It is invoked once per fix and never changes the return value:
|
|
73
73
|
|
|
74
74
|
```ruby
|
|
75
|
-
|
|
76
|
-
result
|
|
77
|
-
|
|
78
|
-
|
|
75
|
+
warns = []
|
|
76
|
+
result = SmarterJSON.process("[1,,2]", on_warning: ->(w) { warns << w })
|
|
77
|
+
result # => [1, 2]
|
|
78
|
+
warns.map(&:type) # => [:empty_slot]
|
|
79
|
+
warns.first.to_s # => "extra comma, collapsed an empty slot at line 1, col 4"
|
|
79
80
|
```
|
|
80
81
|
|
|
81
|
-
Each warning is a `SmarterJSON::Warning` with `type`, `message`, `line`, and `col`. The types are `:empty_slot` (a collapsed empty comma slot), `:empty_value` (a key with no value, read as `null`), and `:duplicate_key` (a repeated key that was dropped). Clean input
|
|
82
|
+
Each warning is a `SmarterJSON::Warning` with `type`, `message`, `line`, and `col`. The types are `:empty_slot` (a collapsed empty comma slot), `:empty_value` (a key with no value, read as `null`), and `:duplicate_key` (a repeated key that was dropped). Clean input never invokes the handler. It fires on every path — including the streaming block form — and works the same on the C and pure-Ruby paths. See [Configuration Options](./options.md).
|
|
82
83
|
|
|
83
84
|
---------------
|
|
84
85
|
|
data/docs/options.md
CHANGED
|
@@ -22,27 +22,28 @@ These options are passed to [`SmarterJSON.process`](./basic_read_api.md) and `Sm
|
|
|
22
22
|
| `:bigdecimal_load`| `:auto` | `:auto` keeps high-precision decimals as `BigDecimal` (matches Oj); `:float` forces every number to `Float`; `:bigdecimal` forces every decimal to `BigDecimal`. |
|
|
23
23
|
| `:acceleration` | `true` | Use the C extension when it is compiled and loadable; `false` forces the pure-Ruby parser. Both produce identical results. |
|
|
24
24
|
| `:encoding` | `nil` | Labels the input's encoding (e.g. `"UTF-8"`). It does **not** trigger a transcoding pass — see below. |
|
|
25
|
-
| `:
|
|
25
|
+
| `:on_warning` | `nil` | A callable invoked once per lenient fix applied, passed a `SmarterJSON::Warning`. Never changes the return value. See below. |
|
|
26
26
|
|
|
27
27
|
```ruby
|
|
28
28
|
SmarterJSON.process('{"a": 1}', symbolize_keys: true) # => {:a=>1}
|
|
29
29
|
SmarterJSON.process('{"a":1,"a":2}', duplicate_key: :raise) # raises SmarterJSON::ParseError
|
|
30
30
|
SmarterJSON.process(big_decimal_json, bigdecimal_load: :float) # every number as Float (fastest)
|
|
31
|
-
SmarterJSON.process("[1,,2]",
|
|
31
|
+
SmarterJSON.process("[1,,2]", on_warning: ->(w) { puts w }) # => [1, 2], and prints the warning
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
### A note on `:
|
|
34
|
+
### A note on `:on_warning`
|
|
35
35
|
|
|
36
|
-
`smarter_json` is lenient by design — it salvages your data instead of rejecting the whole document over a stray comma. `
|
|
36
|
+
`smarter_json` is lenient by design — it salvages your data instead of rejecting the whole document over a stray comma. `on_warning:` keeps that, but also hands you a record of what it had to fix, so leniency is transparent rather than silent. It takes a callable that the parser invokes once per fix, passing a `SmarterJSON::Warning` (with `type` (a Symbol), `message`, `line`, and `col`). It never changes the return value — `process` still hands back the bare value — and it fires on every path, including the streaming block form. With no handler (the default), nothing is recorded and there is zero overhead.
|
|
37
37
|
|
|
38
38
|
```ruby
|
|
39
|
-
|
|
39
|
+
warns = []
|
|
40
|
+
result = SmarterJSON.process("[1,,2]", on_warning: ->(w) { warns << w })
|
|
40
41
|
result # => [1, 2]
|
|
41
|
-
|
|
42
|
-
|
|
42
|
+
warns.map(&:type) # => [:empty_slot]
|
|
43
|
+
warns.first.to_s # => "extra comma, collapsed an empty slot at line 1, col 4"
|
|
43
44
|
```
|
|
44
45
|
|
|
45
|
-
The warning types are `:empty_slot` (a collapsed empty comma slot, e.g. `[1,,2]`), `:empty_value` (a key with no value, read as `null`, e.g. `{a:}`), and `:duplicate_key` (a repeated key that was dropped). Clean input
|
|
46
|
+
The warning types are `:empty_slot` (a collapsed empty comma slot, e.g. `[1,,2]`), `:empty_value` (a key with no value, read as `null`, e.g. `{a:}`), and `:duplicate_key` (a repeated key that was dropped). Clean input never invokes the handler. Warnings work on both the C and pure-Ruby paths, so `acceleration:` doesn't change them.
|
|
46
47
|
|
|
47
48
|
### A note on `:encoding`
|
|
48
49
|
|
|
@@ -34,6 +34,7 @@ static VALUE cParseError;
|
|
|
34
34
|
static VALUE cEncodingError;
|
|
35
35
|
static VALUE cWarning;
|
|
36
36
|
static ID fj_new_id;
|
|
37
|
+
static ID fj_call_id; /* cached :call (invoking the on_warning handler) */
|
|
37
38
|
static VALUE fj_sym_empty_slot;
|
|
38
39
|
static VALUE fj_sym_empty_value;
|
|
39
40
|
static VALUE fj_sym_duplicate_key;
|
|
@@ -60,8 +61,7 @@ typedef struct {
|
|
|
60
61
|
int dup_raise;
|
|
61
62
|
int bigdecimal_load; /* 0 = float, 1 = auto, 2 = bigdecimal */
|
|
62
63
|
fj_kc_slot *kcache; /* per-parse key cache (NULL when interning unavailable) */
|
|
63
|
-
|
|
64
|
-
VALUE warnings; /* rb_ary of SmarterJSON::Warning when collecting, else Qnil */
|
|
64
|
+
VALUE on_warning; /* on_warning: callable invoked per non-fatal lenient fix, else Qnil */
|
|
65
65
|
} fj_state;
|
|
66
66
|
|
|
67
67
|
/* Line/column at the current byte position, computed lazily (only when raising
|
|
@@ -81,14 +81,16 @@ static void fj_line_col(fj_state *st, long *line, long *col) {
|
|
|
81
81
|
*col = c;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
/*
|
|
84
|
+
/* Report a non-fatal lenient fix to the on_warning callable — a no-op (and builds no
|
|
85
|
+
* Warning) when no handler was given. The internal Qnil guard is the safety net; the
|
|
86
|
+
* call sites also guard so the line/col scan is skipped on the fast path. */
|
|
85
87
|
static void fj_warn(fj_state *st, VALUE type_sym, const char *msg) {
|
|
86
88
|
long line, col;
|
|
87
|
-
if (
|
|
89
|
+
if (st->on_warning == Qnil) return;
|
|
88
90
|
fj_line_col(st, &line, &col);
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
91
|
+
rb_funcall(st->on_warning, fj_call_id, 1,
|
|
92
|
+
rb_funcall(cWarning, fj_new_id, 4, type_sym,
|
|
93
|
+
rb_utf8_str_new_cstr(msg), LONG2NUM(line), LONG2NUM(col)));
|
|
92
94
|
}
|
|
93
95
|
|
|
94
96
|
/* 1-based column of the current byte position (bytes since the last line start).
|
|
@@ -1161,9 +1163,9 @@ static VALUE fj_build_object(fj_state *st, const VALUE *pairs, long count) {
|
|
|
1161
1163
|
long entries = count / 2, i;
|
|
1162
1164
|
VALUE hash = rb_hash_new_capa(entries);
|
|
1163
1165
|
|
|
1164
|
-
/* Fast path: bulk insert. Skipped when
|
|
1165
|
-
* per-member loop below to report each dropped duplicate key. */
|
|
1166
|
-
if (!st->symbolize_keys && !st->dup_first_wins &&
|
|
1166
|
+
/* Fast path: bulk insert. Skipped when an on_warning handler is present, which needs
|
|
1167
|
+
* the per-member loop below to report each dropped duplicate key. */
|
|
1168
|
+
if (!st->symbolize_keys && !st->dup_first_wins && st->on_warning == Qnil) {
|
|
1167
1169
|
rb_hash_bulk_insert(count, pairs, hash);
|
|
1168
1170
|
if (st->dup_raise && fj_hash_len(hash) < entries) {
|
|
1169
1171
|
VALUE seen = rb_hash_new_capa(entries);
|
|
@@ -1178,7 +1180,7 @@ static VALUE fj_build_object(fj_state *st, const VALUE *pairs, long count) {
|
|
|
1178
1180
|
|
|
1179
1181
|
for (i = 0; i + 1 < count; i += 2) {
|
|
1180
1182
|
VALUE k = st->symbolize_keys ? rb_funcall(pairs[i], fj_to_sym_id, 0) : pairs[i];
|
|
1181
|
-
if (st->dup_first_wins || st->dup_raise || st->
|
|
1183
|
+
if (st->dup_first_wins || st->dup_raise || st->on_warning != Qnil) {
|
|
1182
1184
|
if (RTEST(rb_funcall(hash, fj_key_p_id, 1, k))) {
|
|
1183
1185
|
if (st->dup_raise) fj_error(st, "duplicate key");
|
|
1184
1186
|
fj_warn(st, fj_sym_duplicate_key, "duplicate key");
|
|
@@ -1271,7 +1273,7 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
|
|
|
1271
1273
|
fj_skip_ws_comments(st);
|
|
1272
1274
|
b = fj_byte(st);
|
|
1273
1275
|
if (b == ',') { /* collapsing separator: skip empty member */
|
|
1274
|
-
if (st->
|
|
1276
|
+
if (st->on_warning != Qnil && !vss) fj_warn(st, fj_sym_empty_slot, "extra comma, collapsed an empty slot");
|
|
1275
1277
|
vss = 0;
|
|
1276
1278
|
fj_advance(st, 1);
|
|
1277
1279
|
continue;
|
|
@@ -1323,7 +1325,7 @@ static VALUE fj_parse_iter(fj_state *st, int implicit_root) {
|
|
|
1323
1325
|
fj_skip_ws_comments(st);
|
|
1324
1326
|
b = fj_byte(st);
|
|
1325
1327
|
if (b == ',') { /* collapsing separator: skip empty slot */
|
|
1326
|
-
if (st->
|
|
1328
|
+
if (st->on_warning != Qnil && !vss) fj_warn(st, fj_sym_empty_slot, "extra comma, collapsed an empty slot");
|
|
1327
1329
|
vss = 0;
|
|
1328
1330
|
fj_advance(st, 1);
|
|
1329
1331
|
continue;
|
|
@@ -1412,8 +1414,7 @@ static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
|
|
|
1412
1414
|
else st.bigdecimal_load = 1; /* :auto (default), including nil */
|
|
1413
1415
|
}
|
|
1414
1416
|
|
|
1415
|
-
st.
|
|
1416
|
-
st.warnings = st.collect_warnings ? rb_ary_new() : Qnil;
|
|
1417
|
+
st.on_warning = rb_hash_aref(opts, ID2SYM(rb_intern("on_warning"))); /* Qnil when absent */
|
|
1417
1418
|
|
|
1418
1419
|
if (st.len >= 3 && (unsigned char)st.buf[0] == 0xEF &&
|
|
1419
1420
|
(unsigned char)st.buf[1] == 0xBB && (unsigned char)st.buf[2] == 0xBF) {
|
|
@@ -1439,10 +1440,10 @@ static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
|
|
|
1439
1440
|
* whitespace / newline / concatenation do), so a bracketless comma list still
|
|
1440
1441
|
* raises in fj_parse_iter — the unsupported implicit-root array. */
|
|
1441
1442
|
fj_skip_ws_comments(&st);
|
|
1442
|
-
if (fj_eof(&st)) return
|
|
1443
|
+
if (fj_eof(&st)) return Qnil;
|
|
1443
1444
|
value = fj_parse_iter(&st, fj_implicit_root_ahead(&st));
|
|
1444
1445
|
fj_skip_ws_comments(&st);
|
|
1445
|
-
if (fj_eof(&st)) return
|
|
1446
|
+
if (fj_eof(&st)) return value;
|
|
1446
1447
|
{
|
|
1447
1448
|
VALUE arr = rb_ary_new();
|
|
1448
1449
|
rb_ary_push(arr, value);
|
|
@@ -1450,7 +1451,7 @@ static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
|
|
|
1450
1451
|
rb_ary_push(arr, fj_parse_iter(&st, fj_implicit_root_ahead(&st)));
|
|
1451
1452
|
fj_skip_ws_comments(&st);
|
|
1452
1453
|
} while (!fj_eof(&st));
|
|
1453
|
-
return
|
|
1454
|
+
return arr;
|
|
1454
1455
|
}
|
|
1455
1456
|
}
|
|
1456
1457
|
|
|
@@ -1463,6 +1464,7 @@ void Init_smarter_json(void) {
|
|
|
1463
1464
|
fj_to_sym_id = rb_intern("to_sym");
|
|
1464
1465
|
fj_key_p_id = rb_intern("key?");
|
|
1465
1466
|
fj_new_id = rb_intern("new");
|
|
1467
|
+
fj_call_id = rb_intern("call");
|
|
1466
1468
|
fj_sym_empty_slot = ID2SYM(rb_intern("empty_slot"));
|
|
1467
1469
|
fj_sym_empty_value = ID2SYM(rb_intern("empty_value"));
|
|
1468
1470
|
fj_sym_duplicate_key = ID2SYM(rb_intern("duplicate_key"));
|
data/lib/smarter_json/parser.rb
CHANGED
|
@@ -57,10 +57,9 @@ module SmarterJSON
|
|
|
57
57
|
Parser.new(input, options).each_value(&block)
|
|
58
58
|
end
|
|
59
59
|
elsif options.fetch(:acceleration, true) && HAS_ACCELERATION
|
|
60
|
-
parse_c(input, options)
|
|
60
|
+
parse_c(input, options)
|
|
61
61
|
else
|
|
62
|
-
|
|
63
|
-
options.fetch(:warnings, false) ? [parser.parse, parser.warnings] : parser.parse
|
|
62
|
+
Parser.new(input, options).parse
|
|
64
63
|
end
|
|
65
64
|
end
|
|
66
65
|
|
|
@@ -143,14 +142,9 @@ module SmarterJSON
|
|
|
143
142
|
symbolize_keys: false, # Symbol keys instead of String
|
|
144
143
|
duplicate_key: :last_wins, # :last_wins | :first_wins | :raise
|
|
145
144
|
bigdecimal_load: :auto, # :auto | :float | :bigdecimal (Oj-compatible)
|
|
146
|
-
|
|
145
|
+
on_warning: nil, # a callable invoked once per non-fatal lenient fix (a SmarterJSON::Warning)
|
|
147
146
|
}.freeze
|
|
148
147
|
|
|
149
|
-
# Warnings collected during the parse (empty slots, empty values, dropped duplicate
|
|
150
|
-
# keys). Empty unless the parser was built with warnings: true. Public so the module
|
|
151
|
-
# functions can read it after parse / each_value.
|
|
152
|
-
attr_reader :warnings
|
|
153
|
-
|
|
154
148
|
def initialize(input, options = {})
|
|
155
149
|
raise ArgumentError, "input must be a String" unless input.is_a?(String)
|
|
156
150
|
|
|
@@ -158,8 +152,7 @@ module SmarterJSON
|
|
|
158
152
|
@symbolize_keys = opts[:symbolize_keys]
|
|
159
153
|
@duplicate_key = opts[:duplicate_key]
|
|
160
154
|
@bigdecimal_load = opts[:bigdecimal_load]
|
|
161
|
-
@
|
|
162
|
-
@warnings = []
|
|
155
|
+
@on_warning = opts[:on_warning]
|
|
163
156
|
|
|
164
157
|
encoding = opts[:encoding]
|
|
165
158
|
@input = encoding ? input.dup.force_encoding(encoding) : input
|
|
@@ -263,7 +256,7 @@ module SmarterJSON
|
|
|
263
256
|
# Commas are collapsing separators inside a container: an empty slot (leading,
|
|
264
257
|
# interior, or trailing comma) adds nothing. Skip it; the next iteration reads
|
|
265
258
|
# the following value/key or the closing bracket.
|
|
266
|
-
warn(:empty_slot, "extra comma — collapsed an empty slot")
|
|
259
|
+
warn(:empty_slot, "extra comma — collapsed an empty slot") if @on_warning && !vss
|
|
267
260
|
vss = false
|
|
268
261
|
advance(1)
|
|
269
262
|
elsif cur_obj
|
|
@@ -300,7 +293,7 @@ module SmarterJSON
|
|
|
300
293
|
elsif [RBRACE, COMMA].include?(b)
|
|
301
294
|
# key with a colon but no value -> null (don't consume } or ,; the loop does)
|
|
302
295
|
store_member(cur, key, nil)
|
|
303
|
-
warn(:empty_value, "key #{key.inspect} had no value — used null")
|
|
296
|
+
warn(:empty_value, "key #{key.inspect} had no value — used null") if @on_warning
|
|
304
297
|
vss = true
|
|
305
298
|
elsif b.nil?
|
|
306
299
|
raise error("unexpected end of input")
|
|
@@ -573,7 +566,7 @@ module SmarterJSON
|
|
|
573
566
|
if hash.key?(k)
|
|
574
567
|
raise error("duplicate key #{k.inspect}") if @duplicate_key == :raise
|
|
575
568
|
|
|
576
|
-
warn(:duplicate_key, "duplicate key #{k.inspect} — #{@duplicate_key}")
|
|
569
|
+
warn(:duplicate_key, "duplicate key #{k.inspect} — #{@duplicate_key}") if @on_warning
|
|
577
570
|
return if @duplicate_key == :first_wins
|
|
578
571
|
end
|
|
579
572
|
hash[k] = value
|
|
@@ -933,11 +926,14 @@ module SmarterJSON
|
|
|
933
926
|
value
|
|
934
927
|
end
|
|
935
928
|
|
|
936
|
-
#
|
|
929
|
+
# Report a non-fatal lenient fix to the on_warning callable. The call-site guards
|
|
930
|
+
# (`if @on_warning`) keep the message string from being built on the fast path; this
|
|
931
|
+
# internal guard is the safety net so a forgotten call-site guard can't crash a
|
|
932
|
+
# handler-less caller.
|
|
937
933
|
def warn(type, message)
|
|
938
|
-
return unless @
|
|
934
|
+
return unless @on_warning
|
|
939
935
|
|
|
940
|
-
@
|
|
936
|
+
@on_warning.call(Warning.new(type, message, @line, @col))
|
|
941
937
|
end
|
|
942
938
|
|
|
943
939
|
def error(message)
|
data/lib/smarter_json/version.rb
CHANGED
data/lib/smarter_json/warning.rb
CHANGED
|
@@ -3,8 +3,8 @@
|
|
|
3
3
|
module SmarterJSON
|
|
4
4
|
# A non-fatal thing the parser worked around while staying lenient — e.g. an empty
|
|
5
5
|
# comma slot it collapsed, a key with no value it read as null, or a duplicate key
|
|
6
|
-
# it dropped.
|
|
7
|
-
#
|
|
6
|
+
# it dropped. Passed to the on_warning: callable (when process / process_file is given
|
|
7
|
+
# one) once per fix; otherwise the parser stays silent and builds no Warning at all.
|
|
8
8
|
#
|
|
9
9
|
# type — a Symbol you can branch on (:empty_slot, :empty_value, :duplicate_key)
|
|
10
10
|
# message — human-readable description
|
metadata
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: smarter_json
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.7.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Tilo Sloboda
|
|
8
8
|
bindir: exe
|
|
9
9
|
cert_chain: []
|
|
10
|
-
date: 2026-06-
|
|
10
|
+
date: 2026-06-03 00:00:00.000000000 Z
|
|
11
11
|
dependencies:
|
|
12
12
|
- !ruby/object:Gem::Dependency
|
|
13
13
|
name: bigdecimal
|