How to change the default value of a Struct attribute? - ruby

According to the documentation unset attributes of Struct are set to nil:
unset parameters default to nil.
Is it possible to specify the default value for particular attributes?
For example, for the following Struct
Struct.new("Person", :name, :happy)
I would like the attribute happy to default to true rather than nil. How can I do this? If I do as follows
Struct.new("Person", :name, :happy = true)
I get
-:1: syntax error, unexpected '=', expecting ')'
Struct.new("Person", :name, :happy = true)
^
-:1: warning: possibly useless use of true in void context

This can also be accomplished by creating your Struct as a subclass, and overriding
initialize with default values as in the following example:
class Person < Struct.new(:name, :happy)
def initialize(name, happy=true); super end
end
On one hand, this method does lead to a little bit of boilerplate; on the other, it does what you're looking for nice and succinctly.
One side-effect (which may be either a benefit or an annoyance depending on your preferences/use case) is that you lose the default Struct behavior of all attributes defaulting to nil -- unless you explicitly set them to be so. In effect, the above example would make name a required parameter unless you declare it as name=nil

Following #rintaun's example you can also do this with keyword arguments in Ruby 2+
A = Struct.new(:a, :b, :c) do
def initialize(a:, b: 2, c: 3); super end
end
A.new
# ArgumentError: missing keyword: a
A.new a: 1
# => #<struct A a=1, b=2, c=3>
A.new a: 1, c: 6
# => #<struct A a=1, b=2, c=6>
UPDATE
The code now needs to be written as follows to work.
A = Struct.new(:a, :b, :c) do
def initialize(a:, b: 2, c: 3)
super(a, b, c)
end
end

I also found this:
Person = Struct.new "Person", :name, :happy do
def initialize(*)
super
self.location ||= true
end
end

#Linuxios gave an answer that overrides member lookup. This has a couple problems: you can't explicitly set a member to nil and there's extra overhead on every member reference. It seems to me you really just want to supply the defaults when initializing a new struct object with partial member values supplied to ::new or ::[].
Here's a module to extend Struct with an additional factory method that lets you describe your desired structure with a hash, where the keys are the member names and the values the defaults to fill in when not supplied at initialization:
# Extend stdlib Struct with a factory method Struct::with_defaults
# to allow StructClasses to be defined so omitted members of new structs
# are initialized to a default instead of nil
module StructWithDefaults
# makes a new StructClass specified by spec hash.
# keys are member names, values are defaults when not supplied to new
#
# examples:
# MyStruct = Struct.with_defaults( a: 1, b: 2, c: 'xyz' )
# MyStruct.new #=> #<struct MyStruct a=1, b=2, c="xyz"
# MyStruct.new(99) #=> #<struct MyStruct a=99, b=2, c="xyz">
# MyStruct[-10, 3.5] #=> #<struct MyStruct a=-10, b=3.5, c="xyz">
def with_defaults(*spec)
new_args = []
new_args << spec.shift if spec.size > 1
spec = spec.first
raise ArgumentError, "expected Hash, got #{spec.class}" unless spec.is_a? Hash
new_args.concat spec.keys
new(*new_args) do
class << self
attr_reader :defaults
end
def initialize(*args)
super
self.class.defaults.drop(args.size).each {|k,v| self[k] = v }
end
end.tap {|s| s.instance_variable_set(:#defaults, spec.dup.freeze) }
end
end
Struct.extend StructWithDefaults

Just add another variation:
class Result < Struct.new(:success, :errors)
def initialize(*)
super
self.errors ||= []
end
end

I think that the override of the #initialize method is the best way, with call to #super(*required_args).
This has an additional advantage of being able to use hash-style arguments. Please see the following complete and compiling example:
Hash-Style Arguments, Default Values, and Ruby Struct
# This example demonstrates how to create Ruby Structs that use
# newer hash-style parameters, as well as the default values for
# some of the parameters, without loosing the benefits of struct's
# implementation of #eql? #hash, #to_s, #inspect, and other
# useful instance methods.
#
# Run this file as follows
#
# > gem install rspec
# > rspec struct_optional_arguments.rb --format documentation
#
class StructWithOptionals < Struct.new(
:encrypted_data,
:cipher_name,
:iv,
:salt,
:version
)
VERSION = '1.0.1'
def initialize(
encrypted_data:,
cipher_name:,
iv: nil,
salt: 'salty',
version: VERSION
)
super(encrypted_data, cipher_name, iv, salt, version)
end
end
require 'rspec'
RSpec.describe StructWithOptionals do
let(:struct) { StructWithOptionals.new(encrypted_data: 'data', cipher_name: 'AES-256-CBC', iv: 'intravenous') }
it 'should be initialized with default values' do
expect(struct.version).to be(StructWithOptionals::VERSION)
end
context 'all fields must be not null' do
%i(encrypted_data cipher_name salt iv version).each do |field|
subject { struct.send(field) }
it field do
expect(subject).to_not be_nil
end
end
end
end

Related

Struct with and without member

I'm a new Rubyist, currently I'm going to use the Struct in class in order to create a temporary object. However, I encounter a question when I use a member in Struct like this:
class A
attr_accessor :b
B = Struct.new(:name, :print_name) do
def print_name
p "Hello #{name}"
end
end
def initialize(input_name)
#b = B.new(input_name)
end
end
a = A.new('Leo')
a.b.print_name # Hello Leo
But I also receive the same result when my Struct B params don't include :print_name.
B = Struct.new(:name) do
...
end
So what is the different ? And when should I use the member param and when I'm not ?
Thanks
In first case you define a class which's initializer takes two arguments - name and print_name.
In second case you define a class which's initializer takes single argument - name.
These has nothing to do with the fact, that you are defining an instance method called print_name.
So instances of both classes (with and without print_name argument) have print_name method defined, thus both examples work identically.
The difference will be visible when you inspect created objects:
# first case with two arguments
foo = B.new(:a, :b)
foo.inspect
=> "#<struct B name=:a, print_name=:b>"
# second case with single argument
foo = B.new(:a)
foo.inspect
=> "#<struct B name=:a>"
Also, when you'll check instance methods of B class for both cases, you can see the difference:
# first case with two arguments
B.instance_methods false
#=> [:name, :name=, :print_name, :print_name=]
# second case with single argument
B.instance_methods false
#=> [:name, :name=, :print_name]
But I also receive the same result when my Struct B params don't
include :print_name
The difference, is that in first case you are able to do the following:
a.b.print_name = 'new print name'
a.b.inspect
#=> "#<struct B name='Leo', print_name='new print name'>"
Whereas in second case it will fail:
a.b.print_name = 'new print name'
#=> NoMethodError: undefined method 'print_name=' for #<struct B name='Leo'>

Assignment Branch Condition too high

I have a simple class that, on initialization, takes between one and eight parameters. It sets the accessors to these to use later. Rubocop is trying to arrest me for the ABC being too high, but I'm not sure if there's actually anything wrong with what I've done. Is this a case where I just disable the inspection on my initialize?
class Foo
attr_accessor :one, :two, :three, :four
attr_accessor :five, :six, :seven, :eight
def initialize(p={})
#one = p[:one] if p[:one].present?
# ...
#eight = p[:eight] if p[:eight].present?
end
end
My only thought on reducing size would be to do something like iterating through all my attr_accessors on initialize, seeing if there is a corresponding symbol passed through in the has, and if so assigning it.
class Foo
attr_accessor :one, :two, :three, :four
attr_accessor :five, :six, :seven, :eight
def initialize(p={})
instance_variables.each do |variable|
send("##{variable}") = p["#{send(variable)}".to_sym] if p["#{send(variable)}".to_sym].present?
end
end
end
But this seems kind of weak.
Here is one of the ways to achieve what you are trying to do:
class Foo
attr_accessor(*%i[one two three four five six seven eight])
def initialize(p = {})
p.keys.each { |k| instance_variable_set("##{k}", p.fetch(k, nil)) }
end
end
Check out for Hash#fetch method.
You can also use it to just access the key-value pairs of p variable, if you instead of 8 variables decide to go with one (#p)
EDIT
Just out of curiosity wrote this version (some meta programming used) - it will dynamically add attr_accessor for added instance variables:
class Foo
def initialize(p = {})
p.keys.each do |k|
instance_variable_set("##{k}", p.fetch(k, nil))
self.class.__send__(:attr_accessor, k)
end
end
end
What is happening, is we take provided to initialize method argument (hash p), get its keys and create instance variables from them, assigning each variable with value corresponding to the key. Then we're defining attr_accessor for each of the keys.
a = Foo.new(a: 2, b: 3)
#=> #<Foo:0x00000002d63ad8 #a=2, #b=3>
You shouldn't assign each of those as different variables. You should rather save that to a variable as a single hash, and access the hash when you need the values. In fact, you already seem to have a variable p. So keep that as #p = p.

Ruby empty parameter in function

I'm trying to enqueue various functions in a generic way with this code :
{ Object.const_get(object_name).new(job[:params]||={}).delay(:queue => queue).send(method_name)}
job is a Hash where I get the name, objects parameters etc...
My problem is in this case :
class Foo
def initialize
puts 'bar'
end
end
Foo doesn't take parameters for its instanciation.
So if I use the previous line with Foo as object_name I'll get this error :
ArgumentError: wrong number pf arguments (1 for 0)
And I absolutly don't want to write something like that :
if job.has_key?[:param] then
Object.const_get(object_name).new(job[:params]||={}).delay(:queue => queue).send(method_name)
else
Object.const_get(object_name).new().delay(:queue => queue).send(method_name)
end
What could I write instead of job[:params]||={} so it works for every case?
Thanks in advance.
you can achieve this with using Foo.send and using an array.
For instance
Object.
const_get(object_name).
send(*(job.has_key?(:param) ? ['new', job[:param]] : ['new']))...
I personally think it is not worth it and an if statement is easier on the eyes.
The initialize method of your Foo class should receive a parameter with a default value. Like this:
class Foo
def initialize(params={})
# Here you do stuff like checking if params is empty or whatever.
end
end
This way you will achieve the two behaviors.
Based on your example, I think the test you accepted might be wrong. Your code suggests that you shouldn't be testing whether the :params key exists in the hash, you should be testing whether initialize takes an argument. If initialize does take an argument, then you send it an argument regardless of whether the :params key exists in the hash. The accepted answer will fail when the :params key doesn't exist in the hash, and yet the initialize method takes an argument--you'll get a 0 for 1 error. Is that a possibility?
class Dog
def initialize(params)
p params
puts "dog"
end
end
class Cat
def initialize
puts "cat"
end
end
class_names = ["Dog", "Cat"]
job = {
params: {a: 1, b: 2, c: 3}
}
class_names.each do |class_name|
class_obj = Object.const_get(class_name)
if class_obj.instance_method(:initialize).arity == 0
send_args = 'new'
else
send_args = 'new', job[:params] ||= {}
end
class_obj.send(*send_args)
end
--output:--
{:a=>1, :b=>2, :c=>3}
dog
cat

Empty hash as default parameter turning into an Array?

A parameter defaulted to an empty hash attrs={} returns an error:
can't convert Array into Hash
(TypeError)
I've tried this on Ruby versions 1.8.6, 1.8.7 and 1.9.1. A hash will be passed to attrs.
class Category < Object
attr_accessor :attributes
attr_accessor :rel_attrs
attr_accessor :cls_str
def initialize (term='',title='', attrs={}, scheme = '', rel=[], cls_str='')
#attributes ={}
#attributes['scheme'] = scheme
#attributes['term'] = term
#attributes['title'] = title
#attributes['related'] = rel
#cls_str = cls_str
if not attrs.empty?
#attributes.update attrs
end
end
end
What am I doing wrong?
Some notes:
You don't have to inherit from Object.
if not can more idiomatically be expressed as unless.
Making the attrs hash the last argument has the advantage that you can leave off the {} around the hash elements when calling Category.new. You can not do this if the hash is the middle, so it would make sense to show us your call to Category.new.
I changed your code accordingly:
class Category
attr_accessor :attributes
attr_accessor :rel_attrs
attr_accessor :cls_str
def initialize (term='',title='', scheme = '', rel=[], cls_str='', attrs={})
#attributes ={}
#attributes['scheme'] = scheme
#attributes['term'] = term
#attributes['title'] = title
#attributes['related'] = rel
#cls_str = cls_str
#attributes.update(attrs) unless attrs.empty?
end
end
Here's how you call it:
>> c = Category.new("term", "title", "scheme", [1,2,3], 'cls_string', :foo => 'bar', :baz => 'qux')
#=> #<Category:0x00000100b7bff0 #attributes={"scheme"=>"scheme", "term"=>"term", "title"=>"title", "related"=>[1, 2, 3], :foo=>"bar", :baz=>"qux"}, cls_str"cls_string"
>> c.attributes
#=> {"scheme"=>"scheme", "term"=>"term", "title"=>"title", "related"=>[1, 2, 3], :foo=>"bar", :baz=>"qux"}
This obviously has the problem that all other optional arguments have to be specified in order to be able to specify attrs. If you don't want this, move attrs back to the middle of the argument list and make sure to include the {} in the call. Or even better, make the whole argument list a hash and merge it with the defaults args. Something like
class Category(opts = {})
# stuff skipped
#options = { :bla => 'foo', :bar => 'baz'}.merge(opts)
# more stuff skipped
end

Initialize a Ruby class from an arbitrary hash, but only keys with matching accessors

Is there a simple way to list the accessors/readers that have been set in a Ruby Class?
class Test
attr_reader :one, :two
def initialize
# Do something
end
def three
end
end
Test.new
=> [one,two]
What I'm really trying to do is to allow initialize to accept a Hash with any number of attributes in, but only commit the ones that have readers already defined. Something like:
def initialize(opts)
opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
eval("##{opt} = \"#{val}\"")
end
end
Any other suggestions?
This is what I use (I call this idiom hash-init).
def initialize(object_attribute_hash = {})
object_attribute_hash.map { |(k, v)| send("#{k}=", v) }
end
If you are on Ruby 1.9 you can do it even cleaner (send allows private methods):
def initialize(object_attribute_hash = {})
object_attribute_hash.map { |(k, v)| public_send("#{k}=", v) }
end
This will raise a NoMethodError if you try to assign to foo and method "foo=" does not exist. If you want to do it clean (assign attrs for which writers exist) you should do a check
def initialize(object_attribute_hash = {})
object_attribute_hash.map do |(k, v)|
writer_m = "#{k}="
send(writer_m, v) if respond_to?(writer_m) }
end
end
however this might lead to situations where you feed your object wrong keys (say from a form) and instead of failing loudly it will just swallow them - painful debugging ahead. So in my book a NoMethodError is a better option (it signifies a contract violation).
If you just want a list of all writers (there is no way to do that for readers) you do
some_object.methods.grep(/\w=$/)
which is "get an array of method names and grep it for entries which end with a single equals sign after a word character".
If you do
eval("##{opt} = \"#{val}\"")
and val comes from a web form - congratulations, you just equipped your app with a wide-open exploit.
You could override attr_reader, attr_writer and attr_accessor to provide some kind of tracking mechanism for your class so you can have better reflection capability such as this.
For example:
class Class
alias_method :attr_reader_without_tracking, :attr_reader
def attr_reader(*names)
attr_readers.concat(names)
attr_reader_without_tracking(*names)
end
def attr_readers
#attr_readers ||= [ ]
end
alias_method :attr_writer_without_tracking, :attr_writer
def attr_writer(*names)
attr_writers.concat(names)
attr_writer_without_tracking(*names)
end
def attr_writers
#attr_writers ||= [ ]
end
alias_method :attr_accessor_without_tracking, :attr_accessor
def attr_accessor(*names)
attr_readers.concat(names)
attr_writers.concat(names)
attr_accessor_without_tracking(*names)
end
end
These can be demonstrated fairly simply:
class Foo
attr_reader :foo, :bar
attr_writer :baz
attr_accessor :foobar
end
puts "Readers: " + Foo.attr_readers.join(', ')
# => Readers: foo, bar, foobar
puts "Writers: " + Foo.attr_writers.join(', ')
# => Writers: baz, foobar
Try something like this:
class Test
attr_accessor :foo, :bar
def initialize(opts = {})
opts.each do |opt, val|
send("#{opt}=", val) if respond_to? "#{opt}="
end
end
end
test = Test.new(:foo => "a", :bar => "b", :baz => "c")
p test.foo # => nil
p test.bar # => nil
p test.baz # => undefined method `baz' for #<Test:0x1001729f0 #bar="b", #foo="a"> (NoMethodError)
This is basically what Rails does when you pass in a params hash to new. It will ignore all parameters it doesn't know about, and it will allow you to set things that aren't necessarily defined by attr_accessor, but still have an appropriate setter.
The only downside is that this really requires that you have a setter defined (versus just the accessor) which may not be what you're looking for.
Accessors are just ordinary methods that happen to access some piece of data. Here's code that will do roughly what you want. It checks if there's a method named for the hash key and sets an accompanying instance variable if so:
def initialize(opts)
opts.each do |opt,val|
instance_variable_set("##{opt}", val.to_s) if respond_to? opt
end
end
Note that this will get tripped up if a key has the same name as a method but that method isn't a simple instance variable access (e.g., {:object_id => 42}). But not all accessors will necessarily be defined by attr_accessor either, so there's not really a better way to tell. I also changed it to use instance_variable_set, which is so much more efficient and secure it's ridiculous.
There's no built-in way to get such a list. The attr_* functions essentially just add methods, create an instance variable, and nothing else. You could write wrappers for them to do what you want, but that might be overkill. Depending on your particular circumstances, you might be able to make use of Object#instance_variable_defined? and Module#public_method_defined?.
Also, avoid using eval when possible:
def initialize(opts)
opts.delete_if{|opt,val| not the_list_of_readers.include?(opt)}.each do |opt,val|
instance_variable_set "##{opt}", val
end
end
You can look to see what methods are defined (with Object#methods), and from those identify the setters (the last character of those is =), but there's no 100% sure way to know that those methods weren't implemented in a non-obvious way that involves different instance variables.
Nevertheless Foo.new.methods.grep(/=$/) will give you a printable list of property setters. Or, since you have a hash already, you can try:
def initialize(opts)
opts.each do |opt,val|
instance_variable_set("##{opt}", val.to_s) if respond_to? "#{opt}="
end
end

Resources