distil/jserializer

View on GitHub
lib/jserializer/base.rb

Summary

Maintainability
A
3 hrs
Test Coverage
module Jserializer
  class Base
    @_attributes = {}
    @_embed = :objects

    class << self
      attr_accessor :_attributes, :_root_key, :_embed

      def attributes(*names)
        names.each { |name| _add_attribute(name, nil) }
      end

      def attribute(name, key: nil)
        _add_attribute(name, key)
      end

      def root(name)
        self._root_key = name
      end

      # Define how associations should be embedded.
      #
      #   embed :objects # Embed associations as full objects
      #   embed :ids     # Embed only the association ids
      #
      def embed(type = :objects)
        self._embed = type
      end

      # embed: :objects || :ids
      # the embed_key only works when embed: ids
      def has_many(name, serializer: nil, key: nil, embed: nil, embed_key: nil)
        association = _build_association(
          name, :has_many, key, serializer, embed, embed_key
        )
        _add_attribute(name, key, association: association)
      end

      def has_one(name, serializer: nil, key: nil, embed: nil, embed_key: nil)
        association = _build_association(
          name, :has_one, key, serializer, embed, embed_key
        )
        _add_attribute(name, key, association: association)
      end

      def _build_association(name, type, key, serializer, embed, embed_key)
        id_only = embed == :ids || (embed.nil? && self._embed == :ids)
        Association.new(
          name, type,
          key: key, serializer: serializer,
          id_only: id_only, embed_key: embed_key
        )
      end

      def _add_attribute(name, key, association: nil)
        self._attributes[name] = {
          key: key,
          include_method: "include_#{name}?".to_sym
        }
        if association
          self._attributes[name][:association] = association
          self._attributes[name][:key] = association.key
        end
        access_name = association ? association.access_name : name
        generate_attribute_methods(name, access_name)
      end

      # Generate attribute access and inclusion check methods
      # This improves performance by avoiding method lookups like:
      #     public_send(name) if respond_to?(name)
      def generate_attribute_methods(name, access_name)
        class_eval <<-METHOD, __FILE__, __LINE__ + 1
          def #{name}
            if ::Hash === @object
              @object.fetch(:'#{access_name}')
            else
              @object.#{access_name}
            end
          end

          def include_#{name}?
            true
          end
        METHOD
      end

      def inherited(subclass)
        super(subclass)
        subclass._attributes = _attributes.dup
      end
    end

    attr_reader :object, :options, :scope

    alias :current_user :scope

    # supported options:
    #   root:
    #   meta:
    #   meta_key:
    #   scope or current_user:
    #   is_collection:
    #   only: []
    #   except: []
    def initialize(object, options = {})
      @object = object
      @scope = options[:scope] || options[:current_user]
      @is_collection = options.delete(:is_collection) || false
      @options = options
      _update_attributes_filter
    end

    # reset object to reuse the serializer instance
    # clear any cached or memoized things
    def reset(object)
      @object = object
    end

    # Returns a hash representation without the root
    def serializable_hash
      return serializable_collection if collection?
      self.class._attributes.each_with_object({}) do |(name, option), hash|
        if _include_from_options?(name) && public_send(option[:include_method])
          hash[option[:key] || name] = _set_value(name, option)
        end
      end
    end

    def serializable_collection
      serializer_object = self.class.new(nil, @options)
      @object.map do |record|
        serializer_object.reset(record)
        serializer_object.serializable_hash
      end
    end

    def collection?
      @is_collection
    end

    def root_name
      return nil if @options[:root] == false
      @options[:root] || self.class._root_key
    end

    def meta_key
      @options[:meta_key] || :meta
    end

    def to_json(*)
      JSON.generate(as_json)
    end

    # Returns a hash representation with the root
    # Available options:
    # :root => true or false
    def as_json(options = {})
      root = options.key?(:root) ? options[:root] : true
      hash = if root && root_name
               { root_name => serializable_hash }
             else
               serializable_hash
             end
      hash[meta_key] = @options[:meta] if @options.key?(:meta)
      hash
    end

    private

    def _include_from_options?(name)
      return true unless @attributes_filter_list
      return @filter_include_return if @attributes_filter_list.include?(name)
      !@filter_include_return
    end

    def _set_value(name, option)
      if option.key?(:association)
        return _build_from_association(name, option[:association])
      end
      public_send(name)
    end

    def _build_from_association(name, association)
      resource = public_send(name)
      return resource if association.id_only
      association.serialize(resource)
    end

    # either only or except can be used to filter attributes
    def _update_attributes_filter
      if @options.key?(:only) && @options[:only].is_a?(Array)
        @attributes_filter_list = @options[:only]
        # if the attribute is included in :only, we will serialize it
        @filter_include_return = true
      elsif @options.key?(:except) && @options[:except].is_a?(Array)
        @attributes_filter_list = @options[:except]
        # if the attribute is included in :except, we will not serialize it
        @filter_include_return = false
      else
        @attributes_filter_list = nil
      end
    end
  end
end