spieker/tokens

View on GitHub
lib/active_record/mixin/token_generator.rb

Summary

Maintainability
A
2 hrs
Test Coverage
# This mixin provides methods on ActiveRecord::Base to generate a token for
# a given column. The token for the column can have a specific length, can
# be uniq and the uniqueness can be scoped on a given column.
#
module ActiveRecord
  module Mixin
    module TokenGenerator
      module InstanceMethods
        # Generates a token for the given active record column
        #
        # == Parameters
        # * columns: the column of the active record as symbol or an array of columns
        # * options: some options for generating the token
        # ** :length: the token length (default: 8)
        # ** :uniq: whether the token must be uniq or not (default: true)
        # ** :scope: the column for the scope to check the uniqueness (default: nil)
        # ** :same_token: if an array of columns is given and this option is true, all columns will get the same token (default: false)
        # ** :characters: an array of characters used for token generation
        #
        # == Example
        #   class User < ActiveRecord::Base
        #     before_validation do |obj|
        #       obj.generate_token :email_confirmation_token, :length => 16
        #     end
        #   end
        #
        def generate_token(columns, *args)
          options = {
            :length     => 8,
            :uniq       => true,
            :scope      => nil,
            :same_token => false,
            :max_try    => 8,
            :characters => ('a'..'z').to_a+('A'..'Z').to_a+(0..9).to_a
          }.merge(args.extract_options!)

          columns = [columns] unless columns.is_a?(Array)
          if options[:length].respond_to?(:call)
            options[:length] = options[:length].call(self)
          end

          result = {}
          token = nil
          columns.each do |column|
            counter = 0
            begin
              raise NoFreeToken.new(column) if counter >= options[:max_try]
              counter += 1

              token = new_token(options[:length], options[:characters]) if(token.blank? or not options[:same_token])
              self.send("#{column}=".to_sym, token)
              result[column.to_sym]      = token
              condition                  = { column => token }
              condition[options[:scope]] = self[options[:scope]] if options[:scope].is_a?(Symbol) or options[:scope].is_a?(String)
            end while options[:uniq] and self.class.exists?(condition)
          end
          result
        end

      end

      module ClassMethods
        # this method generates a token for the given columns before there get validated. Beside the
        # options described at the +generate_token+ method, you can use the following options.
        #
        # :on: (default: :create)
        #
        def tokenize(column, *args)
          options = {
            :on => :create
          }.merge(args.extract_options!)
          before_validation_options = options.reject { |k,v| [:length, :uniq, :scope, :same_token, :characters].include?(k) }
          options.select! { |k,v| [:length, :uniq, :scope, :same_token, :characters].include?(k) }

          before_validation before_validation_options do |obj|
            obj.generate_token column, options
          end
        end
      end

      def self.included(receiver)
        receiver.send :include, InstanceMethods
        receiver.extend ClassMethods
      end
    end
  end
end

ActiveRecord::Base.send :include, ActiveRecord::Mixin::TokenGenerator