module Torque module PostgreSQL module Attributes module Builder class Enum VALID_TYPES = %i[enum enum_set].freeze attr_accessor :klass, :attribute, :subtype, :options, :values, :klass_module, :instance_module # Start a new builder of methods for enum values on ActiveRecord::Base def initialize(klass, attribute, options) @klass = klass @attribute = attribute.to_s @subtype = klass.attribute_types[@attribute] @options = options raise Interrupt unless subtype.respond_to?(:klass) @values = subtype.klass.values if @options[:only] @values &= Array(@options[:only]).map(&:to_s) end if @options[:except] @values -= Array(@options[:except]).map(&:to_s) end end # Get the list of methods based on enum values def values_methods return @values_methods if defined?(@values_methods) prefix = options.fetch(:prefix, nil).try(:<<, '_') suffix = options.fetch(:suffix, nil).try(:prepend, '_') prefix = attribute + '_' if prefix == true suffix = '_' + attribute if suffix == true base = "#{prefix}%s#{suffix}" @values_methods = begin values.map do |val| key = val.downcase.tr('- ', '__') scope = base % key ask = scope + '?' bang = scope + '!' [key, [scope, ask, bang, val]] end.to_h end end # Check if it's building the methods for sets def set_features? options[:set_features].present? end # Check if any of the methods that will be created get in conflict # with the base class methods def conflicting? return if options[:force] == true attributes = attribute.pluralize dangerous?(attributes, true) dangerous?("#{attributes}_keys", true) dangerous?("#{attributes}_texts", true) dangerous?("#{attributes}_options", true) dangerous?("#{attribute}_text") if set_features? dangerous?("has_#{attributes}", true) dangerous?("has_any_#{attributes}", true) end values_methods.each do |attr, (scope, ask, bang, *)| dangerous?(scope, true) dangerous?(bang) dangerous?(ask) end rescue Interrupt => err raise ArgumentError, <<-MSG.squish Enum #{subtype.name} was not able to generate requested methods because the method #{err} already exists in #{klass.name}. MSG end # Create all methods needed def build @klass_module = Module.new @instance_module = Module.new plural stringify all_values set_scopes if set_features? klass.extend klass_module klass.include instance_module end private # Check if the method already exists in the reference class def dangerous?(method_name, class_method = false) if class_method if klass.dangerous_class_method?(method_name) raise Interrupt, method_name.to_s end else if klass.dangerous_attribute_method?(method_name) raise Interrupt, method_name.to_s end end rescue Interrupt => e raise e if Torque::PostgreSQL.config.enum.raise_conflicting type = class_method ? 'class method' : 'instance method' indicator = class_method ? '.' : '#' Torque::PostgreSQL.logger.info(<<~MSG.squish) Creating #{class_method} :#{method_name} for enum. Overwriting existing method #{klass.name}#{indicator}#{method_name}. MSG end # Create the method that allow access to the list of values def plural enum_klass = subtype.klass.name klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{attribute.pluralize} # def roles ::#{enum_klass}.values # Enum::Roles.values end # end def #{attribute.pluralize}_keys # def roles_keys ::#{enum_klass}.keys # Enum::Roles.keys end # end def #{attribute.pluralize}_texts # def roles_texts ::#{enum_klass}.members.map do |member| # Enum::Roles.members do |member| member.text('#{attribute}', self) # member.text('role', self) end # end end # end def #{attribute.pluralize}_options # def roles_options #{attribute.pluralize}_texts.zip(::#{enum_klass}.values) # roles_texts.zip(Enum::Roles.values) end # end RUBY end # Create additional methods when the enum is a set, which needs # better ways to check if values are present or not def set_scopes cast_type = subtype.name.chomp('[]') klass_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def has_#{attribute.pluralize}(*values) # def has_roles(*values) attr = arel_attribute('#{attribute}') # attr = arel_attribute('role') where(attr.contains(::Arel.array(values, cast: '#{cast_type}'))) # where(attr.contains(::Arel.array(values, cast: 'roles'))) end # end def has_any_#{attribute.pluralize}(*values) # def has_roles(*values) attr = arel_attribute('#{attribute}') # attr = arel_attribute('role') where(attr.overlaps(::Arel.array(values, cast: '#{cast_type}'))) # where(attr.overlaps(::Arel.array(values, cast: 'roles'))) end # end RUBY end # Create the method that turn the attribute value into text using # the model scope def stringify instance_module.module_eval <<-RUBY, __FILE__, __LINE__ + 1 def #{attribute}_text # def role_text #{attribute}.text('#{attribute}', self) # role.text('role', self) end # end RUBY end # Create all the methods that represent actions related to the # attribute value def all_values klass_content = '' instance_content = '' enum_klass = subtype.klass.name values_methods.each do |key, (scope, ask, bang, val)| klass_content += <<-RUBY def #{scope} # def admin attr = arel_attribute('#{attribute}') # attr = arel_attribute('role') where(::#{enum_klass}.scope(attr, '#{val}')) # where(Enum::Roles.scope(attr, 'admin')) end # end RUBY instance_content += <<-RUBY def #{ask} # def admin? #{attribute}.#{key}? # role.admin? end # end def #{bang} # admin! self.#{attribute} = '#{val}' # self.role = 'admin' return unless #{attribute}_changed? # return unless role_changed? return save! if Torque::PostgreSQL.config.enum.save_on_bang true # true end # end RUBY end klass_module.module_eval(klass_content) instance_module.module_eval(instance_content) end end end end end end