Class: Functional::FinalStruct

Inherits:
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:

Since:

  • 1.1.0

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)

Since:

  • 1.1.0


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

def initialize(attributes = {})
  raise ArgumentError.new('attributes must be given as a hash or not at all') unless attributes.respond_to?(:to_h)
  @mutex = Mutex.new
  @attribute_hash = {}
  attributes.to_h.each_pair do |field, value|
    set_attribute(field, value)
  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

Since:

  • 1.1.0


219
220
221
222
223
224
225
226
227
228
229
# File 'lib/functional/final_struct.rb', line 219

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

Since:

  • 1.1.0


148
149
150
151
152
153
154
155
# File 'lib/functional/final_struct.rb', line 148

def each_pair
  return enum_for(:each_pair) unless block_given?
  @mutex.synchronize {
    @attribute_hash.each do |field, value|
      yield(field, value)
    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

Since:

  • 1.1.0


173
174
175
# File 'lib/functional/final_struct.rb', line 173

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

Since:

  • 1.1.0


135
136
137
138
139
# File 'lib/functional/final_struct.rb', line 135

def fetch(field, default)
  @mutex.synchronize {
    attribute_has_been_set?(field) ? 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

Since:

  • 1.1.0


73
74
75
76
77
# File 'lib/functional/final_struct.rb', line 73

def get(field)
  @mutex.synchronize {
    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

Since:

  • 1.1.0


123
124
125
126
127
# File 'lib/functional/final_struct.rb', line 123

def get_or_set(field, value)
  @mutex.synchronize {
    attribute_has_been_set?(field) ? get_attribute(field) : 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:

Since:

  • 1.1.0


93
94
95
96
97
98
99
100
101
# File 'lib/functional/final_struct.rb', line 93

def set(field, value)
  @mutex.synchronize {
    if attribute_has_been_set?(field)
      raise FinalityError.new("final accessor '#{field}' has already been set")
    else
      set_attribute(field, value)
    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

Since:

  • 1.1.0


111
112
113
114
115
# File 'lib/functional/final_struct.rb', line 111

def set?(field)
  @mutex.synchronize {
    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

Since:

  • 1.1.0


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

def to_h
  @mutex.synchronize {
    @attribute_hash.dup
  }
end