lucasmedeirosleite/mongoid-followit

View on GitHub
lib/mongoid_followit/follower.rb

Summary

Maintainability
A
0 mins
Test Coverage
module Mongoid
  module Followit
    ##
    # Public: Module that add follower capabilities to a Mongoid model.
    #
    # Examples
    #
    #   class MyModel
    #     include Mongoid::Document
    #     include Mongoid::Followit::Follower
    #
    #     before_follow   :do_something_before
    #     before_unfollow :do_otherthing_before
    #
    #     after_follow   :do_something_after
    #     after_unfollow :do_otherthing_after
    #
    #   end
    module Follower
      def self.included(base)
        patch_class(base)
        generate_callbacks(base)
      end

      ##
      # Public: Check if model is following any other model.
      #
      # Examples
      #
      #   # => person = Person.create!(name: 'Skywalker')
      #   # => person.following?
      #
      # Returns true if model is following other model(s).
      # Returns false if model is not following any model.
      def following?
        Follow.where(follower_class: self.class.to_s, follower_id: id).count > 0
      end

      ##
      # Public: Check if model is following the desired model.
      #
      # followable - Mongoid::Followee model
      #
      # Examples
      #
      #   # => person.follows?(followable)
      #
      # Returns true if model is following the passed model.
      # Returns false if model is not following the passed model.
      def follows?(followable)
        has_followable_link?(:followee, followable)
      end

      ##
      # Public: Creates Follow entries, for the Followee models, representing
      #         the models that are being followed.
      #
      # *followees - A collection of Followee models to be followed.
      #
      # Examples
      #
      #   # => person = Person.create!(name: 'Skywalker')
      #   # => profile = Profile.create!(name: 'Jedi')
      #   # => person.follow(profile)
      #
      # Returns nothing.
      # Raises  Runtime error if at least one of the models passed does not
      #         include the Mongoid::Followit::Followee module.
      def follow(*followees)
        perform_followable_action(:follow, followees)
      end

      ##
      # Public: Destroys the Follow entries, for the Followee models,
      #         making the Followee models to be unfollowed.
      #
      # *followees - A collection of Followee models to be unfollowed.
      #
      # Examples
      #
      #   # => person.unfollow(jedi, padawan)
      #
      # Returns nothing.
      # Raises  Runtime error if at least one of the models passed does not
      #         include the Mongoid::Followit::Followee module.
      def unfollow(*followees)
        perform_followable_action(:unfollow, followees)
      end

      ##
      # Public: Destroys all Follow entries, for the Followee models,
      #         making the Followee models to be unfollowed.
      #
      # Examples
      #
      #   # => person.unfollow_all
      #
      # Returns nothing.
      def unfollow_all
        Follow.where(follower_class: self.class.to_s, follower_id: id).destroy_all
      end

      ##
      # Public: Peform a query to return all Mongoid model
      #         that model is following.
      #
      # criteria(optional) - if true the return will be the type of
      #                      Mongoid::Criteria
      #
      # Examples
      #
      #   class Person
      #     include Mongoid::Document
      #     include Mongoid::Follower
      #
      #     field :name, type: String
      #     validates_uniqueness_of :name
      #   end
      #
      #   # => Person.find_by(name: 'Skywalker').followees
      #
      # Returns An Array of followees if criteria argument is false.
      # Returns A Mongoid::Criteria of followees if  criteria argument is true
      #         and followees are of only one type
      # Raises  HasTwoFolloweeTypesError if criteria argument is true
      #         and model has two or more types of followees
      def followees(criteria: false)
        follow_collection_for_a(:followee, criteria: criteria)
      end

      ##
      # Public: Peform a query to return the total of Mongoid model
      #         that model is following.
      #
      # Examples
      #
      #   # => user.follow(another_user)
      #   # => user.follow(different_user)
      #   # => user.followees_count
      #   # => 2
      #
      # Returns 0 if model is not following anybody.
      # Returns The total of models that the model is following.
      def followees_count
        follow_count_for_a(:followee)
      end

      ##
      # Public: Peform a query to return all common Mongoid model followees.
      #
      # *followers - Mongoid::Followit::Follower models
      #
      # Examples
      #
      #   # => person.common_followees(a_person)
      #
      # Returns An Array of common followees
      def common_followees(*followers)
        all_followees = [followees] + followers.map(&:followees)
        all_followees.inject(:&)
      end

      private_class_method

      ##
      # Internal: Class method that configures the Mongoid model which has
      #           Mongoid::Followit::Follower model included.
      #
      # base - Mongoid model class which will be patched.
      #
      # PS: This method is used inside the .included method inside this Module.
      #
      # Returns nothing.
      def self.patch_class(base)
        base.class_eval do
          include Mongoid::Followit::Queryable
          include ActiveSupport::Callbacks
          define_callbacks :follow, :unfollow
          before_destroy :destroy_follow_data
        end
      end

      ##
      # Internal: Class method that generate callbacks
      #           for the #follow and #unfollow methods.
      #
      # base - Mongoid model class which will be patched.
      #
      # PS: This method is used inside the .included method inside this Module.
      #
      # Returns nothing.
      def self.generate_callbacks(base)
        %w(before after).each do |callback|
          %w(follow unfollow).each do |action|
            base.define_singleton_method("#{callback}_#{action}") do |*ar, &blo|
              set_callback(action.to_sym, callback.to_sym, *ar, &blo)
            end
          end
        end
      end

      private

      def perform_followable_action(action, followables)
        warn_non_unfollowee_existence(followables)
        run_callbacks(action) do
          followables.each do |followable|
            if :follow == action
              Follow.store_by!(followee: followable, follower: self)
            else
              Follow.destroy_by!(followee: followable, follower: self)
            end
          end
        end
      end

      def warn_non_unfollowee_existence(followees)
        unless followees.any? { |f| f.is_a?(Mongoid::Followit::Followee) }
          raise 'Object(s) must include a Mongoid::Followit::Followee'
        end
      end

      ##
      # Internal: Before destroy filter used to clean Mongoid model related
      #           data from the Follow collection.
      #
      #
      # PS: This method is used inside the .patch_class method
      #     inside this Module.
      #
      # Returns nothing.
      def destroy_follow_data
        Follow.destroy_followable_data(self)
      end
    end
  end
end