I'm pretty new to Ruby so apologies if this is an obvious question.
I'd like to use named parameters when instantiating a Struct, i.e. be able to specify which items in the Struct get what values, and default the rest to nil.
For example I want to do:
Movie = Struct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'
This doesn't work.
So I came up with the following:
class MyStruct < Struct
# Override the initialize to handle hashes of named parameters
def initialize *args
if (args.length == 1 and args.first.instance_of? Hash) then
args.first.each_pair do |k, v|
if members.include? k then
self[k] = v
end
end
else
super *args
end
end
end
Movie = MyStruct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'
This seems to work just fine, but I'm not sure if there's a better way of doing this, or if I'm doing something pretty insane. If anyone can validate/rip apart this approach, I'd be most grateful.
UPDATE
I ran this initially in 1.9.2 and it works fine; however having tried it in other versions of Ruby (thank you rvm), it works/doesn't work as follows:
1.8.7: Not working
1.9.1: Working
1.9.2: Working
JRuby (set to run as 1.9.2): not working
JRuby is a problem for me, as I'd like to keep it compatible with that for deployment purposes.
YET ANOTHER UPDATE
In this ever-increasing rambling question, I experimented with the various versions of Ruby and discovered that Structs in 1.9.x store their members as symbols, but in 1.8.7 and JRuby, they are stored as strings, so I updated the code to be the following (taking in the suggestions already kindly given):
class MyStruct < Struct
# Override the initialize to handle hashes of named parameters
def initialize *args
return super unless (args.length == 1 and args.first.instance_of? Hash)
args.first.each_pair do |k, v|
self[k] = v if members.map {|x| x.intern}.include? k
end
end
end
Movie = MyStruct.new :title, :length, :rating
m = Movie.new :title => 'Some Movie', :rating => 'R'
This now appears to work for all the flavours of Ruby that I've tried.
Synthesizing the existing answers reveals a much simpler option for Ruby 2.0+:
class KeywordStruct < Struct
def initialize(**kwargs)
super(*members.map{|k| kwargs[k] })
end
end
Usage is identical to the existing Struct, where any argument not given will default to nil:
Pet = KeywordStruct.new(:animal, :name)
Pet.new(animal: "Horse", name: "Bucephalus") # => #<struct Pet animal="Horse", name="Bucephalus">
Pet.new(name: "Bob") # => #<struct Pet animal=nil, name="Bob">
If you want to require the arguments like Ruby 2.1+'s required kwargs, it's a very small change:
class RequiredKeywordStruct < Struct
def initialize(**kwargs)
super(*members.map{|k| kwargs.fetch(k) })
end
end
At that point, overriding initialize to give certain kwargs default values is also doable:
Pet = RequiredKeywordStruct.new(:animal, :name) do
def initialize(animal: "Cat", **args)
super(**args.merge(animal: animal))
end
end
Pet.new(name: "Bob") # => #<struct Pet animal="Cat", name="Bob">
With newer versions of Ruby you can use keyword_init: true:
Movie = Struct.new(:title, :length, :rating, keyword_init: true)
Movie.new(title: 'Title', length: '120m', rating: 'R')
# => #<struct Movie title="Title", length="120m", rating="R">
The less you know, the better. No need to know whether the underlying data structure uses symbols or string, or even whether it can be addressed as a Hash. Just use the attribute setters:
class KwStruct < Struct.new(:qwer, :asdf, :zxcv)
def initialize *args
opts = args.last.is_a?(Hash) ? args.pop : Hash.new
super *args
opts.each_pair do |k, v|
self.send "#{k}=", v
end
end
end
It takes both positional and keyword arguments:
> KwStruct.new "q", :zxcv => "z"
=> #<struct KwStruct qwer="q", asdf=nil, zxcv="z">
A solution that only allows Ruby keyword arguments (Ruby >=2.0).
class KeywordStruct < Struct
def initialize(**kwargs)
super(kwargs.keys)
kwargs.each { |k, v| self[k] = v }
end
end
Usage:
class Foo < KeywordStruct.new(:bar, :baz, :qux)
end
foo = Foo.new(bar: 123, baz: true)
foo.bar # --> 123
foo.baz # --> true
foo.qux # --> nil
foo.fake # --> NoMethodError
This kind of structure can be really useful as a value object especially if you like more strict method accessors which will actually error instead of returning nil (a la OpenStruct).
Have you considered OpenStruct?
require 'ostruct'
person = OpenStruct.new(:name => "John", :age => 20)
p person # #<OpenStruct name="John", age=20>
p person.name # "John"
p person.adress # nil
You could rearrange the ifs.
class MyStruct < Struct
# Override the initialize to handle hashes of named parameters
def initialize *args
# I think this is called a guard clause
# I suspect the *args is redundant but I'm not certain
return super *args unless (args.length == 1 and args.first.instance_of? Hash)
args.first.each_pair do |k, v|
# I can't remember what having the conditional on the same line is called
self[k] = v if members.include? k
end
end
end
Based on #Andrew Grimm's answer, but using Ruby 2.0's keyword arguments:
class Struct
# allow keyword arguments for Structs
def initialize(*args, **kwargs)
param_hash = kwargs.any? ? kwargs : Hash[ members.zip(args) ]
param_hash.each { |k,v| self[k] = v }
end
end
Note that this does not allow mixing of regular and keyword arguments-- you can only use one or the other.
If your hash keys are in order you can call the splat operator to the rescue:
NavLink = Struct.new(:name, :url, :title)
link = {
name: 'Stack Overflow',
url: 'https://stackoverflow.com',
title: 'Sure whatever'
}
actual_link = NavLink.new(*link.values)
#<struct NavLink name="Stack Overflow", url="https://stackoverflow.com", title="Sure whatever">
If you do need to mix regular and keyword arguments, you can always construct the initializer by hand...
Movie = Struct.new(:title, :length, :rating) do
def initialize(title, length: 0, rating: 'PG13')
self.title = title
self.length = length
self.rating = rating
end
end
m = Movie.new('Star Wars', length: 'too long')
=> #<struct Movie title="Star Wars", length="too long", rating="PG13">
This has the title as a mandatory first argument just for illustration. It also has the advantage that you can set defaults for each keyword argument (though that's unlikely to be helpful if dealing with Movies!).
For a 1-to-1 equivalent with the Struct behavior (raise when the required argument is not given) I use this sometimes (Ruby 2+):
def Struct.keyed(*attribute_names)
Struct.new(*attribute_names) do
def initialize(**kwargs)
attr_values = attribute_names.map{|a| kwargs.fetch(a) }
super(*attr_values)
end
end
end
and from there on
class SimpleExecutor < Struct.keyed :foo, :bar
...
end
This will raise a KeyError if you missed an argument, so real nice for stricter constructors and constructors with lots of arguments, data transfer objects and the like.
this doesn't exactly answer the question but I found it to work well if you have say a hash of values you wish to structify. It has the benefit of offloading the need to remember the order of attributes while also not needing to subClass Struct.
MyStruct = Struct.new(:height, :width, :length)
hash = {height: 10, width: 111, length: 20}
MyStruct.new(*MyStruct.members.map {|key| hash[key] })
Ruby 2.x only (2.1 if you want required keyword args). Only tested in MRI.
def Struct.new_with_kwargs(lamb)
members = lamb.parameters.map(&:last)
Struct.new(*members) do
define_method(:initialize) do |*args|
super(* lamb.(*args))
end
end
end
Foo = Struct.new_with_kwargs(
->(a, b=1, *splat, c:, d: 2, **kwargs) do
# must return an array with values in the same order as lambda args
[a, b, splat, c, d, kwargs]
end
)
Usage:
> Foo.new(-1, 3, 4, c: 5, other: 'foo')
=> #<struct Foo a=-1, b=3, splat=[4], c=5, d=2, kwargs={:other=>"foo"}>
The minor downside is that you have to ensure the lambda returns the values in the correct order; the big upside is that you have the full power of ruby 2's keyword args.
Related
I find myself using hash arguments to constructors quite a bit, especially when writing DSLs for configuration or other bits of API that the end user will be exposed to. What I end up doing is something like the following:
class Example
PROPERTIES = [:name, :age]
PROPERTIES.each { |p| attr_reader p }
def initialize(args)
PROPERTIES.each do |p|
self.instance_variable_set "##{p}", args[p] if not args[p].nil?
end
end
end
Is there no more idiomatic way to achieve this? The throw-away constant and the symbol to string conversion seem particularly egregious.
You don't need the constant, but I don't think you can eliminate symbol-to-string:
class Example
attr_reader :name, :age
def initialize args
args.each do |k,v|
instance_variable_set("##{k}", v) unless v.nil?
end
end
end
#=> nil
e1 = Example.new :name => 'foo', :age => 33
#=> #<Example:0x3f9a1c #name="foo", #age=33>
e2 = Example.new :name => 'bar'
#=> #<Example:0x3eb15c #name="bar">
e1.name
#=> "foo"
e1.age
#=> 33
e2.name
#=> "bar"
e2.age
#=> nil
BTW, you might take a look (if you haven't already) at the Struct class generator class, it's somewhat similar to what you are doing, but no hash-type initialization (but I guess it wouldn't be hard to make adequate generator class).
HasProperties
Trying to implement hurikhan's idea, this is what I came to:
module HasProperties
attr_accessor :props
def has_properties *args
#props = args
instance_eval { attr_reader *args }
end
def self.included base
base.extend self
end
def initialize(args)
args.each {|k,v|
instance_variable_set "##{k}", v if self.class.props.member?(k)
} if args.is_a? Hash
end
end
class Example
include HasProperties
has_properties :foo, :bar
# you'll have to call super if you want custom constructor
def initialize args
super
puts 'init example'
end
end
e = Example.new :foo => 'asd', :bar => 23
p e.foo
#=> "asd"
p e.bar
#=> 23
As I'm not that proficient with metaprogramming, I made the answer community wiki so anyone's free to change the implementation.
Struct.hash_initialized
Expanding on Marc-Andre's answer, here is a generic, Struct based method to create hash-initialized classes:
class Struct
def self.hash_initialized *params
klass = Class.new(self.new(*params))
klass.class_eval do
define_method(:initialize) do |h|
super(*h.values_at(*params))
end
end
klass
end
end
# create class and give it a list of properties
MyClass = Struct.hash_initialized :name, :age
# initialize an instance with a hash
m = MyClass.new :name => 'asd', :age => 32
p m
#=>#<struct MyClass name="asd", age=32>
The Struct clas can help you build such a class. The initializer takes the arguments one by one instead of as a hash, but it's easy to convert that:
class Example < Struct.new(:name, :age)
def initialize(h)
super(*h.values_at(:name, :age))
end
end
If you want to remain more generic, you can call values_at(*self.class.members) instead.
There are some useful things in Ruby for doing this kind of thing.
The OpenStruct class will make the values of a has passed to its initialize
method available as attributes on the class.
require 'ostruct'
class InheritanceExample < OpenStruct
end
example1 = InheritanceExample.new(:some => 'thing', :foo => 'bar')
puts example1.some # => thing
puts example1.foo # => bar
The docs are here:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/ostruct/rdoc/OpenStruct.html
What if you don't want to inherit from OpenStruct (or can't, because you're
already inheriting from something else)? You could delegate all method
calls to an OpenStruct instance with Forwardable.
require 'forwardable'
require 'ostruct'
class DelegationExample
extend Forwardable
def initialize(options = {})
#options = OpenStruct.new(options)
self.class.instance_eval do
def_delegators :#options, *options.keys
end
end
end
example2 = DelegationExample.new(:some => 'thing', :foo => 'bar')
puts example2.some # => thing
puts example2.foo # => bar
Docs for Forwardable are here:
http://www.ruby-doc.org/stdlib-1.9.3/libdoc/forwardable/rdoc/Forwardable.html
Given your hashes would include ActiveSupport::CoreExtensions::Hash::Slice, there is a very nice solution:
class Example
PROPERTIES = [:name, :age]
attr_reader *PROPERTIES #<-- use the star expansion operator here
def initialize(args)
args.slice(PROPERTIES).each {|k,v| #<-- slice comes from ActiveSupport
instance_variable_set "##{k}", v
} if args.is_a? Hash
end
end
I would abstract this to a generic module which you could include and which defines a "has_properties" method to set the properties and do the proper initialization (this is untested, take it as pseudo code):
module HasProperties
def self.has_properties *args
class_eval { attr_reader *args }
end
def self.included base
base.extend InstanceMethods
end
module InstanceMethods
def initialize(args)
args.slice(PROPERTIES).each {|k,v|
instance_variable_set "##{k}", v
} if args.is_a? Hash
end
end
end
My solution is similar to Marc-André Lafortune. The difference is that each value is deleted from the input hash as it is used to assign a member variable. Then the Struct-derived class can perform further processing on whatever may be left in the Hash. For instance, the JobRequest below retains any "extra" arguments from the Hash in an options field.
module Message
def init_from_params(params)
members.each {|m| self[m] ||= params.delete(m)}
end
end
class JobRequest < Struct.new(:url, :file, :id, :command, :created_at, :options)
include Message
# Initialize from a Hash of symbols to values.
def initialize(params)
init_from_params(params)
self.created_at ||= Time.now
self.options = params
end
end
Please take a look at my gem, Valuable:
class PhoneNumber < Valuable
has_value :description
has_value :number
end
class Person < Valuable
has_value :name
has_value :favorite_color, :default => 'red'
has_value :age, :klass => :integer
has_collection :phone_numbers, :klass => PhoneNumber
end
jackson = Person.new(name: 'Michael Jackson', age: '50', phone_numbers: [{description: 'home', number: '800-867-5309'}, {description: 'cell', number: '123-456-7890'})
> jackson.name
=> "Michael Jackson"
> jackson.age
=> 50
> jackson.favorite_color
=> "red"
>> jackson.phone_numbers.first
=> #<PhoneNumber:0x1d5a0 #attributes={:description=>"home", :number=>"800-867-5309"}>
I use it for everything from search classes (EmployeeSearch, TimeEntrySearch) to reporting ( EmployeesWhoDidNotClockOutReport, ExecutiveSummaryReport) to presenters to API endpoints. If you add some ActiveModel bits you can easily hook these classes up to forms for gathering criteria. I hope you find it useful.
I have a situation where I would like to create a class which accepts many arguments and has setters and getters in the fewest lines of code possible (for maintainability). I thought that using a Struct would be a good idea for this:
Customer = Struct.new(:id, :username, :first_name, :last_name, :address1, ...etc...)
Customer.new(123, 'joe', 'Joe', ...etc...)
However, I don't like having to know the exact order of the attributes. I prefer Ruby 2's keyword arguments feature:
class Customer
attr_accessor :id, :username, :first_name, ...etc...
def initialize(id:, username:, first_name:, last_name:, address1:, ...etc...)
#id = id
#username = username
#first_name = first_name
...etc...
end
end
Customer.new(id: 123, username: 'joe', first_name: 'Joe', ...etc...)
However, writing this all out requires a lot more code and is more tedious. Is there a way to achieve the same thing in a short-hand like the Struct?
In ruby 2.5 you can do the following:
Customer = Struct.new(
:id,
:username,
:first_name,
keyword_init: true
)
Customer.new(username: "al1ce", first_name: "alice", id: 123)
=> #<struct Customer id=123, username="al1ce", first_name="alice">
references:
https://ruby-doc.org/core-2.5.0/Struct.html
Cant you just do:
def initialize(hash)
hash.each do |key, value|
send("#{key}=", value)
end
end
UPDATE:
To specify default values you can do:
def initialize(hash)
default_values = {
first_name: ''
}
default_values.merge(hash).each do |key, value|
send("#{key}=", value)
end
end
If you want to specify that given attribute is required, but has no default value you can do:
def initialize(hash)
requried_keys = [:id, :username]
default_values = {
first_name: ''
}
raise 'Required param missing' unless (required_keys - hash.keys).empty?
default_values.merge(hash).each do |key, value|
send("#{key}=", value)
end
end
If you don't care about performance, you can use an OpenStruct.
require 'ostruct'
user = OpenStruct.new(id: 1, username: 'joe', first_name: 'Joe', ...)
user.first_name
=> "Joe"
See this for more details.
It's entirely possible to make it a class and define methods on it:
class Customer < Openstruct
def hello
"world"
end
end
joe = Customer.new(id: 1, username: 'joe', first_name: 'Joe', ...)
joe.hello
=> "world"
But again, because OpenStructs are implemented using method_missing and define_method, they are pretty slow. I would go with BroiSatse's answer. If you care about required parameters, you should so something along the lines of
def initialize(params = {})
if missing_required_param?(params)
raise ArgumentError.new("Missing required parameter")
end
params.each do |k,v|
send("#{k}=", v)
end
end
def missing_required_params?(params)
...
end
In addition, you can rely on the constructor of Struct itself.
def initialize(*args)
super(*args)
# put your magic here!
end
This way you avoid the side-affects of named parameters etc.
You can use Struct and reduce the amount of code to a minimum by adding a factory method (called build here) and if necessary a validate method to your struct
Struct.new("Example",:a,:b) do
def build(a:, b:nil)
s = Struct::Example.new(a,b)
s.validate
return s
end
def validate
unless a == 'stuff' || a == 'nonsense'
raise ValidationError, "broken"
end
end
end
m = Struct.Example.build(a: 'stuff')
where validate is intended to so something like check strings have certain values, rather than just relying on the required parameters check.
Now you only have to remember the order once, when you write the build method
Variation on the theme, but a bit more refined, works in any ruby
class Struct
module HashConstructable
def from_hash hash
rv = new
hash.each do |k, v|
rv.send("#{k}=", v)
end
rv
end
# alias_method :[], :from_hash
end
end
and then
class MyStruct < Struct.new(:foo, :boo)
extend Struct::HashConstructable
end
and you have best of both worlds this way - no funny name clashes and side-effects and it's clear want you want to do when you do it:
MyStruct.from_hash(foo: 'foo')
does exactly what you think it does. With a bit more possible side-effects but nicer syntax you can add the alias_method :[], :from_hash part, this allows you:
MyStruct[foo: 'foo']
this is also nice because it reminds (me) of the Hash[] constructor which creates a hash out of something that isn't hash.
This is one approach you could use.
class A
def initialize(h)
h.each do |k, v|
instance_variable_set("##{k}", v)
create_method("#{k}=") { |v|instance_variable_set("##{k}", v ) }
create_method("#{k}") { instance_variable_get("##{k}") }
end
end
end
def create_method(name, &block)
self.class.send(:define_method, name, &block)
end
a = A.new(apple: 1, orange: 2)
a.apple #=> 1
a.apple = 3
a.apple #=> 3
a.orange #=> 2
create_method is straight from the documentation for Module#define_method.
When I call first_array | second_array on two arrays that contain custom objects:
first_array = [co1, co2, co3]
second_array =[co2, co3, co4]
it returns [co1, co2, co3, co2, co3, co4]. It doesn't remove the duplicates. I tried to call uniq on the result, but it didn't work either. What should I do?
Update:
This is the custom object:
class Task
attr_accessor :status, :description, :priority, :tags
def initiate_task task_line
#status = task_line.split("|")[0]
#description = task_line.split("|")[1]
#priority = task_line.split("|")[2]
#tags = task_line.split("|")[3].split(",")
return self
end
def <=>(another_task)
stat_comp = (#status == another_task.status)
desc_comp = (#description == another_task.description)
prio_comp = (#priority == another_task.priority)
tags_comp = (#tags == another_task.tags)
if(stat_comp&desc_comp&prio_comp&tags_comp) then return 0 end
end
end
and when I create few instances of Task type and drop them into two different arrays and when I try to call '|' on them nothing happens it just returns array including both first and second array's elements without the duplicates removed.
No programming language for itself can be aware if two objects are different if you don't implement the correct equality methods.
In the case of ruby you need to implement eql? and hash in your class definition, as these are the methods that the Array class uses to check for equality as stated on Ruby's Array docs:
def eql?(other_obj)
# Your comparing code goes here
end
def hash
#Generates an unique integer based on instance variables
end
For example:
class A
attr_accessor :name
def initialize(name)
#name = name
end
def eql?(other)
#name.eql?(other.name)
end
def hash
#name.hash
end
end
a = A.new('Peter')
b = A.new('Peter')
arr = [a,b]
puts arr.uniq
Removes b from Array leaving only one object
Hope this helps!
The uniq method can take a block that defines what to compare the objects on. For example:
class Task
attr_accessor :n
def initialize(n)
#n = n
end
end
t1 = Task.new(1)
t2 = Task.new(2)
t3 = Task.new(2)
a = [t1, t2, t3]
a.uniq
#=> [t1, t2, t3] # because all 3 objects are unique
a.uniq { |t| t.n }
#=> [t1, t2] # as it's comparing on the value of n in the object
I tried the solution from fsaravia above and it didn't work out for me. I tried in both Ruby 2.3.1 and Ruby 2.4.0.
The solution I've found is very similar to what fsaravia posted though, with a small tweak. So here it is:
class A
attr_accessor :name
def initialize(name)
#name = name
end
def eql?(other)
hash.eql?(other.hash)
end
def hash
name.hash
end
end
a = A.new('Peter')
b = A.new('Peter')
arr = [a,b]
puts arr.uniq
Please, don't mind that I've removed the # in my example. It won't affect the solution per se. It's just that, IMO, there wasn't any reason to access the instance variable directly, given a reader method was set for that reason.
So...what I really changed is found inside the eql? method, where I used hash instead name. That's it!
If you look at the Array#| operator it says that it uses the eql?-method, which on Object is the same as the == method. You can define that by mixin in the Comparable-module, and then implement the <=>-method, then you'll get lots of comparison-methods for free.
The <=> operator is very easy to implement:
def <=>(obj)
return -1 if this < obj
return 0 if this == obj
return 1 if this > obj
end
Regarding your 'update', is this what you are doing:
a = Task.new # => #<Task:0x007f8d988f1b78>
b = Task.new # => #<Task:0x007f8d992ea300>
c = [a,b] # => [#<Task:0x007f8d988f1b78>, #<Task:0x007f8d992ea300>]
a = Task.new # => #<Task:0x007f8d992d3e48>
d = [a] # => [#<Task:0x007f8d992d3e48>]
e = c|d # => [#<Task:0x007f8d988f1b78>, #<Task:0x007f8d992ea300>, \
#<Task:0x007f8d992d3e48>]
and then suggesting that e = [a, b, a]? If so, that's the problem, because a no longer points to #<Task:0x007f8d988f1b78>. All you can say is e => [#<Task:0x007f8d988f1b78>, b, a]
I took the liberty to rewrite your class and add the methods that needs to be overwritten in order to use uniq (hash and eql?).
class Task
METHODS = [:status, :description, :priority, :tags]
attr_accessor *METHODS
def initialize task_line
#status, #description, #priority, #tags = *task_line.split("|")
#tags = #tags.split(",")
end
def eql? another_task
METHODS.all?{|m| self.send(m)==another_task.send(m)}
end
alias_method :==, :eql? #Strictly not needed for array.uniq
def hash
[#status, #description, #priority, #tags].hash
end
end
x = [Task.new('1|2|3|4'), Task.new('1|2|3|4')]
p x.size #=> 2
p x.uniq.size #=> 1
Let's say I have a Gift object with #name = "book" & #price = 15.95. What's the best way to convert that to the Hash {name: "book", price: 15.95} in Ruby, not Rails (although feel free to give the Rails answer too)?
Just say (current object) .attributes
.attributes returns a hash of any object. And it's much cleaner too.
class Gift
def initialize
#name = "book"
#price = 15.95
end
end
gift = Gift.new
hash = {}
gift.instance_variables.each {|var| hash[var.to_s.delete("#")] = gift.instance_variable_get(var) }
p hash # => {"name"=>"book", "price"=>15.95}
Alternatively with each_with_object:
gift = Gift.new
hash = gift.instance_variables.each_with_object({}) { |var, hash| hash[var.to_s.delete("#")] = gift.instance_variable_get(var) }
p hash # => {"name"=>"book", "price"=>15.95}
Implement #to_hash?
class Gift
def to_hash
hash = {}
instance_variables.each { |var| hash[var.to_s.delete('#')] = instance_variable_get(var) }
hash
end
end
h = Gift.new("Book", 19).to_hash
Gift.new.instance_values # => {"name"=>"book", "price"=>15.95}
You can use as_json method. It'll convert your object into hash.
But, that hash will come as a value to the name of that object as a key. In your case,
{'gift' => {'name' => 'book', 'price' => 15.95 }}
If you need a hash that's stored in the object use as_json(root: false). I think by default root will be false. For more info refer official ruby guide
http://api.rubyonrails.org/classes/ActiveModel/Serializers/JSON.html#method-i-as_json
For Active Record Objects
module ActiveRecordExtension
def to_hash
hash = {}; self.attributes.each { |k,v| hash[k] = v }
return hash
end
end
class Gift < ActiveRecord::Base
include ActiveRecordExtension
....
end
class Purchase < ActiveRecord::Base
include ActiveRecordExtension
....
end
and then just call
gift.to_hash()
purch.to_hash()
class Gift
def to_hash
instance_variables.map do |var|
[var[1..-1].to_sym, instance_variable_get(var)]
end.to_h
end
end
If you are not in an Rails environment (ie. don't have ActiveRecord available), this may be helpful:
JSON.parse( object.to_json )
You can write a very elegant solution using a functional style.
class Object
def hashify
Hash[instance_variables.map { |v| [v.to_s[1..-1].to_sym, instance_variable_get v] }]
end
end
Recursively convert your objects to hash using 'hashable' gem (https://rubygems.org/gems/hashable)
Example
class A
include Hashable
attr_accessor :blist
def initialize
#blist = [ B.new(1), { 'b' => B.new(2) } ]
end
end
class B
include Hashable
attr_accessor :id
def initialize(id); #id = id; end
end
a = A.new
a.to_dh # or a.to_deep_hash
# {:blist=>[{:id=>1}, {"b"=>{:id=>2}}]}
You should override the inspect method of your object to return the desired hash, or just implement a similar method without overriding the default object behaviour.
If you want to get fancier, you can iterate over an object's instance variables with object.instance_variables
Might want to try instance_values. That worked for me.
To plagiarize #Mr. L in a comment above, try #gift.attributes.to_options.
You can use symbolize_keys and in-case you have nested attributes we can use deep_symbolize_keys:
gift.as_json.symbolize_keys => {name: "book", price: 15.95}
Produces a shallow copy as a hash object of just the model attributes
my_hash_gift = gift.attributes.dup
Check the type of the resulting object
my_hash_gift.class
=> Hash
If you need nested objects to be converted as well.
# #fn to_hash obj {{{
# #brief Convert object to hash
#
# #return [Hash] Hash representing converted object
#
def to_hash obj
Hash[obj.instance_variables.map { |key|
variable = obj.instance_variable_get key
[key.to_s[1..-1].to_sym,
if variable.respond_to? <:some_method> then
hashify variable
else
variable
end
]
}]
end # }}}
Gift.new.attributes.symbolize_keys
To do this without Rails, a clean way is to store attributes on a constant.
class Gift
ATTRIBUTES = [:name, :price]
attr_accessor(*ATTRIBUTES)
end
And then, to convert an instance of Gift to a Hash, you can:
class Gift
...
def to_h
ATTRIBUTES.each_with_object({}) do |attribute_name, memo|
memo[attribute_name] = send(attribute_name)
end
end
end
This is a good way to do this because it will only include what you define on attr_accessor, and not every instance variable.
class Gift
ATTRIBUTES = [:name, :price]
attr_accessor(*ATTRIBUTES)
def create_random_instance_variable
#xyz = 123
end
def to_h
ATTRIBUTES.each_with_object({}) do |attribute_name, memo|
memo[attribute_name] = send(attribute_name)
end
end
end
g = Gift.new
g.name = "Foo"
g.price = 5.25
g.to_h
#=> {:name=>"Foo", :price=>5.25}
g.create_random_instance_variable
g.to_h
#=> {:name=>"Foo", :price=>5.25}
I started using structs to make easy to hash conversions.
Instead of using a bare struct I create my own class deriving from a hash this allows you to create your own functions and it documents the properties of a class.
require 'ostruct'
BaseGift = Struct.new(:name, :price)
class Gift < BaseGift
def initialize(name, price)
super(name, price)
end
# ... more user defined methods here.
end
g = Gift.new('pearls', 20)
g.to_h # returns: {:name=>"pearls", :price=>20}
Following Nate's answer which I haven't been able to compile:
Option 1
class Object
def to_hash
instance_variables.map{ |v| Hash[v.to_s.delete("#").to_sym, instance_variable_get(v)] }.inject(:merge)
end
end
And then you call it like that:
my_object.to_hash[:my_variable_name]
Option 2
class Object
def to_hash
instance_variables.map{ |v| Hash[v.to_s.delete("#"), instance_variable_get(v)] }.inject(:merge)
end
end
And then you call it like that:
my_object.to_hash["my_variable_name"]
Question:
Using Ruby it is simple to add custom methods to existing classes, but how do you add custom properties? Here is an example of what I am trying to do:
myarray = Array.new();
myarray.concat([1,2,3]);
myarray._meta_ = Hash.new(); # obviously, this wont work
myarray._meta_['createdby'] = 'dreftymac';
myarray._meta_['lastupdate'] = '1993-12-12';
## desired result
puts myarray._meta_['createdby']; #=> 'dreftymac'
puts myarray.inspect() #=> [1,2,3]
The goal is to construct the class definition in such a way that the stuff that does not work in the example above will work as expected.
Update: (clarify question) One aspect that was left out of the original question: it is also a goal to add "default values" that would ordinarily be set-up in the initialize method of the class.
Update: (why do this) Normally, it is very simple to just create a custom class that inherits from Array (or whatever built-in class you want to emulate). This question derives from some "testing-only" code and is not an attempt to ignore this generally acceptable approach.
Isn't a property just a getter and a setter? If so, couldn't you just do:
class Array
# Define the setter
def _meta_=(value)
#_meta_ = value
end
# Define the getter
def _meta_
#_meta_
end
end
Then, you can do:
x = Array.new
x._meta_
# => nil
x._meta_ = {:name => 'Bob'}
x._meta_
# => {:name => 'Bob'}
Does that help?
Recall that in Ruby, you do not have access to attributes (instance variables) outside of that instance. You only have access to an instance's public methods.
You can use attr_accessor to create a method for a class that acts as a property as you describe:
irb(main):001:0> class Array
irb(main):002:1> attr_accessor :_meta_
irb(main):003:1> end
=> nil
irb(main):004:0>
irb(main):005:0* x = [1,2,3]
=> [1, 2, 3]
irb(main):006:0> x._meta_ = Hash.new
=> {}
irb(main):007:0> x._meta_[:key] = 'value'
=> "value"
irb(main):008:0>
For a simple way to do a default initialization for an accessor, we'll need to basically reimplement attr_accessor ourselves:
class Class
def attr_accessor_with_default accessor, default_value
define_method(accessor) do
name = "##{accessor}"
instance_variable_set(name, default_value) unless instance_variable_defined?(name)
instance_variable_get(name)
end
define_method("#{accessor}=") do |val|
instance_variable_set("##{accessor}", val)
end
end
end
class Array
attr_accessor_with_default :_meta_, {}
end
x = [1,2,3]
x._meta_[:key] = 'value'
p x._meta_
y = [4,5,6]
y._meta_[:foo] = 'bar'
p y._meta_
But wait! The output is incorrect:
{:key=>"value"}
{:foo=>"bar", :key=>"value"}
We've created a closure around the default value of a literal hash.
A better way might be to simply use a block:
class Class
def attr_accessor_with_default accessor, &default_value_block
define_method(accessor) do
name = "##{accessor}"
instance_variable_set(name, default_value_block.call) unless instance_variable_defined?(name)
instance_variable_get(name)
end
define_method("#{accessor}=") do |val|
instance_variable_set("##{accessor}", val)
end
end
end
class Array
attr_accessor_with_default :_meta_ do Hash.new end
end
x = [1,2,3]
x._meta_[:key] = 'value'
p x._meta_
y = [4,5,6]
y._meta_[:foo] = 'bar'
p y._meta_
Now the output is correct because Hash.new is called every time the default value is retrieved, as opposed to reusing the same literal hash every time.
{:key=>"value"}
{:foo=>"bar"}