module Enumbler::Enabler::ClassMethods
These ClassMethods
can be included in any model that you wish to Enumble!
class Color < ApplicationRecord include Enumbler::Enabler enumble :black, 1 enumble :white, 2 end
Constants
- ENUMBLER_CONFLICT_MESSAGE
This idea sourced lovingly from ActiveRecord::Enum
Attributes
Public Instance Methods
Defines an Enumble
for this model. An enum with integrity.
# in your migration create_table :colors, force: true do |t| t.string :label, null: false, index: { unique: true } end class Color < ApplicationRecord include Enumbler::Enabler enumble :black, 1, hex: '000000' enumble :white, 2, hex: 'ffffff' enumble :dark_brown, 3, # label: 'dark-brown' enumble :black_hole, 3, label: 'Oh my! It is a black hole!' end # Dynamically adds the following methods: Color::BLACK #=> 1 Color.black #=> Color.find(1) color.black? #=> true || false color.is_black #=> true || false
@param enum [Symbol] the enum representation @param id [Integer] the primary key value @param label [String] optional: label for humans @param **attributes [Hash] optional: additional attributes and values that
will be saved to the database for this enumble record
# File lib/enumbler/enabler.rb, line 85 def enumble(enum, id, label: nil, **attributes) raise_error_if_model_does_not_support_attributes(attributes) id = validate_id_is_numeric(enum, id) @enumbles ||= Enumbler::Collection.new @enumbled_model = self @enumbler_label_column_name ||= :label enumble = Enumble.new(enum, id, label: label, label_column_name: @enumbler_label_column_name, **attributes) if @enumbles.include?(enumble) raise Error, "You cannot add the same Enumble twice! Attempted to add: #{enum}, #{id}." end define_dynamic_methods_and_constants_for_enumbled_model(enumble) @enumbles << enumble end
By default, the Enumbler
is expecting a table with an underlying column named `label` that represents the enum in the database. You can change this by calling `enumber_label_column_name` before you `enumble`!
ActiveRecord::Schema.define do create_table :feelings, force: true do |t| t.string :emotion, null: false, index: { unique: true } end end class Feeling < ApplicationRecord # @!parse extend Enumbler::Enabler::ClassMethods include Enumbler::Enabler enumbler_label_column_name :emotion enumble :sad, 1 enumble :happy, 2 enumble :verklempt, 3, label: 'overcome with emotion' end
# File lib/enumbler/enabler.rb, line 125 def enumbler_label_column_name(label_column_name) @enumbler_label_column_name = label_column_name end
Like `ActiveRecord#find_by`, will try and return an instance of this model that matches any of our enumble attributes (instance, id, string, or symbol).
Color.find_by_enumble(1) Color.find_by_enumble(:black) Color.find_by_enumble("black") Color.find_by_enumble("BLACK") Color.find_by_enumble(Color.black) # => self Color.find_by_enumble("whoops") # => nil
@param arg [Class, String, Integer, Symbol] search argument @param case_sensitive [Boolean] string search to be case sensitive (default: false) @param raise_error [Boolean] whether to raise RecordNotFound error (default: false) @return [self]
# File lib/enumbler/enabler.rb, line 144 def find_by_enumble(arg, case_sensitive: false, raise_error: false) return arg if arg.instance_of?(@enumbled_model) id = id_from_enumbler(arg, case_sensitive: case_sensitive, raise_error: raise_error) find_by = raise_error ? :find_by! : :find_by @enumbled_model.public_send(find_by, id: id) rescue Enumbler::Error raise ActiveRecord::RecordNotFound.new("Couldn't find #{@enumbled_model}", @enumbled_model) end
Like `ActiveRecord#find`, will try and return an instance of this model that matches any of our enumble attributes (instance, id, string, or symbol) raises a `RecordNotFound` error if none found.
Color.find_by_enumble!(1) Color.find_by_enumble!(:black) Color.find_by_enumble!("black") Color.find_by_enumble!("BLACK") Color.find_by_enumble!(Color.black) # => returns self Color.find_by_enumble!("whoops") # => raise ActiveRecord::RecordNotFound
@param arg [Class, String, Integer, Symbol] search argument @param case_sensitive [Boolean] string search to be case sensitive (default: false) @param raise_error [Boolean] whether to raise RecordNotFound error (default: false) @return [self]
# File lib/enumbler/enabler.rb, line 170 def find_by_enumble!(arg, case_sensitive: false) find_by_enumble(arg, case_sensitive: case_sensitive, raise_error: true) end
See {.find_enumbles}. Simply returns the first object. Use when you want one argument to be found and not returned in an array. @raise [Error] when there is no [Enumbler::Enumble] to be found and
`raise_error: true`
@param args [Integer, String, Symbol] @param case_sensitive [Boolean] should a String search be case sensitive
(default: false)
@param raise_error [Boolean] raise an error if not found (default:
false)
@return [Enumbler::Enumble]
# File lib/enumbler/enabler.rb, line 184 def find_enumble(arg, case_sensitive: false, raise_error: false) find_enumbles(arg, case_sensitive: case_sensitive, raise_error: raise_error).first end
See {.find_enumbles}. Simply returns the first object. Use when you want one argument to be found and not returned in an array. Raises error if none found. @raise [Error] when there is no [Enumbler::Enumble] to be found and
`raise_error: true`
@param args [Integer, String, Symbol] @param case_sensitive [Boolean] should a String search be case sensitive
(default: false)
@return [Enumbler::Enumble]
# File lib/enumbler/enabler.rb, line 197 def find_enumble!(arg, case_sensitive: false) find_enumbles(arg, case_sensitive: case_sensitive, raise_error: true).first end
Finds an array of {Enumbler::Enumble} objects matching the given argument. Accepts an Integer, String, Symbol
, or ActiveRecord instance.
This method is designed to let you get information about the record without having to hit the database. Returns `nil` when none found unless `raise_error` is `true`.
Color.find_enumbles(:black, 'white', 'not-found') #=> [Enumbler::Enumble<:black>, Enumbler::Enumble<:white>, nil]
@raise [Error] when there is no [Enumbler::Enumble] to be found and
`raise_error: true`
@param args [Integer, String, Symbol] @param case_sensitive [Boolean] should a String search be case sensitive
(default: false)
@param raise_error [Boolean] raise an error if not found (default:
false)
@return [Array<Enumbler::Enumble>]
# File lib/enumbler/enabler.rb, line 219 def find_enumbles(*args, case_sensitive: false, raise_error: false) args.flatten.compact.uniq.map do |arg| err = "Unable to find a #{@enumbled_model}#enumble with #{arg}" begin arg = Integer(arg) # raises Type error if not a real integer enumble = @enumbled_model.enumbles.find { |e| e.id == arg } rescue TypeError, ArgumentError enumble = if arg.is_a?(Symbol) @enumbled_model.enumbles.find { |e| e.enum == arg } elsif arg.is_a?(Enumbler::Enumble) @enumbled_model.enumbles.find { |e| e.enum == arg.enum } elsif arg.is_a?(String) @enumbled_model.enumbles.find do |e| if case_sensitive [e.label, e.enum.to_s].include?(arg) else arg.casecmp?(e.label) || arg.casecmp?(e.enum.to_s) end end elsif arg.instance_of?(@enumbled_model) arg.enumble end end if enumble.present? enumble else raise Error if raise_error nil end rescue Error raise Error, err end end
See {.find_enumbles}. Same method, only raises error when none found. @raise [Error] when there is no [Enumbler::Enumble] to be found @param args [Integer, String, Symbol] @param case_sensitive [Boolean] should a String search be case sensitive
(default: false)
@return [Array<Enumbler::Enumble>]
# File lib/enumbler/enabler.rb, line 263 def find_enumbles!(*args, case_sensitive: false) find_enumbles(*args, case_sensitive: case_sensitive, raise_error: true) end
Return the record id for a given argument. Can accept an Integer, a Symbol
, or an instance of Enumbled model. This lookup is a database-free lookup.
Color.id_from_enumbler(1) # => 1 Color.id_from_enumbler(:black) # => 1 Color.id_from_enumbler(Color.black) # => 1
@raise [Error] when there is no enumble to be found @param arg [Integer, Symbol
, Class] @param case_sensitive [Boolean] should a string search be performed with
case sensitivity (default: false)
@param raise_error [Boolean] raise an error if not found (default:
false)
@return [Integer]
# File lib/enumbler/enabler.rb, line 282 def id_from_enumbler(arg, case_sensitive: false, raise_error: false) ids_from_enumbler(arg, case_sensitive: case_sensitive, raise_error: raise_error).first end
See {.ids_from_enumbler}. Raises error if none found. @raise [Error] when there is no enumble to be found @param arg [Integer, Symbol
, Class] @param case_sensitive [Boolean] should a string search be performed with
case sensitivity (default: false)
@param raise_error [Boolean] raise an error if not found (default:
false)
@return [Integer]
# File lib/enumbler/enabler.rb, line 294 def id_from_enumbler!(arg, case_sensitive: false) ids_from_enumbler(arg, case_sensitive: case_sensitive, raise_error: true).first end
Return the record id(s) based on different argument types. Can accept an Integer, a Symbol
, or an instance of Enumbled model. This lookup is a database-free lookup.
Color.ids_from_enumbler(1, 2) # => [1, 2] Color.ids_from_enumbler(:black, :white) # => [1, 2] Color.ids_from_enumbler('black', :white) # => [1, 2] Color.ids_from_enumbler(Color.black, Color.white) # => [1, 2]
@raise [Error] when there is no enumble to be found @param *args [Integer, Symbol
, Class] @param case_sensitive [Boolean] should a string search be performed with
case sensitivity (default: false)
@param raise_error [Boolean] raise an error if not found (default:
false)
@return [Array<Integer>]
# File lib/enumbler/enabler.rb, line 314 def ids_from_enumbler(*args, case_sensitive: false, raise_error: false) enumbles = find_enumbles(*args, case_sensitive: case_sensitive, raise_error: raise_error) enumbles.map { |e| e&.id } end
See {.ids_from_enumbler}. Raises error when none found. @raise [Error] when there is no enumble to be found @param *args [Integer, Symbol
, Class] @param case_sensitive [Boolean] should a string search be performed with
case sensitivity (default: false)
@return [Array<Integer>]
# File lib/enumbler/enabler.rb, line 325 def ids_from_enumbler!(*args, case_sensitive: false) enumbles = find_enumbles!(*args, case_sensitive: case_sensitive) enumbles.map(&:id) end
Seeds the database with the Enumbler
data. @param delete_missing_records [Boolean] remove any records that are no
longer defined (default: false)
@param validate [Boolean] validate on save?
# File lib/enumbler/enabler.rb, line 334 def seed_the_enumbler(delete_missing_records: false, validate: true) max_database_id = all.order("id desc").take&.id || 0 max_enumble_id = @enumbles.map(&:id).max # If we are not deleting records, we just need to update each listed # enumble and skip anything else in the database. If we are deleting # records, we need to know the max database id. iterator = if !delete_missing_records @enumbles.map(&:id) elsif max_enumble_id > max_database_id (1..max_enumble_id) else (1..max_database_id) end discarded_ids = [] iterator.each do |id| enumble = @enumbles.find { |e| e.id == id } if enumble.nil? discarded_ids << id next end record = find_or_initialize_by(id: id) record.attributes = enumble.attributes record.save!(validate: validate) end where(id: discarded_ids).delete_all if delete_missing_records end
Seeds the database with the Enumble
data, removing any records that are no longer defined. @param validate [Boolean] validate on save?
# File lib/enumbler/enabler.rb, line 370 def seed_the_enumbler!(validate: true) seed_the_enumbler(delete_missing_records: true, validate: validate) end
Private Instance Methods
# File lib/enumbler/enabler.rb, line 376 def define_dynamic_methods_and_constants_for_enumbled_model(enumble) method_name = "#{enumble.enum}?" not_method_name = "not_#{enumble.enum}?" alias_method_name = "is_#{enumble.enum}" any_method_name = "any_#{enumble.enum}?" [method_name, not_method_name, alias_method_name].each do |mname| detect_enumbler_conflict(enumble.enum, mname) end [enumble.enum, any_method_name].each do |mname| detect_enumbler_conflict(enumble.enum, mname, klass_method: true) end const_set(enumble.enum.to_s.upcase, enumble.id) define_method(method_name) { id == enumble.id } define_method(not_method_name) { id != enumble.id } alias_method alias_method_name, method_name define_singleton_method(enumble.enum) do |attr = nil| return find(enumble.id) if attr.nil? enumble.send(attr) rescue NoMethodError raise Enumbler::Error, "The attribute #{attr} is not supported on this Enumble." end define_singleton_method(any_method_name) do exists?(id: enumble.id) rescue NoMethodError raise Enumbler::Error, "The attribute #{attr} is not supported on this Enumble." end end
# File lib/enumbler/enabler.rb, line 417 def detect_enumbler_conflict(enumble_name, method_name, klass_method: false) if klass_method && dangerous_class_method?(method_name) raise_conflict_error(enumble_name, method_name, type: "class") elsif klass_method && method_defined_within?(method_name, ActiveRecord::Relation) raise_conflict_error(enumble_name, method_name, type: "class", source: ActiveRecord::Relation.name) elsif !klass_method && dangerous_attribute_method?(method_name) raise_conflict_error(enumble_name, method_name) end end
# File lib/enumbler/enabler.rb, line 427 def raise_conflict_error(enumble_name, method_name, type: "instance", source: "ActiveRecord") raise Error, format( ENUMBLER_CONFLICT_MESSAGE, enum: enumble_name, klass: name, type: type, method: method_name, source: source, ) end
# File lib/enumbler/enabler.rb, line 448 def raise_error_if_model_does_not_support_attributes(attributes) return if attributes.blank? unsupported_attrs = attributes.reject { |key, _value| has_attribute?(key) } return if unsupported_attrs.blank? ActiveRecord::Migration.check_pending! raise Enumbler::Error, "The model #{self} does not support the attribute(s): #{unsupported_attrs.keys.map(&:to_s).to_sentence}" rescue ActiveRecord::PendingMigrationError warn "[Enumbler Warning] => The model #{self} does not currently support the attribute(s): #{unsupported_attrs.keys.map(&:to_s).to_sentence}." \ " You have a pending migration which hopefully would remedy this! If not, you need to add a migration for this attibrute or" \ " remove it from the Enumbler." rescue ActiveRecord::StatementInvalid warn "[Enumbler Warning] => Unable to find a table for #{self}."\ "This is to be expected if there is a pending migration; however, if there is not then something is amiss." end
I accidentally forgot to provide an id one time and it was confusing as the last argument became the hash of options. This should help.
# File lib/enumbler/enabler.rb, line 441 def validate_id_is_numeric(enum, id) Integer(id) rescue ArgumentError, TypeError raise Enumbler::Error, "You must provide a numeric primary key, like: `enumble :#{enum}, 1 `" end