├── .gitignore ├── .rspec ├── .travis.yml ├── Gemfile ├── MIT-LICENSE ├── README.md ├── Rakefile ├── bin ├── console └── setup ├── lib ├── activesupport │ └── json_encoder.rb └── yagni_json_encoder.rb ├── test ├── decoding_test.rb ├── encoding_test.rb ├── encoding_test_cases.rb ├── test_helper.rb └── time_zone_test_helpers.rb └── yagni_json_encoder.gemspec /.gitignore: -------------------------------------------------------------------------------- 1 | /.bundle/ 2 | /.yardoc 3 | /Gemfile.lock 4 | /_yardoc/ 5 | /coverage/ 6 | /doc/ 7 | /pkg/ 8 | /spec/reports/ 9 | /tmp/ 10 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --format documentation 2 | --color 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | rvm: 3 | - 2.2.4 4 | - 2.3.4 5 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | # Specify your gem's dependencies in yagni_json_encoder.gemspec 4 | gemspec 5 | -------------------------------------------------------------------------------- /MIT-LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016 Ian Clay Ker-Seymer 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining 4 | a copy of this software and associated documentation files (the 5 | "Software"), to deal in the Software without restriction, including 6 | without limitation the rights to use, copy, modify, merge, publish, 7 | distribute, sublicense, and/or sell copies of the Software, and to 8 | permit persons to whom the Software is furnished to do so, subject to 9 | the following conditions: 10 | 11 | The above copyright notice and this permission notice shall be 12 | included in all copies or substantial portions of the Software. 13 | 14 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 15 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 16 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 17 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 18 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 19 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 20 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 21 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # YagniJsonEncoder 2 | 3 | ## DEPRECATED: use OJ directly 4 | 5 | Oj now supports the ability to optimize Rails without any modifications. Read more about it [here](https://github.com/ohler55/oj/blob/master/pages/Rails.md) 6 | 7 | [![Build Status](https://travis-ci.org/ianks/yagni_json_encoder.svg?branch=master)](https://travis-ci.org/ianks/yagni_json_encoder) 8 | 9 | This gem overrides the [default ActiveSupport JSON 10 | encoder](https://github.com/rails/rails/blob/master/activesupport/lib/active_support/json/encoding.rb) 11 | with a faster encoder which makes a few assumptions about your app. 12 | 13 | 1. You do not need the escape HTML entities in your JSON 14 | 2. You do not need any special escaping besides that provided by the JSON 15 | standard. 16 | 17 | Under the hood, this app removes Rails' special JSON escaping, and relies on 18 | [Oj](https://github.com/ohler55/oj) to do the heavy lifting of JSON encoding. 19 | 20 | 21 | ## Why? 22 | 23 | By default, Rails plays it safe and escapes `\u2028` and `\u2029`. In order to 24 | do this, a `#gsub` call (O(n) time complexity) is required on every string it 25 | encounters. I do not interface with old browsers, so escaping these characters 26 | is useless to me. Instead, we dont escape these characters, and let Oj do all 27 | the heavy JSON encoding work. 28 | 29 | 30 | ## Perfomance (show me the money!) 31 | 32 | In a real Rails app, I benchmarked `Tips.all.to_json` with the different 33 | encoders. On average, YagniJsonEncoder is ~2x as fast. However, you could 34 | see bigger gains if your models are text/string-heavy. 35 | 36 | ``` 37 | Calculating ------------------------------------- 38 | YagniJsonEncoder 10.000 i/100ms 39 | JSONGemEncoder 5.000 i/100ms 40 | ------------------------------------------------- 41 | YagniJsonEncoder 105.536 (± 6.6%) i/s - 530.000 42 | JSONGemEncoder 50.605 (± 4.0%) i/s - 255.000 43 | 44 | Comparison: 45 | YagniJsonEncoder: 105.5 i/s 46 | JSONGemEncoder: 50.6 i/s - 2.09x slower 47 | ``` 48 | 49 | 50 | ## Installation 51 | 52 | Just add this line to your application's Gemfile: 53 | 54 | ```ruby 55 | gem 'yagni_json_encoder' 56 | ``` 57 | 58 | And then execute: 59 | 60 | $ bundle 61 | 62 | Or install it yourself as: 63 | 64 | $ gem install yagni_json_encoder 65 | 66 | 67 | ## Contributing 68 | 69 | Bug reports and pull requests are welcome on GitHub at 70 | https://github.com/ianks/yagni_json_encoder. 71 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | require 'bundler/gem_tasks' 2 | require 'rake/testtask' 3 | 4 | Rake::TestTask.new do |t| 5 | t.libs = ['test'] 6 | t.pattern = 'test/**/*_test.rb' 7 | t.ruby_opts = ['-w'] 8 | end 9 | 10 | task default: :test 11 | -------------------------------------------------------------------------------- /bin/console: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env ruby 2 | 3 | require "bundler/setup" 4 | require "yagni_json_encoder" 5 | 6 | # You can add fixtures and/or initialization code here to make experimenting 7 | # with your gem easier. You can also use a different console, if you like. 8 | 9 | # (If you use this, don't forget to add pry to your Gemfile!) 10 | # require "pry" 11 | # Pry.start 12 | 13 | require "irb" 14 | IRB.start 15 | -------------------------------------------------------------------------------- /bin/setup: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | set -euo pipefail 3 | IFS=$'\n\t' 4 | set -vx 5 | 6 | bundle install 7 | 8 | # Do any other automated setup that you need to do here 9 | -------------------------------------------------------------------------------- /lib/activesupport/json_encoder.rb: -------------------------------------------------------------------------------- 1 | module ActiveSupport 2 | self.json_encoder = YagniJsonEncoder 3 | 4 | def self.escape_html_entities_in_json=(*_args) 5 | raise StandardError, 6 | 'escape_html_entities_in_json is not supported in YagniJsonEncoder' 7 | end 8 | 9 | def self.parse_json_times 10 | raise StandardError, 11 | 'ActiveSupport.parse_json_times not supported in YagniJsonEncoder' 12 | end 13 | 14 | module JSON 15 | def self.decode(json) 16 | YagniJsonEncoder::JSON.parse(json, quirks_mode: true) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /lib/yagni_json_encoder.rb: -------------------------------------------------------------------------------- 1 | begin 2 | require 'json' 3 | rescue StandardError 4 | nil # ignore 5 | end 6 | 7 | require "active_support/json" 8 | require 'oj' 9 | 10 | class YagniJsonEncoder 11 | Oj.default_options = { mode: :compat, after_sep: 0 } 12 | JSON = Oj.mimic_JSON 13 | 14 | attr_reader :options 15 | 16 | def initialize(options = nil) 17 | @options = options || {} 18 | end 19 | 20 | def encode(value) 21 | JSON.generate value.as_json(options.dup), 22 | quirks_mode: true, 23 | max_nesting: false 24 | end 25 | end 26 | 27 | require 'activesupport/json_encoder' 28 | -------------------------------------------------------------------------------- /test/decoding_test.rb: -------------------------------------------------------------------------------- 1 | require 'yagni_json_encoder' 2 | require "test_helper" 3 | require "active_support/time" 4 | require "time_zone_test_helpers" 5 | 6 | class TestJSONDecoding < ActiveSupport::TestCase 7 | include TimeZoneTestHelpers 8 | 9 | class Foo 10 | def self.json_create(object) 11 | "Foo" 12 | end 13 | end 14 | 15 | TESTS = { 16 | %q({"returnTo":{"\/categories":"\/"}}) => { "returnTo" => { "/categories" => "/" } }, 17 | %q({"return\\"To\\":":{"\/categories":"\/"}}) => { "return\"To\":" => { "/categories" => "/" } }, 18 | %q({"returnTo":{"\/categories":1}}) => { "returnTo" => { "/categories" => 1 } }, 19 | %({"returnTo":[1,"a"]}) => { "returnTo" => [1, "a"] }, 20 | %({"returnTo":[1,"\\"a\\",", "b"]}) => { "returnTo" => [1, "\"a\",", "b"] }, 21 | %({"a": "'", "b": "5,000"}) => { "a" => "'", "b" => "5,000" }, 22 | %({"a": "a's, b's and c's", "b": "5,000"}) => { "a" => "a's, b's and c's", "b" => "5,000" }, 23 | # multibyte 24 | %({"matzue": "松江", "asakusa": "浅草"}) => { "matzue" => "松江", "asakusa" => "浅草" }, 25 | %({"a": "2007-01-01"}) => { "a" => Date.new(2007, 1, 1) }, 26 | %({"a": "2007-01-01 01:12:34 Z"}) => { "a" => Time.utc(2007, 1, 1, 1, 12, 34) }, 27 | %(["2007-01-01 01:12:34 Z"]) => [Time.utc(2007, 1, 1, 1, 12, 34)], 28 | %(["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)], 29 | # no time zone 30 | %({"a": "2007-01-01 01:12:34"}) => { "a" => Time.new(2007, 1, 1, 1, 12, 34, "-05:00") }, 31 | # invalid date 32 | %({"a": "1089-10-40"}) => { "a" => "1089-10-40" }, 33 | # xmlschema date notation 34 | %({"a": "2009-08-10T19:01:02"}) => { "a" => Time.new(2009, 8, 10, 19, 1, 2, "-04:00") }, 35 | %({"a": "2009-08-10T19:01:02Z"}) => { "a" => Time.utc(2009, 8, 10, 19, 1, 2) }, 36 | %({"a": "2009-08-10T19:01:02+02:00"}) => { "a" => Time.utc(2009, 8, 10, 17, 1, 2) }, 37 | %({"a": "2009-08-10T19:01:02-05:00"}) => { "a" => Time.utc(2009, 8, 11, 00, 1, 2) }, 38 | # needs to be *exact* 39 | %({"a": " 2007-01-01 01:12:34 Z "}) => { "a" => " 2007-01-01 01:12:34 Z " }, 40 | %({"a": "2007-01-01 : it's your birthday"}) => { "a" => "2007-01-01 : it's your birthday" }, 41 | %([]) => [], 42 | %({}) => {}, 43 | %({"a":1}) => { "a" => 1 }, 44 | %({"a": ""}) => { "a" => "" }, 45 | %({"a":"\\""}) => { "a" => "\"" }, 46 | %({"a": null}) => { "a" => nil }, 47 | %({"a": true}) => { "a" => true }, 48 | %({"a": false}) => { "a" => false }, 49 | '{"bad":"\\\\","trailing":""}' => { "bad" => "\\", "trailing" => "" }, 50 | %q({"a": "http:\/\/test.host\/posts\/1"}) => { "a" => "http://test.host/posts/1" }, 51 | %q({"a": "\u003cunicode\u0020escape\u003e"}) => { "a" => "" }, 52 | '{"a": "\\\\u0020skip double backslashes"}' => { "a" => "\\u0020skip double backslashes" }, 53 | %q({"a": "\u003cbr /\u003e"}) => { "a" => "
" }, 54 | %q({"b":["\u003ci\u003e","\u003cb\u003e","\u003cu\u003e"]}) => { "b" => ["", "", ""] }, 55 | # test combination of dates and escaped or unicode encoded data in arrays 56 | %q([{"d":"1970-01-01", "s":"\u0020escape"},{"d":"1970-01-01", "s":"\u0020escape"}]) => 57 | [{ "d" => Date.new(1970, 1, 1), "s" => " escape" }, { "d" => Date.new(1970, 1, 1), "s" => " escape" }], 58 | %q([{"d":"1970-01-01","s":"http:\/\/example.com"},{"d":"1970-01-01","s":"http:\/\/example.com"}]) => 59 | [{ "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }, 60 | { "d" => Date.new(1970, 1, 1), "s" => "http://example.com" }], 61 | # tests escaping of "\n" char with Yaml backend 62 | %q({"a":"\n"}) => { "a" => "\n" }, 63 | %q({"a":"\u000a"}) => { "a" => "\n" }, 64 | %q({"a":"Line1\u000aLine2"}) => { "a" => "Line1\nLine2" }, 65 | # prevent json unmarshalling 66 | '{"json_class":"TestJSONDecoding::Foo"}' => { "json_class" => "TestJSONDecoding::Foo" }, 67 | # json "fragments" - these are invalid JSON, but ActionPack relies on this 68 | '"a string"' => "a string", 69 | "1.1" => 1.1, 70 | "1" => 1, 71 | "-1" => -1, 72 | "true" => true, 73 | "false" => false, 74 | "null" => nil 75 | } 76 | 77 | TESTS.each_with_index do |(json, expected), index| 78 | fail_message = "JSON decoding failed for #{json}" 79 | 80 | test "json decodes #{index}" do 81 | with_tz_default "Eastern Time (US & Canada)" do 82 | with_parse_json_times(true) do 83 | silence_warnings do 84 | if expected.nil? 85 | assert_nil ActiveSupport::JSON.decode(json), fail_message 86 | else 87 | assert_equal expected, ActiveSupport::JSON.decode(json), fail_message 88 | end 89 | end 90 | end 91 | end 92 | end 93 | end 94 | 95 | test "json decodes time json with time parsing disabled" do 96 | with_parse_json_times(false) do 97 | expected = { "a" => "2007-01-01 01:12:34 Z" } 98 | assert_equal expected, ActiveSupport::JSON.decode(%({"a": "2007-01-01 01:12:34 Z"})) 99 | end 100 | end 101 | 102 | def test_failed_json_decoding 103 | assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%(undefined)) } 104 | assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({a: 1})) } 105 | assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%({: 1})) } 106 | # assert_raise(ActiveSupport::JSON.parse_error) { ActiveSupport::JSON.decode(%()) } 107 | end 108 | 109 | def test_cannot_pass_unsupported_options 110 | assert_raise(ArgumentError) { ActiveSupport::JSON.decode("", create_additions: true) } 111 | end 112 | 113 | private 114 | 115 | def with_parse_json_times(value) 116 | end 117 | end 118 | -------------------------------------------------------------------------------- /test/encoding_test.rb: -------------------------------------------------------------------------------- 1 | require 'yagni_json_encoder' 2 | require 'active_support/time' 3 | require 'encoding_test_cases' 4 | require 'securerandom' 5 | require 'test_helper' 6 | require 'time_zone_test_helpers' 7 | 8 | class TestJSONEncoding < ActiveSupport::TestCase 9 | include TimeZoneTestHelpers 10 | 11 | def sorted_json(json) 12 | return json unless json =~ /^\{.*\}$/ 13 | '{' + json[1..-2].split(',').sort.join(',') + '}' 14 | end 15 | 16 | JSONTest::EncodingTestCases.constants.each do |class_tests| 17 | define_method("test_#{class_tests[0..-6].underscore}") do 18 | begin 19 | prev = ActiveSupport.use_standard_json_time_format 20 | 21 | ActiveSupport.use_standard_json_time_format = class_tests =~ /^Standard/ 22 | JSONTest::EncodingTestCases.const_get(class_tests).each do |pair| 23 | assert_equal pair.last, sorted_json(ActiveSupport::JSON.encode(pair.first)) 24 | end 25 | ensure 26 | ActiveSupport.use_standard_json_time_format = prev 27 | end 28 | end 29 | end 30 | 31 | def test_yagni_json_encoder_loaded 32 | assert_equal ActiveSupport.json_encoder, YagniJsonEncoder 33 | end 34 | 35 | def test_process_status 36 | message = "https://github.com/rubinius/rubinius/issues/3334" 37 | skip message if RUBY_ENGINE == 'rbx' 38 | 39 | # There doesn't seem to be a good way to get a handle on a Process::Status object without actually 40 | # creating a child process, hence this to populate $? 41 | system("not_a_real_program_#{SecureRandom.hex}") 42 | assert_equal %({"exitstatus":#{$?.exitstatus},"pid":#{$?.pid}}), ActiveSupport::JSON.encode($?) 43 | end 44 | 45 | def test_hash_encoding 46 | assert_equal %({\"a\":\"b\"}), ActiveSupport::JSON.encode(:a => :b) 47 | assert_equal %({\"a\":1}), ActiveSupport::JSON.encode('a' => 1) 48 | assert_equal %({\"a\":[1,2]}), ActiveSupport::JSON.encode('a' => [1,2]) 49 | assert_equal %({"1":2}), ActiveSupport::JSON.encode(1 => 2) 50 | 51 | assert_equal %({\"a\":\"b\",\"c\":\"d\"}), sorted_json(ActiveSupport::JSON.encode(:a => :b, :c => :d)) 52 | end 53 | 54 | def test_escape_html_entities_in_json_fails 55 | assert_raise do 56 | ActiveSupport.escape_html_entities_in_json = true 57 | end 58 | end 59 | 60 | def test_hash_keys_encoding 61 | assert_equal "{\"<>\":\"<>\"}", ActiveSupport::JSON.encode("<>" => "<>") 62 | end 63 | 64 | def test_utf8_string_encoded_properly 65 | result = ActiveSupport::JSON.encode('€2.99') 66 | assert_equal '"€2.99"', result 67 | assert_equal(Encoding::UTF_8, result.encoding) 68 | 69 | result = ActiveSupport::JSON.encode('✎☺') 70 | assert_equal '"✎☺"', result 71 | assert_equal(Encoding::UTF_8, result.encoding) 72 | end 73 | 74 | def test_non_utf8_string_transcodes 75 | skip 76 | s = '二'.encode('Shift_JIS') 77 | result = ActiveSupport::JSON.encode(s) 78 | assert_equal '"二"', result 79 | assert_equal Encoding::UTF_8, result.encoding 80 | end 81 | 82 | def test_wide_utf8_chars 83 | w = '𠜎' 84 | result = ActiveSupport::JSON.encode(w) 85 | assert_equal '"𠜎"', result 86 | end 87 | 88 | def test_wide_utf8_roundtrip 89 | hash = { string: "𐒑" } 90 | json = ActiveSupport::JSON.encode(hash) 91 | decoded_hash = ActiveSupport::JSON.decode(json) 92 | assert_equal "𐒑", decoded_hash['string'] 93 | end 94 | 95 | def test_hash_key_identifiers_are_always_quoted 96 | values = {0 => 0, 1 => 1, :_ => :_, "$" => "$", "a" => "a", :A => :A, :A0 => :A0, "A0B" => "A0B"} 97 | assert_equal %w( "$" "A" "A0" "A0B" "_" "a" "0" "1" ).sort, object_keys(ActiveSupport::JSON.encode(values)) 98 | end 99 | 100 | def test_hash_should_allow_key_filtering_with_only 101 | assert_equal %({"a":1}), ActiveSupport::JSON.encode({'a' => 1, :b => 2, :c => 3}, :only => 'a') 102 | end 103 | 104 | def test_hash_should_allow_key_filtering_with_except 105 | assert_equal %({"b":2}), ActiveSupport::JSON.encode({'foo' => 'bar', :b => 2, :c => 3}, :except => ['foo', :c]) 106 | end 107 | 108 | def test_time_to_json_includes_local_offset 109 | with_standard_json_time_format(true) do 110 | with_env_tz 'US/Eastern' do 111 | assert_equal %("2005-02-01T15:15:10.000-05:00"), ActiveSupport::JSON.encode(Time.local(2005,2,1,15,15,10)) 112 | end 113 | end 114 | end 115 | 116 | def test_hash_with_time_to_json 117 | with_standard_json_time_format(false) do 118 | assert_equal '{"time":"2009/01/01 00:00:00 +0000"}', { :time => Time.utc(2009) }.to_json 119 | end 120 | end 121 | 122 | def test_nested_hash_with_float 123 | assert_nothing_raised do 124 | hash = { 125 | "CHI" => { 126 | :display_name => "chicago", 127 | :latitude => 123.234 128 | } 129 | } 130 | ActiveSupport::JSON.encode(hash) 131 | end 132 | end 133 | 134 | def test_hash_like_with_options 135 | h = JSONTest::Hashlike.new 136 | json = h.to_json :only => [:foo] 137 | 138 | assert_equal({"foo"=>"hello"}, JSON.parse(json)) 139 | end 140 | 141 | def test_object_to_json_with_options 142 | obj = Object.new 143 | obj.instance_variable_set :@foo, "hello" 144 | obj.instance_variable_set :@bar, "world" 145 | json = obj.to_json :only => ["foo"] 146 | 147 | assert_equal({"foo"=>"hello"}, JSON.parse(json)) 148 | end 149 | 150 | def test_struct_to_json_with_options 151 | struct = Struct.new(:foo, :bar).new 152 | struct.foo = "hello" 153 | struct.bar = "world" 154 | json = struct.to_json :only => [:foo] 155 | 156 | assert_equal({"foo"=>"hello"}, JSON.parse(json)) 157 | end 158 | 159 | def test_hash_should_pass_encoding_options_to_children_in_as_json 160 | person = { 161 | :name => 'John', 162 | :address => { 163 | :city => 'London', 164 | :country => 'UK' 165 | } 166 | } 167 | json = person.as_json :only => [:address, :city] 168 | 169 | assert_equal({ 'address' => { 'city' => 'London' }}, json) 170 | end 171 | 172 | def test_hash_should_pass_encoding_options_to_children_in_to_json 173 | person = { 174 | :name => 'John', 175 | :address => { 176 | :city => 'London', 177 | :country => 'UK' 178 | } 179 | } 180 | json = person.to_json :only => [:address, :city] 181 | 182 | assert_equal(%({"address":{"city":"London"}}), json) 183 | end 184 | 185 | def test_array_should_pass_encoding_options_to_children_in_as_json 186 | people = [ 187 | { :name => 'John', :address => { :city => 'London', :country => 'UK' }}, 188 | { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }} 189 | ] 190 | json = people.as_json :only => [:address, :city] 191 | expected = [ 192 | { 'address' => { 'city' => 'London' }}, 193 | { 'address' => { 'city' => 'Paris' }} 194 | ] 195 | 196 | assert_equal(expected, json) 197 | end 198 | 199 | def test_array_should_pass_encoding_options_to_children_in_to_json 200 | people = [ 201 | { :name => 'John', :address => { :city => 'London', :country => 'UK' }}, 202 | { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }} 203 | ] 204 | json = people.to_json :only => [:address, :city] 205 | 206 | assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) 207 | end 208 | 209 | People = Class.new(BasicObject) do 210 | include Enumerable 211 | def initialize() 212 | @people = [ 213 | { :name => 'John', :address => { :city => 'London', :country => 'UK' }}, 214 | { :name => 'Jean', :address => { :city => 'Paris' , :country => 'France' }} 215 | ] 216 | end 217 | def each(*, &blk) 218 | @people.each do |p| 219 | yield p if blk 220 | p 221 | end.each 222 | end 223 | end 224 | 225 | def test_enumerable_should_generate_json_with_as_json 226 | json = People.new.as_json :only => [:address, :city] 227 | expected = [ 228 | { 'address' => { 'city' => 'London' }}, 229 | { 'address' => { 'city' => 'Paris' }} 230 | ] 231 | 232 | assert_equal(expected, json) 233 | end 234 | 235 | def test_enumerable_should_generate_json_with_to_json 236 | json = People.new.to_json :only => [:address, :city] 237 | assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) 238 | end 239 | 240 | def test_enumerable_should_pass_encoding_options_to_children_in_as_json 241 | json = People.new.each.as_json :only => [:address, :city] 242 | expected = [ 243 | { 'address' => { 'city' => 'London' }}, 244 | { 'address' => { 'city' => 'Paris' }} 245 | ] 246 | 247 | assert_equal(expected, json) 248 | end 249 | 250 | def test_enumerable_should_pass_encoding_options_to_children_in_to_json 251 | json = People.new.each.to_json :only => [:address, :city] 252 | 253 | assert_equal(%([{"address":{"city":"London"}},{"address":{"city":"Paris"}}]), json) 254 | end 255 | 256 | class CustomWithOptions 257 | attr_accessor :foo, :bar 258 | 259 | def as_json(options={}) 260 | options[:only] = %w(foo bar) 261 | super(options) 262 | end 263 | end 264 | 265 | def test_hash_to_json_should_not_keep_options_around 266 | f = CustomWithOptions.new 267 | f.foo = "hello" 268 | f.bar = "world" 269 | 270 | hash = {"foo" => f, "other_hash" => {"foo" => "other_foo", "test" => "other_test"}} 271 | assert_equal({"foo"=>{"foo"=>"hello","bar"=>"world"}, 272 | "other_hash" => {"foo"=>"other_foo","test"=>"other_test"}}, ActiveSupport::JSON.decode(hash.to_json)) 273 | end 274 | 275 | def test_array_to_json_should_not_keep_options_around 276 | f = CustomWithOptions.new 277 | f.foo = "hello" 278 | f.bar = "world" 279 | 280 | array = [f, {"foo" => "other_foo", "test" => "other_test"}] 281 | assert_equal([{"foo"=>"hello","bar"=>"world"}, 282 | {"foo"=>"other_foo","test"=>"other_test"}], ActiveSupport::JSON.decode(array.to_json)) 283 | end 284 | 285 | class OptionsTest 286 | def as_json(options = :default) 287 | options 288 | end 289 | end 290 | 291 | def test_hash_as_json_without_options 292 | json = { foo: OptionsTest.new }.as_json 293 | assert_equal({"foo" => :default}, json) 294 | end 295 | 296 | def test_array_as_json_without_options 297 | json = [ OptionsTest.new ].as_json 298 | assert_equal([:default], json) 299 | end 300 | 301 | def test_struct_encoding 302 | Struct.new('UserNameAndEmail', :name, :email) 303 | Struct.new('UserNameAndDate', :name, :date) 304 | Struct.new('Custom', :name, :sub) 305 | user_email = Struct::UserNameAndEmail.new 'David', 'sample@example.com' 306 | user_birthday = Struct::UserNameAndDate.new 'David', Date.new(2010, 01, 01) 307 | custom = Struct::Custom.new 'David', user_birthday 308 | 309 | 310 | json_strings = "" 311 | json_string_and_date = "" 312 | json_custom = "" 313 | 314 | assert_nothing_raised do 315 | json_strings = user_email.to_json 316 | json_string_and_date = user_birthday.to_json 317 | json_custom = custom.to_json 318 | end 319 | 320 | assert_equal({"name" => "David", 321 | "sub" => { 322 | "name" => "David", 323 | "date" => "2010-01-01" }}, ActiveSupport::JSON.decode(json_custom)) 324 | 325 | assert_equal({"name" => "David", "email" => "sample@example.com"}, 326 | ActiveSupport::JSON.decode(json_strings)) 327 | 328 | assert_equal({"name" => "David", "date" => "2010-01-01"}, 329 | ActiveSupport::JSON.decode(json_string_and_date)) 330 | end 331 | 332 | def test_nil_true_and_false_represented_as_themselves 333 | assert_equal nil, nil.as_json 334 | assert_equal true, true.as_json 335 | assert_equal false, false.as_json 336 | end 337 | 338 | class HashWithAsJson < Hash 339 | attr_accessor :as_json_called 340 | 341 | def initialize(*) 342 | super 343 | end 344 | 345 | def as_json(options={}) 346 | @as_json_called = true 347 | super 348 | end 349 | end 350 | 351 | def test_json_gem_dump_by_passing_active_support_encoder 352 | h = HashWithAsJson.new 353 | h[:foo] = "hello" 354 | h[:bar] = "world" 355 | 356 | assert_equal %({"foo":"hello","bar":"world"}), JSON.dump(h) 357 | assert_nil h.as_json_called 358 | end 359 | 360 | def test_json_gem_generate_by_passing_active_support_encoder 361 | h = HashWithAsJson.new 362 | h[:foo] = "hello" 363 | h[:bar] = "world" 364 | 365 | assert_equal %({"foo":"hello","bar":"world"}), JSON.generate(h) 366 | assert_nil h.as_json_called 367 | end 368 | 369 | def test_json_gem_pretty_generate_by_passing_active_support_encoder 370 | h = HashWithAsJson.new 371 | h[:foo] = "hello" 372 | h[:bar] = "world" 373 | 374 | assert_equal < "hello", :bar => "world" } 13 | end 14 | end 15 | 16 | class Custom 17 | def initialize(serialized) 18 | @serialized = serialized 19 | end 20 | 21 | def as_json(options = nil) 22 | @serialized 23 | end 24 | end 25 | 26 | class MyStruct < Struct.new(:name, :value) 27 | def initialize(*) 28 | @unused = "unused instance variable" 29 | super 30 | end 31 | end 32 | 33 | module EncodingTestCases 34 | TrueTests = [[ true, %(true) ]] 35 | FalseTests = [[ false, %(false) ]] 36 | NilTests = [[ nil, %(null) ]] 37 | NumericTests = [[ 1, %(1) ], 38 | [ 2.5, %(2.5) ], 39 | [ 0.0/0.0, %(null) ], 40 | [ 1.0/0.0, %(null) ], 41 | [ -1.0/0.0, %(null) ], 42 | [ BigDecimal('0.0')/BigDecimal('0.0'), %(null) ], 43 | [ BigDecimal('2.5'), %("#{BigDecimal('2.5')}") ]] 44 | 45 | StringTests = [[ 'http://test.host/posts/1', %("http://test.host/posts/1")]] 46 | 47 | ArrayTests = [[ ['a', 'b', 'c'], %([\"a\",\"b\",\"c\"]) ], 48 | [ [1, 'a', :b, nil, false], %([1,\"a\",\"b\",null,false]) ]] 49 | 50 | HashTests = [[ {foo: "bar"}, %({\"foo\":\"bar\"}) ], 51 | [ {1 => 1, 2 => 'a', 3 => :b, 4 => nil, 5 => false}, %({\"1\":1,\"2\":\"a\",\"3\":\"b\",\"4\":null,\"5\":false}) ]] 52 | 53 | RangeTests = [[ 1..2, %("1..2")], 54 | [ 1...2, %("1...2")], 55 | [ 1.5..2.5, %("1.5..2.5")]] 56 | 57 | SymbolTests = [[ :a, %("a") ], 58 | [ :this, %("this") ], 59 | [ :"a b", %("a b") ]] 60 | 61 | ObjectTests = [[ Foo.new(1, 2), %({\"a\":1,\"b\":2}) ]] 62 | HashlikeTests = [[ Hashlike.new, %({\"bar\":\"world\",\"foo\":\"hello\"}) ]] 63 | StructTests = [[ MyStruct.new(:foo, "bar"), %({\"name\":\"foo\",\"value\":\"bar\"}) ], 64 | [ MyStruct.new(nil, nil), %({\"name\":null,\"value\":null}) ]] 65 | CustomTests = [[ Custom.new("custom"), '"custom"' ], 66 | [ Custom.new(nil), 'null' ], 67 | [ Custom.new(:a), '"a"' ], 68 | [ Custom.new([ :foo, "bar" ]), '["foo","bar"]' ], 69 | [ Custom.new({ :foo => "hello", :bar => "world" }), '{"bar":"world","foo":"hello"}' ], 70 | [ Custom.new(Hashlike.new), '{"bar":"world","foo":"hello"}' ], 71 | [ Custom.new(Custom.new(Custom.new(:a))), '"a"' ]] 72 | 73 | RegexpTests = [[ /^a/, '"(?-mix:^a)"' ], [/^\w{1,2}[a-z]+/ix, '"(?ix-m:^\\\\w{1,2}[a-z]+)"']] 74 | 75 | DateTests = [[ Date.new(2005,2,1), %("2005/02/01") ]] 76 | TimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] 77 | DateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005/02/01 15:15:10 +0000") ]] 78 | 79 | StandardDateTests = [[ Date.new(2005,2,1), %("2005-02-01") ]] 80 | StandardTimeTests = [[ Time.utc(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000Z") ]] 81 | StandardDateTimeTests = [[ DateTime.civil(2005,2,1,15,15,10), %("2005-02-01T15:15:10.000+00:00") ]] 82 | StandardStringTests = [[ 'this is the ', %("this is the ")]] 83 | end 84 | end 85 | -------------------------------------------------------------------------------- /test/test_helper.rb: -------------------------------------------------------------------------------- 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 18 | -------------------------------------------------------------------------------- /test/time_zone_test_helpers.rb: -------------------------------------------------------------------------------- 1 | module TimeZoneTestHelpers 2 | def with_tz_default(tz = nil) 3 | old_tz = Time.zone 4 | Time.zone = tz 5 | yield 6 | ensure 7 | Time.zone = old_tz 8 | end 9 | 10 | def with_env_tz(new_tz = 'US/Eastern') 11 | old_tz, ENV['TZ'] = ENV['TZ'], new_tz 12 | yield 13 | ensure 14 | old_tz ? ENV['TZ'] = old_tz : ENV.delete('TZ') 15 | end 16 | end 17 | -------------------------------------------------------------------------------- /yagni_json_encoder.gemspec: -------------------------------------------------------------------------------- 1 | # coding: utf-8 2 | lib = File.expand_path('../lib', __FILE__) 3 | $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) 4 | 5 | Gem::Specification.new do |spec| 6 | spec.name = "yagni_json_encoder" 7 | spec.version = "1.0.0" 8 | spec.authors = ["Ian Ker-Seymer"] 9 | spec.email = ["i.kerseymer@gmail.com"] 10 | spec.summary = "~2x faster JSON encoder for Rails" 11 | spec.homepage = "https://github.com/ianks/yagni_json_encoder" 12 | spec.license = 'MIT' 13 | 14 | spec.files = Dir['MIT-LICENSE', 'README.md', 'lib/**/*'] 15 | spec.test_files = Dir['test/**/*.rb'] 16 | spec.require_paths = ["lib"] 17 | 18 | spec.add_development_dependency "rake" 19 | 20 | spec.add_dependency "oj", "~> 2.18.5" 21 | spec.add_dependency 'activesupport', '>= 4.1.0' 22 | end 23 | --------------------------------------------------------------------------------