Class: Functional::FinalStruct

Inherits:
Synchronization::Object
  • Object
show all
Defined in:
lib/functional/final_struct.rb

Overview

Note:

This is a write-once, read-many, thread safe object that can be used in concurrent systems. Thread safety guarantees cannot be made about objects contained within this object, however. Ruby variables are mutable references to mutable objects. This cannot be changed. The best practice it to only encapsulate immutable, frozen, or thread safe objects. Ultimately, thread safety is the responsibility of the programmer.

A variation on Ruby's OpenStruct in which all fields are "final" (meaning that new fields can be arbitrarily added to a FinalStruct object but once set each field becomes immutable). Additionally, predicate methods exist for all fields and these predicates indicate if the field has been set.

There are two ways to initialize a FinalStruct: with zero arguments or with a Hash (or any other object that implements a to_h method). The only difference in behavior is that a FinalStruct initialized with a hash will pre-define and pre-populate attributes named for the hash keys and with values corresponding to the hash values.

Examples:

Instanciation With No Fields

bucket = Functional::FinalStruct.new

bucket.foo      #=> nil
bucket.foo?     #=> false

bucket.foo = 42 #=> 42
bucket.foo      #=> 42
bucket.foo?     #=> true

bucket.foo = 42 #=> Functional::FinalityError: final accessor 'bar' has already been set

Instanciation With a Hash

name = Functional::FinalStruct.new(first: 'Douglas', last: 'Adams')

name.first           #=> 'Douglas'
name.last            #=> 'Adams'
name.first?          #=> true
name.last?           #=> true

name.middle #=> nil
name.middle?         #=> false
name.middle = 'Noel' #=> 'Noel'
name.middle?         #=> true

name.first = 'Sam'   #=> Functional::FinalityError: final accessor 'first' has already been set

See Also:

Instance Method Summary collapse

Constructor Details

#initialize(attributes = {}) ⇒ FinalStruct

Creates a new FinalStruct object. By default, the resulting FinalStruct object will have no attributes. The optional hash, if given, will generate attributes and values (can be a Hash or any object with a to_h method).

Parameters:

  • attributes (Hash) (defaults to: {})

    the field/value pairs to set on creation

Raises:

  • (ArgumentError)


55
56
57
58
59
60
61
62
63
64
# File 'lib/functional/final_struct.rb', line 55

def initialize(attributes = {})
  raise ArgumentError.new('attributes must be given as a hash or not at all') unless attributes.respond_to?(:to_h)
  super
  synchronize do
    @attribute_hash = {}
    attributes.to_h.each_pair do |field, value|
      ns_set_attribute(field, value)
    end
  end
end

Dynamic Method Handling

This class handles dynamic methods through the method_missing method

#method_missing(symbol, *args) ⇒ Object (private)

Check the method name and args for signatures matching potential final attribute reader, writer, and predicate methods. If the signature matches a reader or predicate, treat the attribute as unset. If the signature matches a writer, attempt to set the new attribute.

Parameters:

  • symbol (Symbol)

    the name of the called function

  • args (Array)

    zero or more arguments

Returns:

  • (Object)

    the result of the proxied method or the super call



208
209
210
211
212
213
214
215
216
217
218
# File 'lib/functional/final_struct.rb', line 208

def method_missing(symbol, *args)
  if args.length == 1 && (match = /([^=]+)=$/.match(symbol))
    set(match[1], args.first)
  elsif args.length == 0 && (match = /([^\?]+)\?$/.match(symbol))
    set?(match[1])
  elsif args.length == 0
    get(symbol)
  else
    super
  end
end

Instance Method Details

#each_pair {|field, value| ... } ⇒ Enumerable

Calls the block once for each attribute, passing the key/value pair as parameters. If no block is given, an enumerator is returned instead.

Yield Parameters:

  • field (Symbol)

    the struct field for the current iteration

  • value (Object)

    the value of the current field

Returns:

  • (Enumerable)

    when no block is given



139
140
141
142
143
144
145
146
# File 'lib/functional/final_struct.rb', line 139

def each_pair
  return enum_for(:each_pair) unless block_given?
  synchronize do
    @attribute_hash.each do |field, value|
      yield(field, value)
    end
  end
end

#eql?(other) ⇒ Boolean Also known as: ==

Compares this object and other for equality. A FinalStruct is eql? to other when other is a FinalStruct and the two objects have identical fields and values.

Parameters:

  • other (Object)

    the other record to compare for equality

Returns:

  • (Boolean)

    true when equal else false



162
163
164
# File 'lib/functional/final_struct.rb', line 162

def eql?(other)
  other.is_a?(self.class) && to_h == other.to_h
end

#fetch(field, default) ⇒ Object

Get the current value of the given field if already set else return the given default value.

Parameters:

  • field (Symbol)

    the field to get the value for

  • default (Object)

    the value to return if the field has not been set

Returns:

  • (Object)

    the value of the given field else the given default value



128
129
130
# File 'lib/functional/final_struct.rb', line 128

def fetch(field, default)
  synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : default }
end

#get(field) ⇒ Object Also known as: []

Get the value of the given field.

Parameters:

  • field (Symbol)

    the field to retrieve the value for

Returns:

  • (Object)

    the value of the field is set else nil



72
73
74
# File 'lib/functional/final_struct.rb', line 72

def get(field)
  synchronize { ns_get_attribute(field) }
end

#get_or_set(field, value) ⇒ Object

Get the current value of the given field if already set else set the value of the given field to the given value.

Parameters:

  • field (Symbol)

    the field to get or set the value for

  • value (Object)

    the value to set the field to when not previously set

Returns:

  • (Object)

    the final value of the given field



118
119
120
# File 'lib/functional/final_struct.rb', line 118

def get_or_set(field, value)
  synchronize { ns_attribute_has_been_set?(field) ? ns_get_attribute(field) : ns_set_attribute(field, value) }
end

#set(field, value) ⇒ Object Also known as: []=

Set the value of the give field to the given value.

It is a logical error to attempt to set a final field more than once, as this violates the concept of finality. Calling the method a second or subsequent time for a given field will result in an exception being raised.

Parameters:

  • field (Symbol)

    the field to set the value for

  • value (Object)

    the value to set the field to

Returns:

  • (Object)

    the final value of the given field

Raises:



90
91
92
93
94
95
96
97
98
# File 'lib/functional/final_struct.rb', line 90

def set(field, value)
  synchronize do
    if ns_attribute_has_been_set?(field)
      raise FinalityError.new("final accessor '#{field}' has already been set")
    else
      ns_set_attribute(field, value)
    end
  end
end

#set?(field) ⇒ Boolean

Check the internal hash to unambiguously verify that the given attribute has been set.

Parameters:

  • field (Symbol)

    the field to get the value for

Returns:

  • (Boolean)

    true if the field has been set else false



108
109
110
# File 'lib/functional/final_struct.rb', line 108

def set?(field)
  synchronize { ns_attribute_has_been_set?(field) }
end

#to_hHash

Converts the FinalStruct to a Hash with keys representing each attribute (as symbols) and their corresponding values.

Returns:

  • (Hash)

    a Hash representing this struct



152
153
154
# File 'lib/functional/final_struct.rb', line 152

def to_h
  synchronize { @attribute_hash.dup }
end