nosj 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,370 @@
1
+ //! The nosj sinks: `RubyValueSink` builds Ruby VALUEs directly during
2
+ //! the parse (with interned-key caches and gem-compatible option
3
+ //! handling); `NullSink` powers `NOSJ.valid?` by discarding every
4
+ //! event. Raw VALUE construction helpers live here too.
5
+
6
+ use ahash::AHashMap;
7
+
8
+ use crate::state::VStackShadow;
9
+
10
+ /// Sanity ceiling for pending values (memory bomb guard), not a design limit.
11
+ const SINK_STACK_MAX: usize = 1 << 26;
12
+
13
+ /// The JSON gem's default nesting limit; matching it is part of drop-in
14
+ /// compatibility (gem raises NestingError past 100 levels).
15
+ pub(crate) const MAX_NESTING: usize = 100;
16
+
17
+ const KEY_CACHE_CAP: usize = 2048;
18
+
19
+ /// Why a sink stopped the drive; mapped onto the gem's exceptions in
20
+ /// [`crate::parse::finish_drive`].
21
+ pub(crate) enum SinkAbort {
22
+ Overflow,
23
+ BadBigint,
24
+ TooDeep,
25
+ }
26
+
27
+ /// `RB_INT2FIX` ported from Ruby's public inline headers: fixnums are
28
+ /// `(i << 1) + 1` for `|i| <= LONG_MAX / 2`, and the range is defined by
29
+ /// the C `long`, which is 32-bit on Windows (LLP64): fixnums there hold
30
+ /// only 31 bits, and tagging anything wider crashes Ruby with
31
+ /// "Unnormalized Fixnum value". C extensions get the check inlined by the
32
+ /// header; going through the extern `rb_ll2inum` costs an FFI call per
33
+ /// integer. Non-fixable values still take the call.
34
+ // The widening is an identity on LP64 hosts (clippy flags it there) but
35
+ // required on Windows, where c_long is 32-bit.
36
+ #[allow(clippy::unnecessary_cast)]
37
+ #[inline(always)]
38
+ fn int_to_raw(i: i64) -> rb_sys::VALUE {
39
+ const FIXABLE_MIN: i64 = (std::os::raw::c_long::MIN / 2) as i64;
40
+ const FIXABLE_MAX: i64 = (std::os::raw::c_long::MAX / 2) as i64;
41
+ if (FIXABLE_MIN..=FIXABLE_MAX).contains(&i) {
42
+ ((i as u64) << 1).wrapping_add(1) as rb_sys::VALUE
43
+ } else {
44
+ unsafe { rb_sys::rb_ll2inum(i) }
45
+ }
46
+ }
47
+
48
+ #[inline(always)]
49
+ fn str_to_raw(s: &str) -> rb_sys::VALUE {
50
+ unsafe {
51
+ rb_sys::rb_utf8_str_new(
52
+ s.as_ptr() as *const std::os::raw::c_char,
53
+ s.len() as std::os::raw::c_long,
54
+ )
55
+ }
56
+ }
57
+
58
+ #[inline(always)]
59
+ fn interned_str_raw(s: &str) -> rb_sys::VALUE {
60
+ unsafe {
61
+ rb_sys::rb_enc_interned_str(
62
+ s.as_ptr() as *const std::os::raw::c_char,
63
+ s.len() as std::os::raw::c_long,
64
+ rb_sys::rb_utf8_encoding(),
65
+ )
66
+ }
67
+ }
68
+
69
+ /// Interned-key cache with epoch eviction: when the cap is reached the whole
70
+ /// cache is cleared (hot keys repopulate immediately). Without this, one
71
+ /// document with many unique keys (citm's numeric id maps) permanently fills
72
+ /// the cache and every later document pays full interning per key, measured
73
+ /// as a 1.37x regression on twitter parsed after citm in the same process.
74
+ #[inline(always)]
75
+ fn intern_key_cached(
76
+ key: &str,
77
+ map: &mut AHashMap<Box<str>, rb_sys::VALUE>,
78
+ shadow: &mut VStackShadow,
79
+ ) -> rb_sys::VALUE {
80
+ if key.is_empty() || key.len() > 64 {
81
+ return interned_str_raw(key);
82
+ }
83
+
84
+ if let Some(&v) = map.get(key) {
85
+ return v;
86
+ }
87
+
88
+ let raw = interned_str_raw(key);
89
+ if map.len() >= KEY_CACHE_CAP {
90
+ map.clear();
91
+ shadow.values.clear();
92
+ }
93
+ map.insert(Box::from(key), raw);
94
+ shadow.values.push(raw);
95
+ raw
96
+ }
97
+
98
+ /// Symbol-mode key cache. Static symbols from `rb_intern3` are permanent, so
99
+ /// no GC shadow is needed; epoch-cleared at capacity like the string cache.
100
+ #[inline(always)]
101
+ fn intern_symbol_cached(key: &str, map: &mut AHashMap<Box<str>, rb_sys::VALUE>) -> rb_sys::VALUE {
102
+ #[inline(always)]
103
+ fn intern(key: &str) -> rb_sys::VALUE {
104
+ unsafe {
105
+ rb_sys::rb_id2sym(rb_sys::rb_intern3(
106
+ key.as_ptr() as *const std::os::raw::c_char,
107
+ key.len() as std::os::raw::c_long,
108
+ rb_sys::rb_utf8_encoding(),
109
+ ))
110
+ }
111
+ }
112
+
113
+ if key.is_empty() || key.len() > 64 {
114
+ return intern(key);
115
+ }
116
+ if let Some(&v) = map.get(key) {
117
+ return v;
118
+ }
119
+ let raw = intern(key);
120
+ if map.len() >= KEY_CACHE_CAP {
121
+ map.clear();
122
+ }
123
+ map.insert(Box::from(key), raw);
124
+ raw
125
+ }
126
+
127
+ /// nosj::Sink building Ruby VALUEs on the heap value stack. Pending
128
+ /// VALUEs are kept alive by the pinned TypedData wrapper's precise dmark.
129
+ pub(crate) struct RubyValueSink<'a> {
130
+ pub(crate) stack: &'a mut Vec<rb_sys::VALUE>,
131
+ pub(crate) keys: &'a mut AHashMap<Box<str>, rb_sys::VALUE>,
132
+ pub(crate) key_shadow: &'a mut VStackShadow,
133
+ pub(crate) depth: usize,
134
+ /// JSON.parse-compatible options; defaults keep the fast paths.
135
+ pub(crate) symbolize: bool,
136
+ pub(crate) freeze: bool,
137
+ pub(crate) max_nesting: usize,
138
+ }
139
+
140
+ // Tried and rejected (2026-07-10): a jiter-style cache of repeated VALUE
141
+ // strings (rb_str_dup of a canonical copy, gated to heap lengths 40..=200
142
+ // where the dup shares its buffer copy-on-write). Measured twitter parse
143
+ // 0.95x vs 0.90x without: Ruby must dup for mutability where Python only
144
+ // INCREFs, and the hash+insert overhead exceeds the CoW savings.
145
+
146
+ impl RubyValueSink<'_> {
147
+ #[inline(always)]
148
+ fn push_raw(&mut self, raw: rb_sys::VALUE) -> Result<(), SinkAbort> {
149
+ if self.stack.len() >= SINK_STACK_MAX {
150
+ return Err(SinkAbort::Overflow);
151
+ }
152
+ self.stack.push(raw);
153
+ Ok(())
154
+ }
155
+
156
+ #[inline(always)]
157
+ fn enter_container(&mut self) -> Result<(), SinkAbort> {
158
+ self.depth += 1;
159
+ if self.depth > self.max_nesting {
160
+ return Err(SinkAbort::TooDeep);
161
+ }
162
+ Ok(())
163
+ }
164
+ }
165
+
166
+ impl nosj::Sink for RubyValueSink<'_> {
167
+ type Error = SinkAbort;
168
+
169
+ #[inline(always)]
170
+ fn null(&mut self) -> Result<(), SinkAbort> {
171
+ self.push_raw(rb_sys::special_consts::Qnil as rb_sys::VALUE)
172
+ }
173
+
174
+ #[inline(always)]
175
+ fn boolean(&mut self, value: bool) -> Result<(), SinkAbort> {
176
+ self.push_raw(if value {
177
+ rb_sys::special_consts::Qtrue as rb_sys::VALUE
178
+ } else {
179
+ rb_sys::special_consts::Qfalse as rb_sys::VALUE
180
+ })
181
+ }
182
+
183
+ #[inline(always)]
184
+ fn int(&mut self, value: i64) -> Result<(), SinkAbort> {
185
+ self.push_raw(int_to_raw(value))
186
+ }
187
+
188
+ #[inline(always)]
189
+ fn float(&mut self, value: f64) -> Result<(), SinkAbort> {
190
+ self.push_raw(unsafe { rb_sys::rb_float_new(value) })
191
+ }
192
+
193
+ #[inline(always)]
194
+ fn big_int(&mut self, digits: &str) -> Result<(), SinkAbort> {
195
+ let c = std::ffi::CString::new(digits).map_err(|_| SinkAbort::BadBigint)?;
196
+ self.push_raw(unsafe { rb_sys::rb_cstr2inum(c.as_ptr(), 10) })
197
+ }
198
+
199
+ #[inline(always)]
200
+ fn str(&mut self, value: &str) -> Result<(), SinkAbort> {
201
+ let raw = if self.freeze {
202
+ // Gem parity: freeze mode dedupes strings via the fstring table.
203
+ interned_str_raw(value)
204
+ } else {
205
+ str_to_raw(value)
206
+ };
207
+ self.push_raw(raw)
208
+ }
209
+
210
+ #[inline(always)]
211
+ fn key(&mut self, key: &str) -> Result<(), SinkAbort> {
212
+ let raw = if self.symbolize {
213
+ intern_symbol_cached(key, self.keys)
214
+ } else {
215
+ intern_key_cached(key, self.keys, self.key_shadow)
216
+ };
217
+ self.push_raw(raw)
218
+ }
219
+
220
+ /// Lone-low-surrogate content: gem parity is a UTF-8-encoded Ruby string
221
+ /// carrying the raw WTF-8 bytes (broken coderange, like the gem's).
222
+ #[inline(always)]
223
+ fn str_bytes(&mut self, value: &[u8]) -> Result<(), SinkAbort> {
224
+ let raw = unsafe {
225
+ let s = rb_sys::rb_utf8_str_new(
226
+ value.as_ptr() as *const std::os::raw::c_char,
227
+ value.len() as std::os::raw::c_long,
228
+ );
229
+ if self.freeze {
230
+ rb_sys::rb_str_freeze(s)
231
+ } else {
232
+ s
233
+ }
234
+ };
235
+ self.push_raw(raw)
236
+ }
237
+
238
+ #[inline(always)]
239
+ fn key_bytes(&mut self, key: &[u8]) -> Result<(), SinkAbort> {
240
+ // Interning is skipped: these keys are pathological, not hot.
241
+ let raw = unsafe {
242
+ rb_sys::rb_utf8_str_new(
243
+ key.as_ptr() as *const std::os::raw::c_char,
244
+ key.len() as std::os::raw::c_long,
245
+ )
246
+ };
247
+ let frozen = unsafe { rb_sys::rb_str_freeze(raw) };
248
+ self.push_raw(frozen)
249
+ }
250
+
251
+ #[inline(always)]
252
+ fn begin_array(&mut self) -> Result<(), SinkAbort> {
253
+ self.enter_container()
254
+ }
255
+
256
+ #[inline(always)]
257
+ fn begin_object(&mut self) -> Result<(), SinkAbort> {
258
+ self.enter_container()
259
+ }
260
+
261
+ #[inline(always)]
262
+ fn mark(&self) -> usize {
263
+ self.stack.len()
264
+ }
265
+
266
+ // Note: eager arrays (begin_array allocating + array_checkpoint spilling)
267
+ // were measured SLOWER here; rb_ary_new + rb_ary_cat growth per small
268
+ // array loses to one exact-size rb_ary_new_from_values, and the hoped-for
269
+ // GC-marking savings didn't materialize (sweep/free dominates, not
270
+ // pending-stack marking). The default no-op hooks stay available for
271
+ // sinks where the trade differs.
272
+ #[inline(always)]
273
+ fn end_array(&mut self, mark: usize, _len: usize) -> Result<(), SinkAbort> {
274
+ self.depth -= 1;
275
+ let n = self.stack.len() - mark;
276
+ let raw = unsafe {
277
+ let a = rb_sys::rb_ary_new_from_values(
278
+ n as std::os::raw::c_long,
279
+ self.stack.as_ptr().add(mark),
280
+ );
281
+ if self.freeze {
282
+ rb_sys::rb_obj_freeze(a);
283
+ }
284
+ a
285
+ };
286
+ self.stack.truncate(mark);
287
+ self.push_raw(raw)
288
+ }
289
+
290
+ #[inline(always)]
291
+ fn end_object(&mut self, mark: usize, pairs: usize) -> Result<(), SinkAbort> {
292
+ self.depth -= 1;
293
+ let n = self.stack.len() - mark;
294
+ let hash_raw = unsafe { rb_sys::rb_hash_new_capa(pairs as std::os::raw::c_long) };
295
+ unsafe {
296
+ rb_sys::rb_hash_bulk_insert(
297
+ n as std::os::raw::c_long,
298
+ self.stack.as_ptr().add(mark),
299
+ hash_raw,
300
+ );
301
+ if self.freeze {
302
+ rb_sys::rb_obj_freeze(hash_raw);
303
+ }
304
+ }
305
+ self.stack.truncate(mark);
306
+ self.push_raw(hash_raw)
307
+ }
308
+ }
309
+
310
+ /// Validation-only sink: every event is a no-op except nesting-depth
311
+ /// tracking, so `NOSJ.valid?` runs the full parser (tokenizers,
312
+ /// string decode, number validation) without allocating a single VALUE.
313
+ pub(crate) struct NullSink {
314
+ pub(crate) depth: usize,
315
+ pub(crate) max_nesting: usize,
316
+ }
317
+
318
+ impl nosj::Sink for NullSink {
319
+ type Error = SinkAbort;
320
+
321
+ fn null(&mut self) -> Result<(), SinkAbort> {
322
+ Ok(())
323
+ }
324
+ fn boolean(&mut self, _: bool) -> Result<(), SinkAbort> {
325
+ Ok(())
326
+ }
327
+ fn int(&mut self, _: i64) -> Result<(), SinkAbort> {
328
+ Ok(())
329
+ }
330
+ fn float(&mut self, _: f64) -> Result<(), SinkAbort> {
331
+ Ok(())
332
+ }
333
+ fn big_int(&mut self, _: &str) -> Result<(), SinkAbort> {
334
+ Ok(())
335
+ }
336
+ fn str(&mut self, _: &str) -> Result<(), SinkAbort> {
337
+ Ok(())
338
+ }
339
+ fn key(&mut self, _: &str) -> Result<(), SinkAbort> {
340
+ Ok(())
341
+ }
342
+ fn str_bytes(&mut self, _: &[u8]) -> Result<(), SinkAbort> {
343
+ Ok(())
344
+ }
345
+ fn mark(&self) -> usize {
346
+ 0
347
+ }
348
+ fn begin_array(&mut self) -> Result<(), SinkAbort> {
349
+ self.depth += 1;
350
+ if self.depth > self.max_nesting {
351
+ return Err(SinkAbort::TooDeep);
352
+ }
353
+ Ok(())
354
+ }
355
+ fn begin_object(&mut self) -> Result<(), SinkAbort> {
356
+ self.depth += 1;
357
+ if self.depth > self.max_nesting {
358
+ return Err(SinkAbort::TooDeep);
359
+ }
360
+ Ok(())
361
+ }
362
+ fn end_array(&mut self, _: usize, _: usize) -> Result<(), SinkAbort> {
363
+ self.depth -= 1;
364
+ Ok(())
365
+ }
366
+ fn end_object(&mut self, _: usize, _: usize) -> Result<(), SinkAbort> {
367
+ self.depth -= 1;
368
+ Ok(())
369
+ }
370
+ }
@@ -0,0 +1,89 @@
1
+ //! Per-thread parser state and the GC-visible value-stack shadow.
2
+ //!
3
+ //! Pending VALUEs live on heap-allocated stacks that Ruby's GC can see
4
+ //! through a wrapped handle object whose magnus-driven mark walks the
5
+ //! live entries (the JSON gem's rvalue_stack design): precise marking
6
+ //! of only `values.len()` slots, growing on demand so huge flat arrays
7
+ //! (mesh.json) never overflow.
8
+
9
+ use ahash::AHashMap;
10
+ use magnus::typed_data::Obj;
11
+ use magnus::{DataTypeFunctions, TypedData};
12
+ use nosj::Buffers;
13
+ use std::cell::RefCell;
14
+
15
+ /// Everything a parse touches, allocated once per thread and reused:
16
+ /// nosj's scratch buffers, the interned-key caches, and the GC-marked
17
+ /// stacks.
18
+ pub(crate) struct PullState {
19
+ pub(crate) bufs: Buffers,
20
+ pub(crate) keys: AHashMap<Box<str>, rb_sys::VALUE>,
21
+ /// Separate cache for symbolize_names mode: symbol and string VALUEs
22
+ /// must never share a map.
23
+ pub(crate) sym_keys: AHashMap<Box<str>, rb_sys::VALUE>,
24
+ /// Leaked once per thread; kept alive + GC-marked via the wrapped
25
+ /// handle.
26
+ pub(crate) vstack: Option<&'static mut VStackShadow>,
27
+ /// Marked shadow holding the cached key VALUEs; keys are kept alive by
28
+ /// this (collectable on epoch clear), NOT by per-key eternal GC pins.
29
+ pub(crate) key_shadow: Option<&'static mut VStackShadow>,
30
+ }
31
+
32
+ thread_local! {
33
+ pub(crate) static PULL_STATE: RefCell<PullState> = RefCell::new(PullState {
34
+ bufs: Buffers::new(),
35
+ keys: AHashMap::with_capacity(256),
36
+ sym_keys: AHashMap::new(),
37
+ vstack: None,
38
+ key_shadow: None,
39
+ });
40
+ }
41
+
42
+ /// GC-marked holder for pending VALUEs.
43
+ pub(crate) struct VStackShadow {
44
+ pub(crate) values: Vec<rb_sys::VALUE>,
45
+ }
46
+
47
+ /// Ruby-side handle over a leaked shadow: magnus drives the GC mark
48
+ /// through [`DataTypeFunctions::mark`] (its trampoline, not ours),
49
+ /// pinning every pending VALUE with `rb_gc_mark` semantics. The class
50
+ /// is defined (and made a private constant) at init.
51
+ #[derive(TypedData)]
52
+ #[magnus(class = "NOSJ::ValueStackShadow", mark)]
53
+ pub(crate) struct ShadowHandle(*const VStackShadow);
54
+
55
+ // SAFETY: the pointee is leaked for the process lifetime and each
56
+ // handle stays with the thread that created it; the only cross-thread
57
+ // access is the GC mark read, which runs at safepoints while the
58
+ // owning thread is parked (the same contract the previous
59
+ // rb_data_type_t dmark relied on).
60
+ unsafe impl Send for ShadowHandle {}
61
+
62
+ impl DataTypeFunctions for ShadowHandle {
63
+ fn mark(&self, marker: &magnus::gc::Marker) {
64
+ use magnus::rb_sys::FromRawValue;
65
+ // SAFETY: the shadow is leaked for the process lifetime, and
66
+ // GC marks at safepoints while the owning thread is not
67
+ // mutating the stack (the same contract the previous
68
+ // hand-written dmark relied on). Entries are live VALUEs
69
+ // pushed by the sinks.
70
+ let shadow = unsafe { &*self.0 };
71
+ for &v in &shadow.values {
72
+ marker.mark(unsafe { magnus::Value::from_raw(v) });
73
+ }
74
+ }
75
+ }
76
+
77
+ /// Create (once per thread) a leaked, GC-marked VStackShadow.
78
+ pub(crate) fn ensure_marked_shadow(slot: &mut Option<&'static mut VStackShadow>) {
79
+ if slot.is_none() {
80
+ let ruby = magnus::Ruby::get().expect("called on a Ruby thread");
81
+ let shadow: &'static mut VStackShadow = Box::leak(Box::new(VStackShadow {
82
+ values: Vec::with_capacity(1024),
83
+ }));
84
+ let ptr = std::ptr::from_mut::<VStackShadow>(shadow).cast_const();
85
+ let handle: Obj<ShadowHandle> = ruby.obj_wrap(ShadowHandle(ptr));
86
+ ruby.gc_register_mark_object(handle);
87
+ *slot = Some(shadow);
88
+ }
89
+ }
data/lib/nosj/json.rb ADDED
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Drop-in acceleration for the JSON module:
4
+ #
5
+ # require "nosj/json"
6
+ #
7
+ # reroutes JSON.parse, JSON.generate, JSON.pretty_generate and JSON.dump
8
+ # through NOSJ whenever the requested options fall within NOSJ's
9
+ # supported set, and falls back to the original json implementation for
10
+ # everything else (create_additions, object_class/array_class,
11
+ # decimal_class, on_load procs, JSON::State instances, IO arguments).
12
+ # Entry points built on JSON.parse (JSON.load, JSON.parse!,
13
+ # JSON.load_file, JSON.unsafe_load) pick up the fast path automatically
14
+ # and keep their exact legacy behavior when they need unsupported options
15
+ # (JSON.load's create_additions default always takes the fallback).
16
+ #
17
+ # Exceptions from the fast path are re-raised as the JSON classes
18
+ # (JSON::ParserError, JSON::GeneratorError, JSON::NestingError), so
19
+ # existing rescue clauses keep working. Parse error MESSAGES are
20
+ # NOSJ's (byte offsets rather than the gem's phrasing).
21
+ #
22
+ # Not rerouted: obj.to_json (core extensions drive the gem's generator
23
+ # directly), and objects with a custom to_json inside a rerouted
24
+ # generate receive no State argument (documented NOSJ divergence).
25
+
26
+ require "json"
27
+ require "nosj"
28
+
29
+ module NOSJ
30
+ # Implementation detail of `require "nosj/json"`.
31
+ # @private
32
+ module JSONDropIn
33
+ PARSE_OPTS = %i[symbolize_names freeze max_nesting allow_nan
34
+ allow_trailing_comma].freeze
35
+ GENERATE_OPTS = %i[indent space space_before object_nl array_nl
36
+ max_nesting allow_nan ascii_only script_safe
37
+ escape_slash strict depth
38
+ buffer_initial_length].freeze
39
+
40
+ module_function
41
+
42
+ # The fast path handles nil or a plain Hash whose every key NOSJ
43
+ # implements; anything else (JSON::State, exotic options, string
44
+ # keys) belongs to the original implementation.
45
+ def supported?(opts, allowed)
46
+ return true if opts.nil?
47
+ return false unless opts.instance_of?(Hash)
48
+ opts.each_key { |k| return false unless allowed.include?(k) }
49
+ true
50
+ end
51
+
52
+ def parse(source, opts)
53
+ NOSJ.parse(source, opts)
54
+ rescue RuntimeError => e
55
+ raise ::JSON::ParserError, e.message
56
+ end
57
+
58
+ def generate(obj, opts, pretty)
59
+ pretty ? NOSJ.pretty_generate(obj, opts) : NOSJ.generate(obj, opts)
60
+ rescue NOSJ::NestingError => e
61
+ raise ::JSON::NestingError, e.message
62
+ rescue NOSJ::GeneratorError => e
63
+ raise ::JSON::GeneratorError, e.message
64
+ end
65
+ end
66
+ end
67
+
68
+ # Reopened by `require "nosj/json"` to reroute the module functions
69
+ # through NOSJ; behavior is documented on the require and in the
70
+ # README, not here.
71
+ # @private
72
+ module JSON
73
+ class << self
74
+ unless method_defined?(:nosj_original_parse) || private_method_defined?(:nosj_original_parse)
75
+ alias_method :nosj_original_parse, :parse
76
+ alias_method :nosj_original_generate, :generate
77
+ alias_method :nosj_original_pretty_generate, :pretty_generate
78
+ alias_method :nosj_original_dump, :dump
79
+
80
+ def parse(source, opts = nil)
81
+ if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::PARSE_OPTS)
82
+ NOSJ::JSONDropIn.parse(source, opts)
83
+ else
84
+ nosj_original_parse(source, opts)
85
+ end
86
+ end
87
+
88
+ def generate(obj, opts = nil)
89
+ if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::GENERATE_OPTS)
90
+ NOSJ::JSONDropIn.generate(obj, opts, false)
91
+ else
92
+ nosj_original_generate(obj, opts)
93
+ end
94
+ end
95
+
96
+ def pretty_generate(obj, opts = nil)
97
+ if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::GENERATE_OPTS)
98
+ NOSJ::JSONDropIn.generate(obj, opts, true)
99
+ else
100
+ nosj_original_pretty_generate(obj, opts)
101
+ end
102
+ end
103
+
104
+ def dump(obj, an_io = nil, limit = nil, kwargs = nil)
105
+ # Fast path for the common shapes, dump(obj) and dump(obj, opts
106
+ # hash), mirroring the gem: dump defaults merged under the
107
+ # user's options, NestingError surfaced as ArgumentError. IO and
108
+ # limit arguments take the original implementation.
109
+ if limit.nil? && kwargs.nil? && (an_io.nil? || an_io.instance_of?(Hash))
110
+ opts = _dump_default_options
111
+ opts = opts.merge(an_io) if an_io
112
+ if NOSJ::JSONDropIn.supported?(opts, NOSJ::JSONDropIn::GENERATE_OPTS)
113
+ begin
114
+ return NOSJ::JSONDropIn.generate(obj, opts, false)
115
+ rescue ::JSON::NestingError
116
+ raise ArgumentError, "exceed depth limit"
117
+ end
118
+ end
119
+ end
120
+ nosj_original_dump(obj, an_io, limit, kwargs)
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ # MultiJson adapter:
4
+ #
5
+ # require "nosj/multi_json"
6
+ # MultiJson.use NOSJ::MultiJsonAdapter
7
+ #
8
+ # Anything speaking MultiJson (Faraday middleware and friends) then
9
+ # parses and generates through NOSJ.
10
+
11
+ require "json"
12
+ require "multi_json"
13
+ require "multi_json/adapter"
14
+ require "nosj"
15
+
16
+ module NOSJ
17
+ # MultiJson adapter routing +MultiJson.load+/+dump+ through nosj.
18
+ #
19
+ # multi_json 2.x renamed its namespace MultiJson -> MultiJSON; the
20
+ # adapter inherits from whichever this installation defines.
21
+ #
22
+ # @example
23
+ # require "nosj/multi_json"
24
+ # MultiJson.use NOSJ::MultiJsonAdapter
25
+ class MultiJsonAdapter < (defined?(::MultiJSON) ? ::MultiJSON::Adapter : ::MultiJson::Adapter)
26
+ # MultiJson wraps whatever the adapter's ParseError names.
27
+ ParseError = ::JSON::ParserError
28
+
29
+ SYMBOLIZE = {symbolize_names: true}.freeze
30
+ private_constant :SYMBOLIZE
31
+
32
+ # @param string [String] the JSON document
33
+ # @param options [Hash] multi_json load options; +symbolize_keys+
34
+ # arrives normalized as +symbolize_names+
35
+ # @return [Object] the parsed value tree
36
+ # @raise [JSON::ParserError] when the document is malformed
37
+ def load(string, options = {})
38
+ ::NOSJ.parse(string, options[:symbolize_names] ? SYMBOLIZE : nil)
39
+ rescue RuntimeError => e
40
+ raise ParseError, e.message
41
+ end
42
+
43
+ # @param object [Object] the value tree to serialize
44
+ # @param options [Hash] multi_json dump options; +pretty+ selects
45
+ # pretty-printing
46
+ # @return [String] the JSON document
47
+ def dump(object, options = {})
48
+ options[:pretty] ? ::NOSJ.pretty_generate(object) : ::NOSJ.generate(object)
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Precompiled platform gems ship one extension per Ruby minor under
4
+ # lib/nosj/<major.minor>/; the source gem compiles straight to
5
+ # lib/nosj/nosj.<dlext>.
6
+ begin
7
+ ruby_version = RUBY_VERSION[/\d+\.\d+/]
8
+ require_relative "#{ruby_version}/nosj"
9
+ rescue LoadError
10
+ require_relative "nosj"
11
+ end
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NOSJ
4
+ # The gem version.
5
+ VERSION = "0.1.0"
6
+ end