diff --git a/benchmark/encoder.rb b/benchmark/encoder.rb
index 5f3de6f5..92464cea 100644
--- a/benchmark/encoder.rb
+++ b/benchmark/encoder.rb
@@ -17,8 +17,10 @@
def implementations(ruby_obj)
state = JSON::State.new(JSON.dump_default_options)
+ coder = JSON::Coder.new
{
json: ["json", proc { JSON.generate(ruby_obj) }],
+ json_coder: ["json_coder", proc { coder.dump(ruby_obj) }],
oj: ["oj", proc { Oj.dump(ruby_obj) }],
}
end
diff --git a/benchmark/parser.rb b/benchmark/parser.rb
index bacb8e9e..8bf30c0f 100644
--- a/benchmark/parser.rb
+++ b/benchmark/parser.rb
@@ -15,9 +15,11 @@
def benchmark_parsing(name, json_output)
puts "== Parsing #{name} (#{json_output.size} bytes)"
+ coder = JSON::Coder.new
Benchmark.ips do |x|
x.report("json") { JSON.parse(json_output) } if RUN[:json]
+ x.report("json_coder") { coder.load(json_output) } if RUN[:json_coder]
x.report("oj") { Oj.load(json_output) } if RUN[:oj]
x.report("Oj::Parser") { Oj::Parser.new(:usual).parse(json_output) } if RUN[:oj]
x.report("rapidjson") { RapidJSON.parse(json_output) } if RUN[:rapidjson]
diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c
index 62c0c420..34f921d3 100644
--- a/ext/json/ext/generator/generator.c
+++ b/ext/json/ext/generator/generator.c
@@ -12,6 +12,7 @@ typedef struct JSON_Generator_StateStruct {
VALUE space_before;
VALUE object_nl;
VALUE array_nl;
+ VALUE as_json;
long max_nesting;
long depth;
@@ -30,8 +31,8 @@ typedef struct JSON_Generator_StateStruct {
static VALUE mJSON, cState, cFragment, mString_Extend, eGeneratorError, eNestingError, Encoding_UTF_8;
static ID i_to_s, i_to_json, i_new, i_pack, i_unpack, i_create_id, i_extend, i_encode;
-static ID sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan,
- sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict;
+static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan,
+ sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json;
#define GET_STATE_TO(self, state) \
@@ -648,6 +649,7 @@ static void State_mark(void *ptr)
rb_gc_mark_movable(state->space_before);
rb_gc_mark_movable(state->object_nl);
rb_gc_mark_movable(state->array_nl);
+ rb_gc_mark_movable(state->as_json);
}
static void State_compact(void *ptr)
@@ -658,6 +660,7 @@ static void State_compact(void *ptr)
state->space_before = rb_gc_location(state->space_before);
state->object_nl = rb_gc_location(state->object_nl);
state->array_nl = rb_gc_location(state->array_nl);
+ state->as_json = rb_gc_location(state->as_json);
}
static void State_free(void *ptr)
@@ -714,6 +717,7 @@ static void vstate_spill(struct generate_json_data *data)
RB_OBJ_WRITTEN(vstate, Qundef, state->space_before);
RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl);
RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl);
+ RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
}
static inline VALUE vstate_get(struct generate_json_data *data)
@@ -982,6 +986,8 @@ static void generate_json_fragment(FBuffer *buffer, struct generate_json_data *d
static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj)
{
VALUE tmp;
+ bool as_json_called = false;
+start:
if (obj == Qnil) {
generate_json_null(buffer, data, state, obj);
} else if (obj == Qfalse) {
@@ -1025,7 +1031,13 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON
default:
general:
if (state->strict) {
- raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
+ if (RTEST(state->as_json) && !as_json_called) {
+ obj = rb_proc_call_with_block(state->as_json, 1, &obj, Qnil);
+ as_json_called = true;
+ goto start;
+ } else {
+ raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
+ }
} else if (rb_respond_to(obj, i_to_json)) {
tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data));
Check_Type(tmp, T_STRING);
@@ -1126,6 +1138,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig)
objState->space_before = origState->space_before;
objState->object_nl = origState->object_nl;
objState->array_nl = origState->array_nl;
+ objState->as_json = origState->as_json;
return obj;
}
@@ -1277,6 +1290,28 @@ static VALUE cState_array_nl_set(VALUE self, VALUE array_nl)
return Qnil;
}
+/*
+ * call-seq: as_json()
+ *
+ * This string is put at the end of a line that holds a JSON array.
+ */
+static VALUE cState_as_json(VALUE self)
+{
+ GET_STATE(self);
+ return state->as_json;
+}
+
+/*
+ * call-seq: as_json=(as_json)
+ *
+ * This string is put at the end of a line that holds a JSON array.
+ */
+static VALUE cState_as_json_set(VALUE self, VALUE as_json)
+{
+ GET_STATE(self);
+ RB_OBJ_WRITE(self, &state->as_json, rb_convert_type(as_json, T_DATA, "Proc", "to_proc"));
+ return Qnil;
+}
/*
* call-seq: check_circular?
@@ -1498,6 +1533,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
else if (key == sym_script_safe) { state->script_safe = RTEST(val); }
else if (key == sym_escape_slash) { state->script_safe = RTEST(val); }
else if (key == sym_strict) { state->strict = RTEST(val); }
+ else if (key == sym_as_json) { state->as_json = rb_convert_type(val, T_DATA, "Proc", "to_proc"); }
return ST_CONTINUE;
}
@@ -1589,6 +1625,8 @@ void Init_generator(void)
rb_define_method(cState, "object_nl=", cState_object_nl_set, 1);
rb_define_method(cState, "array_nl", cState_array_nl, 0);
rb_define_method(cState, "array_nl=", cState_array_nl_set, 1);
+ rb_define_method(cState, "as_json", cState_as_json, 0);
+ rb_define_method(cState, "as_json=", cState_as_json_set, 1);
rb_define_method(cState, "max_nesting", cState_max_nesting, 0);
rb_define_method(cState, "max_nesting=", cState_max_nesting_set, 1);
rb_define_method(cState, "script_safe", cState_script_safe, 0);
@@ -1680,6 +1718,7 @@ void Init_generator(void)
sym_script_safe = ID2SYM(rb_intern("script_safe"));
sym_escape_slash = ID2SYM(rb_intern("escape_slash"));
sym_strict = ID2SYM(rb_intern("strict"));
+ sym_as_json = ID2SYM(rb_intern("as_json"));
usascii_encindex = rb_usascii_encindex();
utf8_encindex = rb_utf8_encindex();
diff --git a/java/src/json/ext/Generator.java b/java/src/json/ext/Generator.java
index 4ab92805..66986927 100644
--- a/java/src/json/ext/Generator.java
+++ b/java/src/json/ext/Generator.java
@@ -510,6 +510,14 @@ void generate(ThreadContext context, Session session, IRubyObject object, Output
RubyString generateNew(ThreadContext context, Session session, IRubyObject object) {
GeneratorState state = session.getState(context);
if (state.strict()) {
+ if (state.getAsJSON() != null ) {
+ IRubyObject value = state.getAsJSON().call(context, object);
+ Handler handler = getHandlerFor(context.runtime, value);
+ if (handler == GENERIC_HANDLER) {
+ throw Utils.buildGeneratorError(context, object, value + " returned by as_json not allowed in JSON").toThrowable();
+ }
+ return handler.generateNew(context, session, value);
+ }
throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable();
} else if (object.respondsTo("to_json")) {
IRubyObject result = object.callMethod(context, "to_json", state);
diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java
index 92d0c49a..e51192ef 100644
--- a/java/src/json/ext/GeneratorState.java
+++ b/java/src/json/ext/GeneratorState.java
@@ -14,6 +14,7 @@
import org.jruby.RubyInteger;
import org.jruby.RubyNumeric;
import org.jruby.RubyObject;
+import org.jruby.RubyProc;
import org.jruby.RubyString;
import org.jruby.anno.JRubyMethod;
import org.jruby.runtime.Block;
@@ -22,6 +23,7 @@
import org.jruby.runtime.Visibility;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
+import org.jruby.util.TypeConverter;
/**
* The JSON::Ext::Generator::State
class.
@@ -58,6 +60,8 @@ public class GeneratorState extends RubyObject {
*/
private ByteList arrayNl = ByteList.EMPTY_BYTELIST;
+ private RubyProc asJSON;
+
/**
* The maximum level of nesting of structures allowed.
* 0
means disabled.
@@ -211,6 +215,7 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) {
this.spaceBefore = orig.spaceBefore;
this.objectNl = orig.objectNl;
this.arrayNl = orig.arrayNl;
+ this.asJSON = orig.asJSON;
this.maxNesting = orig.maxNesting;
this.allowNaN = orig.allowNaN;
this.asciiOnly = orig.asciiOnly;
@@ -353,6 +358,22 @@ public IRubyObject array_nl_set(ThreadContext context,
return arrayNl;
}
+ public RubyProc getAsJSON() {
+ return asJSON;
+ }
+
+ @JRubyMethod(name="as_json")
+ public IRubyObject as_json_get(ThreadContext context) {
+ return asJSON == null ? context.getRuntime().getFalse() : asJSON;
+ }
+
+ @JRubyMethod(name="as_json=")
+ public IRubyObject as_json_set(ThreadContext context,
+ IRubyObject asJSON) {
+ this.asJSON = (RubyProc)TypeConverter.convertToType(asJSON, context.getRuntime().getProc(), "to_proc");
+ return asJSON;
+ }
+
@JRubyMethod(name="check_circular?")
public RubyBoolean check_circular_p(ThreadContext context) {
return RubyBoolean.newBoolean(context, maxNesting != 0);
@@ -487,6 +508,8 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) {
ByteList arrayNl = opts.getString("array_nl");
if (arrayNl != null) this.arrayNl = arrayNl;
+ this.asJSON = opts.getProc("as_json");
+
ByteList objectNl = opts.getString("object_nl");
if (objectNl != null) this.objectNl = objectNl;
@@ -522,6 +545,7 @@ public RubyHash to_h(ThreadContext context) {
result.op_aset(context, runtime.newSymbol("space_before"), space_before_get(context));
result.op_aset(context, runtime.newSymbol("object_nl"), object_nl_get(context));
result.op_aset(context, runtime.newSymbol("array_nl"), array_nl_get(context));
+ result.op_aset(context, runtime.newSymbol("as_json"), as_json_get(context));
result.op_aset(context, runtime.newSymbol("allow_nan"), allow_nan_p(context));
result.op_aset(context, runtime.newSymbol("ascii_only"), ascii_only_p(context));
result.op_aset(context, runtime.newSymbol("max_nesting"), max_nesting_get(context));
diff --git a/java/src/json/ext/OptionsReader.java b/java/src/json/ext/OptionsReader.java
index ff976c38..985bc018 100644
--- a/java/src/json/ext/OptionsReader.java
+++ b/java/src/json/ext/OptionsReader.java
@@ -10,10 +10,12 @@
import org.jruby.RubyClass;
import org.jruby.RubyHash;
import org.jruby.RubyNumeric;
+import org.jruby.RubyProc;
import org.jruby.RubyString;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import org.jruby.util.ByteList;
+import org.jruby.util.TypeConverter;
final class OptionsReader {
private final ThreadContext context;
@@ -110,4 +112,10 @@ public RubyHash getHash(String key) {
if (value == null || value.isNil()) return new RubyHash(runtime);
return (RubyHash) value;
}
+
+ RubyProc getProc(String key) {
+ IRubyObject value = get(key);
+ if (value == null) return null;
+ return (RubyProc)TypeConverter.convertToType(value, runtime.getProc(), "to_proc");
+ }
}
diff --git a/lib/json/common.rb b/lib/json/common.rb
index a9682b94..2ba8c43d 100644
--- a/lib/json/common.rb
+++ b/lib/json/common.rb
@@ -844,6 +844,27 @@ def merge_dump_options(opts, strict: NOT_SET)
class << self
private :merge_dump_options
end
+
+ class Coder
+ def initialize(options = nil, &as_json)
+ if options.nil?
+ options = { strict: true }
+ else
+ options[:strict] = true
+ end
+ options[:as_json] = as_json if as_json
+ @state = State.new(options)
+ @parser_config = Ext::Parser::Config.new(options)
+ end
+
+ def dump(...)
+ @state.generate(...)
+ end
+
+ def load(source)
+ @parser_config.parse(source)
+ end
+ end
end
module ::Kernel
diff --git a/lib/json/ext/generator/state.rb b/lib/json/ext/generator/state.rb
index 6cd9496e..d40c3b5e 100644
--- a/lib/json/ext/generator/state.rb
+++ b/lib/json/ext/generator/state.rb
@@ -58,6 +58,7 @@ def to_h
space_before: space_before,
object_nl: object_nl,
array_nl: array_nl,
+ as_json: as_json,
allow_nan: allow_nan?,
ascii_only: ascii_only?,
max_nesting: max_nesting,
diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb
index f73263cd..5467fa6f 100644
--- a/lib/json/truffle_ruby/generator.rb
+++ b/lib/json/truffle_ruby/generator.rb
@@ -142,6 +142,7 @@ def initialize(opts = nil)
@array_nl = ''
@allow_nan = false
@ascii_only = false
+ @as_json = false
@depth = 0
@buffer_initial_length = 1024
@script_safe = false
@@ -167,6 +168,9 @@ def initialize(opts = nil)
# This string is put at the end of a line that holds a JSON array.
attr_accessor :array_nl
+ # This proc converts unsupported types into native JSON types.
+ attr_accessor :as_json
+
# This integer returns the maximum level of data structure nesting in
# the generated JSON, max_nesting = 0 if no maximum is checked.
attr_accessor :max_nesting
@@ -251,6 +255,7 @@ def configure(opts)
@object_nl = opts[:object_nl] || '' if opts.key?(:object_nl)
@array_nl = opts[:array_nl] || '' if opts.key?(:array_nl)
@allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
+ @as_json = opts[:as_json].to_proc if opts.key?(:as_json)
@ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
@depth = opts[:depth] || 0
@buffer_initial_length ||= opts[:buffer_initial_length]
@@ -403,8 +408,20 @@ module Object
# it to a JSON string, and returns the result. This is a fallback, if no
# special method #to_json was defined for some object.
def to_json(state = nil, *)
- if state && State.from_state(state).strict?
- raise GeneratorError.new("#{self.class} not allowed in JSON", self)
+ state = State.from_state(state) if state
+ if state&.strict?
+ value = self
+ if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
+ if state.as_json
+ value = state.as_json.call(value)
+ unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value
+ raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
+ end
+ value.to_json(state)
+ else
+ raise GeneratorError.new("#{value.class} not allowed in JSON", value)
+ end
+ end
else
to_s.to_json
end
@@ -455,7 +472,15 @@ def json_transform(state)
result = +"#{result}#{key_json}#{state.space_before}:#{state.space}"
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
- raise GeneratorError.new("#{value.class} not allowed in JSON", value)
+ if state.as_json
+ value = state.as_json.call(value)
+ unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value
+ raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
+ end
+ result << value.to_json(state)
+ else
+ raise GeneratorError.new("#{value.class} not allowed in JSON", value)
+ end
elsif value.respond_to?(:to_json)
result << value.to_json(state)
else
@@ -508,7 +533,15 @@ def json_transform(state)
result << delim unless first
result << state.indent * depth if indent
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
- raise GeneratorError.new("#{value.class} not allowed in JSON", value)
+ if state.as_json
+ value = state.as_json.call(value)
+ unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value
+ raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
+ end
+ result << value.to_json(state)
+ else
+ raise GeneratorError.new("#{value.class} not allowed in JSON", value)
+ end
elsif value.respond_to?(:to_json)
result << value.to_json(state)
else
diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb
new file mode 100755
index 00000000..37331c4e
--- /dev/null
+++ b/test/json/json_coder_test.rb
@@ -0,0 +1,38 @@
+#!/usr/bin/env ruby
+# frozen_string_literal: true
+
+require_relative 'test_helper'
+
+class JSONCoderTest < Test::Unit::TestCase
+ def test_json_coder_with_proc
+ coder = JSON::Coder.new do |object|
+ "[Object object]"
+ end
+ assert_equal %(["[Object object]"]), coder.dump([Object.new])
+ end
+
+ def test_json_coder_with_proc_with_unsupported_value
+ coder = JSON::Coder.new do |object|
+ Object.new
+ end
+ assert_raise(JSON::GeneratorError) { coder.dump([Object.new]) }
+ end
+
+ def test_json_coder_options
+ coder = JSON::Coder.new(array_nl: "\n") do |object|
+ 42
+ end
+
+ assert_equal "[\n42\n]", coder.dump([Object.new])
+ end
+
+ def test_json_coder_load
+ coder = JSON::Coder.new
+ assert_equal [1,2,3], coder.load("[1,2,3]")
+ end
+
+ def test_json_coder_load_options
+ coder = JSON::Coder.new(symbolize_names: true)
+ assert_equal({a: 1}, coder.load('{"a":1}'))
+ end
+end
diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb
index 824de2c1..92115637 100755
--- a/test/json/json_generator_test.rb
+++ b/test/json/json_generator_test.rb
@@ -200,6 +200,7 @@ def test_pretty_state
assert_equal({
:allow_nan => false,
:array_nl => "\n",
+ :as_json => false,
:ascii_only => false,
:buffer_initial_length => 1024,
:depth => 0,
@@ -218,6 +219,7 @@ def test_safe_state
assert_equal({
:allow_nan => false,
:array_nl => "",
+ :as_json => false,
:ascii_only => false,
:buffer_initial_length => 1024,
:depth => 0,
@@ -236,6 +238,7 @@ def test_fast_state
assert_equal({
:allow_nan => false,
:array_nl => "",
+ :as_json => false,
:ascii_only => false,
:buffer_initial_length => 1024,
:depth => 0,
@@ -666,4 +669,9 @@ def test_fragment
fragment = JSON::Fragment.new(" 42")
assert_equal '{"number": 42}', JSON.generate({ number: fragment })
end
+
+ def test_json_generate_as_json_convert_to_proc
+ object = Object.new
+ assert_equal object.object_id.to_json, JSON.generate(object, strict: true, as_json: :object_id)
+ end
end