Class: SafeOstruct
- Inherits:
-
Object
- Object
- SafeOstruct
- Defined in:
- lib/safe_ostruct.rb,
lib/safe_ostruct/version.rb
Overview
SafeOstruct
SafeOstruct is a fast, flexible struct-like class with an OpenStruct-style
interface. It handles missing methods gracefully by returning nil instead
of raising a NoMethodError for undefined attributes or methods.
For speed, SafeOstruct.new returns an instance of a cached "shape" subclass
generated per unique key set. Shape classes carry real attr_accessor
methods backed by instance variables, so attribute reads and writes are
ordinary inline-cached method calls (Struct-like speed) rather than
method_missing dispatch. method_missing remains as a fallback for
attributes added after creation and always returns nil for anything
undefined.
Key features:
- Define attributes dynamically with keyword arguments.
- Handle missing methods by returning
nilinstead of raisingNoMethodError. - Support both symbol and string access for attributes.
- Dynamically add or update attributes using method assignment or hash-style assignment.
- Remove attributes dynamically using
delete_field. - OpenStruct API parity:
==,eql?,hash,dig,each_pair,inspect.
Example usage:
obj = SafeOstruct.new(name: 'John Doe')
obj.name # => 'John Doe'
obj[:name] # => 'John Doe'
obj['name'] # => 'John Doe'
obj.address = '123 Main St'
obj.age # => nil (undefined attributes return nil)
obj.delete_field(:address)
obj.to_h # => { name: 'John Doe' }
Storage model: attribute values live in instance variables (@name), except
keys that are not valid identifiers (e.g. "foo-bar") or that start with
__, which live in an internal overflow hash and are reachable via [],
to_h, each_pair, and delete_field. Keys that collide with existing
methods (e.g. :to_h, :class) are stored but only reachable hash-style.
Constant Summary collapse
- VALID_ATTRIBUTE_NAME =
Attribute names that can be stored as instance variables.
/\A[a-zA-Z_][a-zA-Z0-9_]*\z/- SHAPE_CACHE_LIMIT =
Maximum number of shape classes cached per base class. Beyond this, instances are built on the base class itself (correct but slower), so arbitrary key sets (e.g. from JSON) cannot create classes unboundedly.
1000- VERSION =
"2.0.0".freeze
Class Attribute Summary collapse
-
.__base__ ⇒ Class?
readonly
The user-visible class a shape was generated from.
-
.__overflow_keys__ ⇒ Array<Symbol>
readonly
Frozen list of shape keys stored in the overflow hash.
Class Method Summary collapse
- .inherited(subclass) ⇒ Object
-
.new(**kwargs) ⇒ SafeOstruct
Builds an instance of the cached shape class for the given key set.
Instance Method Summary collapse
-
#==(other) ⇒ Boolean
(also: #eql?)
Two SafeOstructs are equal when their attributes are equal, regardless of which shape class backs them.
-
#[](key) ⇒ Object?
Access attributes using [] with symbol or string.
-
#[]=(key, value) ⇒ Object
Set attributes using [] with symbol or string.
-
#delete_field(key) ⇒ Object?
Deletes an attribute from the struct.
-
#dig(key, *keys) ⇒ Object?
Extracts a nested value, like Hash#dig.
-
#each_pair {|key, value| ... } ⇒ SafeOstruct, Enumerator
Yields each attribute name and value; returns an Enumerator without a block.
-
#hash ⇒ Integer
Hash code derived from the attributes.
-
#initialize(**kwargs) ⇒ SafeOstruct
constructor
Direct construction path (used when the shape cache is bypassed).
-
#inspect ⇒ String
(also: #to_s)
Readable representation, e.g.
-
#method_missing(method_name, *args, &_block) ⇒ Object?
Handles attributes added after creation and reads of undefined attributes, which return
nilinstead of raisingNoMethodError. -
#respond_to_missing?(method_name, include_private = false) ⇒ Boolean
True for attributes that currently hold a value.
-
#to_h ⇒ Hash
Converts the attributes of the SafeOstruct into a hash.
Constructor Details
#initialize(**kwargs) ⇒ SafeOstruct
Direct construction path (used when the shape cache is bypassed).
168 169 170 |
# File 'lib/safe_ostruct.rb', line 168 def initialize(**kwargs) __init_attributes__(kwargs) unless kwargs.empty? end |
Dynamic Method Handling
This class handles dynamic methods through the method_missing method
#method_missing(method_name, *args, &_block) ⇒ Object?
Handles attributes added after creation and reads of undefined
attributes, which return nil instead of raising NoMethodError.
178 179 180 181 182 183 184 |
# File 'lib/safe_ostruct.rb', line 178 def method_missing(method_name, *args, &_block) if method_name.end_with?("=") self[method_name[0...-1]] = args.first else self[method_name] end end |
Class Attribute Details
.__base__ ⇒ Class? (readonly)
Returns the user-visible class a shape was generated from.
74 75 76 |
# File 'lib/safe_ostruct.rb', line 74 def __base__ @__base__ end |
.__overflow_keys__ ⇒ Array<Symbol> (readonly)
Returns frozen list of shape keys stored in the overflow hash.
71 72 73 |
# File 'lib/safe_ostruct.rb', line 71 def __overflow_keys__ @__overflow_keys__ end |
Class Method Details
.inherited(subclass) ⇒ Object
76 77 78 79 80 81 82 83 |
# File 'lib/safe_ostruct.rb', line 76 def inherited(subclass) super subclass.instance_variable_set(:@__shape__, false) subclass.instance_variable_set(:@__shapes__, {}) subclass.instance_variable_set(:@__shapes_lock__, Mutex.new) subclass.instance_variable_set(:@__overflow_keys__, [].freeze) subclass.instance_variable_set(:@__base__, nil) end |
.new(**kwargs) ⇒ SafeOstruct
Builds an instance of the cached shape class for the given key set. On shape classes this constructs normally.
90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 |
# File 'lib/safe_ostruct.rb', line 90 def new(**kwargs) return super if @__shape__ keys = kwargs.keys unless keys.all?(Symbol) kwargs = kwargs.transform_keys(&:to_sym) keys = kwargs.keys end shape = __shape_for__(keys) if shape instance = shape.allocate instance.__send__(:__shape_init__, kwargs) else instance = allocate instance.__send__(:__init_attributes__, kwargs) end instance end |
Instance Method Details
#==(other) ⇒ Boolean Also known as: eql?
Two SafeOstructs are equal when their attributes are equal, regardless of which shape class backs them.
284 285 286 |
# File 'lib/safe_ostruct.rb', line 284 def ==(other) other.is_a?(SafeOstruct) && to_h == other.to_h end |
#[](key) ⇒ Object?
Access attributes using [] with symbol or string.
190 191 192 193 194 195 196 197 |
# File 'lib/safe_ostruct.rb', line 190 def [](key) key = key.to_sym unless key.is_a?(Symbol) if (ivar = IVAR_LOOKUP[key]) instance_variable_get(ivar) elsif (overflow = @__overflow__) overflow[key] end end |
#[]=(key, value) ⇒ Object
Set attributes using [] with symbol or string.
203 204 205 206 207 208 209 210 |
# File 'lib/safe_ostruct.rb', line 203 def []=(key, value) key = key.to_sym unless key.is_a?(Symbol) if (ivar = IVAR_LOOKUP[key]) instance_variable_set(ivar, value) else (@__overflow__ ||= {})[key] = value end end |
#delete_field(key) ⇒ Object?
Deletes an attribute from the struct.
231 232 233 234 235 236 237 238 |
# File 'lib/safe_ostruct.rb', line 231 def delete_field(key) key = key.to_sym unless key.is_a?(Symbol) if (ivar = IVAR_LOOKUP[key]) remove_instance_variable(ivar) if instance_variable_defined?(ivar) else @__overflow__&.delete(key) end end |
#dig(key, *keys) ⇒ Object?
Extracts a nested value, like Hash#dig.
274 275 276 277 |
# File 'lib/safe_ostruct.rb', line 274 def dig(key, *keys) value = self[key] keys.empty? ? value : value&.dig(*keys) end |
#each_pair {|key, value| ... } ⇒ SafeOstruct, Enumerator
Yields each attribute name and value; returns an Enumerator without a block.
261 262 263 264 265 266 267 |
# File 'lib/safe_ostruct.rb', line 261 def each_pair(&block) pairs = to_h return pairs.each_pair unless block_given? pairs.each_pair(&block) self end |
#hash ⇒ Integer
Returns hash code derived from the attributes.
290 291 292 |
# File 'lib/safe_ostruct.rb', line 290 def hash to_h.hash end |
#inspect ⇒ String Also known as: to_s
Returns readable representation, e.g. #
295 296 297 298 299 300 |
# File 'lib/safe_ostruct.rb', line 295 def inspect base = self.class.__base__ || self.class label = base.name || "SafeOstruct" attrs = to_h.map { |key, value| "#{key}=#{value.inspect}" }.join(", ") attrs.empty? ? "#<#{label}>" : "#<#{label} #{attrs}>" end |
#respond_to_missing?(method_name, include_private = false) ⇒ Boolean
True for attributes that currently hold a value. Shape attributes also respond via their real accessor methods.
218 219 220 221 222 223 224 225 |
# File 'lib/safe_ostruct.rb', line 218 def respond_to_missing?(method_name, include_private = false) if (ivar = IVAR_LOOKUP[method_name]) instance_variable_defined?(ivar) || super else overflow = @__overflow__ (overflow ? overflow.key?(method_name) : false) || super end end |
#to_h ⇒ Hash
Converts the attributes of the SafeOstruct into a hash.
Mutating the returned hash does not affect the struct.
244 245 246 247 248 249 250 251 252 253 254 |
# File 'lib/safe_ostruct.rb', line 244 def to_h hash = {} instance_variables.each do |ivar| next if ivar.start_with?("@__") hash[ivar[1..].to_sym] = instance_variable_get(ivar) end overflow = @__overflow__ hash.merge!(overflow) if overflow && !overflow.empty? hash end |