diff options
19 files changed, 336 insertions, 41 deletions
diff --git a/actionpack/CHANGELOG.md b/actionpack/CHANGELOG.md index 8181b386be..112a787d3b 100644 --- a/actionpack/CHANGELOG.md +++ b/actionpack/CHANGELOG.md @@ -1,3 +1,32 @@ +* Introducing Variants + + We often want to render different html/json/xml templates for phones, + tablets, and desktop browsers. Variants make it easy. + + The request variant is a specialization of the request format, like :tablet, + :phone, or :desktop. + + You can set the variant in a before_action: + + request.variant = :tablet if request.user_agent =~ /iPad/ + + Respond to variants in the action just like you respond to formats: + + respond_to do |format| + format.html do |html| + html.tablet # renders app/views/projects/show.html+tablet.erb + html.phone { extra_setup; render ... } + end + end + + Provide separate templates for each format and variant: + + app/views/projects/show.html.erb + app/views/projects/show.html+tablet.erb + app/views/projects/show.html+phone.erb + + *Łukasz Strzałkowski* + * Fix header `Content-Type: #<Mime::NullType:...>` in localized template. When localized template has no format in the template name, diff --git a/actionpack/lib/abstract_controller/collector.rb b/actionpack/lib/abstract_controller/collector.rb index 09b9e7ddf0..ddd56b354a 100644 --- a/actionpack/lib/abstract_controller/collector.rb +++ b/actionpack/lib/abstract_controller/collector.rb @@ -23,7 +23,17 @@ module AbstractController protected def method_missing(symbol, &block) - mime_constant = Mime.const_get(symbol.upcase) + const_name = symbol.upcase + + unless Mime.const_defined?(const_name) + raise NoMethodError, "To respond to a custom format, register it as a MIME type first: " \ + "http://guides.rubyonrails.org/action_controller_overview.html#restful-downloads. " \ + "If you meant to respond to a variant like :tablet or :phone, not a custom format, " \ + "be sure to nest your variant response within a format response: " \ + "format.html { |html| html.tablet { ... } }" + end + + mime_constant = Mime.const_get(const_name) if Mime::SET.include?(mime_constant) AbstractController::Collector.generate_method_for_mime(mime_constant) diff --git a/actionpack/lib/abstract_controller/rendering.rb b/actionpack/lib/abstract_controller/rendering.rb index fb8f40cb9b..ce3a0316c4 100644 --- a/actionpack/lib/abstract_controller/rendering.rb +++ b/actionpack/lib/abstract_controller/rendering.rb @@ -102,6 +102,8 @@ module AbstractController # :api: private def _normalize_render(*args, &block) options = _normalize_args(*args, &block) + #TODO: remove defined? when we restore AP <=> AV dependency + options[:variant] = request.variant if defined?(request) && request.variant.present? _normalize_options(options) options end diff --git a/actionpack/lib/action_controller/metal/mime_responds.rb b/actionpack/lib/action_controller/metal/mime_responds.rb index 84ade41036..54d3be68f0 100644 --- a/actionpack/lib/action_controller/metal/mime_responds.rb +++ b/actionpack/lib/action_controller/metal/mime_responds.rb @@ -181,13 +181,40 @@ module ActionController #:nodoc: # end # end # + # Formats can have different variants. + # + # The request variant is a specialization of the request format, like <tt>:tablet</tt>, + # <tt>:phone</tt>, or <tt>:desktop<tt>. + # + # We often want to render different html/json/xml templates for phones, + # tablets, and desktop browsers. Variants make it easy. + # + # You can set the variant in a +before_action+: + # + # request.variant = :tablet if request.user_agent =~ /iPad/ + # + # Respond to variants in the action just like you respond to formats: + # + # respond_to do |format| + # format.html do |html| + # html.tablet # renders app/views/projects/show.html+tablet.erb + # html.phone { extra_setup; render ... } + # end + # end + # + # Provide separate templates for each format and variant: + # + # app/views/projects/show.html.erb + # app/views/projects/show.html+tablet.erb + # app/views/projects/show.html+phone.erb + # # Be sure to check the documentation of +respond_with+ and # <tt>ActionController::MimeResponds.respond_to</tt> for more examples. def respond_to(*mimes, &block) raise ArgumentError, "respond_to takes either types or a block, never both" if mimes.any? && block_given? if collector = retrieve_collector_from_mimes(mimes, &block) - response = collector.response + response = collector.response(request.variant) response ? response.call : render({}) end end @@ -260,7 +287,7 @@ module ActionController #:nodoc: # * for other requests - i.e. data formats such as xml, json, csv etc, if # the resource passed to +respond_with+ responds to <code>to_<format></code>, # the method attempts to render the resource in the requested format - # directly, e.g. for an xml request, the response is equivalent to calling + # directly, e.g. for an xml request, the response is equivalent to calling # <code>render xml: resource</code>. # # === Nested resources @@ -321,13 +348,15 @@ module ActionController #:nodoc: # 2. <tt>:action</tt> - overwrites the default render action used after an # unsuccessful html +post+ request. def respond_with(*resources, &block) - raise "In order to use respond_with, first you need to declare the formats your " \ - "controller responds to in the class level" if self.class.mimes_for_respond_to.empty? + if self.class.mimes_for_respond_to.empty? + raise "In order to use respond_with, first you need to declare the " \ + "formats your controller responds to in the class level." + end if collector = retrieve_collector_from_mimes(&block) options = resources.size == 1 ? {} : resources.extract_options! options = options.clone - options[:default_response] = collector.response + options[:default_response] = collector.response(request.variant) (options.delete(:responder) || self.class.responder).call(self, resources, options) end end @@ -417,13 +446,28 @@ module ActionController #:nodoc: @responses[mime_type] ||= block end - def response - @responses.fetch(format, @responses[Mime::ALL]) + def response(variant) + response = @responses.fetch(format, @responses[Mime::ALL]) + if response.nil? || response.arity == 0 + response + else + lambda { response.call VariantFilter.new(variant) } + end end def negotiate_format(request) @format = request.negotiate_mime(@responses.keys) end + + class VariantFilter #:nodoc: + def initialize(variant) + @variant = variant + end + + def method_missing(name) + yield if name == @variant + end + end end end end diff --git a/actionpack/lib/action_dispatch/http/mime_negotiation.rb b/actionpack/lib/action_dispatch/http/mime_negotiation.rb index 40bb060d52..346598b6de 100644 --- a/actionpack/lib/action_dispatch/http/mime_negotiation.rb +++ b/actionpack/lib/action_dispatch/http/mime_negotiation.rb @@ -10,6 +10,8 @@ module ActionDispatch self.ignore_accept_header = false end + attr_reader :variant + # The MIME type of the HTTP request, such as Mime::XML. # # For backward compatibility, the post \format is extracted from the @@ -64,6 +66,18 @@ module ActionDispatch end end + # Sets the \variant for template. + def variant=(variant) + if variant.is_a? Symbol + @variant = variant + else + raise ArgumentError, "request.variant must be set to a Symbol, not a #{variant.class}. " \ + "For security reasons, never directly set the variant to a user-provided value, " \ + "like params[:variant].to_sym. Check user-provided value against a whitelist first, " \ + "then set the variant: request.variant = :tablet if params[:variant] == 'tablet'" + end + end + # Sets the \format by string extension, which can be used to force custom formats # that are not controlled by the extension. # diff --git a/actionpack/test/abstract/collector_test.rb b/actionpack/test/abstract/collector_test.rb index 5709ad0378..b1a5044399 100644 --- a/actionpack/test/abstract/collector_test.rb +++ b/actionpack/test/abstract/collector_test.rb @@ -37,7 +37,7 @@ module AbstractController test "does not register unknown mime types" do collector = MyCollector.new - assert_raise NameError do + assert_raise NoMethodError do collector.unknown end end diff --git a/actionpack/test/controller/mime/respond_to_test.rb b/actionpack/test/controller/mime/respond_to_test.rb index 774dabe105..2b6c8739af 100644 --- a/actionpack/test/controller/mime/respond_to_test.rb +++ b/actionpack/test/controller/mime/respond_to_test.rb @@ -146,6 +146,26 @@ class RespondToController < ActionController::Base end end + def variant_with_implicit_rendering + end + + def variant_with_format_and_custom_render + request.variant = :mobile + + respond_to do |type| + type.html { render text: "mobile" } + end + end + + def multiple_variants_for_format + respond_to do |type| + type.html do |html| + html.tablet { render text: "tablet" } + html.phone { render text: "phone" } + end + end + end + protected def set_layout case action_name @@ -490,4 +510,38 @@ class RespondToControllerTest < ActionController::TestCase get :using_defaults, :format => "invalidformat" end end + + def test_invalid_variant + @request.variant = :invalid + assert_raises(ActionView::MissingTemplate) do + get :variant_with_implicit_rendering + end + end + + def test_variant_not_set_regular_template_missing + assert_raises(ActionView::MissingTemplate) do + get :variant_with_implicit_rendering + end + end + + def test_variant_with_implicit_rendering + @request.variant = :mobile + get :variant_with_implicit_rendering + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_variant_with_format_and_custom_render + @request.variant = :phone + get :variant_with_format_and_custom_render + assert_equal "text/html", @response.content_type + assert_equal "mobile", @response.body + end + + def test_multiple_variants_for_format + @request.variant = :tablet + get :multiple_variants_for_format + assert_equal "text/html", @response.content_type + assert_equal "tablet", @response.body + end end diff --git a/actionpack/test/dispatch/request_test.rb b/actionpack/test/dispatch/request_test.rb index 9c7789bcfb..b308c5749a 100644 --- a/actionpack/test/dispatch/request_test.rb +++ b/actionpack/test/dispatch/request_test.rb @@ -844,6 +844,19 @@ class RequestTest < ActiveSupport::TestCase end end + test "setting variant" do + request = stub_request + request.variant = :mobile + assert_equal :mobile, request.variant + end + + test "setting variant with non symbol value" do + request = stub_request + assert_raise ArgumentError do + request.variant = "mobile" + end + end + protected def stub_request(env = {}) diff --git a/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb new file mode 100644 index 0000000000..317801ad30 --- /dev/null +++ b/actionpack/test/fixtures/respond_to/variant_with_implicit_rendering.html+mobile.erb @@ -0,0 +1 @@ +mobile
\ No newline at end of file diff --git a/actionview/lib/action_view/lookup_context.rb b/actionview/lib/action_view/lookup_context.rb index c6ff683827..e07d9b6314 100644 --- a/actionview/lib/action_view/lookup_context.rb +++ b/actionview/lib/action_view/lookup_context.rb @@ -52,6 +52,7 @@ module ActionView locales end register_detail(:formats) { ActionView::Base.default_formats || [:html, :text, :js, :css, :xml, :json] } + register_detail(:variants) { [] } register_detail(:handlers){ Template::Handlers.extensions } class DetailsKey #:nodoc: diff --git a/actionview/lib/action_view/rendering.rb b/actionview/lib/action_view/rendering.rb index 82db9e26df..99b95fdfb7 100644 --- a/actionview/lib/action_view/rendering.rb +++ b/actionview/lib/action_view/rendering.rb @@ -88,10 +88,14 @@ module ActionView private - # Find and renders a template based on the options given. + # Find and render a template based on the options given. # :api: private def _render_template(options) #:nodoc: + variant = options[:variant] + lookup_context.rendered_format = nil if options[:formats] + lookup_context.variants = [variant] if variant + view_renderer.render(view_context, options) end diff --git a/actionview/lib/action_view/template/handlers/erb.rb b/actionview/lib/action_view/template/handlers/erb.rb index c8a0059596..4523060442 100644 --- a/actionview/lib/action_view/template/handlers/erb.rb +++ b/actionview/lib/action_view/template/handlers/erb.rb @@ -18,7 +18,7 @@ module ActionView src << "@output_buffer.safe_append='" src << "\n" * @newline_pending if @newline_pending > 0 src << escape_text(text) - src << "';" + src << "'.freeze;" @newline_pending = 0 end @@ -67,7 +67,7 @@ module ActionView def flush_newline_if_pending(src) if @newline_pending > 0 - src << "@output_buffer.safe_append='#{"\n" * @newline_pending}';" + src << "@output_buffer.safe_append='#{"\n" * @newline_pending}'.freeze;" @newline_pending = 0 end end diff --git a/actionview/lib/action_view/template/resolver.rb b/actionview/lib/action_view/template/resolver.rb index 3279f068c9..3a3b74cdd5 100644 --- a/actionview/lib/action_view/template/resolver.rb +++ b/actionview/lib/action_view/template/resolver.rb @@ -162,8 +162,8 @@ module ActionView # An abstract class that implements a Resolver with path semantics. class PathResolver < Resolver #:nodoc: - EXTENSIONS = [:locale, :formats, :handlers] - DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{.:handlers,}" + EXTENSIONS = { :locale => ".", :formats => ".", :variants => "+", :handlers => "." } + DEFAULT_PATTERN = ":prefix/:action{.:locale,}{.:formats,}{+:variants,}{.:handlers,}" def initialize(pattern=nil) @pattern = pattern || DEFAULT_PATTERN @@ -240,7 +240,9 @@ module ActionView end handler = Template.handler_for_extension(extension) - format = pieces.last && Template::Types[pieces.last] + format = pieces.last && pieces.last.split(EXTENSIONS[:variants], 2).first # remove variant from format + format &&= Template::Types[format] + [handler, format] end end @@ -303,12 +305,13 @@ module ActionView # An Optimized resolver for Rails' most common case. class OptimizedFileSystemResolver < FileSystemResolver #:nodoc: def build_query(path, details) - exts = EXTENSIONS.map { |ext| details[ext] } query = escape_entry(File.join(@path, path)) - query + exts.map { |ext| - "{#{ext.compact.uniq.map { |e| ".#{e}," }.join}}" - }.join + exts = EXTENSIONS.map do |ext, prefix| + "{#{details[ext].compact.uniq.map { |e| "#{prefix}#{e}," }.join}}" + end.join + + query + exts end end diff --git a/actionview/lib/action_view/testing/resolvers.rb b/actionview/lib/action_view/testing/resolvers.rb index 7afa2fa613..af53ad3b25 100644 --- a/actionview/lib/action_view/testing/resolvers.rb +++ b/actionview/lib/action_view/testing/resolvers.rb @@ -21,7 +21,7 @@ module ActionView #:nodoc: def query(path, exts, formats) query = "" - EXTENSIONS.each do |ext| + EXTENSIONS.each_key do |ext| query << '(' << exts[ext].map {|e| e && Regexp.escape(".#{e}") }.join('|') << '|)' end query = /^(#{Regexp.escape(path)})#{query}$/ diff --git a/actionview/test/template/lookup_context_test.rb b/actionview/test/template/lookup_context_test.rb index a6a3d6279e..ce9485e146 100644 --- a/actionview/test/template/lookup_context_test.rb +++ b/actionview/test/template/lookup_context_test.rb @@ -36,6 +36,11 @@ class LookupContextTest < ActiveSupport::TestCase assert @lookup_context.formats.frozen? end + test "provides getters and setters for variants" do + @lookup_context.variants = [:mobile] + assert_equal [:mobile], @lookup_context.variants + end + test "provides getters and setters for formats" do @lookup_context.formats = [:html] assert_equal [:html], @lookup_context.formats @@ -254,7 +259,7 @@ class TestMissingTemplate < ActiveSupport::TestCase test "if a single prefix is passed as a string and the lookup fails, MissingTemplate accepts it" do e = assert_raise ActionView::MissingTemplate do - details = {:handlers=>[], :formats=>[], :locale=>[]} + details = {:handlers=>[], :formats=>[], :variants=>[], :locale=>[]} @lookup_context.view_paths.find("foo", "parent", true, details) end assert_match %r{Missing partial parent/_foo with .* Searched in:\n \* "/Path/to/views"\n}, e.message diff --git a/actionview/test/template/testing/fixture_resolver_test.rb b/actionview/test/template/testing/fixture_resolver_test.rb index 9649f349cb..d6cfa997cd 100644 --- a/actionview/test/template/testing/fixture_resolver_test.rb +++ b/actionview/test/template/testing/fixture_resolver_test.rb @@ -3,13 +3,13 @@ require 'abstract_unit' class FixtureResolverTest < ActiveSupport::TestCase def test_should_return_empty_list_for_unknown_path resolver = ActionView::FixtureResolver.new() - templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => []}) + templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => []}) assert_equal [], templates, "expected an empty list of templates" end def test_should_return_template_for_declared_path resolver = ActionView::FixtureResolver.new("arbitrary/path.erb" => "this text") - templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :handlers => [:erb]}) + templates = resolver.find_all("path", "arbitrary", false, {:locale => [], :formats => [:html], :variants => [], :handlers => [:erb]}) assert_equal 1, templates.size, "expected one template" assert_equal "this text", templates.first.source assert_equal "arbitrary/path", templates.first.virtual_path diff --git a/activesupport/CHANGELOG.md b/activesupport/CHANGELOG.md index b63c44fbcb..2cb44d8371 100644 --- a/activesupport/CHANGELOG.md +++ b/activesupport/CHANGELOG.md @@ -366,6 +366,10 @@ *Yves Senn* +* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. + + *Toshinori Kajihara* + * Raise an error when multiple `included` blocks are defined for a Concern. The old behavior would silently discard previously defined blocks, running only the last one. diff --git a/guides/source/4_1_release_notes.md b/guides/source/4_1_release_notes.md index de29b2e58e..a8ec40adee 100644 --- a/guides/source/4_1_release_notes.md +++ b/guides/source/4_1_release_notes.md @@ -3,6 +3,7 @@ Ruby on Rails 4.1 Release Notes Highlights in Rails 4.1: +* Variants * Action View extracted from Action Pack These release notes cover only the major changes. To know about various bug @@ -27,6 +28,38 @@ guide. Major Features -------------- +### Variants + + We often want to render different html/json/xml templates for phones, + tablets, and desktop browsers. Variants makes it easy. + + The request variant is a specialization of the request format, like `:tablet`, + `:phone`, or `:desktop`. + + You can set the variant in a before_action: + + ```ruby + request.variant = :tablet if request.user_agent =~ /iPad/ + ``` + + Respond to variants in the action just like you respond to formats: + + ```ruby + respond_to do |format| + format.html do |html| + html.tablet # renders app/views/projects/show.html+tablet.erb + html.phone { extra_setup; render ... } + end + end + ``` + + Provide separate templates for each format and variant: + + ``` + app/views/projects/show.html.erb + app/views/projects/show.html+tablet.erb + app/views/projects/show.html+phone.erb + ``` Documentation ------------- @@ -104,6 +137,17 @@ for detailed changes. ### Removals +* Removed `MultiJSON` dependency. As a result, `ActiveSupport::JSON.decode` + no longer accepts an options hash for `MultiJSON`. ([Pull Request](https://github.com/rails/rails/pull/10576) / [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Removed support for the `encode_json` hook used for encoding custom objects into + JSON. This feature has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Related Pull Request](https://github.com/rails/rails/pull/12183) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Removed deprecated `ActiveSupport::JSON::Variable` with no replacement. + * Removed deprecated `String#encoding_aware?` core extensions (`core_ext/string/encoding`). * Removed deprecated `Module#local_constant_names` in favor of `Module#local_constants`. @@ -138,8 +182,32 @@ for detailed changes. explicitly convert the value into an AS::Duration, i.e. `5.ago` => `5.seconds.ago` ([Pull Request](https://github.com/rails/rails/pull/12389)) +* Deprecated the require path `active_support/core_ext/object/to_json`. Require + `active_support/core_ext/object/json` instead. ([Pull Request](https://github.com/rails/rails/pull/12203)) + +* Deprecated `ActiveSupport::JSON::Encoding::CircularReferenceError`. This feature + has been extracted into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Pull Request](https://github.com/rails/rails/pull/12785) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Deprecated `ActiveSupport.encode_big_decimal_as_string` option. This feature has + been extracetd into the [activesupport-json_encoder](https://github.com/rails/activesupport-json_encoder) + gem. + ([Pull Request](https://github.com/rails/rails/pull/13060) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + ### Notable changes +* `ActiveSupport`'s JSON encoder has been rewritten to take advantage of the + JSON gem rather than doing custom encoding in pure-Ruby. + ([Pull Request](https://github.com/rails/rails/pull/12183) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + +* Improved compatibility with the JSON gem. + ([Pull Request](https://github.com/rails/rails/pull/12862) / + [More Details](upgrading_ruby_on_rails.html#changes-in-json-handling)) + * Added `ActiveSupport::Testing::TimeHelpers#travel` and `#travel_to`. These methods change current time to the given time or time difference by stubbing `Time.now` and @@ -186,8 +254,8 @@ for detailed changes. ### Notable changes -* Take a hash with options inside array in - `#url_for`. ([Pull Request](https://github.com/rails/rails/pull/9599)) +* `#url_for` takes a hash with options inside an + array. ([Pull Request](https://github.com/rails/rails/pull/9599)) * Added `session#fetch` method fetch behaves similarly to [Hash#fetch](http://www.ruby-doc.org/core-1.9.3/Hash.html#method-i-fetch), @@ -233,29 +301,18 @@ for detailed changes. * Removed deprecated `SchemaStatements#distinct`. -* Moved deprecated `ActiveRecord::TestCase` into the rails test +* Moved deprecated `ActiveRecord::TestCase` into the Rails test suite. The class is no longer public and is only used for internal Rails tests. * Removed support for deprecated option `:restrict` for `:dependent` in associations. -* Removed support for deprecated `delete_sql` in associations. - -* Removed support for deprecated `insert_sql` in associations. - -* Removed support for deprecated `finder_sql` in associations. - -* Removed support for deprecated `counter_sql` in associations. +* Removed support for deprecated `:delete_sql`, `:insert_sql`, `:finder_sql` + and `:counter_sql` options in associations. * Removed deprecated method `type_cast_code` from Column. -* Removed deprecated options `delete_sql` and `insert_sql` from HABTM - association. - -* Removed deprecated options `finder_sql` and `counter_sql` from - collection association. - * Removed deprecated `ActiveRecord::Base#connection` method. Make sure to access it via the class. @@ -274,9 +331,9 @@ for detailed changes. * Removed `activerecord-deprecated_finders` as a dependency -* Usage of `implicit_readonly` is being removed. Please use `readonly` method +* Removed usage of `implicit_readonly`. Please use `readonly` method explicitly to mark records as - `readonly. ([Pull Request](https://github.com/rails/rails/pull/10769)) + `readonly`. ([Pull Request](https://github.com/rails/rails/pull/10769)) ### Deprecations diff --git a/guides/source/upgrading_ruby_on_rails.md b/guides/source/upgrading_ruby_on_rails.md index 3fbc913d8b..8ad7e62789 100644 --- a/guides/source/upgrading_ruby_on_rails.md +++ b/guides/source/upgrading_ruby_on_rails.md @@ -27,6 +27,60 @@ Upgrading from Rails 4.0 to Rails 4.1 NOTE: This section is a work in progress. +### Changes in JSON handling + +The are a few major changes related to JSON handling in Rails 4.1. + +#### MultiJSON removal + +MultiJSON has reached its [end-of-life](https://github.com/rails/rails/pull/10576) +and has been removed from Rails. + +If your application currently depend on MultiJSON directly, you have a few options: + +1. Add 'multi_json' to your Gemfile. Note that this might cease to work in the future + +2. Migrate away from MultiJSON by using `obj.to_json`, and `JSON.parse(str)` instead. + +WARNING: Do not simply replace `MultiJson.dump` and `MultiJson.load` with +`JSON.dump` and `JSON.load`. These JSON gem APIs are meant for serializing and +deserializing arbitrary Ruby objects and are generally [unsafe](http://www.ruby-doc.org/stdlib-2.0.0/libdoc/json/rdoc/JSON.html#method-i-load). + +#### JSON gem compatibility + +Historically, Rails had some compatibility issues with the JSON gem. Using +`JSON.generate` and `JSON.dump` inside a Rails application could produce +unexpected errors. + +Rails 4.1 fixed these issues by isolating its own encoder from the JSON gem. The +JSON gem APIs will function as normal, but they will not have access to any +Rails-specific features. For example: + +```ruby +class FooBar + def as_json(options = nil) + { foo: "bar" } + end +end + +>> FooBar.new.to_json # => "{\"foo\":\"bar\"}" +>> JSON.generate(FooBar.new, quirks_mode: true) # => "\"#<FooBar:0x007fa80a481610>\"" +``` + +#### New JSON encoder + +The JSON encoder in Rails 4.1 has been rewritten to take advantage of the JSON +gem. For most applications, this should be a transparent change. However, as +part of the rewrite, the following features have been removed from the encoder: + +1. Circular data structure detection +2. Support for the `encode_json` hook +3. Option to encode `BigDecimal` objects as numbers instead of strings + +If you application depends on one of these features, you can get them back by +adding the [`activesupport-json_encoder`](https://github.com/rails/activesupport-json_encoder) +gem to your Gemfile. + ### Methods defined in Active Record fixtures Rails 4.1 evaluates each fixture's ERB in a separate context, so helper methods |