Class: Struct

Inherits:
Object
  • Object
show all
Includes:
Enumerable
Defined in:
lib/pure-struct.rb

Overview

Optional re-implmentation of Struct in Pure Ruby (to get around JS issues in Opal Struct)

Constant Summary collapse

CLASS_DEFINITION_FOR_ATTRIBUTES =
lambda do |attributes, keyword_init|
  lambda do |defined_class|
    attributes.each do |attribute|
      define_method(attribute) do
        self[attribute]
      end
      define_method("#{attribute}=") do |value|
        self[attribute] = value
      end
    end
  
    define_method(:members) do
      (@members ||= attributes).clone
    end
    
    def []=(attribute, value)
      normalized_attribute = attribute.to_sym
      raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
      @member_values[normalized_attribute] = value
    end
    
    def [](attribute)
      normalized_attribute = attribute.to_sym
      raise NameError, "no member #{attribute} in struct" unless @members.include?(normalized_attribute)
      @member_values[normalized_attribute]
    end
    
    def each(&block)
      to_a.each(&block)
    end
    
    def each_pair(&block)
      @member_values.each_pair(&block)
    end
    
    def to_h
      @member_values.clone
    end
    
    def to_a
      @member_values.values
    end
    
    def size
      @members.size
    end
    alias length size
    
    def dig(*args)
      @member_values.dig(*args)
    end
    
    def select(&block)
      to_a.select(&block)
    end
    
    def eql?(other)
      instance_of?(other.class) &&
        @members.all? { |key| self[key].eql?(other[key]) }
    end
    
    def ==(other)
      other = coerce(other).first if respond_to?(:coerce, true)
      other.kind_of?(self.class) &&
        @members.all? { |key| self[key] == other[key] }
    end
    
    def hash
      if RUBY_ENGINE == 'opal'
        # Opal doesn't implement hash as Integer everywhere, returning strings as themselves,
        # so build a String everywhere for now as the safest common denominator to be consistent.
        self.class.hash.to_s +
          to_a.each_with_index.map {|value, i| (i+1).to_s + value.hash.to_s}.reduce(:+)
      else
        self.class.hash +
          to_a.each_with_index.map {|value, i| i+1 * value.hash}.sum
      end
    end
            
    if keyword_init
      def initialize(struct_class_keyword_args = {})
        members
        @member_values = {}
        struct_class_keyword_args.each do |attribute, value|
          self[attribute] = value
        end
      end
    else
      def initialize(*attribute_values)
        members
        @member_values = {}
        attribute_values.each_with_index do |value, i|
          attribute = @members[i]
          self[attribute] = value
        end
      end
    end
  end
end
ARG_VALIDATION =
lambda do |class_name_or_attribute, *attributes|
  class_name_or_attribute.nil? || attributes.any?(&:nil?)
end
CLASS_NAME_EXTRACTION =
lambda do |class_name_or_attribute|
  if class_name_or_attribute.is_a?(String) && RUBY_ENGINE != 'opal'
    raise NameError, "identifier name needs to be constant" unless class_name_or_attribute.match(/^[A-Z]/)
    class_name_or_attribute
  end
end

Class Method Summary collapse

Class Method Details

.__new__Object



141
# File 'lib/pure-struct.rb', line 141

alias __new__ new

.new(class_name_or_attribute, *attributes, keyword_init: false) ⇒ Object



142
143
144
145
146
147
148
149
150
# File 'lib/pure-struct.rb', line 142

def new(class_name_or_attribute, *attributes, keyword_init: false)
  raise 'Arguments cannot be nil' if ARG_VALIDATION[class_name_or_attribute, *attributes]
  class_name = CLASS_NAME_EXTRACTION[class_name_or_attribute]
  attributes.unshift(class_name_or_attribute) if class_name.nil?
  attributes = attributes.map(&:to_sym)
  struct_class = Class.new(self, &CLASS_DEFINITION_FOR_ATTRIBUTES[attributes, keyword_init])
  struct_class.singleton_class.define_method(:new) {|*args, &block| __new__(*args, &block)}
  class_name.nil? ? struct_class : const_set(class_name, struct_class)
end