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.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +118 -0
- data/lib/active_support/json/encoding/active_support_encoder.rb +211 -0
- data/lib/activesupport/json_encoder.rb +3 -0
- data/test/active_support_encoder_test.rb +476 -0
- data/test/test_helper.rb +17 -0
- metadata +72 -0
checksums.yaml
ADDED
@@ -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
|
data/MIT-LICENSE
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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,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
|
data/test/test_helper.rb
ADDED
@@ -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
|