lib/active_model/csverizer.rb in csverizer-0.0.6 vs lib/active_model/csverizer.rb in csverizer-0.0.7

- old
+ new

@@ -1,49 +1,321 @@ require 'csv' module ActiveModel class Csverizer - @_attributes = [] - @associations = [] - @root = true + @mutex = Mutex.new class << self - attr_accessor :_attributes, :associations, :root + def inherited(base) + base._root = _root + base._attributes = (_attributes || []).dup + base._associations = (_associations || {}).dup + end + + def setup + @mutex.synchronize do + yield CONFIG + end + end + + EMBED_IN_ROOT_OPTIONS = [ + :include, + :embed_in_root, + :embed_in_root_key, + :embed_namespace + ].freeze + + def embed(type, options={}) + CONFIG.embed = type + if EMBED_IN_ROOT_OPTIONS.any? { |opt| options[opt].present? } + CONFIG.embed_in_root = true + end + if options[:embed_in_root_key].present? + CONFIG.embed_in_root_key = options[:embed_in_root_key] + end + ActiveSupport::Deprecation.warn <<-WARN +** Notice: embed is deprecated. ** +The use of .embed method on a Serializer will be soon removed, as this should have a global scope and not a class scope. +Please use the global .setup method instead: +ActiveModel::Serializer.setup do |config| + config.embed = :#{type} + config.embed_in_root = #{CONFIG.embed_in_root || false} +end + WARN + end + + def format_keys(format) + @key_format = format + end + attr_reader :key_format + + def serializer_for(resource, options = {}) + if resource.respond_to?(:serializer_class) + resource.serializer_class + elsif resource.respond_to?(:to_ary) + if Object.constants.include?(:ArraySerializer) + ::ArraySerializer + else + ArraySerializer + end + else + klass_name = build_serializer_class(resource, options) + Serializer.serializers_cache.fetch_or_store(klass_name) do + _const_get(klass_name) + end + end + end + + attr_accessor :_root, :_attributes, :_associations + alias root _root= + alias root= _root= + + def root_name + if name + root_name = name.demodulize.underscore.sub(/_serializer$/, '') + CONFIG.plural_default_root ? root_name.pluralize : root_name + end + end + + def attributes(*attrs) + attrs.each do |attr| + striped_attr = strip_attribute attr + + @_attributes << striped_attr + + define_method striped_attr do + object.read_attribute_for_serialization attr + end unless method_defined?(attr) + end + end + + def has_one(*attrs) + associate(Association::HasOne, *attrs) + end + + def has_many(*attrs) + associate(Association::HasMany, *attrs) + end + + def serializers_cache + @serializers_cache ||= Concurrent::Map.new + end + + private + + def strip_attribute(attr) + symbolized = attr.is_a?(Symbol) + + attr = attr.to_s.gsub(/\?\Z/, '') + attr = attr.to_sym if symbolized + attr + end + + def build_serializer_class(resource, options) + "".tap do |klass_name| + klass_name << "#{options[:namespace]}::" if options[:namespace] + klass_name << options[:prefix].to_s.classify if options[:prefix] + klass_name << "#{resource.class.name}Serializer" + end + end + + def associate(klass, *attrs) + options = attrs.extract_options! + + attrs.each do |attr| + define_method attr do + object.send attr + end unless method_defined?(attr) + + @_associations[attr] = klass.new(attr, options) + end + end end - def self.inherited(base) - base._attributes = [] - base.associations = [] - base.root = true + def initialize(object, options={}) + @object = object + @scope = options[:scope] + @root = options.fetch(:root, self.class._root) + @polymorphic = options.fetch(:polymorphic, false) + @meta_key = options[:meta_key] || :meta + @meta = options[@meta_key] + @wrap_in_array = options[:_wrap_in_array] + @only = options[:only] ? Array(options[:only]) : nil + @except = options[:except] ? Array(options[:except]) : nil + @key_format = options[:key_format] + @context = options[:context] + @namespace = options[:namespace] + @prefix = options.fetch(:prefix, '') end + attr_accessor :object, :scope, :root, :meta_key, :meta, :key_format, :context, :polymorphic - def self.attributes(*attributes) - @_attributes = @_attributes.concat(attributes) + def json_key + key = if root == true || root.nil? + self.class.root_name + else + root + end + + key_format == :lower_camel && key.present? ? key.camelize(:lower) : key end - def self.has_one(associated) - @associations << { - associated: associated, - serializer: ActiveModel::CsverizerFactory - } + def attributes + filter(self.class._attributes.dup).each_with_object({}) do |name, hash| + hash[name] = send(name) + end end - def self.has_many(associated) - @associations << { - associated: associated, - serializer: ActiveModel::CsvArraySerializer - } + def associations(options={}) + associations = self.class._associations + included_associations = filter(associations.keys) + associations.each_with_object({}) do |(name, association), hash| + if included_associations.include? name + if association.embed_ids? + ids = serialize_ids association + if association.embed_namespace? + hash = hash[association.embed_namespace] ||= {} + hash[association.key] = ids + else + hash[association.key] = ids + end + elsif association.embed_objects? + if association.embed_namespace? + hash = hash[association.embed_namespace] ||= {} + end + hash[association.embedded_key] = serialize association, options + end + end + end end - attr_reader :object + def filter(keys) + if @only + keys & @only + elsif @except + keys - @except + else + keys + end + end - def initialize(object, options = {}) - @object = object - @root = options.fetch(:root, self.class.root) - @prefix = options.fetch(:prefix, '') + def embedded_in_root_associations + associations = self.class._associations + included_associations = filter(associations.keys) + associations.each_with_object({}) do |(name, association), hash| + if included_associations.include? name + association_serializer = build_serializer(association) + # we must do this always because even if the current association is not + # embedded in root, it might have its own associations that are embedded in root + hash.merge!(association_serializer.embedded_in_root_associations) do |key, oldval, newval| + if oldval.respond_to?(:to_ary) + [oldval, newval].flatten.uniq + else + oldval.merge(newval) { |_, oldval, newval| [oldval, newval].flatten.uniq } + end + end + + if association.embed_in_root? + if association.embed_in_root_key? + hash = hash[association.embed_in_root_key] ||= {} + end + + serialized_data = association_serializer.serializable_object + key = association.root_key + if hash.has_key?(key) + hash[key].concat(serialized_data).uniq! + else + hash[key] = serialized_data + end + end + end + end end + def build_serializer(association) + object = send(association.name) + association.build_serializer(object, association_options_for_serializer(association)) + end + + def association_options_for_serializer(association) + prefix = association.options[:prefix] + namespace = association.options[:namespace] || @namespace || self.namespace + + { scope: scope }.tap do |opts| + opts[:namespace] = namespace if namespace + opts[:prefix] = prefix if prefix + end + end + + def serialize(association,options={}) + build_serializer(association).serializable_object(options) + end + + def serialize_ids(association) + associated_data = send(association.name) + if associated_data.respond_to?(:to_ary) + associated_data.map { |elem| serialize_id(elem, association) } + else + serialize_id(associated_data, association) if associated_data + end + end + + def key_format + @key_format || self.class.key_format || CONFIG.key_format + end + + def format_key(key) + if key_format == :lower_camel + key.to_s.camelize(:lower) + else + key + end + end + + def convert_keys(hash) + Hash[hash.map do |k,v| + key = if k.is_a?(Symbol) + format_key(k).to_sym + else + format_key(k) + end + + [key ,v] + end] + end + + attr_writer :serialization_options + def serialization_options + @serialization_options || {} + end + + def serializable_object(options={}) + self.serialization_options = options + return @wrap_in_array ? [] : nil if @object.nil? + hash = attributes + hash.merge! associations(options) + hash = convert_keys(hash) if key_format.present? + hash = { :type => type_name(@object), type_name(@object) => hash } if @polymorphic + @wrap_in_array ? [hash] : hash + end + alias_method :serializable_hash, :serializable_object + + def serialize_id(elem, association) + id = elem.read_attribute_for_serialization(association.embed_key) + association.polymorphic? ? { id: id, type: type_name(elem) } : id + end + + def type_name(elem) + elem.class.to_s.demodulize.underscore.to_sym + end + + def to_csv + CSV.generate do |csv| + csv << attribute_names + to_a.each { |record| csv << record } + end + end + def to_a return [[]] unless @object values = [] values << self.class._attributes.collect { |name| read_attribute(name) } @@ -53,38 +325,29 @@ end values end - def to_csv - CSV.generate do |csv| - csv << attribute_names if @root - to_a.each { |record| csv << record } - end - end - def attribute_names names = self.class._attributes.collect do |attribute| @prefix + attribute.to_s end associated_serializers.reduce(names) do |names, serializer| names.concat serializer.attribute_names end end - private - def read_attribute(name) return send(name) if respond_to?(name) object.read_attribute_for_serialization(name) end def read_association(name) respond_to?(name) ? send(name) : object.send(name) end def associated_serializers - @associated_serializers ||= self.class.associations.collect do |hash| + @associated_serializers ||= self.class._associations.collect do |hash| object = read_attribute(hash[:associated]) hash[:serializer].new(object, prefix: hash[:associated].to_s + '_') end end end