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.
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,92 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: nosj
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Yaroslav Markin
8
+ bindir: bin
9
+ cert_chain: []
10
+ date: 1980-01-02 00:00:00.000000000 Z
11
+ dependencies:
12
+ - !ruby/object:Gem::Dependency
13
+ name: rb_sys
14
+ requirement: !ruby/object:Gem::Requirement
15
+ requirements:
16
+ - - "~>"
17
+ - !ruby/object:Gem::Version
18
+ version: 0.9.91
19
+ type: :runtime
20
+ prerelease: false
21
+ version_requirements: !ruby/object:Gem::Requirement
22
+ requirements:
23
+ - - "~>"
24
+ - !ruby/object:Gem::Version
25
+ version: 0.9.91
26
+ description: 'gem nosj is an extremely fast, json-gem-compatible JSON parser and generator
27
+ for Ruby: Rust and SIMD via the first-party nosj crate, precompiled platform gems
28
+ with per-platform PGO, partial parsing (JSON Pointer, single and batch), allocation-free
29
+ validation, and a one-line JSON module drop-in.'
30
+ email:
31
+ - yaroslav@markin.net
32
+ executables: []
33
+ extensions:
34
+ - ext/nosj/extconf.rb
35
+ extra_rdoc_files: []
36
+ files:
37
+ - CHANGELOG.md
38
+ - Cargo.lock
39
+ - Cargo.toml
40
+ - LICENSE.txt
41
+ - NOTICE
42
+ - README.md
43
+ - ext/nosj/Cargo.toml
44
+ - ext/nosj/extconf.rb
45
+ - ext/nosj/src/gen/errors.rs
46
+ - ext/nosj/src/gen/hash_iter.rs
47
+ - ext/nosj/src/gen/keys.rs
48
+ - ext/nosj/src/gen/mod.rs
49
+ - ext/nosj/src/gen/opts.rs
50
+ - ext/nosj/src/gen/ruby.rs
51
+ - ext/nosj/src/gen/walker.rs
52
+ - ext/nosj/src/lib.rs
53
+ - ext/nosj/src/parse.rs
54
+ - ext/nosj/src/pointer.rs
55
+ - ext/nosj/src/sink.rs
56
+ - ext/nosj/src/state.rs
57
+ - lib/nosj.rb
58
+ - lib/nosj/json.rb
59
+ - lib/nosj/multi_json.rb
60
+ - lib/nosj/native.rb
61
+ - lib/nosj/version.rb
62
+ - sig/nosj.rbs
63
+ homepage: https://github.com/yaroslav/nosj-ruby
64
+ licenses:
65
+ - MIT
66
+ metadata:
67
+ allowed_push_host: https://rubygems.org
68
+ homepage_uri: https://github.com/yaroslav/nosj-ruby
69
+ source_code_uri: https://github.com/yaroslav/nosj-ruby
70
+ changelog_uri: https://github.com/yaroslav/nosj-ruby/blob/main/CHANGELOG.md
71
+ bug_tracker_uri: https://github.com/yaroslav/nosj-ruby/issues
72
+ documentation_uri: https://rubydoc.info/gems/nosj
73
+ rubygems_mfa_required: 'true'
74
+ rdoc_options: []
75
+ require_paths:
76
+ - lib
77
+ required_ruby_version: !ruby/object:Gem::Requirement
78
+ requirements:
79
+ - - ">="
80
+ - !ruby/object:Gem::Version
81
+ version: 3.3.0
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: 3.3.11
87
+ requirements: []
88
+ rubygems_version: 4.0.16
89
+ specification_version: 4
90
+ summary: An extremely fast JSON parser and generator for Ruby, written in Rust and
91
+ SIMD-accelerated on every platform.
92
+ test_files: []