diff --git a/ext/json/ext/generator/generator.c b/ext/json/ext/generator/generator.c index be490171f..fbef1980b 100644 --- a/ext/json/ext/generator/generator.c +++ b/ext/json/ext/generator/generator.c @@ -27,14 +27,16 @@ typedef struct JSON_Generator_StateStruct { bool ascii_only; bool script_safe; bool strict; + bool rfc8785; VALUE sort_keys; } JSON_Generator_State; -static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc; +static VALUE mJSON, cState, cFragment, eGeneratorError, eNestingError, Encoding_UTF_8, default_sort_keys_proc, + rfc8785_number_formater_proc, rfc8785_sort_keys_proc; static ID i_to_s, i_to_json, i_new, i_encode; static VALUE sym_indent, sym_space, sym_space_before, sym_object_nl, sym_array_nl, sym_max_nesting, sym_allow_nan, sym_allow_duplicate_key, - sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_strict, sym_as_json, sym_sort_keys; + sym_ascii_only, sym_depth, sym_buffer_initial_length, sym_script_safe, sym_strict, sym_as_json, sym_sort_keys, sym_rfc8785; #define GET_STATE_TO(self, state) \ @@ -1052,7 +1054,10 @@ static inline long increase_depth(struct generate_json_data *data) static void generate_json_object(FBuffer *buffer, struct generate_json_data *data, VALUE obj) { - if (RB_UNLIKELY(data->state->sort_keys)) { + if (RB_UNLIKELY(data->state->rfc8785)) { + obj = rb_proc_call_with_block(rfc8785_sort_keys_proc, 1, &obj, Qnil); + Check_Type(obj, T_HASH); + } else if (RB_UNLIKELY(data->state->sort_keys)) { obj = rb_proc_call_with_block(data->state->sort_keys, 1, &obj, Qnil); Check_Type(obj, T_HASH); } @@ -1180,6 +1185,13 @@ static void generate_json_float(FBuffer *buffer, struct generate_json_data *data return; } + if (RB_UNLIKELY(data->state->rfc8785)) { + VALUE str = rb_proc_call_with_block(rfc8785_number_formater_proc, 1, &obj, Qnil); + Check_Type(str, T_STRING); + fbuffer_append_str(buffer, str); + return; + } + /* This implementation writes directly into the buffer. We reserve * the 32 characters that fpconv_dtoa states as its maximum. */ @@ -1757,6 +1769,28 @@ static VALUE cState_set_default_sort_keys_proc(VALUE self, VALUE proc) return default_sort_keys_proc = proc; } +static VALUE cState_set_rfc8785_number_formater_proc(VALUE self, VALUE proc) +{ + if (!rb_obj_is_proc(proc)) { + rb_raise(rb_eTypeError, "rfc8785_number_formater_proc must be a Proc"); + } + if (rfc8785_number_formater_proc) { + rb_raise(rb_eArgError, "rfc8785_number_formater_proc can only be set once"); + } + return rfc8785_number_formater_proc = proc; +} + +static VALUE cState_set_rfc8785_sort_keys_proc(VALUE self, VALUE proc) +{ + if (!rb_obj_is_proc(proc)) { + rb_raise(rb_eTypeError, "rfc8785_sort_keys_proc must be a Proc"); + } + if (rfc8785_sort_keys_proc) { + rb_raise(rb_eArgError, "rfc8785_sort_keys_proc can only be set once"); + } + return rfc8785_sort_keys_proc = proc; +} + static VALUE normalize_sort_keys(VALUE value) { if (rb_obj_is_proc(value)) { @@ -1775,7 +1809,7 @@ static VALUE normalize_sort_keys(VALUE value) * * Get the value of sort_keys. */ -static VALUE cState_sort_keys_p(VALUE self) +static VALUE cState_sort_keys(VALUE self) { GET_STATE(self); return state->sort_keys; @@ -1795,7 +1829,33 @@ static VALUE cState_sort_keys_set(VALUE self, VALUE value) rb_check_frozen(self); GET_STATE(self); RB_OBJ_WRITE(self, &state->sort_keys, normalize_sort_keys(value)); - return Qnil; + return state->sort_keys; +} + + +/* + * call-seq: rfc8785? + * + * Get whether RFC8785 (canonicalization) is enabled. + */ +static VALUE cState_rfc8785_p(VALUE self) +{ + GET_STATE(self); + return state->rfc8785 ? Qtrue : Qfalse; +} + +/* + * call-seq: rfc8785= + * + * Sets whether RFC8785 (canonicalization) is enabled. + */ + +static VALUE cState_rfc8785_set(VALUE self, VALUE value) +{ + rb_check_frozen(self); + GET_STATE(self); + state->rfc8785 = RTEST(value); + return state->rfc8785 ? Qtrue : Qfalse; } static VALUE cState_allow_duplicate_key_p(VALUE self) @@ -1895,6 +1955,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_strict) { state->strict = RTEST(val); } else if (key == sym_allow_duplicate_key) { state->allow_duplicate_key = RTEST(val); } + else if (key == sym_rfc8785) { state->rfc8785 = RTEST(val); } else if (key == sym_as_json) { VALUE proc = RTEST(val) ? as_json_config(val) : Qfalse; state->as_json_single_arg = proc && rb_proc_arity(proc) == 1; @@ -1990,6 +2051,8 @@ void Init_generator(void) VALUE mGenerator = rb_define_module_under(mExt, "Generator"); rb_global_variable(&default_sort_keys_proc); + rb_global_variable(&rfc8785_number_formater_proc); + rb_global_variable(&rfc8785_sort_keys_proc); rb_global_variable(&eGeneratorError); eGeneratorError = rb_path2class("JSON::GeneratorError"); @@ -2001,6 +2064,8 @@ void Init_generator(void) rb_define_alloc_func(cState, cState_s_allocate); rb_define_singleton_method(cState, "from_state", cState_from_state_s, 1); rb_define_singleton_method(cState, "default_sort_keys_proc=", cState_set_default_sort_keys_proc, 1); + rb_define_singleton_method(cState, "rfc8785_number_formater_proc=", cState_set_rfc8785_number_formater_proc, 1); + rb_define_singleton_method(cState, "rfc8785_sort_keys_proc=", cState_set_rfc8785_sort_keys_proc, 1); rb_define_method(cState, "initialize", cState_initialize, -1); rb_define_alias(cState, "initialize", "initialize"); // avoid method redefinition warnings @@ -2038,8 +2103,10 @@ void Init_generator(void) rb_define_method(cState, "buffer_initial_length=", cState_buffer_initial_length_set, 1); rb_define_method(cState, "generate", cState_generate, -1); rb_define_method(cState, "_generate_no_fallback", cState_generate_no_fallback, -1); - rb_define_method(cState, "sort_keys", cState_sort_keys_p, 0); + rb_define_method(cState, "sort_keys", cState_sort_keys, 0); rb_define_method(cState, "sort_keys=", cState_sort_keys_set, 1); + rb_define_method(cState, "rfc8785?", cState_rfc8785_p, 0); + rb_define_method(cState, "rfc8785=", cState_rfc8785_set, 1); rb_define_private_method(cState, "allow_duplicate_key?", cState_allow_duplicate_key_p, 0); @@ -2069,6 +2136,7 @@ void Init_generator(void) sym_as_json = ID2SYM(rb_intern("as_json")); sym_allow_duplicate_key = ID2SYM(rb_intern("allow_duplicate_key")); sym_sort_keys = ID2SYM(rb_intern("sort_keys")); + sym_rfc8785 = ID2SYM(rb_intern("rfc8785")); 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 fddb0ea32..a38c31d83 100644 --- a/java/src/json/ext/Generator.java +++ b/java/src/json/ext/Generator.java @@ -403,11 +403,10 @@ void generate(ThreadContext context, Session session, RubyFloat object, OutputSt } static void generateFloat(ThreadContext context, Session session, RubyFloat object, OutputStream buffer) throws IOException { + final GeneratorState state = session.getState(context); double value = object.getValue(); if (Double.isInfinite(value) || Double.isNaN(value)) { - GeneratorState state = session.getState(context); - if (!state.allowNaN()) { if (state.strict() && state.getAsJSON() != null) { IRubyObject castedValue = state.getAsJSON().call(context, object, context.getRuntime().getFalse()); @@ -421,7 +420,13 @@ static void generateFloat(ThreadContext context, Session session, RubyFloat obje } } - buffer.write(Double.toString(value).getBytes(UTF_8)); + if (state.rfc8785()) { + RubyProc numberProc = (RubyProc)GeneratorState.rfc8785NumberFormaterProc; + RubyString numberString = (RubyString)Helpers.invoke(context, numberProc, "call", object); + buffer.write(numberString.toString().getBytes(UTF_8)); + } else { + buffer.write(Double.toString(value).getBytes(UTF_8)); + } } private static final byte[] EMPTY_ARRAY_BYTES = "[]".getBytes(); @@ -570,8 +575,12 @@ static void generateHash(ThreadContext context, Session session, RubyHash object } RubyProc sortKeysProc = state.getSortKeysProc(); + if (state.rfc8785()) { + sortKeysProc = (RubyProc)GeneratorState.rfc8785SortKeysProc; + } + if (sortKeysProc != null) { - object = (RubyHash) Helpers.invoke(context, sortKeysProc, "call", object); + object = (RubyHash)Helpers.invoke(context, sortKeysProc, "call", object); } final ByteList objectNl = state.getObjectNl(); diff --git a/java/src/json/ext/GeneratorState.java b/java/src/json/ext/GeneratorState.java index 3fdf4f276..778500ef7 100644 --- a/java/src/json/ext/GeneratorState.java +++ b/java/src/json/ext/GeneratorState.java @@ -37,6 +37,8 @@ public class GeneratorState extends RubyObject { private boolean allowDuplicateKey = false; private static IRubyObject defaultSortKeyProc; + public static IRubyObject rfc8785NumberFormaterProc; + public static IRubyObject rfc8785SortKeysProc; /** * The indenting unit string. Will be repeated several times for larger @@ -112,6 +114,8 @@ public class GeneratorState extends RubyObject { */ private IRubyObject sortKeys; + private boolean rfc8785 = false; + /** * The current depth (inside a #to_json call) */ @@ -172,6 +176,18 @@ public static IRubyObject setDefaultSortKeyProc(IRubyObject klass, IRubyObject p return proc; } + @JRubyMethod(meta=true, name="rfc8785_number_formater_proc=") + public static IRubyObject setRfc8785NumberFormaterProc(IRubyObject klass, IRubyObject proc) { + rfc8785NumberFormaterProc = proc; + return proc; + } + + @JRubyMethod(meta=true, name="rfc8785_sort_keys_proc=") + public static IRubyObject setRfc8785SortKeysProc(IRubyObject klass, IRubyObject proc) { + rfc8785SortKeysProc = proc; + return proc; + } + /** * State#initialize(opts = {}) *

@@ -493,6 +509,22 @@ public IRubyObject sort_keys_set(ThreadContext context, IRubyObject sortKeys) { return sortKeys; } + @JRubyMethod(name="rfc8785?") + public IRubyObject rfc8785_p(ThreadContext context) { + return RubyBoolean.newBoolean(context, rfc8785); + } + + @JRubyMethod(name="rfc8785=") + public IRubyObject rfc8785_set(ThreadContext context, IRubyObject rfc8785) { + checkFrozen(); + this.rfc8785 = rfc8785.isTrue(); + return rfc8785; + } + + public boolean rfc8785() { + return this.rfc8785; + } + public int getDepth() { return depth; } @@ -568,6 +600,8 @@ public IRubyObject _configure(ThreadContext context, IRubyObject vOpts) { if (opts.hasKey("sort_keys")) sortKeys = normalizeSortKeys(context, opts.get("sort_keys")); + rfc8785 = opts.getBool("rfc8785", rfc8785); + opts.ensureEmpty(); return this; diff --git a/lib/json.rb b/lib/json.rb index f5f5ade89..576d86de9 100644 --- a/lib/json.rb +++ b/lib/json.rb @@ -414,6 +414,8 @@ # When +true+, keys are sorted lexicographically. When a \Proc, it receives # the entire \Hash and must return a \Hash with its pairs in the desired # order, allowing for arbitrary sort orders. +# - Option +rfc8785+ (boolean) controls whether the generated JSON will be canonicalized +# as defined in RFC8785. # # In this example, +obj+ is used first to generate the shortest # \JSON data (no whitespace), then again with all formatting options diff --git a/lib/json/ext.rb b/lib/json/ext.rb index 8c8c6f258..19683f024 100644 --- a/lib/json/ext.rb +++ b/lib/json/ext.rb @@ -28,6 +28,18 @@ def parse end end + class << self + if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda) + def shareable_lambda(block) # :nodoc: + Ractor.shareable_lambda(&block) + end + else + def shareable_lambda(block) # :nodoc: + block + end + end + end + require 'json/ext/parser' Ext::Parser::Config = Ext::ParserConfig JSON.parser = Ext::Parser @@ -42,11 +54,50 @@ def parse # The default proc used when the +sort_keys+ generation option is +true+. # It returns a new hash with the entries sorted by their keys. - sort_keys_proc = ->(hash) { hash.sort.to_h } - if defined?(::Ractor) && Ractor.respond_to?(:shareable_lambda) - sort_keys_proc = Ractor.shareable_lambda(&sort_keys_proc) - end - generator::State.default_sort_keys_proc = sort_keys_proc + generator::State.default_sort_keys_proc = shareable_lambda(->(hash) { + hash.sort.to_h + }) + + # Directly lifted from Gregg Kellogg's json-canonicalization + generator::State.rfc8785_number_formater_proc = shareable_lambda(->(num) { + if num.zero? + "0" + else + if num < 0 + num, sign = -num, '-' + end + native_rep = "%.15E" % num + decimal, exponential = native_rep.split('E') + exp_val = exponential.to_i + exponential = exp_val > 0 ? ('+' + exp_val.to_s) : exp_val.to_s + + integral, fractional = decimal.split('.') + fractional = fractional.sub(/0+$/, '') # Remove trailing zeros + + if exp_val > 0 && exp_val < 21 + while exp_val > 0 + integral += fractional.to_s[0] || '0' + fractional = fractional.to_s[1..-1] + exp_val -= 1 + end + exponential = nil + elsif exp_val == 0 + exponential = nil + elsif exp_val < 0 && exp_val > -7 + # Small numbers are shown as 0.etc with e-6 as lower limit + fractional, integral, exponential = integral + fractional.to_s, '0', nil + fractional = ("0" * (-exp_val - 1)) + fractional + end + + fractional = nil if fractional.to_s.empty? + sign.to_s + integral + (fractional ? ".#{fractional}" : '') + (exponential ? "e#{exponential}" : '') + end + }) + + generator::State.rfc8785_sort_keys_proc = shareable_lambda(->(hash) { + hash.sort_by { |k| k.to_s.encode(Encoding::UTF_16) }.to_h + }) + JSON.generator = generator end diff --git a/lib/json/ext/generator/state.rb b/lib/json/ext/generator/state.rb index a9578b9bd..2715a03f6 100644 --- a/lib/json/ext/generator/state.rb +++ b/lib/json/ext/generator/state.rb @@ -54,7 +54,8 @@ def to_h strict: strict?, depth: depth, buffer_initial_length: buffer_initial_length, - sort_keys: sort_keys + sort_keys: sort_keys, + rfc8785: rfc8785?, } allow_duplicate_key = allow_duplicate_key? diff --git a/lib/json/truffle_ruby/generator.rb b/lib/json/truffle_ruby/generator.rb index fe5daafff..99e5e7ec6 100644 --- a/lib/json/truffle_ruby/generator.rb +++ b/lib/json/truffle_ruby/generator.rb @@ -112,6 +112,8 @@ def self.valid_utf8?(string) # while generating a JSON text from a Ruby data structure. class State singleton_class.attr_accessor :default_sort_keys_proc # :nodoc: + singleton_class.attr_accessor :rfc8785_number_formater_proc # :nodoc: + singleton_class.attr_accessor :rfc8785_sort_keys_proc # :nodoc: def self.generate(obj, opts = nil, io = nil) new(opts).generate(obj, io) @@ -168,6 +170,7 @@ def initialize(opts = nil) @max_nesting = 100 @sort_keys = false @allow_duplicate_key = false + @rfc8785 = false _configure(**opts) if opts end @@ -209,6 +212,9 @@ def initialize(opts = nil) # desired order. attr_reader :sort_keys + # Controls whether RFC8785 (canonicalization) mode is enabled. Otherwise returns false. + attr_writer :rfc8785 + def sort_keys=(value) # :nodoc: type_error = false @sort_keys = case value @@ -288,6 +294,11 @@ def strict? @strict end + # Returns true, if RFC8785 (canonicalization) mode is enabled. Otherwise returns false. + def rfc8785? + @rfc8785 + end + # Configure this State instance with the Hash _opts_, and return # itself. def configure(options) @@ -308,7 +319,7 @@ def configure(options) array_nl: @array_nl, allow_nan: @allow_nan, as_json: @as_json, ascii_only: @ascii_only, sort_keys: @sort_keys, depth: @depth, buffer_initial_length: @buffer_initial_length, allow_duplicate_key: @allow_duplicate_key, script_safe: @script_safe, strict: @strict, - max_nesting: @max_nesting + max_nesting: @max_nesting, rfc8785: @rfc8785 ) if depth.negative? raise ArgumentError, "depth must be >= 0 (got #{depth})" @@ -333,6 +344,8 @@ def configure(options) @script_safe = script_safe @strict = strict @max_nesting = max_nesting || 0 + self.rfc8785 = rfc8785 + self end @@ -366,12 +379,9 @@ def generate(obj, anIO = nil) !@ascii_only and !@script_safe and @max_nesting == 0 and (!@strict || Symbol === obj) and !@sort_keys result = generate_json(obj, ''.dup) else - if @sort_keys - obj = @sort_keys.call(obj) - end - result = obj.to_json(self) end + JSON::TruffleRuby::Generator.valid_utf8?(result) or raise GeneratorError.new( "source sequence #{result.inspect} is illegal/malformed utf-8", obj @@ -393,6 +403,11 @@ def generate(obj, anIO = nil) buf << '{' first = true key_type = nil + if @rfc8785 + obj = State.rfc8785_sort_keys_proc.call(obj) + elsif @sort_keys + obj = @sort_keys.call(obj) + end obj.each_pair do |k,v| if first key_type = k.class @@ -524,7 +539,15 @@ def json_transform(state) first = true key_type = nil indent = !state.object_nl.empty? - each { |key, value| + hash = self + + if state.rfc8785? + hash = State.rfc8785_sort_keys_proc.call(hash) + elsif state.sort_keys + hash = state.sort_keys.call(hash) + end + + hash.each { |key, value| if first key_type = key.class else @@ -658,6 +681,7 @@ module Float # Returns a JSON string representation for this Float number. def to_json(state = nil, *args) state = State.from_state(state) + if infinite? || nan? if state.allow_nan? to_s @@ -679,6 +703,8 @@ def to_json(state = nil, *args) else raise GeneratorError.new("#{self} not allowed in JSON", self) end + elsif state.rfc8785? + State.rfc8785_number_formater_proc.call(self) else to_s end diff --git a/test/json/fixtures/rfc8785/input/arrays.json b/test/json/fixtures/rfc8785/input/arrays.json new file mode 100644 index 000000000..9154bdf15 --- /dev/null +++ b/test/json/fixtures/rfc8785/input/arrays.json @@ -0,0 +1,8 @@ +[ + 56, + { + "d": true, + "10": null, + "1": [ ] + } +] \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/input/french.json b/test/json/fixtures/rfc8785/input/french.json new file mode 100644 index 000000000..d4ad2e4ed --- /dev/null +++ b/test/json/fixtures/rfc8785/input/french.json @@ -0,0 +1,6 @@ +{ + "peach": "This sorting order", + "péché": "is wrong according to French", + "pêche": "but canonicalization MUST", + "sin": "ignore locale" +} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/input/structures.json b/test/json/fixtures/rfc8785/input/structures.json new file mode 100644 index 000000000..eb71efb84 --- /dev/null +++ b/test/json/fixtures/rfc8785/input/structures.json @@ -0,0 +1,8 @@ +{ + "1": {"f": {"f": "hi","F": 5} ,"\n": 56.0}, + "10": { }, + "": "empty", + "a": { }, + "111": [ {"e": "yes","E": "no" } ], + "A": { } +} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/input/unicode.json b/test/json/fixtures/rfc8785/input/unicode.json new file mode 100644 index 000000000..9ec5cc7db --- /dev/null +++ b/test/json/fixtures/rfc8785/input/unicode.json @@ -0,0 +1,3 @@ +{ + "Unnormalized Unicode":"A\u030a" +} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/input/values.json b/test/json/fixtures/rfc8785/input/values.json new file mode 100644 index 000000000..f7712c2fb --- /dev/null +++ b/test/json/fixtures/rfc8785/input/values.json @@ -0,0 +1,5 @@ +{ + "numbers": [333333333.33333329, 1E30, 4.50, 2e-3, 0.000000000000000000000000001], + "string": "\u20ac$\u000F\u000aA'\u0042\u0022\u005c\\\"\/", + "literals": [null, true, false] +} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/input/wierd.json b/test/json/fixtures/rfc8785/input/wierd.json new file mode 100644 index 000000000..940bec4f9 --- /dev/null +++ b/test/json/fixtures/rfc8785/input/wierd.json @@ -0,0 +1,11 @@ +{ + "\u20ac": "Euro Sign", + "\r": "Carriage Return", + "\u000a": "Newline", + "1": "One", + "\u0080": "Control\u007f", + "\ud83d\ude02": "Smiley", + "\u00f6": "Latin Small Letter O With Diaeresis", + "\ufb33": "Hebrew Letter Dalet With Dagesh", + "": "Browser Challenge" +} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/output/arrays.json b/test/json/fixtures/rfc8785/output/arrays.json new file mode 100644 index 000000000..5efb93db7 --- /dev/null +++ b/test/json/fixtures/rfc8785/output/arrays.json @@ -0,0 +1 @@ +[56,{"1":[],"10":null,"d":true}] \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/output/french.json b/test/json/fixtures/rfc8785/output/french.json new file mode 100644 index 000000000..2e15cd1b2 --- /dev/null +++ b/test/json/fixtures/rfc8785/output/french.json @@ -0,0 +1 @@ +{"peach":"This sorting order","péché":"is wrong according to French","pêche":"but canonicalization MUST","sin":"ignore locale"} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/output/structures.json b/test/json/fixtures/rfc8785/output/structures.json new file mode 100644 index 000000000..dc21e2424 --- /dev/null +++ b/test/json/fixtures/rfc8785/output/structures.json @@ -0,0 +1 @@ +{"":"empty","1":{"\n":56,"f":{"F":5,"f":"hi"}},"10":{},"111":[{"E":"no","e":"yes"}],"A":{},"a":{}} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/output/unicode.json b/test/json/fixtures/rfc8785/output/unicode.json new file mode 100644 index 000000000..ee60fd121 --- /dev/null +++ b/test/json/fixtures/rfc8785/output/unicode.json @@ -0,0 +1 @@ +{"Unnormalized Unicode":"Å"} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/output/values.json b/test/json/fixtures/rfc8785/output/values.json new file mode 100644 index 000000000..29b720b6e --- /dev/null +++ b/test/json/fixtures/rfc8785/output/values.json @@ -0,0 +1 @@ +{"literals":[null,true,false],"numbers":[333333333.3333333,1e+30,4.5,0.002,1e-27],"string":"€$\u000f\nA'B\"\\\\\"/"} \ No newline at end of file diff --git a/test/json/fixtures/rfc8785/output/wierd.json b/test/json/fixtures/rfc8785/output/wierd.json new file mode 100644 index 000000000..62c83a333 --- /dev/null +++ b/test/json/fixtures/rfc8785/output/wierd.json @@ -0,0 +1 @@ +{"\n":"Newline","\r":"Carriage Return","1":"One","":"Browser Challenge","€":"Control","ö":"Latin Small Letter O With Diaeresis","€":"Euro Sign","😂":"Smiley","דּ":"Hebrew Letter Dalet With Dagesh"} \ No newline at end of file diff --git a/test/json/json_generator_test.rb b/test/json/json_generator_test.rb index a901840d9..0a20357f0 100755 --- a/test/json/json_generator_test.rb +++ b/test/json/json_generator_test.rb @@ -366,6 +366,7 @@ def test_state_defaults space: "", space_before: "", sort_keys: false, + rfc8785: false, }.sort_by { |n,| n.to_s }.to_h, state.to_h.sort_by { |n,| n.to_s }.to_h) state = JSON::State.new(allow_duplicate_key: true) @@ -385,6 +386,7 @@ def test_state_defaults space: "", space_before: "", sort_keys: false, + rfc8785: false, }.sort_by { |n,| n.to_s }, state.to_h.sort_by { |n,| n.to_s }) end @@ -1227,4 +1229,34 @@ def test_large_depth_raises end end + def test_rfc8785_numbers + assert_rfc8785 '-9007199254740992', -9007199254740992 + assert_rfc8785 '0', 0 + assert_rfc8785 '0.000001', 0.000001 + assert_rfc8785 '1e+21', 1e+21 + assert_rfc8785 '9.999999999999997e+22', 9.999999999999997e+22 + assert_rfc8785 '9.999999999999997e-7', 9.999999999999997e-7 + assert_rfc8785 '9007199254740992', 9007199254740992 + assert_rfc8785 '9007199254740994', 9007199254740994 + assert_rfc8785 '9007199254740996', 9007199254740996 + assert_rfc8785 '999999999999999700000', 999999999999999700000 + assert_rfc8785 '999999999999999900000', 999999999999999900000 + assert_rfc8785 '333333333.3333333', 333333333.33333329 + end + + fixtures_path = File.expand_path('../fixtures/rfc8785/', __FILE__) + Dir[File.join(fixtures_path, "input/*.json", __FILE__)].each do |input| + filename = File.basename(input) + name, _ = File.basename(filename, ".json") + expected = File.join(fixtures_path, 'output', filename) + define_method("test_rfc8785_#{name}") do + assert_rfc8785(File.read(output), JSON.load_file(input)) + end + end + + private + + def assert_rfc8785(expected, value) + assert_equal(expected, JSON.generate(value, rfc8785: true)) + end end