Module: NOSJ

Defined in:
lib/nosj.rb,
lib/nosj/json.rb,
lib/nosj/lazy.rb,
lib/nosj/rails.rb,
lib/nosj/version.rb,
lib/nosj/multi_json.rb,
sig/nosj.rbs

Overview

Public interfaces only. The native layer (NOSJ.parse_native and friends) and the JSON drop-in's patched JSON singleton methods are implementation detail and deliberately unsigned.

Defined Under Namespace

Modules: JSONDropIn Classes: Error, GeneratorError, Lazy, MultiJsonAdapter, NestingError, ParserError, PatchError, RailsEncoder

Constant Summary collapse

VERSION =

The gem version.

Returns:

  • (String)
"0.3.0"

Class Method Summary collapse

Class Method Details

.at_pointer(source, pointer, opts = nil) ⇒ Object?

Partial parsing by JSON Pointer (with the standard +~0+/+~1+ escapes). The matched subtree materializes under the same options as parse.

Examples:

NOSJ.at_pointer(json, "/users/3/name")  #=> "grace" or nil

Parameters:

  • source (String)

    the JSON document

  • pointer (String)

    a JSON Pointer (empty string = whole document)

  • opts (Hash, nil) (defaults to: nil)

    materialization options

Returns:

  • (Object, nil)

    the matched value, or nil when the pointer does not resolve

Raises:

  • (ArgumentError)

    for a malformed pointer (non-empty without a leading /)



221
222
223
# File 'lib/nosj.rb', line 221

def self.at_pointer(source, pointer, opts = nil)
  at_pointer_native(source, pointer, opts)
end

.at_pointer_file(path, pointer, opts = nil) ⇒ Object?

at_pointer against a file: memory-maps it, resolves the pointer, materializes only the matched subtree, and never reads the rest into Ruby.

Examples:

NOSJ.at_pointer_file("huge.json", "/users/3/name")

Parameters:

  • path (String)

    the file to query (UTF-8)

  • pointer (String)

    an RFC 6901 JSON Pointer

  • opts (Hash, nil) (defaults to: nil)

    materialization options

Returns:

  • (Object, nil)

    nil when the pointer misses

Raises:

  • (ArgumentError)

    for malformed pointers



305
306
307
# File 'lib/nosj.rb', line 305

def self.at_pointer_file(path, pointer, opts = nil)
  at_pointer_file_native(path, pointer, opts)
end

.at_pointers(source, pointers, opts = nil) ⇒ Array<Object, nil>

Batch at_pointer: the pointer-string counterpart of dig_many, resolving the whole set in one pass.

Examples:

NOSJ.at_pointers(json, ["/users/3/name", "/meta/count"])
#=> ["grace", 42]

Parameters:

  • source (String)

    the JSON document

  • pointers (Array<String>)

    JSON Pointers, one per result

  • opts (Hash, nil) (defaults to: nil)

    materialization options

Returns:

  • (Array<Object, nil>)

    positionally aligned with pointers

Raises:

  • (ArgumentError)

    for malformed pointers



237
238
239
# File 'lib/nosj.rb', line 237

def self.at_pointers(source, pointers, opts = nil)
  at_pointers_native(source, pointers, opts)
end

.dig(source, *path) ⇒ Object?

Partial parsing, Hash#dig-shaped: extracts one value from a JSON string without materializing the rest of the document. Skipped content is stepped over at SIMD block speed, so a lookup costs what it skips, not what the document weighs.

Examples:

NOSJ.dig(json, "users", 3, "name")  #=> "grace" or nil

Parameters:

  • source (String)

    the JSON document

  • path (Array<String, Symbol, Integer>)

    object keys and array indices; unlike Array#dig, negative indices return nil (JSON Pointer has no equivalent)

Returns:

  • (Object, nil)

    the matched value, or nil when the path does not resolve

Raises:

  • (ArgumentError)

    for path elements that are not Strings, Symbols, or Integers



180
181
182
# File 'lib/nosj.rb', line 180

def self.dig(source, *path)
  dig_native(source, path)
end

.dig_file(path, *path_elements) ⇒ Object?

dig against a file: the Hash#dig-shaped counterpart of at_pointer_file. Negative indices resolve to nil.

Examples:

NOSJ.dig_file("huge.json", "users", 3, "name")

Parameters:

  • path (String)

    the file to query (UTF-8)

  • path_elements (Array<String, Symbol, Integer>)

Returns:

  • (Object, nil)


318
319
320
# File 'lib/nosj.rb', line 318

def self.dig_file(path, *path_elements)
  dig_file_native(path, path_elements)
end

.dig_many(source, paths, opts = nil) ⇒ Array<Object, nil>

Batch dig: many paths resolved in ONE pass over the document. The walk descends only into subtrees some path still needs, so a batch costs about as much as its single deepest lookup.

On malformed documents a batch may raise where a single dig would return nil: one pass scans every byte some path needs.

Examples:

NOSJ.dig_many(json, [["users", 3, "name"], ["meta", "count"]])
#=> ["grace", 42]

Parameters:

  • source (String)

    the JSON document

  • paths (Array<Array<String, Symbol, Integer>>)

    one dig path per result

  • opts (Hash, nil) (defaults to: nil)

    materialization options (parse's symbolize_names, freeze, ...)

Returns:

  • (Array<Object, nil>)

    positionally aligned with paths

Raises:

  • (ArgumentError)

    for malformed paths



202
203
204
# File 'lib/nosj.rb', line 202

def self.dig_many(source, paths, opts = nil)
  dig_many_native(source, paths, opts)
end

.each_line(source, opts) ⇒ nil .each_line(source, opts) ⇒ Enumerator[value, nil]

NDJSON / JSON Lines: one parsed value per non-blank line.

Overloads:

  • .each_line(source, opts) ⇒ nil

    Parameters:

    • source (String)
    • opts (opts)

    Returns:

    • (nil)
  • .each_line(source, opts) ⇒ Enumerator[value, nil]

    Parameters:

    • source (String)
    • opts (opts)

    Returns:

    • (Enumerator[value, nil])

Yields:

Yield Parameters:

  • arg0 (value)

Yield Returns:

  • (void)


500
501
502
503
# File 'lib/nosj.rb', line 500

def self.each_line(source, opts = nil, &block)
  return enum_for(:each_line, source, opts) unless block
  each_line_native(source, opts, &block)
end

.each_line_file(path, opts) ⇒ nil .each_line_file(path, opts) ⇒ Enumerator[value, nil]

each_line against a file: the NDJSON stream is walked over a read-only memory map, so the file never becomes a Ruby String.

Examples:

NOSJ.each_line_file("events.ndjson") { |event| ingest(event) }

Overloads:

  • .each_line_file(path, opts) ⇒ nil

    Parameters:

    • path (String)
    • opts (opts)

    Returns:

    • (nil)
  • .each_line_file(path, opts) ⇒ Enumerator[value, nil]

    Parameters:

    • path (String)
    • opts (opts)

    Returns:

    • (Enumerator[value, nil])

Parameters:

  • path (String)

    the NDJSON file (UTF-8)

  • opts (Hash, nil) (defaults to: nil)

    the same options as parse, applied per line

Yields:

Yield Parameters:

  • value (Object)

    one parsed document per non-blank line

  • arg0 (value)

Yield Returns:

  • (void)

Returns:

  • (Enumerator)

    when no block is given, else nil

Raises:

  • (SystemCallError)

    Errno::ENOENT and friends

  • (ParserError)

    on the first malformed line



517
518
519
520
# File 'lib/nosj.rb', line 517

def self.each_line_file(path, opts = nil, &block)
  return enum_for(:each_line_file, path, opts) unless block
  each_line_file_native(path, opts, &block)
end

.generate(obj, opts = nil) ⇒ String

Generates JSON, JSON.generate-compatible: identical output bytes, including the gem's exact float formatting. Implemented natively (no Ruby forwarder frame; the definition lives in the extension).

Examples:

NOSJ.generate({"a" => [1, true]})  #=> '{"a":[1,true]}'

Parameters:

  • obj (Object)

    the value tree to serialize

  • opts (Hash, nil) (defaults to: nil)

    indent, space, space_before, object_nl, array_nl, max_nesting (Integer or false), allow_nan, ascii_only, script_safe (alias escape_slash), strict, depth, buffer_initial_length

Returns:

  • (String)

    the JSON document

Raises:

  • (GeneratorError)

    for non-finite floats without allow_nan, unsupported objects under strict, or broken string encodings

  • (NestingError)

    when nesting exceeds max_nesting



# File 'lib/nosj.rb', line 111

.generate_lines(values, opts = nil) ⇒ String

Generates NDJSON / JSON Lines: one compact document per element, each terminated with a newline, built in a single pass into one buffer. Formatting options containing newlines raise ArgumentError (they would break the line framing); everything else from generate applies.

Examples:

NOSJ.generate_lines([{a: 1}, {b: 2}])  #=> %({"a":1}\n{"b":2}\n)

Parameters:

  • values (Array, Enumerable)

    one document per element

  • opts (Hash, nil) (defaults to: nil)

    the same options as generate

Returns:

  • (String)

    the NDJSON document (empty when values is empty)

Raises:

  • (ArgumentError)

    for formatting options that contain newlines



536
537
538
# File 'lib/nosj.rb', line 536

def self.generate_lines(values, opts = nil)
  generate_lines_native(lines_array(values), opts)
end

.lazy(source, opts = nil) ⇒ NOSJ::Lazy, Object

Wraps a JSON document for lazy, on-demand access: returns a Lazy node for a container root, or the materialized value for a scalar root. The document bytes are copied once; no parsing happens beyond locating the root value.

Examples:

doc = NOSJ.lazy('{"users":[{"name":"ada"},{"name":"grace"}]}')
doc["users"][1]["name"]  #=> "grace" — the rest is never parsed

Parameters:

  • source (String)

    the JSON document (UTF-8 or US-ASCII)

  • opts (Hash, nil) (defaults to: nil)

    parse options applied whenever a value materializes: symbolize_names, freeze, max_nesting, allow_nan, allow_trailing_comma

Returns:

Raises:

  • (RuntimeError)

    when the document root is malformed



162
163
164
# File 'lib/nosj/lazy.rb', line 162

def self.lazy(source, opts = nil)
  lazy_native(source, opts)
end

.load_file(path, opts = nil) ⇒ Object

Parses a JSON file, like +JSON.load_file+—except the file is read natively into a reused buffer, so no file-sized Ruby String is ever created (or garbage-collected).

Examples:

NOSJ.load_file("config.json", symbolize_names: true)

Parameters:

  • path (String)

    the file to parse (UTF-8)

  • opts (Hash, nil) (defaults to: nil)

    the same options as parse

Returns:

  • (Object)

    the parsed value tree

Raises:

  • (SystemCallError)

    Errno::ENOENT and friends, like File.read

  • (ParserError)

    when the document is malformed or not UTF-8



253
254
255
# File 'lib/nosj.rb', line 253

def self.load_file(path, opts = nil)
  load_file_native(path, opts)
end

.load_lazy_file(path, opts = nil) ⇒ NOSJ::Lazy, Object

Wraps a JSON file as a lazy document (lazy for files): the file is memory-mapped read-only, so beyond one sequential UTF-8 check, pages you never read are never loaded from disk. The mapping lives as long as any node on it; the file must not be modified while it is in use.

Examples:

doc = NOSJ.load_lazy_file("huge.json")
doc["users"][3]["name"]   # touches only these pages

Parameters:

  • path (String)

    the file to wrap (UTF-8)

  • opts (Hash, nil) (defaults to: nil)

    parse options applied on materialization

Returns:

Raises:

  • (SystemCallError)

    Errno::ENOENT and friends

  • (ParserError)

    when the file is not UTF-8 or the root is malformed



289
290
291
# File 'lib/nosj.rb', line 289

def self.load_lazy_file(path, opts = nil)
  load_lazy_file_native(path, opts)
end

.merge_patch(json, patch, opts = nil) ⇒ String

RFC 7386 JSON Merge Patch (semantic: parse, merge, generate).

Parameters:

  • json (String)
  • patch (Object)
  • opts (opts) (defaults to: nil)

Returns:

  • (String)


458
459
460
461
# File 'lib/nosj.rb', line 458

def self.merge_patch(json, patch, opts = nil)
  return generate(patch, opts) unless patch.is_a?(Hash)
  generate(merge_patch_value(parse(json), patch), opts)
end

.minify(json, opts = nil) ⇒ String

Reformat without building values: parser events pipe straight into the emission kernels. minify is compact; reformat takes pretty: and the generate formatting/escape options.

Parameters:

  • json (String)
  • opts (opts) (defaults to: nil)

Returns:

  • (String)


340
341
342
# File 'lib/nosj.rb', line 340

def self.minify(json, opts = nil)
  reformat_native(json, opts)
end

.parse(source, opts = nil) ⇒ Object

Parses a JSON document, JSON.parse-compatible: same values, same option names, same behavior, byte-for-byte.

The json gem's legacy object-deserialization options (+object_class+, array_class, decimal_class, create_additions) are deliberately unsupported and raise ArgumentError.

Examples:

NOSJ.parse('{"a":[1,true]}')                      #=> {"a" => [1, true]}
NOSJ.parse('{"a":1}', symbolize_names: true)      #=> {a: 1}

Parameters:

  • source (String)

    the JSON document (UTF-8 or US-ASCII)

  • opts (Hash, nil) (defaults to: nil)

    symbolize_names, freeze, max_nesting (Integer or false for unlimited), allow_nan, allow_trailing_comma

Returns:

  • (Object)

    the parsed value tree

Raises:

  • (ParserError)

    when the document is malformed or not UTF-8; carries the failure position (NOSJ::ParserError#line and friends)

  • (NestingError)

    when nesting exceeds max_nesting

  • (ArgumentError)

    for unsupported options



107
108
109
# File 'lib/nosj.rb', line 107

def self.parse(source, opts = nil)
  parse_native(source, opts)
end

.patch(json, ops, opts = nil) ⇒ String

RFC 6902 JSON Patch over the raw document.

Parameters:

  • json (String)
  • ops (Array[Hash[untyped, untyped]])
  • opts (opts) (defaults to: nil)

Returns:

  • (String)


439
440
441
# File 'lib/nosj.rb', line 439

def self.patch(json, ops, opts = nil)
  patch_native(json, ops, opts)
end

.pretty_generate(obj, opts = nil) ⇒ String

Generates human-readable JSON, JSON.pretty_generate-compatible (two-space indent, newlines between elements). Options override the pretty defaults and are otherwise the same as generate.

Parameters:

  • obj (Object)

    the value tree to serialize

  • opts (Hash, nil) (defaults to: nil)

Returns:

  • (String)

    the pretty-printed JSON document



138
139
140
141
# File 'lib/nosj.rb', line 138

def self.pretty_generate(obj, opts = nil)
  opts = opts.nil? ? PRETTY_GENERATE_OPTS : PRETTY_GENERATE_OPTS.merge(opts)
  generate_native(obj, opts)
end

.reformat(json, opts = nil) ⇒ String

Reformats a document without building any Ruby values: minify's pipe with formatting. pretty: true is a shorthand for pretty_generate's layout; the individual generate formatting and escape options (+indent+, space, object_nl, ascii_only, script_safe, ...) compose with it and win over it.

Examples:

NOSJ.reformat(json, pretty: true)
NOSJ.reformat(json, ascii_only: true)   # escape-transcode, compact

Parameters:

  • json (String)

    the document (UTF-8 or US-ASCII)

  • opts (Hash, nil) (defaults to: nil)

    pretty, generate formatting/escape options, and minify's acceptance options

Returns:

  • (String)

    the reformatted document

Raises:



362
363
364
365
366
367
368
369
# File 'lib/nosj.rb', line 362

def self.reformat(json, opts = nil)
  if opts&.key?(:pretty)
    pretty = opts[:pretty]
    opts = opts.except(:pretty)
    opts = PRETTY_GENERATE_OPTS.merge(opts) if pretty
  end
  reformat_native(json, opts)
end

.reformat_file(path, opts = nil) ⇒ String

reformat against a file: the pipe runs over a read-only memory map, so the input document never becomes a Ruby String; only the result does.

Examples:

compact = NOSJ.reformat_file("big.json")            # minify
pretty  = NOSJ.reformat_file("big.json", pretty: true)

Parameters:

  • path (String)

    the file to reformat (UTF-8)

  • opts (Hash, nil) (defaults to: nil)

    same options as reformat

Returns:

  • (String)

    the reformatted document

Raises:

  • (SystemCallError)

    Errno::ENOENT and friends

  • (ParserError)

    when the file is malformed or not UTF-8



384
385
386
387
388
389
390
391
# File 'lib/nosj.rb', line 384

def self.reformat_file(path, opts = nil)
  if opts&.key?(:pretty)
    pretty = opts[:pretty]
    opts = opts.except(:pretty)
    opts = PRETTY_GENERATE_OPTS.merge(opts) if pretty
  end
  reformat_file_native(path, opts)
end

.splice(json, edits, opts = nil) ⇒ String

Byte-splicing edits: JSON Pointer => replacement value, resolved in one pass; bytes outside the target spans are copied untouched.

Parameters:

  • json (String)
  • edits (Hash[String, untyped])
  • opts (opts) (defaults to: nil)

Returns:

  • (String)


414
415
416
# File 'lib/nosj.rb', line 414

def self.splice(json, edits, opts = nil)
  splice_native(json, edits, opts)
end

.stats(source, opts = nil) ⇒ Hash[Symbol, untyped]

Document statistics (byte_size, root, max_depth, values, keys, key_histogram, containers, strings) from one counting-sink pass.

Parameters:

  • source (String)
  • opts (opts) (defaults to: nil)

Returns:

  • (Hash[Symbol, untyped])


603
604
605
# File 'lib/nosj.rb', line 603

def self.stats(source, opts = nil)
  stats_native(source, opts)
end

.stats_file(path, opts = nil) ⇒ Hash

stats against a file: memory-maps it and runs the counting pass without reading the document into Ruby. byte_size is the file size.

Examples:

NOSJ.stats_file("huge.json") => {byte_size: 41_943_040, ...}

Parameters:

  • path (String)

    the file to inspect (UTF-8)

  • opts (Hash, nil) (defaults to: nil)

    same options as stats

Returns:

  • (Hash)

    the statistics described on stats

Raises:

  • (SystemCallError)

    Errno::ENOENT and friends

  • (ParserError)

    when the file is malformed or not UTF-8



619
620
621
# File 'lib/nosj.rb', line 619

def self.stats_file(path, opts = nil)
  stats_file_native(path, opts)
end

.valid?(source, opts = nil) ⇒ Boolean

Validates a document without building any Ruby values: the full parser (tokenizers, string decode, number validation) runs into a null sink, 1.8-2.5x faster than parse.

Returns true iff NOSJ.parse(source, opts) would succeed: parse refusals (malformed JSON, wrong encoding, too-deep nesting) are false, while option and argument-type errors still raise exactly like parse.

Examples:

NOSJ.valid?('{"a":1}')  #=> true
NOSJ.valid?('{"a":}')   #=> false

Parameters:

  • source (String)

    the JSON document

  • opts (Hash, nil) (defaults to: nil)

    same options as parse

Returns:

  • (Boolean)

Raises:

  • (ArgumentError)

    for unsupported options



160
161
162
# File 'lib/nosj.rb', line 160

def self.valid?(source, opts = nil)
  valid_native(source, opts)
end

.write_file(path, obj, opts = nil) ⇒ Integer

Generates obj as JSON and writes it to path, streaming the generator's buffer straight to disk—no intermediate Ruby String.

Examples:

NOSJ.write_file("out.json", {"a" => [1, true]})   #=> 14
NOSJ.write_file("pretty.json", obj, indent: "  ", object_nl: "\n")

Parameters:

  • path (String)

    the file to (over)write

  • obj (Object)

    the value tree to generate

  • opts (Hash, nil) (defaults to: nil)

    the same options as generate

Returns:

  • (Integer)

    the number of bytes written, like File.write

Raises:



270
271
272
# File 'lib/nosj.rb', line 270

def self.write_file(path, obj, opts = nil)
  write_file_native(path, obj, opts)
end

.write_lines(path, values, opts = nil) ⇒ Integer

generate_lines straight to a file, streaming the generator's buffer to disk like write_file.

Examples:

NOSJ.write_lines("out.ndjson", events)  #=> bytes written

Parameters:

  • path (String)

    the file to (over)write

  • values (Array, Enumerable)

    one document per line

  • opts (Hash, nil) (defaults to: nil)

    the same options as generate

Returns:

  • (Integer)

    the number of bytes written, like File.write

Raises:

  • (SystemCallError)

    Errno::ENOENT and friends

  • (ArgumentError)

    for formatting options that contain newlines



553
554
555
# File 'lib/nosj.rb', line 553

def self.write_lines(path, values, opts = nil)
  write_lines_native(path, lines_array(values), opts)
end