suweller/mongoid-autoinc

View on GitHub
lib/autoinc.rb

Summary

Maintainability
A
0 mins
Test Coverage
require 'autoinc/incrementor'

# namespace all +Mongoid::Autoinc+ functionality to +Mongoid+ to reduce
# possible clashes with other gems.
module Mongoid
  # Include module to allow defining of autoincrementing fields.
  #
  # @example
  #   class Invoice
  #     include Mongoid::Document
  #     include Mongoid::Autoinc
  #
  #     field :number, type: Integer
  #     increments :number
  #   end
  module Autoinc
    extend ActiveSupport::Concern

    AlreadyAssignedError = Class.new(StandardError)
    AutoIncrementsError = Class.new(StandardError)

    included { before_create(:update_auto_increments) }

    # +Mongoid::Autoinc+ class methods to allow for autoincrementing fields.
    module ClassMethods
      # Returns all incrementing fields of the document
      #
      # @example
      #   class Invoice
      #     include Mongoid::Document
      #     include Mongoid::Autoinc
      #
      #     field :number, type: Integer
      #     increments :number
      #   end
      #   Invoice.incrementing_fields # => {number: {auto: true}}
      #
      # @return [ Hash ] +Hash+ with fields and their autoincrement options
      def incrementing_fields
        if superclass.respond_to?(:incrementing_fields)
          @incrementing_fields ||= superclass.incrementing_fields.dup
        else
          @incrementing_fields ||= {}
        end
      end

      # Set an autoincrementing field for a +Mongoid::Document+
      #
      # @param [ Symbol ] field The name of the field to apply autoincrement to
      # @param [ Hash ] options The options to pass to that field
      #
      # @example
      #   class Invoice
      #     include Mongoid::Document
      #     include Mongoid::Autoinc
      #
      #     field :number, type: Integer
      #     increments :number
      #   end
      #
      # @example
      #   class User
      #     include Mongoid::Document
      #     include Mongoid::Autoinc
      #
      #     field :number, type: Integer
      #     increments :number, auto: false
      #   end
      def increments(field, options = {})
        incrementing_fields[field] = options.reverse_merge!(auto: true)
        attr_protected(field) if respond_to?(:attr_protected)
      end
    end

    # Manually assign the next number to the passed autoinc field.
    #
    # @raise [ Mongoid::Autoinc::AutoIncrementsError ] When `auto: true` is set
    # in the increments call for `field`
    # @raise [ AlreadyAssignedError ] When called more then once.
    #
    # @return [ Fixnum ] The assigned number
    def assign!(field)
      options = self.class.incrementing_fields[field]
      fail AutoIncrementsError if options[:auto]
      fail AlreadyAssignedError if send(field).present?
      increment!(field, options)
    end

    # Sets autoincrement values for all autoincrement fields.
    #
    # @return [ true ]
    def update_auto_increments
      self.class.incrementing_fields.each do |field, options|
        increment!(field, options) if options[:auto]
      end && true
    end

    # Set autoincrement value for the passed autoincrement field,
    # using the passed options
    #
    # @param [ Symbol ] field Field to set the autoincrement value for.
    # @param [ Hash ] options Options to pass through to the serializer.
    #
    # @return [ true ] The value of `write_attribute`
    def increment!(field, options)
      options = options.dup
      model_name = (options.delete(:model_name) || self.class.model_name).to_s
      options[:scope] = evaluate_scope(options[:scope]) if options[:scope]
      options[:step] = evaluate_step(options[:step]) if options[:step]
      write_attribute(
          field.to_sym,
          Mongoid::Autoinc::Incrementor.new(model_name, field, options).inc
      )
    end

    # Asserts the validity of the passed scope
    #
    # @param [ Object ] scope The +Symbol+ or +Proc+ to evaluate
    #
    # @raise [ ArgumentError ] When +scope+ is not a +Symbol+ or +Proc+
    #
    # @return [ Object ] The scope of the autoincrement call
    def evaluate_scope(scope)
      return send(scope) if scope.is_a? Symbol
      return instance_exec(&scope) if scope.is_a? Proc
      fail ArgumentError, 'scope is not a Symbol or a Proc'
    end

    # Returns the number to add to the current increment
    #
    # @param [ Object ] step The +Integer+ to be returned
    # or +Proc+ to be evaluated
    #
    # @raise [ ArgumentError ] When +step+ is not an +Integer+ or +Proc+
    #
    # @return [ Integer ] The number to add to the current increment
    def evaluate_step(step)
      return step if step.is_a? Integer
      return evaluate_step_proc(step) if step.is_a? Proc
      fail ArgumentError, 'step is not an Integer or a Proc'
    end

    # Executes a proc and returns its +Integer+ value
    #
    # @param [ Proc ] step_proc The +Proc+ to call
    #
    # @raise [ ArgumentError ] When +step_proc+ does not evaluate to +Integer+
    #
    # @return [ Integer ] The number to add to the current increment
    def evaluate_step_proc(step_proc)
      result = instance_exec(&step_proc)
      return result if result.is_a? Integer
      fail 'step Proc does not evaluate to an Integer'
    end
  end
end