activesupport-json_encoder 1.0.0.beta

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 3101736985bb0414aa4b7023b0d4bc71fd6b5b68
4
+ data.tar.gz: 76b13afa567d7cc872961d52d30d216c256ab564
5
+ SHA512:
6
+ metadata.gz: 5077b6cb57f13ea824baebbb8b5933a2bd0deb634467db12b3884dd7f47a8132cd3fcb31b8b8d2065b9716ef716f6ab57a9211614691ce3aca12dd1a0fd60e3d
7
+ data.tar.gz: 3c9132aaaf46abadc0ff39e9f19037b6bece9081d3eabdc2dcd79b2919fe172b540119473713aa160293622f237628d11efd38a100295786c36328b62059c4a4
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2013 Vancouver Ruby Meetup Group
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,118 @@
1
+ Active Support JSON Encoder
2
+ ===========================
3
+
4
+ A pure-Ruby ActiveSupport JSON encoder. This was the default encoder used
5
+ by ActiveSupport prior to Rails 4.1. The main advantage of using this
6
+ encoder over the new default is for the `#encode_json` support (see below).
7
+
8
+ Installation
9
+ ------------
10
+
11
+ Simply include this gem in your Gemfile:
12
+
13
+ gem 'activesupport-json_encoder', github: 'activesupport-json_encoder'
14
+
15
+ Configuration
16
+ -------------
17
+
18
+ By default, ActiveSupport encodes `BigDecimal` objects as a string:
19
+
20
+ ```ruby
21
+ >> { big_number: BigDecimal.new('12345678901234567890') }.to_json
22
+ => "{\"big_number\":\"12345678901234567890.0\"}"
23
+ ```
24
+
25
+ To change this, you can set `ActiveSupport.encode_big_decimal_as_string` to
26
+ `false`:
27
+
28
+ ```ruby
29
+ >> ActiveSupport.encode_big_decimal_as_string = false
30
+ >> { big_number: BigDecimal.new('12345678901234567890') }.to_json
31
+ => "{\"big_number\":12345678901234567890.0}"
32
+ ```
33
+
34
+ Beware that you may lose precision on the consuming-end if you do this:
35
+
36
+ ```javascript
37
+ > // Parsing this in JavaScript in the browser
38
+ > JSON.parse("{\"big_number\":12345678901234567890.0}").big_number
39
+ 12345678901234567000
40
+ ```
41
+
42
+ JSON Serialization for Custom Objects
43
+ -------------------------------------
44
+
45
+ By default, when the encoder encounters a Ruby object that it does not
46
+ recognize, it will serilizes its instance variables:
47
+
48
+ ```ruby
49
+ >> class MyClass
50
+ >> def initialize
51
+ >> @foo = :bar
52
+ >> end
53
+ >> end
54
+ => nil
55
+ >> MyClass.new.to_json
56
+ => "{\"foo\":\"bar\"}"
57
+ ```
58
+
59
+ There are two ways to customize this behavior on a per-class basis. Typically,
60
+ you should override `#as_json` to return a Ruby-representation of your object.
61
+ Any options passed to `#to_json` will be made available to this method:
62
+
63
+ ```ruby
64
+ >> class MyClass
65
+ >> def as_json(options = {})
66
+ >> options[:as_array] ? [:foo, :bar] : {foo: :bar}
67
+ >> end
68
+ >> end
69
+ => nil
70
+ >> MyClass.new.to_json
71
+ => "{\"foo\":\"bar\"}"
72
+ >> MyClass.new.to_json(as_array: true)
73
+ => "[\"foo\",\"bar\"]"
74
+ ```
75
+
76
+ This method is supported by all encoders.
77
+
78
+ However, sometimes this might not give you enough control. For example, you
79
+ might want to encode numeric values in a certain format. In this case, you can
80
+ override the `#encoder_json` method. This method has access to the `Encoder`
81
+ object and is expected to return a `String` that would be injected to the JSON
82
+ output directly:
83
+
84
+ ```ruby
85
+ >> class Money
86
+ >> def initialize(dollars, cents)
87
+ >> @dollars = dollars
88
+ >> @cents = cents
89
+ >> end
90
+ >>
91
+ >> def as_json(options = nil)
92
+ >> # Opt-out from the default Object#as_json
93
+ >> self
94
+ >> end
95
+ >>
96
+ >> def encode_json(encoder)
97
+ >> if @cents.to_i < 10
98
+ >> "#{@dollars.to_i}.0#{@cents.to_i}"
99
+ >> else
100
+ >> "#{@dollars.to_i}.#{@cents.to_i}"
101
+ >> end
102
+ >> end
103
+ >> end
104
+ => nil
105
+ >> { price: Money.new(0,10) }.to_json
106
+ => "{\"price\":0.10}"
107
+ ```
108
+
109
+ Beware that this function is specific to this gem and is not supported by
110
+ other encoders. You should also be extra careful to return valid JSON because
111
+ the return value of this method will be injected into the output with no
112
+ sanitization whatsoever. Use this method with extreme caution, especially
113
+ when dealing with user input.
114
+
115
+ Dependencies
116
+ ------------
117
+
118
+ * `activesupport` >= 4.1.0.beta
@@ -0,0 +1,211 @@
1
+ require 'active_support'
2
+ require 'active_support/json'
3
+ require 'active_support/core_ext/module/delegation'
4
+ require 'set'
5
+ require 'bigdecimal'
6
+
7
+ module ActiveSupport
8
+ module JSON
9
+ module Encoding
10
+ class CircularReferenceError < StandardError; end
11
+
12
+ class ActiveSupportEncoder
13
+ attr_reader :options
14
+
15
+ def initialize(options = nil)
16
+ @options = options || {}
17
+ @seen = Set.new
18
+ end
19
+
20
+ def encode(value, use_options = true)
21
+ check_for_circular_references(value) do
22
+ jsonified = use_options ? value.as_json(options_for(value)) : value.as_json
23
+ jsonified.respond_to?(:encode_json) ? jsonified.encode_json(self) : encode(jsonified, false)
24
+ end
25
+ end
26
+
27
+ # like encode, but only calls as_json, without encoding to string.
28
+ def as_json(value, use_options = true)
29
+ check_for_circular_references(value) do
30
+ use_options ? value.as_json(options_for(value)) : value.as_json
31
+ end
32
+ end
33
+
34
+ def options_for(value)
35
+ if value.is_a?(Array) || value.is_a?(Hash)
36
+ # hashes and arrays need to get encoder in the options, so that
37
+ # they can detect circular references.
38
+ options.merge(encoder: self)
39
+ else
40
+ options.dup
41
+ end
42
+ end
43
+
44
+ def escape(string)
45
+ self.class.escape(string)
46
+ end
47
+
48
+ class << self
49
+ ESCAPED_CHARS = {
50
+ "\u0000" => '\u0000', "\u0001" => '\u0001',
51
+ "\u0002" => '\u0002', "\u0003" => '\u0003',
52
+ "\u0004" => '\u0004', "\u0005" => '\u0005',
53
+ "\u0006" => '\u0006', "\u0007" => '\u0007',
54
+ "\u0008" => '\b', "\u0009" => '\t',
55
+ "\u000A" => '\n', "\u000B" => '\u000B',
56
+ "\u000C" => '\f', "\u000D" => '\r',
57
+ "\u000E" => '\u000E', "\u000F" => '\u000F',
58
+ "\u0010" => '\u0010', "\u0011" => '\u0011',
59
+ "\u0012" => '\u0012', "\u0013" => '\u0013',
60
+ "\u0014" => '\u0014', "\u0015" => '\u0015',
61
+ "\u0016" => '\u0016', "\u0017" => '\u0017',
62
+ "\u0018" => '\u0018', "\u0019" => '\u0019',
63
+ "\u001A" => '\u001A', "\u001B" => '\u001B',
64
+ "\u001C" => '\u001C', "\u001D" => '\u001D',
65
+ "\u001E" => '\u001E', "\u001F" => '\u001F',
66
+ "\u2028" => '\u2028', "\u2029" => '\u2029',
67
+ '"' => '\"',
68
+ '\\' => '\\\\',
69
+ '>' => '\u003E',
70
+ '<' => '\u003C',
71
+ '&' => '\u0026'}
72
+
73
+ ESCAPE_REGEX_WITH_HTML = /[\u0000-\u001F\u2028\u2029"\\><&]/u
74
+ ESCAPE_REGEX_WITHOUT_HTML = /[\u0000-\u001F\u2028\u2029"\\]/u
75
+
76
+ def escape(string)
77
+ string = string.encode ::Encoding::UTF_8, undef: :replace
78
+ regex = Encoding.escape_html_entities_in_json ? ESCAPE_REGEX_WITH_HTML : ESCAPE_REGEX_WITHOUT_HTML
79
+ %("#{string.gsub regex, ESCAPED_CHARS}")
80
+ end
81
+ end
82
+
83
+ private
84
+ def check_for_circular_references(value)
85
+ unless @seen.add?(value.__id__)
86
+ raise CircularReferenceError, 'object references itself'
87
+ end
88
+ yield
89
+ ensure
90
+ @seen.delete(value.__id__)
91
+ end
92
+ end
93
+
94
+ class << self
95
+ remove_method :encode_big_decimal_as_string, :encode_big_decimal_as_string=
96
+
97
+ # If false, serializes BigDecimal objects as numeric instead of wrapping
98
+ # them in a string.
99
+ attr_accessor :encode_big_decimal_as_string
100
+ end
101
+
102
+ self.encode_big_decimal_as_string = true
103
+ end
104
+ end
105
+ end
106
+
107
+ class TrueClass
108
+ def encode_json(encoder) #:nodoc:
109
+ to_s
110
+ end
111
+ end
112
+
113
+ class FalseClass
114
+ def encode_json(encoder) #:nodoc:
115
+ to_s
116
+ end
117
+ end
118
+
119
+ class NilClass
120
+ def encode_json(encoder) #:nodoc:
121
+ 'null'
122
+ end
123
+ end
124
+
125
+ class String
126
+ def encode_json(encoder) #:nodoc:
127
+ ActiveSupport::JSON::Encoding::ActiveSupportEncoder.escape(self)
128
+ end
129
+ end
130
+
131
+ class Numeric
132
+ def encode_json(encoder) #:nodoc:
133
+ to_s
134
+ end
135
+ end
136
+
137
+ class BigDecimal
138
+ # A BigDecimal would be naturally represented as a JSON number. Most libraries,
139
+ # however, parse non-integer JSON numbers directly as floats. Clients using
140
+ # those libraries would get in general a wrong number and no way to recover
141
+ # other than manually inspecting the string with the JSON code itself.
142
+ #
143
+ # That's why a JSON string is returned. The JSON literal is not numeric, but
144
+ # if the other end knows by contract that the data is supposed to be a
145
+ # BigDecimal, it still has the chance to post-process the string and get the
146
+ # real value.
147
+ #
148
+ # Use <tt>ActiveSupport.encode_big_decimal_as_string = true</tt> to
149
+ # override this behavior.
150
+
151
+ remove_method :as_json
152
+
153
+ def as_json(options = nil) #:nodoc:
154
+ if finite?
155
+ ActiveSupport.encode_big_decimal_as_string ? to_s : self
156
+ else
157
+ nil
158
+ end
159
+ end
160
+
161
+ def encode_json(encoder) #:nodoc:
162
+ to_s
163
+ end
164
+ end
165
+
166
+ class Array
167
+ remove_method :as_json
168
+
169
+ def as_json(options = nil) #:nodoc:
170
+ # use encoder as a proxy to call as_json on all elements, to protect from circular references
171
+ encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::ActiveSupportEncoder.new(options)
172
+ map { |v| encoder.as_json(v, options) }
173
+ end
174
+
175
+ def encode_json(encoder) #:nodoc:
176
+ "[#{map { |v| encoder.encode(v, false) } * ','}]"
177
+ end
178
+ end
179
+
180
+ class Hash
181
+ remove_method :as_json
182
+
183
+ def as_json(options = nil) #:nodoc:
184
+ # create a subset of the hash by applying :only or :except
185
+ subset = if options
186
+ if attrs = options[:only]
187
+ slice(*Array(attrs))
188
+ elsif attrs = options[:except]
189
+ except(*Array(attrs))
190
+ else
191
+ self
192
+ end
193
+ else
194
+ self
195
+ end
196
+
197
+ # use encoder as a proxy to call as_json on all values in the subset, to protect from circular references
198
+ encoder = options && options[:encoder] || ActiveSupport::JSON::Encoding::ActiveSupportEncoder.new(options)
199
+ Hash[subset.map { |k, v| [k.to_s, encoder.as_json(v, options)] }]
200
+ end
201
+
202
+ def encode_json(encoder) #:nodoc:
203
+ # values are encoded with use_options = false, because we don't want hash representations from ActiveModel to be
204
+ # processed once again with as_json with options, as this could cause unexpected results (i.e. missing fields);
205
+
206
+ # on the other hand, we need to run as_json on the elements, because the model representation may contain fields
207
+ # like Time/Date in their original (not jsonified) form, etc.
208
+
209
+ "{#{map { |k,v| "#{encoder.encode(k.to_s)}:#{encoder.encode(v, false)}" } * ','}}"
210
+ end
211
+ end
@@ -0,0 +1,3 @@
1
+ require 'active_support/json/encoding/active_support_encoder'
2
+
3
+ ActiveSupport.json_encoder = ActiveSupport::JSON::Encoding::ActiveSupportEncoder
@@ -0,0 +1,476 @@
1
+ # encoding: utf-8
2
+ require 'test_helper'
3
+ require 'securerandom'
4
+ require 'active_support/core_ext/string/inflections'
5
+ require 'activesupport/json_encoder'
6
+
7
+ class TestJSONEncoding < ActiveSupport::TestCase
8
+ class Foo
9
+ def initialize(a, b)
10
+ @a, @b = a, b
11
+ end
12
+ end
13
+
14
+ class Hashlike
15
+ def to_hash
16
+ { :foo => "hello", :bar => "world" }
17
+ end
18
+ end
19
+
20
+ class Custom
21
+ def initialize(serialized)
22
+ @serialized = serialized
23
+ end
24
+
25
+ def as_json(options = nil)
26
+ @serialized
27
+ end
28
+ end
29
+
30
+ class CustomWithOptions
31
+ attr_accessor :foo, :bar
32
+
33
+ def as_json(options={})
34
+ options[:only] = %w(foo bar)
35
+ super(options)
36
+ end
37
+ end
38
+
39
+ class OptionsTest
40
+ def as_json(options = :default)
41
+ options
42
+ end
43
+ end
44
+
45
+ class EncodeJsonTest
46
+ def as_json(*)
47
+ self
48
+ end
49
+
50
+ def encode_json(encoder)
51
+ 'encode_json'
52
+ end
53
+ end
54
+
55
+ class HashWithAsJson < Hash
56
+ attr_accessor :as_json_called
57
+
58
+ def initialize(*)
59
+ super
60
+ end
61
+
62
+ def as_json(options={})
63
+ @as_json_called = true
64
+ super
65
+ end
66
+ end
67
+
68
+ TrueTests = [[ true, %(true) ]]
69
+ FalseTests = [[ false, %(false) ]]
70
+ NilTests = [[ nil, %(null) ]]
71
+ NumericTests = [[ 1, %(1) ],
72
+ [ 2.5, %(2.5) ],
73
+ [ 0.0/0.0, %(null) ],
74
+ [ 1.0/0.0, %(null) ],
75
+ [ -1.0/0.0, %(null) ],
76
+ [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ],
77
+ [ BigDecimal('2.5'), %("#{BigDecimal('2.5').to_s}") ]]
78
+
79
+ StringTests = [[ 'this is the <string>', %("this is the \\u003Cstring\\u003E")],
80
+ [ 'a "string" with quotes & an ampersand', %("a \\"string\\" with quotes \\u0026 an ampersand") ],
81
+ [ 'http://test.host/posts/1', %("http://test.host/posts/1")],
82
+ [ "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",
83
+ %("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") ]]
84
+
85
+ ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ],
86
+ [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]]
87
+
88
+ RangeTests = [[ 1..2, %("1..2")],
89
+ [ 1...2, %("1...2")],
90
+ [ 1.5..2.5, %("1.5..2.5")]]
91
+
92
+ SymbolTests = [[ :a, %("a") ],
93
+ [ :this, %("this") ],
94
+ [ :"a b", %("a b") ]]
95
+
96
+ ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]]
97
+ HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]]
98
+ CustomTests = [[ Custom.new("custom"), '"custom"' ],
99
+ [ Custom.new(nil), 'null' ],
100
+ [ Custom.new(:a), '"a"' ],
101
+ [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ],
102
+ [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ],
103
+ [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ],
104
+ [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]]
105
+
106
+ EncodeJsonTests = [[ EncodeJsonTest.new, 'encode_json' ],
107
+ [ [EncodeJsonTest.new], '[encode_json]' ],
108
+ [ { json: EncodeJsonTest.new }, '{"json":encode_json}' ],
109
+ [ Custom.new(EncodeJsonTest.new), 'encode_json' ]]
110
+
111
+ RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']]
112
+
113
+ DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]]
114
+ TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
115
+ DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]]
116
+
117
+ StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]]
118
+ StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]]
119
+ StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]]
120
+ StandardStringTests = [[ 'this is the <string>', %("this is the <string>")]]
121
+
122
+ def sorted_json(json)
123
+ return json unless json =~ /^\{.*\}$/
124
+ '{' + json[1..-2].split(',').sort.join(',') + '}'
125
+ end
126
+
127
+ constants.grep(/Tests$/).each do |class_tests|
128
+ define_method("test_#{class_tests[0..-6].underscore}") do
129
+ begin
130
+ prev = ActiveSupport.use_standard_json_time_format
131
+
132
+ ActiveSupport.escape_html_entities_in_json = class_tests !~ /^Standard/
133
+ ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/
134
+ self.class.const_get(class_tests).each do |pair|
135
+ assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first))
136
+ end
137
+ ensure
138
+ ActiveSupport.escape_html_entities_in_json = false
139
+ ActiveSupport.use_standard_json_time_format = prev
140
+ end
141
+ end
142
+ end
143
+
144
+ def test_process_status
145
+ # There doesn't seem to be a good way to get a handle on a Process::Status object without actually
146
+ # creating a child process, hence this to populate $?
147
+ system("not_a_real_program_#{SecureRandom.hex}")
148
+ assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?)
149
+ end
150
+
151
+ def test_hash_encoding
152
+ assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b)
153
+ assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1)
154
+ assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode('a' => [1,2])
155
+ assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2)
156
+
157
+ assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d))
158
+ end
159
+
160
+ def test_utf8_string_encoded_properly
161
+ result = ActiveSupport::JSON.encode('€2.99')
162
+ assert_equal '"€2.99"', result
163
+ assert_equal(Encoding::UTF_8, result.encoding)
164
+
165
+ result = ActiveSupport::JSON.encode('✎☺')
166
+ assert_equal '"✎☺"', result
167
+ assert_equal(Encoding::UTF_8, result.encoding)
168
+ end
169
+
170
+ def test_non_utf8_string_transcodes
171
+ s = '二'.encode('Shift_JIS')
172
+ result = ActiveSupport::JSON.encode(s)
173
+ assert_equal '"二"', result
174
+ assert_equal Encoding::UTF_8, result.encoding
175
+ end
176
+
177
+ def test_wide_utf8_chars
178
+ w = '𠜎'
179
+ result = ActiveSupport::JSON.encode(w)
180
+ assert_equal '"𠜎"', result
181
+ end
182
+
183
+ def test_wide_utf8_roundtrip
184
+ hash = { string: "𐒑" }
185
+ json = ActiveSupport::JSON.encode(hash)
186
+ decoded_hash = ActiveSupport::JSON.decode(json)
187
+ assert_equal "𐒑", decoded_hash['string']
188
+ end
189
+
190
+ def test_exception_raised_when_encoding_circular_reference_in_array
191
+ a = [1]
192
+ a << a
193
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
194
+ end
195
+
196
+ def test_exception_raised_when_encoding_circular_reference_in_hash
197
+ a = { :name => 'foo' }
198
+ a[:next] = a
199
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
200
+ end
201
+
202
+ def test_exception_raised_when_encoding_circular_reference_in_hash_inside_array
203
+ a = { :name => 'foo', :sub => [] }
204
+ a[:sub] << a
205
+ assert_raise(ActiveSupport::JSON::Encoding::CircularReferenceError) { ActiveSupport::JSON.encode(a) }
206
+ end
207
+
208
+ def test_hash_key_identifiers_are_always_quoted
209
+ values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"}
210
+ assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values))
211
+ end
212
+
213
+ def test_hash_should_allow_key_filtering_with_only
214
+ assert_equal %({"a":1}), ActiveSupport::JSON.encode({'a' => 1, :b => 2, :c => 3}, :only => 'a')
215
+ end
216
+
217
+ def test_hash_should_allow_key_filtering_with_except
218
+ assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c])
219
+ end
220
+
221
+ def test_time_to_json_includes_local_offset
222
+ prev = ActiveSupport.use_standard_json_time_format
223
+ ActiveSupport.use_standard_json_time_format = true
224
+ with_env_tz 'US/Eastern' do
225
+ assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10))
226
+ end
227
+ ensure
228
+ ActiveSupport.use_standard_json_time_format = prev
229
+ end
230
+
231
+ def test_hash_with_time_to_json
232
+ prev = ActiveSupport.use_standard_json_time_format
233
+ ActiveSupport.use_standard_json_time_format = false
234
+ assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json
235
+ ensure
236
+ ActiveSupport.use_standard_json_time_format = prev
237
+ end
238
+
239
+ def test_nested_hash_with_float
240
+ assert_nothing_raised do
241
+ hash = {
242
+ "CHI" => {
243
+ :display_name => "chicago",
244
+ :latitude => 123.234
245
+ }
246
+ }
247
+ ActiveSupport::JSON.encode(hash)
248
+ end
249
+ end
250
+
251
+ def test_hash_like_with_options
252
+ h = Hashlike.new
253
+ json = h.to_json :only => [:foo]
254
+
255
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
256
+ end
257
+
258
+ def test_object_to_json_with_options
259
+ obj = Object.new
260
+ obj.instance_variable_set :@foo, "hello"
261
+ obj.instance_variable_set :@bar, "world"
262
+ json = obj.to_json :only => ["foo"]
263
+
264
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
265
+ end
266
+
267
+ def test_struct_to_json_with_options
268
+ struct = Struct.new(:foo, :bar).new
269
+ struct.foo = "hello"
270
+ struct.bar = "world"
271
+ json = struct.to_json :only => [:foo]
272
+
273
+ assert_equal({"foo"=>"hello"}, JSON.parse(json))
274
+ end
275
+
276
+ def test_hash_should_pass_encoding_options_to_children_in_as_json
277
+ person = {
278
+ :name => 'John',
279
+ :address => {
280
+ :city => 'London',
281
+ :country => 'UK'
282
+ }
283
+ }
284
+ json = person.as_json :only => [:address, :city]
285
+
286
+ assert_equal({ 'address' => { 'city' => 'London' }}, json)
287
+ end
288
+
289
+ def test_hash_should_pass_encoding_options_to_children_in_to_json
290
+ person = {
291
+ :name => 'John',
292
+ :address => {
293
+ :city => 'London',
294
+ :country => 'UK'
295
+ }
296
+ }
297
+ json = person.to_json :only => [:address, :city]
298
+
299
+ assert_equal(%({"address":{"city":"London"}}), json)
300
+ end
301
+
302
+ def test_array_should_pass_encoding_options_to_children_in_as_json
303
+ people = [
304
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
305
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
306
+ ]
307
+ json = people.as_json :only => [:address, :city]
308
+ expected = [
309
+ { 'address' => { 'city' => 'London' }},
310
+ { 'address' => { 'city' => 'Paris' }}
311
+ ]
312
+
313
+ assert_equal(expected, json)
314
+ end
315
+
316
+ def test_array_should_pass_encoding_options_to_children_in_to_json
317
+ people = [
318
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
319
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
320
+ ]
321
+ json = people.to_json :only => [:address, :city]
322
+
323
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
324
+ end
325
+
326
+ def test_enumerable_should_pass_encoding_options_to_children_in_as_json
327
+ people = [
328
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
329
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
330
+ ]
331
+ json = people.each.as_json :only => [:address, :city]
332
+ expected = [
333
+ { 'address' => { 'city' => 'London' }},
334
+ { 'address' => { 'city' => 'Paris' }}
335
+ ]
336
+
337
+ assert_equal(expected, json)
338
+ end
339
+
340
+ def test_enumerable_should_pass_encoding_options_to_children_in_to_json
341
+ people = [
342
+ { :name => 'John', :address => { :city => 'London', :country => 'UK' }},
343
+ { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }}
344
+ ]
345
+ json = people.each.to_json :only => [:address, :city]
346
+
347
+ assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json)
348
+ end
349
+
350
+ def test_hash_to_json_should_not_keep_options_around
351
+ f = CustomWithOptions.new
352
+ f.foo = "hello"
353
+ f.bar = "world"
354
+
355
+ hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}}
356
+ assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"},
357
+ "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json))
358
+ end
359
+
360
+ def test_array_to_json_should_not_keep_options_around
361
+ f = CustomWithOptions.new
362
+ f.foo = "hello"
363
+ f.bar = "world"
364
+
365
+ array = [f, {"foo" => "other_foo", "test" => "other_test"}]
366
+ assert_equal([{"foo"=>"hello","bar"=>"world"},
367
+ {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json))
368
+ end
369
+
370
+ def test_hash_as_json_without_options
371
+ json = { foo: OptionsTest.new }.as_json
372
+ assert_equal({"foo" => :default}, json)
373
+ end
374
+
375
+ def test_array_as_json_without_options
376
+ json = [ OptionsTest.new ].as_json
377
+ assert_equal([:default], json)
378
+ end
379
+
380
+ def test_struct_encoding
381
+ Struct.new('UserNameAndEmail', :name, :email)
382
+ Struct.new('UserNameAndDate', :name, :date)
383
+ Struct.new('Custom', :name, :sub)
384
+ user_email = Struct::UserNameAndEmail.new 'David', 'sample@example.com'
385
+ user_birthday = Struct::UserNameAndDate.new 'David', Date.new(2010, 01, 01)
386
+ custom = Struct::Custom.new 'David', user_birthday
387
+
388
+
389
+ json_strings = ""
390
+ json_string_and_date = ""
391
+ json_custom = ""
392
+
393
+ assert_nothing_raised do
394
+ json_strings = user_email.to_json
395
+ json_string_and_date = user_birthday.to_json
396
+ json_custom = custom.to_json
397
+ end
398
+
399
+ assert_equal({"name" => "David",
400
+ "sub" => {
401
+ "name" => "David",
402
+ "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom))
403
+
404
+ assert_equal({"name" => "David", "email" => "sample@example.com"},
405
+ ActiveSupport::JSON.decode(json_strings))
406
+
407
+ assert_equal({"name" => "David", "date" => "2010-01-01"},
408
+ ActiveSupport::JSON.decode(json_string_and_date))
409
+ end
410
+
411
+ def test_opt_out_big_decimal_string_serialization
412
+ big_decimal = BigDecimal('2.5')
413
+
414
+ begin
415
+ ActiveSupport.encode_big_decimal_as_string = false
416
+ assert_equal big_decimal.to_s, big_decimal.to_json
417
+ ensure
418
+ ActiveSupport.encode_big_decimal_as_string = true
419
+ end
420
+ end
421
+
422
+ def test_reading_encode_big_decimal_as_string_option
423
+ assert_not_deprecated { assert ActiveSupport.encode_big_decimal_as_string }
424
+ end
425
+
426
+ def test_nil_true_and_false_represented_as_themselves
427
+ assert_equal nil, nil.as_json
428
+ assert_equal true, true.as_json
429
+ assert_equal false, false.as_json
430
+ end
431
+
432
+ def test_json_gem_dump_by_passing_active_support_encoder
433
+ h = HashWithAsJson.new
434
+ h[:foo] = "hello"
435
+ h[:bar] = "world"
436
+
437
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h)
438
+ assert_nil h.as_json_called
439
+ end
440
+
441
+ def test_json_gem_generate_by_passing_active_support_encoder
442
+ h = HashWithAsJson.new
443
+ h[:foo] = "hello"
444
+ h[:bar] = "world"
445
+
446
+ assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h)
447
+ assert_nil h.as_json_called
448
+ end
449
+
450
+ def test_json_gem_pretty_generate_by_passing_active_support_encoder
451
+ h = HashWithAsJson.new
452
+ h[:foo] = "hello"
453
+ h[:bar] = "world"
454
+
455
+ assert_equal <<EXPECTED.chomp, JSON.pretty_generate(h)
456
+ {
457
+ "foo": "hello",
458
+ "bar": "world"
459
+ }
460
+ EXPECTED
461
+ assert_nil h.as_json_called
462
+ end
463
+
464
+ protected
465
+
466
+ def object_keys(json_object)
467
+ json_object[1..-2].scan(/([^{}:,\s]+):/).flatten.sort
468
+ end
469
+
470
+ def with_env_tz(new_tz = 'US/Eastern')
471
+ old_tz, ENV['TZ'] = ENV['TZ'], new_tz
472
+ yield
473
+ ensure
474
+ old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ')
475
+ end
476
+ end
@@ -0,0 +1,17 @@
1
+ require 'active_support/core_ext/kernel/reporting'
2
+
3
+ silence_warnings do
4
+ Encoding.default_internal = "UTF-8"
5
+ Encoding.default_external = "UTF-8"
6
+ end
7
+
8
+ require 'bundler/setup'
9
+ require 'minitest/autorun'
10
+ require 'active_support'
11
+ require 'active_support/test_case'
12
+ require 'active_support/testing/autorun'
13
+
14
+ Thread.abort_on_exception = true
15
+
16
+ # Show backtraces for deprecated behavior for quicker cleanup.
17
+ ActiveSupport::Deprecation.debug = true
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: activesupport-json_encoder
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0.beta
5
+ platform: ruby
6
+ authors:
7
+ - David Heinemeier Hansson
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2013-12-03 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - '>='
18
+ - !ruby/object:Gem::Version
19
+ version: 4.1.0.beta
20
+ - - <
21
+ - !ruby/object:Gem::Version
22
+ version: '5.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - '>='
28
+ - !ruby/object:Gem::Version
29
+ version: 4.1.0.beta
30
+ - - <
31
+ - !ruby/object:Gem::Version
32
+ version: '5.0'
33
+ description: A pure-Ruby ActiveSupport JSON encoder
34
+ email:
35
+ - david@loudthinking.com
36
+ executables: []
37
+ extensions: []
38
+ extra_rdoc_files: []
39
+ files:
40
+ - MIT-LICENSE
41
+ - README.md
42
+ - lib/active_support/json/encoding/active_support_encoder.rb
43
+ - lib/activesupport/json_encoder.rb
44
+ - test/active_support_encoder_test.rb
45
+ - test/test_helper.rb
46
+ homepage: https://github.com/vanruby/activesupport-json_encoder
47
+ licenses:
48
+ - MIT
49
+ metadata: {}
50
+ post_install_message:
51
+ rdoc_options: []
52
+ require_paths:
53
+ - lib
54
+ required_ruby_version: !ruby/object:Gem::Requirement
55
+ requirements:
56
+ - - '>='
57
+ - !ruby/object:Gem::Version
58
+ version: 1.9.3
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - '>'
62
+ - !ruby/object:Gem::Version
63
+ version: 1.3.1
64
+ requirements: []
65
+ rubyforge_project:
66
+ rubygems_version: 2.0.3
67
+ signing_key:
68
+ specification_version: 4
69
+ summary: A pure-Ruby ActiveSupport JSON encoder (extracted from core in Rails 4.1)
70
+ test_files:
71
+ - test/active_support_encoder_test.rb
72
+ - test/test_helper.rb