module OptionsModel::Concerns::Attributes::ClassMethods

Public Instance Methods

attribute(name, cast_type, default: nil, array: false) click to toggle source
# File lib/options_model/concerns/attributes.rb, line 9
        def attribute(name, cast_type, default: nil, array: false)
          check_not_finalized!

          name = name.to_sym
          check_name_validity! name

          ActiveModel::Type.lookup(cast_type)

          attribute_defaults[name] = default
          default_extractor =
            if default.respond_to?(:call)
              ".call"
            elsif default.duplicable?
              ".deep_dup"
            else
              ""
            end

          generated_attribute_methods.synchronize do
            generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
            def #{name}
              value = attributes[:#{name}]
              return value unless value.nil?
              attributes[:#{name}] = self.class.attribute_defaults[:#{name}]#{default_extractor}
              attributes[:#{name}]
            end
            STR

            if array
              generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
              def #{name}=(value)
                if value.respond_to?(:to_a)
                  attributes[:#{name}] = value.to_a.map { |i| ActiveModel::Type.lookup(:#{cast_type}).cast(i) }
                elsif value.nil?
                  attributes[:#{name}] = self.class.attribute_defaults[:#{name}]#{default_extractor}
                else
                  raise ArgumentError,
                        "`value` should respond to `to_a`, but got \#{value.class} -- \#{value.inspect}"
                end
              end
              STR
            else
              generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
              def #{name}=(value)
                attributes[:#{name}] = ActiveModel::Type.lookup(:#{cast_type}).cast(value)
              end
              STR

              generated_attribute_methods.send :alias_method, :"#{name}?", name if cast_type == :boolean
            end
          end

          attribute_names_for_inlining << name

          self
        end
attribute_defaults() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 132
def attribute_defaults
  @attribute_defaults ||= ActiveSupport::HashWithIndifferentAccess.new
end
attribute_names() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 148
def attribute_names
  attribute_names_for_nesting + attribute_names_for_inlining
end
attribute_names_for_inlining() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 144
def attribute_names_for_inlining
  @attribute_names_for_inlining ||= Set.new
end
attribute_names_for_nesting() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 140
def attribute_names_for_nesting
  @attribute_names_for_nesting ||= Set.new
end
embeds_one(name, class_name: nil, anonymous_class: nil) click to toggle source
# File lib/options_model/concerns/attributes.rb, line 89
        def embeds_one(name, class_name: nil, anonymous_class: nil)
          check_not_finalized!

          raise ArgumentError, "must provide at least one of `class_name` or `anonymous_class`" if class_name.blank? && anonymous_class.nil?

          name = name.to_sym
          check_name_validity! name

          nested_classes[name] = if class_name.present?
            class_name.constantize
          else
            anonymous_class
          end

          generated_attribute_methods.synchronize do
            generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
            def #{name}
              nested_attributes[:#{name}] ||= self.class.nested_classes[:#{name}].new(attributes[:#{name}])
            end
            STR

            generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
            def #{name}=(value)
              klass = self.class.nested_classes[:#{name}]
              if value.respond_to?(:to_h)
                nested_attributes[:#{name}] = klass.new(value.to_h)
              elsif value.is_a? klass
                nested_attributes[:#{name}] = value
              elsif value.nil?
                nested_attributes[:#{name}] = klass.new
              else
                raise ArgumentError,
                      "`value` should respond to `to_h` or \#{klass}, but got \#{value.class}"
              end
            end
            STR
          end

          attribute_names_for_nesting << name

          self
        end
enum_attribute(name, enum, default: nil, allow_nil: false) click to toggle source
# File lib/options_model/concerns/attributes.rb, line 66
        def enum_attribute(name, enum, default: nil, allow_nil: false)
          check_not_finalized!

          raise ArgumentError, "enum should be an Array and can't empty" unless enum.is_a?(Array) && enum.any?

          enum = enum.map(&:to_s)

          attribute name, :string, default: default

          pluralized_name = name.to_s.pluralize
          generated_class_methods.synchronize do
            generated_class_methods.module_eval <<-STR, __FILE__, __LINE__ + 1
            def #{pluralized_name}
              %w(#{enum.join(' ')}).freeze
            end
            STR

            validates name, inclusion: { in: enum }, allow_nil: allow_nil
          end

          self
        end
finalize!(nested = true) click to toggle source
# File lib/options_model/concerns/attributes.rb, line 156
def finalize!(nested = true)
  nested_classes.values.each(&:finalize!) if nested

  @finalized = true
end
finalized?() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 152
def finalized?
  @finalized ||= false
end
nested_classes() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 136
def nested_classes
  @nested_classes ||= ActiveSupport::HashWithIndifferentAccess.new
end

Protected Instance Methods

check_name_validity!(symbolized_name) click to toggle source
# File lib/options_model/concerns/attributes.rb, line 164
def check_name_validity!(symbolized_name)
  if dangerous_attribute_method?(symbolized_name)
    raise ArgumentError, "#{symbolized_name} is defined by #{OptionsModel::Base}. Check to make sure that you don't have an attribute or method with the same name."
  end

  if attribute_names_for_inlining.include?(symbolized_name) || attribute_names_for_nesting.include?(symbolized_name)
    raise ArgumentError, "duplicate define attribute `#{symbolized_name}`"
  end
end
check_not_finalized!() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 174
def check_not_finalized!
  raise "can't modify finalized #{self}" if finalized?
end
generated_attribute_methods() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 196
def generated_attribute_methods
  @generated_attribute_methods ||= Module.new do
    extend Mutex_m
  end.tap { |mod| include mod }
end
generated_class_methods() click to toggle source
# File lib/options_model/concerns/attributes.rb, line 202
def generated_class_methods
  @generated_class_methods ||= Module.new do
    extend Mutex_m
  end.tap { |mod| extend mod }
end