twitter/activerecord-reputation-system

View on GitHub
lib/reputation_system/network.rb

Summary

Maintainability
B
6 hrs
Test Coverage
##
#  Copyright 2012 Twitter, Inc
#
#  Licensed under the Apache License, Version 2.0 (the "License");
#  you may not use this file except in compliance with the License.
#  You may obtain a copy of the License at
#
#  http://www.apache.org/licenses/LICENSE-2.0
#
#  Unless required by applicable law or agreed to in writing, software
#  distributed under the License is distributed on an "AS IS" BASIS,
#  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
#  See the License for the specific language governing permissions and
#  limitations under the License.
##

module ReputationSystem
  class Network
    class << self
      def has_reputation_for?(class_name, reputation_name)
        reputation_def = get_reputation_def(class_name, reputation_name)
        !!reputation_def[:source]
      end

      def get_reputation_defs(class_name)
        network[class_name.to_sym] ||= {}
      end

      def get_reputation_def(class_name, reputation_name)
        reputation_def = {}
        unless class_name == "ActiveRecord::Base"
          reputation_defs = get_reputation_defs(class_name)
          reputation_defs[reputation_name.to_sym] ||= {}
          reputation_def = reputation_defs[reputation_name.to_sym]
          if reputation_def == {}
            begin
              # This recursion finds reputation definition in the ancestor in case of STI.
              klass = class_name.constantize.superclass
              reputation_def = get_reputation_def(klass.name, reputation_name) if klass
            rescue NameError
              # Class might have not been initialized yet at this point.
            end
          end
        end
        reputation_def
      end

      def add_reputation_def(class_name, reputation_name, options)
        reputation_defs = get_reputation_defs(class_name)
        options[:source] = convert_to_array_if_hash(options[:source])
        options[:source_of] ||= []
        options[:source_of] = convert_to_array_if_hash(options[:source_of])
        options[:aggregated_by] = options[:aggregated_by] || :sum
        assign_self_as_default_value_for_of_attr(options[:source])
        assign_self_as_default_value_for_of_attr(options[:source_of])
        reputation_defs[reputation_name] = options
        options[:source].each do |s|
          src_class_name = derive_class_name_from_attribute(class_name, s[:of])
          if has_reputation_for?(src_class_name, s[:reputation])
            derive_source_of_from_source(class_name, reputation_name, s, src_class_name)
          else
            # Because the source class might not have been initialized at this time.
            derive_source_of_from_source_later(class_name, reputation_name, s, src_class_name)
          end
        end unless is_primary_reputation?(class_name, reputation_name)
        perform_derive_later(class_name, reputation_name)
        construct_scoped_reputation_options(class_name, reputation_name, options)
      end

      def remove_reputation_def(class_name, reputation_name)
        reputation_defs = get_reputation_defs(class_name)
        reputation_defs.delete(reputation_name.to_sym)
      end

      def is_primary_reputation?(class_name, reputation_name)
        options = get_reputation_def(class_name, reputation_name)
        options[:source].is_a?(Symbol)
      end

      def add_scope_for(class_name, reputation_name, scope)
        options = get_reputation_def(class_name, reputation_name)
        if has_scope?(class_name, reputation_name, scope)
          raise ArgumentError, "#{scope} is already defined for #{reputation_name}"
        else
          options[:scopes].push scope.to_sym if options[:scopes]
          create_scoped_reputation_def(class_name, reputation_name, scope, options)
        end
      end

      def has_scopes?(class_name, reputation_name)
        !get_reputation_def(class_name, reputation_name)[:scopes].nil?
      end

      def has_scope?(class_name, reputation_name, scope)
        scopes = get_reputation_def(class_name, reputation_name)[:scopes]
        scopes && scopes.include?(scope.to_sym)
      end

      def get_scoped_reputation_name(class_name, reputation_name, scope)
        raise ArgumentError, "#{reputation_name.to_s} is not defined for #{class_name}" unless has_reputation_for?(class_name, reputation_name)
        scope = scope.to_sym if scope
        validate_scope_necessity(class_name, reputation_name, scope)
        validate_scope_existence(class_name, reputation_name, scope)
        "#{reputation_name}#{"_#{scope}" if scope}"
      end

      def get_weight_of_source_from_reputation_name_of_target(target, source_name, reputation_name)
        source = get_reputation_def(target.class.name, reputation_name)[:source]
        if source.is_a?(Array)
          source.each do |s|
            srn = get_scoped_reputation_name_from_source_def_and_target(s, target)
            return s[:weight] if srn.to_sym == source_name.to_sym
          end
        else
          source[:weight]
        end
      end

      protected

        def network
          @network ||= {}
        end

        def data_for_derive_later
          @data_for_derive_later ||= {}
        end

        def create_scoped_reputation_def(class_name, reputation_name, scope, options)
          raise ArgumentError, "#{reputation_name} does not have scope." unless has_scopes?(class_name, reputation_name)
          scope_options = options.reject { |k, v| ![:source, :aggregated_by].include? k }
          reputation_def = get_reputation_def(class_name, reputation_name)
          unless is_primary_reputation?(class_name, reputation_name)
            scope_options[:source] = []
            reputation_def[:source].each { |sd| scope_options[:source].push create_source_reputation_def(sd, scope) }
          end
          (reputation_def[:source_of] || []).each do |so|
            if source_of_defined_for_scope?(so, scope)
              scope_options[:source_of] ||= []
              scope_options[:source_of].push so
            end
          end
          srn = get_scoped_reputation_name(class_name, reputation_name, scope)
          network[class_name.to_sym][srn.to_sym] = scope_options
        end

        def create_source_reputation_def(source_def, scope)
          rep = {}
          rep[:reputation] = source_def[:reputation]
          # Passing "this" is not pretty but in some case "instance_exec" method
          # does not give right context for some reason.
          # This could be ruby bug. Needs further investigation.
          if source_def[:of].is_a? Proc
            rep[:of] = lambda { |this| instance_exec(this, scope.to_s, &source_def[:of]) }
          else
            rep[:of] = source_def[:of]
          end
          rep
        end

        def get_scoped_reputation_name_from_source_def_and_target(source_def, target)
          scope = target.evaluate_reputation_scope(source_def[:scope]) if source_def[:scope]
          of = target.get_attributes_of(source_def)
          class_name = (of.is_a?(Array) ? of[0] : of).class.name
          get_scoped_reputation_name(class_name, source_def[:reputation], scope)
        end

        def source_of_defined_for_scope?(source_of_def, scope)
          defined_for_scope = source_of_def[:defined_for_scope]
          defined_for_scope.nil? || (defined_for_scope && defined_for_scope.include?(scope.to_sym))
        end

        def construct_scoped_reputation_options(class_name, reputation_name, options)
          scopes = get_reputation_def(class_name, reputation_name)[:scopes]
          scopes.each do |scope|
            create_scoped_reputation_def(class_name, reputation_name, scope, options)
          end if scopes
        end

        def derive_source_of_from_source(class_name, reputation_name, source, src_class_name)
          of_value = derive_of_value(class_name, source[:of], src_class_name)
          reputation_def = get_reputation_def(src_class_name, source[:reputation])
          reputation_def[:source_of] ||= []
          unless source_of_include_reputation?(reputation_def[:source_of], reputation_name)
            reputation_def[:source_of] << {:reputation => reputation_name.to_sym, :of => of_value.to_sym}
          end
        end

        def derive_of_value(class_name, source_of, src_class_name)
          if not_source_of_self?(source_of)
            attr = class_name.tableize
            class_has_attribute?(src_class_name, attr) ? attr :  attr.chomp('s')
          else
            "self"
          end
        end

        def not_source_of_self?(source_of)
          source_of && source_of.is_a?(Symbol) && source_of != :self
        end

        def class_has_attribute?(class_name, attribute)
          klass = class_name.to_s.constantize
          klass.instance_methods.include?(attribute.to_s) || klass.instance_methods.include?(attribute.to_sym)
        end

        def source_of_include_reputation?(source_of, reputation_name)
          source_of.map { |rep| rep[:reputation] }.include?(reputation_name.to_sym)
        end

        def derive_source_of_from_source_later(class_name, reputation_name, source, src_class_name)
          reputation = source[:reputation].to_sym
          src_class_name = src_class_name.to_sym
          data = data_for_derive_later
          data[src_class_name] ||= {}
          data[src_class_name][reputation] ||= {}
          data[src_class_name][reputation].merge!(:source => source, :class_name => class_name, :reputation_name => reputation_name)
        end

        def perform_derive_later(src_class_name, reputation)
          src_class_name = src_class_name.to_sym
          reputation = reputation.to_sym
          data = data_for_derive_later
          if data[src_class_name] && data[src_class_name][reputation]
            class_name = data[src_class_name][reputation][:class_name]
            source = data[src_class_name][reputation][:source]
            reputation_name = data[src_class_name][reputation][:reputation_name]
            derive_source_of_from_source(class_name, reputation_name, source, src_class_name)
            data[src_class_name].delete(reputation)
          end
        end

        def derive_class_name_from_attribute(class_name, attribute)
          if attribute && attribute != :self && attribute != "self"
            attribute.to_s.camelize.chomp('s')
          else
            class_name
          end
        end

        def convert_to_array_if_hash(tar)
          tar.is_a?(Hash) ? [tar] : tar
        end

        def assign_self_as_default_value_for_of_attr(tar)
          tar.each { |s| s[:of] = :self unless s[:of] } if tar.is_a? Array
        end

        def validate_scope_necessity(class_name, reputation_name, scope)
          if scope.nil? && has_scopes?(class_name, reputation_name)
            raise ArgumentError, "Evaluations of #{reputation_name} must have scope specified."
          end
        end

        def validate_scope_existence(class_name, reputation_name, scope)
          if !scope.nil? && !has_scope?(class_name, reputation_name, scope)
            raise ArgumentError, "#{reputation_name} does not have scope #{scope}"
          end
        end
    end
  end
end