oj 3.8.0 → 3.10.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +2 -2
  3. data/ext/oj/custom.c +61 -36
  4. data/ext/oj/dump.c +9 -12
  5. data/ext/oj/dump_compat.c +6 -9
  6. data/ext/oj/dump_object.c +14 -9
  7. data/ext/oj/extconf.rb +1 -0
  8. data/ext/oj/mimic_json.c +4 -2
  9. data/ext/oj/object.c +8 -5
  10. data/ext/oj/oj.c +47 -30
  11. data/ext/oj/oj.h +6 -4
  12. data/ext/oj/parse.c +15 -2
  13. data/ext/oj/parse.h +1 -0
  14. data/ext/oj/rails.c +9 -2
  15. data/ext/oj/resolve.c +3 -3
  16. data/ext/oj/sparse.c +4 -0
  17. data/ext/oj/util.c +5 -5
  18. data/ext/oj/val_stack.c +9 -9
  19. data/ext/oj/val_stack.h +9 -9
  20. data/lib/oj/json.rb +1 -1
  21. data/lib/oj/version.rb +1 -1
  22. data/pages/Options.md +4 -0
  23. data/pages/Rails.md +21 -21
  24. data/test/activesupport5/abstract_unit.rb +45 -0
  25. data/test/activesupport5/decoding_test.rb +68 -60
  26. data/test/activesupport5/encoding_test.rb +111 -96
  27. data/test/activesupport5/encoding_test_cases.rb +33 -25
  28. data/test/activesupport5/test_helper.rb +43 -21
  29. data/test/activesupport5/time_zone_test_helpers.rb +18 -3
  30. data/test/activesupport6/abstract_unit.rb +44 -0
  31. data/test/activesupport6/decoding_test.rb +133 -0
  32. data/test/activesupport6/encoding_test.rb +507 -0
  33. data/test/activesupport6/encoding_test_cases.rb +98 -0
  34. data/test/activesupport6/test_common.rb +17 -0
  35. data/test/activesupport6/test_helper.rb +163 -0
  36. data/test/activesupport6/time_zone_test_helpers.rb +39 -0
  37. data/test/bar.rb +8 -11
  38. data/test/baz.rb +16 -0
  39. data/test/test_compat.rb +0 -7
  40. data/test/test_custom.rb +6 -2
  41. data/test/test_integer_range.rb +1 -2
  42. data/test/test_object.rb +4 -3
  43. data/test/test_strict.rb +24 -1
  44. data/test/test_various.rb +41 -62
  45. metadata +20 -2
@@ -1,6 +1,10 @@
1
- require 'bigdecimal'
2
- require 'date'
3
- require 'time'
1
+ # frozen_string_literal: true
2
+
3
+ require "bigdecimal"
4
+ require "date"
5
+ require "time"
6
+ require "pathname"
7
+ require "uri"
4
8
 
5
9
  module JSONTest
6
10
  class Foo
@@ -11,7 +15,7 @@ module JSONTest
11
15
 
12
16
  class Hashlike
13
17
  def to_hash
14
- { :foo => "hello", :bar => "world" }
18
+ { foo: "hello", bar: "world" }
15
19
  end
16
20
  end
17
21
 
@@ -25,7 +29,7 @@ module JSONTest
25
29
  end
26
30
  end
27
31
 
28
- class MyStruct < Struct.new(:name, :value)
32
+ MyStruct = Struct.new(:name, :value) do
29
33
  def initialize(*)
30
34
  @unused = "unused instance variable"
31
35
  super
@@ -38,23 +42,23 @@ module JSONTest
38
42
  NilTests = [[ nil, %(null) ]]
39
43
  NumericTests = [[ 1, %(1) ],
40
44
  [ 2.5, %(2.5) ],
41
- [ 0.0/0.0, %(null) ],
42
- [ 1.0/0.0, %(null) ],
43
- [ -1.0/0.0, %(null) ],
44
- [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ],
45
- [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]]
45
+ [ 0.0 / 0.0, %(null) ],
46
+ [ 1.0 / 0.0, %(null) ],
47
+ [ -1.0 / 0.0, %(null) ],
48
+ [ BigDecimal("0.0") / BigDecimal("0.0"), %(null) ],
49
+ [ BigDecimal("2.5"), %("#{BigDecimal('2.5')}") ]]
46
50
 
47
- StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")],
51
+ StringTests = [[ "this is the <string>", %("this is the \\u003cstring\\u003e")],
48
52
  [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
49
- [ 'http://test.host/posts/1', %("http://test.host/posts/1")],
53
+ [ "http://test.host/posts/1", %("http://test.host/posts/1")],
50
54
  [ "Control characters: \x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0a\x0b\x0c\x0d\x0e\x0f\x10\x11\x12\x13\x14\x15\x16\x17\x18\x19\x1a\x1b\x1c\x1d\x1e\x1f\u2028\u2029",
51
55
  %("Control characters: \\u0000\\u0001\\u0002\\u0003\\u0004\\u0005\\u0006\\u0007\\b\\t\\n\\u000b\\f\\r\\u000e\\u000f\\u0010\\u0011\\u0012\\u0013\\u0014\\u0015\\u0016\\u0017\\u0018\\u0019\\u001a\\u001b\\u001c\\u001d\\u001e\\u001f\\u2028\\u2029") ]]
52
56
 
53
- ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
54
- [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
57
+ ArrayTests = [[ ["a", "b", "c"], %([\"a\",\"b\",\"c\"]) ],
58
+ [ [1, "a", :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
55
59
 
56
- HashTests = [[ {foo: "bar"}, %({\"foo\":\"bar\"}) ],
57
- [ {1 => 1, 2 => 'a', 3 => :b, 4 => nil, 5 => false}, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]]
60
+ HashTests = [[ { foo: "bar" }, %({\"foo\":\"bar\"}) ],
61
+ [ { 1 => 1, 2 => "a", 3 => :b, 4 => nil, 5 => false }, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]]
58
62
 
59
63
  RangeTests = [[ 1..2, %("1..2")],
60
64
  [ 1...2, %("1...2")],
@@ -69,22 +73,26 @@ module JSONTest
69
73
  StructTests = [[ MyStruct.new(:foo, "bar"), %({\"name\":\"foo\",\"value\":\"bar\"}) ],
70
74
  [ MyStruct.new(nil, nil), %({\"name\":null,\"value\":null}) ]]
71
75
  CustomTests = [[ Custom.new("custom"), '"custom"' ],
72
- [ Custom.new(nil), 'null' ],
76
+ [ Custom.new(nil), "null" ],
73
77
  [ Custom.new(:a), '"a"' ],
74
78
  [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
75
- [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
79
+ [ Custom.new(foo: "hello", bar: "world"), '{"bar":"world","foo":"hello"}' ],
76
80
  [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
77
81
  [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
78
82
 
79
83
  RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
80
84
 
81
- DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]]
82
- TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
83
- DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
85
+ URITests = [[ URI.parse("http://example.com"), %("http://example.com") ]]
86
+
87
+ PathnameTests = [[ Pathname.new("lib/index.rb"), %("lib/index.rb") ]]
88
+
89
+ DateTests = [[ Date.new(2005, 2, 1), %("2005/02/01") ]]
90
+ TimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]]
91
+ DateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005/02/01 15:15:10 +0000") ]]
84
92
 
85
- StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]]
86
- StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
87
- StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
88
- StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]]
93
+ StandardDateTests = [[ Date.new(2005, 2, 1), %("2005-02-01") ]]
94
+ StandardTimeTests = [[ Time.utc(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000Z") ]]
95
+ StandardDateTimeTests = [[ DateTime.civil(2005, 2, 1, 15, 15, 10), %("2005-02-01T15:15:10.000+00:00") ]]
96
+ StandardStringTests = [[ "this is the <string>", %("this is the <string>")]]
89
97
  end
90
98
  end
@@ -1,25 +1,54 @@
1
- require 'active_support/testing/assertions'
2
- require 'active_support/testing/deprecation'
3
- require 'active_support/testing/declarative'
4
- require 'minitest/autorun'
1
+ # frozen_string_literal: true
2
+
3
+ gem "minitest" # make sure we get the gem, not stdlib
4
+ require "minitest"
5
+ require "active_support/testing/tagged_logging"
6
+ require "active_support/testing/setup_and_teardown"
7
+ require "active_support/testing/assertions"
8
+ require "active_support/testing/deprecation"
9
+ require "active_support/testing/declarative"
10
+ require "active_support/testing/isolation"
11
+ require "active_support/testing/constant_lookup"
12
+ require "active_support/testing/time_helpers"
13
+ require "active_support/testing/file_fixtures"
5
14
 
6
15
  module ActiveSupport
7
16
  class TestCase < ::Minitest::Test
8
- # Skips the current run on Rubinius using Minitest::Assertions#skip
9
- private def rubinius_skip(message = "")
10
- skip message if RUBY_ENGINE == "rbx"
11
- end
12
- # Skips the current run on JRuby using Minitest::Assertions#skip
13
- private def jruby_skip(message = "")
14
- skip message if defined?(JRUBY_VERSION)
15
- end
16
-
17
17
  Assertion = Minitest::Assertion
18
18
 
19
+ class << self
20
+ # Sets the order in which test cases are run.
21
+ #
22
+ # ActiveSupport::TestCase.test_order = :random # => :random
23
+ #
24
+ # Valid values are:
25
+ # * +:random+ (to run tests in random order)
26
+ # * +:parallel+ (to run tests in parallel)
27
+ # * +:sorted+ (to run tests alphabetically by method name)
28
+ # * +:alpha+ (equivalent to +:sorted+)
29
+ def test_order=(new_order)
30
+ ActiveSupport.test_order = new_order
31
+ end
32
+
33
+ # Returns the order in which test cases are run.
34
+ #
35
+ # ActiveSupport::TestCase.test_order # => :random
36
+ #
37
+ # Possible values are +:random+, +:parallel+, +:alpha+, +:sorted+.
38
+ # Defaults to +:random+.
39
+ def test_order
40
+ ActiveSupport.test_order ||= :random
41
+ end
42
+ end
43
+
19
44
  alias_method :method_name, :name
20
45
 
46
+ include ActiveSupport::Testing::TaggedLogging
47
+ prepend ActiveSupport::Testing::SetupAndTeardown
21
48
  include ActiveSupport::Testing::Assertions
22
49
  include ActiveSupport::Testing::Deprecation
50
+ include ActiveSupport::Testing::TimeHelpers
51
+ include ActiveSupport::Testing::FileFixtures
23
52
  extend ActiveSupport::Testing::Declarative
24
53
 
25
54
  # test/unit backwards compatibility methods
@@ -38,13 +67,6 @@ module ActiveSupport
38
67
  alias :assert_not_respond_to :refute_respond_to
39
68
  alias :assert_not_same :refute_same
40
69
 
41
- # Fails if the block raises an exception.
42
- #
43
- # assert_nothing_raised do
44
- # ...
45
- # end
46
- def assert_nothing_raised(*args)
47
- yield
48
- end
70
+ ActiveSupport.run_load_hooks(:active_support_test_case, self)
49
71
  end
50
72
  end
@@ -1,3 +1,5 @@
1
+ # frozen_string_literal: true
2
+
1
3
  module TimeZoneTestHelpers
2
4
  def with_tz_default(tz = nil)
3
5
  old_tz = Time.zone
@@ -7,11 +9,11 @@ module TimeZoneTestHelpers
7
9
  Time.zone = old_tz
8
10
  end
9
11
 
10
- def with_env_tz(new_tz = 'US/Eastern')
11
- old_tz, ENV['TZ'] = ENV['TZ'], new_tz
12
+ def with_env_tz(new_tz = "US/Eastern")
13
+ old_tz, ENV["TZ"] = ENV["TZ"], new_tz
12
14
  yield
13
15
  ensure
14
- old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
16
+ old_tz ? ENV["TZ"] = old_tz : ENV.delete("TZ")
15
17
  end
16
18
 
17
19
  def with_preserve_timezone(value)
@@ -21,4 +23,17 @@ module TimeZoneTestHelpers
21
23
  ensure
22
24
  ActiveSupport.to_time_preserves_timezone = old_preserve_tz
23
25
  end
26
+
27
+ def with_tz_mappings(mappings)
28
+ old_mappings = ActiveSupport::TimeZone::MAPPING.dup
29
+ ActiveSupport::TimeZone.clear
30
+ ActiveSupport::TimeZone::MAPPING.clear
31
+ ActiveSupport::TimeZone::MAPPING.merge!(mappings)
32
+
33
+ yield
34
+ ensure
35
+ ActiveSupport::TimeZone.clear
36
+ ActiveSupport::TimeZone::MAPPING.clear
37
+ ActiveSupport::TimeZone::MAPPING.merge!(old_mappings)
38
+ end
24
39
  end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ ORIG_ARGV = ARGV.dup
4
+
5
+ require "active_support/core_ext/kernel/reporting"
6
+
7
+ silence_warnings do
8
+ Encoding.default_internal = Encoding::UTF_8
9
+ Encoding.default_external = Encoding::UTF_8
10
+ end
11
+
12
+ require "active_support/testing/autorun"
13
+ require "active_support/testing/method_call_assertions"
14
+
15
+ ENV["NO_RELOAD"] = "1"
16
+ require "active_support"
17
+
18
+ Thread.abort_on_exception = true
19
+
20
+ # Show backtraces for deprecated behavior for quicker cleanup.
21
+ ActiveSupport::Deprecation.debug = true
22
+
23
+ # Default to old to_time behavior but allow running tests with new behavior
24
+ ActiveSupport.to_time_preserves_timezone = ENV["PRESERVE_TIMEZONES"] == "1"
25
+
26
+ # Disable available locale checks to avoid warnings running the test suite.
27
+ I18n.enforce_available_locales = false
28
+
29
+ class ActiveSupport::TestCase
30
+ include ActiveSupport::Testing::MethodCallAssertions
31
+
32
+ private
33
+ # Skips the current run on Rubinius using Minitest::Assertions#skip
34
+ def rubinius_skip(message = "")
35
+ skip message if RUBY_ENGINE == "rbx"
36
+ end
37
+
38
+ # Skips the current run on JRuby using Minitest::Assertions#skip
39
+ def jruby_skip(message = "")
40
+ skip message if defined?(JRUBY_VERSION)
41
+ end
42
+ end
43
+
44
+ require_relative "test_common"
@@ -0,0 +1,133 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "abstract_unit"
4
+ require "active_support/json"
5
+ require "active_support/time"
6
+ require_relative "time_zone_test_helpers"
7
+
8
+ require 'oj'
9
+
10
+ Oj::Rails.set_decoder()
11
+
12
+ class TestJSONDecoding < ActiveSupport::TestCase
13
+ include TimeZoneTestHelpers
14
+
15
+ # Added for testing if Oj is used.
16
+ test "oj is used as an encoder" do
17
+ assert_equal ActiveSupport.json_encoder, Oj::Rails::Encoder
18
+ end
19
+
20
+ class Foo
21
+ def self.json_create(object)
22
+ "Foo"
23
+ end
24
+ end
25
+
26
+ TESTS = {
27
+ %q({"returnTo":{"\/categories":"\/"}}) => { "returnTo" => { "/categories" => "/" } },
28
+ %q({"return\\"To\\":":{"\/categories":"\/"}}) => { "return\"To\":" => { "/categories" => "/" } },
29
+ %q({"returnTo":{"\/categories":1}}) => { "returnTo" => { "/categories" => 1 } },
30
+ %({"returnTo":[1,"a"]}) => { "returnTo" => [1, "a"] },
31
+ %({"returnTo":[1,"\\"a\\",", "b"]}) => { "returnTo" => [1, "\"a\",", "b"] },
32
+ %({"a": "'", "b": "5,000"}) => { "a" => "'", "b" => "5,000" },
33
+ %({"a": "a's, b's and c's", "b": "5,000"}) => { "a" => "a's, b's and c's", "b" => "5,000" },
34
+ # multibyte
35
+ %({"matzue": "松江", "asakusa": "浅草"}) => { "matzue" => "松江", "asakusa" => "浅草" },
36
+ %({"a": "2007-01-01"}) => { "a" => Date.new(2007, 1, 1) },
37
+ %({"a": "2007-01-01 01:12:34 Z"}) => { "a" => Time.utc(2007, 1, 1, 1, 12, 34) },
38
+ %(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)],
39
+ %(["2007-01-01 01:12:34 Z", "2007-01-01 01:12:35 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34), Time.utc(2007, 1, 1, 1, 12, 35)],
40
+ # no time zone
41
+ %({"a": "2007-01-01 01:12:34"}) => { "a" => Time.new(2007, 1, 1, 1, 12, 34, "-05:00") },
42
+ # invalid date
43
+ %({"a": "1089-10-40"}) => { "a" => "1089-10-40" },
44
+ # xmlschema date notation
45
+ %({"a": "2009-08-10T19:01:02"}) => { "a" => Time.new(2009, 8, 10, 19, 1, 2, "-04:00") },
46
+ %({"a": "2009-08-10T19:01:02Z"}) => { "a" => Time.utc(2009, 8, 10, 19, 1, 2) },
47
+ %({"a": "2009-08-10T19:01:02+02:00"}) => { "a" => Time.utc(2009, 8, 10, 17, 1, 2) },
48
+ %({"a": "2009-08-10T19:01:02-05:00"}) => { "a" => Time.utc(2009, 8, 11, 00, 1, 2) },
49
+ # needs to be *exact*
50
+ %({"a": " 2007-01-01 01:12:34 Z "}) => { "a" => " 2007-01-01 01:12:34 Z " },
51
+ %({"a": "2007-01-01 : it's your birthday"}) => { "a" => "2007-01-01 : it's your birthday" },
52
+ %([]) => [],
53
+ %({}) => {},
54
+ %({"a":1}) => { "a" => 1 },
55
+ %({"a": ""}) => { "a" => "" },
56
+ %({"a":"\\""}) => { "a" => "\"" },
57
+ %({"a": null}) => { "a" => nil },
58
+ %({"a": true}) => { "a" => true },
59
+ %({"a": false}) => { "a" => false },
60
+ '{"bad":"\\\\","trailing":""}' => { "bad" => "\\", "trailing" => "" },
61
+ %q({"a": "http:\/\/test.host\/posts\/1"}) => { "a" => "http://test.host/posts/1" },
62
+ %q({"a": "\u003cunicode\u0020escape\u003e"}) => { "a" => "<unicode escape>" },
63
+ '{"a": "\\\\u0020skip double backslashes"}' => { "a" => "\\u0020skip double backslashes" },
64
+ %q({"a": "\u003cbr /\u003e"}) => { "a" => "<br />" },
65
+ %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => { "b" => ["<i>", "<b>", "<u>"] },
66
+ # test combination of dates and escaped or unicode encoded data in arrays
67
+ %q([{"d":"1970-01-01", "s":"\u0020escape"},{"d":"1970-01-01", "s":"\u0020escape"}]) =>
68
+ [{ "d" => Date.new(1970, 1, 1), "s" => " escape" }, { "d" => Date.new(1970, 1, 1), "s" => " escape" }],
69
+ %q([{"d":"1970-01-01","s":"http:\/\/example.com"},{"d":"1970-01-01","s":"http:\/\/example.com"}]) =>
70
+ [{ "d" => Date.new(1970, 1, 1), "s" => "http://example.com" },
71
+ { "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }],
72
+ # tests escaping of "\n" char with Yaml backend
73
+ %q({"a":"\n"}) => { "a" => "\n" },
74
+ %q({"a":"\u000a"}) => { "a" => "\n" },
75
+ %q({"a":"Line1\u000aLine2"}) => { "a" => "Line1\nLine2" },
76
+ # prevent json unmarshalling
77
+ '{"json_class":"TestJSONDecoding::Foo"}' => { "json_class" => "TestJSONDecoding::Foo" },
78
+ # json "fragments" - these are invalid JSON, but ActionPack relies on this
79
+ '"a string"' => "a string",
80
+ "1.1" => 1.1,
81
+ "1" => 1,
82
+ "-1" => -1,
83
+ "true" => true,
84
+ "false" => false,
85
+ "null" => nil
86
+ }
87
+
88
+ TESTS.each_with_index do |(json, expected), index|
89
+ fail_message = "JSON decoding failed for #{json}"
90
+
91
+ test "json decodes #{index}" do
92
+ with_tz_default "Eastern Time (US & Canada)" do
93
+ with_parse_json_times(true) do
94
+ silence_warnings do
95
+ if expected.nil?
96
+ assert_nil ActiveSupport::JSON.decode(json), fail_message
97
+ else
98
+ assert_equal expected, ActiveSupport::JSON.decode(json), fail_message
99
+ end
100
+ end
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ test "json decodes time json with time parsing disabled" do
107
+ with_parse_json_times(false) do
108
+ expected = { "a" => "2007-01-01 01:12:34 Z" }
109
+ assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
110
+ end
111
+ end
112
+
113
+ def test_failed_json_decoding
114
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) }
115
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) }
116
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) }
117
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) }
118
+ end
119
+
120
+ def test_cannot_pass_unsupported_options
121
+ assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) }
122
+ end
123
+
124
+ private
125
+
126
+ def with_parse_json_times(value)
127
+ old_value = ActiveSupport.parse_json_times
128
+ ActiveSupport.parse_json_times = value
129
+ yield
130
+ ensure
131
+ ActiveSupport.parse_json_times = old_value
132
+ end
133
+ end
@@ -0,0 +1,507 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "securerandom"
4
+ require_relative "abstract_unit"
5
+ require "active_support/core_ext/string/inflections"
6
+ require "active_support/json"
7
+ require "active_support/time"
8
+ require_relative "time_zone_test_helpers"
9
+ require_relative "encoding_test_cases"
10
+
11
+ require 'oj'
12
+
13
+ # Sets the ActiveSupport encoder to be Oj and also wraps the setting of
14
+ # globals.
15
+ Oj::Rails.set_encoder()
16
+ #Oj::Rails.optimize(Hash, Array, BigDecimal, Time, Range, Regexp, ActiveSupport::TimeWithZone)
17
+ Oj::Rails.optimize()
18
+
19
+ class TestJSONEncoding < ActiveSupport::TestCase
20
+ include TimeZoneTestHelpers
21
+
22
+ # Added for testing if Oj is used.
23
+ test "oj is used as an encoder" do
24
+ assert_equal ActiveSupport.json_encoder, Oj::Rails::Encoder
25
+ end
26
+
27
+ def sorted_json(json)
28
+ if json.start_with?("{") && json.end_with?("}")
29
+ "{" + json[1..-2].split(",").sort.join(",") + "}"
30
+ else
31
+ json
32
+ end
33
+ end
34
+
35
+ JSONTest::EncodingTestCases.constants.each do |class_tests|
36
+ define_method("test_#{class_tests[0..-6].underscore}") do
37
+ prev = ActiveSupport.use_standard_json_time_format
38
+
39
+ standard_class_tests = /Standard/.match?(class_tests)
40
+
41
+ ActiveSupport.escape_html_entities_in_json = !standard_class_tests
42
+ ActiveSupport.use_standard_json_time_format = standard_class_tests
43
+ JSONTest::EncodingTestCases.const_get(class_tests).each do |pair|
44
+ assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
45
+ end
46
+ ensure
47
+ ActiveSupport.escape_html_entities_in_json = false
48
+ ActiveSupport.use_standard_json_time_format = prev
49
+ end
50
+ end
51
+
52
+ def test_process_status
53
+ rubinius_skip "https://github.com/rubinius/rubinius/issues/3334"
54
+
55
+ # There doesn't seem to be a good way to get a handle on a Process::Status object without actually
56
+ # creating a child process, hence this to populate $?
57
+ system("not_a_real_program_#{SecureRandom.hex}")
58
+ assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
59
+ end
60
+
61
+ def test_hash_encoding
62
+ assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(a: :b)
63
+ assert_equal %({\"a\":1}), ActiveSupport::JSON.encode("a" => 1)
64
+ assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode("a" => [1, 2])
65
+ assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
66
+
67
+ assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(a: :b, c: :d))
68
+ end
69
+
70
+ def test_hash_keys_encoding
71
+ ActiveSupport.escape_html_entities_in_json = true
72
+ assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", ActiveSupport::JSON.encode("<>" => "<>")
73
+ ensure
74
+ ActiveSupport.escape_html_entities_in_json = false
75
+ end
76
+
77
+ def test_utf8_string_encoded_properly
78
+ # The original test seems to expect that
79
+ # ActiveSupport.escape_html_entities_in_json reverts to true even after
80
+ # being set to false. I haven't been able to figure that out so the value is
81
+ # set to true, the default, before running the test. This might be wrong but
82
+ # for now it will have to do.
83
+ ActiveSupport.escape_html_entities_in_json = true
84
+ result = ActiveSupport::JSON.encode("€2.99")
85
+ assert_equal '"€2.99"', result
86
+ assert_equal(Encoding::UTF_8, result.encoding)
87
+
88
+ result = ActiveSupport::JSON.encode("✎☺")
89
+ assert_equal '"✎☺"', result
90
+ assert_equal(Encoding::UTF_8, result.encoding)
91
+ end
92
+
93
+ def test_non_utf8_string_transcodes
94
+ s = "二".encode("Shift_JIS")
95
+ result = ActiveSupport::JSON.encode(s)
96
+ assert_equal '"二"', result
97
+ assert_equal Encoding::UTF_8, result.encoding
98
+ end
99
+
100
+ def test_wide_utf8_chars
101
+ w = "𠜎"
102
+ result = ActiveSupport::JSON.encode(w)
103
+ assert_equal '"𠜎"', result
104
+ end
105
+
106
+ def test_wide_utf8_roundtrip
107
+ hash = { string: "𐒑" }
108
+ json = ActiveSupport::JSON.encode(hash)
109
+ decoded_hash = ActiveSupport::JSON.decode(json)
110
+ assert_equal "𐒑", decoded_hash["string"]
111
+ end
112
+
113
+ def test_hash_key_identifiers_are_always_quoted
114
+ values = { 0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B" }
115
+ assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
116
+ end
117
+
118
+ def test_hash_should_allow_key_filtering_with_only
119
+ assert_equal %({"a":1}), ActiveSupport::JSON.encode({ "a" => 1, :b => 2, :c => 3 }, { only: "a" })
120
+ end
121
+
122
+ def test_hash_should_allow_key_filtering_with_except
123
+ assert_equal %({"b":2}), ActiveSupport::JSON.encode({ "foo" => "bar", :b => 2, :c => 3 }, { except: ["foo", :c] })
124
+ end
125
+
126
+ def test_time_to_json_includes_local_offset
127
+ with_standard_json_time_format(true) do
128
+ with_env_tz "US/Eastern" do
129
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005, 2, 1, 15, 15, 10))
130
+ end
131
+ end
132
+ end
133
+
134
+ def test_hash_with_time_to_json
135
+ with_standard_json_time_format(false) do
136
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { time: Time.utc(2009) }.to_json
137
+ end
138
+ end
139
+
140
+ def test_nested_hash_with_float
141
+ assert_nothing_raised do
142
+ hash = {
143
+ "CHI" => {
144
+ display_name: "chicago",
145
+ latitude: 123.234
146
+ }
147
+ }
148
+ ActiveSupport::JSON.encode(hash)
149
+ end
150
+ end
151
+
152
+ def test_hash_like_with_options
153
+ h = JSONTest::Hashlike.new
154
+ json = h.to_json only: [:foo]
155
+
156
+ assert_equal({ "foo" => "hello" }, JSON.parse(json))
157
+ end
158
+
159
+ def test_object_to_json_with_options
160
+ obj = Object.new
161
+ obj.instance_variable_set :@foo, "hello"
162
+ obj.instance_variable_set :@bar, "world"
163
+ json = obj.to_json only: ["foo"]
164
+
165
+ assert_equal({ "foo" => "hello" }, JSON.parse(json))
166
+ end
167
+
168
+ def test_struct_to_json_with_options
169
+ struct = Struct.new(:foo, :bar).new
170
+ struct.foo = "hello"
171
+ struct.bar = "world"
172
+ json = struct.to_json only: [:foo]
173
+
174
+ assert_equal({ "foo" => "hello" }, JSON.parse(json))
175
+ end
176
+
177
+ def test_struct_to_json_with_options_nested
178
+ klass = Struct.new(:foo, :bar)
179
+ struct = klass.new "hello", "world"
180
+ parent_struct = klass.new struct, "world"
181
+ json = parent_struct.to_json only: [:foo]
182
+
183
+ assert_equal({ "foo" => { "foo" => "hello" } }, JSON.parse(json))
184
+ end
185
+
186
+
187
+ def test_hash_should_pass_encoding_options_to_children_in_as_json
188
+ person = {
189
+ name: "John",
190
+ address: {
191
+ city: "London",
192
+ country: "UK"
193
+ }
194
+ }
195
+ json = person.as_json only: [:address, :city]
196
+
197
+ assert_equal({ "address" => { "city" => "London" } }, json)
198
+ end
199
+
200
+ def test_hash_should_pass_encoding_options_to_children_in_to_json
201
+ person = {
202
+ name: "John",
203
+ address: {
204
+ city: "London",
205
+ country: "UK"
206
+ }
207
+ }
208
+ json = person.to_json only: [:address, :city]
209
+
210
+ assert_equal(%({"address":{"city":"London"}}), json)
211
+ end
212
+
213
+ def test_array_should_pass_encoding_options_to_children_in_as_json
214
+ people = [
215
+ { name: "John", address: { city: "London", country: "UK" } },
216
+ { name: "Jean", address: { city: "Paris", country: "France" } }
217
+ ]
218
+ json = people.as_json only: [:address, :city]
219
+ expected = [
220
+ { "address" => { "city" => "London" } },
221
+ { "address" => { "city" => "Paris" } }
222
+ ]
223
+
224
+ assert_equal(expected, json)
225
+ end
226
+
227
+ def test_array_should_pass_encoding_options_to_children_in_to_json
228
+ people = [
229
+ { name: "John", address: { city: "London", country: "UK" } },
230
+ { name: "Jean", address: { city: "Paris", country: "France" } }
231
+ ]
232
+ json = people.to_json only: [:address, :city]
233
+
234
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
235
+ end
236
+
237
+ People = Class.new(BasicObject) do
238
+ include Enumerable
239
+ def initialize
240
+ @people = [
241
+ { name: "John", address: { city: "London", country: "UK" } },
242
+ { name: "Jean", address: { city: "Paris", country: "France" } }
243
+ ]
244
+ end
245
+ def each(*, &blk)
246
+ @people.each do |p|
247
+ yield p if blk
248
+ p
249
+ end.each
250
+ end
251
+ end
252
+
253
+ def test_enumerable_should_generate_json_with_as_json
254
+ json = People.new.as_json only: [:address, :city]
255
+ expected = [
256
+ { "address" => { "city" => "London" } },
257
+ { "address" => { "city" => "Paris" } }
258
+ ]
259
+
260
+ assert_equal(expected, json)
261
+ end
262
+
263
+ def test_enumerable_should_generate_json_with_to_json
264
+ json = People.new.to_json only: [:address, :city]
265
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
266
+ end
267
+
268
+ def test_enumerable_should_pass_encoding_options_to_children_in_as_json
269
+ json = People.new.each.as_json only: [:address, :city]
270
+ expected = [
271
+ { "address" => { "city" => "London" } },
272
+ { "address" => { "city" => "Paris" } }
273
+ ]
274
+
275
+ assert_equal(expected, json)
276
+ end
277
+
278
+ def test_enumerable_should_pass_encoding_options_to_children_in_to_json
279
+ json = People.new.each.to_json only: [:address, :city]
280
+
281
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
282
+ end
283
+
284
+ class CustomWithOptions
285
+ attr_accessor :foo, :bar
286
+
287
+ def as_json(options = {})
288
+ options[:only] = %w(foo bar)
289
+ super(options)
290
+ end
291
+ end
292
+
293
+ def test_hash_to_json_should_not_keep_options_around
294
+ f = CustomWithOptions.new
295
+ f.foo = "hello"
296
+ f.bar = "world"
297
+
298
+ hash = { "foo" => f, "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }
299
+ assert_equal({ "foo" => { "foo" => "hello", "bar" => "world" },
300
+ "other_hash" => { "foo" => "other_foo", "test" => "other_test" } }, ActiveSupport::JSON.decode(hash.to_json))
301
+ end
302
+
303
+ def test_array_to_json_should_not_keep_options_around
304
+ f = CustomWithOptions.new
305
+ f.foo = "hello"
306
+ f.bar = "world"
307
+
308
+ array = [f, { "foo" => "other_foo", "test" => "other_test" }]
309
+ assert_equal([{ "foo" => "hello", "bar" => "world" },
310
+ { "foo" => "other_foo", "test" => "other_test" }], ActiveSupport::JSON.decode(array.to_json))
311
+ end
312
+
313
+ class OptionsTest
314
+ def as_json(options = :default)
315
+ options
316
+ end
317
+ end
318
+
319
+ def test_hash_as_json_without_options
320
+ json = { foo: OptionsTest.new }.as_json
321
+ assert_equal({ "foo" => :default }, json)
322
+ end
323
+
324
+ def test_array_as_json_without_options
325
+ json = [ OptionsTest.new ].as_json
326
+ assert_equal([:default], json)
327
+ end
328
+
329
+ def test_struct_encoding
330
+ Struct.new("UserNameAndEmail", :name, :email)
331
+ Struct.new("UserNameAndDate", :name, :date)
332
+ Struct.new("Custom", :name, :sub)
333
+ user_email = Struct::UserNameAndEmail.new "David", "sample@example.com"
334
+ user_birthday = Struct::UserNameAndDate.new "David", Date.new(2010, 01, 01)
335
+ custom = Struct::Custom.new "David", user_birthday
336
+
337
+ json_strings = ""
338
+ json_string_and_date = ""
339
+ json_custom = ""
340
+
341
+ assert_nothing_raised do
342
+ json_strings = user_email.to_json
343
+ json_string_and_date = user_birthday.to_json
344
+ json_custom = custom.to_json
345
+ end
346
+
347
+ assert_equal({ "name" => "David",
348
+ "sub" => {
349
+ "name" => "David",
350
+ "date" => "2010-01-01" } }, ActiveSupport::JSON.decode(json_custom))
351
+
352
+ assert_equal({ "name" => "David", "email" => "sample@example.com" },
353
+ ActiveSupport::JSON.decode(json_strings))
354
+
355
+ assert_equal({ "name" => "David", "date" => "2010-01-01" },
356
+ ActiveSupport::JSON.decode(json_string_and_date))
357
+ end
358
+
359
+ def test_nil_true_and_false_represented_as_themselves
360
+ assert_nil nil.as_json
361
+ assert_equal true, true.as_json
362
+ assert_equal false, false.as_json
363
+ end
364
+
365
+ class HashWithAsJson < Hash
366
+ attr_accessor :as_json_called
367
+
368
+ def initialize(*)
369
+ super
370
+ end
371
+
372
+ def as_json(options = {})
373
+ @as_json_called = true
374
+ super
375
+ end
376
+ end
377
+
378
+ def test_json_gem_dump_by_passing_active_support_encoder
379
+ h = HashWithAsJson.new
380
+ h[:foo] = "hello"
381
+ h[:bar] = "world"
382
+
383
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h)
384
+ assert_nil h.as_json_called
385
+ end
386
+
387
+ def test_json_gem_generate_by_passing_active_support_encoder
388
+ h = HashWithAsJson.new
389
+ h[:foo] = "hello"
390
+ h[:bar] = "world"
391
+
392
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h)
393
+ assert_nil h.as_json_called
394
+ end
395
+
396
+ def test_json_gem_pretty_generate_by_passing_active_support_encoder
397
+ h = HashWithAsJson.new
398
+ h[:foo] = "hello"
399
+ h[:bar] = "world"
400
+
401
+ assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h)
402
+ {
403
+ "foo": "hello",
404
+ "bar": "world"
405
+ }
406
+ EXPECTED
407
+ assert_nil h.as_json_called
408
+ end
409
+
410
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
411
+ with_standard_json_time_format(false) do
412
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
413
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
414
+ assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
415
+ end
416
+ end
417
+
418
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
419
+ with_standard_json_time_format(true) do
420
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
421
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
422
+ assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
423
+ end
424
+ end
425
+
426
+ def test_twz_to_json_with_custom_time_precision
427
+ with_standard_json_time_format(true) do
428
+ with_time_precision(0) do
429
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
430
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
431
+ assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
432
+ end
433
+ end
434
+ end
435
+
436
+ def test_time_to_json_with_custom_time_precision
437
+ with_standard_json_time_format(true) do
438
+ with_time_precision(0) do
439
+ assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
440
+ end
441
+ end
442
+ end
443
+
444
+ def test_datetime_to_json_with_custom_time_precision
445
+ with_standard_json_time_format(true) do
446
+ with_time_precision(0) do
447
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
448
+ end
449
+ end
450
+ end
451
+
452
+ def test_twz_to_json_when_wrapping_a_date_time
453
+ zone = ActiveSupport::TimeZone["Eastern Time (US & Canada)"]
454
+ time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
455
+ assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
456
+ end
457
+
458
+ def test_exception_to_json
459
+ exception = Exception.new("foo")
460
+ assert_equal '"foo"', ActiveSupport::JSON.encode(exception)
461
+ end
462
+
463
+ class InfiniteNumber
464
+ def as_json(options = nil)
465
+ { "number" => Float::INFINITY }
466
+ end
467
+ end
468
+
469
+ def test_to_json_works_when_as_json_returns_infinite_number
470
+ assert_equal '{"number":null}', InfiniteNumber.new.to_json
471
+ end
472
+
473
+ class NaNNumber
474
+ def as_json(options = nil)
475
+ { "number" => Float::NAN }
476
+ end
477
+ end
478
+
479
+ def test_to_json_works_when_as_json_returns_NaN_number
480
+ assert_equal '{"number":null}', NaNNumber.new.to_json
481
+ end
482
+
483
+ def test_to_json_works_on_io_objects
484
+ assert_equal STDOUT.to_s.to_json, STDOUT.to_json
485
+ end
486
+
487
+ private
488
+
489
+ def object_keys(json_object)
490
+ json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
491
+ end
492
+
493
+ def with_standard_json_time_format(boolean = true)
494
+ old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
495
+ yield
496
+ ensure
497
+ ActiveSupport.use_standard_json_time_format = old
498
+ end
499
+
500
+ def with_time_precision(value)
501
+ old_value = ActiveSupport::JSON::Encoding.time_precision
502
+ ActiveSupport::JSON::Encoding.time_precision = value
503
+ yield
504
+ ensure
505
+ ActiveSupport::JSON::Encoding.time_precision = old_value
506
+ end
507
+ end