module Stannum::Struct

Abstract class for defining objects with structured attributes.

@example Defining Attributes

class Widget
  include Stannum::Struct

  attribute :name,        String
  attribute :description, String,  optional: true
  attribute :quantity,    Integer, default:  0
end

widget = Widget.new(name: 'Self-sealing Stem Bolt')
widget.name        #=> 'Self-sealing Stem Bolt'
widget.description #=> nil
widget.quantity    #=> 0
widget.attributes  #=>
# {
#   name:        'Self-sealing Stem Bolt',
#   description: nil,
#   quantity:    0
# }

@example Setting Attributes

widget.description = 'A stem bolt, but self sealing.'
widget.attributes #=>
# {
#   name:        'Self-sealing Stem Bolt',
#   description: 'A stem bolt, but self sealing.',
#   quantity:    0
# }

widget.assign_attributes(quantity: 50)
widget.attributes #=>
# {
#   name:        'Self-sealing Stem Bolt',
#   description: 'A stem bolt, but self sealing.',
#   quantity:    50
# }

widget.attributes = (name: 'Inverse Chronoton Emitter')
# {
#   name:        'Inverse Chronoton Emitter',
#   description: nil,
#   quantity:    0
# }

@example Defining Attribute Constraints

Widget::Contract.matches?(quantity: -5)                    #=> false
Widget::Contract.matches?(name: 'Capacitor', quantity: -5) #=> true

class Widget
  constraint(:quantity) { |qty| qty >= 0 }
end

Widget::Contract.matches?(name: 'Capacitor', quantity: -5) #=> false
Widget::Contract.matches?(name: 'Capacitor', quantity: 10) #=> true

@example Defining Struct Constraints

Widget::Contract.matches?(name: 'Diode') #=> true

class Widget
  constraint { |struct| struct.description&.include?(struct.name) }
end

Widget::Contract.matches?(name: 'Diode') #=> false
Widget::Contract.matches?(
  name:        'Diode',
  description: 'A low budget Diode',
) #=> true

Public Class Methods

build(struct_class) click to toggle source

@private

# File lib/stannum/struct.rb, line 230
def build(struct_class)
  return if struct_class?(struct_class)

  initialize_attributes(struct_class)
  initialize_contract(struct_class)
end
new(attributes = {}) click to toggle source

Initializes the struct with the given attributes.

For each key in the attributes hash, the corresponding writer method will be called with the attribute value. If the hash does not include the key for an attribute, or if the value is nil, the attribute will be set to its default value.

If the attributes hash includes any keys that do not correspond to an attribute, the struct will raise an error.

@param attributes [Hash] The initial attributes for the struct.

@see attributes=

@raise ArgumentError if given an invalid attributes hash.

# File lib/stannum/struct.rb, line 287
def initialize(attributes = {})
  @attributes = {}

  self.attributes = attributes
end

Private Class Methods

included(other) click to toggle source
Calls superclass method
# File lib/stannum/struct.rb, line 239
def included(other)
  super

  Struct.build(other) if other.is_a?(Class)
end
initialize_attributes(struct_class) click to toggle source
# File lib/stannum/struct.rb, line 245
def initialize_attributes(struct_class)
  attributes = Stannum::Schema.new

  struct_class.const_set(:Attributes, attributes)

  if struct_class?(struct_class.superclass)
    attributes.include(struct_class.superclass::Attributes)
  end

  struct_class.include(attributes)
end
initialize_contract(struct_class) click to toggle source
# File lib/stannum/struct.rb, line 257
def initialize_contract(struct_class)
  contract = Stannum::Contract.new

  struct_class.const_set(:Contract, contract)

  return unless struct_class?(struct_class.superclass)

  contract.concat(struct_class.superclass::Contract)
end
struct_class?(struct_class) click to toggle source
# File lib/stannum/struct.rb, line 267
def struct_class?(struct_class)
  struct_class.const_defined?(:Attributes, false)
end

Public Instance Methods

==(other) click to toggle source

Compares the struct with the other object.

The other object must be an instance of the current class. In addition, the attributes hashes of the two objects must be equal.

@return true if the object is a matching struct.

# File lib/stannum/struct.rb, line 299
def ==(other)
  return false unless other.class == self.class

  raw_attributes == other.raw_attributes
end
[](key) click to toggle source

Retrieves the attribute with the given key.

@param key [String, Symbol] The attribute key.

@return [Object] the value of the attribute.

@raise ArgumentError if the key is not a valid attribute.

# File lib/stannum/struct.rb, line 312
def [](key)
  validate_attribute_key(key)

  send(self.class::Attributes[key].reader_name)
end
[]=(key, value) click to toggle source

Sets the given attribute to the given value.

@param key [String, Symbol] The attribute key. @param value [Object] The value for the attribute.

@raise ArgumentError if the key is not a valid attribute.

# File lib/stannum/struct.rb, line 324
def []=(key, value)
  validate_attribute_key(key)

  send(self.class::Attributes[key].writer_name, value)
end
assign(attributes)
Alias for: assign_attributes
assign_attributes(attributes) click to toggle source

Updates the struct’s attributes with the given values.

This method is used to update some (but not all) of the attributes of the struct. For each key in the hash, it calls the corresponding writer method with the value for that attribute. If the value is nil, this will set the attribute value to the default for that attribute.

Any attributes that are not in the given hash are unchanged.

If the attributes hash includes any keys that do not correspond to an attribute, the struct will raise an error.

@param attributes [Hash] The initial attributes for the struct.

@raise ArgumentError if the key is not a valid attribute.

@see attributes=

# File lib/stannum/struct.rb, line 347
def assign_attributes(attributes)
  unless attributes.is_a?(Hash)
    raise ArgumentError, 'attributes must be a Hash'
  end

  attributes.each do |attr_name, value|
    validate_attribute_key(attr_name)

    attribute = self.class.attributes[attr_name]

    send(attribute.writer_name, value)
  end
end
Also aliased as: assign
attributes() click to toggle source

@return [Hash] the current attributes of the struct.

# File lib/stannum/struct.rb, line 363
def attributes
  tools.hash_tools.deep_dup(@attributes)
end
Also aliased as: to_h
attributes=(attributes) click to toggle source

Replaces the struct’s attributes with the given values.

This method is used to update all of the attributes of the struct. For each attribute, the writer method is called with the value from the hash, or nil if the corresponding key is not present in the hash. Any nil or missing keys set the attribute value to the attribute’s default value.

If the attributes hash includes any keys that do not correspond to an attribute, the struct will raise an error.

@param attributes [Hash] The initial attributes for the struct.

@raise ArgumentError if the key is not a valid attribute.

@see assign_attributes

# File lib/stannum/struct.rb, line 383
def attributes=(attributes) # rubocop:disable Metrics/MethodLength
  unless attributes.is_a?(Hash)
    raise ArgumentError, 'attributes must be a Hash'
  end

  attributes.each_key { |attr_name| validate_attribute_key(attr_name) }

  self.class::Attributes.each_value do |attribute|
    send(
      attribute.writer_name,
      attributes.fetch(
        attribute.name,
        attributes.fetch(attribute.name.intern, attribute.default)
      )
    )
  end
end
inspect() click to toggle source

@return [String] a string representation of the struct and its attributes.

# File lib/stannum/struct.rb, line 402
def inspect # rubocop:disable Metrics/AbcSize
  if self.class.attributes.each_key.size.zero?
    return "#<#{self.class.name}>"
  end

  buffer = +"#<#{self.class.name}"

  self.class.attributes.each_key.with_index \
  do |attribute, index|
    buffer << ',' unless index.zero?
    buffer << " #{attribute}: #{@attributes[attribute].inspect}"
  end

  buffer << '>'
end
to_h()
Alias for: attributes

Protected Instance Methods

raw_attributes() click to toggle source
# File lib/stannum/struct.rb, line 420
def raw_attributes
  @attributes
end

Private Instance Methods

tools() click to toggle source
# File lib/stannum/struct.rb, line 426
def tools
  SleepingKingStudios::Tools::Toolbelt.instance
end
validate_attribute_key(key) click to toggle source
# File lib/stannum/struct.rb, line 430
def validate_attribute_key(key)
  raise ArgumentError, "attribute can't be blank" if key.nil?

  unless key.is_a?(String) || key.is_a?(Symbol)
    raise ArgumentError, 'attribute must be a String or Symbol'
  end

  raise ArgumentError, "attribute can't be blank" if key.empty?

  return if self.class::Attributes.key?(key.to_s)

  raise ArgumentError, "unknown attribute #{key.inspect}"
end