Module: SmarterJSON

Defined in:
lib/smarter_json.rb,
lib/smarter_json/errors.rb,
lib/smarter_json/parser.rb,
lib/smarter_json/version.rb,
lib/smarter_json/warning.rb,
lib/smarter_json/generator.rb,
ext/smarter_json/smarter_json.c

Defined Under Namespace

Classes: EncodingError, Error, GenerateError, Generator, ParseError, Parser, Warning

Constant Summary collapse

HAS_ACCELERATION =
respond_to?(:parse_c)
VERSION =
"0.6.0"

Class Method Summary collapse

Class Method Details

.generate(obj, options = {}) ⇒ Object

SmarterJSON.generate(obj, options = {}) — write a Ruby value as JSON.

options:

:json   (default) — standard JSON. Hash -> object, Array -> array,
                    scalar -> scalar. Always valid, interoperable JSON.
:ndjson           — newline-delimited JSON. An Array writes one element per
                    line; any other value writes as a single line. The
                    inverse of process reading NDJSON back into an Array.

options: spaces per nesting level for pretty-printing (Integer, default

0 = compact). Empty objects/arrays stay inline. Not allowed with :ndjson (a
record must be a single line) — combining them raises ArgumentError.

Symbol keys/values are emitted as strings; BigDecimal as a JSON number. Unsupported types (Time, custom objects) and non-finite Floats raise SmarterJSON::GenerateError. Returns a String.



24
25
26
# File 'lib/smarter_json/generator.rb', line 24

def generate(obj, options = {})
  Generator.new(options).generate(obj)
end

.parse_c(input, opts) ⇒ Object



1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
# File 'ext/smarter_json/smarter_json.c', line 1374

static VALUE fj_parse_c(VALUE self, VALUE input, VALUE opts) {
  fj_state st;
  VALUE value, enc_opt, dk;

  Check_Type(input, T_STRING);

  enc_opt = rb_hash_aref(opts, ID2SYM(rb_intern("encoding")));
  if (!NIL_P(enc_opt)) {
    input = rb_funcall(rb_str_dup(input), rb_intern("force_encoding"), 1, enc_opt);
  }
  if (!RTEST(rb_funcall(input, rb_intern("valid_encoding?"), 0))) {
    VALUE name = rb_funcall(rb_funcall(input, rb_intern("encoding"), 0), rb_intern("name"), 0);
    VALUE msg = rb_sprintf("invalid byte sequence for %" PRIsVALUE, name);
    rb_exc_raise(rb_funcall(cEncodingError, rb_intern("new"), 3, msg, Qnil, Qnil));
  }

  st.buf = RSTRING_PTR(input);
  st.len = RSTRING_LEN(input);
  st.pos = 0;
  st.enc = rb_enc_get(input);
  st.depth = 0;
#ifdef HAVE_RB_ENC_INTERNED_STR
  fj_kc_slot kcache[FJ_KCACHE_SIZE];
  memset(kcache, 0, sizeof(kcache));
  st.kcache = kcache;
#else
  st.kcache = NULL;
#endif

  st.symbolize_keys = RTEST(rb_hash_aref(opts, ID2SYM(rb_intern("symbolize_keys"))));
  dk = rb_hash_aref(opts, ID2SYM(rb_intern("duplicate_key")));
  st.dup_first_wins = (dk == ID2SYM(rb_intern("first_wins")));
  st.dup_raise = (dk == ID2SYM(rb_intern("raise")));

  {
    VALUE bd = rb_hash_aref(opts, ID2SYM(rb_intern("bigdecimal_load")));
    if (bd == ID2SYM(rb_intern("float"))) st.bigdecimal_load = 0;
    else if (bd == ID2SYM(rb_intern("bigdecimal"))) st.bigdecimal_load = 2;
    else st.bigdecimal_load = 1; /* :auto (default), including nil */
  }

  st.collect_warnings = RTEST(rb_hash_aref(opts, ID2SYM(rb_intern("warnings"))));
  st.warnings = st.collect_warnings ? rb_ary_new() : Qnil;

  if (st.len >= 3 && (unsigned char)st.buf[0] == 0xEF &&
      (unsigned char)st.buf[1] == 0xBB && (unsigned char)st.buf[2] == 0xBF) {
    st.pos = 3;
  }

  /* With a block: yield each top-level value until EOF (JSONL / NDJSON /
   * concatenated). Same loop as the Ruby each_value path, on the C parser. */
  if (rb_block_given_p()) {
    for (;;) {
      fj_skip_ws_comments(&st);
      if (fj_eof(&st)) break;
      rb_yield(fj_parse_iter(&st, fj_implicit_root_ahead(&st)));
    }
    return Qnil;
  }

  /* No block: auto-detect the document count for free — it is the same "is there
   * trailing content after the first value?" check that used to raise. 0 documents
   * -> nil; 1 document -> the value itself (single-document hot path, no Array
   * allocated); 2+ documents (NDJSON / JSONL / concatenated / whitespace-separated)
   * -> an Array of every top-level value. Commas do NOT separate documents (only
   * whitespace / newline / concatenation do), so a bracketless comma list still
   * raises in fj_parse_iter — the unsupported implicit-root array. */
  fj_skip_ws_comments(&st);
  if (fj_eof(&st)) return st.collect_warnings ? rb_assoc_new(Qnil, st.warnings) : Qnil;
  value = fj_parse_iter(&st, fj_implicit_root_ahead(&st));
  fj_skip_ws_comments(&st);
  if (fj_eof(&st)) return st.collect_warnings ? rb_assoc_new(value, st.warnings) : value;
  {
    VALUE arr = rb_ary_new();
    rb_ary_push(arr, value);
    do {
      rb_ary_push(arr, fj_parse_iter(&st, fj_implicit_root_ahead(&st)));
      fj_skip_ws_comments(&st);
    } while (!fj_eof(&st));
    return st.collect_warnings ? rb_assoc_new(arr, st.warnings) : arr;
  }
}

.process(input, options = {}, &block) ⇒ Object

SmarterJSON.process(input, options = {}) — the main entry point.

‘input` is either a String of JSON content or an IO to read from. (A String is always content, never a filename — use process_file for paths.) The values in `options` override Parser::DEFAULT_OPTIONS.

Without a block: returns nil (zero documents), the value (one document), or an Array of the values (two or more — NDJSON / JSONL / concatenated / whitespace- separated). :acceleration (default true) selects the C extension when compiled and loaded (SmarterJSON::HAS_ACCELERATION); otherwise the pure-Ruby parser.

With a block: yields each top-level document as it is parsed, and returns nil. For an IO this streams document-by-document in bounded memory — it reads the stream as newline-delimited documents (NDJSON / JSONL), one per line.



23
24
25
26
27
28
29
30
31
# File 'lib/smarter_json/parser.rb', line 23

def process(input, options = {}, &block)
  if input.is_a?(String)
    process_content(input, options, &block)
  elsif input.respond_to?(:read)
    block ? stream_io(input, options, &block) : process_content(input.read, options)
  else
    raise ArgumentError, "SmarterJSON.process expects a String or an IO, got #{input.class}"
  end
end

.process_file(path, options = {}, &block) ⇒ Object

SmarterJSON.process_file(path, options = {}) — open a file and process it.

The :encoding option labels the file’s encoding (default “UTF-8”); it does NOT trigger a transcoding pass — the parser works on the bytes in their native encoding and emits string values with the same encoding tag. With a block, streams document-by-document straight from disk in bounded memory (never loading the whole file); the documents are read as newline-delimited (NDJSON / JSONL), one per line.



41
42
43
44
45
46
47
48
# File 'lib/smarter_json/parser.rb', line 41

def process_file(path, options = {}, &block)
  encoding = options.fetch(:encoding, "UTF-8")
  if block
    File.open(path, "r:#{encoding}") { |io| stream_io(io, options, &block) }
  else
    process_content(File.read(path, encoding: encoding), options)
  end
end