# frozen_string_literal: true module JSI module Util::Private # like a Struct, but stores all the attributes in one @attributes Hash, instead of individual instance # variables for each attribute. # this tends to be easier to work with and more flexible. keys which are symbols are converted to strings. class AttrStruct class AttrStructError < StandardError end class UndefinedAttributeKey < AttrStructError end class << self # creates a AttrStruct subclass with the given attribute keys. # @param attribute_keys [Enumerable] def subclass(*attribute_keys) bad = attribute_keys.reject { |key| key.respond_to?(:to_str) || key.is_a?(Symbol) } unless bad.empty? raise ArgumentError, "attribute keys must be String or Symbol; got keys: #{bad.map(&:inspect).join(', ')}" end attribute_keys = attribute_keys.map { |key| convert_key(key) } all_attribute_keys = (self.attribute_keys + attribute_keys).freeze Class.new(self).tap do |klass| klass.define_singleton_method(:attribute_keys) { all_attribute_keys } attribute_keys.each do |attribute_key| # reader klass.send(:define_method, attribute_key) do @attributes[attribute_key] end # writer klass.send(:define_method, "#{attribute_key}=") do |value| @attributes[attribute_key] = value end end end end alias_method :[], :subclass # the attribute keys defined for this class # @return [Set] def attribute_keys # empty for AttrStruct itself; redefined on each subclass Util::Private::EMPTY_SET end # returns a frozen string, given a string or symbol. # returns anything else as-is for the caller to handle. # @api private def convert_key(key) # TODO use Symbol#name when available on supported rubies key.is_a?(Symbol) ? key.to_s.freeze : key.frozen? ? key : key.is_a?(String) ? key.dup.freeze : key end end def initialize(attributes = {}) unless attributes.respond_to?(:to_hash) raise(TypeError, "expected attributes to be a Hash; got: #{attributes.inspect}") end @attributes = {} attributes.to_hash.each do |k, v| @attributes[self.class.convert_key(k)] = v end bad = @attributes.keys.reject { |k| attribute_keys.include?(k) } unless bad.empty? raise UndefinedAttributeKey, "undefined attribute keys: #{bad.map(&:inspect).join(', ')}" end end def [](key) @attributes[key.is_a?(Symbol) ? key.to_s : key] end def []=(key, value) key = self.class.convert_key(key) unless attribute_keys.include?(key) raise UndefinedAttributeKey, "undefined attribute key: #{key.inspect}" end @attributes[key] = value end # @return [String] def inspect "\#<#{self.class.name}#{@attributes.empty? ? '' : ' '}#{@attributes.map { |k, v| "#{k}: #{v.inspect}" }.join(', ')}>" end alias_method :to_s, :inspect # pretty-prints a representation of self to the given printer # @return [void] def pretty_print(q) q.text '#<' q.text self.class.name q.group_sub { q.nest(2) { q.breakable(@attributes.empty? ? '' : ' ') q.seplist(@attributes, nil, :each_pair) { |k, v| q.group { q.text k q.text ': ' q.pp v } } } } q.breakable '' q.text '>' end # (see AttrStruct.attribute_keys) def attribute_keys self.class.attribute_keys end include FingerprintHash def jsi_fingerprint {class: self.class, attributes: @attributes} end end end end