Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Browse files

getting SAJ ready for release

  • Loading branch information...
commit cbe5b91c3507b33a3d7d7d1d6fef3db8c83c4d43 1 parent 87a7a73
@ohler55 authored
View
1  ext/oj/cache.c
@@ -51,7 +51,6 @@ oj_cache_new(Cache *cache) {
}
(*cache)->key = 0;
(*cache)->value = Qundef;
- //bzero((*cache)->slots, sizeof((*cache)->slots));
memset((*cache)->slots, 0, sizeof((*cache)->slots));
}
View
94 ext/oj/oj.c
@@ -65,6 +65,12 @@ ID oj_tv_usec_id;
ID oj_utc_offset_id;
ID oj_write_id;
+ID oj_hash_start_id;
+ID oj_hash_end_id;
+ID oj_array_start_id;
+ID oj_array_end_id;
+ID oj_add_value_id;
+
VALUE oj_bag_class;
VALUE oj_parse_error_class;
VALUE oj_bigdecimal_class;
@@ -416,7 +422,6 @@ load_with_opts(VALUE input, Options copts) {
rb_raise(rb_eArgError, "load() expected a String or IO Object.");
}
}
- // TBD pick SAJ or normal
obj = oj_parse(json, copts);
if (copts->max_stack < len) {
xfree(json);
@@ -544,6 +549,85 @@ to_file(int argc, VALUE *argv, VALUE self) {
return Qnil;
}
+/* call-seq: saj_parse(handler, io)
+ *
+ * Parses an IO stream or file containing an JSON document. Raises an exception
+ * if the JSON is malformed.
+ * @param [Oj::Saj] handler SAJ (responds to Oj::Saj methods) like handler
+ * @param [IO|String] io IO Object to read from
+ */
+static VALUE
+saj_parse(int argc, VALUE *argv, VALUE self) {
+ struct _Options copts = oj_default_options;
+ char *json;
+ size_t len;
+ VALUE input = argv[1];
+
+ if (argc < 2) {
+ rb_raise(rb_eArgError, "Wrong number of arguments to saj_parse.\n");
+ }
+ if (rb_type(input) == T_STRING) {
+ // the json string gets modified so make a copy of it
+ len = RSTRING_LEN(input) + 1;
+ if (copts.max_stack < len) {
+ json = ALLOC_N(char, len);
+ } else {
+ json = ALLOCA_N(char, len);
+ }
+ strcpy(json, StringValuePtr(input));
+ } else {
+ VALUE clas = rb_obj_class(input);
+ VALUE s;
+
+ if (oj_stringio_class == clas) {
+ s = rb_funcall2(input, oj_string_id, 0, 0);
+ len = RSTRING_LEN(s) + 1;
+ if (copts.max_stack < len) {
+ json = ALLOC_N(char, len);
+ } else {
+ json = ALLOCA_N(char, len);
+ }
+ strcpy(json, StringValuePtr(s));
+#ifndef JRUBY_RUBY
+#if !IS_WINDOWS
+ // JRuby gets confused with what is the real fileno.
+ } else if (rb_respond_to(input, oj_fileno_id) && Qnil != (s = rb_funcall(input, oj_fileno_id, 0))) {
+ int fd = FIX2INT(s);
+ ssize_t cnt;
+
+ len = lseek(fd, 0, SEEK_END);
+ lseek(fd, 0, SEEK_SET);
+ if (copts.max_stack < len) {
+ json = ALLOC_N(char, len + 1);
+ } else {
+ json = ALLOCA_N(char, len + 1);
+ }
+ if (0 >= (cnt = read(fd, json, len)) || cnt != (ssize_t)len) {
+ rb_raise(rb_eIOError, "failed to read from IO Object.");
+ }
+ json[len] = '\0';
+#endif
+#endif
+ } else if (rb_respond_to(input, oj_read_id)) {
+ s = rb_funcall2(input, oj_read_id, 0, 0);
+ len = RSTRING_LEN(s) + 1;
+ if (copts.max_stack < len) {
+ json = ALLOC_N(char, len);
+ } else {
+ json = ALLOCA_N(char, len);
+ }
+ strcpy(json, StringValuePtr(s));
+ } else {
+ rb_raise(rb_eArgError, "saj_parse() expected a String or IO Object.");
+ }
+ }
+ oj_saj_parse(*argv, json);
+ if (copts.max_stack < len) {
+ xfree(json);
+ }
+ return Qnil;
+}
+
// Mimic JSON section
static VALUE
@@ -881,6 +965,8 @@ void Init_oj() {
rb_define_module_function(Oj, "dump", dump, -1);
rb_define_module_function(Oj, "to_file", to_file, -1);
+ rb_define_module_function(Oj, "saj_parse", saj_parse, -1);
+
oj_as_json_id = rb_intern("as_json");
oj_fileno_id = rb_intern("fileno");
oj_instance_variables_id = rb_intern("instance_variables");
@@ -899,6 +985,12 @@ void Init_oj() {
oj_utc_offset_id = rb_intern("utc_offset");
oj_write_id = rb_intern("write");
+ oj_hash_start_id = rb_intern("hash_start");
+ oj_hash_end_id = rb_intern("hash_end");
+ oj_array_start_id = rb_intern("array_start");
+ oj_array_end_id = rb_intern("array_end");
+ oj_add_value_id = rb_intern("add_value");
+
oj_bag_class = rb_const_get_at(Oj, rb_intern("Bag"));
oj_parse_error_class = rb_const_get_at(Oj, rb_intern("ParseError"));
oj_struct_class = rb_const_get(rb_cObject, rb_intern("Struct"));
View
9 ext/oj/oj.h
@@ -144,6 +144,8 @@ typedef struct _Leaf {
} *Leaf;
extern VALUE oj_parse(char *json, Options options);
+extern void oj_saj_parse(VALUE handler, char *json);
+
extern char* oj_write_obj_to_str(VALUE obj, Options copts);
extern void oj_write_obj_to_file(VALUE obj, const char *path, Options copts);
extern char* oj_write_leaf_to_str(Leaf leaf, Options copts);
@@ -164,6 +166,7 @@ extern rb_encoding *oj_utf8_encoding;
extern VALUE oj_bag_class;
extern VALUE oj_bigdecimal_class;
extern VALUE oj_doc_class;
+extern VALUE oj_parse_error_class;
extern VALUE oj_stringio_class;
extern VALUE oj_struct_class;
extern VALUE oj_time_class;
@@ -185,6 +188,12 @@ extern ID oj_tv_sec_id;
extern ID oj_tv_usec_id;
extern ID oj_utc_offset_id;
+extern ID oj_hash_start_id;
+extern ID oj_hash_end_id;
+extern ID oj_array_start_id;
+extern ID oj_array_end_id;
+extern ID oj_add_value_id;
+
extern Cache oj_class_cache;
extern Cache oj_attr_cache;
View
762 ext/oj/saj.c
@@ -0,0 +1,762 @@
+/* saj.c
+ * Copyright (c) 2012, Peter Ohler
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of Peter Ohler nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#if !IS_WINDOWS
+#include <sys/resource.h> // for getrlimit() on linux
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <math.h>
+
+//Workaround:
+#ifndef INFINITY
+#define INFINITY (1.0/0.0)
+#endif
+
+#include "oj.h"
+
+typedef struct _CX {
+ VALUE *cur;
+ VALUE *end;
+ VALUE stack[1024];
+} *CX;
+
+typedef struct _ParseInfo {
+ char *str; /* buffer being read from */
+ char *s; /* current position in buffer */
+ void *stack_min;
+ VALUE handler;
+ int has_hash_start;
+ int has_hash_end;
+ int has_array_start;
+ int has_array_end;
+ int has_add_value;
+} *ParseInfo;
+
+static void read_next(ParseInfo pi, const char *key);
+static void read_hash(ParseInfo pi, const char *key);
+static void read_array(ParseInfo pi, const char *key);
+static void read_str(ParseInfo pi, const char *key);
+static void read_num(ParseInfo pi, const char *key);
+static void read_true(ParseInfo pi, const char *key);
+static void read_false(ParseInfo pi, const char *key);
+static void read_nil(ParseInfo pi, const char *key);
+static void next_non_white(ParseInfo pi);
+static char* read_quoted_value(ParseInfo pi);
+static void skip_comment(ParseInfo pi);
+
+/* This XML parser is a single pass, destructive, callback parser. It is a
+ * single pass parse since it only make one pass over the characters in the
+ * XML document string. It is destructive because it re-uses the content of
+ * the string for values in the callback and places \0 characters at various
+ * places to mark the end of tokens and strings. It is a callback parser like
+ * a SAX parser because it uses callback when document elements are
+ * encountered.
+ *
+ * Parsing is very tolerant. Lack of headers and even mispelled element
+ * endings are passed over without raising an error. A best attempt is made in
+ * all cases to parse the string.
+ */
+
+inline static void
+next_non_white(ParseInfo pi) {
+ for (; 1; pi->s++) {
+ switch(*pi->s) {
+ case ' ':
+ case '\t':
+ case '\f':
+ case '\n':
+ case '\r':
+ break;
+ case '/':
+ skip_comment(pi);
+ break;
+ default:
+ return;
+ }
+ }
+}
+
+inline static void
+next_white(ParseInfo pi) {
+ for (; 1; pi->s++) {
+ switch(*pi->s) {
+ case ' ':
+ case '\t':
+ case '\f':
+ case '\n':
+ case '\r':
+ case '\0':
+ return;
+ default:
+ break;
+ }
+ }
+}
+
+inline static unsigned long
+read_ulong(const char *s, ParseInfo pi) {
+ unsigned long n = 0;
+
+ for (; '\0' != *s; s++) {
+ if ('0' <= *s && *s <= '9') {
+ n = n * 10 + (*s - '0');
+ } else {
+ raise_error("Not a valid ID number", pi->str, pi->s);
+ }
+ }
+ return n;
+}
+
+inline static void
+call_add_value(VALUE handler, VALUE value, const char *key) {
+ VALUE k;
+
+ if (0 == key) {
+ k = Qnil;
+ } else {
+ k = rb_str_new2(key);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(k, oj_utf8_encoding);
+#endif
+ }
+ rb_funcall(handler, oj_add_value_id, 2, value, k);
+}
+
+inline static void
+call_no_value(VALUE handler, ID method, const char *key) {
+ VALUE k;
+
+ if (0 == key) {
+ k = Qnil;
+ } else {
+ k = rb_str_new2(key);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(k, oj_utf8_encoding);
+#endif
+ }
+ rb_funcall(handler, method, 1, k);
+}
+
+static void
+skip_comment(ParseInfo pi) {
+ pi->s++; // skip first /
+ if ('*' == *pi->s) {
+ pi->s++;
+ for (; '\0' != *pi->s; pi->s++) {
+ if ('*' == *pi->s && '/' == *(pi->s + 1)) {
+ pi->s++;
+ return;
+ } else if ('\0' == *pi->s) {
+ raise_error("comment not terminated", pi->str, pi->s);
+ }
+ }
+ } else if ('/' == *pi->s) {
+ for (; 1; pi->s++) {
+ switch (*pi->s) {
+ case '\n':
+ case '\r':
+ case '\f':
+ case '\0':
+ return;
+ default:
+ break;
+ }
+ }
+ } else {
+ raise_error("invalid comment", pi->str, pi->s);
+ }
+}
+
+static void
+read_next(ParseInfo pi, const char *key) {
+ VALUE obj;
+
+ if ((void*)&obj < pi->stack_min) {
+ rb_raise(rb_eSysStackError, "JSON is too deeply nested");
+ }
+ next_non_white(pi); // skip white space
+ switch (*pi->s) {
+ case '{':
+ read_hash(pi, key);
+ break;
+ case '[':
+ read_array(pi, key);
+ break;
+ case '"':
+ read_str(pi, key);
+ break;
+ case '+':
+ case '-':
+ case '0':
+ case '1':
+ case '2':
+ case '3':
+ case '4':
+ case '5':
+ case '6':
+ case '7':
+ case '8':
+ case '9':
+ read_num(pi, key);
+ break;
+ case 'I':
+ read_num(pi, key);
+ break;
+ case 't':
+ read_true(pi, key);
+ break;
+ case 'f':
+ read_false(pi, key);
+ break;
+ case 'n':
+ read_nil(pi, key);
+ break;
+ case '\0':
+ return;
+ default:
+ // TBD error
+ return;
+ break;
+ }
+}
+
+static void
+read_hash(ParseInfo pi, const char *key) {
+ const char *ks;
+
+ if (pi->has_hash_start) {
+ call_no_value(pi->handler, oj_hash_start_id, key);
+ }
+ pi->s++;
+ next_non_white(pi);
+ if ('}' == *pi->s) {
+ pi->s++;
+ } else {
+ while (1) {
+ next_non_white(pi);
+ ks = read_quoted_value(pi);
+ next_non_white(pi);
+ if (':' == *pi->s) {
+ pi->s++;
+ } else {
+ raise_error("invalid format, expected :", pi->str, pi->s);
+ }
+ read_next(pi, ks);
+ next_non_white(pi);
+ if ('}' == *pi->s) {
+ pi->s++;
+ break;
+ } else if (',' == *pi->s) {
+ pi->s++;
+ } else {
+ //printf("*** '%s'\n", pi->s);
+ raise_error("invalid format, expected , or } while in an object", pi->str, pi->s);
+ }
+ }
+ }
+ if (pi->has_hash_end) {
+ call_no_value(pi->handler, oj_hash_end_id, key);
+ }
+}
+
+static void
+read_array(ParseInfo pi, const char *key) {
+ if (pi->has_array_start) {
+ call_no_value(pi->handler, oj_array_start_id, key);
+ }
+ pi->s++;
+ next_non_white(pi);
+ if (']' == *pi->s) {
+ pi->s++;
+ } else {
+ while (1) {
+ read_next(pi, 0);
+ next_non_white(pi);
+ if (',' == *pi->s) {
+ pi->s++;
+ } else if (']' == *pi->s) {
+ pi->s++;
+ break;
+ } else {
+ raise_error("invalid format, expected , or ] while in an array", pi->str, pi->s);
+ }
+ }
+ }
+ if (pi->has_array_end) {
+ call_no_value(pi->handler, oj_array_end_id, key);
+ }
+}
+
+static void
+read_str(ParseInfo pi, const char *key) {
+ char *text;
+ VALUE s;
+
+ text = read_quoted_value(pi);
+ s = rb_str_new2(text);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(s, oj_utf8_encoding);
+#endif
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, s, key);
+ }
+}
+
+#ifdef RUBINIUS_RUBY
+#define NUM_MAX 0x07FFFFFF
+#else
+#define NUM_MAX (FIXNUM_MAX >> 8)
+#endif
+
+static void
+read_num(ParseInfo pi, const char *key) {
+ char *start = pi->s;
+ int64_t n = 0;
+ long a = 0;
+ long div = 1;
+ long e = 0;
+ int neg = 0;
+ int eneg = 0;
+ int big = 0;
+ VALUE num;
+
+ if ('-' == *pi->s) {
+ pi->s++;
+ neg = 1;
+ } else if ('+' == *pi->s) {
+ pi->s++;
+ }
+ if ('I' == *pi->s) {
+ if (0 != strncmp("Infinity", pi->s, 8)) {
+ raise_error("number or other value", pi->str, pi->s);
+ }
+ pi->s += 8;
+ if (neg) {
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, rb_float_new(-INFINITY), key);
+ }
+ } else {
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, rb_float_new(INFINITY), key);
+ }
+ }
+ return;
+ }
+ for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
+ if (big) {
+ big++;
+ } else {
+ n = n * 10 + (*pi->s - '0');
+ if (NUM_MAX <= n) {
+ big = 1;
+ }
+ }
+ }
+ if ('.' == *pi->s) {
+ pi->s++;
+ for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
+ a = a * 10 + (*pi->s - '0');
+ div *= 10;
+ if (NUM_MAX <= div) {
+ big = 1;
+ }
+ }
+ }
+ if ('e' == *pi->s || 'E' == *pi->s) {
+ pi->s++;
+ if ('-' == *pi->s) {
+ pi->s++;
+ eneg = 1;
+ } else if ('+' == *pi->s) {
+ pi->s++;
+ }
+ for (; '0' <= *pi->s && *pi->s <= '9'; pi->s++) {
+ e = e * 10 + (*pi->s - '0');
+ if (NUM_MAX <= e) {
+ big = 1;
+ }
+ }
+ }
+ if (0 == e && 0 == a && 1 == div) {
+ if (big) {
+ char c = *pi->s;
+
+ *pi->s = '\0';
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, rb_funcall(oj_bigdecimal_class, oj_new_id, 1, rb_str_new2(start)), key);
+ }
+ *pi->s = c;
+ } else {
+ if (neg) {
+ n = -n;
+ }
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, LONG2NUM(n), key);
+ }
+ }
+ return;
+ } else { // decimal
+ if (big) {
+ char c = *pi->s;
+
+ *pi->s = '\0';
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, rb_funcall(oj_bigdecimal_class, oj_new_id, 1, rb_str_new2(start)), key);
+ }
+ *pi->s = c;
+ } else {
+ double d = (double)n + (double)a / (double)div;
+
+ if (neg) {
+ d = -d;
+ }
+ if (1 < big) {
+ e += big - 1;
+ }
+ if (0 != e) {
+ if (eneg) {
+ e = -e;
+ }
+ d *= pow(10.0, e);
+ }
+ num = rb_float_new(d);
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, num, key);
+ }
+ }
+ }
+}
+
+static void
+read_true(ParseInfo pi, const char *key) {
+ pi->s++;
+ if ('r' != *pi->s || 'u' != *(pi->s + 1) || 'e' != *(pi->s + 2)) {
+ raise_error("invalid format, expected 'true'", pi->str, pi->s);
+ }
+ pi->s += 3;
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, Qtrue, key);
+ }
+}
+
+static void
+read_false(ParseInfo pi, const char *key) {
+ pi->s++;
+ if ('a' != *pi->s || 'l' != *(pi->s + 1) || 's' != *(pi->s + 2) || 'e' != *(pi->s + 3)) {
+ raise_error("invalid format, expected 'false'", pi->str, pi->s);
+ }
+ pi->s += 4;
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, Qfalse, key);
+ }
+}
+
+static void
+read_nil(ParseInfo pi, const char *key) {
+ pi->s++;
+ if ('u' != *pi->s || 'l' != *(pi->s + 1) || 'l' != *(pi->s + 2)) {
+ raise_error("invalid format, expected 'nil'", pi->str, pi->s);
+ }
+ pi->s += 3;
+ if (pi->has_add_value) {
+ call_add_value(pi->handler, Qnil, key);
+ }
+}
+
+static uint32_t
+read_hex(ParseInfo pi, char *h) {
+ uint32_t b = 0;
+ int i;
+
+ // TBD this can be made faster with a table
+ for (i = 0; i < 4; i++, h++) {
+ b = b << 4;
+ if ('0' <= *h && *h <= '9') {
+ b += *h - '0';
+ } else if ('A' <= *h && *h <= 'F') {
+ b += *h - 'A' + 10;
+ } else if ('a' <= *h && *h <= 'f') {
+ b += *h - 'a' + 10;
+ } else {
+ pi->s = h;
+ raise_error("invalid hex character", pi->str, pi->s);
+ }
+ }
+ return b;
+}
+
+static char*
+unicode_to_chars(ParseInfo pi, char *t, uint32_t code) {
+ if (0x0000007F >= code) {
+ *t = (char)code;
+ } else if (0x000007FF >= code) {
+ *t++ = 0xC0 | (code >> 6);
+ *t = 0x80 | (0x3F & code);
+ } else if (0x0000FFFF >= code) {
+ *t++ = 0xE0 | (code >> 12);
+ *t++ = 0x80 | ((code >> 6) & 0x3F);
+ *t = 0x80 | (0x3F & code);
+ } else if (0x001FFFFF >= code) {
+ *t++ = 0xF0 | (code >> 18);
+ *t++ = 0x80 | ((code >> 12) & 0x3F);
+ *t++ = 0x80 | ((code >> 6) & 0x3F);
+ *t = 0x80 | (0x3F & code);
+ } else if (0x03FFFFFF >= code) {
+ *t++ = 0xF8 | (code >> 24);
+ *t++ = 0x80 | ((code >> 18) & 0x3F);
+ *t++ = 0x80 | ((code >> 12) & 0x3F);
+ *t++ = 0x80 | ((code >> 6) & 0x3F);
+ *t = 0x80 | (0x3F & code);
+ } else if (0x7FFFFFFF >= code) {
+ *t++ = 0xFC | (code >> 30);
+ *t++ = 0x80 | ((code >> 24) & 0x3F);
+ *t++ = 0x80 | ((code >> 18) & 0x3F);
+ *t++ = 0x80 | ((code >> 12) & 0x3F);
+ *t++ = 0x80 | ((code >> 6) & 0x3F);
+ *t = 0x80 | (0x3F & code);
+ } else {
+ raise_error("invalid Unicode", pi->str, pi->s);
+ }
+ return t;
+}
+
+/* Assume the value starts immediately and goes until the quote character is
+ * reached again. Do not read the character after the terminating quote.
+ */
+static char*
+read_quoted_value(ParseInfo pi) {
+ char *value = 0;
+ char *h = pi->s; // head
+ char *t = h; // tail
+ uint32_t code;
+
+ h++; // skip quote character
+ t++;
+ value = h;
+ for (; '"' != *h; h++, t++) {
+ if ('\0' == *h) {
+ pi->s = h;
+ raise_error("quoted string not terminated", pi->str, pi->s);
+ } else if ('\\' == *h) {
+ h++;
+ switch (*h) {
+ case 'n': *t = '\n'; break;
+ case 'r': *t = '\r'; break;
+ case 't': *t = '\t'; break;
+ case 'f': *t = '\f'; break;
+ case 'b': *t = '\b'; break;
+ case '"': *t = '"'; break;
+ case '/': *t = '/'; break;
+ case '\\': *t = '\\'; break;
+ case 'u':
+ h++;
+ code = read_hex(pi, h);
+ h += 3;
+ if (0x0000D800 <= code && code <= 0x0000DFFF) {
+ uint32_t c1 = (code - 0x0000D800) & 0x000003FF;
+ uint32_t c2;
+
+ h++;
+ if ('\\' != *h || 'u' != *(h + 1)) {
+ pi->s = h;
+ raise_error("invalid escaped character", pi->str, pi->s);
+ }
+ h += 2;
+ c2 = read_hex(pi, h);
+ h += 3;
+ c2 = (c2 - 0x0000DC00) & 0x000003FF;
+ code = ((c1 << 10) | c2) + 0x00010000;
+ }
+ t = unicode_to_chars(pi, t, code);
+ break;
+ default:
+ pi->s = h;
+ raise_error("invalid escaped character", pi->str, pi->s);
+ break;
+ }
+ } else if (t != h) {
+ *t = *h;
+ }
+ }
+ *t = '\0'; // terminate value
+ pi->s = h + 1;
+
+ return value;
+}
+
+inline static int
+respond_to(VALUE obj, ID method) {
+#ifdef JRUBY_RUBY
+ /* There is a bug in JRuby where rb_respond_to() returns true (1) even if
+ * a method is private. */
+ {
+ VALUE args[1];
+
+ *args = ID2SYM(method);
+ return (Qtrue == rb_funcall2(obj, rb_intern("respond_to?"), 1, args));
+ }
+#else
+ return rb_respond_to(obj, method);
+#endif
+}
+
+void
+oj_saj_parse(VALUE handler, char *json) {
+ VALUE obj = Qnil;
+ struct _ParseInfo pi;
+
+ if (0 == json) {
+ raise_error("Invalid arg, xml string can not be null", json, 0);
+ }
+ /* skip UTF-8 BOM if present */
+ if (0xEF == (uint8_t)*json && 0xBB == (uint8_t)json[1] && 0xBF == (uint8_t)json[2]) {
+ json += 3;
+ }
+ /* initialize parse info */
+ pi.str = json;
+ pi.s = json;
+#if IS_WINDOWS
+ pi.stack_min = (void*)((char*)&obj - (512 * 1024)); // assume a 1M stack and give half to ruby
+#else
+ {
+ struct rlimit lim;
+
+ if (0 == getrlimit(RLIMIT_STACK, &lim)) {
+ pi.stack_min = (void*)((char*)&obj - (lim.rlim_cur / 4 * 3)); // let 3/4ths of the stack be used only
+ } else {
+ pi.stack_min = 0; // indicates not to check stack limit
+ }
+ }
+#endif
+ pi.handler = handler;
+ pi.has_hash_start = respond_to(handler, oj_hash_start_id);
+ pi.has_hash_end = respond_to(handler, oj_hash_end_id);
+ pi.has_array_start = respond_to(handler, oj_array_start_id);
+ pi.has_array_end = respond_to(handler, oj_array_end_id);
+ pi.has_add_value = respond_to(handler, oj_add_value_id);
+ read_next(&pi, 0);
+ next_non_white(&pi);
+ if ('\0' != *pi.s) {
+ raise_error("invalid format, extra characters", pi.str, pi.s);
+ }
+}
+
+
+#if 0
+static void
+cx_add(CX cx, VALUE obj, const char *key) {
+ if (0 == cx->cur) {
+ cx->cur = cx->stack;
+ *cx->cur = obj;
+ } else {
+ if (0 != key) {
+ VALUE ks = rb_str_new2(key);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(ks, oj_utf8_encoding);
+#endif
+ rb_hash_aset(*cx->cur, ks, obj);
+ } else {
+ rb_ary_push(*cx->cur, obj);
+ }
+ }
+}
+
+static void
+cx_push(CX cx, VALUE obj, const char *key) {
+ if (0 == cx->cur) {
+ cx->cur = cx->stack;
+ } else {
+ if (cx->end <= cx->cur) {
+ rb_raise(oj_parse_error_class, "too deeply nested");
+ }
+ cx_add(cx, obj, key);
+ cx->cur++;
+ }
+ *cx->cur = obj;
+}
+
+static void
+hash_start(void *context, const char *key) {
+ cx_push((CX)context, rb_hash_new(), key);
+}
+
+static void
+col_end(void *context, const char *key) {
+ ((CX)context)->cur--;
+}
+
+static void
+array_start(void *context, const char *key) {
+ cx_push((CX)context, rb_ary_new(), key);
+}
+
+static void
+add_str(void *context, const char *str, const char *key) {
+ VALUE s;
+
+ s = rb_str_new2(str);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(s, oj_utf8_encoding);
+#endif
+ cx_add((CX)context, s, key);
+}
+
+static void
+add_big(void *context, const char *str, const char *key) {
+ cx_add((CX)context, rb_funcall(oj_bigdecimal_class, oj_new_id, 1, rb_str_new2(str)), key);
+}
+
+static void
+add_float(void *context, double num, const char *key) {
+ cx_add((CX)context, rb_float_new(num), key);
+}
+
+static void
+add_fixnum(void *context, int64_t num, const char *key) {
+ cx_add((CX)context, LONG2NUM(num), key);
+}
+
+static void
+add_true(void *context, const char *key) {
+ cx_add((CX)context, Qtrue, key);
+}
+
+static void
+add_false(void *context, const char *key) {
+ cx_add((CX)context, Qfalse, key);
+}
+
+static void
+add_nil(void *context, const char *key) {
+ cx_add((CX)context, Qnil, key);
+}
+#endif
View
391 ext/oj/tp.x
@@ -0,0 +1,391 @@
+/* tp.c
+ * Copyright (c) 2012, Peter Ohler
+ * All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are met:
+ *
+ * - Redistributions of source code must retain the above copyright notice, this
+ * list of conditions and the following disclaimer.
+ *
+ * - Redistributions in binary form must reproduce the above copyright notice,
+ * this list of conditions and the following disclaimer in the documentation
+ * and/or other materials provided with the distribution.
+ *
+ * - Neither the name of Peter Ohler nor the names of its contributors may be
+ * used to endorse or promote products derived from this software without
+ * specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
+ * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
+ * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+ * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+ * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+ * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+ * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+ * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+ * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+/*** This is just a prototype. Turns out the waiting for processing is expensive so the approach is slower that just
+ *** parsing with callbacks. The parsing is only 10% or less of the processing so not much gain would be expected even
+ *** in the best case.
+ ***/
+
+#include <pthread.h>
+#if !IS_WINDOWS
+#include <sys/resource.h> // for getrlimit() on linux
+#endif
+#include <stdlib.h>
+#include <stdio.h>
+#include <string.h>
+#include <sys/time.h>
+#include <time.h>
+
+#include "oj.h"
+
+typedef enum {
+ UNDEF = 0,
+ HASH_START,
+ HASH_END,
+ ARRAY_START,
+ ARRAY_END,
+ ADD_STR,
+ ADD_BIG,
+ ADD_FLOAT,
+ ADD_FIXNUM,
+ ADD_TRUE,
+ ADD_FALSE,
+ ADD_NIL,
+ DONE
+} OpType;
+
+typedef struct _Op {
+ OpType op;
+ const char *key;
+ union {
+ VALUE rval;
+ double d;
+ uint64_t i;
+ const char *s;
+ };
+} *Op;
+
+typedef struct _CX {
+ char *json;
+ VALUE *cur;
+ VALUE *end;
+ struct _Op ops[4096];
+ Op ops_end;
+ Op iop;
+ VALUE stack[1024];
+} *CX;
+
+typedef struct _Wait {
+ pthread_mutex_t mutex;
+ pthread_cond_t cond;
+ volatile int waiting;
+} *Wait;
+
+
+static void hash_start(void *context, const char *key);
+static void hash_end(void *context, const char *key);
+static void array_start(void *context, const char *key);
+static void array_end(void *context, const char *key);
+static void add_str(void *context, const char *str, const char *key);
+static void add_big(void *context, const char *str, const char *key);
+static void add_float(void *context, double num, const char *key);
+static void add_fixnum(void *context, int64_t num, const char *key);
+static void add_true(void *context, const char *key);
+static void add_false(void *context, const char *key);
+static void add_nil(void *context, const char *key);
+static void done(void *context);
+
+static struct _SajCB tcb = {
+ hash_start,
+ hash_end,
+ array_start,
+ array_end,
+ add_str,
+ add_big,
+ add_float,
+ add_fixnum,
+ add_true,
+ add_false,
+ add_nil,
+ done
+};
+
+static inline void
+wakeup(Wait w) {
+ if (w->waiting) {
+ pthread_mutex_lock(&w->mutex);
+ pthread_cond_signal(&w->cond);
+ pthread_mutex_unlock(&w->mutex);
+ }
+}
+
+static int
+wait_init(Wait w) {
+ int err;
+
+ w->waiting = 0;
+ if (0 != (err = pthread_mutex_init(&w->mutex, 0)) ||
+ 0 != (err = pthread_cond_init(&w->cond, 0))) {
+ return err;
+ }
+ return 0;
+}
+
+static void
+wait_for_it(Wait w, double timeout) {
+ struct timespec done;
+ struct timeval tv;
+ double end;
+
+ gettimeofday(&tv, 0);
+ end = (double)tv.tv_sec + (double)tv.tv_usec / 1000000.0;
+ end += timeout;
+
+ done.tv_sec = (time_t)end;
+ done.tv_nsec = (long)((end - done.tv_sec) * 1000000000.0);
+ pthread_mutex_lock(&w->mutex);
+ w->waiting = 1;
+ pthread_cond_timedwait(&w->cond, &w->mutex, &done);
+ w->waiting = 0;
+ pthread_mutex_unlock(&w->mutex);
+}
+
+static int
+wait_destroy(Wait w) {
+ int err;
+
+ if (0 != (err = pthread_mutex_destroy(&w->mutex))) {
+ return err;
+ }
+ if (0 != (err = pthread_cond_destroy(&w->cond))) {
+ return err;
+ }
+ return 0;
+}
+
+static void
+cx_add(CX cx, VALUE obj, const char *key) {
+ if (0 == cx->cur) {
+ cx->cur = cx->stack;
+ *cx->cur = obj;
+ } else {
+ if (0 != key) {
+ VALUE ks = rb_str_new2(key);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(ks, oj_utf8_encoding);
+#endif
+ rb_hash_aset(*cx->cur, ks, obj);
+ } else {
+ rb_ary_push(*cx->cur, obj);
+ }
+ }
+}
+
+static void
+cx_push(CX cx, VALUE obj, const char *key) {
+ if (0 == cx->cur) {
+ cx->cur = cx->stack;
+ } else {
+ if (cx->end <= cx->cur) {
+ rb_raise(oj_parse_error_class, "too deeply nested");
+ }
+ cx_add(cx, obj, key);
+ cx->cur++;
+ }
+ *cx->cur = obj;
+}
+
+static pthread_t parse_thread;
+static CX job = 0;
+static struct _Wait parse_wait;
+static struct _Wait proc_wait;
+
+static void*
+parse_loop(void *data) {
+ while (1) {
+ if (0 == job) {
+ wait_for_it(&parse_wait, 0.1);
+ }
+ if (0 != job) {
+ CX cx = job;
+
+ job = 0;
+ oj_saj_parse(cx->json, &tcb, cx);
+ }
+ }
+ return 0;
+}
+
+void oj_tp_init() {
+ wait_init(&parse_wait);
+ wait_init(&proc_wait);
+ pthread_create(&parse_thread, 0, parse_loop, (void*)job);
+}
+
+VALUE
+oj_t_parse(char *json) {
+ // TBD change to allocated for each thread or each call, make smaller and block parser when full
+ struct _CX cx;
+ Op op;
+
+ cx.json = json;
+ cx.cur = 0;
+ cx.end = cx.stack + (sizeof(cx.stack) / sizeof(*cx.stack));
+ *cx.stack = Qnil;
+
+ cx.ops_end = cx.ops + (sizeof(cx.ops) / sizeof(*cx.ops));
+ cx.iop = cx.ops;
+ op = cx.ops;
+ op->op = UNDEF;
+
+#if 1
+ job = &cx;
+ wakeup(&parse_wait);
+#else
+ oj_saj_parse(cx.json, &tcb, &cx);
+#endif
+ for (; op < cx.ops_end; op++) {
+ while (UNDEF == op->op) {
+ wait_for_it(&proc_wait, 0.0001);
+ }
+ switch (op->op) {
+ case HASH_START:
+ cx_push(&cx, rb_hash_new(), op->key);
+ break;
+ case HASH_END:
+ cx.cur--;
+ break;
+ case ARRAY_START:
+ cx_push(&cx, rb_ary_new(), op->key);
+ break;
+ case ARRAY_END:
+ cx.cur--;
+ break;
+ case ADD_STR:
+ {
+ VALUE s;
+
+ s = rb_str_new2(op->s);
+#if HAS_ENCODING_SUPPORT
+ rb_enc_associate(s, oj_utf8_encoding);
+#endif
+ cx_add(&cx, s, op->key);
+ }
+ break;
+ case ADD_BIG:
+ cx_add(&cx, rb_funcall(oj_bigdecimal_class, oj_new_id, 1, rb_str_new2(op->s)), op->key);
+ break;
+ case ADD_FLOAT:
+ cx_add(&cx, rb_float_new(op->d), op->key);
+ break;
+ case ADD_FIXNUM:
+ cx_add(&cx, LONG2NUM(op->i), op->key);
+ break;
+ case ADD_TRUE:
+ cx_add(&cx, Qtrue, op->key);
+ break;
+ case ADD_FALSE:
+ cx_add(&cx, Qfalse, op->key);
+ break;
+ case ADD_NIL:
+ cx_add(&cx, Qnil, op->key);
+ break;
+ case DONE:
+ return *cx.stack;
+ default:
+ // TBD raise
+ printf("*** unknown op type %d\n", op->op);
+ return Qnil;
+ }
+ }
+ return *cx.stack;
+}
+
+inline static void
+append_op(CX cx, OpType op, const char *key) {
+ (cx->iop + 1)->op = UNDEF;
+ cx->iop->key = key;
+ cx->iop->op = op;
+ wakeup(&proc_wait);
+ cx->iop++;
+}
+
+static void
+hash_start(void *context, const char *key) {
+ append_op((CX)context, HASH_START, key);
+}
+
+static void
+hash_end(void *context, const char *key) {
+ append_op((CX)context, HASH_END, key);
+}
+
+static void
+array_start(void *context, const char *key) {
+ append_op((CX)context, ARRAY_START, key);
+}
+
+static void
+array_end(void *context, const char *key) {
+ append_op((CX)context, ARRAY_END, key);
+}
+
+static void
+add_str(void *context, const char *str, const char *key) {
+ CX cx = (CX)context;
+
+ cx->iop->s = str;
+ append_op(cx, ADD_STR, key);
+}
+
+static void
+add_big(void *context, const char *str, const char *key) {
+ CX cx = (CX)context;
+
+ cx->iop->s = str;
+ append_op(cx, ADD_BIG, key);
+}
+
+static void
+add_float(void *context, double num, const char *key) {
+ CX cx = (CX)context;
+
+ cx->iop->d = num;
+ append_op(cx, ADD_FLOAT, key);
+}
+
+static void
+add_fixnum(void *context, int64_t num, const char *key) {
+ CX cx = (CX)context;
+
+ cx->iop->i = num;
+ append_op(cx, ADD_FIXNUM, key);
+}
+
+static void
+add_true(void *context, const char *key) {
+ append_op((CX)context, ADD_TRUE, key);
+}
+
+static void
+add_false(void *context, const char *key) {
+ append_op((CX)context, ADD_FALSE, key);
+}
+
+static void
+add_nil(void *context, const char *key) {
+ append_op((CX)context, ADD_NIL, key);
+}
+
+static void
+done(void *context) {
+ append_op((CX)context, DONE, 0);
+}
+
View
1  lib/oj.rb
@@ -28,5 +28,6 @@ module Oj
require 'oj/bag'
require 'oj/error'
require 'oj/mimic'
+require 'oj/saj'
require 'oj/oj' # C extension
View
47 notes
@@ -3,49 +3,12 @@
^c^d hide subtree
^c^s show subtree
-- prepop
- - write perf test for 10 arrays and some numbers
- - separate out new array function (inline)
- - create queue
- - circular with floating insert and pop pointers
- - mark unused as Qnil
- - if one not available, create it in the normal way
- - use for array, hash, and object
+- saj_parse (Simple API for JSON)
-- next
- - optimize read_hex in load.c
- - add options for path in fetch
- - wild cards
- - setup travis to run tests
- - does not load the oj gem in some cases
-- stream
- - use with Oj::Doc
- - use with standard
- - add callback SAX like option
- - callback parser
- - object_start
- - object_end
- - array_start
- - array_end
- - value - provide accessor call or object to get value
- - key - provide accessor call or object to get value
-
- - default callbacks use straight C and create Hash or Object according to mode
- - callback object
- - check respond_to? for each method at start
- - methods to indicate that defaults should be used instead of ignored
- - use_default_hash_callback?
- - use_default_array_callback?
- - use_default_key_callback?
- - use_default_value_callback?
- - or maybe
- - hash_callback? returns nil, :default, :my_hash_callback
- - default is to check hash_start and hash_end and no nothing if they do not exist
- -
-
- - dump
- - support stream as arg
- - always dump to stream/file if possible (check performance)
+---------------------------
+Tried a separate thread for the parser and the results were poor. The parsing is 10% to 15% of the total so splitting
+ruby calls and c does not help much and the overhead of swapping data was too high. It was not possible to split ruby
+calls into both threads due to not getting a lock on the ruby environment for object creation.
View
102 test/perf_saj.rb
@@ -0,0 +1,102 @@
+#!/usr/bin/env ruby -wW1
+# encoding: UTF-8
+
+$: << '.'
+$: << File.join(File.dirname(__FILE__), "../lib")
+$: << File.join(File.dirname(__FILE__), "../ext")
+
+require 'optparse'
+require 'yajl'
+require 'perf'
+require 'json'
+require 'json/ext'
+require 'oj'
+
+$verbose = false
+$indent = 0
+$iter = 10000
+$gets = 0
+$fetch = false
+$write = false
+$read = false
+
+opts = OptionParser.new
+opts.on("-v", "verbose") { $verbose = true }
+opts.on("-c", "--count [Int]", Integer, "iterations") { |i| $iter = i }
+opts.on("-i", "--indent [Int]", Integer, "indentation") { |i| $indent = i }
+opts.on("-g", "--gets [Int]", Integer, "number of gets") { |i| $gets = i }
+opts.on("-f", "fetch") { $fetch = true }
+opts.on("-w", "write") { $write = true }
+opts.on("-r", "read") { $read = true }
+opts.on("-h", "--help", "Show this display") { puts opts; Process.exit!(0) }
+files = opts.parse(ARGV)
+
+class AllSaj < Oj::Saj
+ def initialize()
+ end
+
+ def hash_start(key)
+ end
+
+ def hash_end(key)
+ end
+
+ def array_start(key)
+ end
+
+ def array_end(key)
+ end
+
+ def add_value(value, key)
+ end
+end # AllSaj
+
+$obj = {
+ 'a' => 'Alpha', # string
+ 'b' => true, # boolean
+ 'c' => 12345, # number
+ 'd' => [ true, [false, {'12345' => 12345, 'nil' => nil}, 3.967, { 'x' => 'something', 'y' => false, 'z' => true}, nil]], # mix it up array
+ 'e' => { 'one' => 1, 'two' => 2 }, # hash
+ 'f' => nil, # nil
+ 'g' => 12345678901234567890123456789, # big number
+ 'h' => { 'a' => { 'b' => { 'c' => { 'd' => {'e' => { 'f' => { 'g' => nil }}}}}}}, # deep hash, not that deep
+ 'i' => [[[[[[[nil]]]]]]] # deep array, again, not that deep
+}
+
+Oj.default_options = { :indent => $indent, :mode => :compat }
+
+$json = Oj.dump($obj)
+$failed = {} # key is same as String used in tests later
+
+def capture_error(tag, orig, load_key, dump_key, &blk)
+ begin
+ obj = blk.call(orig)
+ raise "#{tag} #{dump_key} and #{load_key} did not return the same object as the original." unless orig == obj
+ rescue Exception => e
+ $failed[tag] = "#{e.class}: #{e.message}"
+ end
+end
+
+# Verify that all packages dump and load correctly and return the same Object as the original.
+capture_error('Oj::Doc', $obj, 'load', 'dump') { |o| Oj::Doc.open(Oj.dump(o, :mode => :strict)) { |f| f.fetch() } }
+capture_error('Yajl', $obj, 'encode', 'parse') { |o| Yajl::Parser.parse(Yajl::Encoder.encode(o)) }
+capture_error('JSON::Ext', $obj, 'generate', 'parse') { |o| JSON.generator = JSON::Ext::Generator; JSON::Ext::Parser.new(JSON.generate(o)).parse }
+
+if $verbose
+ puts "json:\n#{$json}\n"
+end
+
+saj_handler = AllSaj.new()
+
+puts '-' * 80
+puts "Parse Performance"
+perf = Perf.new()
+perf.add('Oj::Saj', 'parse') { Oj.saj_parse(saj_handler, $json) } unless $failed.has_key?('Oj::Saj')
+perf.add('Yajl', 'parse') { Yajl::Parser.parse($json) } unless $failed.has_key?('Yajl')
+perf.add('JSON::Ext', 'parse') { JSON::Ext::Parser.new($json).parse } unless $failed.has_key?('JSON::Ext')
+perf.run($iter)
+
+unless $failed.empty?
+ puts "The following packages were not included for the reason listed"
+ $failed.each { |tag,msg| puts "***** #{tag}: #{msg}" }
+end
View
80 test/test_saj.rb
@@ -0,0 +1,80 @@
+#!/usr/bin/env ruby
+# encoding: UTF-8
+
+# Ubuntu does not accept arguments to ruby when called using env. To get warnings to show up the -w options is
+# required. That can be set in the RUBYOPT environment variable.
+# export RUBYOPT=-w
+
+$VERBOSE = true
+
+$: << File.join(File.dirname(__FILE__), "../lib")
+$: << File.join(File.dirname(__FILE__), "../ext")
+
+require 'test/unit'
+require 'oj'
+require 'pp'
+
+$json = %{{
+ "array": [
+ {
+ "num" : 3,
+ "string": "message",
+ "hash" : {
+ "h2" : {
+ "a" : [ 1, 2, 3 ]
+ }
+ }
+ }
+ ],
+ "boolean" : true
+}}
+
+class AllSaj < Oj::Saj
+ attr_accessor :calls
+
+ def initialize()
+ @calls = []
+ end
+
+ def hash_start(key)
+ @calls << [:hash_start, key]
+ end
+
+ def hash_end(key)
+ @calls << [:hash_end, key]
+ end
+
+ def array_start(key)
+ @calls << [:array_start, key]
+ end
+
+ def array_end(key)
+ @calls << [:array_end, key]
+ end
+
+ def add_value(value, key)
+ @calls << [:add_value, value, key]
+ end
+
+end # AllSaj
+
+class SajTest < ::Test::Unit::TestCase
+ def test_nil
+ handler = AllSaj.new()
+ json = %{null}
+ Oj.saj_parse(handler, json)
+ assert_equal(1, handler.calls.size)
+ assert_equal(:add_value, handler.calls[0][0])
+ assert_equal(nil, handler.calls[0][1])
+ end
+
+ # TBD other tests
+
+ def test_full
+ handler = AllSaj.new()
+ Oj.saj_parse(handler, $json)
+ pp handler.calls
+ end
+end
+
+
Please sign in to comment.
Something went wrong with that request. Please try again.