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.
Related
I'm working to create a few Ruby builder objects, and thinking on how I could reuse some of Ruby's magic to reduce the logic of the builder to a single class/module. It's been ~10 years since my last dance with the language, so a bit rusty.
For example, I have this builder:
class Person
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
def build
output = {}
PROPERTIES.each do |prop|
if self.respond_to?(prop) and !self.send(prop).nil?
value = self.send(prop)
# if value itself is a builder, evalute it
output[prop] = value.respond_to?(:build) ? value.build : value
end
end
output
end
def method_missing(m, *args, &block)
if m.to_s.start_with?("set_")
mm = m.to_s.gsub("set_", "")
if PROPERTIES.include?(mm.to_sym)
self.send("#{mm}=", *args)
return self
end
end
end
end
Which can be used like so:
Person.new(name: "Joe").set_age(30).build
# => {name: "Joe", age: 30}
I would like to be able to refactor everything to a class and/or module so that I could create multiple such builders that'll only need to define attributes and inherit or include the rest (and possibly extend each other).
class BuilderBase
# define all/most relevant methods here for initialization,
# builder attributes and object construction
end
module BuilderHelper
# possibly throw some of the methods here for better scope access
end
class Person < BuilderBase
include BuilderHelper
PROPERTIES = [:name, :age, :email, :address]
attr_accessor(*PROPERTIES)
end
# Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").build
class Server < BuilderBase
include BuilderHelper
PROPERTIES = [:cpu, :memory, :disk_space]
attr_accessor(*PROPERTIES)
end
# Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").build
I've been able to get this far:
class BuilderBase
def initialize(**kwargs)
kwargs.each do |k, v|
self.send("#{k}=", v) if self.respond_to?(k)
end
end
end
class Person < BuilderBase
PROPERTIES = [:name, :age]
attr_accessor(*PROPERTIES)
def build
...
end
def method_missing(m, *args, &block)
...
end
end
Trying to extract method_missing and build into the base class or a module keeps throwing an error at me saying something like:
NameError: uninitialized constant BuilderHelper::PROPERTIES
OR
NameError: uninitialized constant BuilderBase::PROPERTIES
Essentially the neither the parent class nor the mixin are able to access the child class' attributes. For the parent this makes sense, but not sure why the mixin can't read the values inside the class it was included into. This being Ruby I'm sure there's some magical way to do this that I have missed.
Help appreciated - thanks!
I reduced your sample to the required parts and came up with:
module Mixin
def say_mixin
puts "Mixin: Value defined in #{self.class::VALUE}"
end
end
class Parent
def say_parent
puts "Parent: Value defined in #{self.class::VALUE}"
end
end
class Child < Parent
include Mixin
VALUE = "CHILD"
end
child = Child.new
child.say_mixin
child.say_parent
This is how you could access a CONSTANT that lives in the child/including class from the parent/included class.
But I don't see why you want to have this whole Builder thing in the first place. Would an OpenStruct not work for your case?
Interesting question. As mentioned by #Pascal, an OpenStruct might already do what you're looking for.
Still, it might be more concise to explicitly define the setter methods. It might also be clearer to replace the PROPERTIES constants by methods calls. And since I'd expect a build method to return a complete object and not just a Hash, I renamed it to to_h:
class BuilderBase
def self.properties(*ps)
ps.each do |property|
attr_reader property
define_method :"set_#{property}" do |value|
instance_variable_set(:"##{property}", value)
#hash[property] = value
self
end
end
end
def initialize(**kwargs)
#hash = {}
kwargs.each do |k, v|
self.send("set_#{k}", v) if self.respond_to?(k)
end
end
def to_h
#hash
end
end
class Person < BuilderBase
properties :name, :age, :email, :address
end
p Person.new(name: "Joe").set_age(30).set_email("joe#mail.com").set_address("NYC").to_h
# {:name=>"Joe", :age=>30, :email=>"joe#mail.com", :address=>"NYC"}
class Server < BuilderBase
properties :cpu, :memory, :disk_space
end
p Server.new.set_cpu("i9").set_memory("32GB").set_disk_space("1TB").to_h
# {:cpu=>"i9", :memory=>"32GB", :disk_space=>"1TB"}
I think no need to declare PROPERTIES, we can create a general builder like this:
class Builder
attr_reader :build
def initialize(clazz)
#build = clazz.new
end
def self.build(clazz, &block)
builder = Builder.new(clazz)
builder.instance_eval(&block)
builder.build
end
def set(attr, val)
#build.send("#{attr}=", val)
self
end
def method_missing(m, *args, &block)
if #build.respond_to?("#{m}=")
set(m, *args)
else
#build.send("#{m}", *args, &block)
end
self
end
def respond_to_missing?(method_name, include_private = false)
#build.respond_to?(method_name) || super
end
end
Using
class Test
attr_accessor :x, :y, :z
attr_reader :w, :u, :v
def set_w(val)
#w = val&.even? ? val : 0
end
def add_u(val)
#u = val if val&.odd?
end
end
test1 = Builder.build(Test) {
x 1
y 2
z 3
} # <Test:0x000055b6b0fb2888 #x=1, #y=2, #z=3>
test2 = Builder.new(Test).set(:x, 1988).set_w(6).add_u(2).build
# <Test:0x000055b6b0fb23b0 #x=1988, #w=6>
I'm attempting to adapt the method-chaining example cited in this posting (Method chaining and lazy evaluation in Ruby) to work with an object that implements the Enumerable class (Implement a custom Enumerable collection class in Ruby )
Coffee class:
class Coffee
attr_accessor :name
attr_accessor :strength
def initialize(name, strength)
#name = name
#strength = strength
end
def <=>(other_coffee)
self.strength <=> other_coffee.strength
end
def to_s
"<name: #{name}, strength: #{strength}>"
end
end
Criteria class:
class Criteria
def initialize(klass)
#klass = klass
end
def criteria
#criteria ||= {:conditions => {}}
end
# only show coffee w/ this strength
def strength(strength)
criteria[:strength] = strength
self
end
# if there are multiple coffees, choose the first n=limit
def limit(limit)
criteria[:limit] = limit
self
end
# allow collection enumeration
def each(&block)
#klass.collection.select { |c| c[:strength] == criteria[:strength] }.each(&block)
end
end
CoffeeShop class:
class CoffeeShop
include Enumerable
def self.collection
#collection=[]
#collection << Coffee.new("Laos", 10)
#collection << Coffee.new("Angkor", 7)
#collection << Coffee.new("Nescafe", 1)
end
def self.limit(*args)
Criteria.new(self).limit(*args)
end
def self.strength(*args)
Criteria.new(self).strength(*args)
end
end
When I run this code:
CoffeeShop.strength(10).each { |c| puts c.inspect }
I get an error:
criteria.rb:32:in block in each': undefined method '[]' for #<Coffee:0x007fd25c8ec520 #name="Laos", #strength=10>
I'm certain that I haven't defined the Criteria.each method correctly, but I'm not sure how to correct it. How do I correct this?
Moreover, the each method doesn't support the limit as currently written. Is there a better way to filter the array such that it is easier to support both the strength and limit?
Other coding suggestions are appreciated.
Your Coffee class defines method accessors for name and strength. For a single coffee object, you can thus get the attributes with
coffee.name
# => "Laos"
coffee.strength
# => 10
In your Criteria#each method, you try to access the attributes using the subscript operator, i.e. c[:strength] (with c being an Instance of Coffee in this case). Now, on your Coffee class, you have not implemented the subscript accessor which resulting in the NoMethodError you see there.
You could thus either adapt your Criteria#each method as follows:
def each(&block)
#klass.collection.select { |c| c.strength == criteria[:strength] }.each(&block)
end
or you could implement the subscript operators on your Coffee class:
class Coffee
attr_accessor :name
attr_accessor :strength
# ...
def [](key)
public_send(key)
end
def []=(key, value)
public_send(:"#{key}=", value)
end
end
Noe, as an addendum, you might want to extend your each method in any case. A common (and often implicitly expected) pattern is that methods like each return an Enumerator if no block was given. This allows patterns like CoffeeShop.strength(10).each.group_by(&:strength).
You can implement this b a simple on-liner in your method:
def each(&block)
return enum_for(__method__) unless block_given?
#klass.collection.select { |c| c.strength == criteria[:strength] }.each(&block)
end
I have a class that was built for subclassing.
class A
def initialize(name)
end
def some
# to define in subclass
end
end
# usage
p A.new('foo').some
#=> nil
In my use case, I don't want to create a subclass since I need just one instance. Therefore, I'll change the initialize method to support the following usage.
p A.new('foo') { 'YEAH' }.some
#=> YEAH
How could I support the usage above?
BTW: I found the following solutions for a Ruby 1.8.7 project, but they look awkward to me.
class A
def singleton_class
class << self; self; end
end
def initialize(name, &block)
#name = name
self.singleton_class.send(:define_method, :some) { block.call } if block_given?
end
def some
# to define in subclass
end
end
You can store the block argument in an instance variable and call it later on:
class A
def initialize(name, &block)
#name = name
#block = block
end
def some
#block.call
end
end
A.new('foo') { 'YEAH' }.some
#=> "YEAH"
You can also pass arguments into the block:
class A
# ...
def some
#block.call(#name)
end
end
A.new('foo') { |s| s.upcase }.some
#=> "FOO"
Or instance_exec the block in the context of the receiver:
class A
# ...
def some
instance_exec(&#block)
end
end
Which allows you to bypass encapsulation:
A.new('foo') { #name.upcase }.some
#=> "FOO"
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 several classes and I want each one to maintain on the class level a hash of all the instances that have been created for future lookup. Something akin to:
class A
def initialize(id, otherstuff)
# object creation logic
##my_hash[id]=self
end
def self.find(id)
##my_hash[id]
end
end
so I can then A.find(id) and get the right instance back.
There are several of these classes (A, B, etc), all having ids, all of which I want to have this functionality.
Can I have them all inherit from a superclass which has a generic version of this which they can leverage so I don't have to reimplement many things for every class?
Yes, you can either inherit from the same superclass, or use modules and include:
module M
def initialize(id)
##all ||= {}
##all[id] = self
end
def print
p ##all
end
end
class C
include M
def initialize(id)
super
puts "C instantiated"
end
end
If you want to keep separate indexes for each subclass, you can do something like:
def initialize(id)
##all ||= {}
##all[self.class] ||= {}
##all[self.class][id] = self
end
Edit: After your comment, I see that you need to keep per-class indexes. So:
class A
def initialize(id)
self.class.index(id, self)
end
def self.index id, instance
#all ||= {}
#all[id] = instance
end
def self.find(id)
#all[id]
end
end
class B < A
end
class C < A
end
a = A.new(1)
b = B.new(2)
c = C.new(3)
p A.find(1)
#=> #<A:0x10016c190>
p B.find(2)
#=> #<B:0x10016c140>
p C.find(3)
#=> #<C:0x10016c118>
p A.find(2)
#=> nil