Introduce JSON::Coder
Co-authored-by: Jean Boussier <jean.boussier@gmail.com>
This commit is contained in:
parent
53cf2170f9
commit
89e316ad06
@ -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) {
|
||||
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);
|
||||
@ -1610,6 +1648,7 @@ void Init_generator(void)
|
||||
rb_define_method(cState, "buffer_initial_length", cState_buffer_initial_length, 0);
|
||||
rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1);
|
||||
rb_define_method(cState, "generate", cState_generate, -1);
|
||||
rb_define_alias(cState, "generate_new", "generate"); // :nodoc:
|
||||
|
||||
rb_define_singleton_method(cState, "generate", cState_m_generate, 3);
|
||||
|
||||
@ -1680,6 +1719,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();
|
||||
|
@ -174,7 +174,18 @@ module JSON
|
||||
# This allows to easily assemble multiple JSON fragments that have
|
||||
# been peristed somewhere without having to parse them nor resorting
|
||||
# to string interpolation.
|
||||
#
|
||||
# Note: no validation is performed on the provided string. it is the
|
||||
# responsability of the caller to ensure the string contains valid JSON.
|
||||
Fragment = Struct.new(:json) do
|
||||
def initialize(json)
|
||||
unless string = String.try_convert(json)
|
||||
raise TypeError, " no implicit conversion of #{json.class} into String"
|
||||
end
|
||||
|
||||
super(string)
|
||||
end
|
||||
|
||||
def to_json(state = nil, *)
|
||||
json
|
||||
end
|
||||
@ -851,6 +862,82 @@ module JSON
|
||||
class << self
|
||||
private :merge_dump_options
|
||||
end
|
||||
|
||||
# JSON::Coder holds a parser and generator configuration.
|
||||
#
|
||||
# module MyApp
|
||||
# JSONC_CODER = JSON::Coder.new(
|
||||
# allow_trailing_comma: true
|
||||
# )
|
||||
# end
|
||||
#
|
||||
# MyApp::JSONC_CODER.load(document)
|
||||
#
|
||||
class Coder
|
||||
# :call-seq:
|
||||
# JSON.new(options = nil, &block)
|
||||
#
|
||||
# Argument +options+, if given, contains a \Hash of options for both parsing and generating.
|
||||
# See {Parsing Options}[#module-JSON-label-Parsing+Options], and {Generating Options}[#module-JSON-label-Generating+Options].
|
||||
#
|
||||
# For generation, the <tt>strict: true</tt> option is always set. When a Ruby object with no native \JSON counterpart is
|
||||
# encoutered, the block provided to the initialize method is invoked, and must return a Ruby object that has a native
|
||||
# \JSON counterpart:
|
||||
#
|
||||
# module MyApp
|
||||
# API_JSON_CODER = JSON::Coder.new do |object|
|
||||
# case object
|
||||
# when Time
|
||||
# object.iso8601(3)
|
||||
# else
|
||||
# object # Unknown type, will raise
|
||||
# end
|
||||
# end
|
||||
# end
|
||||
#
|
||||
# puts MyApp::API_JSON_CODER.dump(Time.now.utc) # => "2025-01-21T08:41:44.286Z"
|
||||
#
|
||||
def initialize(options = nil, &as_json)
|
||||
if options.nil?
|
||||
options = { strict: true }
|
||||
else
|
||||
options = options.dup
|
||||
options[:strict] = true
|
||||
end
|
||||
options[:as_json] = as_json if as_json
|
||||
options[:create_additions] = false unless options.key?(:create_additions)
|
||||
|
||||
@state = State.new(options).freeze
|
||||
@parser_config = Ext::Parser::Config.new(options)
|
||||
end
|
||||
|
||||
# call-seq:
|
||||
# dump(object) -> String
|
||||
# dump(object, io) -> io
|
||||
#
|
||||
# Serialize the given object into a \JSON document.
|
||||
def dump(object, io = nil)
|
||||
@state.generate_new(object, io)
|
||||
end
|
||||
alias_method :generate, :dump
|
||||
|
||||
# call-seq:
|
||||
# load(string) -> Object
|
||||
#
|
||||
# Parse the given \JSON document and return an equivalent Ruby object.
|
||||
def load(source)
|
||||
@parser_config.parse(source)
|
||||
end
|
||||
alias_method :parse, :load
|
||||
|
||||
# call-seq:
|
||||
# load(path) -> Object
|
||||
#
|
||||
# Parse the given \JSON document and return an equivalent Ruby object.
|
||||
def load_file(path)
|
||||
load(File.read(path, encoding: Encoding::UTF_8))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
module ::Kernel
|
||||
|
@ -58,6 +58,7 @@ module JSON
|
||||
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,
|
||||
|
38
test/json/json_coder_test.rb
Executable file
38
test/json/json_coder_test.rb
Executable file
@ -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
|
@ -200,6 +200,7 @@ class JSONGeneratorTest < Test::Unit::TestCase
|
||||
assert_equal({
|
||||
:allow_nan => false,
|
||||
:array_nl => "\n",
|
||||
:as_json => false,
|
||||
:ascii_only => false,
|
||||
:buffer_initial_length => 1024,
|
||||
:depth => 0,
|
||||
@ -218,6 +219,7 @@ class JSONGeneratorTest < Test::Unit::TestCase
|
||||
assert_equal({
|
||||
:allow_nan => false,
|
||||
:array_nl => "",
|
||||
:as_json => false,
|
||||
:ascii_only => false,
|
||||
:buffer_initial_length => 1024,
|
||||
:depth => 0,
|
||||
@ -236,6 +238,7 @@ class JSONGeneratorTest < Test::Unit::TestCase
|
||||
assert_equal({
|
||||
:allow_nan => false,
|
||||
:array_nl => "",
|
||||
:as_json => false,
|
||||
:ascii_only => false,
|
||||
:buffer_initial_length => 1024,
|
||||
:depth => 0,
|
||||
@ -666,4 +669,9 @@ class JSONGeneratorTest < Test::Unit::TestCase
|
||||
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
|
||||
|
Loading…
x
Reference in New Issue
Block a user