module Functional::Record

An immutable data structure with multiple data fields. A ‘Record` is a convenient way to bundle a number of field attributes together, using accessor methods, without having to write an explicit class. The `Record` module generates new `AbstractStruct` subclasses that hold a set of fields with a reader method for each field.

A ‘Record` is very similar to a Ruby `Struct` and shares many of its behaviors and attributes. Unlike a # Ruby `Struct`, a `Record` is immutable: its values are set at construction and can never be changed. Divergence between the two classes derive from this core difference.

{include:file:doc/record.md}

@see Functional::Union @see Functional::Protocol @see Functional::TypeCheck

@!macro thread_safe_immutable_object

Public Instance Methods

new(*fields, &block) click to toggle source

Create a new record class with the given fields.

@return [Functional::AbstractStruct] the new record subclass @raise [ArgumentError] no fields specified or an invalid type

specification is given
# File lib/functional/record.rb, line 33
def new(*fields, &block)
  raise ArgumentError.new('no fields provided') if fields.empty?

  name = nil
  types = nil

  # check if a name for registration is given
  if fields.first.is_a?(String)
    name = fields.first
    fields = fields[1..fields.length-1]
  end

  # check for a set of type/protocol specifications
  if fields.size == 1 && fields.first.respond_to?(:to_h)
    types = fields.first
    fields = fields.first.keys
    check_types!(types)
  end

  build(name, fields, types, &block)
rescue
  raise ArgumentError.new('invalid specification')
end

Private Instance Methods

build(name, fields, types, &block) click to toggle source

Use the given ‘AbstractStruct` class and build the methods necessary to support the given data fields.

@param [String] name the name under which to register the record when given @param [Array] fields the list of symbolic names for all data fields @return [Functional::AbstractStruct] the record class

# File lib/functional/record.rb, line 179
def build(name, fields, types, &block)
  fields = [name].concat(fields) unless name.nil?
  record, fields = AbstractStruct.define_class(self, :record, fields)
  record.class_variable_set(:@@restrictions, Restrictions.new(types, &block))
  define_initializer(record)
  fields.each do |field|
    define_reader(record, field)
  end
  record
end
check_types!(types) click to toggle source

Validate the given type/protocol specification.

@param [Hash] types the type specification @raise [ArgumentError] when the specification is not valid

# File lib/functional/record.rb, line 166
def check_types!(types)
  return if types.nil?
  unless types.all?{|k,v| v.is_a?(Module) || v.is_a?(Symbol) }
    raise ArgumentError.new('invalid specification')
  end
end
define_initializer(record) click to toggle source

Define an initializer method on the given record class.

@param [Functional::AbstractStruct] record the new record class @return [Functional::AbstractStruct] the record class

Calls superclass method
# File lib/functional/record.rb, line 194
def define_initializer(record)
  record.send(:define_method, :initialize) do |data = {}|
    super()
    restrictions = record.class_variable_get(:@@restrictions)
    data = record.fields.reduce({}) do |memo, field|
      memo[field] = data.fetch(field, restrictions.clone_default(field))
      memo
    end
    restrictions.validate!(data)
    set_data_hash(data)
    set_values_array(data.values)
    ensure_ivar_visibility!
    self.freeze
  end
  record
end
define_reader(record, field) click to toggle source

Define a reader method on the given record class for the given data field.

@param [Functional::AbstractStruct] record the new record class @param [Symbol] field symbolic name of the current data field @return [Functional::AbstractStruct] the record class

# File lib/functional/record.rb, line 216
def define_reader(record, field)
  record.send(:define_method, field) do
    to_h[field]
  end
  record
end