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/generator.rb,
ext/smarter_json/smarter_json.c

Defined Under Namespace

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

Constant Summary collapse

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

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



1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
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
# File 'ext/smarter_json/smarter_json.c', line 1336

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 */
  }

  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 Qnil;
  value = fj_parse_iter(&st, fj_implicit_root_ahead(&st));
  fj_skip_ws_comments(&st);
  if (fj_eof(&st)) return 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 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