diff options
Diffstat (limited to 'activemodel')
16 files changed, 796 insertions, 447 deletions
diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 71a737caff..caea0b86bd 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,4 +1,14 @@ -* Added ActiveModel::Errors#added? to check if a specific error has been added *Martin Svalin* +## Rails 3.2.0 (unreleased) ## + +* 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 d0e2a6f39c..7ea04344f0 100644 --- a/activemodel/lib/active_model.rb +++ b/activemodel/lib/active_model.rb @@ -43,6 +43,7 @@ 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/callbacks.rb b/activemodel/lib/active_model/callbacks.rb index 37d0c9a0b9..0621a175bd 100644 --- a/activemodel/lib/active_model/callbacks.rb +++ b/activemodel/lib/active_model/callbacks.rb @@ -41,7 +41,7 @@ module ActiveModel # You can choose not to have all three callbacks by passing a hash to the # define_model_callbacks method. # - # define_model_callbacks :create, :only => :after, :before + # define_model_callbacks :create, :only => [:after, :before] # # Would only create the after_create and before_create callback methods in your # class. diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index f16459ede2..953d24a3b2 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -12,19 +12,22 @@ module ActiveModel def initialize(klass, namespace = nil, name = nil) name ||= klass.name + + raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if name.blank? + super(name) - @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace - @klass = klass - @singular = _singularize(self).freeze - @plural = ActiveSupport::Inflector.pluralize(@singular).freeze - @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze - @human = ActiveSupport::Inflector.humanize(@element).freeze - @collection = ActiveSupport::Inflector.tableize(self).freeze + @unnamespaced = self.sub(/^#{namespace.name}::/, '') if namespace + @klass = klass + @singular = _singularize(self).freeze + @plural = ActiveSupport::Inflector.pluralize(@singular).freeze + @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(self)).freeze + @human = ActiveSupport::Inflector.humanize(@element).freeze + @collection = ActiveSupport::Inflector.tableize(self).freeze @partial_path = "#{@collection}/#{@element}".freeze - @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze - @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze - @i18n_key = self.underscore.to_sym + @param_key = (namespace ? _singularize(@unnamespaced) : @singular).freeze + @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural).freeze + @i18n_key = self.underscore.to_sym end # Transform the model name into a more humane format, using I18n. By default, @@ -79,7 +82,9 @@ module ActiveModel # used to retrieve all kinds of naming-related information. def model_name @_model_name ||= begin - namespace = self.parents.detect { |n| n.respond_to?(:_railtie) } + namespace = self.parents.detect do |n| + n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming? + end ActiveModel::Name.new(self, namespace) end end diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb new file mode 100644 index 0000000000..86770a25e4 --- /dev/null +++ b/activemodel/lib/active_model/serializable.rb @@ -0,0 +1,144 @@ +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" + + 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 <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 diff --git a/activemodel/lib/active_model/serializable/json.rb b/activemodel/lib/active_model/serializable/json.rb new file mode 100644 index 0000000000..79173929e4 --- /dev/null +++ b/activemodel/lib/active_model/serializable/json.rb @@ -0,0 +1,108 @@ +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 <tt>include_root_in_json</tt> 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 <tt>:root</tt> 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 + # <tt>false</tt>. + # + # 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 <tt>:only</tt> and <tt>:except</tt> 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 <tt>:methods</tt>: + # + # 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 <tt>:include</tt>: + # + # 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 new file mode 100644 index 0000000000..d11cee9b42 --- /dev/null +++ b/activemodel/lib/active_model/serializable/xml.rb @@ -0,0 +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 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 + # + # <?xml version="1.0" encoding="UTF-8"?> + # <user> + # <id type="integer">1</id> + # <name>David</name> + # <age type="integer">16</age> + # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> + # </user> + # + # The <tt>:only</tt> and <tt>:except</tt> 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 <tt>:methods</tt>. + # + # To include associations use <tt>:include</tt>. + # + # 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 a4b58ab456..439302c632 100644 --- a/activemodel/lib/active_model/serialization.rb +++ b/activemodel/lib/active_model/serialization.rb @@ -1,139 +1,10 @@ -require 'active_support/core_ext/hash/except' -require 'active_support/core_ext/hash/slice' -require 'active_support/core_ext/array/wrap' - - 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 # => "<?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 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 + extend ActiveSupport::Concern + include ActiveModel::Serializable - hash + included do + ActiveSupport::Deprecation.warn "ActiveModel::Serialization is deprecated in favor of ActiveModel::Serializable" 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 +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb new file mode 100644 index 0000000000..0e23df2f2b --- /dev/null +++ b/activemodel/lib/active_model/serializer.rb @@ -0,0 +1,253 @@ +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 c845440120..9efd7c5f69 100644 --- a/activemodel/lib/active_model/serializers/json.rb +++ b/activemodel/lib/active_model/serializers/json.rb @@ -1,108 +1,12 @@ -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::Serialization + include ActiveModel::Serializable::JSON 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 <tt>include_root_in_json</tt> 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 <tt>:root</tt> 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 - # <tt>false</tt>. - # - # 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 <tt>:only</tt> and <tt>:except</tt> 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 <tt>:methods</tt>: - # - # 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 <tt>:include</tt>: - # - # 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 + ActiveSupport::Deprecation.warn "ActiveModel::Serializers::JSON is deprecated in favor of ActiveModel::Serializable::JSON" end end end -end +end
\ No newline at end of file diff --git a/activemodel/lib/active_model/serializers/xml.rb b/activemodel/lib/active_model/serializers/xml.rb index d61d9d7119..620390da6b 100644 --- a/activemodel/lib/active_model/serializers/xml.rb +++ b/activemodel/lib/active_model/serializers/xml.rb @@ -1,195 +1,14 @@ -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::Serialization - - 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] + include ActiveModel::Serializable::XML - 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 - # - # <?xml version="1.0" encoding="UTF-8"?> - # <user> - # <id type="integer">1</id> - # <name>David</name> - # <age type="integer">16</age> - # <created-at type="datetime">2011-01-30T22:29:23Z</created-at> - # </user> - # - # The <tt>:only</tt> and <tt>:except</tt> 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 <tt>:methods</tt>. - # - # To include associations use <tt>:include</tt>. - # - # For further documentation see activerecord/lib/active_record/serializers/xml_serializer.xml. - def to_xml(options = {}, &block) - Serializer.new(self, options).serialize(&block) - end + Serializer = ActiveModel::Serializable::XML::Serializer - def from_xml(xml) - self.attributes = Hash.from_xml(xml).values.first - self + included do + ActiveSupport::Deprecation.warn "ActiveModel::Serializers::Xml is deprecated in favor of ActiveModel::Serializable::XML" end end end -end +end
\ No newline at end of file diff --git a/activemodel/test/cases/naming_test.rb b/activemodel/test/cases/naming_test.rb index 5f943729dd..e8db73ba52 100644 --- a/activemodel/test/cases/naming_test.rb +++ b/activemodel/test/cases/naming_test.rb @@ -74,10 +74,6 @@ class NamingWithNamespacedModelInIsolatedNamespaceTest < ActiveModel::TestCase def test_param_key assert_equal 'post', @model_name.param_key end - - def test_recognizing_namespace - assert_equal 'Post', Blog::Post.model_name.instance_variable_get("@unnamespaced") - end end class NamingWithNamespacedModelInSharedNamespaceTest < ActiveModel::TestCase @@ -160,6 +156,40 @@ class NamingWithSuppliedModelNameTest < ActiveModel::TestCase end end +class NamingUsingRelativeModelNameTest < ActiveModel::TestCase + def setup + @model_name = Blog::Post.model_name + end + + def test_singular + assert_equal 'blog_post', @model_name.singular + end + + def test_plural + assert_equal 'blog_posts', @model_name.plural + end + + def test_element + assert_equal 'post', @model_name.element + end + + def test_collection + assert_equal 'blog/posts', @model_name.collection + end + + def test_human + assert_equal 'Post', @model_name.human + end + + def test_route_key + assert_equal 'posts', @model_name.route_key + end + + def test_param_key + assert_equal 'post', @model_name.param_key + end +end + class NamingHelpersTest < Test::Unit::TestCase def setup @klass = Contact @@ -217,3 +247,16 @@ class NamingHelpersTest < Test::Unit::TestCase ActiveModel::Naming.send(method, *args) end end + +class NameWithAnonymousClassTest < Test::Unit::TestCase + def test_anonymous_class_without_name_argument + assert_raises(ArgumentError) do + ActiveModel::Name.new(Class.new) + end + end + + def test_anonymous_class_with_name_argument + model_name = ActiveModel::Name.new(Class.new, nil, "Anonymous") + assert_equal "Anonymous", model_name + end +end diff --git a/activemodel/test/cases/serializers/json_serialization_test.rb b/activemodel/test/cases/serializable/json_test.rb index a754d610b9..ad5b04091e 100644 --- a/activemodel/test/cases/serializers/json_serialization_test.rb +++ b/activemodel/test/cases/serializable/json_test.rb @@ -5,7 +5,7 @@ require 'active_support/core_ext/object/instance_variables' class Contact extend ActiveModel::Naming - include ActiveModel::Serializers::JSON + include ActiveModel::Serializable::JSON include ActiveModel::Validations def attributes=(hash) diff --git a/activemodel/test/cases/serializers/xml_serialization_test.rb b/activemodel/test/cases/serializable/xml_test.rb index fc73d9dcd8..817ca1e736 100644 --- a/activemodel/test/cases/serializers/xml_serialization_test.rb +++ b/activemodel/test/cases/serializable/xml_test.rb @@ -5,7 +5,7 @@ require 'ostruct' class Contact extend ActiveModel::Naming - include ActiveModel::Serializers::Xml + include ActiveModel::Serializable::XML attr_accessor :address, :friends @@ -24,7 +24,7 @@ end class Address extend ActiveModel::Naming - include ActiveModel::Serializers::Xml + include ActiveModel::Serializable::XML attr_accessor :street, :city, :state, :zip diff --git a/activemodel/test/cases/serialization_test.rb b/activemodel/test/cases/serializable_test.rb index b8dad9d51f..46ee372c6f 100644 --- a/activemodel/test/cases/serialization_test.rb +++ b/activemodel/test/cases/serializable_test.rb @@ -3,7 +3,7 @@ require 'active_support/core_ext/object/instance_variables' class SerializationTest < ActiveModel::TestCase class User - include ActiveModel::Serialization + include ActiveModel::Serializable attr_accessor :name, :email, :gender, :address, :friends @@ -22,7 +22,7 @@ class SerializationTest < ActiveModel::TestCase end class Address - include ActiveModel::Serialization + include ActiveModel::Serializable attr_accessor :street, :city, :state, :zip diff --git a/activemodel/test/models/blog_post.rb b/activemodel/test/models/blog_post.rb index d289177259..46eba857df 100644 --- a/activemodel/test/models/blog_post.rb +++ b/activemodel/test/models/blog_post.rb @@ -1,10 +1,6 @@ module Blog - def self._railtie - Object.new - end - - def self.table_name_prefix - "blog_" + def self.use_relative_model_naming? + true end class Post |