Skip to content

Commit a086f4b

Browse files
etiennebarriebyroot
andcommitted
Introduce JSON::Coder
Co-authored-by: Jean Boussier <[email protected]>
1 parent f8817fe commit a086f4b

File tree

11 files changed

+191
-7
lines changed

11 files changed

+191
-7
lines changed

benchmark/encoder.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,10 @@
1717

1818
def implementations(ruby_obj)
1919
state = JSON::State.new(JSON.dump_default_options)
20+
coder = JSON::Coder.new
2021
{
2122
json: ["json", proc { JSON.generate(ruby_obj) }],
23+
json_coder: ["json_coder", proc { coder.dump(ruby_obj) }],
2224
oj: ["oj", proc { Oj.dump(ruby_obj) }],
2325
}
2426
end

benchmark/parser.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,11 @@
1515

1616
def benchmark_parsing(name, json_output)
1717
puts "== Parsing #{name} (#{json_output.size} bytes)"
18+
coder = JSON::Coder.new
1819

1920
Benchmark.ips do |x|
2021
x.report("json") { JSON.parse(json_output) } if RUN[:json]
22+
x.report("json_coder") { coder.load(json_output) } if RUN[:json_coder]
2123
x.report("oj") { Oj.load(json_output) } if RUN[:oj]
2224
x.report("Oj::Parser") { Oj::Parser.new(:usual).parse(json_output) } if RUN[:oj]
2325
x.report("rapidjson") { RapidJSON.parse(json_output) } if RUN[:rapidjson]

ext/json/ext/generator/generator.c

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ typedef struct JSON_Generator_StateStruct {
1212
VALUE space_before;
1313
VALUE object_nl;
1414
VALUE array_nl;
15+
VALUE as_json;
1516

1617
long max_nesting;
1718
long depth;
@@ -30,8 +31,8 @@ typedef struct JSON_Generator_StateStruct {
3031
static VALUE mJSON, cState, cFragment, mString_Extend, eGeneratorError, eNestingError, Encoding_UTF_8;
3132

3233
static ID i_to_s, i_to_json, i_new, i_pack, i_unpack, i_create_id, i_extend, i_encode;
33-
static ID sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan,
34-
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict;
34+
static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan,
35+
sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_escape_slash, sym_strict, sym_as_json;
3536

3637

3738
#define GET_STATE_TO(self, state) \
@@ -648,6 +649,7 @@ static void State_mark(void *ptr)
648649
rb_gc_mark_movable(state->space_before);
649650
rb_gc_mark_movable(state->object_nl);
650651
rb_gc_mark_movable(state->array_nl);
652+
rb_gc_mark_movable(state->as_json);
651653
}
652654

653655
static void State_compact(void *ptr)
@@ -658,6 +660,7 @@ static void State_compact(void *ptr)
658660
state->space_before = rb_gc_location(state->space_before);
659661
state->object_nl = rb_gc_location(state->object_nl);
660662
state->array_nl = rb_gc_location(state->array_nl);
663+
state->as_json = rb_gc_location(state->as_json);
661664
}
662665

663666
static void State_free(void *ptr)
@@ -714,6 +717,7 @@ static void vstate_spill(struct generate_json_data *data)
714717
RB_OBJ_WRITTEN(vstate, Qundef, state->space_before);
715718
RB_OBJ_WRITTEN(vstate, Qundef, state->object_nl);
716719
RB_OBJ_WRITTEN(vstate, Qundef, state->array_nl);
720+
RB_OBJ_WRITTEN(vstate, Qundef, state->as_json);
717721
}
718722

719723
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
982986
static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON_Generator_State *state, VALUE obj)
983987
{
984988
VALUE tmp;
989+
bool as_json_called = false;
990+
start:
985991
if (obj == Qnil) {
986992
generate_json_null(buffer, data, state, obj);
987993
} else if (obj == Qfalse) {
@@ -1025,7 +1031,13 @@ static void generate_json(FBuffer *buffer, struct generate_json_data *data, JSON
10251031
default:
10261032
general:
10271033
if (state->strict) {
1028-
raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
1034+
if (RTEST(state->as_json) && !as_json_called) {
1035+
obj = rb_proc_call_with_block(state->as_json, 1, &obj, Qnil);
1036+
as_json_called = true;
1037+
goto start;
1038+
} else {
1039+
raise_generator_error(obj, "%"PRIsVALUE" not allowed in JSON", CLASS_OF(obj));
1040+
}
10291041
} else if (rb_respond_to(obj, i_to_json)) {
10301042
tmp = rb_funcall(obj, i_to_json, 1, vstate_get(data));
10311043
Check_Type(tmp, T_STRING);
@@ -1126,6 +1138,7 @@ static VALUE cState_init_copy(VALUE obj, VALUE orig)
11261138
objState->space_before = origState->space_before;
11271139
objState->object_nl = origState->object_nl;
11281140
objState->array_nl = origState->array_nl;
1141+
objState->as_json = origState->as_json;
11291142
return obj;
11301143
}
11311144

@@ -1277,6 +1290,28 @@ static VALUE cState_array_nl_set(VALUE self, VALUE array_nl)
12771290
return Qnil;
12781291
}
12791292

1293+
/*
1294+
* call-seq: as_json()
1295+
*
1296+
* This string is put at the end of a line that holds a JSON array.
1297+
*/
1298+
static VALUE cState_as_json(VALUE self)
1299+
{
1300+
GET_STATE(self);
1301+
return state->as_json;
1302+
}
1303+
1304+
/*
1305+
* call-seq: as_json=(as_json)
1306+
*
1307+
* This string is put at the end of a line that holds a JSON array.
1308+
*/
1309+
static VALUE cState_as_json_set(VALUE self, VALUE as_json)
1310+
{
1311+
GET_STATE(self);
1312+
RB_OBJ_WRITE(self, &state->as_json, rb_convert_type(as_json, T_DATA, "Proc", "to_proc"));
1313+
return Qnil;
1314+
}
12801315

12811316
/*
12821317
* call-seq: check_circular?
@@ -1498,6 +1533,7 @@ static int configure_state_i(VALUE key, VALUE val, VALUE _arg)
14981533
else if (key == sym_script_safe) { state->script_safe = RTEST(val); }
14991534
else if (key == sym_escape_slash) { state->script_safe = RTEST(val); }
15001535
else if (key == sym_strict) { state->strict = RTEST(val); }
1536+
else if (key == sym_as_json) { state->as_json = rb_convert_type(val, T_DATA, "Proc", "to_proc"); }
15011537
return ST_CONTINUE;
15021538
}
15031539

@@ -1589,6 +1625,8 @@ void Init_generator(void)
15891625
rb_define_method(cState, "object_nl=", cState_object_nl_set, 1);
15901626
rb_define_method(cState, "array_nl", cState_array_nl, 0);
15911627
rb_define_method(cState, "array_nl=", cState_array_nl_set, 1);
1628+
rb_define_method(cState, "as_json", cState_as_json, 0);
1629+
rb_define_method(cState, "as_json=", cState_as_json_set, 1);
15921630
rb_define_method(cState, "max_nesting", cState_max_nesting, 0);
15931631
rb_define_method(cState, "max_nesting=", cState_max_nesting_set, 1);
15941632
rb_define_method(cState, "script_safe", cState_script_safe, 0);
@@ -1680,6 +1718,7 @@ void Init_generator(void)
16801718
sym_script_safe = ID2SYM(rb_intern("script_safe"));
16811719
sym_escape_slash = ID2SYM(rb_intern("escape_slash"));
16821720
sym_strict = ID2SYM(rb_intern("strict"));
1721+
sym_as_json = ID2SYM(rb_intern("as_json"));
16831722

16841723
usascii_encindex = rb_usascii_encindex();
16851724
utf8_encindex = rb_utf8_encindex();

java/src/json/ext/Generator.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,14 @@ void generate(ThreadContext context, Session session, IRubyObject object, Output
510510
RubyString generateNew(ThreadContext context, Session session, IRubyObject object) {
511511
GeneratorState state = session.getState(context);
512512
if (state.strict()) {
513+
if (state.getAsJSON() != null ) {
514+
IRubyObject value = state.getAsJSON().call(context, object);
515+
Handler handler = getHandlerFor(context.runtime, value);
516+
if (handler == GENERIC_HANDLER) {
517+
throw Utils.buildGeneratorError(context, object, value + " returned by as_json not allowed in JSON").toThrowable();
518+
}
519+
return handler.generateNew(context, session, value);
520+
}
513521
throw Utils.buildGeneratorError(context, object, object + " not allowed in JSON").toThrowable();
514522
} else if (object.respondsTo("to_json")) {
515523
IRubyObject result = object.callMethod(context, "to_json", state);

java/src/json/ext/GeneratorState.java

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
import org.jruby.RubyInteger;
1515
import org.jruby.RubyNumeric;
1616
import org.jruby.RubyObject;
17+
import org.jruby.RubyProc;
1718
import org.jruby.RubyString;
1819
import org.jruby.anno.JRubyMethod;
1920
import org.jruby.runtime.Block;
@@ -22,6 +23,7 @@
2223
import org.jruby.runtime.Visibility;
2324
import org.jruby.runtime.builtin.IRubyObject;
2425
import org.jruby.util.ByteList;
26+
import org.jruby.util.TypeConverter;
2527

2628
/**
2729
* The <code>JSON::Ext::Generator::State</code> class.
@@ -58,6 +60,8 @@ public class GeneratorState extends RubyObject {
5860
*/
5961
private ByteList arrayNl = ByteList.EMPTY_BYTELIST;
6062

63+
private RubyProc asJSON;
64+
6165
/**
6266
* The maximum level of nesting of structures allowed.
6367
* <code>0</code> means disabled.
@@ -211,6 +215,7 @@ public IRubyObject initialize_copy(ThreadContext context, IRubyObject vOrig) {
211215
this.spaceBefore = orig.spaceBefore;
212216
this.objectNl = orig.objectNl;
213217
this.arrayNl = orig.arrayNl;
218+
this.asJSON = orig.asJSON;
214219
this.maxNesting = orig.maxNesting;
215220
this.allowNaN = orig.allowNaN;
216221
this.asciiOnly = orig.asciiOnly;
@@ -353,6 +358,22 @@ public IRubyObject array_nl_set(ThreadContext context,
353358
return arrayNl;
354359
}
355360

361+
public RubyProc getAsJSON() {
362+
return asJSON;
363+
}
364+
365+
@JRubyMethod(name="as_json")
366+
public IRubyObject as_json_get(ThreadContext context) {
367+
return asJSON == null ? context.getRuntime().getFalse() : asJSON;
368+
}
369+
370+
@JRubyMethod(name="as_json=")
371+
public IRubyObject as_json_set(ThreadContext context,
372+
IRubyObject asJSON) {
373+
this.asJSON = (RubyProc)TypeConverter.convertToType(asJSON, context.getRuntime().getProc(), "to_proc");
374+
return asJSON;
375+
}
376+
356377
@JRubyMethod(name="check_circular?")
357378
public RubyBoolean check_circular_p(ThreadContext context) {
358379
return RubyBoolean.newBoolean(context, maxNesting != 0);
@@ -487,6 +508,8 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) {
487508
ByteList arrayNl = opts.getString("array_nl");
488509
if (arrayNl != null) this.arrayNl = arrayNl;
489510

511+
this.asJSON = opts.getProc("as_json");
512+
490513
ByteList objectNl = opts.getString("object_nl");
491514
if (objectNl != null) this.objectNl = objectNl;
492515

@@ -522,6 +545,7 @@ public RubyHash to_h(ThreadContext context) {
522545
result.op_aset(context, runtime.newSymbol("space_before"), space_before_get(context));
523546
result.op_aset(context, runtime.newSymbol("object_nl"), object_nl_get(context));
524547
result.op_aset(context, runtime.newSymbol("array_nl"), array_nl_get(context));
548+
result.op_aset(context, runtime.newSymbol("as_json"), as_json_get(context));
525549
result.op_aset(context, runtime.newSymbol("allow_nan"), allow_nan_p(context));
526550
result.op_aset(context, runtime.newSymbol("ascii_only"), ascii_only_p(context));
527551
result.op_aset(context, runtime.newSymbol("max_nesting"), max_nesting_get(context));

java/src/json/ext/OptionsReader.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,12 @@
1010
import org.jruby.RubyClass;
1111
import org.jruby.RubyHash;
1212
import org.jruby.RubyNumeric;
13+
import org.jruby.RubyProc;
1314
import org.jruby.RubyString;
1415
import org.jruby.runtime.ThreadContext;
1516
import org.jruby.runtime.builtin.IRubyObject;
1617
import org.jruby.util.ByteList;
18+
import org.jruby.util.TypeConverter;
1719

1820
final class OptionsReader {
1921
private final ThreadContext context;
@@ -110,4 +112,10 @@ public RubyHash getHash(String key) {
110112
if (value == null || value.isNil()) return new RubyHash(runtime);
111113
return (RubyHash) value;
112114
}
115+
116+
RubyProc getProc(String key) {
117+
IRubyObject value = get(key);
118+
if (value == null) return null;
119+
return (RubyProc)TypeConverter.convertToType(value, runtime.getProc(), "to_proc");
120+
}
113121
}

lib/json/common.rb

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -844,6 +844,27 @@ def merge_dump_options(opts, strict: NOT_SET)
844844
class << self
845845
private :merge_dump_options
846846
end
847+
848+
class Coder
849+
def initialize(options = nil, &as_json)
850+
if options.nil?
851+
options = { strict: true }
852+
else
853+
options[:strict] = true
854+
end
855+
options[:as_json] = as_json if as_json
856+
@state = State.new(options)
857+
@parser_config = Ext::Parser::Config.new(options)
858+
end
859+
860+
def dump(...)
861+
@state.generate(...)
862+
end
863+
864+
def load(source)
865+
@parser_config.parse(source)
866+
end
867+
end
847868
end
848869

849870
module ::Kernel

lib/json/ext/generator/state.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ def to_h
5858
space_before: space_before,
5959
object_nl: object_nl,
6060
array_nl: array_nl,
61+
as_json: as_json,
6162
allow_nan: allow_nan?,
6263
ascii_only: ascii_only?,
6364
max_nesting: max_nesting,

lib/json/truffle_ruby/generator.rb

Lines changed: 37 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ def initialize(opts = nil)
142142
@array_nl = ''
143143
@allow_nan = false
144144
@ascii_only = false
145+
@as_json = false
145146
@depth = 0
146147
@buffer_initial_length = 1024
147148
@script_safe = false
@@ -167,6 +168,9 @@ def initialize(opts = nil)
167168
# This string is put at the end of a line that holds a JSON array.
168169
attr_accessor :array_nl
169170

171+
# This proc converts unsupported types into native JSON types.
172+
attr_accessor :as_json
173+
170174
# This integer returns the maximum level of data structure nesting in
171175
# the generated JSON, max_nesting = 0 if no maximum is checked.
172176
attr_accessor :max_nesting
@@ -251,6 +255,7 @@ def configure(opts)
251255
@object_nl = opts[:object_nl] || '' if opts.key?(:object_nl)
252256
@array_nl = opts[:array_nl] || '' if opts.key?(:array_nl)
253257
@allow_nan = !!opts[:allow_nan] if opts.key?(:allow_nan)
258+
@as_json = opts[:as_json].to_proc if opts.key?(:as_json)
254259
@ascii_only = opts[:ascii_only] if opts.key?(:ascii_only)
255260
@depth = opts[:depth] || 0
256261
@buffer_initial_length ||= opts[:buffer_initial_length]
@@ -403,8 +408,20 @@ module Object
403408
# it to a JSON string, and returns the result. This is a fallback, if no
404409
# special method #to_json was defined for some object.
405410
def to_json(state = nil, *)
406-
if state && State.from_state(state).strict?
407-
raise GeneratorError.new("#{self.class} not allowed in JSON", self)
411+
state = State.from_state(state) if state
412+
if state&.strict?
413+
value = self
414+
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
415+
if state.as_json
416+
value = state.as_json.call(value)
417+
unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value
418+
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
419+
end
420+
value.to_json(state)
421+
else
422+
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
423+
end
424+
end
408425
else
409426
to_s.to_json
410427
end
@@ -455,7 +472,15 @@ def json_transform(state)
455472

456473
result = +"#{result}#{key_json}#{state.space_before}:#{state.space}"
457474
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
458-
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
475+
if state.as_json
476+
value = state.as_json.call(value)
477+
unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value
478+
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
479+
end
480+
result << value.to_json(state)
481+
else
482+
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
483+
end
459484
elsif value.respond_to?(:to_json)
460485
result << value.to_json(state)
461486
else
@@ -508,7 +533,15 @@ def json_transform(state)
508533
result << delim unless first
509534
result << state.indent * depth if indent
510535
if state.strict? && !(false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value)
511-
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
536+
if state.as_json
537+
value = state.as_json.call(value)
538+
unless false == value || true == value || nil == value || String === value || Array === value || Hash === value || Integer === value || Float === value
539+
raise GeneratorError.new("#{value.class} returned by #{state.as_json} not allowed in JSON", value)
540+
end
541+
result << value.to_json(state)
542+
else
543+
raise GeneratorError.new("#{value.class} not allowed in JSON", value)
544+
end
512545
elsif value.respond_to?(:to_json)
513546
result << value.to_json(state)
514547
else

0 commit comments

Comments
 (0)