nosj 0.1.0 → 0.3.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 +110 -1
- data/Cargo.lock +13 -3
- data/README.md +272 -24
- data/ext/nosj/Cargo.toml +5 -2
- data/ext/nosj/src/errors.rs +171 -0
- data/ext/nosj/src/files.rs +240 -0
- data/ext/nosj/src/gen/errors.rs +2 -9
- data/ext/nosj/src/gen/mod.rs +198 -20
- data/ext/nosj/src/gen/opts.rs +87 -9
- data/ext/nosj/src/gen/ruby.rs +51 -0
- data/ext/nosj/src/gen/walker.rs +97 -59
- data/ext/nosj/src/lazy.rs +422 -0
- data/ext/nosj/src/lib.rs +60 -0
- data/ext/nosj/src/lines.rs +90 -0
- data/ext/nosj/src/parse.rs +50 -14
- data/ext/nosj/src/patch.rs +547 -0
- data/ext/nosj/src/pointer.rs +26 -17
- data/ext/nosj/src/reformat.rs +301 -0
- data/ext/nosj/src/sink.rs +6 -0
- data/ext/nosj/src/stats.rs +301 -0
- data/lib/nosj/json.rb +30 -6
- data/lib/nosj/lazy.rb +165 -0
- data/lib/nosj/multi_json.rb +1 -1
- data/lib/nosj/rails.rb +76 -0
- data/lib/nosj/version.rb +1 -1
- data/lib/nosj.rb +434 -5
- data/sig/nosj.rbs +120 -0
- metadata +16 -3
data/lib/nosj/lazy.rb
ADDED
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module NOSJ
|
|
4
|
+
# A lazy view of one JSON container inside a document created with
|
|
5
|
+
# {NOSJ.lazy}. Nothing is parsed until it is touched: indexing walks
|
|
6
|
+
# the raw bytes to the requested child (skipping siblings at SIMD
|
|
7
|
+
# block speed), returns another Lazy node for containers, and
|
|
8
|
+
# materializes plain Ruby values for scalars. Resolved children are
|
|
9
|
+
# cached, so repeated access is free and object identity is stable.
|
|
10
|
+
#
|
|
11
|
+
# Validation is as-you-go: skipped content is bracket-balance
|
|
12
|
+
# checked and resolved targets fully validated, so a malformed region
|
|
13
|
+
# raises when an access first walks it, not at {NOSJ.lazy} time.
|
|
14
|
+
#
|
|
15
|
+
# Nodes hold their own stable copy of the document (shared across the
|
|
16
|
+
# whole node tree), so mutating the source string later is harmless.
|
|
17
|
+
#
|
|
18
|
+
# @example Pull two fields out of a big document
|
|
19
|
+
# doc = NOSJ.lazy(huge_json)
|
|
20
|
+
# doc["users"][3]["name"] # only this path is parsed
|
|
21
|
+
# doc.dig("meta", "count")
|
|
22
|
+
#
|
|
23
|
+
# @example Materialize a subtree
|
|
24
|
+
# doc["users"][3].value #=> {"name" => ..., ...}
|
|
25
|
+
class Lazy
|
|
26
|
+
include Enumerable
|
|
27
|
+
|
|
28
|
+
# Resolves one child. String and Symbol keys index objects; Integer
|
|
29
|
+
# indices index arrays (negative indices resolve to +nil+, as in
|
|
30
|
+
# {NOSJ.dig}). Containers come back as further Lazy nodes, scalars
|
|
31
|
+
# as plain Ruby values, misses as +nil+. Results are cached on the
|
|
32
|
+
# node.
|
|
33
|
+
#
|
|
34
|
+
# @param token [String, Symbol, Integer]
|
|
35
|
+
# @return [NOSJ::Lazy, Object, nil]
|
|
36
|
+
def [](token)
|
|
37
|
+
cache = (@children ||= {})
|
|
38
|
+
cache.fetch(token) { cache[token] = __get(token) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Hash#dig-shaped access, fused into a single resolution: the whole
|
|
42
|
+
# path resolves in one walk of this node's bytes, no intermediate
|
|
43
|
+
# nodes. Semantics match {NOSJ.dig}: misses, negative indices, and
|
|
44
|
+
# steps into scalars resolve to +nil+. Not cached.
|
|
45
|
+
#
|
|
46
|
+
# @param path [Array<String, Symbol, Integer>]
|
|
47
|
+
# @return [NOSJ::Lazy, Object, nil]
|
|
48
|
+
def dig(first, *rest)
|
|
49
|
+
__dig([first, *rest])
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Resolves an RFC 6901 JSON Pointer within this node's subtree,
|
|
53
|
+
# with the same lazy/materialize behavior as {#[]}. Not cached.
|
|
54
|
+
#
|
|
55
|
+
# @param pointer [String] e.g. <tt>"/users/3/name"</tt>
|
|
56
|
+
# @return [NOSJ::Lazy, Object, nil]
|
|
57
|
+
def at_pointer(pointer)
|
|
58
|
+
__at_pointer(pointer)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
# Materializes this node's whole subtree as plain Ruby values,
|
|
62
|
+
# under the options given to {NOSJ.lazy} (+symbolize_names+,
|
|
63
|
+
# +freeze+, ...).
|
|
64
|
+
#
|
|
65
|
+
# @return [Hash, Array]
|
|
66
|
+
def value
|
|
67
|
+
__materialize
|
|
68
|
+
end
|
|
69
|
+
alias_method :materialize, :value
|
|
70
|
+
|
|
71
|
+
# @return [Hash] the materialized subtree
|
|
72
|
+
# @raise [TypeError] on an array node
|
|
73
|
+
def to_h
|
|
74
|
+
raise TypeError, "to_h on a JSON array" unless object?
|
|
75
|
+
|
|
76
|
+
__materialize
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# @return [Array] the materialized subtree
|
|
80
|
+
# @raise [TypeError] on an object node
|
|
81
|
+
def to_a
|
|
82
|
+
raise TypeError, "to_a on a JSON object" unless array?
|
|
83
|
+
|
|
84
|
+
__materialize
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# @return [Boolean] whether this node is a JSON object
|
|
88
|
+
def object?
|
|
89
|
+
__kind == :object
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# @return [Boolean] whether this node is a JSON array
|
|
93
|
+
def array?
|
|
94
|
+
__kind == :array
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
# The object's keys (always Strings), read in one walk without
|
|
98
|
+
# materializing any values.
|
|
99
|
+
#
|
|
100
|
+
# @return [Array<String>]
|
|
101
|
+
# @raise [TypeError] on an array node
|
|
102
|
+
def keys
|
|
103
|
+
__keys
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
# Entry count (object pairs or array elements), read in one walk
|
|
107
|
+
# without materializing anything.
|
|
108
|
+
#
|
|
109
|
+
# @return [Integer]
|
|
110
|
+
def size
|
|
111
|
+
__size
|
|
112
|
+
end
|
|
113
|
+
alias_method :length, :size
|
|
114
|
+
alias_method :count, :size
|
|
115
|
+
|
|
116
|
+
# @return [Boolean]
|
|
117
|
+
def empty?
|
|
118
|
+
size.zero?
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
# Iterates direct children, resolved in one walk: object nodes
|
|
122
|
+
# yield +[key, child]+ pairs (like Hash), array nodes yield
|
|
123
|
+
# children. Containers arrive as Lazy nodes, scalars as values.
|
|
124
|
+
#
|
|
125
|
+
# @return [Enumerator] when no block is given
|
|
126
|
+
def each(&block)
|
|
127
|
+
return enum_for(:each) unless block
|
|
128
|
+
|
|
129
|
+
if object?
|
|
130
|
+
__children.each { |pair| yield pair }
|
|
131
|
+
else
|
|
132
|
+
__children.each { |child| yield child }
|
|
133
|
+
end
|
|
134
|
+
self
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
# @return [String] kind and span size, without touching content
|
|
138
|
+
def inspect
|
|
139
|
+
"#<NOSJ::Lazy #{__kind} (#{__byte_size} bytes)>"
|
|
140
|
+
end
|
|
141
|
+
alias_method :to_s, :inspect
|
|
142
|
+
|
|
143
|
+
private :__get, :__dig, :__at_pointer, :__materialize, :__kind,
|
|
144
|
+
:__byte_size, :__keys, :__size, :__children
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# Wraps a JSON document for lazy, on-demand access: returns a
|
|
148
|
+
# {NOSJ::Lazy} node for a container root, or the materialized value
|
|
149
|
+
# for a scalar root. The document bytes are copied once; no parsing
|
|
150
|
+
# happens beyond locating the root value.
|
|
151
|
+
#
|
|
152
|
+
# @example
|
|
153
|
+
# doc = NOSJ.lazy('{"users":[{"name":"ada"},{"name":"grace"}]}')
|
|
154
|
+
# doc["users"][1]["name"] #=> "grace" — the rest is never parsed
|
|
155
|
+
#
|
|
156
|
+
# @param source [String] the JSON document (UTF-8 or US-ASCII)
|
|
157
|
+
# @param opts [Hash, nil] {NOSJ.parse} options applied whenever a
|
|
158
|
+
# value materializes: +symbolize_names+, +freeze+, +max_nesting+,
|
|
159
|
+
# +allow_nan+, +allow_trailing_comma+
|
|
160
|
+
# @return [NOSJ::Lazy, Object]
|
|
161
|
+
# @raise [RuntimeError] when the document root is malformed
|
|
162
|
+
def self.lazy(source, opts = nil)
|
|
163
|
+
lazy_native(source, opts)
|
|
164
|
+
end
|
|
165
|
+
end
|
data/lib/nosj/multi_json.rb
CHANGED
|
@@ -36,7 +36,7 @@ module NOSJ
|
|
|
36
36
|
# @raise [JSON::ParserError] when the document is malformed
|
|
37
37
|
def load(string, options = {})
|
|
38
38
|
::NOSJ.parse(string, options[:symbolize_names] ? SYMBOLIZE : nil)
|
|
39
|
-
rescue
|
|
39
|
+
rescue ::NOSJ::ParserError, ::NOSJ::NestingError => e
|
|
40
40
|
raise ParseError, e.message
|
|
41
41
|
end
|
|
42
42
|
|
data/lib/nosj/rails.rb
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Rails mode: `require "nosj/rails"` accelerates a Rails application in
|
|
4
|
+
# both directions.
|
|
5
|
+
#
|
|
6
|
+
# - It installs {NOSJ::RailsEncoder} as ActiveSupport's JSON encoder
|
|
7
|
+
# (the official +ActiveSupport::JSON::Encoding.json_encoder+ seam, the
|
|
8
|
+
# same one Oj's Rails mode uses). That captures every encode a Rails
|
|
9
|
+
# app performs through +obj.to_json+, +render json:+, and
|
|
10
|
+
# +ActiveSupport::JSON.encode+: the object tree is walked natively,
|
|
11
|
+
# values that are not JSON-native recurse through +as_json+ exactly
|
|
12
|
+
# like ActiveSupport's own encoder, and non-finite floats encode as
|
|
13
|
+
# +null+ (+Float#as_json+ parity).
|
|
14
|
+
# - It loads the `nosj/json` drop-in, so +JSON.parse+ (and with it
|
|
15
|
+
# +ActiveSupport::JSON.decode+ and JSON request-body parsing) takes
|
|
16
|
+
# the fast path.
|
|
17
|
+
#
|
|
18
|
+
# In a Rails Gemfile:
|
|
19
|
+
#
|
|
20
|
+
# gem "nosj", require: "nosj/rails"
|
|
21
|
+
#
|
|
22
|
+
# +JSON::Fragment+ values splice their pre-rendered JSON, like current
|
|
23
|
+
# ActiveSupport. On older ActiveSupport (before its encoder learned
|
|
24
|
+
# about fragments) stock encoding dumps the fragment's instance
|
|
25
|
+
# variables instead; there this encoder deliberately diverges in favor
|
|
26
|
+
# of real splicing.
|
|
27
|
+
|
|
28
|
+
require "nosj/json"
|
|
29
|
+
require "active_support"
|
|
30
|
+
require "active_support/json"
|
|
31
|
+
|
|
32
|
+
module NOSJ
|
|
33
|
+
# ActiveSupport JSON encoder backed by nosj: the interface of
|
|
34
|
+
# ActiveSupport's +JSONGemEncoder+ (accepts the options hash, encodes
|
|
35
|
+
# one value), with the tree walk, generation, AND the HTML/JS-safety
|
|
36
|
+
# escape pass running natively (a byte scan instead of ActiveSupport's
|
|
37
|
+
# Ruby regex post-pass), honoring +escape_html_entities_in_json+ (and,
|
|
38
|
+
# where present, +escape_js_separators_in_json+ and the per-call
|
|
39
|
+
# +escape:+ / +escape_html_entities:+ options).
|
|
40
|
+
class RailsEncoder
|
|
41
|
+
# The escape_js_separators_in_json knob is newer than the Rails
|
|
42
|
+
# versions we support; its presence is fixed at load time (only its
|
|
43
|
+
# value can change at runtime). Absent, ActiveSupport always
|
|
44
|
+
# escapes the JS separators.
|
|
45
|
+
HAS_JS_SEPARATORS_KNOB =
|
|
46
|
+
ActiveSupport::JSON::Encoding.respond_to?(:escape_js_separators_in_json)
|
|
47
|
+
private_constant :HAS_JS_SEPARATORS_KNOB
|
|
48
|
+
|
|
49
|
+
attr_reader :options
|
|
50
|
+
|
|
51
|
+
def initialize(options = nil)
|
|
52
|
+
@options = options || {}
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def encode(value)
|
|
56
|
+
value = value.as_json(@options.dup) unless @options.empty?
|
|
57
|
+
if @options.fetch(:escape, true)
|
|
58
|
+
escape_html = @options.fetch(:escape_html_entities) do
|
|
59
|
+
ActiveSupport::JSON::Encoding.escape_html_entities_in_json
|
|
60
|
+
end
|
|
61
|
+
NOSJ.generate_rails_native(value, !!escape_html, escape_js_separators?)
|
|
62
|
+
else
|
|
63
|
+
NOSJ.generate_rails_native(value, false, false)
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
private
|
|
68
|
+
|
|
69
|
+
def escape_js_separators?
|
|
70
|
+
!HAS_JS_SEPARATORS_KNOB ||
|
|
71
|
+
ActiveSupport::JSON::Encoding.escape_js_separators_in_json
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
ActiveSupport::JSON::Encoding.json_encoder = RailsEncoder
|
|
76
|
+
end
|
data/lib/nosj/version.rb
CHANGED