RestPack/restpack_serializer

View on GitHub
lib/restpack_serializer/serializable.rb

Summary

Maintainability
A
3 hrs
Test Coverage
require 'active_support/concern'
require_relative "options"
require_relative "serializable/attributes"
require_relative "serializable/filterable"
require_relative "serializable/paging"
require_relative "serializable/resource"
require_relative "serializable/single"
require_relative "serializable/side_loading"
require_relative "serializable/side_load_data_builder"
require_relative "serializable/sortable"

module RestPack
  module Serializer
    extend ActiveSupport::Concern
    mattr_accessor :class_map
    @@class_map ||= {}

    included do
      identifier = self.to_s.underscore.chomp('_serializer')
      @@class_map[identifier] = self
      @@class_map[identifier.split('/').last] = self
    end

    include RestPack::Serializer::Paging
    include RestPack::Serializer::Resource
    include RestPack::Serializer::Single
    include RestPack::Serializer::Attributes
    include RestPack::Serializer::Filterable
    include RestPack::Serializer::SideLoading
    include RestPack::Serializer::Sortable

    class InvalidInclude < Exception; end

    def as_json(model, context = {})
      return if model.nil?
      if model.kind_of?(Array)
        return model.map { |item| self.class.new.as_json(item, context) }
      end

      apply_whitelist_and_blacklist(context)
      @model, @context = model, context

      data = {}
      if self.class.serializable_attributes.present?
        self.class.serializable_attributes.each do |key, attribute|
          method_name = attribute[:include_method_name]
          name = attribute[:name]
          if self.class.memoized_has_user_defined_method?(method_name)
            data[key] = self.send(name) if self.send(method_name)
          else
            #the default implementation of `include_abc?`
            if @context[method_name].nil? || @context[method_name]
              data[key] = self.send(name)
            end
          end
        end
      end

      add_custom_attributes(data)
      add_links(model, data) if self.class.has_associations?

      data
    end

    def to_json(model, context = {})
      as_json(model, context).to_json
    end

    def custom_attributes
      nil
    end

    private

    def add_custom_attributes(data)
      custom = custom_attributes
      data.merge!(custom) if custom
    end

    def apply_whitelist_and_blacklist(context)
      blacklist = context[:attribute_blacklist]
      whitelist = context[:attribute_whitelist]

      if blacklist.present? && whitelist.present?
        raise ArgumentError.new "the context can't define both an `attribute_whitelist` and an `attribute_blacklist`"
      end

      if blacklist.present?
        blacklist = csv_to_symbol_array(blacklist)
        self.class.serializable_attributes.each do |key, value|
          if blacklist.include? key
            context[value[:include_method_name]] = false
          end
        end
      end

      if whitelist.present?
        whitelist = csv_to_symbol_array(whitelist)
        self.class.serializable_attributes.each do |key, value|
          unless whitelist.include? key
            context[value[:include_method_name]] = false
          end
        end
      end
    end

    def csv_to_symbol_array(maybe_csv)
      if maybe_csv.is_a? String
        maybe_csv.split(',').map {|a| a.strip.to_sym}
      else
        maybe_csv
      end
    end

    def add_links(model, data)
      self.class.associations.each do |association|
        data[:links] ||= {}
        links_value = case
        when association.macro == :belongs_to
          model.send(association.foreign_key).try(:to_s)
        when association.macro.to_s.match(/has_/)
          if model.send(association.name).loaded?
            model.send(association.name).collect { |associated| associated.id.to_s }
          else
            model.send(association.name).pluck(:id).map(&:to_s)
          end
        end
        unless links_value.blank?
          data[:links][association.name.to_sym] = links_value
        end
      end
      data
    end

    module ClassMethods
      attr_accessor :model_class, :href_prefix, :key, :user_defined_methods, :track_defined_methods

      def method_added(name)
        #we track used defined methods so that we can make quick decisions at runtime
        @user_defined_methods ||= []
        if @track_defined_methods
          @user_defined_methods << name
        end
      end

      def has_user_defined_method?(method_name)
        if user_defined_methods && user_defined_methods.include?(method_name)
          true
        elsif superclass.respond_to?(:has_user_defined_method?)
          superclass.has_user_defined_method?(method_name)
        else
          false
        end
      end

      def memoized_has_user_defined_method?(method_name)
        @memoized_user_defined_methods ||= {}

        if @memoized_user_defined_methods.has_key? method_name
          return @memoized_user_defined_methods[method_name]
        else
          has_method = has_user_defined_method?(method_name)
          @memoized_user_defined_methods[method_name] = has_method
          return has_method
        end
      end

      def array_as_json(models, context = {})
        new.as_json(models, context)
      end

      def as_json(model, context = {})
        new.as_json(model, context)
      end

      def to_json(model, context = {})
        new.as_json(model, context).to_json
      end

      def serialize(models, context = {})
        models = [models] unless models.kind_of?(Array)

        {
          self.key() => models.map {|model| self.as_json(model, context)}
        }
      end

      def model_class
        @model_class || self.name.chomp('Serializer').constantize
      end

      def href_prefix
        @href_prefix || RestPack::Serializer.config.href_prefix
      end

      def key
        (@key || self.model_class.send(:table_name)).to_sym
      end

      def singular_key
        self.key.to_s.singularize.to_sym
      end

      def plural_key
        self.key
      end
    end
  end
end