joshuabowers/opium

View on GitHub
lib/opium/model/relation.rb

Summary

Maintainability
A
2 hrs
Test Coverage
module Opium
  module Model
    class Relation < Criteria
      include ActiveModel::Dirty
      
      class << self
        def to_parse( object )
          class_name = determine_class_name_from( object, 'parse Relation hash' )
          fail ArgumentError, "could not determine class_name from #{ object.inspect }" unless class_name
          { __type: 'Relation', className: class_name }.with_indifferent_access
        end
        
        def to_ruby( object )
          return if object.nil? || object == []
          return object if object.is_a? self
          class_name = determine_class_name_from( object, self.name )
          new( class_name ).tap do |relation|
            case object
            when String
            when is_descendant.curry[Opium::Model]
              relation.push object
            when contains_descendant.curry[Opium::Model]
              object.select(&is_descendant.curry[Opium::Model]).each {|model| relation.push model}
            end
          end
        end
        
        private
        
        def determine_class_name_from( object, context )
          case object
          when Hash
            fetch_hash_key_from( object, 'class_name' ) || fetch_hash_key_from( object, 'model_name' )
          when String, Symbol
            object
          when is_descendant.curry[Opium::Model]
            object.model_name
          when contains_descendant.curry[Opium::Model]
            object.select(&is_descendant.curry[Opium::Model]).first.model_name
          when self
            object.class_name
          else
            fail ArgumentError, "could not convert #{ object.inspect } to a #{ context }"
          end
        end
        
        def is_descendant
          @is_descendant ||= ->( expected_type, object ) { ( object.is_a?( Class ) ? object : object.class ) <= expected_type }
        end
        
        def contains_descendant
          @contains_descendant ||= ->( expected_type, object ) do 
            object.is_a?( Enumerable ) ? object.any?( &is_descendant.curry[expected_type] ) : is_descendant.curry[expected_type]
          end
        end
        
        def fetch_hash_key_from( hash, key )
          snake_case_key = key.to_s.underscore
          lower_camel_key = key.to_s.camelcase(:lower)
          
          hash[ snake_case_key ] || hash[ snake_case_key.to_sym ] || hash[ lower_camel_key ] || hash[ lower_camel_key.to_sym ]
        end
      end
      
      def initialize( model_name )
        super
        update_variable!( :cache, true )
      end
      
      def to_parse
        self.class.to_parse self
      end
      
      def empty?
        owner.nil? || owner.new_record? ? true : super
      end
      
      attr_reader :owner, :metadata
      
      def owner=(value)
        @owner = value
        update_constraint!( :where, '$relatedTo' => { 'object' => value.to_parse } )
      end
      
      def metadata=(value)
        @metadata = value
        update_constraint!( :where, '$relatedTo' => { 'key' => value.relation_name.to_s } )
      end
      
      alias_method :class_name, :model_name
      
      #TODO: likely will need to reimplement .each
      
      def each(&block)
        if !block_given?
          to_enum(:each)
        else
          super() {|model| block.call( model ) unless __deletions__.include?( model ) }
          (__additions__ - __deletions__).each(&block)
        end
      end
      
      def push( object )
        __additions__.push( object )
        self
      end
      
      alias_method :<<, :push
      
      def delete( object )
        __deletions__.push( object )
        self
      end
      
      def build( params = {} )
        model.new( params || {} ).tap do |instance|
          push instance
        end
      end
      
      alias_method :new, :build
      
      def save
        self.reject {|model| model.persisted?}.each(&:save)
        __apply_additions__
        __apply_deletions__
        true
      end
      
      def parse_response
        @parse_response ||= []
      end
      
      private
      
      def __relation_deltas__
        @__relation_deltas__ ||= {}
      end
      
      def __additions__
        __relation_deltas__[:additions] ||= []
      end
      
      def __deletions__
        __relation_deltas__[:deletions] ||= []
      end
      
      def __apply_additions__
        unless __additions__.empty?
          parse_response << owner.class.http_put( owner.id, { metadata.relation_name => { __op: 'AddRelation', objects: __additions__.map(&:to_parse) } } )
          @cache.concat( __additions__ )
          __additions__.clear
        end
      end
      
      def __apply_deletions__
        unless __deletions__.empty?
          parse_response << owner.class.http_put( owner.id, { metadata.relation_name => { __op: 'RemoveRelation', objects: __deletions__.map(&:to_parse) } } )
          @cache = @cache - __deletions__
          __deletions__.clear
        end
      end
    end
  end
end