From c3de52d7ed470433e9ecd44226518797c0a9f389 Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Mon, 26 Sep 2011 19:29:37 -0400 Subject: Initial implementation of ActiveModel::Serializer --- activemodel/lib/active_model/serializer.rb | 46 ++++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 activemodel/lib/active_model/serializer.rb (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb new file mode 100644 index 0000000000..2eca3ebc5e --- /dev/null +++ b/activemodel/lib/active_model/serializer.rb @@ -0,0 +1,46 @@ +require "active_support/core_ext/class/attribute" +require "active_support/core_ext/string/inflections" +require "set" + +module ActiveModel + class Serializer + class_attribute :_attributes + self._attributes = Set.new + + def self.attributes(*attrs) + self._attributes += attrs + end + + attr_reader :object, :scope + + def self.inherited(klass) + name = klass.name.demodulize.underscore.sub(/_serializer$/, '') + + klass.class_eval do + alias_method name.to_sym, :object + end + end + + def initialize(object, scope) + @object, @scope = object, scope + end + + def as_json(*) + serializable_hash + end + + def serializable_hash(*) + attributes + end + + def attributes + hash = {} + + _attributes.each do |name| + hash[name] = @object.read_attribute_for_serialization(name) + end + + hash + end + end +end -- cgit v1.2.3 From e407dfb9bf6ee3b12d699511ef05e6f260c5edf1 Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Tue, 27 Sep 2011 17:34:47 -0400 Subject: Don't require serializable_hash to take options. --- activemodel/lib/active_model/serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 2eca3ebc5e..a2035ae2cb 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -29,7 +29,7 @@ module ActiveModel serializable_hash end - def serializable_hash(*) + def serializable_hash attributes end -- cgit v1.2.3 From 2a4aaae72af037715db81fda332190df62f3ec44 Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 16:20:45 +0200 Subject: Added has_one and has_many --- activemodel/lib/active_model/serializer.rb | 56 +++++++++++++++++++++++++----- 1 file changed, 47 insertions(+), 9 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index a2035ae2cb..dc5e2aadb3 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -7,20 +7,41 @@ module ActiveModel class_attribute :_attributes self._attributes = Set.new - def self.attributes(*attrs) - self._attributes += attrs - end + class_attribute :_associations + self._associations = {} - attr_reader :object, :scope + class << self + def attributes(*attrs) + self._attributes += attrs + end + + def has_many(*attrs) + options = attrs.extract_options! + options[:has_many] = true + hash = {} + attrs.each { |attr| hash[attr] = options } + self._associations = _associations.merge(hash) + end + + def has_one(*attrs) + options = attrs.extract_options! + options[:has_one] = true + hash = {} + attrs.each { |attr| hash[attr] = options } + self._associations = _associations.merge(hash) + end - def self.inherited(klass) - name = klass.name.demodulize.underscore.sub(/_serializer$/, '') + def inherited(klass) + name = klass.name.demodulize.underscore.sub(/_serializer$/, '') - klass.class_eval do - alias_method name.to_sym, :object + klass.class_eval do + alias_method name.to_sym, :object + end end end + attr_reader :object, :scope + def initialize(object, scope) @object, @scope = object, scope end @@ -30,7 +51,24 @@ module ActiveModel end def serializable_hash - attributes + hash = attributes + + _associations.each do |association, options| + associated_object = object.send(association) + serializer = options[:serializer] + + if options[:has_many] + serialized_array = associated_object.map do |item| + serializer.new(item, scope).serializable_hash + end + + hash[association] = serialized_array + elsif options[:has_one] + hash[association] = serializer.new(associated_object, scope).serializable_hash + end + end + + hash end def attributes -- cgit v1.2.3 From 776da539d72c98d077f97789a1265dd23a79711f Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 16:53:22 +0200 Subject: Add support for implicit serializers --- activemodel/lib/active_model/serializer.rb | 63 ++++++++++++++++++------------ 1 file changed, 39 insertions(+), 24 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index dc5e2aadb3..c5b433df51 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -1,37 +1,62 @@ 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 class Serializer + module Associations + class Config < Struct.new(:name, :options) + def serializer + options[:serializer] + end + end + + class HasMany < Config + def serialize(collection, scope) + collection.map do |item| + serializer.new(item, scope).serializable_hash + end + end + end + + class HasOne < Config + def serialize(object, scope) + serializer.new(object, scope).serializable_hash + end + end + end + class_attribute :_attributes self._attributes = Set.new class_attribute :_associations - self._associations = {} + self._associations = [] class << self def attributes(*attrs) self._attributes += attrs end - def has_many(*attrs) + def associate(klass, attrs) options = attrs.extract_options! - options[:has_many] = true - hash = {} - attrs.each { |attr| hash[attr] = options } - self._associations = _associations.merge(hash) + self._associations += attrs.map do |attr| + options[:serializer] ||= const_get("#{attr.to_s.camelize}Serializer") + klass.new(attr, options) + end + end + + def has_many(*attrs) + associate(Associations::HasMany, attrs) end def has_one(*attrs) - options = attrs.extract_options! - options[:has_one] = true - hash = {} - attrs.each { |attr| hash[attr] = options } - self._associations = _associations.merge(hash) + associate(Associations::HasOne, attrs) end def inherited(klass) + return if klass.anonymous? + name = klass.name.demodulize.underscore.sub(/_serializer$/, '') klass.class_eval do @@ -53,19 +78,9 @@ module ActiveModel def serializable_hash hash = attributes - _associations.each do |association, options| - associated_object = object.send(association) - serializer = options[:serializer] - - if options[:has_many] - serialized_array = associated_object.map do |item| - serializer.new(item, scope).serializable_hash - end - - hash[association] = serialized_array - elsif options[:has_one] - hash[association] = serializer.new(associated_object, scope).serializable_hash - end + _associations.each do |association| + associated_object = object.send(association.name) + hash[association.name] = association.serialize(associated_object, scope) end hash -- cgit v1.2.3 From 22c322f056f42d95b0421e6608f404134463de13 Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 17:01:08 +0200 Subject: Add support for overriding associations, mostly used for authorization --- activemodel/lib/active_model/serializer.rb | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index c5b433df51..6e89eec09c 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -41,6 +41,10 @@ module ActiveModel def associate(klass, attrs) 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 @@ -79,7 +83,7 @@ module ActiveModel hash = attributes _associations.each do |association| - associated_object = object.send(association.name) + associated_object = send(association.name) hash[association.name] = association.serialize(associated_object, scope) end -- cgit v1.2.3 From 322f47898e80af3fcdc3cb3db35e177d8216a2d2 Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 18:27:56 +0200 Subject: Add association_ids --- activemodel/lib/active_model/serializer.rb | 30 +++++++++++++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 6e89eec09c..99a007de31 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -18,12 +18,25 @@ module ActiveModel 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 def serialize(object, scope) serializer.new(object, scope).serializable_hash end + + def serialize_ids(object, scope) + object.read_attribute_for_serialization(:id) + end end end @@ -80,7 +93,11 @@ module ActiveModel end def serializable_hash - hash = attributes + attributes.merge(associations) + end + + def associations + hash = {} _associations.each do |association| associated_object = send(association.name) @@ -90,6 +107,17 @@ module ActiveModel hash end + 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 + def attributes hash = {} -- cgit v1.2.3 From 7a28498b55913aa0fd1d3529909ab57eaf64af0e Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 18:37:12 +0200 Subject: Fix nil has_one association --- activemodel/lib/active_model/serializer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 99a007de31..1d11870bb4 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -31,11 +31,11 @@ module ActiveModel class HasOne < Config def serialize(object, scope) - serializer.new(object, scope).serializable_hash + object && serializer.new(object, scope).serializable_hash end def serialize_ids(object, scope) - object.read_attribute_for_serialization(:id) + object && object.read_attribute_for_serialization(:id) end end end -- cgit v1.2.3 From a230f040ff61f069d46a9b86417a8e251016d5db Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 18:54:20 +0200 Subject: Add support for the root attribute --- activemodel/lib/active_model/serializer.rb | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 1d11870bb4..98801ae633 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -46,6 +46,8 @@ module ActiveModel class_attribute :_associations self._associations = [] + class_attribute :_root + class << self def attributes(*attrs) self._attributes += attrs @@ -71,6 +73,10 @@ module ActiveModel associate(Associations::HasOne, attrs) end + def root(name) + self._root = name + end + def inherited(klass) return if klass.anonymous? @@ -78,6 +84,7 @@ module ActiveModel klass.class_eval do alias_method name.to_sym, :object + root name.to_sym unless self._root == false end end end @@ -89,7 +96,11 @@ module ActiveModel end def as_json(*) - serializable_hash + if _root + { _root => serializable_hash } + else + serializable_hash + end end def serializable_hash -- cgit v1.2.3 From 2abb2e617af8e3353d4411a8bd51d03256e0274a Mon Sep 17 00:00:00 2001 From: Jose and Yehuda Date: Sat, 15 Oct 2011 19:22:16 +0200 Subject: Add initial support for embed API --- activemodel/lib/active_model/serializer.rb | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 98801ae633..6d0746a3e8 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -21,7 +21,7 @@ module ActiveModel def serialize_ids(collection, scope) # use named scopes if they are present - return collection.ids if collection.respond_to?(:ids) + #return collection.ids if collection.respond_to?(:ids) collection.map do |item| item.read_attribute_for_serialization(:id) @@ -47,6 +47,9 @@ module ActiveModel self._associations = [] class_attribute :_root + class_attribute :_embed + self._embed = :objects + class_attribute :_root_embed class << self def attributes(*attrs) @@ -73,6 +76,11 @@ module ActiveModel associate(Associations::HasOne, attrs) end + def embed(type, options={}) + self._embed = type + self._root_embed = true if options[:include] + end + def root(name) self._root = name end @@ -97,14 +105,22 @@ module ActiveModel def as_json(*) if _root - { _root => serializable_hash } + hash = { _root => serializable_hash } + hash.merge!(associations) if _root_embed + hash else serializable_hash end end def serializable_hash - attributes.merge(associations) + if _embed == :ids + attributes.merge(association_ids) + elsif _embed == :objects + attributes.merge(associations) + else + attributes + end end def associations -- cgit v1.2.3 From af64ac4e5ce8406137d5520fa88e8f652ab703e9 Mon Sep 17 00:00:00 2001 From: Oscar Del Ben Date: Mon, 14 Nov 2011 16:56:05 +0100 Subject: use any? instead of !empty? --- activemodel/lib/active_model/dirty.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/dirty.rb b/activemodel/lib/active_model/dirty.rb index 166cccf161..026f077ee7 100644 --- a/activemodel/lib/active_model/dirty.rb +++ b/activemodel/lib/active_model/dirty.rb @@ -98,7 +98,7 @@ module ActiveModel # person.name = 'bob' # person.changed? # => true def changed? - !changed_attributes.empty? + changed_attributes.any? end # List of attributes with unsaved changes. -- cgit v1.2.3 From 9fa329b7544b15cdf5751d518e380abc82468df0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Mon, 14 Nov 2011 20:12:17 +0100 Subject: Speed up attribute invocation by checking if both name and calls are compilable. --- activemodel/lib/active_model/attribute_methods.rb | 59 ++++++++++++----------- 1 file changed, 32 insertions(+), 27 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/attribute_methods.rb b/activemodel/lib/active_model/attribute_methods.rb index ef0b95424e..e69cb5c459 100644 --- a/activemodel/lib/active_model/attribute_methods.rb +++ b/activemodel/lib/active_model/attribute_methods.rb @@ -57,7 +57,8 @@ module ActiveModel module AttributeMethods extend ActiveSupport::Concern - COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/ + NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/ + CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/ included do class_attribute :attribute_method_matchers, :instance_writer => false @@ -112,7 +113,7 @@ module ActiveModel # If we can compile the method name, do it. Otherwise use define_method. # This is an important *optimization*, please don't change it. define_method # has slower dispatch and consumes more memory. - if name =~ COMPILABLE_REGEXP + if name =~ NAME_COMPILABLE_REGEXP sing.class_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{name}; #{value.nil? ? 'nil' : value.to_s.inspect}; end RUBY @@ -240,18 +241,7 @@ module ActiveModel attribute_method_matchers.each do |matcher| matcher_new = matcher.method_name(new_name).to_s matcher_old = matcher.method_name(old_name).to_s - - if matcher_new =~ COMPILABLE_REGEXP && matcher_old =~ COMPILABLE_REGEXP - module_eval <<-RUBY, __FILE__, __LINE__ + 1 - def #{matcher_new}(*args) - send(:#{matcher_old}, *args) - end - RUBY - else - define_method(matcher_new) do |*args| - send(matcher_old, *args) - end - end + define_optimized_call self, matcher_new, matcher_old end end @@ -293,17 +283,7 @@ module ActiveModel if respond_to?(generate_method) send(generate_method, attr_name) else - if method_name =~ COMPILABLE_REGEXP - defn = "def #{method_name}(*args)" - else - defn = "define_method(:'#{method_name}') do |*args|" - end - - generated_attribute_methods.module_eval <<-RUBY, __FILE__, __LINE__ + 1 - #{defn} - send(:#{matcher.method_missing_target}, '#{attr_name}', *args) - end - RUBY + define_optimized_call generated_attribute_methods, method_name, matcher.method_missing_target, attr_name.to_s end end end @@ -342,11 +322,11 @@ module ActiveModel # used to alleviate the GC, which ultimately also speeds up the app # significantly (in our case our test suite finishes 10% faster with # this cache). - def attribute_method_matchers_cache + def attribute_method_matchers_cache #:nodoc: @attribute_method_matchers_cache ||= {} end - def attribute_method_matcher(method_name) + def attribute_method_matcher(method_name) #:nodoc: if attribute_method_matchers_cache.key?(method_name) attribute_method_matchers_cache[method_name] else @@ -359,6 +339,31 @@ module ActiveModel end end + # Define a method `name` in `mod` that dispatches to `send` + # using the given `extra` args. This fallbacks `define_method` + # and `send` if the given names cannot be compiled. + def define_optimized_call(mod, name, send, *extra) #:nodoc: + if name =~ NAME_COMPILABLE_REGEXP + defn = "def #{name}(*args)" + else + defn = "define_method(:'#{name}') do |*args|" + end + + extra = (extra.map(&:inspect) << "*args").join(", ") + + if send =~ CALL_COMPILABLE_REGEXP + target = "#{send}(#{extra})" + else + target = "send(:'#{send}', #{extra})" + end + + mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1 + #{defn} + #{target} + end + RUBY + end + class AttributeMethodMatcher attr_reader :prefix, :suffix, :method_missing_target -- cgit v1.2.3 From efbb73562dde7510d0a08e91a09f1545880b35cb Mon Sep 17 00:00:00 2001 From: Alexey Vakhov Date: Sat, 19 Nov 2011 12:19:59 +0600 Subject: Small docs fix in Active Model callbacks module --- activemodel/lib/active_model/callbacks.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) (limited to 'activemodel/lib/active_model') 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. -- cgit v1.2.3 From fd86a1b6b068df87164d5763bdcd4a323a1e76f4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Nov 2011 19:06:45 +0000 Subject: Rely on a public contract between railties instead of accessing railtie methods directly. --- activemodel/lib/active_model/naming.rb | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index f16459ede2..2566920d63 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -13,18 +13,18 @@ module ActiveModel def initialize(klass, namespace = nil, name = nil) name ||= klass.name 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 +79,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 -- cgit v1.2.3 From 8896b4fdc8a543157cdf4dfc378607ebf6c10ab0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Nov 2011 23:18:13 +0000 Subject: Implement ArraySerializer and move old serialization API to a new namespace. The following constants were renamed: ActiveModel::Serialization => ActiveModel::Serializable ActiveModel::Serializers::JSON => ActiveModel::Serializable::JSON ActiveModel::Serializers::Xml => ActiveModel::Serializable::XML The main motivation for such a change is that `ActiveModel::Serializers::JSON` was not actually a serializer, but a module that when included allows the target to be serializable to JSON. With such changes, we were able to clean up the namespace to add true serializers as the ArraySerializer. --- activemodel/lib/active_model/serializable.rb | 156 +++++++++++++++++ activemodel/lib/active_model/serializable/json.rb | 108 ++++++++++++ activemodel/lib/active_model/serializable/xml.rb | 195 ++++++++++++++++++++++ activemodel/lib/active_model/serialization.rb | 139 +-------------- activemodel/lib/active_model/serializer.rb | 60 +++++++ activemodel/lib/active_model/serializers/json.rb | 102 +---------- activemodel/lib/active_model/serializers/xml.rb | 191 +-------------------- 7 files changed, 532 insertions(+), 419 deletions(-) create mode 100644 activemodel/lib/active_model/serializable.rb create mode 100644 activemodel/lib/active_model/serializable/json.rb create mode 100644 activemodel/lib/active_model/serializable/xml.rb (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb new file mode 100644 index 0000000000..769e934dbe --- /dev/null +++ b/activemodel/lib/active_model/serializable.rb @@ -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' + +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" + + include ActiveModel::Serializer::Scope + + module ClassMethods #:nodoc: + def _model_serializer + @_model_serializer ||= ActiveModel::Serializer::Finder.find(self, self) + 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 model_serializer + self.class._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 :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 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 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 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 + # + # + # + # 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 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 # => "\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 + 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 :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 +end \ No newline at end of file diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 6d0746a3e8..a541a1053d 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -1,10 +1,70 @@ require "active_support/core_ext/class/attribute" require "active_support/core_ext/string/inflections" require "active_support/core_ext/module/anonymous" +require "active_support/core_ext/module/introspection" require "set" module ActiveModel + # Active Model Array Serializer + class ArraySerializer + attr_reader :object, :scope + + def initialize(object, scope) + @object, @scope = object, scope + end + + def serializable_array + @object.map do |item| + if serializer = Serializer::Finder.find(item, scope) + serializer.new(item, scope) + else + item + end + end + end + + def as_json(*args) + serializable_array.as_json(*args) + end + end + + # Active Model Serializer class Serializer + module Finder + mattr_accessor :constantizer + @@constantizer = ActiveSupport::Inflector + + # Finds a serializer for the given object in the given scope. + # If the object implements a +model_serializer+ method, it does + # not do a scope lookup but uses the model_serializer method instead. + def self.find(object, scope) + if object.respond_to?(:model_serializer) + object.model_serializer + else + scope = scope.class unless scope.respond_to?(:const_defined?) + object = object.class unless object.respond_to?(:name) + serializer = "#{object.name.demodulize}Serializer" + + begin + scope.const_get serializer + rescue NameError => e + raise unless e.message =~ /uninitialized constant ([\w_]+::)*#{serializer}$/ + scope.parents.each do |parent| + return parent.const_get(serializer) if parent.const_defined?(serializer) + end + nil + end + end + end + end + + # Defines the serialization scope. Core extension serializers + # are defined in this module so a scoped lookup is able to find + # core extension serializers. + module Scope + ArraySerializer = ::ActiveModel::ArraySerializer + end + module Associations class Config < Struct.new(:name, :options) def serializer 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 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 + 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 - # - # - # - # 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 + 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 -- cgit v1.2.3 From 7fcc8c0a1f38c77b12cb6ffe81fb2887e6c60b85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Wed, 23 Nov 2011 23:45:27 +0000 Subject: Rely solely on active_model_serializer and remove the fancy constant lookup. --- activemodel/lib/active_model/serializable.rb | 12 ++++---- activemodel/lib/active_model/serializer.rb | 45 +++++----------------------- 2 files changed, 14 insertions(+), 43 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb index 769e934dbe..70e27c5683 100644 --- a/activemodel/lib/active_model/serializable.rb +++ b/activemodel/lib/active_model/serializable.rb @@ -1,6 +1,7 @@ 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 @@ -72,11 +73,10 @@ module ActiveModel autoload :JSON, "active_model/serializable/json" autoload :XML, "active_model/serializable/xml" - include ActiveModel::Serializer::Scope - module ClassMethods #:nodoc: - def _model_serializer - @_model_serializer ||= ActiveModel::Serializer::Finder.find(self, self) + def active_model_serializer + return @active_model_serializer if defined?(@active_model_serializer) + @active_model_serializer = "#{self.name}Serializer".safe_constantize end end @@ -108,8 +108,8 @@ module ActiveModel end # Returns a model serializer for this object considering its namespace. - def model_serializer - self.class._model_serializer + def active_model_serializer + self.class.active_model_serializer end private diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index a541a1053d..5478da15c8 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -1,7 +1,6 @@ require "active_support/core_ext/class/attribute" require "active_support/core_ext/string/inflections" require "active_support/core_ext/module/anonymous" -require "active_support/core_ext/module/introspection" require "set" module ActiveModel @@ -15,7 +14,7 @@ module ActiveModel def serializable_array @object.map do |item| - if serializer = Serializer::Finder.find(item, scope) + if item.respond_to?(:active_model_serializer) && (serializer = item.active_model_serializer) serializer.new(item, scope) else item @@ -30,41 +29,6 @@ module ActiveModel # Active Model Serializer class Serializer - module Finder - mattr_accessor :constantizer - @@constantizer = ActiveSupport::Inflector - - # Finds a serializer for the given object in the given scope. - # If the object implements a +model_serializer+ method, it does - # not do a scope lookup but uses the model_serializer method instead. - def self.find(object, scope) - if object.respond_to?(:model_serializer) - object.model_serializer - else - scope = scope.class unless scope.respond_to?(:const_defined?) - object = object.class unless object.respond_to?(:name) - serializer = "#{object.name.demodulize}Serializer" - - begin - scope.const_get serializer - rescue NameError => e - raise unless e.message =~ /uninitialized constant ([\w_]+::)*#{serializer}$/ - scope.parents.each do |parent| - return parent.const_get(serializer) if parent.const_defined?(serializer) - end - nil - end - end - end - end - - # Defines the serialization scope. Core extension serializers - # are defined in this module so a scoped lookup is able to find - # core extension serializers. - module Scope - ArraySerializer = ::ActiveModel::ArraySerializer - end - module Associations class Config < Struct.new(:name, :options) def serializer @@ -216,3 +180,10 @@ module ActiveModel end end end + +class Array + # Array uses ActiveModel::ArraySerializer. + def active_model_serializer + ActiveModel::ArraySerializer + end +end \ No newline at end of file -- cgit v1.2.3 From dc39af0a9a998938a969b214554db624dcdd9c85 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jakub=20Ku=C5=BAma?= Date: Thu, 24 Nov 2011 15:50:21 +0100 Subject: make ActiveModel::Name fail gracefully with anonymous classes --- activemodel/lib/active_model/naming.rb | 3 +++ 1 file changed, 3 insertions(+) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/naming.rb b/activemodel/lib/active_model/naming.rb index 2566920d63..953d24a3b2 100644 --- a/activemodel/lib/active_model/naming.rb +++ b/activemodel/lib/active_model/naming.rb @@ -12,6 +12,9 @@ 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 -- cgit v1.2.3 From 696d01f7f4a8ed787924a41cce6df836cd73c46f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 25 Nov 2011 09:49:54 +0000 Subject: Add docs to serializers. Update CHANGELOGs. --- activemodel/lib/active_model/serializer.rb | 78 +++++++++++++++++++++++++++--- 1 file changed, 71 insertions(+), 7 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializer.rb b/activemodel/lib/active_model/serializer.rb index 5478da15c8..0e23df2f2b 100644 --- a/activemodel/lib/active_model/serializer.rb +++ b/activemodel/lib/active_model/serializer.rb @@ -5,6 +5,9 @@ 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 @@ -28,15 +31,46 @@ module ActiveModel 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 - class Config < Struct.new(:name, :options) + module Associations #:nodoc: + class Config < Struct.new(:name, :options) #:nodoc: def serializer options[:serializer] end end - class HasMany < Config + class HasMany < Config #:nodoc: def serialize(collection, scope) collection.map do |item| serializer.new(item, scope).serializable_hash @@ -45,7 +79,7 @@ module ActiveModel def serialize_ids(collection, scope) # use named scopes if they are present - #return collection.ids if collection.respond_to?(:ids) + # return collection.ids if collection.respond_to?(:ids) collection.map do |item| item.read_attribute_for_serialization(:id) @@ -53,7 +87,7 @@ module ActiveModel end end - class HasOne < Config + class HasOne < Config #:nodoc: def serialize(object, scope) object && serializer.new(object, scope).serializable_hash end @@ -76,11 +110,12 @@ module ActiveModel 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) + def associate(klass, attrs) #:nodoc: options = attrs.extract_options! self._associations += attrs.map do |attr| unless method_defined?(attr) @@ -92,24 +127,43 @@ module ActiveModel 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) + def inherited(klass) #:nodoc: return if klass.anonymous? name = klass.name.demodulize.underscore.sub(/_serializer$/, '') @@ -127,6 +181,8 @@ module ActiveModel @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 } @@ -137,6 +193,8 @@ module ActiveModel end end + # Returns a hash representation of the serializable + # object without the root. def serializable_hash if _embed == :ids attributes.merge(association_ids) @@ -147,6 +205,8 @@ module ActiveModel end end + # Returns a hash representation of the serializable + # object associations. def associations hash = {} @@ -158,6 +218,8 @@ module ActiveModel hash end + # Returns a hash representation of the serializable + # object associations ids. def association_ids hash = {} @@ -169,6 +231,8 @@ module ActiveModel hash end + # Returns a hash representation of the serializable + # object attributes. def attributes hash = {} -- cgit v1.2.3 From 0a4035b12a6c59253cb60f9e3456513c6a6a9d33 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Valim?= Date: Fri, 25 Nov 2011 19:29:39 +0000 Subject: Revert the serializers API as other alternatives are now also under discussion --- activemodel/lib/active_model/serializable.rb | 12 ------------ 1 file changed, 12 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/serializable.rb b/activemodel/lib/active_model/serializable.rb index 70e27c5683..86770a25e4 100644 --- a/activemodel/lib/active_model/serializable.rb +++ b/activemodel/lib/active_model/serializable.rb @@ -73,13 +73,6 @@ module ActiveModel 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 ||= {} @@ -107,11 +100,6 @@ module ActiveModel 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 -- cgit v1.2.3 From 448df2d100feccf1d5df9db2b02b732a775d8406 Mon Sep 17 00:00:00 2001 From: Alexey Vakhov Date: Sun, 27 Nov 2011 10:23:40 +0400 Subject: Cosmetic fixes in AM validatations docs --- activemodel/lib/active_model/validations/presence.rb | 6 +++--- activemodel/lib/active_model/validations/with.rb | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) (limited to 'activemodel/lib/active_model') diff --git a/activemodel/lib/active_model/validations/presence.rb b/activemodel/lib/active_model/validations/presence.rb index 35af7152db..9a643a6f5c 100644 --- a/activemodel/lib/active_model/validations/presence.rb +++ b/activemodel/lib/active_model/validations/presence.rb @@ -25,14 +25,14 @@ module ActiveModel # This is due to the way Object#blank? handles boolean values: false.blank? # => true. # # Configuration options: - # * message - A custom error message (default is: "can't be blank"). + # * :message - A custom error message (default is: "can't be blank"). # * :on - Specifies when this validation is active. Runs in all # validation contexts by default (+nil+), other options are :create # and :update. - # * if - Specifies a method, proc or string to call to determine if the validation should + # * :if - Specifies a method, proc or string to call to determine if the validation should # occur (e.g. :if => :allow_validation, or :if => Proc.new { |user| user.signup_step > 2 }). # The method, proc or string should return or evaluate to a true or false value. - # * unless - Specifies a method, proc or string to call to determine if the validation should + # * :unless - Specifies a method, proc or string to call to determine if the validation should # not occur (e.g. :unless => :skip_validation, or :unless => Proc.new { |user| user.signup_step <= 2 }). # The method, proc or string should return or evaluate to a true or false value. # * :strict - Specifies whether validation should be strict. diff --git a/activemodel/lib/active_model/validations/with.rb b/activemodel/lib/active_model/validations/with.rb index 93a340eb39..72b8562b93 100644 --- a/activemodel/lib/active_model/validations/with.rb +++ b/activemodel/lib/active_model/validations/with.rb @@ -56,7 +56,7 @@ module ActiveModel # if the validation should occur (e.g. :if => :allow_validation, # or :if => Proc.new { |user| user.signup_step > 2 }). # The method, proc or string should return or evaluate to a true or false value. - # * unless - Specifies a method, proc or string to call to + # * :unless - Specifies a method, proc or string to call to # determine if the validation should not occur # (e.g. :unless => :skip_validation, or # :unless => Proc.new { |user| user.signup_step <= 2 }). -- cgit v1.2.3