nosj 0.1.0-x86_64-linux

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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0a0fd640bbecb91b12ed17a8ee5edc5b87befe85b1fcdcd124c58624255b3d02
4
+ data.tar.gz: 6cc111ee1c0a1d55640325d48e5b71fad155f18d218211a9fcaf2f3f2a3a54b7
5
+ SHA512:
6
+ metadata.gz: 6dcc90744851a53f59a088e2ea4429ece7df99e2b72cbe81d049818641beb500b6847b9a3d9f703094c6d2f9aec669c89ab3ae652d47d29a8f3cefe0ee195ba9
7
+ data.tar.gz: 1f50198d497a46a8abbd281934ce3a04e5a704a1cbec221e2cf1dc0d570de817e447c4eb580c61e288960d5c797ba8f3f7b5e9215e85f90b439ae02e9dce57db
data/CHANGELOG.md ADDED
@@ -0,0 +1,10 @@
1
+ ## [0.1.0] - 2026-07-15
2
+
3
+ Initial release.
4
+
5
+ - `NOSJ.parse`, `NOSJ.generate`, and `NOSJ.pretty_generate`: `json`-gem-compatible parsing and generation—same output bytes, same option names, same error classes and messages—built on the first-party SIMD [nosj](https://crates.io/crates/nosj) crate (NEON on Apple Silicon; SSE2/AVX2 on x86-64, selected at runtime). Faster than the `json` gem and the third-party parsers (Oj, RapidJSON, FastJsonparser, Yajl) across the benchmark corpus, in both directions.
6
+ - Partial parsing: `NOSJ.dig` and `NOSJ.at_pointer` resolve a JSON Pointer and materialize only the matched subtree; `NOSJ.dig_many` and `NOSJ.at_pointers` resolve whole batches of paths in a single pass over the document.
7
+ - `NOSJ.valid?`: full-strictness validation that allocates no Ruby objects.
8
+ - Drop-in acceleration: `require "nosj/json"` reroutes `JSON.parse`, `JSON.generate`, `JSON.pretty_generate`, and `JSON.dump` through nosj, falling back to the original implementation for unsupported options; `require "nosj/multi_json"` adds a MultiJson adapter.
9
+ - Precompiled platform gems, each built natively with profile-guided optimization: Linux x86-64 and arm64 (glibc and musl), macOS (Apple Silicon), Windows (x64), for Ruby 3.3 through 4.0. Other platforms compile the source gem.
10
+ - RBS signatures and full YARD documentation.
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2026 Yaroslav Markin
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/NOTICE ADDED
@@ -0,0 +1,13 @@
1
+ nosj (Ruby gem)
2
+
3
+ Copyright (c) 2026 Yaroslav Markin. Released under the MIT License
4
+ (see LICENSE.txt).
5
+
6
+ Every distribution of this gem embeds the nosj Rust crate
7
+ (https://github.com/yaroslav/nosj), compiled into the native
8
+ extension: precompiled platform gems ship the compiled binary, and the
9
+ source gem compiles the crate at install time. The crate is licensed
10
+ MIT AND BSL-1.0 AND Apache-2.0; its NOTICE file itemizes the derived
11
+ components (the fpconv/Grisu2 float formatter under BSL-1.0; the
12
+ fast_float decimal conversion and simdjson structural-indexing
13
+ techniques under Apache-2.0) and reproduces each component license.
data/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # gem nosj
2
+
3
+ **gem nosj** is an **very fast JSON parser and generator for Ruby**, written in Rust on the first-party [nosj](https://github.com/yaroslav/nosj) crate and **SIMD-accelerated** on every platform (NEON on Apple Silicon, SSE2/AVX2 on x86-64).
4
+
5
+ > gem nosj is the powerful evil twin of the json gem.
6
+
7
+ [![GitHub Release](https://img.shields.io/github/v/release/yaroslav/nosj-ruby)](https://github.com/yaroslav/nosj-ruby/releases)
8
+ [![Docs](https://img.shields.io/badge/yard-docs-blue.svg)](https://rubydoc.info/gems/nosj)
9
+
10
+ - It is **faster** than gem json and every
11
+ third-party parser, including Oj, RapidJSON, FastJsonparser, Yajl. 1.0–1.8× faster than the bundled json gem, 1.3–11× faster than Oj, and up to 17×
12
+ faster than Yajl—[see Benchmarks](#benchmarks).
13
+ - It comes **precompiled** (platform gems built with per-platform optimizations,
14
+ nothing to compile on install).
15
+ - It has a **partial parsing mode**: JSON Pointer lookups that pull single values out of big documents in microseconds, skipping everything else.
16
+ - Same API and option names as gem json.
17
+
18
+ **And there's more**: validate documents without building a single Ruby object, resolve whole batches of paths in one pass, and accelerate an entire application with a one-line drop-in.
19
+
20
+ - [Requirements](#requirements)
21
+ - [Getting started](#getting-started)
22
+ - [What's in the box](#whats-in-the-box)
23
+ - [Benchmarks](#benchmarks)
24
+ - [Switching from the json gem](#switching-from-the-json-gem)
25
+ - [How it works](#how-it-works)
26
+ - [Development](#development)
27
+ - [License](#license)
28
+
29
+ ## Requirements
30
+
31
+ - **Ruby 3.3 or newer** (CRuby; tested on 3.3, 3.4, and 4.0).
32
+ - **Linux** (x86-64 and arm64, glibc and musl), **macOS** (Apple
33
+ Silicon), or **Windows** (x64): these platforms install precompiled,
34
+ per-platform-optimized gems with nothing to build.
35
+
36
+ ## Getting started
37
+
38
+ ```bash
39
+ bundle add nosj
40
+ ```
41
+
42
+ ```ruby
43
+ require "nosj"
44
+
45
+ NOSJ.parse('{"a":[1,true]}') #=> {"a" => [1, true]}
46
+ NOSJ.generate({"a" => [1, true]}) #=> '{"a":[1,true]}'
47
+ ```
48
+
49
+ That's it—if you know the `json` gem, you already know `nosj`.
50
+
51
+ Want the speedup without touching your code? One line reroutes
52
+ `JSON.parse`, `JSON.generate`, `JSON.pretty_generate`, and `JSON.dump`
53
+ through nosj:
54
+
55
+ ```ruby
56
+ require "nosj/json"
57
+ ```
58
+
59
+ In a Bundler app (Rails included) that can live entirely in the
60
+ Gemfile you can do this:
61
+
62
+ ```ruby
63
+ gem "nosj", require: "nosj/json"
64
+ ```
65
+
66
+ Options nosj supports take the fast path; anything exotic
67
+ (`create_additions`, `object_class`, `JSON::State`, procs, IO
68
+ arguments) falls back to the original implementation, so `JSON.load`,
69
+ `JSON.parse!`, and `JSON.load_file` keep their exact behavior.
70
+ Exceptions re-raise as the JSON classes, so your rescue clauses keep
71
+ working. Measured through the patch: parse 1.11×, generate 1.05× over
72
+ the original gem. (A MultiJson adapter ships too:
73
+ `require "nosj/multi_json"`, then `MultiJson.use NOSJ::MultiJsonAdapter`.)
74
+
75
+ ## What's in the box
76
+
77
+ **The `json` gem API**, on the `NOSJ` module:
78
+
79
+ ```ruby
80
+ NOSJ.parse(src, symbolize_names: true) # also: freeze, max_nesting,
81
+ # allow_nan, allow_trailing_comma
82
+ NOSJ.generate(obj) # indent, space, object_nl, ...,
83
+ NOSJ.pretty_generate(obj) # ascii_only, script_safe, strict
84
+ ```
85
+
86
+ **Validation without parsing.** `NOSJ.valid?` runs the full
87
+ parser—tokenizers, string decode, number validation—into a null sink
88
+ and allocates no Ruby objects at all. It is 2-4× faster than
89
+ `NOSJ.parse`, which already leads every parser above:
90
+
91
+ ```ruby
92
+ NOSJ.valid?('{"a":1}') #=> true
93
+ NOSJ.valid?('{"a":}') #=> false
94
+ NOSJ.valid?(src, max_nesting: false) # same options as parse
95
+ ```
96
+
97
+ **Partial parsing.** Pull values out of a document without
98
+ materializing the rest—skipped content is stepped over at SIMD block
99
+ speed, so a lookup costs what it skips, not what the document weighs:
100
+
101
+ ```ruby
102
+ NOSJ.dig(json, "users", 3, "name") # Hash#dig-shaped
103
+ NOSJ.at_pointer(json, "/users/3/name") # JSON Pointer
104
+
105
+ # Many lookups in one pass. A batch costs about as much as its
106
+ # single deepest member:
107
+ NOSJ.at_pointers(json, ["/users/3/name", "/meta/count"])
108
+ NOSJ.dig_many(json, [["users", 3, "name"], ["meta", "count"]])
109
+ ```
110
+
111
+ Example: an early field resolves in ~0.35µs where `JSON.parse(json).dig(...)`
112
+ costs ~980µs on the same document—three orders of magnitude. A field
113
+ at the far end of a 570 KB document costs ~71µs, still 13× faster
114
+ than parse-then-dig. Misses return nil; matched subtrees materialize
115
+ with the same options as `parse` (`symbolize_names:`, `freeze:`).
116
+
117
+ ## Benchmarks
118
+
119
+ Every installed JSON gem, benchmark-ips: AWS EC2 c7a.2xlarge (AMD EPYC 9R14, Zen 4), Ruby 4.0.6 + YJIT, json 2.21.1, Oj 3.17.4, RapidJSON 0.4.0, FastJsonparser 0.6.0, Yajl 1.4.3, PGO build, 2026-07-16. `×N` = times slower than nosj.
120
+
121
+ Parse:
122
+
123
+ | file | nosj (i/s) | json | Oj | FastJsonparser | RapidJSON | Yajl |
124
+ |---|---:|---:|---:|---:|---:|---:|
125
+ | activitypub | **12.4k** | ×1.18 | ×1.53 | ×1.89 | ×1.99 | ×4.57 |
126
+ | canada | **248** | ×1.10 | ×8.28 | ×1.45 | ×1.51 | ×4.96 |
127
+ | citm_catalog | **504** | ×1.03 | ×1.97 | ×2.07 | ×1.83 | ×4.93 |
128
+ | gsoc-2018 | **397** | ×1.30 | ×1.47 | ×1.81 | ×1.80 | ×4.78 |
129
+ | homebrew-formula | **15.2** | ×1.13 | ×1.54 | ×2.42 | ×2.02 | ×4.30 |
130
+ | homebrew-llvm | **49.3k** | ×1.60 | ×1.48 | ×1.50 | ×1.85 | ×5.11 |
131
+ | mesh | **1.1k** | ×1.30 | ×3.93 | ×1.82 | ×1.95 | ×6.96 |
132
+ | numbers | **5.9k** | ×1.19 | ×4.64 | ×1.43 | ×1.75 | ×7.04 |
133
+ | ohai | **14.4k** | ×1.47 | ×1.61 | ×2.11 | ×1.97 | ×4.71 |
134
+ | simple | **977k** | ×1.31 | ×1.77 | ×2.08 | ×1.63 | ×5.15 |
135
+ | small_mixed | **3.2M** | ×1.48 | ×2.85 | ×2.44 | ×1.82 | ×6.75 |
136
+ | tolstoy | **8.9k** | ×1.79 | ×1.96 | ×2.29 | ×2.10 | ×17.31 |
137
+ | twitter | **1.1k** | ×1.09 | ×1.83 | ×2.25 | ×2.61 | ×5.40 |
138
+
139
+ Generate:
140
+
141
+ | file | nosj (i/s) | json | Oj | RapidJSON | Yajl |
142
+ |---|---:|---:|---:|---:|---:|
143
+ | activitypub | **34.8k** | ×1.20 | ×1.78 | ×2.41 | ×5.84 |
144
+ | canada | **150** | ×0.98\* | ×10.98 | ×11.20 | ×10.87 |
145
+ | citm_catalog | **1.3k** | ×1.04 | ×1.44 | ×1.55 | ×2.85 |
146
+ | gsoc-2018 | **1.1k** | ×1.28 | ×2.64 | ×3.64 | ×11.00 |
147
+ | homebrew-formula | **20.9** | ×1.07 | ×1.25 | ×1.62 | ×3.03 |
148
+ | homebrew-llvm | **71.5k** | ×1.23 | ×2.22 | ×3.04 | ×5.80 |
149
+ | mesh | **613** | ×1.08 | ×9.24 | ×9.40 | ×9.26 |
150
+ | numbers | **2.1k** | ×1.03 | ×10.63 | ×11.00 | ×10.69 |
151
+ | ohai | **39.4k** | ×1.07 | ×1.29 | ×1.50 | ×3.37 |
152
+ | simple | **2.2M** | ×1.03 | ×1.58 | ×1.62 | ×4.36 |
153
+ | small_mixed | **5.6M** | ×1.09 | ×2.26 | ×1.83 | ×7.16 |
154
+ | tolstoy | **8.3k** | ×1.30 | ×4.15 | ×6.50 | ×14.54 |
155
+ | twitter | **2.9k** | ×1.09 | ×1.46 | ×2.01 | ×4.05 |
156
+
157
+ \* canada-generate is a statistical tie with the json gem (within
158
+ measurement error).
159
+
160
+ Reproduce with `rake bench` (the parity-gated comparison, after a PGO retrain—the shipping configuration) or `rake bench:ips` (the multi-gem shoot-out).
161
+
162
+ ## Switching from the json gem
163
+
164
+ You mostly don't have to do anything. Some differences:
165
+
166
+ - The legacy object-deserialization options (`create_additions`,
167
+ `object_class`, `array_class`, `decimal_class`) raise ArgumentError;
168
+ the `nosj/json` drop-in falls back to the original gem for them.
169
+ - Behaviors the `json` gem itself deprecates (JS comments, raw invalid
170
+ UTF-8) follow the strict semantics instead.
171
+ - Unlike `Array#dig`, negative indices in `NOSJ.dig` return nil (JSON
172
+ Pointer has no equivalent).
173
+ - Parse error *messages* use byte offsets rather than the gem's
174
+ phrasing (classes match).
175
+
176
+ Everything else—including the gem's exact float formatting, which is
177
+ not the shortest-round-trip form most libraries emit—matches
178
+ byte-for-byte and is verified continuously against the full corpus.
179
+
180
+ ## How it works
181
+
182
+ Most fast parsers build their own tree first and convert it into Ruby
183
+ objects second, paying for every string and container twice. nosj is
184
+ built on the [nosj](https://github.com/yaroslav/nosj) Rust crate, an
185
+ event parser with no tree of its own:
186
+
187
+ - **No intermediate tree.** The crate parses with NEON/SSE2/AVX2 SIMD
188
+ kernels and emits *events*; the extension builds interned hash keys,
189
+ strings, and containers directly on Ruby's heap during the parse,
190
+ with GC-safe value stacks and epoch-evicted key caches. Generation
191
+ walks Ruby objects once, streaming through fused scan-and-store
192
+ escape kernels.
193
+ - **Byte-exact floats.** Output reproduces the json gem's fpconv
194
+ (Grisu2) float format digit for digit—round-tripping is verified,
195
+ not assumed.
196
+ - **PGO everywhere.** Local builds, CI, and every precompiled platform
197
+ gem train on the benchmark corpus before the shipping compile; the
198
+ precompiled binaries use portable codegen with SIMD tiers detected at
199
+ runtime.
200
+
201
+ ## Development
202
+
203
+ ```bash
204
+ bundle exec rake compile # build the extension (applies a PGO profile if present)
205
+ bundle exec rake spec # the gem-parity suite
206
+ bundle exec rake bench # PGO retrain + the parity-gated sweep vs the json gem
207
+ bundle exec rake bench:fast # the sweep without retraining
208
+ bundle exec rake "bench:ips[twitter]" # multi-gem shoot-out (benchmark-ips); no args = full corpus
209
+ ```
210
+
211
+ ## License
212
+
213
+ MIT. The underlying Rust crate is `MIT AND BSL-1.0 AND Apache-2.0`; its
214
+ NOTICE file itemizes the derived components.
Binary file
Binary file
Binary file
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
data/lib/nosj.rb ADDED
@@ -0,0 +1,193 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "nosj/version"
4
+ require_relative "nosj/native"
5
+
6
+ # nosj is the evil twin of the +json+ gem: the same API, output bytes,
7
+ # option names, and error messages, backed by the Rust
8
+ # {https://github.com/yaroslav/nosj nosj} crate with SIMD-accelerated
9
+ # parsing and generation. Beyond the +json+ gem's surface it adds
10
+ # zero-allocation validation ({.valid?}) and partial parsing
11
+ # ({.dig}, {.at_pointer}, and their batch forms).
12
+ #
13
+ # Options arrive as a positional Hash (the +json+ gem's own calling
14
+ # convention); an explicit +**kwargs+ would allocate per call.
15
+ #
16
+ # @example The json gem API
17
+ # NOSJ.parse('{"a":[1,true]}') #=> {"a" => [1, true]}
18
+ # NOSJ.generate({"a" => [1, true]}) #=> '{"a":[1,true]}'
19
+ #
20
+ # @example Drop-in acceleration for the JSON module
21
+ # require "nosj/json"
22
+ # JSON.parse(src) # routed through nosj
23
+ module NOSJ
24
+ # Base class for nosj errors.
25
+ class Error < StandardError; end
26
+
27
+ # Raised when a value cannot be generated (non-finite floats without
28
+ # +allow_nan+, unsupported objects under +strict+, broken encodings).
29
+ # Message-compatible with +JSON::GeneratorError+.
30
+ class GeneratorError < Error; end
31
+
32
+ # Raised when generation exceeds +max_nesting+. Message-compatible
33
+ # with +JSON::NestingError+.
34
+ class NestingError < Error; end
35
+
36
+ PRETTY_GENERATE_OPTS = {
37
+ indent: " ", space: " ", object_nl: "\n", array_nl: "\n"
38
+ }.freeze
39
+ private_constant :PRETTY_GENERATE_OPTS
40
+
41
+ # Parses a JSON document, JSON.parse-compatible: same values, same
42
+ # option names, same behavior, byte-for-byte.
43
+ #
44
+ # The +json+ gem's legacy object-deserialization options
45
+ # (+object_class+, +array_class+, +decimal_class+,
46
+ # +create_additions+) are deliberately unsupported and raise
47
+ # ArgumentError.
48
+ #
49
+ # @example
50
+ # NOSJ.parse('{"a":[1,true]}') #=> {"a" => [1, true]}
51
+ # NOSJ.parse('{"a":1}', symbolize_names: true) #=> {a: 1}
52
+ #
53
+ # @param source [String] the JSON document (UTF-8 or US-ASCII)
54
+ # @param opts [Hash, nil] +symbolize_names+, +freeze+, +max_nesting+
55
+ # (Integer or +false+ for unlimited), +allow_nan+,
56
+ # +allow_trailing_comma+
57
+ # @return [Object] the parsed value tree
58
+ # @raise [RuntimeError] when the document is malformed or not UTF-8
59
+ # @raise [ArgumentError] for unsupported options
60
+ def self.parse(source, opts = nil)
61
+ parse_native(source, opts)
62
+ end
63
+
64
+ # @!method self.generate(obj, opts = nil)
65
+ # Generates JSON, JSON.generate-compatible: identical output bytes,
66
+ # including the gem's exact float formatting. Implemented natively
67
+ # (no Ruby forwarder frame; the definition lives in the extension).
68
+ #
69
+ # @example
70
+ # NOSJ.generate({"a" => [1, true]}) #=> '{"a":[1,true]}'
71
+ #
72
+ # @param obj [Object] the value tree to serialize
73
+ # @param opts [Hash, nil] +indent+, +space+, +space_before+,
74
+ # +object_nl+, +array_nl+, +max_nesting+ (Integer or +false+),
75
+ # +allow_nan+, +ascii_only+, +script_safe+ (alias +escape_slash+),
76
+ # +strict+, +depth+, +buffer_initial_length+
77
+ # @return [String] the JSON document
78
+ # @raise [GeneratorError] for non-finite floats without +allow_nan+,
79
+ # unsupported objects under +strict+, or broken string encodings
80
+ # @raise [NestingError] when nesting exceeds +max_nesting+
81
+
82
+ # Generates human-readable JSON, JSON.pretty_generate-compatible
83
+ # (two-space indent, newlines between elements). Options override the
84
+ # pretty defaults and are otherwise the same as {.generate}.
85
+ #
86
+ # @param obj [Object] the value tree to serialize
87
+ # @param opts [Hash, nil] see {.generate}
88
+ # @return [String] the pretty-printed JSON document
89
+ # @raise [GeneratorError] (see {.generate})
90
+ # @raise [NestingError] (see {.generate})
91
+ def self.pretty_generate(obj, opts = nil)
92
+ opts = opts.nil? ? PRETTY_GENERATE_OPTS : PRETTY_GENERATE_OPTS.merge(opts)
93
+ generate_native(obj, opts)
94
+ end
95
+
96
+ # Validates a document without building any Ruby values: the full
97
+ # parser (tokenizers, string decode, number validation) runs into a
98
+ # null sink, 1.8-2.5x faster than {.parse}.
99
+ #
100
+ # Returns true iff <code>NOSJ.parse(source, opts)</code> would
101
+ # succeed: parse refusals (malformed JSON, wrong encoding, too-deep
102
+ # nesting) are +false+, while option and argument-type errors still
103
+ # raise exactly like {.parse}.
104
+ #
105
+ # @example
106
+ # NOSJ.valid?('{"a":1}') #=> true
107
+ # NOSJ.valid?('{"a":}') #=> false
108
+ #
109
+ # @param source [String] the JSON document
110
+ # @param opts [Hash, nil] same options as {.parse}
111
+ # @return [Boolean]
112
+ # @raise [ArgumentError] for unsupported options
113
+ def self.valid?(source, opts = nil)
114
+ valid_native(source, opts)
115
+ end
116
+
117
+ # Partial parsing, Hash#dig-shaped: extracts one value from a JSON
118
+ # string without materializing the rest of the document. Skipped
119
+ # content is stepped over at SIMD block speed, so a lookup costs what
120
+ # it skips, not what the document weighs.
121
+ #
122
+ # @example
123
+ # NOSJ.dig(json, "users", 3, "name") #=> "grace" or nil
124
+ #
125
+ # @param source [String] the JSON document
126
+ # @param path [Array<String, Symbol, Integer>] object keys and array
127
+ # indices; unlike Array#dig, negative indices return +nil+ (JSON
128
+ # Pointer has no equivalent)
129
+ # @return [Object, nil] the matched value, or +nil+ when the path
130
+ # does not resolve
131
+ # @raise [ArgumentError] for path elements that are not Strings,
132
+ # Symbols, or Integers
133
+ def self.dig(source, *path)
134
+ dig_native(source, path)
135
+ end
136
+
137
+ # Batch {.dig}: many paths resolved in ONE pass over the document.
138
+ # The walk descends only into subtrees some path still needs, so a
139
+ # batch costs about as much as its single deepest lookup.
140
+ #
141
+ # On malformed documents a batch may raise where a single dig would
142
+ # return +nil+: one pass scans every byte some path needs.
143
+ #
144
+ # @example
145
+ # NOSJ.dig_many(json, [["users", 3, "name"], ["meta", "count"]])
146
+ # #=> ["grace", 42]
147
+ #
148
+ # @param source [String] the JSON document
149
+ # @param paths [Array<Array<String, Symbol, Integer>>] one dig path
150
+ # per result
151
+ # @param opts [Hash, nil] materialization options ({.parse}'s
152
+ # +symbolize_names+, +freeze+, ...)
153
+ # @return [Array<Object, nil>] positionally aligned with +paths+
154
+ # @raise [ArgumentError] for malformed paths
155
+ def self.dig_many(source, paths, opts = nil)
156
+ dig_many_native(source, paths, opts)
157
+ end
158
+
159
+ # Partial parsing by JSON Pointer (with the standard +~0+/+~1+
160
+ # escapes). The matched subtree materializes under the same options
161
+ # as {.parse}.
162
+ #
163
+ # @example
164
+ # NOSJ.at_pointer(json, "/users/3/name") #=> "grace" or nil
165
+ #
166
+ # @param source [String] the JSON document
167
+ # @param pointer [String] a JSON Pointer (empty string = whole
168
+ # document)
169
+ # @param opts [Hash, nil] materialization options
170
+ # @return [Object, nil] the matched value, or +nil+ when the pointer
171
+ # does not resolve
172
+ # @raise [ArgumentError] for a malformed pointer (non-empty without a
173
+ # leading +/+)
174
+ def self.at_pointer(source, pointer, opts = nil)
175
+ at_pointer_native(source, pointer, opts)
176
+ end
177
+
178
+ # Batch {.at_pointer}: the pointer-string counterpart of {.dig_many},
179
+ # resolving the whole set in one pass.
180
+ #
181
+ # @example
182
+ # NOSJ.at_pointers(json, ["/users/3/name", "/meta/count"])
183
+ # #=> ["grace", 42]
184
+ #
185
+ # @param source [String] the JSON document
186
+ # @param pointers [Array<String>] JSON Pointers, one per result
187
+ # @param opts [Hash, nil] materialization options
188
+ # @return [Array<Object, nil>] positionally aligned with +pointers+
189
+ # @raise [ArgumentError] for malformed pointers
190
+ def self.at_pointers(source, pointers, opts = nil)
191
+ at_pointers_native(source, pointers, opts)
192
+ end
193
+ end
data/sig/nosj.rbs ADDED
@@ -0,0 +1,61 @@
1
+ # Public interfaces only. The native layer (NOSJ.parse_native and
2
+ # friends) and the JSON drop-in's patched JSON singleton methods are
3
+ # implementation detail and deliberately unsigned.
4
+ module NOSJ
5
+ VERSION: String
6
+
7
+ # Any value JSON can represent, as Ruby objects: what parse returns
8
+ # and what partial parsing materializes. Object keys are String
9
+ # (Symbol with symbolize_names). generate accepts more than this
10
+ # alias (anything with to_json, unless strict), hence untyped there.
11
+ type value = nil | bool | Integer | Float | String
12
+ | Array[value] | Hash[String | Symbol, value]
13
+
14
+ # Options arrive as a positional Hash (the JSON gem's own calling
15
+ # convention; an explicit **kwargs would allocate per call), nil when
16
+ # omitted. Parse: symbolize_names, freeze, max_nesting, allow_nan,
17
+ # allow_trailing_comma. Generate: indent, space, space_before,
18
+ # object_nl, array_nl, max_nesting, allow_nan, ascii_only,
19
+ # script_safe/escape_slash, strict, depth, buffer_initial_length.
20
+ type opts = Hash[Symbol, untyped]?
21
+
22
+ # One NOSJ.dig path element (negative Integer indices resolve to nil).
23
+ type path_element = String | Symbol | Integer
24
+
25
+ class Error < StandardError
26
+ end
27
+
28
+ class GeneratorError < Error
29
+ end
30
+
31
+ class NestingError < Error
32
+ end
33
+
34
+ def self.parse: (String source, ?opts opts) -> value
35
+
36
+ def self.generate: (untyped obj, ?opts opts) -> String
37
+
38
+ def self.pretty_generate: (untyped obj, ?opts opts) -> String
39
+
40
+ def self.valid?: (String source, ?opts opts) -> bool
41
+
42
+ def self.dig: (String source, *path_element path) -> value
43
+
44
+ def self.dig_many: (String source, Array[Array[path_element]] paths, ?opts opts) -> Array[value]
45
+
46
+ def self.at_pointer: (String source, String pointer, ?opts opts) -> value
47
+
48
+ def self.at_pointers: (String source, Array[String] pointers, ?opts opts) -> Array[value]
49
+
50
+ # Defined by `require "nosj/multi_json"`. The runtime superclass is
51
+ # multi_json's Adapter (whose namespace differs across multi_json
52
+ # versions), so it is not declared here.
53
+ class MultiJsonAdapter
54
+ # multi_json wraps whatever the adapter's ParseError names.
55
+ ParseError: singleton(::JSON::ParserError)
56
+
57
+ def load: (String string, ?Hash[Symbol, untyped] options) -> value
58
+
59
+ def dump: (untyped object, ?Hash[Symbol, untyped] options) -> String
60
+ end
61
+ end
metadata ADDED
@@ -0,0 +1,67 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nosj
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: x86_64-linux
6
+ authors:
7
+ - Yaroslav Markin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies: []
12
+ description: 'gem nosj is an extremely fast, json-gem-compatible JSON parser and generator
13
+ for Ruby: Rust and SIMD via the first-party nosj crate, precompiled platform gems
14
+ with per-platform PGO, partial parsing (JSON Pointer, single and batch), allocation-free
15
+ validation, and a one-line JSON module drop-in.'
16
+ email:
17
+ - yaroslav@markin.net
18
+ executables: []
19
+ extensions: []
20
+ extra_rdoc_files: []
21
+ files:
22
+ - CHANGELOG.md
23
+ - LICENSE.txt
24
+ - NOTICE
25
+ - README.md
26
+ - lib/nosj.rb
27
+ - lib/nosj/3.3/nosj.so
28
+ - lib/nosj/3.4/nosj.so
29
+ - lib/nosj/4.0/nosj.so
30
+ - lib/nosj/json.rb
31
+ - lib/nosj/multi_json.rb
32
+ - lib/nosj/native.rb
33
+ - lib/nosj/version.rb
34
+ - sig/nosj.rbs
35
+ homepage: https://github.com/yaroslav/nosj-ruby
36
+ licenses:
37
+ - MIT
38
+ metadata:
39
+ allowed_push_host: https://rubygems.org
40
+ homepage_uri: https://github.com/yaroslav/nosj-ruby
41
+ source_code_uri: https://github.com/yaroslav/nosj-ruby
42
+ changelog_uri: https://github.com/yaroslav/nosj-ruby/blob/main/CHANGELOG.md
43
+ bug_tracker_uri: https://github.com/yaroslav/nosj-ruby/issues
44
+ documentation_uri: https://rubydoc.info/gems/nosj
45
+ rubygems_mfa_required: 'true'
46
+ rdoc_options: []
47
+ require_paths:
48
+ - lib
49
+ required_ruby_version: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: '3.3'
54
+ - - "<"
55
+ - !ruby/object:Gem::Version
56
+ version: 4.1.dev
57
+ required_rubygems_version: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: 3.3.11
62
+ requirements: []
63
+ rubygems_version: 4.0.16
64
+ specification_version: 4
65
+ summary: An extremely fast JSON parser and generator for Ruby, written in Rust and
66
+ SIMD-accelerated on every platform.
67
+ test_files: []