diff --git a/active_model_serializers.gemspec b/active_model_serializers.gemspec index f4f90a81..0971a26e 100644 --- a/active_model_serializers.gemspec +++ b/active_model_serializers.gemspec @@ -1,7 +1,7 @@ # -*- encoding: utf-8 -*- $:.unshift File.expand_path("../lib", __FILE__) -require "active_model/serializers/version" +require "active_model/serializer/version" Gem::Specification.new do |gem| gem.authors = ["José Valim", "Yehuda Katz"] diff --git a/lib/active_model/array_serializer.rb b/lib/active_model/array_serializer.rb index 518323ca..f6474323 100644 --- a/lib/active_model/array_serializer.rb +++ b/lib/active_model/array_serializer.rb @@ -1,3 +1,5 @@ +require 'active_model/serializable' +require 'active_model/serializer/caching' require "active_support/core_ext/class/attribute" require 'active_support/dependencies' require 'active_support/descendants_tracker' @@ -15,6 +17,9 @@ module ActiveModel class ArraySerializer extend ActiveSupport::DescendantsTracker + include ActiveModel::Serializable + include ActiveModel::Serializer::Caching + attr_reader :object, :options class_attribute :root @@ -30,60 +35,25 @@ module ActiveModel end def initialize(object, options={}) - @object, @options = object, options + @object = object + @options = options end - def meta_key - @options[:meta_key].try(:to_sym) || :meta - end - - def include_meta(hash) - hash[meta_key] = @options[:meta] if @options.has_key?(:meta) - end - - def as_json(*args) - @options[:hash] = hash = {} - @options[:unique_values] = {} - - if root = @options[:root] - hash.merge!(root => serializable_array) - include_meta hash - hash - else - serializable_array - end - end - - def to_json(*args) - if perform_caching? - cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) do - super - end - else - super - end + def serialize_object + serializable_array end def serializable_array - if perform_caching? - cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'serializable-array']) do - _serializable_array - end - else - _serializable_array - end - end - - private - def _serializable_array - @object.map do |item| - if @options.has_key? :each_serializer - serializer = @options[:each_serializer] + object.map do |item| + if options.has_key? :each_serializer + serializer = options[:each_serializer] elsif item.respond_to?(:active_model_serializer) serializer = item.active_model_serializer + else + serializer = DefaultSerializer end - serializable = serializer ? serializer.new(item, @options) : DefaultSerializer.new(item, @options) + serializable = serializer.new(item, options) if serializable.respond_to?(:serializable_hash) serializable.serializable_hash @@ -92,13 +62,5 @@ module ActiveModel end end end - - def expand_cache_key(*args) - ActiveSupport::Cache.expand_cache_key(args) - end - - def perform_caching? - perform_caching && cache && respond_to?(:cache_key) - end end end diff --git a/lib/active_model/serializable.rb b/lib/active_model/serializable.rb new file mode 100644 index 00000000..7122ae20 --- /dev/null +++ b/lib/active_model/serializable.rb @@ -0,0 +1,49 @@ +require 'active_support/core_ext/object/to_json' + +module ActiveModel + # Enable classes to Classes including this module to serialize themselves by implementing a serialize method and an options method. + # + # Example: + # + # require 'active_model_serializers' + # + # class MySerializer + # include ActiveModel::Serializable + # + # def initialize + # @options = {} + # end + # + # attr_reader :options + # + # def serialize + # { a: 1 } + # end + # end + # + # puts MySerializer.new.to_json + module Serializable + def as_json(args={}) + if root = args[:root] || options[:root] + options[:hash] = hash = {} + options[:unique_values] = {} + + hash.merge!(root => serialize) + include_meta hash + hash + else + serialize + end + end + + private + + def include_meta(hash) + hash[meta_key] = options[:meta] if options.has_key?(:meta) + end + + def meta_key + options[:meta_key].try(:to_sym) || :meta + end + end +end diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb index 281af425..c4477fb1 100644 --- a/lib/active_model/serializer.rb +++ b/lib/active_model/serializer.rb @@ -1,3 +1,5 @@ +require 'active_model/serializable' +require 'active_model/serializer/caching' require "active_support/core_ext/class/attribute" require "active_support/core_ext/module/anonymous" require 'active_support/dependencies' @@ -40,6 +42,9 @@ module ActiveModel class Serializer extend ActiveSupport::DescendantsTracker + include ActiveModel::Serializable + include ActiveModel::Serializer::Caching + INCLUDE_METHODS = {} INSTRUMENT = { :serialize => :"serialize.serializer", :associations => :"associations.serializer" } @@ -70,7 +75,6 @@ module ActiveModel class_attribute :perform_caching class << self - # set perform caching like root def cached(value = true) self.perform_caching = value end @@ -125,7 +129,7 @@ module ActiveModel define_include_method attr - self._associations[attr] = klass.refine(attr, options) + self._associations[attr] = [klass, options] end end @@ -148,7 +152,7 @@ module ActiveModel # with the association name does not exist, the association name is # dispatched to the serialized object. def has_many(*attrs) - associate(Associations::HasMany, attrs) + associate(Association::HasMany, attrs) end # Defines an association in the object should be rendered. @@ -158,7 +162,7 @@ module ActiveModel # with the association name does not exist, the association name is # dispatched to the serialized object. def has_one(*attrs) - associate(Associations::HasOne, attrs) + associate(Association::HasOne, attrs) end # Return a schema hash for the current serializer. This information @@ -213,8 +217,8 @@ module ActiveModel end associations = {} - _associations.each do |attr, association_class| - association = association_class.new(attr, self) + _associations.each do |attr, (association_class, options)| + association = association_class.new(attr, options) if model_association = klass.reflect_on_association(association.name) # Real association. @@ -316,49 +320,23 @@ module ActiveModel @options[:url_options] || {} end - def meta_key - @options[:meta_key].try(:to_sym) || :meta - end - - def include_meta(hash) - hash[meta_key] = @options[:meta] if @options.has_key?(:meta) - end - - def to_json(*args) - if perform_caching? - cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) do - super - end - else - super - end - end - # Returns a json representation of the serializable # object including the root. - def as_json(options={}) - if root = options.fetch(:root, @options.fetch(:root, root_name)) - @options[:hash] = hash = {} - @options[:unique_values] = {} + def as_json(args={}) + super(root: args.fetch(:root, options.fetch(:root, root_name))) + end - hash.merge!(root => serializable_hash) - include_meta hash - hash - else - serializable_hash - end + def serialize_object + serializable_hash end # Returns a hash representation of the serializable # object without the root. def serializable_hash - if perform_caching? - cache.fetch expand_cache_key([self.class.to_s.underscore, cache_key, 'serializable-hash']) do - _serializable_hash - end - else - _serializable_hash - end + return nil if @object.nil? + @node = attributes + include_associations! if _embed + @node end def include_associations! @@ -374,23 +352,8 @@ module ActiveModel end def include!(name, options={}) - # Make sure that if a special options[:hash] was passed in, we generate - # a new unique values hash and don't clobber the original. If the hash - # passed in is the same as the current options hash, use the current - # unique values. - # - # TODO: Should passing in a Hash even be public API here? - unique_values = - if hash = options[:hash] - if @options[:hash] == hash - @options[:unique_values] ||= {} - else - {} - end - else - hash = @options[:hash] - @options[:unique_values] ||= {} - end + hash = @options[:hash] + unique_values = @options[:unique_values] ||= {} node = options[:node] ||= @node value = options[:value] @@ -403,19 +366,28 @@ module ActiveModel end end + klass, klass_options = _associations[name] association_class = - if klass = _associations[name] + if klass + options = klass_options.merge options klass elsif value.respond_to?(:to_ary) - Associations::HasMany + Association::HasMany else - Associations::HasOne + Association::HasOne end - association = association_class.new(name, self, options) + options = default_embed_options.merge!(options) + options[:value] ||= send(name) + association = association_class.new(name, options, self.options) if association.embed_ids? - node[association.key] = association.serialize_ids + node[association.key] = + if options[:embed_key] || self.respond_to?(name) || !self.object.respond_to?(association.id_key) + association.serialize_ids + else + self.object.read_attribute_for_serialization(association.id_key) + end if association.embed_in_root? && hash.nil? raise IncludeError.new(self.class, association.name) @@ -473,27 +445,21 @@ module ActiveModel alias :read_attribute_for_serialization :send - def _serializable_hash - return nil if @object.nil? - @node = attributes - include_associations! if _embed - @node - end - - def perform_caching? - perform_caching && cache && respond_to?(:cache_key) - end - - def expand_cache_key(*args) - ActiveSupport::Cache.expand_cache_key(args) - end - # Use ActiveSupport::Notifications to send events to external systems. # The event name is: name.class_name.serializer def instrument(name, payload = {}, &block) event_name = INSTRUMENT[name] ActiveSupport::Notifications.instrument(event_name, payload, &block) end + + private + + def default_embed_options + { + :embed => _embed, + :include => _root_embed + } + end end # DefaultSerializer diff --git a/lib/active_model/serializer/associations.rb b/lib/active_model/serializer/associations.rb index 8606b930..63760d41 100644 --- a/lib/active_model/serializer/associations.rb +++ b/lib/active_model/serializer/associations.rb @@ -1,232 +1,172 @@ module ActiveModel class Serializer - module Associations #:nodoc: - class Config #:nodoc: - class_attribute :options + class Association #:nodoc: + # name: The name of the association. + # + # options: A hash. These keys are accepted: + # + # value: The object we're associating with. + # + # serializer: The class used to serialize the association. + # + # embed: Define how associations should be embedded. + # - :objects # Embed associations as full objects. + # - :ids # Embed only the association ids. + # - :ids, :include => true # Embed the association ids and include objects in the root. + # + # include: Used in conjunction with embed :ids. Includes the objects in the root. + # + # root: Used in conjunction with include: true. Defines the key used to embed the objects. + # + # key: Key name used to store the ids in. + # + # embed_key: Method used to fetch ids. Defaults to :id. + # + # polymorphic: Is the association is polymorphic?. Values: true or false. + def initialize(name, options={}, serializer_options={}) + @name = name + @object = options[:value] - def self.refine(name, class_options) - current_class = self + embed = options[:embed] + @embed_ids = embed == :id || embed == :ids + @embed_objects = embed == :object || embed == :objects + @embed_key = options[:embed_key] || :id + @embed_in_root = options[:include] - Class.new(self) do - singleton_class.class_eval do - define_method(:to_s) do - "(subclass of #{current_class.name})" - end + serializer = options[:serializer] + @serializer = serializer.is_a?(String) ? serializer.constantize : serializer - alias inspect to_s - end + @options = options + @serializer_options = serializer_options + end - self.options = class_options + attr_reader :object, :root, :name, :embed_ids, :embed_objects, :embed_in_root + alias embeddable? object + alias embed_objects? embed_objects + alias embed_ids? embed_ids + alias use_id_key? embed_ids? + alias embed_in_root? embed_in_root - # cache the root so we can reuse it without falling back on a per-instance basis - begin - self.options[:root] ||= self.new(name, nil).root - rescue - # this could fail if it needs a valid source, for example a polymorphic association - end - - end - end - - self.options = {} - - def initialize(name, source, options={}) - @name = name - @source = source - @options = options - end - - def option(key, default=nil) - if @options.key?(key) - @options[key] - elsif self.class.options.key?(key) - self.class.options[key] - else - default - end - end - - def target_serializer - serializer = option(:serializer) - serializer.is_a?(String) ? serializer.constantize : serializer - end - - def source_serializer - @source - end - - def key - option(:key) || @name - end - - def root - option(:root) || @name - end - - def name - option(:name) || @name - end - - def associated_object - option(:value) || source_serializer.send(name) - end - - def embed_ids? - [:id, :ids].include? option(:embed, source_serializer._embed) - end - - def embed_objects? - [:object, :objects].include? option(:embed, source_serializer._embed) - end - - def embed_in_root? - option(:include, source_serializer._root_embed) - end - - def embeddable? - !associated_object.nil? - end - - protected - - def find_serializable(object) - if target_serializer - target_serializer.new(object, source_serializer.options) - elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer) - ams.new(object, source_serializer.options) - else - object - end + def key + if key = options[:key] + key + elsif use_id_key? + id_key + else + name end end - class HasMany < Config #:nodoc: - def key - if key = option(:key) - key - elsif embed_ids? - "#{@name.to_s.singularize}_ids".to_sym - else - @name - end + private + + attr_reader :embed_key, :serializer, :options, :serializer_options + + def find_serializable(object) + if serializer + serializer.new(object, serializer_options) + elsif object.respond_to?(:active_model_serializer) && (ams = object.active_model_serializer) + ams.new(object, serializer_options) + else + object + end + end + + class HasMany < Association #:nodoc: + def root + options[:root] || name end - def embed_key - if key = option(:embed_key) - key - else - :id - end - end - - def serialize - associated_object.map do |item| - find_serializable(item).serializable_hash - end + def id_key + "#{name.to_s.singularize}_ids".to_sym end def serializables - associated_object.map do |item| + object.map do |item| find_serializable(item) end end + def serialize + object.map do |item| + find_serializable(item).serializable_hash + end + end + def serialize_ids - ids_key = "#{@name.to_s.singularize}_ids".to_sym - if !option(:embed_key) && !source_serializer.respond_to?(@name.to_s) && source_serializer.object.respond_to?(ids_key) - source_serializer.object.read_attribute_for_serialization(ids_key) - else - associated_object.map do |item| - item.read_attribute_for_serialization(embed_key) - end + object.map do |item| + item.read_attribute_for_serialization(embed_key) end end end - class HasOne < Config #:nodoc: - def embeddable? - if polymorphic? && associated_object.nil? - false - else - true - end - end - - def polymorphic? - option :polymorphic + class HasOne < Association #:nodoc: + def initialize(name, options={}, serializer_options={}) + super + @polymorphic = options[:polymorphic] end def root - if root = option(:root) + if root = options[:root] root elsif polymorphic? - associated_object.class.to_s.pluralize.demodulize.underscore.to_sym + object.class.to_s.pluralize.demodulize.underscore.to_sym else - @name.to_s.pluralize.to_sym + name.to_s.pluralize.to_sym end end - def key - if key = option(:key) - key - elsif embed_ids? && !polymorphic? - "#{@name}_id".to_sym - else - @name - end + def id_key + "#{name}_id".to_sym end - def embed_key - if key = option(:embed_key) - key - else - :id - end - end - - def polymorphic_key - associated_object.class.to_s.demodulize.underscore.to_sym - end - - def serialize - object = associated_object - - if object && polymorphic? - { - :type => polymorphic_key, - polymorphic_key => find_serializable(object).serializable_hash - } - elsif object - find_serializable(object).serializable_hash - end + def embeddable? + super || !polymorphic? end def serializables - object = associated_object value = object && find_serializable(object) value ? [value] : [] end - def serialize_ids - id_key = "#{@name}_id".to_sym - - if polymorphic? - if associated_object + def serialize + if object + if polymorphic? { :type => polymorphic_key, - :id => associated_object.read_attribute_for_serialization(embed_key) + polymorphic_key => find_serializable(object).serializable_hash } else - nil + find_serializable(object).serializable_hash end - elsif !option(:embed_key) && !source_serializer.respond_to?(@name.to_s) && source_serializer.object.respond_to?(id_key) - source_serializer.object.read_attribute_for_serialization(id_key) - elsif associated_object - associated_object.read_attribute_for_serialization(embed_key) - else - nil end end + + def serialize_ids + if object + id = object.read_attribute_for_serialization(embed_key) + if polymorphic? + { + :type => polymorphic_key, + :id => id + } + else + id + end + end + end + + private + + attr_reader :polymorphic + alias polymorphic? polymorphic + + def use_id_key? + embed_ids? && !polymorphic? + end + + def polymorphic_key + object.class.to_s.demodulize.underscore.to_sym + end end end end diff --git a/lib/active_model/serializer/caching.rb b/lib/active_model/serializer/caching.rb new file mode 100644 index 00000000..50fcf7b5 --- /dev/null +++ b/lib/active_model/serializer/caching.rb @@ -0,0 +1,37 @@ +module ActiveModel + class Serializer + module Caching + def to_json(*args) + if caching_enabled? + key = expand_cache_key([self.class.to_s.underscore, cache_key, 'to-json']) + cache.fetch key do + super + end + else + super + end + end + + def serialize(*args) + if caching_enabled? + key = expand_cache_key([self.class.to_s.underscore, cache_key, 'serialize']) + cache.fetch key do + serialize_object + end + else + serialize_object + end + end + + private + + def caching_enabled? + perform_caching && cache && respond_to?(:cache_key) + end + + def expand_cache_key(*args) + ActiveSupport::Cache.expand_cache_key(args) + end + end + end +end diff --git a/lib/active_model/serializers/version.rb b/lib/active_model/serializer/version.rb similarity index 100% rename from lib/active_model/serializers/version.rb rename to lib/active_model/serializer/version.rb diff --git a/test/caching_test.rb b/test/caching_test.rb index 869f0f93..ee1dd263 100644 --- a/test/caching_test.rb +++ b/test/caching_test.rb @@ -68,7 +68,7 @@ class CachingTest < ActiveModel::TestCase instance.to_json - assert_equal(instance.serializable_hash, serializer.cache.read('serializer/Adam/serializable-hash')) + assert_equal(instance.serializable_hash, serializer.cache.read('serializer/Adam/serialize')) assert_equal(instance.to_json, serializer.cache.read('serializer/Adam/to-json')) end @@ -90,7 +90,7 @@ class CachingTest < ActiveModel::TestCase instance.to_json - assert_equal instance.serializable_array, serializer.cache.read('array_serializer/cache-key/serializable-array') + assert_equal instance.serializable_array, serializer.cache.read('array_serializer/cache-key/serialize') assert_equal instance.to_json, serializer.cache.read('array_serializer/cache-key/to-json') end end