module RangeComponentAttributes

Constants

VERSION

Public Instance Methods

range_component_attributes( range_name, lower_name: " click to toggle source

range_component_attributes creates attributes corresponding to the lower and upper bounds of `range_name`. `lower_name` and `upper_name` controls the names of these attributes.

`type_converter` is a callable object that converts its argument to the proper type. There are builtin type converters `IntegerConverter`, `DecimalConverter`, `FloatConverter`, and `DateConverter`.

In addition, `lower_type_converter` and `upper_type_converter` can be separately specified. This is especially useful when the attributes should behave differently for blank values. For example, the upper bound may want to consider a blank value as Float::INFINITY.

`exclude_end` controls whether the end is exclusive or not. Ranges are automatically normalized to this type. This is useful because PostgreSQL automatically normalizes ranges of discrete values to exclusive ends. e.g. `[1, 10]` becomes `[1,11)`. RangeComponentAttributes will handle this so the exact bound values persist even when PostgreSQL has changed them.

Validations are automatically added that create an error if an assignment to bounds attribute fails due to a type conversion error. In addition, a validation checks that the lower bound is less than the upper bound. This error message can be customized by supplying `crossed_bounds_message`.

Calls superclass method
# File lib/range_component_attributes.rb, line 34
def range_component_attributes(
  range_name,
  lower_name: "#{range_name}_lower",
  upper_name: "#{range_name}_upper",
  type_converter: nil,
  lower_type_converter: nil,
  upper_type_converter: nil,
  exclude_end: true,
  crossed_bounds_message: "must be less than upper bound"
)
  range_wrapper_name = "#{range_name}_wrapper"
  lower_type_converter ||= type_converter
  upper_type_converter ||= type_converter
  raise ArgumentError, "must provide lower_type_converter or type_converter" unless lower_type_converter
  raise ArgumentError, "must provide upper_type_converter or type_converter" unless upper_type_converter

  mod = Module.new do
    define_method range_wrapper_name do
      if instance_variable_defined?("@#{range_wrapper_name}")
        instance_variable_get("@#{range_wrapper_name}")
      else
        instance_variable_set("@#{range_wrapper_name}",
          RangeWrapper.new(
            lower_type_converter: lower_type_converter,
            upper_type_converter: upper_type_converter,
            exclude_end: exclude_end,
            range: send(range_name),
            crossed_bounds_message: crossed_bounds_message
          )
        )
      end
    end

    define_method "#{lower_name}" do
      send(range_wrapper_name).lower
    end

    define_method "#{lower_name}=" do |val|
      range_wrapper = send(range_wrapper_name)
      range_wrapper.lower = val
      if range_wrapper.valid?
        send("#{range_name}=", range_wrapper.range)
      end
    end

    define_method "#{upper_name}" do
      send(range_wrapper_name).upper
    end

    define_method "#{upper_name}=" do |val|
      range_wrapper = send(range_wrapper_name)
      range_wrapper.upper = val
      if range_wrapper.valid?
        send("#{range_name}=", range_wrapper.range)
      end
    end

    define_method "#{range_name}=" do |val|
      send(range_wrapper_name).range = val
      super val
    end

    define_method "check_#{range_name}_errors" do
      range_wrapper = send(range_wrapper_name)
      unless range_wrapper.valid?
        errors.add lower_name, range_wrapper.errors[:lower] if range_wrapper.errors[:lower]
        errors.add upper_name, range_wrapper.errors[:upper] if range_wrapper.errors[:upper]
        errors.add upper_name, range_wrapper.errors[:range] if range_wrapper.errors[:range]
      end
    end

    define_method "reload" do |options=nil|
      result = super options
      send(range_wrapper_name).range = send(range_name)
      result
    end
  end

  validate "check_#{range_name}_errors".to_sym

  self.include mod
end