Class: ImmutableStruct
- Inherits:
-
Object
- Object
- ImmutableStruct
- Defined in:
- lib/immutable-struct.rb
Overview
Creates classes for value objects/read-only records. Most useful when creating model objects for concepts not stored in the database.
This will create a class that has attr_readers for all given attributes, as well as a hash-based constructor. Further, the block given to with_attributes will be evaluated as if it were inside a class definition, allowing you to add methods, include or extend modules, or do whatever else you want.
Constant Summary collapse
- VERSION =
:nodoc:
'2.2.2'
Class Method Summary collapse
-
.new(*attributes, &block) ⇒ Object
Create a new class with the given read-only attributes.
Class Method Details
.new(*attributes, &block) ⇒ Object
Create a new class with the given read-only attributes.
- attributes
-
list of symbols or strings that can be used to create attributes. Any attribute with a question mark in it (e.g.
:foo?) will create an attribute without a question mark that passes through the raw value and an attribute with the question mark that coerces that value to a boolean. You would initialize it with the non-question-mark value An attribute that is an array of one symbol will create an attribute named for that symbol, but that doesn’t return nil, instead returning theto_aof the value passed to the construtor. - block
-
if present, evaluates in the context of the new class, so
def,def.self,includeandextendshould all work as in a normal class definition.
Example:
Person = ImmutableStruct.new(:name, :location, :minor?, [:aliases])
p = Person.new(name: 'Dave', location: Location.new("DC"), minor: false)
p.name # => 'Dave'
p.location # => <Location: @where="DC">
p.minor # => false
p.minor? # => false
p = Person.new(name: 'Rudy', minor: "yup")
p.name # => 'Rudy'
p.location # => nil
p.minor # => "yup"
p.minor? # => true
new_person = p.merge(name: "Other Dave", age: 41) # returns a new object with merged attributes
new_person.name # => "Other Dave"
new_person.age # => 41
new_person.active? # => true
Note that you also get an implementation of to_h that will include all no-arg methods in its output:
Person = ImmutableStruct.new(:name, :location, :minor?, [:aliases])
p = Person.new(name: 'Dave', minor: "yup", aliases: [ "davetron", "davetron5000" ])
p.to_h # => { name: "Dave", minor: "yup", minor?: true, aliases: ["davetron", "davetron5000" ] }
This has two subtle side-effects:
-
Methods that take no args, but are not ‘attributes’ will get called by
to_h. This shouldn’t be a problem, because you should not generally be doing this on a struct-like class. -
Methods that take no args, but call
to_hwill stack overflow. This is because the class’ internals have no way to know about this. This is particularly a problem if you want to define your ownto_jsonmethod that serializes the result ofto_h.
59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 |
# File 'lib/immutable-struct.rb', line 59 def self.new(*attributes,&block) klass = Class.new do attributes.each do |attribute| if attribute.to_s =~ /(^.*)\?$/ raw_name = $1 attr_reader raw_name define_method(attribute) do !!instance_variable_get("@#{raw_name}") end elsif attribute.kind_of?(Array) and attribute.size == 1 attr_reader attribute[0] else attr_reader attribute end end define_method(:initialize) do |*args| attrs = args[0] || {} attributes.each do |attribute| if attribute.kind_of?(Array) and attribute.size == 1 ivar_name = attribute[0].to_s instance_variable_set("@#{ivar_name}", (attrs[ivar_name.to_s] || attrs[ivar_name.to_sym]).to_a) else ivar_name = attribute.to_s.gsub(/\?$/,'') attr_value = attrs[ivar_name.to_s].nil? ? attrs[ivar_name.to_sym] : attrs[ivar_name.to_s] instance_variable_set("@#{ivar_name}", attr_value) end end end define_method(:==) do |other| return false unless other.is_a?(klass) attributes.all? { |attribute| self.send(attribute) == other.send(attribute) } end define_method(:merge) do |new_attrs| attrs = to_h klass.new(attrs.merge(new_attrs)) end alias_method :eql?, :== define_method(:hash) do attribute_values = attributes.map { |attr| self.send(attr) } (attribute_values + [self.class]).hash end end klass.class_exec(&block) unless block.nil? imethods = klass.instance_methods(include_super=false).map { |method_name| klass.instance_method(method_name) }.reject { |method| method.arity != 0 }.map(&:name).map(&:to_sym) klass.class_exec(imethods) do |imethods| define_method(:to_h) do imethods.inject({}) do |hash, method| next hash if [:==, :eql?, :merge, :hash].include?(method) hash.merge(method.to_sym => self.send(method)) end end end klass end |