oj 2.18.5 → 3.0.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 (111) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +33 -226
  3. data/ext/oj/circarray.c +0 -25
  4. data/ext/oj/circarray.h +0 -25
  5. data/ext/oj/code.c +227 -0
  6. data/ext/oj/code.h +40 -0
  7. data/ext/oj/compat.c +126 -38
  8. data/ext/oj/custom.c +1097 -0
  9. data/ext/oj/dump.c +658 -2376
  10. data/ext/oj/dump.h +92 -0
  11. data/ext/oj/dump_compat.c +937 -0
  12. data/ext/oj/dump_leaf.c +254 -0
  13. data/ext/oj/dump_object.c +810 -0
  14. data/ext/oj/dump_rails.c +329 -0
  15. data/ext/oj/dump_strict.c +416 -0
  16. data/ext/oj/err.c +0 -25
  17. data/ext/oj/err.h +8 -2
  18. data/ext/oj/fast.c +24 -24
  19. data/ext/oj/mimic_json.c +817 -0
  20. data/ext/oj/mimic_rails.c +806 -0
  21. data/ext/oj/mimic_rails.h +17 -0
  22. data/ext/oj/object.c +18 -72
  23. data/ext/oj/odd.c +0 -25
  24. data/ext/oj/odd.h +2 -27
  25. data/ext/oj/oj.c +655 -1503
  26. data/ext/oj/oj.h +93 -40
  27. data/ext/oj/parse.c +99 -46
  28. data/ext/oj/parse.h +12 -26
  29. data/ext/oj/reader.c +1 -25
  30. data/ext/oj/reader.h +3 -25
  31. data/ext/oj/resolve.c +9 -11
  32. data/ext/oj/resolve.h +2 -2
  33. data/ext/oj/rxclass.c +133 -0
  34. data/ext/oj/rxclass.h +27 -0
  35. data/ext/oj/saj.c +4 -25
  36. data/ext/oj/scp.c +3 -25
  37. data/ext/oj/sparse.c +89 -13
  38. data/ext/oj/stream_writer.c +301 -0
  39. data/ext/oj/strict.c +4 -27
  40. data/ext/oj/string_writer.c +480 -0
  41. data/ext/oj/val_stack.h +6 -2
  42. data/lib/oj.rb +1 -23
  43. data/lib/oj/easy_hash.rb +12 -4
  44. data/lib/oj/json.rb +172 -0
  45. data/lib/oj/mimic.rb +123 -18
  46. data/lib/oj/state.rb +131 -0
  47. data/lib/oj/version.rb +1 -1
  48. data/pages/Advanced.md +22 -0
  49. data/pages/Compatibility.md +25 -0
  50. data/pages/Custom.md +23 -0
  51. data/pages/Encoding.md +65 -0
  52. data/pages/JsonGem.md +79 -0
  53. data/pages/Modes.md +140 -0
  54. data/pages/Options.md +250 -0
  55. data/pages/Rails.md +60 -0
  56. data/pages/Security.md +20 -0
  57. data/test/activesupport4/decoding_test.rb +105 -0
  58. data/test/activesupport4/encoding_test.rb +531 -0
  59. data/test/activesupport4/test_helper.rb +41 -0
  60. data/test/activesupport5/decoding_test.rb +125 -0
  61. data/test/activesupport5/encoding_test.rb +483 -0
  62. data/test/activesupport5/encoding_test_cases.rb +90 -0
  63. data/test/activesupport5/test_helper.rb +50 -0
  64. data/test/activesupport5/time_zone_test_helpers.rb +24 -0
  65. data/test/json_gem/json_addition_test.rb +216 -0
  66. data/test/json_gem/json_common_interface_test.rb +143 -0
  67. data/test/json_gem/json_encoding_test.rb +109 -0
  68. data/test/json_gem/json_ext_parser_test.rb +20 -0
  69. data/test/json_gem/json_fixtures_test.rb +35 -0
  70. data/test/json_gem/json_generator_test.rb +383 -0
  71. data/test/json_gem/json_generic_object_test.rb +90 -0
  72. data/test/json_gem/json_parser_test.rb +470 -0
  73. data/test/json_gem/json_string_matching_test.rb +42 -0
  74. data/test/json_gem/test_helper.rb +18 -0
  75. data/test/perf_compat.rb +30 -28
  76. data/test/perf_object.rb +1 -1
  77. data/test/perf_strict.rb +18 -1
  78. data/test/sample.rb +0 -1
  79. data/test/test_compat.rb +169 -93
  80. data/test/test_custom.rb +355 -0
  81. data/test/test_file.rb +0 -8
  82. data/test/test_null.rb +376 -0
  83. data/test/test_object.rb +268 -3
  84. data/test/test_scp.rb +22 -1
  85. data/test/test_strict.rb +160 -4
  86. data/test/test_various.rb +52 -620
  87. data/test/tests.rb +14 -0
  88. data/test/tests_mimic.rb +14 -0
  89. data/test/tests_mimic_addition.rb +7 -0
  90. metadata +89 -47
  91. data/test/activesupport_datetime_test.rb +0 -23
  92. data/test/bug.rb +0 -51
  93. data/test/bug2.rb +0 -10
  94. data/test/bug3.rb +0 -46
  95. data/test/bug_fast.rb +0 -32
  96. data/test/bug_load.rb +0 -24
  97. data/test/crash.rb +0 -111
  98. data/test/curl/curl_oj.rb +0 -46
  99. data/test/curl/get_oj.rb +0 -24
  100. data/test/curl/just_curl.rb +0 -31
  101. data/test/curl/just_oj.rb +0 -51
  102. data/test/example.rb +0 -11
  103. data/test/foo.rb +0 -24
  104. data/test/io.rb +0 -48
  105. data/test/isolated/test_mimic_rails_datetime.rb +0 -27
  106. data/test/mod.rb +0 -16
  107. data/test/rails.rb +0 -50
  108. data/test/russian.rb +0 -18
  109. data/test/struct.rb +0 -29
  110. data/test/test_serializer.rb +0 -59
  111. data/test/write_timebars.rb +0 -31
@@ -0,0 +1,60 @@
1
+ # Oj Rails Compatibility
2
+
3
+ The `:rails` mode mimics the ActiveSupport version 5 encoder. Rails and
4
+ ActiveSupport are built around the use of the `as_json(*)` method defined for
5
+ a class. Oj attempts to provide the same functionality by being a drop in
6
+ replacement with a few exceptions.
7
+
8
+ ```ruby
9
+ require 'oj'
10
+
11
+ Oj::Rails.set_encoder()
12
+ Oj::Rails.set_decoder()
13
+ Oj::Rails.optimize(Array, BigDecimal, Hash, Range, Regexp, Time)
14
+ ```
15
+
16
+ Some of the Oj options are supported as arguments to the encoder if called
17
+ from Oj::Rails.encode() but when using the Oj::Rails::Encoder class the
18
+ encode() method does not support optional arguments as required by the
19
+ ActiveSupport compliance guidelines. The general approach Rails takes for
20
+ configuring encoding options is to either set global values or to create a new
21
+ instance of the Encoder class and provide options in the initializer.
22
+
23
+ The globals that ActiveSupport uses for encoding are:
24
+
25
+ * ActiveSupport::JSON::Encoding.use_standard_json_time_format
26
+ * ActiveSupport::JSON::Encoding.escape_html_entities_in_json
27
+ * ActiveSupport::JSON::Encoding.time_precision
28
+ * ActiveSupport::JSON::Encoding.json_encoder
29
+
30
+ Those globals are aliased to also be accessed from the ActiveSupport module
31
+ directly so ActiveSupport::JSON::Encoding.time_precision can also be accessed
32
+ from ActiveSupport.time_precision. Oj makes use of these globals in mimicing
33
+ Rails after the Oj::Rails.set_encode() method is called. That also sets the
34
+ ActiveSupport.json_encoder to the Oj::Rails::Encoder class.
35
+
36
+ Options passed into a call to to_json() are passed to the as_json()
37
+ methods. These are mostly ignored by Oj and simply passed on without
38
+ modifications as per the guidelines. The exception to this are the options
39
+ specific to Oj such as the :circular option which it used to detect circular
40
+ references while encoding.
41
+
42
+ By default Oj acts like the ActiveSupport encoder and honors any changes in
43
+ the as_json() methods. There are also optimized encoders for some
44
+ classes. When the optimized encoder it toggled the as_json() methods will not
45
+ be called for that class but instead the optimized version will be called. The
46
+ optimized version is the same as the ActiveSupport default encoding for a
47
+ given class. The optimized versions are toggled with the optimize() and
48
+ deoptimize() methods.
49
+
50
+ The classes that can be put in optimized mode are:
51
+
52
+ * Array
53
+ * BigDecimal
54
+ * Hash
55
+ * Range
56
+ * Regexp
57
+ * Time
58
+
59
+ The ActiveSupport decoder is the JSON.parse() method. Calling the
60
+ Oj::Rails.set_decoder() method replaces that method with the Oj equivelant.
@@ -0,0 +1,20 @@
1
+ # Security and Optimization
2
+
3
+ Two settings in Oj are useful for parsing but do expose a vulnerability if used
4
+ from an untrusted source. Symbolized keys can cause memory to be filled with
5
+ previous versions of ruby. Ruby 2.1 and below does not garbage collect
6
+ Symbols. The same is true for auto defining classes in all versions of ruby;
7
+ memory will also be exhausted if too many classes are automatically
8
+ defined. Auto defining is a useful feature during development and from trusted
9
+ sources but it allows too many classes to be created in the object load mode and
10
+ auto defined is used with an untrusted source. The `Oj.strict_load()` method
11
+ sets and uses the most strict and safest options. It should be used by
12
+ developers who find it difficult to understand the options available in Oj.
13
+
14
+ The options in Oj are designed to provide flexibility to the developer. This
15
+ flexibility allows Objects to be serialized and deserialized. No methods are
16
+ ever called on these created Objects but that does not stop the developer from
17
+ calling methods on them. As in any system, check your inputs before working with
18
+ them. Taking an arbitrary `String` from a user and evaluating it is never a good
19
+ idea from an unsecure source. The same is true for `Object` attributes as they
20
+ are not more than `String`s. Always check inputs from untrusted sources.
@@ -0,0 +1,105 @@
1
+ # encoding: utf-8
2
+ require 'activesupport4/test_helper'
3
+ require 'active_support/json'
4
+ require 'active_support/time'
5
+
6
+ class TestJSONDecoding < ActiveSupport::TestCase
7
+ class Foo
8
+ def self.json_create(object)
9
+ "Foo"
10
+ end
11
+ end
12
+
13
+ TESTS = {
14
+ %q({"returnTo":{"\/categories":"\/"}}) => {"returnTo" => {"/categories" => "/"}},
15
+ %q({"return\\"To\\":":{"\/categories":"\/"}}) => {"return\"To\":" => {"/categories" => "/"}},
16
+ %q({"returnTo":{"\/categories":1}}) => {"returnTo" => {"/categories" => 1}},
17
+ %({"returnTo":[1,"a"]}) => {"returnTo" => [1, "a"]},
18
+ %({"returnTo":[1,"\\"a\\",", "b"]}) => {"returnTo" => [1, "\"a\",", "b"]},
19
+ %({"a": "'", "b": "5,000"}) => {"a" => "'", "b" => "5,000"},
20
+ %({"a": "a's, b's and c's", "b": "5,000"}) => {"a" => "a's, b's and c's", "b" => "5,000"},
21
+ # multibyte
22
+ %({"matzue": "松江", "asakusa": "浅草"}) => {"matzue" => "松江", "asakusa" => "浅草"},
23
+ %({"a": "2007-01-01"}) => {'a' => Date.new(2007, 1, 1)},
24
+ %({"a": "2007-01-01 01:12:34 Z"}) => {'a' => Time.utc(2007, 1, 1, 1, 12, 34)},
25
+ %(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)],
26
+ %(["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)],
27
+ # no time zone
28
+ %({"a": "2007-01-01 01:12:34"}) => {'a' => "2007-01-01 01:12:34"},
29
+ # invalid date
30
+ %({"a": "1089-10-40"}) => {'a' => "1089-10-40"},
31
+ # xmlschema date notation
32
+ %({"a": "2009-08-10T19:01:02Z"}) => {'a' => Time.utc(2009, 8, 10, 19, 1, 2)},
33
+ %({"a": "2009-08-10T19:01:02+02:00"}) => {'a' => Time.utc(2009, 8, 10, 17, 1, 2)},
34
+ %({"a": "2009-08-10T19:01:02-05:00"}) => {'a' => Time.utc(2009, 8, 11, 00, 1, 2)},
35
+ # needs to be *exact*
36
+ %({"a": " 2007-01-01 01:12:34 Z "}) => {'a' => " 2007-01-01 01:12:34 Z "},
37
+ %({"a": "2007-01-01 : it's your birthday"}) => {'a' => "2007-01-01 : it's your birthday"},
38
+ %([]) => [],
39
+ %({}) => {},
40
+ %({"a":1}) => {"a" => 1},
41
+ %({"a": ""}) => {"a" => ""},
42
+ %({"a":"\\""}) => {"a" => "\""},
43
+ %({"a": null}) => {"a" => nil},
44
+ %({"a": true}) => {"a" => true},
45
+ %({"a": false}) => {"a" => false},
46
+ %q({"bad":"\\\\","trailing":""}) => {"bad" => "\\", "trailing" => ""},
47
+ %q({"a": "http:\/\/test.host\/posts\/1"}) => {"a" => "http://test.host/posts/1"},
48
+ %q({"a": "\u003cunicode\u0020escape\u003e"}) => {"a" => "<unicode escape>"},
49
+ %q({"a": "\\\\u0020skip double backslashes"}) => {"a" => "\\u0020skip double backslashes"},
50
+ %q({"a": "\u003cbr /\u003e"}) => {'a' => "<br />"},
51
+ %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => {'b' => ["<i>","<b>","<u>"]},
52
+ # test combination of dates and escaped or unicode encoded data in arrays
53
+ %q([{"d":"1970-01-01", "s":"\u0020escape"},{"d":"1970-01-01", "s":"\u0020escape"}]) =>
54
+ [{'d' => Date.new(1970, 1, 1), 's' => ' escape'},{'d' => Date.new(1970, 1, 1), 's' => ' escape'}],
55
+ %q([{"d":"1970-01-01","s":"http:\/\/example.com"},{"d":"1970-01-01","s":"http:\/\/example.com"}]) =>
56
+ [{'d' => Date.new(1970, 1, 1), 's' => 'http://example.com'},
57
+ {'d' => Date.new(1970, 1, 1), 's' => 'http://example.com'}],
58
+ # tests escaping of "\n" char with Yaml backend
59
+ %q({"a":"\n"}) => {"a"=>"\n"},
60
+ %q({"a":"\u000a"}) => {"a"=>"\n"},
61
+ %q({"a":"Line1\u000aLine2"}) => {"a"=>"Line1\nLine2"},
62
+ # prevent json unmarshalling
63
+ %q({"json_class":"TestJSONDecoding::Foo"}) => {"json_class"=>"TestJSONDecoding::Foo"},
64
+ # json "fragments" - these are invalid JSON, but ActionPack relies on this
65
+ %q("a string") => "a string",
66
+ %q(1.1) => 1.1,
67
+ %q(1) => 1,
68
+ %q(-1) => -1,
69
+ %q(true) => true,
70
+ %q(false) => false,
71
+ %q(null) => nil
72
+ }
73
+
74
+ TESTS.each_with_index do |(json, expected), index|
75
+ test "json decodes #{index}" do
76
+ prev = ActiveSupport.parse_json_times
77
+ ActiveSupport.parse_json_times = true
78
+ silence_warnings do
79
+ assert_equal expected, ActiveSupport::JSON.decode(json), "JSON decoding \
80
+ failed for #{json}"
81
+ end
82
+ ActiveSupport.parse_json_times = prev
83
+ end
84
+ end
85
+
86
+ test "json decodes time json with time parsing disabled" do
87
+ prev = ActiveSupport.parse_json_times
88
+ ActiveSupport.parse_json_times = false
89
+ expected = {"a" => "2007-01-01 01:12:34 Z"}
90
+ assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"}))
91
+ ActiveSupport.parse_json_times = prev
92
+ end
93
+
94
+ def test_failed_json_decoding
95
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) }
96
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) }
97
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) }
98
+ assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) }
99
+ end
100
+
101
+ def test_cannot_pass_unsupported_options
102
+ assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) }
103
+ end
104
+ end
105
+
@@ -0,0 +1,531 @@
1
+ # encoding: utf-8
2
+ require 'activesupport4/test_helper'
3
+ require 'securerandom'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'active_support/json'
6
+ require 'active_support/time'
7
+
8
+
9
+ class TestJSONEncoding < ActiveSupport::TestCase
10
+ class Foo
11
+ def initialize(a, b)
12
+ @a, @b = a, b
13
+ end
14
+ end
15
+
16
+ class Hashlike
17
+ def to_hash
18
+ { :foo => "hello", :bar => "world" }
19
+ end
20
+ end
21
+
22
+ class Custom
23
+ def initialize(serialized)
24
+ @serialized = serialized
25
+ end
26
+
27
+ def as_json(options = nil)
28
+ @serialized
29
+ end
30
+ end
31
+
32
+ class CustomWithOptions
33
+ attr_accessor :foo, :bar
34
+
35
+ def as_json(options={})
36
+ options[:only] = %w(foo bar)
37
+ super(options)
38
+ end
39
+ end
40
+
41
+ class OptionsTest
42
+ def as_json(options = :default)
43
+ options
44
+ end
45
+ end
46
+
47
+ class HashWithAsJson < Hash
48
+ attr_accessor :as_json_called
49
+
50
+ def initialize(*)
51
+ super
52
+ end
53
+
54
+ def as_json(options={})
55
+ @as_json_called = true
56
+ super
57
+ end
58
+ end
59
+
60
+ TrueTests = [[ true, %(true) ]]
61
+ FalseTests = [[ false, %(false) ]]
62
+ NilTests = [[ nil, %(null) ]]
63
+ NumericTests = [[ 1, %(1) ],
64
+ [ 2.5, %(2.5) ],
65
+ [ 0.0/0.0, %(null) ],
66
+ [ 1.0/0.0, %(null) ],
67
+ [ -1.0/0.0, %(null) ],
68
+ [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ],
69
+ [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]]
70
+
71
+ StringTests = [[ 'this is the <string>', %("this is the \\u003cstring\\u003e")],
72
+ [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
73
+ [ 'http://test.host/posts/1', %("http://test.host/posts/1")],
74
+ [ "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",
75
+ %("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") ]]
76
+
77
+ ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
78
+ [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
79
+
80
+ RangeTests = [[ 1..2, %("1..2")],
81
+ [ 1...2, %("1...2")],
82
+ [ 1.5..2.5, %("1.5..2.5")]]
83
+
84
+ SymbolTests = [[ :a, %("a") ],
85
+ [ :this, %("this") ],
86
+ [ :"a b", %("a b") ]]
87
+
88
+ ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]]
89
+ HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]]
90
+ CustomTests = [[ Custom.new("custom"), '"custom"' ],
91
+ [ Custom.new(nil), 'null' ],
92
+ [ Custom.new(:a), '"a"' ],
93
+ [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
94
+ [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
95
+ [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
96
+ [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
97
+
98
+ RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
99
+
100
+ DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]]
101
+ TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
102
+ DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
103
+
104
+ StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]]
105
+ StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
106
+ StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
107
+ StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]]
108
+
109
+ def sorted_json(json)
110
+ return json unless json =~ /^\{.*\}$/
111
+ '{' + json[1..-2].split(',').sort.join(',') + '}'
112
+ end
113
+
114
+ constants.grep(/Tests$/).each do |class_tests|
115
+ define_method("test_#{class_tests[0..-6].underscore}") do
116
+ begin
117
+ prev = ActiveSupport.use_standard_json_time_format
118
+
119
+ ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/
120
+ ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/
121
+ self.class.const_get(class_tests).each do |pair|
122
+ assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
123
+ end
124
+ ensure
125
+ ActiveSupport.escape_html_entities_in_json = false
126
+ ActiveSupport.use_standard_json_time_format = prev
127
+ end
128
+ end
129
+ end
130
+
131
+ def test_process_status
132
+ # There doesn't seem to be a good way to get a handle on a Process::Status object without actually
133
+ # creating a child process, hence this to populate $?
134
+ system("not_a_real_program_#{SecureRandom.hex}")
135
+ assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
136
+ end
137
+
138
+ def test_hash_encoding
139
+ assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b)
140
+ assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1)
141
+ assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode('a' => [1,2])
142
+ assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
143
+
144
+ assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d))
145
+ end
146
+
147
+ def test_hash_keys_encoding
148
+ ActiveSupport.escape_html_entities_in_json = true
149
+ assert_equal "{\"\\u003c\\u003e\":\"\\u003c\\u003e\"}", ActiveSupport::JSON.encode("<>" => "<>")
150
+ ensure
151
+ ActiveSupport.escape_html_entities_in_json = false
152
+ end
153
+
154
+ def test_utf8_string_encoded_properly
155
+ result = ActiveSupport::JSON.encode('€2.99')
156
+ assert_equal '"€2.99"', result
157
+ assert_equal(Encoding::UTF_8, result.encoding)
158
+
159
+ result = ActiveSupport::JSON.encode('✎☺')
160
+ assert_equal '"✎☺"', result
161
+ assert_equal(Encoding::UTF_8, result.encoding)
162
+ end
163
+
164
+ def test_non_utf8_string_transcodes
165
+ s = '二'.encode('Shift_JIS')
166
+ result = ActiveSupport::JSON.encode(s)
167
+ assert_equal '"二"', result
168
+ assert_equal Encoding::UTF_8, result.encoding
169
+ end
170
+
171
+ def test_wide_utf8_chars
172
+ w = '𠜎'
173
+ result = ActiveSupport::JSON.encode(w)
174
+ assert_equal '"𠜎"', result
175
+ end
176
+
177
+ def test_wide_utf8_roundtrip
178
+ hash = { string: "𐒑" }
179
+ json = ActiveSupport::JSON.encode(hash)
180
+ decoded_hash = ActiveSupport::JSON.decode(json)
181
+ assert_equal "𐒑", decoded_hash['string']
182
+ end
183
+
184
+ def test_reading_encode_big_decimal_as_string_option
185
+ assert_deprecated do
186
+ assert ActiveSupport.encode_big_decimal_as_string
187
+ end
188
+ end
189
+
190
+ def test_setting_deprecated_encode_big_decimal_as_string_option
191
+ assert_raise(NotImplementedError) do
192
+ ActiveSupport.encode_big_decimal_as_string = true
193
+ end
194
+
195
+ assert_raise(NotImplementedError) do
196
+ ActiveSupport.encode_big_decimal_as_string = false
197
+ end
198
+ end
199
+
200
+ def test_exception_raised_when_encoding_circular_reference_in_array
201
+ a = [1]
202
+ a << a
203
+ assert_deprecated do
204
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
205
+ end
206
+ end
207
+
208
+ def test_exception_raised_when_encoding_circular_reference_in_hash
209
+ a = { :name => 'foo' }
210
+ a[:next] = a
211
+ assert_deprecated do
212
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
213
+ end
214
+ end
215
+
216
+ def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array
217
+ a = { :name => 'foo', :sub => [] }
218
+ a[:sub] << a
219
+ assert_deprecated do
220
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
221
+ end
222
+ end
223
+
224
+ def test_hash_key_identifiers_are_always_quoted
225
+ values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
226
+ assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
227
+ end
228
+
229
+ def test_hash_should_allow_key_filtering_with_only
230
+ assert_equal %({"a":1}), ActiveSupport::JSON.encode({'a' => 1, :b => 2, :c => 3}, :only => 'a')
231
+ end
232
+
233
+ def test_hash_should_allow_key_filtering_with_except
234
+ assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c])
235
+ end
236
+
237
+ def test_time_to_json_includes_local_offset
238
+ with_standard_json_time_format(true) do
239
+ with_env_tz 'US/Eastern' do
240
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
241
+ end
242
+ end
243
+ end
244
+
245
+ def test_hash_with_time_to_json
246
+ with_standard_json_time_format(false) do
247
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
248
+ end
249
+ end
250
+
251
+ def test_nested_hash_with_float
252
+ assert_nothing_raised do
253
+ hash = {
254
+ "CHI" => {
255
+ :display_name => "chicago",
256
+ :latitude => 123.234
257
+ }
258
+ }
259
+ ActiveSupport::JSON.encode(hash)
260
+ end
261
+ end
262
+
263
+ def test_hash_like_with_options
264
+ h = Hashlike.new
265
+ json = h.to_json :only => [:foo]
266
+
267
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
268
+ end
269
+
270
+ def test_object_to_json_with_options
271
+ obj = Object.new
272
+ obj.instance_variable_set :@foo, "hello"
273
+ obj.instance_variable_set :@bar, "world"
274
+ json = obj.to_json :only => ["foo"]
275
+
276
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
277
+ end
278
+
279
+ def test_struct_to_json_with_options
280
+ struct = Struct.new(:foo, :bar).new
281
+ struct.foo = "hello"
282
+ struct.bar = "world"
283
+ json = struct.to_json :only => [:foo]
284
+
285
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
286
+ end
287
+
288
+ def test_hash_should_pass_encoding_options_to_children_in_as_json
289
+ person = {
290
+ :name => 'John',
291
+ :address => {
292
+ :city => 'London',
293
+ :country => 'UK'
294
+ }
295
+ }
296
+ json = person.as_json :only => [:address, :city]
297
+
298
+ assert_equal({ 'address' => { 'city' => 'London' }}, json)
299
+ end
300
+
301
+ def test_hash_should_pass_encoding_options_to_children_in_to_json
302
+ person = {
303
+ :name => 'John',
304
+ :address => {
305
+ :city => 'London',
306
+ :country => 'UK'
307
+ }
308
+ }
309
+ json = person.to_json :only => [:address, :city]
310
+
311
+ assert_equal(%({"address":{"city":"London"}}), json)
312
+ end
313
+
314
+ def test_array_should_pass_encoding_options_to_children_in_as_json
315
+ people = [
316
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
317
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
318
+ ]
319
+ json = people.as_json :only => [:address, :city]
320
+ expected = [
321
+ { 'address' => { 'city' => 'London' }},
322
+ { 'address' => { 'city' => 'Paris' }}
323
+ ]
324
+
325
+ assert_equal(expected, json)
326
+ end
327
+
328
+ def test_array_should_pass_encoding_options_to_children_in_to_json
329
+ people = [
330
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
331
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
332
+ ]
333
+ json = people.to_json :only => [:address, :city]
334
+
335
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
336
+ end
337
+
338
+ def test_enumerable_should_pass_encoding_options_to_children_in_as_json
339
+ people = [
340
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
341
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
342
+ ]
343
+ json = people.each.as_json :only => [:address, :city]
344
+ expected = [
345
+ { 'address' => { 'city' => 'London' }},
346
+ { 'address' => { 'city' => 'Paris' }}
347
+ ]
348
+
349
+ assert_equal(expected, json)
350
+ end
351
+
352
+ def test_enumerable_should_pass_encoding_options_to_children_in_to_json
353
+ people = [
354
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
355
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
356
+ ]
357
+ json = people.each.to_json :only => [:address, :city]
358
+
359
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
360
+ end
361
+
362
+ def test_hash_to_json_should_not_keep_options_around
363
+ f = CustomWithOptions.new
364
+ f.foo = "hello"
365
+ f.bar = "world"
366
+
367
+ hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
368
+ assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
369
+ "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json))
370
+ end
371
+
372
+ def test_array_to_json_should_not_keep_options_around
373
+ f = CustomWithOptions.new
374
+ f.foo = "hello"
375
+ f.bar = "world"
376
+
377
+ array = [f, {"foo" => "other_foo", "test" => "other_test"}]
378
+ assert_equal([{"foo"=>"hello","bar"=>"world"},
379
+ {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json))
380
+ end
381
+
382
+ def test_hash_as_json_without_options
383
+ json = { foo: OptionsTest.new }.as_json
384
+ assert_equal({"foo" => :default}, json)
385
+ end
386
+
387
+ def test_array_as_json_without_options
388
+ json = [ OptionsTest.new ].as_json
389
+ assert_equal([:default], json)
390
+ end
391
+
392
+ def test_struct_encoding
393
+ Struct.new('UserNameAndEmail', :name, :email)
394
+ Struct.new('UserNameAndDate', :name, :date)
395
+ Struct.new('Custom', :name, :sub)
396
+ user_email = Struct::UserNameAndEmail.new 'David', 'sample@example.com'
397
+ user_birthday = Struct::UserNameAndDate.new 'David', Date.new(2010, 01, 01)
398
+ custom = Struct::Custom.new 'David', user_birthday
399
+
400
+
401
+ json_strings = ""
402
+ json_string_and_date = ""
403
+ json_custom = ""
404
+
405
+ assert_nothing_raised do
406
+ json_strings = user_email.to_json
407
+ json_string_and_date = user_birthday.to_json
408
+ json_custom = custom.to_json
409
+ end
410
+
411
+ assert_equal({"name" => "David",
412
+ "sub" => {
413
+ "name" => "David",
414
+ "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom))
415
+
416
+ assert_equal({"name" => "David", "email" => "sample@example.com"},
417
+ ActiveSupport::JSON.decode(json_strings))
418
+
419
+ assert_equal({"name" => "David", "date" => "2010-01-01"},
420
+ ActiveSupport::JSON.decode(json_string_and_date))
421
+ end
422
+
423
+ def test_nil_true_and_false_represented_as_themselves
424
+ assert_equal nil, nil.as_json
425
+ assert_equal true, true.as_json
426
+ assert_equal false, false.as_json
427
+ end
428
+
429
+ def test_json_gem_dump_by_passing_active_support_encoder
430
+ h = HashWithAsJson.new
431
+ h[:foo] = "hello"
432
+ h[:bar] = "world"
433
+
434
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h)
435
+ assert_nil h.as_json_called
436
+ end
437
+
438
+ def test_json_gem_generate_by_passing_active_support_encoder
439
+ h = HashWithAsJson.new
440
+ h[:foo] = "hello"
441
+ h[:bar] = "world"
442
+
443
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h)
444
+ assert_nil h.as_json_called
445
+ end
446
+
447
+ def test_json_gem_pretty_generate_by_passing_active_support_encoder
448
+ h = HashWithAsJson.new
449
+ h[:foo] = "hello"
450
+ h[:bar] = "world"
451
+
452
+ assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h)
453
+ {
454
+ "foo": "hello",
455
+ "bar": "world"
456
+ }
457
+ EXPECTED
458
+ assert_nil h.as_json_called
459
+ end
460
+
461
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_false
462
+ with_standard_json_time_format(false) do
463
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
464
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
465
+ assert_equal "\"1999/12/31 19:00:00 -0500\"", ActiveSupport::JSON.encode(time)
466
+ end
467
+ end
468
+
469
+ def test_twz_to_json_with_use_standard_json_time_format_config_set_to_true
470
+ with_standard_json_time_format(true) do
471
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
472
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
473
+ assert_equal "\"1999-12-31T19:00:00.000-05:00\"", ActiveSupport::JSON.encode(time)
474
+ end
475
+ end
476
+
477
+ def test_twz_to_json_with_custom_time_precision
478
+ with_standard_json_time_format(true) do
479
+ ActiveSupport::JSON::Encoding.time_precision = 0
480
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
481
+ time = ActiveSupport::TimeWithZone.new(Time.utc(2000), zone)
482
+ assert_equal "\"1999-12-31T19:00:00-05:00\"", ActiveSupport::JSON.encode(time)
483
+ end
484
+ ensure
485
+ ActiveSupport::JSON::Encoding.time_precision = 3
486
+ end
487
+
488
+ def test_time_to_json_with_custom_time_precision
489
+ with_standard_json_time_format(true) do
490
+ ActiveSupport::JSON::Encoding.time_precision = 0
491
+ assert_equal "\"2000-01-01T00:00:00Z\"", ActiveSupport::JSON.encode(Time.utc(2000))
492
+ end
493
+ ensure
494
+ ActiveSupport::JSON::Encoding.time_precision = 3
495
+ end
496
+
497
+ def test_datetime_to_json_with_custom_time_precision
498
+ with_standard_json_time_format(true) do
499
+ ActiveSupport::JSON::Encoding.time_precision = 0
500
+ assert_equal "\"2000-01-01T00:00:00+00:00\"", ActiveSupport::JSON.encode(DateTime.new(2000))
501
+ end
502
+ ensure
503
+ ActiveSupport::JSON::Encoding.time_precision = 3
504
+ end
505
+
506
+ def test_twz_to_json_when_wrapping_a_date_time
507
+ zone = ActiveSupport::TimeZone['Eastern Time (US & Canada)']
508
+ time = ActiveSupport::TimeWithZone.new(DateTime.new(2000), zone)
509
+ assert_equal '"1999-12-31T19:00:00.000-05:00"', ActiveSupport::JSON.encode(time)
510
+ end
511
+
512
+ protected
513
+
514
+ def object_keys(json_object)
515
+ json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
516
+ end
517
+
518
+ def with_env_tz(new_tz = 'US/Eastern')
519
+ old_tz, ENV['TZ'] = ENV['TZ'], new_tz
520
+ yield
521
+ ensure
522
+ old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
523
+ end
524
+
525
+ def with_standard_json_time_format(boolean = true)
526
+ old, ActiveSupport.use_standard_json_time_format = ActiveSupport.use_standard_json_time_format, boolean
527
+ yield
528
+ ensure
529
+ ActiveSupport.use_standard_json_time_format = old
530
+ end
531
+ end