module Uniqueness module Model # :nodoc: def self.included(base) base.extend ClassMethods # Track all uniqueness arguments base.class_attribute :uniqueness_options end module ClassMethods # Adds random field support to Rails models # # Examples: # To auto-generate a new random string for field +foo+ # has_random_field :foo # # You can customize the generated string by # passing an options hash. The following keys are supported: # # +:length+ number of characters, defaults to 32 # # +:case_sensitive+ defaults to true # # +:type+ type of string, defaults to :auto # can be one of: :human, :auto # # +:blacklist+ characters to exclude when generating the random # string, defaults to [] # # +:scope+ defines the `ActiveRecord` `scope` applied before # calculating the `position` field value. # defaults to [] def has_unique_field(name, options = {}) self.uniqueness_options ||= {} self.uniqueness_options[name] = self.uniqueness_default_options.merge(options) before_validation :uniqueness_generate validate :uniqueness_validation end # Default sorting options def uniqueness_default_options { length: 32, type: :auto, blacklist: [], scope: [] } end end # Generates a new code based on given options def uniqueness_generate self.uniqueness_options.each do |field, options| value = send(field) unless value.present? dict = uniqueness_dictionary - options[:blacklist] dict -= [*(:A..:Z)].map(&:to_s) unless options[:case_sensitive] dict -= uniqueness_ambigious_dictionary if options[:type].to_sym == :human value = Array.new(options[:length]).map { dict[rand(dict.length)] }.join self.send("#{field}=", value) end end end # Dictionary used for uniqueness generation def uniqueness_dictionary [*(:A..:Z), *(:a..:z), *(0..9)].map(&:to_s) end def uniqueness_ambigious_dictionary [:b, :B, :o, :O, :q, :I, :l, :L, :s, :S, :u, :U, :z, :Z].map(&:to_s) end def uniqueness_validation self.class.uniqueness_options.each do |field, options| value = send(field) if value.nil? errors.add(field, 'should not be empty') else conditions = {} options[:scope].each do |s| conditions[s] = send(s) end conditions[field] = value query = self.class.where(conditions) errors.add(field, 'should be unique') if query.any? end end end end end