Related
TLDR I am working on a project that creates instances using an API. I want to be able to recall all of my instances but can't figure out how. I'm relatively new to Ruby and programming in general so I hope I'm explaining everything well enough. Here's my code.
class Suggestion
attr_accessor :type, :participants
attr_reader :activity, :price, :link, :key, :accessibility
##all = []
def initialize(type, participants)
#type = type
#participants = participants
# #activity = []
# #price = price
# #key = key
# #accessibility = accessibility
##all << self
end
# def save
# ##all << self
# end
def self.all
##all
end
# def events
# ##all.map
# end
def list_events
# binding.pry
Suggestion.all.map #{|event| [:activity, :type, :participants, :price, :link, :key, :accessibility]}
end
end
any and all help would be greatly appreciated
What Already Works
I may be misunderstanding your issue, but it seems like your code mostly works as-is. I refactored it a little to make it a little easier to see what's going on, but your basic approach of self-registration with a class variable during object initialization seems sound. Consider:
class Suggestion
##all = []
def initialize(type, participants)
#type = type
#participants = participants
##all << self
end
def self.all
##all
end
end
s1 = Suggestion.new :foo, %w[Alice Bob]
#=> #<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>
s2 = Suggestion.new :bar, %w[Charlie Dana]
#=> #<Suggestion:0x00007faed7113900 #participants=:bar, #type=:foo>
Suggestion.all
#=>
[#<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>,
#<Suggestion:0x00007f9671089058
#participants=["Charlie", "Dana"],
#type=:bar>]
What Might Need Improvement
You probably just need class or instance methods that "do a thing" with each instance (or selected elements from specific instances) stored in ##all. For example:
# print the object ID of each instance of Suggestion
Suggestion.all.map { |suggestion| suggestion.object_id }
#=> [240, 260]
You might also want to use Array#select to extract specific instances that meet defined criteria, or Array#map to do something with your matching instances. For example:
# re-open the class to add some getter methods
class Suggestion
attr_reader :type, :participants
end
# only operate on instances of a given type
Suggestion.all.select { |s| s.type == :foo }
#=> [#<Suggestion:0x00007f9671154578 #participants=["Alice", "Bob"], #type=:foo>]
# invoke Suggestion#participants on matching instances
Suggestion.all.map { |s| s.participants if s.type == :bar }.compact
#=> [["Charlie", "Dana"]]
As long as you have a collection through which you can iterate (e.g. your class' ##all Array) you can filter, map, or otherwise operate on some or all of your instances at need. There are certainly more complex approaches, but I'd strive to keep it as simple as possible.
Consider the following class definition, where I've made #all a class instance variable, rather than a class variable. Use of the former is generally preferred over use of the latter.
class Suggestion
attr_accessor :type, :participants
#all = []
class << self
attr_accessor :all
end
def initialize(type, participants)
#type = type
#participants = participants
self.class.all << self
end
def list_events
self.class.all.map do |instance|
{ type: instance.type, participants: instance.participants }
end
end
def self.list_events
all.map do |instance|
{ type: instance.type, participants: instance.participants }
end
end
end
The snippet
class << self
attr_accessor :all
end
creates a read-write accessor for #all. class << self causes self to be changed to Suggestion's singleton class. If desired you could replace this with
superclass.public_send(:attr_accessor, :all)
Let's try it.
Suggestion.all
#=> []
s1 = Suggestion.new(1, ['Bob', 'Sally'])
#=> #<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>]
s2 = Suggestion.new(2, ['Ronda', 'Cliff'])
#<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>,
# #<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>]
s3 = Suggestion.new(3, ['Weasel', 'Isobel'])
#=> #<Suggestion:0x00007fc677077598 #type=3, #participants=["Weasel", "Isobel"]>
Suggestion.all
#=> [#<Suggestion:0x00007fc677084888 #type=1, #participants=["Bob", "Sally"]>,
# #<Suggestion:0x00007fc677b577b0 #type=2, #participants=["Ronda", "Cliff"]>,
# #<Suggestion:0x00007fc677077598 #type=3, #participants=["Weasel", "Isobel"]>]
s1.list_events
#=> [{:type=>1, :participants=>["Bob", "Sally"]},
# {:type=>2, :participants=>["Ronda", "Cliff"]},
# {:type=>3, :participants=>["Weasel", "Isobel"]}]
Suggestion.list_events
#=> [{:type=>1, :participants=>["Bob", "Sally"]},
# {:type=>2, :participants=>["Ronda", "Cliff"]},
# {:type=>3, :participants=>["Weasel", "Isobel"]}]
I included Suggestion#instance to show how an instance can access (read or write) the class instance variable #all.
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 want to be able to have a virtual attribute on a non-database model that is a hash. I just can't figure out what the syntax is for adding and removing items from this hash:
If I define:
attr_accessor :foo, :bar
then in a method in the model, I can use:
self.foo = "x"
But I can't say:
self.bar["item"] = "value"
Try
self.bar = Hash.new
self.bar["item"] = "value"
class YourModel
def bar
#bar ||= Hash.new
end
def foo
bar["item"] = "value"
end
end
but classic approach would be:
class YourModel
def initialize
#bar = Hash.new
end
def foo
#bar["item"] = "value"
end
end
Just use OpenStruct, Hash with Indifferent Access or Active Model.
When you are calling:
attr_accessor :foo, :bar
on your class, Ruby does something like the following behind the curtains:
def foo
return #foo
end
def foo=(val)
#foo = val
end
def bar
return #bar
end
def bar=(val)
#bar = val
end
The methods #foo and #bar are just returning the instance variables and #foo= and #bar= just set them. So if you want one of them to contain a Hash, you have to assign this Hash somewhere.
My favorite solution would be the following:
class YourModel
# generate the default accessor methods
attr_accessor :foo, :bar
# overwrite #bar so that it always returns a hash
def bar
#bar ||= {}
end
end
Please help me get all instance variables declared in a class the same way instance_methods shows me all methods available in a class.
class A
attr_accessor :ab, :ac
end
puts A.instance_methods #gives ab and ac
puts A.something #gives me #ab #ac...
You can use instance_variables:
A.instance_variables
but that’s probably not what you want, since that gets the instance variables in the class A, not an instance of that class. So you probably want:
a = A.new
a.instance_variables
But note that just calling attr_accessor doesn’t define any instance variables (it just defines methods), so there won’t be any in the instance until you set them explicitly.
a = A.new
a.instance_variables #=> []
a.ab = 'foo'
a.instance_variables #=> [:#ab]
If you want to get all instances variables values you can try something like this :
class A
attr_accessor :foo, :bar
def context
self.instance_variables.map do |attribute|
{ attribute => self.instance_variable_get(attribute) }
end
end
end
a = A.new
a.foo = "foo"
a.bar = 42
a.context #=> [{ :#foo => "foo" }, { :#bar => 42 }]
It's not foolproof - additional methods could be defined on the class that match the pattern - but one way I found that has suited my needs is
A.instance_methods.grep(/[a-z_]+=/).map{ |m| m.to_s.gsub(/^(.+)=$/, '#\1') }
If you want to get a hash of all instance variables, in the manner of attributes, following on from Aschen's answer you can do
class A
attr_accessor :foo, :bar
def attributes
self.instance_variables.map do |attribute|
key = attribute.to_s.gsub('#','')
[key, self.instance_variable_get(attribute)]
end.to_h
end
end
a = A.new
a.foo = "foo"
a.bar = 42
a.context #=> {'foo' => 'foo', 'bar' => 42}
Building on the answer from #Obromios , I added .to_h and .to_s to a class to allow for pleasant, flexible dumping of attributes suitable for display to an end user.
This particular class (not an ActiveRecord model) will have a variety of attributes set in different situations. Only those attribs that have values will appear when printing myvar.to_s, which was my desire.
class LocalError
attr_accessor :product_code, :event_description, :error_code, :error_column, :error_row
def to_h
instance_variables.map do |attribute|
key = attribute.to_s.gsub('#', '')
[key, self.instance_variable_get(attribute)]
end.to_h
end
def to_s
to_h.to_s
end
end
This allows me to put this simple code in a mailer template:
Data error: <%= #data_error %>
And it produces (for example):
Data error: {"event_description"=>"invalid date", "error_row"=>13}
This is nice, as the mailer doesn't have to be updated as the LocalError attributes change in the future.
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"]