nosj 0.4.0 → 0.5.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 +105 -0
- data/Cargo.lock +17 -17
- data/README.md +32 -15
- data/ext/nosj/Cargo.toml +6 -6
- data/ext/nosj/fuzz/Cargo.toml +1 -1
- data/ext/nosj/fuzz/src/prelude.rb +20 -44
- data/ext/nosj/src/files.rs +21 -20
- data/ext/nosj/src/gen/errors.rs +12 -10
- data/ext/nosj/src/gen/mod.rs +8 -10
- data/ext/nosj/src/gen/opts.rs +76 -141
- data/ext/nosj/src/gen/ruby.rs +50 -22
- data/ext/nosj/src/gen/walker.rs +219 -102
- data/ext/nosj/src/lazy.rs +60 -50
- data/ext/nosj/src/lib.rs +4 -0
- data/ext/nosj/src/lines.rs +27 -17
- data/ext/nosj/src/locate.rs +162 -0
- data/ext/nosj/src/opt_reader.rs +170 -0
- data/ext/nosj/src/parse.rs +170 -99
- data/ext/nosj/src/patch.rs +25 -15
- data/ext/nosj/src/pointer.rs +9 -11
- data/ext/nosj/src/reformat.rs +83 -140
- data/ext/nosj/src/sink.rs +193 -60
- data/ext/nosj/src/state.rs +65 -14
- data/ext/nosj/src/stats.rs +35 -33
- data/lib/nosj/json.rb +105 -39
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +36 -22
- data/sig/nosj.rbs +4 -3
- metadata +4 -2
data/ext/nosj/src/state.rs
CHANGED
|
@@ -10,33 +10,78 @@ use ahash::AHashMap;
|
|
|
10
10
|
use magnus::typed_data::Obj;
|
|
11
11
|
use magnus::{DataTypeFunctions, TypedData};
|
|
12
12
|
use nosj::Buffers;
|
|
13
|
-
use std::cell::
|
|
13
|
+
use std::cell::Cell;
|
|
14
|
+
use std::thread::LocalKey;
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
///
|
|
16
|
+
use crate::sink::DupScratch;
|
|
17
|
+
|
|
18
|
+
/// Run `f` on a pooled thread-local value, taken OUT of its cell for the
|
|
19
|
+
/// call and stored back afterwards. Pooled state is never borrowed
|
|
20
|
+
/// across a call that can reach Ruby: any allocation can raise
|
|
21
|
+
/// NoMemoryError, whose longjmp skips these frames, and a RefCell borrow
|
|
22
|
+
/// held across it would stay borrowed for good (under panic=abort, the
|
|
23
|
+
/// next borrow kills the process). A value lost that way is only leaked;
|
|
24
|
+
/// the next call, like a nested one finding the cell empty, starts from
|
|
25
|
+
/// `T::default()`. The outermost call's value is the one stored back.
|
|
26
|
+
pub(crate) fn with_taken<T: Default, R>(
|
|
27
|
+
key: &'static LocalKey<Cell<T>>,
|
|
28
|
+
f: impl FnOnce(&mut T) -> R,
|
|
29
|
+
) -> R {
|
|
30
|
+
let mut value = key.take();
|
|
31
|
+
let result = f(&mut value);
|
|
32
|
+
key.set(value);
|
|
33
|
+
result
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// Everything a parse touches, reused across calls: nosj's scratch
|
|
37
|
+
/// buffers, the interned-key caches, and the GC-marked stacks.
|
|
18
38
|
pub(crate) struct PullState {
|
|
19
39
|
pub(crate) bufs: Buffers,
|
|
20
40
|
pub(crate) keys: AHashMap<Box<str>, rb_sys::VALUE>,
|
|
21
41
|
/// Separate cache for symbolize_names mode: symbol and string VALUEs
|
|
22
42
|
/// must never share a map.
|
|
23
43
|
pub(crate) sym_keys: AHashMap<Box<str>, rb_sys::VALUE>,
|
|
24
|
-
/// Leaked once per
|
|
44
|
+
/// Leaked once per state; kept alive + GC-marked via the wrapped
|
|
25
45
|
/// handle.
|
|
26
46
|
pub(crate) vstack: Option<&'static mut VStackShadow>,
|
|
27
47
|
/// Marked shadow holding the cached key VALUEs; keys are kept alive by
|
|
28
48
|
/// this (collectable on epoch clear), NOT by per-key eternal GC pins.
|
|
29
49
|
pub(crate) key_shadow: Option<&'static mut VStackShadow>,
|
|
50
|
+
/// Duplicate-key detection buffers for hash-less sinks.
|
|
51
|
+
pub(crate) dup: DupScratch,
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
impl PullState {
|
|
55
|
+
#[cold]
|
|
56
|
+
fn fresh() -> Box<Self> {
|
|
57
|
+
Box::new(PullState {
|
|
58
|
+
bufs: Buffers::new(),
|
|
59
|
+
keys: AHashMap::with_capacity(256),
|
|
60
|
+
sym_keys: AHashMap::new(),
|
|
61
|
+
vstack: None,
|
|
62
|
+
key_shadow: None,
|
|
63
|
+
dup: DupScratch::default(),
|
|
64
|
+
})
|
|
65
|
+
}
|
|
30
66
|
}
|
|
31
67
|
|
|
32
68
|
thread_local! {
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
69
|
+
static PULL_STATE: Cell<Option<Box<PullState>>> = const { Cell::new(None) };
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Run `f` on this thread's parse state, taken out like [`with_taken`]
|
|
73
|
+
/// (a state lost to a longjmp leaks its shadows' last VALUEs; a nested
|
|
74
|
+
/// call would start a fresh one). Unlike the generate scratch, parse
|
|
75
|
+
/// bodies never run Ruby code, so the thread cannot hop native threads
|
|
76
|
+
/// mid-call and one thread-local access serves both the take and the
|
|
77
|
+
/// put-back: measured ~5ns per call on tiny documents against two.
|
|
78
|
+
pub(crate) fn with_pull_state<R>(f: impl FnOnce(&mut PullState) -> R) -> R {
|
|
79
|
+
PULL_STATE.with(|cell| {
|
|
80
|
+
let mut state = cell.take().unwrap_or_else(PullState::fresh);
|
|
81
|
+
let result = f(&mut state);
|
|
82
|
+
cell.set(Some(state));
|
|
83
|
+
result
|
|
84
|
+
})
|
|
40
85
|
}
|
|
41
86
|
|
|
42
87
|
/// GC-marked holder for pending VALUEs.
|
|
@@ -48,6 +93,12 @@ pub(crate) struct VStackShadow {
|
|
|
48
93
|
/// through [`DataTypeFunctions::mark`] (its trampoline, not ours),
|
|
49
94
|
/// pinning every pending VALUE with `rb_gc_mark` semantics. The class
|
|
50
95
|
/// is defined (and made a private constant) at init.
|
|
96
|
+
///
|
|
97
|
+
/// Deliberately NOT `wb_protected`: parses push VALUEs into the shadow
|
|
98
|
+
/// with plain stores, no write barriers, so an old protected handle
|
|
99
|
+
/// would let the GC miss young values it holds (a use-after-free).
|
|
100
|
+
/// Staying write-barrier-unprotected makes every GC rescan the handle,
|
|
101
|
+
/// which costs nothing measurable: there are one to three per thread.
|
|
51
102
|
#[derive(TypedData)]
|
|
52
103
|
#[magnus(class = "NOSJ::ValueStackShadow", mark)]
|
|
53
104
|
pub(crate) struct ShadowHandle(*const VStackShadow);
|
|
@@ -74,7 +125,7 @@ impl DataTypeFunctions for ShadowHandle {
|
|
|
74
125
|
}
|
|
75
126
|
}
|
|
76
127
|
|
|
77
|
-
/// Create (once per
|
|
128
|
+
/// Create (once per owning state) a leaked, GC-marked VStackShadow.
|
|
78
129
|
pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>) {
|
|
79
130
|
if slot.is_none() {
|
|
80
131
|
let ruby = magnus::Ruby::get().expect("called on a Ruby thread");
|
|
@@ -83,7 +134,7 @@ pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>)
|
|
|
83
134
|
}));
|
|
84
135
|
let ptr = std::ptr::from_mut::<VStackShadow>(shadow).cast_const();
|
|
85
136
|
let handle: Obj<ShadowHandle> = ruby.obj_wrap(ShadowHandle(ptr));
|
|
86
|
-
|
|
137
|
+
ruby.gc_register_mark_object(handle);
|
|
87
138
|
*slot = Some(shadow);
|
|
88
139
|
}
|
|
89
140
|
}
|
data/ext/nosj/src/stats.rs
CHANGED
|
@@ -8,11 +8,11 @@ use ahash::AHashMap;
|
|
|
8
8
|
use magnus::value::ReprValue;
|
|
9
9
|
use magnus::{Error, RHash, RString, Ruby, Value};
|
|
10
10
|
|
|
11
|
-
use crate::errors::{nesting_error, parser_error, parser_error_at};
|
|
12
11
|
use crate::files::with_mapped_file;
|
|
13
|
-
use crate::
|
|
12
|
+
use crate::opt_reader::{Opt, OptReader};
|
|
13
|
+
use crate::parse::{drive_error, max_nesting_of, options_hash, utf8_input, ParseNativeOpts};
|
|
14
14
|
use crate::sink::SinkAbort;
|
|
15
|
-
use crate::state::
|
|
15
|
+
use crate::state::with_pull_state;
|
|
16
16
|
|
|
17
17
|
/// What the document's root value was; reported as a Symbol. The
|
|
18
18
|
/// Default is never observable (a successful pass always saw a root
|
|
@@ -239,41 +239,41 @@ fn stats_to_hash(ruby: &Ruby, s: &StatsSink, byte_size: usize) -> Result<Value,
|
|
|
239
239
|
Ok(out.as_value())
|
|
240
240
|
}
|
|
241
241
|
|
|
242
|
-
///
|
|
243
|
-
///
|
|
244
|
-
///
|
|
245
|
-
///
|
|
246
|
-
fn
|
|
247
|
-
let o =
|
|
248
|
-
|
|
249
|
-
|
|
242
|
+
/// Stats' options: `max_nesting`, which here defaults to UNLIMITED (a
|
|
243
|
+
/// deep blob is exactly what a diagnostic should describe, not refuse),
|
|
244
|
+
/// and the grammar extensions. Decoded before the source is borrowed:
|
|
245
|
+
/// a hash lookup can run a key's own `hash`/`eql?`.
|
|
246
|
+
fn stats_opts(ruby: &Ruby, opts: Value) -> Result<ParseNativeOpts, Error> {
|
|
247
|
+
let mut o = ParseNativeOpts {
|
|
248
|
+
max_nesting: usize::MAX,
|
|
249
|
+
..ParseNativeOpts::default()
|
|
250
|
+
};
|
|
251
|
+
let Some(h) = options_hash(ruby, opts)? else {
|
|
252
|
+
return Ok(o);
|
|
253
|
+
};
|
|
254
|
+
let mut r = OptReader::new(ruby, h);
|
|
255
|
+
if let Some(v) = r.get(Opt::MaxNesting) {
|
|
256
|
+
o.max_nesting = max_nesting_of(v);
|
|
257
|
+
}
|
|
258
|
+
o.popts.allow_nan = r.truthy(Opt::AllowNan);
|
|
259
|
+
o.popts.allow_trailing_comma = r.truthy(Opt::AllowTrailingComma);
|
|
260
|
+
r.finish()?;
|
|
261
|
+
Ok(o)
|
|
262
|
+
}
|
|
250
263
|
|
|
264
|
+
/// Run the counting pass over already-UTF-8-vouched bytes and build
|
|
265
|
+
/// the result.
|
|
266
|
+
fn stats_over(ruby: &Ruby, input: &[u8], o: &ParseNativeOpts) -> Result<Value, Error> {
|
|
251
267
|
let mut sink = StatsSink {
|
|
252
|
-
max_nesting:
|
|
253
|
-
o.max_nesting
|
|
254
|
-
} else {
|
|
255
|
-
usize::MAX
|
|
256
|
-
},
|
|
268
|
+
max_nesting: o.max_nesting,
|
|
257
269
|
..StatsSink::default()
|
|
258
270
|
};
|
|
259
|
-
let result =
|
|
260
|
-
let mut state = cell.borrow_mut();
|
|
271
|
+
let result = with_pull_state(|state| {
|
|
261
272
|
// Safety: callers verified UTF-8 (coderange or a full scan).
|
|
262
273
|
unsafe { nosj::parse_utf8_unchecked_with(input, &mut state.bufs, &mut sink, o.popts) }
|
|
263
274
|
});
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
Err(nosj::DriveError::Sink(SinkAbort::TooDeep)) => Err(nesting_error(
|
|
267
|
-
ruby,
|
|
268
|
-
format!("nesting of {} is too deep", o.max_nesting.saturating_add(1)),
|
|
269
|
-
)),
|
|
270
|
-
// The other aborts cannot happen (this sink never raises them),
|
|
271
|
-
// but the match must be total.
|
|
272
|
-
Err(nosj::DriveError::Sink(_)) => Err(parser_error(ruby, "stats pass aborted".into())),
|
|
273
|
-
Err(nosj::DriveError::Parse(e)) => {
|
|
274
|
-
Err(parser_error_at(ruby, input, e.offset, e.to_string()))
|
|
275
|
-
}
|
|
276
|
-
}
|
|
275
|
+
result.map_err(|failure| drive_error(ruby, failure, o, input, (0, input.len())))?;
|
|
276
|
+
stats_to_hash(ruby, &sink, input.len())
|
|
277
277
|
}
|
|
278
278
|
|
|
279
279
|
/// `NOSJ.stats(source, opts)`: document statistics from one null-sink
|
|
@@ -284,8 +284,9 @@ pub fn stats_native(
|
|
|
284
284
|
data: RString,
|
|
285
285
|
opts: Value,
|
|
286
286
|
) -> Result<Value, Error> {
|
|
287
|
+
let o = stats_opts(ruby, opts)?;
|
|
287
288
|
let input = utf8_input(ruby, &data)?;
|
|
288
|
-
stats_over(ruby, input,
|
|
289
|
+
stats_over(ruby, input, &o)
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
/// `NOSJ.stats_file(path, opts)`: `NOSJ.stats` against a memory-mapped
|
|
@@ -296,6 +297,7 @@ pub fn stats_file_native(
|
|
|
296
297
|
path: RString,
|
|
297
298
|
opts: Value,
|
|
298
299
|
) -> Result<Value, Error> {
|
|
300
|
+
let o = stats_opts(ruby, opts)?;
|
|
299
301
|
let p = path.to_string()?;
|
|
300
|
-
with_mapped_file(ruby, &p, |map| stats_over(ruby, &map,
|
|
302
|
+
with_mapped_file(ruby, &p, |map| stats_over(ruby, &map, &o))
|
|
301
303
|
}
|
data/lib/nosj/json.rb
CHANGED
|
@@ -12,12 +12,18 @@
|
|
|
12
12
|
# Entry points built on JSON.parse (JSON.load, JSON.parse!,
|
|
13
13
|
# JSON.load_file, JSON.unsafe_load) pick up the fast path automatically
|
|
14
14
|
# and keep their exact legacy behavior when they need unsupported options
|
|
15
|
-
# (JSON.load
|
|
15
|
+
# (json 2's JSON.load passes create_additions, so it always takes the
|
|
16
|
+
# fallback).
|
|
16
17
|
#
|
|
17
|
-
#
|
|
18
|
-
# (
|
|
19
|
-
#
|
|
20
|
-
#
|
|
18
|
+
# The drop-in follows whichever json is installed, 2.x or 3.x: its
|
|
19
|
+
# calling conventions (json 3's keyword-only parse, its dump defaults)
|
|
20
|
+
# and its semantics. NOSJ implements json 3's (duplicate keys and lone
|
|
21
|
+
# surrogates are errors), so whenever the fast path refuses a call, the
|
|
22
|
+
# original gem runs it again and has the last word: json 2 accepts what
|
|
23
|
+
# it always accepted, and every exception is the gem's own, message,
|
|
24
|
+
# json_path and invalid_object included. That second pass happens on
|
|
25
|
+
# failures only; a generate run twice this way calls to_json on the
|
|
26
|
+
# objects visited before the refusal twice.
|
|
21
27
|
#
|
|
22
28
|
# Not rerouted: obj.to_json (core extensions drive the gem's generator
|
|
23
29
|
# directly), and objects with a custom to_json inside a rerouted
|
|
@@ -30,15 +36,26 @@ module NOSJ
|
|
|
30
36
|
# Implementation detail of `require "nosj/json"`.
|
|
31
37
|
# @private
|
|
32
38
|
module JSONDropIn
|
|
33
|
-
#
|
|
34
|
-
#
|
|
35
|
-
|
|
39
|
+
# json 3 made parse's options keyword-only, fixed dump's defaults and
|
|
40
|
+
# raises for options json 2 ignored or aliased.
|
|
41
|
+
JSON3 = ::JSON::VERSION.to_i >= 3
|
|
42
|
+
|
|
36
43
|
PARSE_OPTS = %i[symbolize_names freeze max_nesting allow_nan
|
|
37
|
-
allow_trailing_comma
|
|
44
|
+
allow_trailing_comma allow_duplicate_key].freeze
|
|
45
|
+
# json 2 ignores quirks_mode, which Rails 7.x passes from
|
|
46
|
+
# ActiveSupport::JSON.decode, so its fast path takes the key and
|
|
47
|
+
# drops it (NOSJ.parse always parses top-level scalars, and refuses
|
|
48
|
+
# unknown keys). json 3 raises for it, so there it reaches the gem
|
|
49
|
+
# like any other unknown option.
|
|
50
|
+
QUIRKS_MODE = :quirks_mode
|
|
51
|
+
JSON2_PARSE_OPTS = (PARSE_OPTS + [QUIRKS_MODE]).freeze
|
|
38
52
|
GENERATE_OPTS = %i[indent space space_before object_nl array_nl
|
|
39
|
-
max_nesting allow_nan ascii_only script_safe
|
|
40
|
-
|
|
41
|
-
|
|
53
|
+
max_nesting allow_nan ascii_only script_safe strict depth
|
|
54
|
+
buffer_initial_length allow_duplicate_key].freeze
|
|
55
|
+
JSON3_DUMP_DEFAULTS = {allow_nan: true}.freeze
|
|
56
|
+
# json 2.10 and older accept only strict: in dump's options hash,
|
|
57
|
+
# through this private helper; later versions merge any option.
|
|
58
|
+
DUMP_MERGES_OPTIONS = !::JSON.respond_to?(:merge_dump_options, true)
|
|
42
59
|
|
|
43
60
|
module_function
|
|
44
61
|
|
|
@@ -52,6 +69,13 @@ module NOSJ
|
|
|
52
69
|
true
|
|
53
70
|
end
|
|
54
71
|
|
|
72
|
+
# Generate options the fast path handles: supported keys, minus the
|
|
73
|
+
# one combination NOSJ refuses (ascii_only with script_safe, which
|
|
74
|
+
# json supports).
|
|
75
|
+
def generate_supported?(opts)
|
|
76
|
+
supported?(opts, GENERATE_OPTS) && !(opts && opts[:ascii_only] && opts[:script_safe])
|
|
77
|
+
end
|
|
78
|
+
|
|
55
79
|
def parse(source, opts)
|
|
56
80
|
# NOSJ.parse is deliberately strict about encodings (json-3.0
|
|
57
81
|
# semantics), but the drop-in must match the installed gem, which
|
|
@@ -61,30 +85,60 @@ module NOSJ
|
|
|
61
85
|
# (copy-on-write bytes), and the validity scan is memoized
|
|
62
86
|
# coderange the parse would compute anyway. Anything else
|
|
63
87
|
# non-UTF-8 (UTF-16, ...) belongs to gem json, which transcodes.
|
|
88
|
+
input = source
|
|
64
89
|
if source.is_a?(String)
|
|
65
90
|
case source.encoding
|
|
66
91
|
when Encoding::UTF_8, Encoding::US_ASCII
|
|
67
92
|
# the fast path as-is
|
|
68
93
|
when Encoding::BINARY
|
|
69
94
|
utf8 = source.dup.force_encoding(Encoding::UTF_8)
|
|
70
|
-
|
|
95
|
+
input = utf8 if utf8.valid_encoding?
|
|
71
96
|
else
|
|
72
|
-
return
|
|
97
|
+
return original_parse(source, opts)
|
|
73
98
|
end
|
|
74
99
|
end
|
|
75
|
-
NOSJ.parse(
|
|
76
|
-
rescue NOSJ::NestingError
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
100
|
+
NOSJ.parse(input, opts)
|
|
101
|
+
rescue NOSJ::ParserError, NOSJ::NestingError
|
|
102
|
+
original_parse(source, opts)
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
# json 2's options without quirks_mode (see QUIRKS_MODE), allocating
|
|
106
|
+
# nothing for Rails' lone `quirks_mode: true`.
|
|
107
|
+
def without_quirks_mode(opts)
|
|
108
|
+
return opts unless opts&.key?(QUIRKS_MODE)
|
|
109
|
+
if opts.size == 1
|
|
110
|
+
nil
|
|
111
|
+
else
|
|
112
|
+
opts.except(QUIRKS_MODE)
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# The installed gem's parse. json 3 takes keywords only; json 2's
|
|
117
|
+
# positional options hash receives them just the same.
|
|
118
|
+
def original_parse(source, opts)
|
|
119
|
+
::JSON.nosj_original_parse(source, **(opts || {}))
|
|
80
120
|
end
|
|
81
121
|
|
|
82
122
|
def generate(obj, opts, pretty)
|
|
83
123
|
pretty ? NOSJ.pretty_generate(obj, opts) : NOSJ.generate(obj, opts)
|
|
84
|
-
rescue NOSJ::NestingError
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
124
|
+
rescue NOSJ::GeneratorError, NOSJ::NestingError
|
|
125
|
+
if pretty
|
|
126
|
+
::JSON.nosj_original_pretty_generate(obj, opts)
|
|
127
|
+
else
|
|
128
|
+
::JSON.nosj_original_generate(obj, opts)
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# The options JSON.dump generates with before the caller's own: fixed
|
|
133
|
+
# in json 3; json 2 reads its user-settable dump_default_options,
|
|
134
|
+
# through the internal reader 2.11 added when it deprecated the
|
|
135
|
+
# public one. Which one is decided here, once.
|
|
136
|
+
if JSON3
|
|
137
|
+
def dump_defaults = JSON3_DUMP_DEFAULTS
|
|
138
|
+
elsif ::JSON.respond_to?(:_dump_default_options)
|
|
139
|
+
def dump_defaults = ::JSON._dump_default_options
|
|
140
|
+
else
|
|
141
|
+
def dump_defaults = ::JSON.dump_default_options
|
|
88
142
|
end
|
|
89
143
|
end
|
|
90
144
|
end
|
|
@@ -101,16 +155,26 @@ module JSON
|
|
|
101
155
|
alias_method :nosj_original_pretty_generate, :pretty_generate
|
|
102
156
|
alias_method :nosj_original_dump, :dump
|
|
103
157
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
NOSJ::JSONDropIn.
|
|
107
|
-
|
|
108
|
-
|
|
158
|
+
if NOSJ::JSONDropIn::JSON3
|
|
159
|
+
def parse(source, **opts)
|
|
160
|
+
if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::PARSE_OPTS)
|
|
161
|
+
NOSJ::JSONDropIn.parse(source, opts)
|
|
162
|
+
else
|
|
163
|
+
nosj_original_parse(source, **opts)
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
else
|
|
167
|
+
def parse(source, opts = nil)
|
|
168
|
+
if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::JSON2_PARSE_OPTS)
|
|
169
|
+
NOSJ::JSONDropIn.parse(source, NOSJ::JSONDropIn.without_quirks_mode(opts))
|
|
170
|
+
else
|
|
171
|
+
nosj_original_parse(source, opts)
|
|
172
|
+
end
|
|
109
173
|
end
|
|
110
174
|
end
|
|
111
175
|
|
|
112
176
|
def generate(obj, opts = nil)
|
|
113
|
-
if NOSJ::JSONDropIn.
|
|
177
|
+
if NOSJ::JSONDropIn.generate_supported?(opts)
|
|
114
178
|
NOSJ::JSONDropIn.generate(obj, opts, false)
|
|
115
179
|
else
|
|
116
180
|
nosj_original_generate(obj, opts)
|
|
@@ -118,7 +182,7 @@ module JSON
|
|
|
118
182
|
end
|
|
119
183
|
|
|
120
184
|
def pretty_generate(obj, opts = nil)
|
|
121
|
-
if NOSJ::JSONDropIn.
|
|
185
|
+
if NOSJ::JSONDropIn.generate_supported?(opts)
|
|
122
186
|
NOSJ::JSONDropIn.generate(obj, opts, true)
|
|
123
187
|
else
|
|
124
188
|
nosj_original_pretty_generate(obj, opts)
|
|
@@ -127,17 +191,19 @@ module JSON
|
|
|
127
191
|
|
|
128
192
|
def dump(obj, an_io = nil, limit = nil, kwargs = nil)
|
|
129
193
|
# Fast path for the common shapes, dump(obj) and dump(obj, opts
|
|
130
|
-
# hash)
|
|
131
|
-
#
|
|
132
|
-
#
|
|
133
|
-
|
|
134
|
-
|
|
194
|
+
# hash): the installed gem's dump defaults merged under the
|
|
195
|
+
# caller's options. IO and limit arguments, and anything the
|
|
196
|
+
# fast path refuses, take gem json's own dump, which also turns
|
|
197
|
+
# json 2's NestingError into its ArgumentError.
|
|
198
|
+
if limit.nil? && kwargs.nil? &&
|
|
199
|
+
(an_io.nil? || NOSJ::JSONDropIn::DUMP_MERGES_OPTIONS && an_io.instance_of?(Hash))
|
|
200
|
+
opts = NOSJ::JSONDropIn.dump_defaults
|
|
135
201
|
opts = opts.merge(an_io) if an_io
|
|
136
|
-
if NOSJ::JSONDropIn.
|
|
202
|
+
if NOSJ::JSONDropIn.generate_supported?(opts)
|
|
137
203
|
begin
|
|
138
|
-
return NOSJ
|
|
139
|
-
rescue ::
|
|
140
|
-
|
|
204
|
+
return NOSJ.generate(obj, opts)
|
|
205
|
+
rescue NOSJ::GeneratorError, NOSJ::NestingError
|
|
206
|
+
# the gem's own dump below runs it again and decides
|
|
141
207
|
end
|
|
142
208
|
end
|
|
143
209
|
end
|
data/lib/nosj/version.rb
CHANGED
data/lib/nosj.rb
CHANGED
|
@@ -86,10 +86,13 @@ module NOSJ
|
|
|
86
86
|
# Parses a JSON document, JSON.parse-compatible: same values, same
|
|
87
87
|
# option names, same behavior, byte-for-byte.
|
|
88
88
|
#
|
|
89
|
-
#
|
|
90
|
-
# (
|
|
91
|
-
# +
|
|
92
|
-
#
|
|
89
|
+
# Options follow json 3: an unknown key raises ArgumentError
|
|
90
|
+
# (<code>unknown keyword: foo</code>). The json options nosj does not
|
|
91
|
+
# implement (+object_class+, +array_class+, +decimal_class+,
|
|
92
|
+
# +on_load+, +create_additions+, +allow_comments+,
|
|
93
|
+
# +allow_control_characters+, +allow_invalid_escape+) raise
|
|
94
|
+
# ArgumentError unless falsy, since their falsy default is nosj's
|
|
95
|
+
# behavior.
|
|
93
96
|
#
|
|
94
97
|
# @example
|
|
95
98
|
# NOSJ.parse('{"a":[1,true]}') #=> {"a" => [1, true]}
|
|
@@ -98,12 +101,14 @@ module NOSJ
|
|
|
98
101
|
# @param source [String] the JSON document (UTF-8 or US-ASCII)
|
|
99
102
|
# @param opts [Hash, nil] +symbolize_names+, +freeze+, +max_nesting+
|
|
100
103
|
# (Integer or +false+ for unlimited), +allow_nan+,
|
|
101
|
-
# +allow_trailing_comma+
|
|
104
|
+
# +allow_trailing_comma+, +allow_duplicate_key+ (json 3 semantics:
|
|
105
|
+
# a repeated key raises unless this is true, then the last one wins)
|
|
102
106
|
# @return [Object] the parsed value tree
|
|
103
|
-
# @raise [ParserError] when the document is malformed
|
|
104
|
-
#
|
|
107
|
+
# @raise [ParserError] when the document is malformed, repeats a key,
|
|
108
|
+
# holds a lone surrogate (+"\udc00"+), or is not UTF-8; carries the
|
|
109
|
+
# failure position ({ParserError#line} and friends)
|
|
105
110
|
# @raise [NestingError] when nesting exceeds +max_nesting+
|
|
106
|
-
# @raise [ArgumentError] for unsupported options
|
|
111
|
+
# @raise [ArgumentError] for unknown or unsupported options
|
|
107
112
|
def self.parse(source, opts = nil)
|
|
108
113
|
parse_native(source, opts)
|
|
109
114
|
end
|
|
@@ -119,12 +124,18 @@ module NOSJ
|
|
|
119
124
|
# @param obj [Object] the value tree to serialize
|
|
120
125
|
# @param opts [Hash, nil] +indent+, +space+, +space_before+,
|
|
121
126
|
# +object_nl+, +array_nl+, +max_nesting+ (Integer or +false+),
|
|
122
|
-
# +allow_nan+, +ascii_only+, +script_safe+
|
|
123
|
-
# +
|
|
127
|
+
# +allow_nan+, +ascii_only+, +script_safe+, +strict+, +depth+,
|
|
128
|
+
# +buffer_initial_length+, +allow_duplicate_key+ (json 3
|
|
129
|
+
# semantics: keys that render alike, like <code>"a"</code> and
|
|
130
|
+
# <code>:a</code>, raise unless true). As in json 3, an unknown
|
|
131
|
+
# key raises (+escape_slash+ is gone: use +script_safe+), and the
|
|
132
|
+
# unimplemented +sort_keys+ and +as_json+ raise unless falsy.
|
|
124
133
|
# @return [String] the JSON document
|
|
125
134
|
# @raise [GeneratorError] for non-finite floats without +allow_nan+,
|
|
126
|
-
# unsupported objects under +strict+,
|
|
135
|
+
# unsupported objects under +strict+, keys that render alike, or
|
|
136
|
+
# broken string encodings
|
|
127
137
|
# @raise [NestingError] when nesting exceeds +max_nesting+
|
|
138
|
+
# @raise [ArgumentError] for unknown or unsupported options
|
|
128
139
|
|
|
129
140
|
# Generates human-readable JSON, JSON.pretty_generate-compatible
|
|
130
141
|
# (two-space indent, newlines between elements). Options override the
|
|
@@ -156,7 +167,7 @@ module NOSJ
|
|
|
156
167
|
# @param source [String] the JSON document
|
|
157
168
|
# @param opts [Hash, nil] same options as {.parse}
|
|
158
169
|
# @return [Boolean]
|
|
159
|
-
# @raise [ArgumentError] for unsupported options
|
|
170
|
+
# @raise [ArgumentError] for unknown or unsupported options
|
|
160
171
|
def self.valid?(source, opts = nil)
|
|
161
172
|
valid_native(source, opts)
|
|
162
173
|
end
|
|
@@ -205,7 +216,8 @@ module NOSJ
|
|
|
205
216
|
|
|
206
217
|
# Partial parsing by JSON Pointer (with the standard +~0+/+~1+
|
|
207
218
|
# escapes). The matched subtree materializes under the same options
|
|
208
|
-
# as {.parse}
|
|
219
|
+
# as {.parse}; +allow_nan+ and +allow_trailing_comma+ also govern the
|
|
220
|
+
# walk to it.
|
|
209
221
|
#
|
|
210
222
|
# @example
|
|
211
223
|
# NOSJ.at_pointer(json, "/users/3/name") #=> "grace" or nil
|
|
@@ -322,18 +334,19 @@ module NOSJ
|
|
|
322
334
|
# Minifies a document without building any Ruby values: the parser's
|
|
323
335
|
# events pipe straight into the emission kernels, SIMD in and SIMD
|
|
324
336
|
# out. Output is exactly what <code>generate(parse(json))</code>
|
|
325
|
-
# would produce,
|
|
326
|
-
#
|
|
327
|
-
#
|
|
328
|
-
#
|
|
337
|
+
# would produce, and it accepts exactly what {.parse} accepts; under
|
|
338
|
+
# +allow_duplicate_key+, repeated keys pass through instead of being
|
|
339
|
+
# collapsed (a reformatter must not silently drop data). Numbers come
|
|
340
|
+
# out in the canonical spelling (+1.50+ becomes +1.5+) and string
|
|
341
|
+
# escapes are normalized.
|
|
329
342
|
#
|
|
330
343
|
# @example
|
|
331
344
|
# NOSJ.minify(%({ "a": [1, 2],\n "b": "x" })) #=> '{"a":[1,2],"b":"x"}'
|
|
332
345
|
#
|
|
333
346
|
# @param json [String] the document (UTF-8 or US-ASCII)
|
|
334
347
|
# @param opts [Hash, nil] acceptance options (+allow_nan+,
|
|
335
|
-
# +allow_trailing_comma+, +max_nesting+);
|
|
336
|
-
# normalized away when accepted
|
|
348
|
+
# +allow_trailing_comma+, +allow_duplicate_key+, +max_nesting+);
|
|
349
|
+
# trailing commas are normalized away when accepted
|
|
337
350
|
# @return [String] the minified document
|
|
338
351
|
# @raise [ParserError] when the document is malformed
|
|
339
352
|
# @raise [NestingError] past +max_nesting+
|
|
@@ -357,8 +370,7 @@ module NOSJ
|
|
|
357
370
|
# @return [String] the reformatted document
|
|
358
371
|
# @raise [ParserError] when the document is malformed
|
|
359
372
|
# @raise [NestingError] past +max_nesting+
|
|
360
|
-
# @raise [GeneratorError]
|
|
361
|
-
# string it cannot represent
|
|
373
|
+
# @raise [GeneratorError] for a non-finite float without +allow_nan+
|
|
362
374
|
def self.reformat(json, opts = nil)
|
|
363
375
|
if opts&.key?(:pretty)
|
|
364
376
|
pretty = opts[:pretty]
|
|
@@ -597,7 +609,9 @@ module NOSJ
|
|
|
597
609
|
#
|
|
598
610
|
# @param source [String] the JSON document (UTF-8 or US-ASCII)
|
|
599
611
|
# @param opts [Hash, nil] +max_nesting+, +allow_nan+,
|
|
600
|
-
# +allow_trailing_comma+ (acceptance options only)
|
|
612
|
+
# +allow_trailing_comma+ (acceptance options only). Being a
|
|
613
|
+
# diagnostic, stats also describes documents {.parse} would refuse
|
|
614
|
+
# for a repeated key or a lone surrogate.
|
|
601
615
|
# @return [Hash] the statistics described above
|
|
602
616
|
# @raise [ParserError] when the document is malformed or not UTF-8
|
|
603
617
|
def self.stats(source, opts = nil)
|
data/sig/nosj.rbs
CHANGED
|
@@ -14,9 +14,10 @@ module NOSJ
|
|
|
14
14
|
# Options arrive as a positional Hash (the JSON gem's own calling
|
|
15
15
|
# convention; an explicit **kwargs would allocate per call), nil when
|
|
16
16
|
# omitted. Parse: symbolize_names, freeze, max_nesting, allow_nan,
|
|
17
|
-
# allow_trailing_comma. Generate: indent, space,
|
|
18
|
-
# object_nl, array_nl, max_nesting, allow_nan,
|
|
19
|
-
# script_safe
|
|
17
|
+
# allow_trailing_comma, allow_duplicate_key. Generate: indent, space,
|
|
18
|
+
# space_before, object_nl, array_nl, max_nesting, allow_nan,
|
|
19
|
+
# ascii_only, script_safe, strict, depth, buffer_initial_length,
|
|
20
|
+
# allow_duplicate_key. Unknown keys raise ArgumentError (json 3).
|
|
20
21
|
type opts = Hash[Symbol, untyped]?
|
|
21
22
|
|
|
22
23
|
# One NOSJ.dig path element (negative Integer indices resolve to nil).
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: nosj
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Yaroslav Markin
|
|
@@ -64,6 +64,8 @@ files:
|
|
|
64
64
|
- ext/nosj/src/lazy.rs
|
|
65
65
|
- ext/nosj/src/lib.rs
|
|
66
66
|
- ext/nosj/src/lines.rs
|
|
67
|
+
- ext/nosj/src/locate.rs
|
|
68
|
+
- ext/nosj/src/opt_reader.rs
|
|
67
69
|
- ext/nosj/src/parse.rs
|
|
68
70
|
- ext/nosj/src/patch.rs
|
|
69
71
|
- ext/nosj/src/pointer.rs
|
|
@@ -104,7 +106,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
104
106
|
- !ruby/object:Gem::Version
|
|
105
107
|
version: 3.3.11
|
|
106
108
|
requirements: []
|
|
107
|
-
rubygems_version: 4.0.
|
|
109
|
+
rubygems_version: 4.0.20
|
|
108
110
|
specification_version: 4
|
|
109
111
|
summary: An extremely fast JSON parser and generator for Ruby, written in Rust and
|
|
110
112
|
SIMD-accelerated on every platform.
|