class ImmutableStruct

Creates classes for value objects/read-only records. Most useful when creating model objects for concepts not stored in the database.

This will create a class that has attr_readers for all given attributes, as well as a hash-based constructor. Further, the block given to with_attributes will be evaluated as if it were inside a class definition, allowing you to add methods, include or extend modules, or do whatever else you want.

Public Class Methods

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

Create a new class with the given read-only attributes.

attributes

list of symbols or strings that can be used to create attributes. Any attribute with a question mark in it (e.g. :foo?) will create an attribute without a question mark that passes through the raw value and an attribute with the question mark that coerces that value to a boolean. You would initialize it with the non-question-mark value An attribute that is an array of one symbol will create an attribute named for that symbol, but that doesn't return nil, instead returning the to_a of the value passed to the construtor.

block

if present, evaluates in the context of the new class, so def, def.self, include and extend should all work as in a normal class definition.

Example:

Person = ImmutableStruct.new(:name, :location, :minor?)

p = Person.new(name: 'Dave', location: Location.new("DC"), minor: false)
p.name     # => 'Dave'
p.location # => <Location: @where="DC">
p.minor    # => false
p.minor?   # => false

p = Person.new(name: 'Rudy', minor: "yup")
p.name     # => 'Rudy'
p.location # => nil
p.minor    # => "yup"
p.minor?   # => true
# File lib/immutable-struct.rb, line 39
def self.new(*attributes,&block)
  klass = Class.new do
    attributes.each do |attribute|
      if attribute.to_s =~ /(^.*)\?$/
        raw_name = $1
        attr_reader raw_name
        define_method(attribute) do
          !!instance_variable_get("@#{raw_name}")
        end
      elsif attribute.kind_of?(Array) and attribute.size == 1
        attr_reader attribute[0]
      else
        attr_reader attribute
      end
    end

    define_method(:initialize) do |*args|
      attrs = args[0] || {}
      attributes.each do |attribute|
        if attribute.kind_of?(Array) and attribute.size == 1
          ivar_name = attribute[0].to_s
          instance_variable_set("@#{ivar_name}", (attrs[ivar_name.to_s] || attrs[ivar_name.to_sym]).to_a)
        else
          ivar_name = attribute.to_s.gsub(/\?$/,'')
          instance_variable_set("@#{ivar_name}",attrs[ivar_name.to_s] || attrs[ivar_name.to_sym])
        end
      end
    end

    define_method(:==) do |other|
      return false unless other.is_a?(klass)
      attributes.all? { |attribute| self.send(attribute) == other.send(attribute) }
    end

    alias_method :eql?, :==
  end
  klass.class_exec(&block) unless block.nil?
  imethods = klass.instance_methods(include_super=false)
  klass.class_exec(imethods) do |imethods|
    define_method(:to_h) do
      imethods.inject({}) do |hash, method|
        next hash if [:==, :eql?].include?(method)
        hash.merge(method.to_sym => self.send(method))
      end
    end
  end
  klass
end