summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--ext/json/generator/generator.c46
-rw-r--r--ext/json/lib/json/common.rb87
-rw-r--r--ext/json/lib/json/ext/generator/state.rb1
-rwxr-xr-xtest/json/json_coder_test.rb38
-rwxr-xr-xtest/json/json_generator_test.rb8
5 files changed, 177 insertions, 3 deletions
diff --git a/ext/json/generator/generator.c b/ext/json/generator/generator.c
index 62c0c420cc..bc08c01008 100644
--- a/ext/json/generator/generator.c
+++ b/ext/json/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);
@@ -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();
diff --git a/ext/json/lib/json/common.rb b/ext/json/lib/json/common.rb
index ea15b70686..dfb9f580bd 100644
--- a/ext/json/lib/json/common.rb
+++ b/ext/json/lib/json/common.rb
@@ -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
diff --git a/ext/json/lib/json/ext/generator/state.rb b/ext/json/lib/json/ext/generator/state.rb
index 6cd9496e67..d40c3b5ec3 100644
--- a/ext/json/lib/json/ext/generator/state.rb
+++ b/ext/json/lib/json/ext/generator/state.rb
@@ -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,
diff --git a/test/json/json_coder_test.rb b/test/json/json_coder_test.rb
new file mode 100755
index 0000000000..37331c4eb8
--- /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 824de2c1a6..92115637e1 100755
--- a/test/json/json_generator_test.rb
+++ b/test/json/json_generator_test.rb
@@ -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