diff --git a/CHANGELOG.md b/CHANGELOG.md index 75e47583..f6dcecad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # CHANGELOG +## 3.11.0 - 2020-01-12 + +- Added `:compat_bigdecimal` to support the JSON gem `:decimal_class` undocumented option. + +- Reverted the use of `:bigdecimal_load` for `:compat` mode. + ## 3.10.18 - 2020-12-25 - Fix modes table by marking compat mode `:bigdecimal_load` instead of `:bigdecimal_as_decimal`. diff --git a/ext/oj/mimic_json.c b/ext/oj/mimic_json.c index 3119fd4f..37efb9f7 100644 --- a/ext/oj/mimic_json.c +++ b/ext/oj/mimic_json.c @@ -364,7 +364,6 @@ mimic_generate_core(int argc, VALUE *argv, Options copts) { struct _out out; VALUE rstr; - // TBD memset(buf, 0, sizeof(buf)); out.buf = buf; @@ -510,6 +509,7 @@ mimic_parse_core(int argc, VALUE *argv, VALUE self, bool bang) { pi.options.create_ok = No; pi.options.allow_nan = (bang ? Yes : No); pi.options.nilnil = No; + pi.options.bigdec_load = RubyDec; pi.options.mode = CompatMode; pi.max_depth = 100; @@ -560,14 +560,7 @@ mimic_parse_core(int argc, VALUE *argv, VALUE self, bool bang) { } } if (Qtrue == rb_funcall(ropts, oj_has_key_id, 1, oj_decimal_class_sym)) { - v = rb_hash_lookup(ropts, oj_decimal_class_sym); - if (rb_cFloat == v) { - pi.options.bigdec_load = FloatDec; - } else if (oj_bigdecimal_class == v) { - pi.options.bigdec_load = BigDec; - } else if (Qnil == v) { - pi.options.bigdec_load = AutoDec; - } + pi.options.compat_bigdec = (oj_bigdecimal_class == rb_hash_lookup(ropts, oj_decimal_class_sym)); } v = rb_hash_lookup(ropts, oj_max_nesting_sym); if (Qtrue == v) { @@ -693,6 +686,7 @@ static struct _options mimic_object_to_json_options = { RubyTime, // time_format No, // bigdec_as_num RubyDec, // bigdec_load + false, // compat_bigdec No, // to_hash No, // to_json No, // as_json diff --git a/ext/oj/oj.c b/ext/oj/oj.c index e55b41c8..cc9a7bd0 100644 --- a/ext/oj/oj.c +++ b/ext/oj/oj.c @@ -107,6 +107,7 @@ static VALUE bigdecimal_load_sym; static VALUE bigdecimal_sym; static VALUE circular_sym; static VALUE class_cache_sym; +static VALUE compat_bigdecimal_sym; static VALUE compat_sym; static VALUE create_id_sym; static VALUE custom_sym; @@ -168,6 +169,7 @@ struct _options oj_default_options = { UnixTime, // time_format NotSet, // bigdec_as_num AutoDec, // bigdec_load + false, // compat_bigdec No, // to_hash No, // to_json No, // as_json @@ -230,6 +232,7 @@ struct _options oj_default_options = { * - *:time_format* [_:unix_|_:unix_zone_|_:xmlschema_|_:ruby_] time format when dumping * - *:bigdecimal_as_decimal* [_Boolean_|_nil_] dump BigDecimal as a decimal number or as a String * - *:bigdecimal_load* [_:bigdecimal_|_:float_|_:auto_|_:fast_] load decimals as BigDecimal instead of as a Float. :auto pick the most precise for the number of digits. :float should be the same as ruby. :fast may require rounding but is must faster. + * - *:compat_bigdecimal* [_true_|_false_] load decimals as BigDecimal instead of as a Float when in compat or rails mode. * - *:create_id* [_String_|_nil_] create id for json compatible object encoding, default is 'json_class' * - *:create_additions* [_Boolean_|_nil_] if true allow creation of instances using create_id on load. * - *:second_precision* [_Fixnum_|_nil_] number of digits after the decimal when dumping the seconds portion of time @@ -334,6 +337,7 @@ get_def_opts(VALUE self) { case AutoDec: default: rb_hash_aset(opts, bigdecimal_load_sym, auto_sym); break; } + rb_hash_aset(opts, compat_bigdecimal_sym, oj_default_options.compat_bigdec ? Qtrue : Qfalse); rb_hash_aset(opts, create_id_sym, (NULL == oj_default_options.create_id) ? Qnil : rb_str_new2(oj_default_options.create_id)); rb_hash_aset(opts, oj_space_sym, (0 == oj_default_options.dump_opts.after_size) ? Qnil : rb_str_new2(oj_default_options.dump_opts.after_sep)); rb_hash_aset(opts, oj_space_before_sym, (0 == oj_default_options.dump_opts.before_size) ? Qnil : rb_str_new2(oj_default_options.dump_opts.before_sep)); @@ -379,6 +383,7 @@ get_def_opts(VALUE self) { * - *:escape* [_:newline_|_:json_|_:xss_safe_|_:ascii_|_unicode_xss_|_nil_] mode encodes all high-bit characters as escaped sequences if :ascii, :json is standand UTF-8 JSON encoding, :newline is the same as :json but newlines are not escaped, :unicode_xss allows unicode but escapes &, <, and >, and any \u20xx characters along with some others, and :xss_safe escapes &, <, and >, and some others. * - *:bigdecimal_as_decimal* [_Boolean_|_nil_] dump BigDecimal as a decimal number or as a String. * - *:bigdecimal_load* [_:bigdecimal_|_:float_|_:auto_|_nil_] load decimals as BigDecimal instead of as a Float. :auto pick the most precise for the number of digits. + * - *:compat_bigdecimal* [_true_|_false_] load decimals as BigDecimal instead of as a Float in compat mode. * - *:mode* [_:object_|_:strict_|_:compat_|_:null_|_:custom_|_:rails_|_:wab_] load and dump mode to use for JSON :strict raises an exception when a non-supported Object is encountered. :compat attempts to extract variable values from an Object using to_json() or to_hash() then it walks the Object's variables if neither is found. The :object mode ignores to_hash() and to_json() methods and encodes variables using code internal to the Oj gem. The :null mode ignores non-supported Objects and replaces them with a null. The :custom mode honors all dump options. The :rails more mimics rails and Active behavior. * - *:time_format* [_:unix_|_:xmlschema_|_:ruby_] time format when dumping in :compat mode :unix decimal number denoting the number of seconds since 1/1/1970, :unix_zone decimal number denoting the number of seconds since 1/1/1970 plus the utc_offset in the exponent, :xmlschema date-time format taken from XML Schema as a String, :ruby Time.to_s formatted String. * - *:create_id* [_String_|_nil_] create id for json compatible object encoding @@ -582,19 +587,21 @@ oj_parse_options(VALUE ropts, Options copts) { rb_raise(rb_eArgError, ":bigdecimal_load must be :bigdecimal, :float, or :auto."); } } + if (Qnil != (v = rb_hash_lookup(ropts, compat_bigdecimal_sym))) { + copts->compat_bigdec = (Qtrue == v); + } if (Qtrue == rb_funcall(ropts, oj_has_key_id, 1, oj_decimal_class_sym)) { v = rb_hash_lookup(ropts, oj_decimal_class_sym); if (rb_cFloat == v) { - copts->bigdec_load = FloatDec; + copts->compat_bigdec = FloatDec; } else if (oj_bigdecimal_class == v) { - copts->bigdec_load = BigDec; + copts->compat_bigdec = BigDec; } else if (Qnil == v) { - copts->bigdec_load = AutoDec; + copts->compat_bigdec = AutoDec; } else { rb_raise(rb_eArgError, ":decimal_class must be BigDecimal, Float, or nil."); } } - if (Qtrue == rb_funcall(ropts, oj_has_key_id, 1, create_id_sym)) { v = rb_hash_lookup(ropts, create_id_sym); if (Qnil == v) { @@ -1663,6 +1670,7 @@ Init_oj() { bigdecimal_sym = ID2SYM(rb_intern("bigdecimal")); rb_gc_register_address(&bigdecimal_sym); circular_sym = ID2SYM(rb_intern("circular")); rb_gc_register_address(&circular_sym); class_cache_sym = ID2SYM(rb_intern("class_cache")); rb_gc_register_address(&class_cache_sym); + compat_bigdecimal_sym = ID2SYM(rb_intern("compat_bigdecimal"));rb_gc_register_address(&compat_bigdecimal_sym); compat_sym = ID2SYM(rb_intern("compat")); rb_gc_register_address(&compat_sym); create_id_sym = ID2SYM(rb_intern("create_id")); rb_gc_register_address(&create_id_sym); custom_sym = ID2SYM(rb_intern("custom")); rb_gc_register_address(&custom_sym); diff --git a/ext/oj/oj.h b/ext/oj/oj.h index 002f6f60..6edf3dfc 100644 --- a/ext/oj/oj.h +++ b/ext/oj/oj.h @@ -135,6 +135,7 @@ typedef struct _options { char time_format; // TimeFormat char bigdec_as_num; // YesNo char bigdec_load; // BigLoad + char compat_bigdec; // boolean (0 or 1) char to_hash; // YesNo char to_json; // YesNo char as_json; // YesNo diff --git a/ext/oj/parse.c b/ext/oj/parse.c index bab42089..4047beb5 100644 --- a/ext/oj/parse.c +++ b/ext/oj/parse.c @@ -385,8 +385,13 @@ read_num(ParseInfo pi) { ni.nan = 0; ni.neg = 0; ni.has_exp = 0; - ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load); - ni.bigdec_load = pi->options.bigdec_load; + if (CompatMode == pi->options.mode) { + ni.no_big = !pi->options.compat_bigdec; + ni.bigdec_load = pi->options.compat_bigdec; + } else { + ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load); + ni.bigdec_load = pi->options.bigdec_load; + } if ('-' == *pi->cur) { pi->cur++; @@ -511,7 +516,11 @@ read_num(ParseInfo pi) { ni.nan = 1; } } - if (BigDec == pi->options.bigdec_load) { + if (CompatMode == pi->options.mode) { + if (pi->options.compat_bigdec) { + ni.big = 1; + } + } else if (BigDec == pi->options.bigdec_load) { ni.big = 1; } if (0 == parent) { diff --git a/ext/oj/sparse.c b/ext/oj/sparse.c index 6ce743a1..46d7c8cd 100644 --- a/ext/oj/sparse.c +++ b/ext/oj/sparse.c @@ -400,8 +400,13 @@ read_num(ParseInfo pi) { ni.nan = 0; ni.neg = 0; ni.has_exp = 0; - ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load); - ni.bigdec_load = pi->options.bigdec_load; + if (CompatMode == pi->options.mode) { + ni.no_big = !pi->options.compat_bigdec; + ni.bigdec_load = pi->options.compat_bigdec; + } else { + ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load); + ni.bigdec_load = pi->options.bigdec_load; + } c = reader_get(&pi->rd); if ('-' == c) { @@ -518,7 +523,11 @@ read_num(ParseInfo pi) { ni.nan = 1; } } - if (BigDec == pi->options.bigdec_load) { + if (CompatMode == pi->options.mode) { + if (pi->options.compat_bigdec) { + ni.big = 1; + } + } else if (BigDec == pi->options.bigdec_load) { ni.big = 1; } add_num_value(pi, &ni); @@ -541,15 +550,24 @@ read_nan(ParseInfo pi) { ni.infinity = 0; ni.nan = 1; ni.neg = 0; - ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load); - ni.bigdec_load = pi->options.bigdec_load; + if (CompatMode == pi->options.mode) { + ni.no_big = !pi->options.compat_bigdec; + ni.bigdec_load = pi->options.compat_bigdec; + } else { + ni.no_big = (FloatDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load); + ni.bigdec_load = pi->options.bigdec_load; + } if ('a' != reader_get(&pi->rd) || ('N' != (c = reader_get(&pi->rd)) && 'n' != c)) { oj_set_error_at(pi, oj_parse_error_class, __FILE__, __LINE__, "not a number or other value"); return; } - if (BigDec == pi->options.bigdec_load) { + if (CompatMode == pi->options.mode) { + if (pi->options.compat_bigdec) { + ni.big = 1; + } + } else if (BigDec == pi->options.bigdec_load) { ni.big = 1; } add_num_value(pi, &ni); @@ -739,8 +757,15 @@ oj_sparse2(ParseInfo pi) { ni.infinity = 0; ni.nan = 1; ni.neg = 0; - ni.no_big = (FloatDec == pi->options.bigdec_load || RubyDec == pi->options.bigdec_load || FastDec == pi->options.bigdec_load); - ni.bigdec_load = pi->options.bigdec_load; + if (CompatMode == pi->options.mode) { + ni.no_big = !pi->options.compat_bigdec; + ni.bigdec_load = pi->options.compat_bigdec; + } else { + ni.no_big = (FloatDec == pi->options.bigdec_load || + FastDec == pi->options.bigdec_load || + RubyDec == pi->options.bigdec_load); + ni.bigdec_load = pi->options.bigdec_load; + } add_num_value(pi, &ni); } else { oj_set_error_at(pi, oj_parse_error_class, __FILE__, __LINE__, "invalid token"); diff --git a/lib/oj/version.rb b/lib/oj/version.rb index a74b3845..99abcd3b 100644 --- a/lib/oj/version.rb +++ b/lib/oj/version.rb @@ -1,5 +1,5 @@ module Oj # Current version of the module. - VERSION = '3.10.18' + VERSION = '3.11.0' end diff --git a/pages/Modes.md b/pages/Modes.md index 5bff950f..e332114d 100644 --- a/pages/Modes.md +++ b/pages/Modes.md @@ -95,7 +95,8 @@ information. | :ascii_only | Boolean | x | x | 2 | 2 | x | x | | | :auto_define | Boolean | | | | | x | x | | | :bigdecimal_as_decimal | Boolean | | | | 3 | x | x | | -| :bigdecimal_load | Boolean | | | x | | | x | | +| :bigdecimal_load | Boolean | | | | | | x | | +| :compat_bigdecimal | Boolean | | | x | | | x | | | :circular | Boolean | x | x | x | x | x | x | | | :class_cache | Boolean | | | | | x | x | | | :create_additions | Boolean | | | x | x | | x | | diff --git a/pages/Options.md b/pages/Options.md index 07f8ed6a..515bf969 100644 --- a/pages/Options.md +++ b/pages/Options.md @@ -70,6 +70,14 @@ This can also be set with `:decimal_class` when used as a load or parse option to match the JSON gem. In that case either `Float`, `BigDecimal`, or `nil` can be provided. +### :compat_bigdecimal [Boolean] + +Determines how to load decimals when in `:compat` mode. + + - `true` convert all decimal numbers to BigDecimal. + + - `false` convert all decimal numbers to Float. + ### :circular [Boolean] Detect circular references while dumping. In :compat mode raise a diff --git a/test/test_compat.rb b/test/test_compat.rb index 68b094cc..47f2c15f 100755 --- a/test/test_compat.rb +++ b/test/test_compat.rb @@ -283,7 +283,7 @@ def test_bigdecimal assert_equal('"0.314159265358979323846e1"', json.downcase) end - def test_bigdecimal_load + def test_decimal_class big = BigDecimal('3.14159265358979323846') # :decimal_class is the undocumented feature. json = Oj.load('3.14159265358979323846', mode: :compat, decimal_class: BigDecimal) diff --git a/test/test_various.rb b/test/test_various.rb index 2730e1ee..7c304f89 100755 --- a/test/test_various.rb +++ b/test/test_various.rb @@ -120,6 +120,7 @@ def test_set_options escape_mode: :ascii, time_format: :unix_zone, bigdecimal_load: :float, + compat_bigdecimal: true, create_id: 'classy', create_additions: true, space: 'z',