Permalink
Browse files

Merge branch 'serializers'

This implements the ActiveModel::Serializer object. Includes code, tests, generators and guides.

From José and Yehuda with love.

Conflicts:
	railties/CHANGELOG.md
  • Loading branch information...
2 parents 4565c87 + 696d01f commit fcacc6986ab60f1fb2e423a73bf47c7abd7b191d @josevalim josevalim committed Nov 25, 2011
Showing with 2,074 additions and 482 deletions.
  1. +1 −0 actionpack/lib/action_controller.rb
  2. +1 −0 actionpack/lib/action_controller/base.rb
  3. +9 −11 actionpack/lib/action_controller/metal/renderers.rb
  4. +51 −0 actionpack/lib/action_controller/metal/serialization.rb
  5. +48 −0 actionpack/test/controller/render_json_test.rb
  6. +13 −1 activemodel/CHANGELOG.md
  7. +3 −0 activemodel/lib/active_model.rb
  8. +156 −0 activemodel/lib/active_model/serializable.rb
  9. +108 −0 activemodel/lib/active_model/serializable/json.rb
  10. +195 −0 activemodel/lib/active_model/serializable/xml.rb
  11. +5 −134 activemodel/lib/active_model/serialization.rb
  12. +253 −0 activemodel/lib/active_model/serializer.rb
  13. +3 −99 activemodel/lib/active_model/serializers/json.rb
  14. +5 −186 activemodel/lib/active_model/serializers/xml.rb
  15. +1 −1 activemodel/test/cases/{serializers/json_serialization_test.rb → serializable/json_test.rb}
  16. +2 −2 activemodel/test/cases/{serializers/xml_serialization_test.rb → serializable/xml_test.rb}
  17. +2 −2 activemodel/test/cases/{serialization_test.rb → serializable_test.rb}
  18. +432 −0 activemodel/test/cases/serializer_test.rb
  19. +1 −1 activerecord/lib/active_record/serialization.rb
  20. +3 −3 activerecord/lib/active_record/serializers/xml_serializer.rb
  21. +2 −2 activesupport/lib/active_support/core_ext/object/to_json.rb
  22. +21 −13 activesupport/lib/active_support/dependencies.rb
  23. +4 −4 activesupport/lib/active_support/json/encoding.rb
  24. +28 −15 activesupport/test/class_cache_test.rb
  25. +9 −7 railties/CHANGELOG.md
  26. +563 −0 railties/guides/source/serializers.textile
  27. +3 −1 railties/lib/rails/generators.rb
  28. +1 −0 railties/lib/rails/generators/rails/scaffold/scaffold_generator.rb
  29. +9 −0 railties/lib/rails/generators/rails/serializer/USAGE
  30. +39 −0 railties/lib/rails/generators/rails/serializer/serializer_generator.rb
  31. +9 −0 railties/lib/rails/generators/rails/serializer/templates/serializer.rb
  32. +13 −0 railties/lib/rails/generators/test_unit/serializer/serializer_generator.rb
  33. +9 −0 railties/lib/rails/generators/test_unit/serializer/templates/unit_test.rb
  34. +9 −0 railties/test/generators/scaffold_generator_test.rb
  35. +63 −0 railties/test/generators/serializer_generator_test.rb
@@ -31,6 +31,7 @@ module ActionController
autoload :RequestForgeryProtection
autoload :Rescue
autoload :Responder
+ autoload :Serialization
autoload :SessionManagement
autoload :Streaming
autoload :Testing
@@ -190,6 +190,7 @@ def self.without_modules(*modules)
Redirecting,
Rendering,
Renderers::All,
+ Serialization,
ConditionalGet,
RackDelegation,
SessionManagement,
@@ -1,5 +1,6 @@
require 'active_support/core_ext/class/attribute'
require 'active_support/core_ext/object/blank'
+require 'set'
module ActionController
# See <tt>Renderers.add</tt>
@@ -12,16 +13,13 @@ module Renderers
included do
class_attribute :_renderers
- self._renderers = {}.freeze
+ self._renderers = Set.new.freeze
end
module ClassMethods
def use_renderers(*args)
- new = _renderers.dup
- args.each do |key|
- new[key] = RENDERERS[key]
- end
- self._renderers = new.freeze
+ renderers = _renderers + args
+ self._renderers = renderers.freeze
end
alias use_renderer use_renderers
end
@@ -31,18 +29,18 @@ def render_to_body(options)
end
def _handle_render_options(options)
- _renderers.each do |name, value|
- if options.key?(name.to_sym)
+ _renderers.each do |name|
+ if options.key?(name)
_process_options(options)
- return send("_render_option_#{name}", options.delete(name.to_sym), options)
+ return send("_render_option_#{name}", options.delete(name), options)
end
end
nil
end
# Hash of available renderers, mapping a renderer name to its proc.
# Default keys are :json, :js, :xml.
- RENDERERS = {}
+ RENDERERS = Set.new
# Adds a new renderer to call within controller actions.
# A renderer is invoked by passing its name as an option to
@@ -79,7 +77,7 @@ def _handle_render_options(options)
# <tt>ActionController::MimeResponds#respond_with</tt>
def self.add(key, &block)
define_method("_render_option_#{key}", &block)
- RENDERERS[key] = block
+ RENDERERS << key.to_sym
end
module All
@@ -0,0 +1,51 @@
+module ActionController
+ # Action Controller Serialization
+ #
+ # Overrides render :json to check if the given object implements +active_model_serializer+
+ # as a method. If so, use the returned serializer instead of calling +to_json+ in the object.
+ #
+ # This module also provides a serialization_scope method that allows you to configure the
+ # +serialization_scope+ of the serializer. Most apps will likely set the +serialization_scope+
+ # to the current user:
+ #
+ # class ApplicationController < ActionController::Base
+ # serialization_scope :current_user
+ # end
+ #
+ # If you need more complex scope rules, you can simply override the serialization_scope:
+ #
+ # class ApplicationController < ActionController::Base
+ # private
+ #
+ # def serialization_scope
+ # current_user
+ # end
+ # end
+ #
+ module Serialization
+ extend ActiveSupport::Concern
+
+ include ActionController::Renderers
+
+ included do
+ class_attribute :_serialization_scope
+ end
+
+ def serialization_scope
+ send(_serialization_scope)
+ end
+
+ def _render_option_json(json, options)
+ if json.respond_to?(:active_model_serializer) && (serializer = json.active_model_serializer)
+ json = serializer.new(json, serialization_scope)
+ end
+ super
+ end
+
+ module ClassMethods
+ def serialization_scope(scope)
+ self._serialization_scope = scope
+ end
+ end
+ end
+end
@@ -15,9 +15,36 @@ def to_json(options = {})
end
end
+ class JsonSerializer
+ def initialize(object, scope)
+ @object, @scope = object, scope
+ end
+
+ def as_json(*)
+ { :object => @object.as_json, :scope => @scope.as_json }
+ end
+ end
+
+ class JsonSerializable
+ def initialize(skip=false)
+ @skip = skip
+ end
+
+ def active_model_serializer
+ JsonSerializer unless @skip
+ end
+
+ def as_json(*)
+ { :serializable_object => true }
+ end
+ end
+
class TestController < ActionController::Base
protect_from_forgery
+ serialization_scope :current_user
+ attr_reader :current_user
+
def self.controller_path
'test'
end
@@ -61,6 +88,16 @@ def render_json_with_extra_options
def render_json_without_options
render :json => JsonRenderable.new
end
+
+ def render_json_with_serializer
+ @current_user = Struct.new(:as_json).new(:current_user => true)
+ render :json => JsonSerializable.new
+ end
+
+ def render_json_with_serializer_api_but_without_serializer
+ @current_user = Struct.new(:as_json).new(:current_user => true)
+ render :json => JsonSerializable.new(true)
+ end
end
tests TestController
@@ -132,4 +169,15 @@ def test_render_json_calls_to_json_from_object
get :render_json_without_options
assert_equal '{"a":"b"}', @response.body
end
+
+ def test_render_json_with_serializer
+ get :render_json_with_serializer
+ assert_match '"scope":{"current_user":true}', @response.body
+ assert_match '"object":{"serializable_object":true}', @response.body
+ end
+
+ def test_render_json_with_serializer_api_but_without_serializer
+ get :render_json_with_serializer_api_but_without_serializer
+ assert_match '{"serializable_object":true}', @response.body
+ end
end
View
@@ -1,4 +1,16 @@
-* Added ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin*
+## Rails 3.2.0 (unreleased) ##
+
+* Add ActiveModel::Serializer that encapsulates an ActiveModel object serialization *José Valim*
+
+* Renamed (with a deprecation the following constants):
+
+ ActiveModel::Serialization => ActiveModel::Serializable
+ ActiveModel::Serializers::JSON => ActiveModel::Serializable::JSON
+ ActiveModel::Serializers::Xml => ActiveModel::Serializable::XML
+
+ *José Valim*
+
+* Add ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin*
* Add ability to define strict validation(with :strict => true option) that always raises exception when fails *Bogdan Gusiev*
@@ -29,6 +29,7 @@
module ActiveModel
extend ActiveSupport::Autoload
+ autoload :ArraySerializer, 'active_model/serializer'
autoload :AttributeMethods
autoload :BlockValidator, 'active_model/validator'
autoload :Callbacks
@@ -43,7 +44,9 @@ module ActiveModel
autoload :Observer, 'active_model/observing'
autoload :Observing
autoload :SecurePassword
+ autoload :Serializable
autoload :Serialization
+ autoload :Serializer
autoload :TestCase
autoload :Translation
autoload :Validations
@@ -0,0 +1,156 @@
+require 'active_support/core_ext/hash/except'
+require 'active_support/core_ext/hash/slice'
+require 'active_support/core_ext/array/wrap'
+require 'active_support/core_ext/string/inflections'
+
+module ActiveModel
+ # == Active Model Serializable
+ #
+ # Provides a basic serialization to a serializable_hash for your object.
+ #
+ # A minimal implementation could be:
+ #
+ # class Person
+ #
+ # include ActiveModel::Serializable
+ #
+ # attr_accessor :name
+ #
+ # def attributes
+ # {'name' => name}
+ # end
+ #
+ # end
+ #
+ # Which would provide you with:
+ #
+ # person = Person.new
+ # person.serializable_hash # => {"name"=>nil}
+ # person.name = "Bob"
+ # person.serializable_hash # => {"name"=>"Bob"}
+ #
+ # You need to declare some sort of attributes hash which contains the attributes
+ # you want to serialize and their current value.
+ #
+ # Most of the time though, you will want to include the JSON or XML
+ # serializations. Both of these modules automatically include the
+ # ActiveModel::Serialization module, so there is no need to explicitly
+ # include it.
+ #
+ # So a minimal implementation including XML and JSON would be:
+ #
+ # class Person
+ #
+ # include ActiveModel::Serializable::JSON
+ # include ActiveModel::Serializable::XML
+ #
+ # attr_accessor :name
+ #
+ # def attributes
+ # {'name' => name}
+ # end
+ #
+ # end
+ #
+ # Which would provide you with:
+ #
+ # person = Person.new
+ # person.serializable_hash # => {"name"=>nil}
+ # person.as_json # => {"name"=>nil}
+ # person.to_json # => "{\"name\":null}"
+ # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
+ #
+ # person.name = "Bob"
+ # person.serializable_hash # => {"name"=>"Bob"}
+ # person.as_json # => {"name"=>"Bob"}
+ # person.to_json # => "{\"name\":\"Bob\"}"
+ # person.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
+ #
+ # Valid options are <tt>:only</tt>, <tt>:except</tt> and <tt>:methods</tt> .
+ module Serializable
+ extend ActiveSupport::Concern
+
+ autoload :JSON, "active_model/serializable/json"
+ autoload :XML, "active_model/serializable/xml"
+
+ module ClassMethods #:nodoc:
+ def active_model_serializer
+ return @active_model_serializer if defined?(@active_model_serializer)
+ @active_model_serializer = "#{self.name}Serializer".safe_constantize
+ end
+ end
+
+ def serializable_hash(options = nil)
+ options ||= {}
+
+ attribute_names = attributes.keys.sort
+ if only = options[:only]
+ attribute_names &= Array.wrap(only).map(&:to_s)
+ elsif except = options[:except]
+ attribute_names -= Array.wrap(except).map(&:to_s)
+ end
+
+ hash = {}
+ attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
+
+ method_names = Array.wrap(options[:methods]).select { |n| respond_to?(n) }
+ method_names.each { |n| hash[n] = send(n) }
+
+ serializable_add_includes(options) do |association, records, opts|
+ hash[association] = if records.is_a?(Enumerable)
+ records.map { |a| a.serializable_hash(opts) }
+ else
+ records.serializable_hash(opts)
+ end
+ end
+
+ hash
+ end
+
+ # Returns a model serializer for this object considering its namespace.
+ def active_model_serializer
+ self.class.active_model_serializer
+ end
+
+ private
+
+ # Hook method defining how an attribute value should be retrieved for
+ # serialization. By default this is assumed to be an instance named after
+ # the attribute. Override this method in subclasses should you need to
+ # retrieve the value for a given attribute differently:
+ #
+ # class MyClass
+ # include ActiveModel::Validations
+ #
+ # def initialize(data = {})
+ # @data = data
+ # end
+ #
+ # def read_attribute_for_serialization(key)
+ # @data[key]
+ # end
+ # end
+ #
+ alias :read_attribute_for_serialization :send
+
+ # Add associations specified via the <tt>:include</tt> option.
+ #
+ # Expects a block that takes as arguments:
+ # +association+ - name of the association
+ # +records+ - the association record(s) to be serialized
+ # +opts+ - options for the association records
+ def serializable_add_includes(options = {}) #:nodoc:
+ return unless include = options[:include]
+
+ unless include.is_a?(Hash)
+ include = Hash[Array.wrap(include).map { |n| n.is_a?(Hash) ? n.to_a.first : [n, {}] }]
+ end
+
+ include.each do |association, opts|
+ if records = send(association)
+ yield association, records, opts
+ end
+ end
+ end
+ end
+end
Oops, something went wrong.

0 comments on commit fcacc69

Please sign in to comment.