From 5b2eb64ceb08cd005dc06b721935de5853971473 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 30 Nov 2011 18:38:28 +0100 Subject: [PATCH] Revert "Implement ArraySerializer and move old serialization API to a new namespace." This reverts commit 8896b4fdc8a543157cdf4dfc378607ebf6c10ab0. Conflicts: activemodel/lib/active_model.rb activemodel/lib/active_model/serializable.rb activemodel/lib/active_model/serializer.rb activemodel/test/cases/serializer_test.rb --- activemodel/CHANGELOG.md | 8 - activemodel/lib/active_model.rb | 1 - activemodel/lib/active_model/serializable.rb | 144 ---------- .../lib/active_model/serializable/json.rb | 108 -------- .../lib/active_model/serializable/xml.rb | 195 -------------- activemodel/lib/active_model/serialization.rb | 143 +++++++++- activemodel/lib/active_model/serializer.rb | 253 ------------------ .../lib/active_model/serializers/json.rb | 102 ++++++- .../lib/active_model/serializers/xml.rb | 191 ++++++++++++- ...alizable_test.rb => serialization_test.rb} | 4 +- .../json_serialization_test.rb} | 2 +- .../xml_serialization_test.rb} | 4 +- .../lib/active_record/serialization.rb | 2 +- .../serializers/xml_serializer.rb | 6 +- activeresource/lib/active_resource/base.rb | 4 +- .../lib/active_support/json/encoding.rb | 2 +- 16 files changed, 433 insertions(+), 736 deletions(-) delete mode 100644 activemodel/lib/active_model/serializable.rb delete mode 100644 activemodel/lib/active_model/serializable/json.rb delete mode 100644 activemodel/lib/active_model/serializable/xml.rb delete mode 100644 activemodel/lib/active_model/serializer.rb rename activemodel/test/cases/{serializable_test.rb => serialization_test.rb} (98%) rename activemodel/test/cases/{serializable/json_test.rb => serializers/json_serialization_test.rb} (99%) rename activemodel/test/cases/{serializable/xml_test.rb => serializers/xml_serialization_test.rb} (98%) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 95b682a4d2..bd9ed996fe 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -5,14 +5,6 @@ *Jon Leighton* -* 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* diff --git a/activemodel/lib/active_model.rb b/activemodel/lib/active_model.rb index 7ea04344f0..d0e2a6f39c 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -43,7 +43,6 @@ module ActiveModel autoload :Observer, 'active_model/observing' autoload :Observing autoload :SecurePassword - autoload :Serializable autoload :Serialization autoload :TestCase autoload :Translation diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb deleted file mode 100644 index 86770a25e4..0000000000 --- a/activemodel/lib/active_model/serializable.rb +++ /dev/null @@ -1,144 +0,0 @@ -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 # => "\n {"name"=>"Bob"} - # person.as_json # => {"name"=>"Bob"} - # person.to_json # => "{\"name\":\"Bob\"}" - # person.to_xml # => "\n:only, :except and :methods . - module Serializable - extend ActiveSupport::Concern - - autoload :JSON, "active_model/serializable/json" - autoload :XML, "active_model/serializable/xml" - - 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 - - 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 :include 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 diff --git a/activemodel/lib/active_model/serializable/json.rb b/activemodel/lib/active_model/serializable/json.rb deleted file mode 100644 index 79173929e4..0000000000 --- a/activemodel/lib/active_model/serializable/json.rb +++ /dev/null @@ -1,108 +0,0 @@ -require 'active_support/json' -require 'active_support/core_ext/class/attribute' - -module ActiveModel - # == Active Model Serializable as JSON - module Serializable - module JSON - extend ActiveSupport::Concern - include ActiveModel::Serializable - - included do - extend ActiveModel::Naming - - class_attribute :include_root_in_json - self.include_root_in_json = true - end - - # Returns a hash representing the model. Some configuration can be - # passed through +options+. - # - # The option include_root_in_json controls the top-level behavior - # of +as_json+. If true (the default) +as_json+ will emit a single root - # node named after the object's type. For example: - # - # user = User.find(1) - # user.as_json - # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} } - # - # ActiveRecord::Base.include_root_in_json = false - # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # This behavior can also be achieved by setting the :root option to +false+ as in: - # - # user = User.find(1) - # user.as_json(root: false) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The remainder of the examples in this section assume include_root_in_json is set to - # false. - # - # Without any +options+, the returned Hash will include all the model's - # attributes. For example: - # - # user = User.find(1) - # user.as_json - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true} - # - # The :only and :except options can be used to limit the attributes - # included, and work similar to the +attributes+ method. For example: - # - # user.as_json(:only => [ :id, :name ]) - # # => {"id": 1, "name": "Konata Izumi"} - # - # user.as_json(:except => [ :id, :created_at, :age ]) - # # => {"name": "Konata Izumi", "awesome": true} - # - # To include the result of some method calls on the model use :methods: - # - # user.as_json(:methods => :permalink) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "permalink": "1-konata-izumi"} - # - # To include associations use :include: - # - # user.as_json(:include => :posts) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, - # {"id": 2, author_id: 1, "title": "So I was thinking"}]} - # - # Second level and higher order associations work as well: - # - # user.as_json(:include => { :posts => { - # :include => { :comments => { - # :only => :body } }, - # :only => :title } }) - # # => {"id": 1, "name": "Konata Izumi", "age": 16, - # "created_at": "2006/08/01", "awesome": true, - # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], - # "title": "Welcome to the weblog"}, - # {"comments": [{"body": "Don't think too hard"}], - # "title": "So I was thinking"}]} - def as_json(options = nil) - root = include_root_in_json - root = options[:root] if options.try(:key?, :root) - if root - root = self.class.model_name.element if root == true - { root => serializable_hash(options) } - else - serializable_hash(options) - end - end - - def from_json(json, include_root=include_root_in_json) - hash = ActiveSupport::JSON.decode(json) - hash = hash.values.first if include_root - self.attributes = hash - self - end - end - end -end diff --git a/activemodel/lib/active_model/serializable/xml.rb b/activemodel/lib/active_model/serializable/xml.rb deleted file mode 100644 index d11cee9b42..0000000000 --- a/activemodel/lib/active_model/serializable/xml.rb +++ /dev/null @@ -1,195 +0,0 @@ -require 'active_support/core_ext/array/wrap' -require 'active_support/core_ext/class/attribute_accessors' -require 'active_support/core_ext/array/conversions' -require 'active_support/core_ext/hash/conversions' -require 'active_support/core_ext/hash/slice' - -module ActiveModel - # == Active Model Serializable as XML - module Serializable - module XML - extend ActiveSupport::Concern - include ActiveModel::Serializable - - class Serializer #:nodoc: - class Attribute #:nodoc: - attr_reader :name, :value, :type - - def initialize(name, serializable, value) - @name, @serializable = name, serializable - value = value.in_time_zone if value.respond_to?(:in_time_zone) - @value = value - @type = compute_type - end - - def decorations - decorations = {} - decorations[:encoding] = 'base64' if type == :binary - decorations[:type] = (type == :string) ? nil : type - decorations[:nil] = true if value.nil? - decorations - end - - protected - - def compute_type - return if value.nil? - type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] - type ||= :string if value.respond_to?(:to_str) - type ||= :yaml - type - end - end - - class MethodAttribute < Attribute #:nodoc: - end - - attr_reader :options - - def initialize(serializable, options = nil) - @serializable = serializable - @options = options ? options.dup : {} - end - - def serializable_hash - @serializable.serializable_hash(@options.except(:include)) - end - - def serializable_collection - methods = Array.wrap(options[:methods]).map(&:to_s) - serializable_hash.map do |name, value| - name = name.to_s - if methods.include?(name) - self.class::MethodAttribute.new(name, @serializable, value) - else - self.class::Attribute.new(name, @serializable, value) - end - end - end - - def serialize - require 'builder' unless defined? ::Builder - - options[:indent] ||= 2 - options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) - - @builder = options[:builder] - @builder.instruct! unless options[:skip_instruct] - - root = (options[:root] || @serializable.class.model_name.element).to_s - root = ActiveSupport::XmlMini.rename_key(root, options) - - args = [root] - args << {:xmlns => options[:namespace]} if options[:namespace] - args << {:type => options[:type]} if options[:type] && !options[:skip_types] - - @builder.tag!(*args) do - add_attributes_and_methods - add_includes - add_extra_behavior - add_procs - yield @builder if block_given? - end - end - - private - - def add_extra_behavior - end - - def add_attributes_and_methods - serializable_collection.each do |attribute| - key = ActiveSupport::XmlMini.rename_key(attribute.name, options) - ActiveSupport::XmlMini.to_tag(key, attribute.value, - options.merge(attribute.decorations)) - end - end - - def add_includes - @serializable.send(:serializable_add_includes, options) do |association, records, opts| - add_associations(association, records, opts) - end - end - - # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. - def add_associations(association, records, opts) - merged_options = opts.merge(options.slice(:builder, :indent)) - merged_options[:skip_instruct] = true - - if records.is_a?(Enumerable) - tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) - type = options[:skip_types] ? { } : {:type => "array"} - association_name = association.to_s.singularize - merged_options[:root] = association_name - - if records.empty? - @builder.tag!(tag, type) - else - @builder.tag!(tag, type) do - records.each do |record| - if options[:skip_types] - record_type = {} - else - record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name - record_type = {:type => record_class} - end - - record.to_xml merged_options.merge(record_type) - end - end - end - else - merged_options[:root] = association.to_s - records.to_xml(merged_options) - end - end - - def add_procs - if procs = options.delete(:procs) - Array.wrap(procs).each do |proc| - if proc.arity == 1 - proc.call(options) - else - proc.call(options, @serializable) - end - end - end - end - end - - # Returns XML representing the model. Configuration can be - # passed through +options+. - # - # Without any +options+, the returned XML string will include all the model's - # attributes. For example: - # - # user = User.find(1) - # user.to_xml - # - # - # - # 1 - # David - # 16 - # 2011-01-30T22:29:23Z - # - # - # The :only and :except options can be used to limit the attributes - # included, and work similar to the +attributes+ method. - # - # To include the result of some method calls on the model use :methods. - # - # To include associations use :include. - # - # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end - - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self - end - end - end -end diff --git a/activemodel/lib/active_model/serialization.rb b/activemodel/lib/active_model/serialization.rb index 439302c632..a4b58ab456 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,10 +1,139 @@ -module ActiveModel - module Serialization - extend ActiveSupport::Concern - include ActiveModel::Serializable +require 'active_support/core_ext/hash/except' +require 'active_support/core_ext/hash/slice' +require 'active_support/core_ext/array/wrap' - included do - ActiveSupport::Deprecation.warn "ActiveModel::Serialization is deprecated in favor of ActiveModel::Serializable" + +module ActiveModel + # == Active Model Serialization + # + # Provides a basic serialization to a serializable_hash for your object. + # + # A minimal implementation could be: + # + # class Person + # + # include ActiveModel::Serialization + # + # 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::Serializers::JSON + # include ActiveModel::Serializers::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 # => "\n {"name"=>"Bob"} + # person.as_json # => {"name"=>"Bob"} + # person.to_json # => "{\"name\":\"Bob\"}" + # person.to_xml # => "\n:only, :except and :methods . + module Serialization + 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 + + 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 :include 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 \ No newline at end of file +end diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb deleted file mode 100644 index 0e23df2f2b..0000000000 --- a/activemodel/lib/active_model/serializer.rb +++ /dev/null @@ -1,253 +0,0 @@ -require "active_support/core_ext/class/attribute" -require "active_support/core_ext/string/inflections" -require "active_support/core_ext/module/anonymous" -require "set" - -module ActiveModel - # Active Model Array Serializer - # - # It serializes an array checking if each element that implements - # the +active_model_serializer+ method passing down the current scope. - class ArraySerializer - attr_reader :object, :scope - - def initialize(object, scope) - @object, @scope = object, scope - end - - def serializable_array - @object.map do |item| - if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer) - serializer.new(item, scope) - else - item - end - end - end - - def as_json(*args) - serializable_array.as_json(*args) - end - end - - # Active Model Serializer - # - # Provides a basic serializer implementation that allows you to easily - # control how a given object is going to be serialized. On initialization, - # it expects to object as arguments, a resource and a scope. For example, - # one may do in a controller: - # - # PostSerializer.new(@post, current_user).to_json - # - # The object to be serialized is the +@post+ and the scope is +current_user+. - # - # We use the scope to check if a given attribute should be serialized or not. - # For example, some attributes maybe only be returned if +current_user+ is the - # author of the post: - # - # class PostSerializer < ActiveModel::Serializer - # attributes :title, :body - # has_many :comments - # - # private - # - # def attributes - # hash = super - # hash.merge!(:email => post.email) if author? - # hash - # end - # - # def author? - # post.author == scope - # end - # end - # - class Serializer - module Associations #:nodoc: - class Config < Struct.new(:name, :options) #:nodoc: - def serializer - options[:serializer] - end - end - - class HasMany < Config #:nodoc: - def serialize(collection, scope) - collection.map do |item| - serializer.new(item, scope).serializable_hash - end - end - - def serialize_ids(collection, scope) - # use named scopes if they are present - # return collection.ids if collection.respond_to?(:ids) - - collection.map do |item| - item.read_attribute_for_serialization(:id) - end - end - end - - class HasOne < Config #:nodoc: - def serialize(object, scope) - object && serializer.new(object, scope).serializable_hash - end - - def serialize_ids(object, scope) - object && object.read_attribute_for_serialization(:id) - end - end - end - - class_attribute :_attributes - self._attributes = Set.new - - class_attribute :_associations - self._associations = [] - - class_attribute :_root - class_attribute :_embed - self._embed = :objects - class_attribute :_root_embed - - class << self - # Define attributes to be used in the serialization. - def attributes(*attrs) - self._attributes += attrs - end - - def associate(klass, attrs) #:nodoc: - options = attrs.extract_options! - self._associations += attrs.map do |attr| - unless method_defined?(attr) - class_eval "def #{attr}() object.#{attr} end", __FILE__, __LINE__ - end - - options[:serializer] ||= const_get("#{attr.to_s.camelize}Serializer") - klass.new(attr, options) - end - end - - # Defines an association in the object should be rendered. - # - # The serializer object should implement the association name - # as a method which should return an array when invoked. If a method - # with the association name does not exist, the association name is - # dispatched to the serialized object. - def has_many(*attrs) - associate(Associations::HasMany, attrs) - end - - # Defines an association in the object should be rendered. - # - # The serializer object should implement the association name - # as a method which should return an object when invoked. If a method - # with the association name does not exist, the association name is - # dispatched to the serialized object. - def has_one(*attrs) - associate(Associations::HasOne, attrs) - end - - # Define how associations should be embedded. - # - # embed :objects # Embed associations as full objects - # embed :ids # Embed only the association ids - # embed :ids, :include => true # Embed the association ids and include objects in the root - # - def embed(type, options={}) - self._embed = type - self._root_embed = true if options[:include] - end - - # Defines the root used on serialization. If false, disables the root. - def root(name) - self._root = name - end - - def inherited(klass) #:nodoc: - return if klass.anonymous? - - name = klass.name.demodulize.underscore.sub(/_serializer$/, '') - - klass.class_eval do - alias_method name.to_sym, :object - root name.to_sym unless self._root == false - end - end - end - - attr_reader :object, :scope - - def initialize(object, scope) - @object, @scope = object, scope - end - - # Returns a json representation of the serializable - # object including the root. - def as_json(*) - if _root - hash = { _root => serializable_hash } - hash.merge!(associations) if _root_embed - hash - else - serializable_hash - end - end - - # Returns a hash representation of the serializable - # object without the root. - def serializable_hash - if _embed == :ids - attributes.merge(association_ids) - elsif _embed == :objects - attributes.merge(associations) - else - attributes - end - end - - # Returns a hash representation of the serializable - # object associations. - def associations - hash = {} - - _associations.each do |association| - associated_object = send(association.name) - hash[association.name] = association.serialize(associated_object, scope) - end - - hash - end - - # Returns a hash representation of the serializable - # object associations ids. - def association_ids - hash = {} - - _associations.each do |association| - associated_object = send(association.name) - hash[association.name] = association.serialize_ids(associated_object, scope) - end - - hash - end - - # Returns a hash representation of the serializable - # object attributes. - def attributes - hash = {} - - _attributes.each do |name| - hash[name] = @object.read_attribute_for_serialization(name) - end - - hash - end - end -end - -class Array - # Array uses ActiveModel::ArraySerializer. - def active_model_serializer - ActiveModel::ArraySerializer - end -end \ No newline at end of file diff --git a/activemodel/lib/active_model/serializers/json.rb b/activemodel/lib/active_model/serializers/json.rb index 9efd7c5f69..c845440120 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,12 +1,108 @@ +require 'active_support/json' +require 'active_support/core_ext/class/attribute' + module ActiveModel + # == Active Model JSON Serializer module Serializers module JSON extend ActiveSupport::Concern - include ActiveModel::Serializable::JSON + include ActiveModel::Serialization included do - ActiveSupport::Deprecation.warn "ActiveModel::Serializers::JSON is deprecated in favor of ActiveModel::Serializable::JSON" + extend ActiveModel::Naming + + class_attribute :include_root_in_json + self.include_root_in_json = true + end + + # Returns a hash representing the model. Some configuration can be + # passed through +options+. + # + # The option include_root_in_json controls the top-level behavior + # of +as_json+. If true (the default) +as_json+ will emit a single root + # node named after the object's type. For example: + # + # user = User.find(1) + # user.as_json + # # => { "user": {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} } + # + # ActiveRecord::Base.include_root_in_json = false + # user.as_json + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # This behavior can also be achieved by setting the :root option to +false+ as in: + # + # user = User.find(1) + # user.as_json(root: false) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # The remainder of the examples in this section assume include_root_in_json is set to + # false. + # + # Without any +options+, the returned Hash will include all the model's + # attributes. For example: + # + # user = User.find(1) + # user.as_json + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true} + # + # The :only and :except options can be used to limit the attributes + # included, and work similar to the +attributes+ method. For example: + # + # user.as_json(:only => [ :id, :name ]) + # # => {"id": 1, "name": "Konata Izumi"} + # + # user.as_json(:except => [ :id, :created_at, :age ]) + # # => {"name": "Konata Izumi", "awesome": true} + # + # To include the result of some method calls on the model use :methods: + # + # user.as_json(:methods => :permalink) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true, + # "permalink": "1-konata-izumi"} + # + # To include associations use :include: + # + # user.as_json(:include => :posts) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true, + # "posts": [{"id": 1, "author_id": 1, "title": "Welcome to the weblog"}, + # {"id": 2, author_id: 1, "title": "So I was thinking"}]} + # + # Second level and higher order associations work as well: + # + # user.as_json(:include => { :posts => { + # :include => { :comments => { + # :only => :body } }, + # :only => :title } }) + # # => {"id": 1, "name": "Konata Izumi", "age": 16, + # "created_at": "2006/08/01", "awesome": true, + # "posts": [{"comments": [{"body": "1st post!"}, {"body": "Second!"}], + # "title": "Welcome to the weblog"}, + # {"comments": [{"body": "Don't think too hard"}], + # "title": "So I was thinking"}]} + def as_json(options = nil) + root = include_root_in_json + root = options[:root] if options.try(:key?, :root) + if root + root = self.class.model_name.element if root == true + { root => serializable_hash(options) } + else + serializable_hash(options) + end + end + + def from_json(json, include_root=include_root_in_json) + hash = ActiveSupport::JSON.decode(json) + hash = hash.values.first if include_root + self.attributes = hash + self end end end -end \ No newline at end of file +end diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index 620390da6b..d61d9d7119 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,14 +1,195 @@ +require 'active_support/core_ext/array/wrap' +require 'active_support/core_ext/class/attribute_accessors' +require 'active_support/core_ext/array/conversions' +require 'active_support/core_ext/hash/conversions' +require 'active_support/core_ext/hash/slice' + module ActiveModel + # == Active Model XML Serializer module Serializers module Xml extend ActiveSupport::Concern - include ActiveModel::Serializable::XML + include ActiveModel::Serialization - Serializer = ActiveModel::Serializable::XML::Serializer + class Serializer #:nodoc: + class Attribute #:nodoc: + attr_reader :name, :value, :type - included do - ActiveSupport::Deprecation.warn "ActiveModel::Serializers::Xml is deprecated in favor of ActiveModel::Serializable::XML" + def initialize(name, serializable, value) + @name, @serializable = name, serializable + value = value.in_time_zone if value.respond_to?(:in_time_zone) + @value = value + @type = compute_type + end + + def decorations + decorations = {} + decorations[:encoding] = 'base64' if type == :binary + decorations[:type] = (type == :string) ? nil : type + decorations[:nil] = true if value.nil? + decorations + end + + protected + + def compute_type + return if value.nil? + type = ActiveSupport::XmlMini::TYPE_NAMES[value.class.name] + type ||= :string if value.respond_to?(:to_str) + type ||= :yaml + type + end + end + + class MethodAttribute < Attribute #:nodoc: + end + + attr_reader :options + + def initialize(serializable, options = nil) + @serializable = serializable + @options = options ? options.dup : {} + end + + def serializable_hash + @serializable.serializable_hash(@options.except(:include)) + end + + def serializable_collection + methods = Array.wrap(options[:methods]).map(&:to_s) + serializable_hash.map do |name, value| + name = name.to_s + if methods.include?(name) + self.class::MethodAttribute.new(name, @serializable, value) + else + self.class::Attribute.new(name, @serializable, value) + end + end + end + + def serialize + require 'builder' unless defined? ::Builder + + options[:indent] ||= 2 + options[:builder] ||= ::Builder::XmlMarkup.new(:indent => options[:indent]) + + @builder = options[:builder] + @builder.instruct! unless options[:skip_instruct] + + root = (options[:root] || @serializable.class.model_name.element).to_s + root = ActiveSupport::XmlMini.rename_key(root, options) + + args = [root] + args << {:xmlns => options[:namespace]} if options[:namespace] + args << {:type => options[:type]} if options[:type] && !options[:skip_types] + + @builder.tag!(*args) do + add_attributes_and_methods + add_includes + add_extra_behavior + add_procs + yield @builder if block_given? + end + end + + private + + def add_extra_behavior + end + + def add_attributes_and_methods + serializable_collection.each do |attribute| + key = ActiveSupport::XmlMini.rename_key(attribute.name, options) + ActiveSupport::XmlMini.to_tag(key, attribute.value, + options.merge(attribute.decorations)) + end + end + + def add_includes + @serializable.send(:serializable_add_includes, options) do |association, records, opts| + add_associations(association, records, opts) + end + end + + # TODO This can likely be cleaned up to simple use ActiveSupport::XmlMini.to_tag as well. + def add_associations(association, records, opts) + merged_options = opts.merge(options.slice(:builder, :indent)) + merged_options[:skip_instruct] = true + + if records.is_a?(Enumerable) + tag = ActiveSupport::XmlMini.rename_key(association.to_s, options) + type = options[:skip_types] ? { } : {:type => "array"} + association_name = association.to_s.singularize + merged_options[:root] = association_name + + if records.empty? + @builder.tag!(tag, type) + else + @builder.tag!(tag, type) do + records.each do |record| + if options[:skip_types] + record_type = {} + else + record_class = (record.class.to_s.underscore == association_name) ? nil : record.class.name + record_type = {:type => record_class} + end + + record.to_xml merged_options.merge(record_type) + end + end + end + else + merged_options[:root] = association.to_s + records.to_xml(merged_options) + end + end + + def add_procs + if procs = options.delete(:procs) + Array.wrap(procs).each do |proc| + if proc.arity == 1 + proc.call(options) + else + proc.call(options, @serializable) + end + end + end + end + end + + # Returns XML representing the model. Configuration can be + # passed through +options+. + # + # Without any +options+, the returned XML string will include all the model's + # attributes. For example: + # + # user = User.find(1) + # user.to_xml + # + # + # + # 1 + # David + # 16 + # 2011-01-30T22:29:23Z + # + # + # The :only and :except options can be used to limit the attributes + # included, and work similar to the +attributes+ method. + # + # To include the result of some method calls on the model use :methods. + # + # To include associations use :include. + # + # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. + def to_xml(options = {}, &block) + Serializer.new(self, options).serialize(&block) + end + + def from_xml(xml) + self.attributes = Hash.from_xml(xml).values.first + self end end end -end \ No newline at end of file +end diff --git a/activemodel/test/cases/serializable_test.rb b/activemodel/test/cases/serialization_test.rb similarity index 98% rename from activemodel/test/cases/serializable_test.rb rename to activemodel/test/cases/serialization_test.rb index 46ee372c6f..b8dad9d51f 100644 --- a/activemodel/test/cases/serializable_test.rb +++ b/activemodel/test/cases/serialization_test.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/object/instance_variables' class SerializationTest < ActiveModel::TestCase class User - include ActiveModel::Serializable + include ActiveModel::Serialization attr_accessor :name, :email, :gender, :address, :friends @@ -22,7 +22,7 @@ class SerializationTest < ActiveModel::TestCase end class Address - include ActiveModel::Serializable + include ActiveModel::Serialization attr_accessor :street, :city, :state, :zip diff --git a/activemodel/test/cases/serializable/json_test.rb b/activemodel/test/cases/serializers/json_serialization_test.rb similarity index 99% rename from activemodel/test/cases/serializable/json_test.rb rename to activemodel/test/cases/serializers/json_serialization_test.rb index e5364d9858..4ac5fb1779 100644 --- a/activemodel/test/cases/serializable/json_test.rb +++ b/activemodel/test/cases/serializers/json_serialization_test.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/object/instance_variables' class Contact extend ActiveModel::Naming - include ActiveModel::Serializable::JSON + include ActiveModel::Serializers::JSON include ActiveModel::Validations def attributes=(hash) diff --git a/activemodel/test/cases/serializable/xml_test.rb b/activemodel/test/cases/serializers/xml_serialization_test.rb similarity index 98% rename from activemodel/test/cases/serializable/xml_test.rb rename to activemodel/test/cases/serializers/xml_serialization_test.rb index 834c4de1e1..38aecf51ff 100644 --- a/activemodel/test/cases/serializable/xml_test.rb +++ b/activemodel/test/cases/serializers/xml_serialization_test.rb @@ -5,7 +5,7 @@ require 'ostruct' class Contact extend ActiveModel::Naming - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml attr_accessor :address, :friends @@ -26,7 +26,7 @@ end class Address extend ActiveModel::Naming - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml attr_accessor :street, :city, :state, :zip diff --git a/activerecord/lib/active_record/serialization.rb b/activerecord/lib/active_record/serialization.rb index c23514c465..5ad40d8cd9 100644 --- a/activerecord/lib/active_record/serialization.rb +++ b/activerecord/lib/active_record/serialization.rb @@ -2,7 +2,7 @@ module ActiveRecord #:nodoc: # = Active Record Serialization module Serialization extend ActiveSupport::Concern - include ActiveModel::Serializable::JSON + include ActiveModel::Serializers::JSON def serializable_hash(options = nil) options = options.try(:clone) || {} diff --git a/activerecord/lib/active_record/serializers/xml_serializer.rb b/activerecord/lib/active_record/serializers/xml_serializer.rb index 2da836ef0c..0e7f57aa43 100644 --- a/activerecord/lib/active_record/serializers/xml_serializer.rb +++ b/activerecord/lib/active_record/serializers/xml_serializer.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/hash/conversions' module ActiveRecord #:nodoc: module Serialization - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::Xml # Builds an XML document to represent the model. Some configuration is # available through +options+. However more complicated cases should @@ -176,13 +176,13 @@ module ActiveRecord #:nodoc: end end - class XmlSerializer < ActiveModel::Serializable::XML::Serializer #:nodoc: + class XmlSerializer < ActiveModel::Serializers::Xml::Serializer #:nodoc: def initialize(*args) super options[:except] = Array.wrap(options[:except]) | Array.wrap(@serializable.class.inheritance_column) end - class Attribute < ActiveModel::Serializable::XML::Serializer::Attribute #:nodoc: + class Attribute < ActiveModel::Serializers::Xml::Serializer::Attribute #:nodoc: def compute_type klass = @serializable.class type = if klass.serialized_attributes.key?(name) diff --git a/activeresource/lib/active_resource/base.rb b/activeresource/lib/active_resource/base.rb index 93991ab6b3..10cc727bd9 100644 --- a/activeresource/lib/active_resource/base.rb +++ b/activeresource/lib/active_resource/base.rb @@ -1477,7 +1477,7 @@ module ActiveResource extend ActiveModel::Naming include CustomMethods, Observing, Validations include ActiveModel::Conversion - include ActiveModel::Serializable::JSON - include ActiveModel::Serializable::XML + include ActiveModel::Serializers::JSON + include ActiveModel::Serializers::Xml end end diff --git a/activesupport/lib/active_support/json/encoding.rb b/activesupport/lib/active_support/json/encoding.rb index b0fd3a3c0e..925fa2a2c7 100644 --- a/activesupport/lib/active_support/json/encoding.rb +++ b/activesupport/lib/active_support/json/encoding.rb @@ -281,4 +281,4 @@ class DateTime strftime('%Y/%m/%d %H:%M:%S %z') end end -end +end \ No newline at end of file