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.
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
@@ -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 RuntimeError => e
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
@@ -2,5 +2,5 @@
2
2
 
3
3
  module NOSJ
4
4
  # The gem version.
5
- VERSION = "0.1.0"
5
+ VERSION = "0.3.0"
6
6
  end