class FlexColumns::Definition::FieldDefinition

A FieldDefinition represents, well, the definition of a field. One of these objects is created for each field you declare in a flex column. It keeps track of (at minimum) the name of the field; it also is responsible for implementing our “shorthand types” system (where declaring your field as :integer adds a validation that requires it to be an integer, for example).

Perhaps most significantly, a FieldDefinition object is responsible for creating the appropriate methods on the flex-column class and on the model class, and also for adding methods to classes that have invoked IncludeFlexColumns#include_flex_columns_from.

Attributes

field_name[R]
flex_column_class[R]
options[R]

Public Class Methods

new(flex_column_class, field_name, additional_arguments, options) click to toggle source

Creates a new instance. flex_column_class is the Class we created for this flex column – i.e., a class that inherits from FlexColumns::Contents::FlexColumnContentsBase. field_name is the name of the field. additional_arguments is an Array containing any additional arguments that were passed – right now, that can only be the type of the field (e.g., :integer, etc.). options is any options that were passed; this can contain:

:visibility, :null, :enum, :limit, :json

:visibility

Can be set to :public or :private; will override the default visibility for fields specified on the flex-column class itself.

:null

If present and set to false, a validation requiring data in this field will be added.

:enum

If present, must be mapped to an Array; a validation requiring the data to be one of the elements of the array will be added.

:limit

If present, must be mapped to an integer; a validation requiring the length of the data to be at most this value will be added.

:json

If present, must be mapped to a String or Symbol; this specifies that the field should be stored under the given key in the JSON, rather than its field name.

# File lib/flex_columns/definition/field_definition.rb, line 44
def initialize(flex_column_class, field_name, additional_arguments, options)
  unless flex_column_class.respond_to?(:is_flex_column_class?) && flex_column_class.is_flex_column_class?
    raise ArgumentError, "You can't define a flex-column field against #{flex_column_class.inspect}; that isn't a flex-column class."
  end

  validate_options(options)
  @flex_column_class = flex_column_class
  @field_name = self.class.normalize_name(field_name)
  @options = options
  @field_type = nil

  apply_additional_arguments(additional_arguments)
  apply_validations!
end
normalize_name(name) click to toggle source

Given the name of a field, returns a normalized version of that name – so we can compare using +==+ without worrying about String vs. Symbol and so on.

# File lib/flex_columns/definition/field_definition.rb, line 15
def normalize_name(name)
  case name
  when Symbol then name
  when String then
    raise "You must supply a non-empty String, not: #{name.inspect}" if name.strip.length == 0
    name.strip.downcase.to_sym
  else raise ArgumentError, "You must supply a name, not: #{name.inspect}"
  end
end

Public Instance Methods

add_methods_to_flex_column_class!(dynamic_methods_module) click to toggle source

Defines appropriate accessor methods for this field on the given DynamicMethodsModule, which should be included in the flex-column class (not the model class). These are quite simple; they always exist (and should overwrite any existing methods, since we’re last-definition-wins). We just need to make them work, and make them private, if needed.

# File lib/flex_columns/definition/field_definition.rb, line 68
def add_methods_to_flex_column_class!(dynamic_methods_module)
  fn = field_name

  dynamic_methods_module.define_method(fn) do
    self[fn]
  end

  dynamic_methods_module.define_method("#{fn}=") do |x|
    self[fn] = x
  end

  if private?
    dynamic_methods_module.private(fn)
    dynamic_methods_module.private("#{fn}=")
  end
end
add_methods_to_included_class!(dynamic_methods_module, association_name, target_class, options) click to toggle source

Defines appropriate accessor methods for this field on the given DynamicMethodsModule, which should be included in some target model class that has said include_flex_columns_from on the clsas containing this field. association_name is the name of the association method name that, when called on the class that includes the DynamicMethodsModule, will return an instance of the model class in which this field lives. target_class is the target class we’re defining methods on, so that we can check if we’re going to conflict with some method there that we should not clobber.

options can contain:

:visibility

If :private, then methods will be defined as private.

:prefix

If specified, then methods will be prefixed with the given prefix. This will override the prefix specified on the flex-column class, if any.

# File lib/flex_columns/definition/field_definition.rb, line 129
    def add_methods_to_included_class!(dynamic_methods_module, association_name, target_class, options)
      return if (! flex_column_class.delegation_type)

      prefix = if options.has_key?(:prefix) then options[:prefix] else flex_column_class.delegation_prefix end
      is_private = private? || (flex_column_class.delegation_type == :private) || (options[:visibility] == :private)

      if is_private && options[:visibility] == :public
        raise ArgumentError, %{You asked for public visibility for methods included from association #{association_name.inspect},
but the flex column #{flex_column_class.model_class.name}.#{flex_column_class.column_name} has its methods
defined with private visibility (either in the flex column itself, or at the model level).

You can't have methods be 'more public' in the included class than they are in the class
they're being included from.}
      end

      mn = field_name
      mn = "#{prefix}_#{mn}".to_sym if prefix

      fcc = flex_column_class
      fn = field_name

      if target_class._flex_columns_safe_to_define_method?(mn)
        dynamic_methods_module.define_method(mn) do
          associated_object = send(association_name) || send("build_#{association_name}")
          flex_column_object = associated_object.send(fcc.column_name)
          flex_column_object.send(fn)
        end

        dynamic_methods_module.define_method("#{mn}=") do |x|
          associated_object = send(association_name) || send("build_#{association_name}")
          flex_column_object = associated_object.send(fcc.column_name)
          flex_column_object.send("#{fn}=", x)
        end

        if is_private
          dynamic_methods_module.private(mn)
          dynamic_methods_module.private("#{mn}=")
        end
      end
    end
add_methods_to_model_class!(dynamic_methods_module, model_class) click to toggle source

Defines appropriate accessor methods for this field on the given DynamicMethodsModule, which should be included in the model class. We also pass model_class so that we can check to see if we’re going to conflict with one of its columns first, or other methods we shouldn’t clobber.

We need to respect visibility (public or private) of methods, and the delegation prefix assigned at the flex-column level.

# File lib/flex_columns/definition/field_definition.rb, line 91
def add_methods_to_model_class!(dynamic_methods_module, model_class)
  return if (! flex_column_class.delegation_type) # :delegate => false on the flex column means don't delegate from the model at all

  mn = field_name
  mn = "#{flex_column_class.delegation_prefix}_#{mn}".to_sym if flex_column_class.delegation_prefix

  if model_class._flex_columns_safe_to_define_method?(mn)
    fcc = flex_column_class
    fn = field_name

    should_be_private = (private? || flex_column_class.delegation_type == :private)

    dynamic_methods_module.define_method(mn) do
      flex_instance = fcc.object_for(self)
      flex_instance[fn]
    end
    dynamic_methods_module.private(mn) if should_be_private

    dynamic_methods_module.define_method("#{mn}=") do |x|
      flex_instance = fcc.object_for(self)
      flex_instance[fn] = x
    end
    dynamic_methods_module.private("#{mn}=") if should_be_private
  end
end
json_storage_name() click to toggle source

Returns the key under which the field’s value should be stored in the JSON.

# File lib/flex_columns/definition/field_definition.rb, line 60
def json_storage_name
  (options[:json] || field_name).to_s.strip.downcase.to_sym
end

Private Instance Methods

apply_additional_arguments(additional_arguments) click to toggle source

Given any additional arguments after the name of the field (e.g., field :foo, :integer), apply them as appropriate. Currently, the only kind of accepted additional argument is a type.

# File lib/flex_columns/definition/field_definition.rb, line 208
def apply_additional_arguments(additional_arguments)
  @type = additional_arguments.shift
  if @type
    begin
      send("apply_validations_for_#{@type}")
    rescue NoMethodError => nme
      raise ArgumentError, "Unknown type: #{@type.inspect}"
    end
  end

  if additional_arguments.length > 0
    raise ArgumentError, "Invalid additional arguments: #{additional_arguments.inspect}"
  end
end
apply_validations!() click to toggle source

Applies any validations resulting from options to this class (but not types; they’re handled by apply_additional_arguments, above). Currently, this applies validations for :null, :enum, and :limit.

# File lib/flex_columns/definition/field_definition.rb, line 305
def apply_validations!
  if not_nullable? && (! skip_not_nullable_validation_due_to_type?)
    flex_column_class.validates field_name, :presence => true
  end

  if options.has_key?(:enum)
    values = options[:enum]
    unless values.kind_of?(Array)
      raise ArgumentError, "Must specify an Array of possible values, not: #{options[:enum].inspect}"
    end

    flex_column_class.validates field_name, :inclusion => { :in => values }
  end

  if options.has_key?(:limit)
    limit = options[:limit]
    raise ArgumentError, "Limit must be > 0, not: #{limit.inspect}" unless limit.kind_of?(Integer) && limit > 0

    flex_column_class.validates field_name, :length => { :maximum => limit }
  end
end
apply_validations_for_boolean() click to toggle source

Apply the correct validations for a field of type :boolean. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 293
def apply_validations_for_boolean
  value_set = [ true, false ]
  value_set << nil unless not_nullable?
  flex_column_class.validates field_name, :inclusion => { :in => value_set }
end
apply_validations_for_date() click to toggle source

Apply the correct validations for a field of type :date. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 263
def apply_validations_for_date
  flex_column_class.validates_each field_name do |record, attr, value|
    record.errors.add(attr, "must be a Date") if value && (! value.kind_of?(Date))
  end
end
apply_validations_for_datetime() click to toggle source

Apply the correct validations for a field of type :datetime. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 285
def apply_validations_for_datetime
  flex_column_class.validates_each field_name do |record, attr, value|
    record.errors.add(attr, "must be a Time or DateTime") if value && (! value.kind_of?(Time)) && (value.class.name != 'DateTime')
  end
end
apply_validations_for_decimal() click to toggle source

Apply the correct validations for a field of type :decimal. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 257
def apply_validations_for_decimal
  apply_validations_for_float
end
apply_validations_for_float() click to toggle source

Apply the correct validations for a field of type :float. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 251
def apply_validations_for_float
  flex_column_class.validates field_name, :numericality => true, :allow_nil => true
end
apply_validations_for_integer() click to toggle source

Apply the correct validations for a field of type :integer. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 229
def apply_validations_for_integer
  options = { :numericality => { :only_integer => true } }
  options[:allow_nil] = true unless not_nullable?
  flex_column_class.validates field_name, options
end
apply_validations_for_string() click to toggle source

Apply the correct validations for a field of type :string. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 237
def apply_validations_for_string
  flex_column_class.validates_each field_name do |record, attr, value|
    record.errors.add(attr, "must be a String") if value && (! value.kind_of?(String)) && (! value.kind_of?(Symbol))
  end
end
apply_validations_for_text() click to toggle source

Apply the correct validations for a field of type :text. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 245
def apply_validations_for_text
  apply_validations_for_string
end
apply_validations_for_time() click to toggle source

Apply the correct validations for a field of type :time. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 271
def apply_validations_for_time
  flex_column_class.validates_each field_name do |record, attr, value|
    record.errors.add(attr, "must be a Time") if value && (! value.kind_of?(Time))
  end
end
apply_validations_for_timestamp() click to toggle source

Apply the correct validations for a field of type :timestamp. (Called from apply_additional_arguments via metaprogramming.)

# File lib/flex_columns/definition/field_definition.rb, line 279
def apply_validations_for_timestamp
  apply_validations_for_datetime
end
not_nullable?() click to toggle source
# File lib/flex_columns/definition/field_definition.rb, line 223
def not_nullable?
  options.has_key?(:null) && (! options[:null])
end
private?() click to toggle source

Should we define private methods?

# File lib/flex_columns/definition/field_definition.rb, line 197
def private?
  case options[:visibility]
  when :public then false
  when :private then true
  when nil then flex_column_class.fields_are_private_by_default?
  else raise "This should never happen: #{options[:visibility].inspect}"
  end
end
skip_not_nullable_validation_due_to_type?() click to toggle source
# File lib/flex_columns/definition/field_definition.rb, line 299
def skip_not_nullable_validation_due_to_type?
  [ :boolean ].include?(@type)
end
validate_options(options) click to toggle source

Checks that the options passed into this class are correct. This is both so that we have good exceptions, and so that we have them early – it’s much nicer if errors happen when you try to define your flex column, rather than much later on, when it really matters, possibly in production.

# File lib/flex_columns/definition/field_definition.rb, line 176
def validate_options(options)
  options.assert_valid_keys(:visibility, :null, :enum, :limit, :json)

  case options[:visibility]
  when nil then nil
  when :public then nil
  when :private then nil
  else raise ArgumentError, "Invalid value for :visibility: #{options[:visibility].inspect}"
  end

  case options[:json]
  when nil, String, Symbol then nil
  else raise ArgumentError, "Invalid value for :json: #{options[:json].inspect}"
  end

  unless [ nil, true, false ].include?(options[:null])
    raise ArgumentError, "Invalid value for :null: #{options[:null].inspect}"
  end
end