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
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
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
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 an initializer method on the given record class.
@param [Functional::AbstractStruct] record the new record class @return [Functional::AbstractStruct] the record class
# 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 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