How to instantiate an instance of a Ruby class when the class changes dynamically? - ruby

So I am trying to create a text game in ruby, and I have attempted to create a fight method that can deal with creating any object. I have a Monsters class in another file, and child classes such as Rogue and Vampire. I have managed to make this work by using a case statement that instantiates an object named m that is either Rogue or Vampire, and putting practically all the methods in the Monsters class so that they share the same method name, but is there a more efficient way of dealing with unknown objects?
My code:
def fight(monsterToFight)
case monsterToFight
when "Rogue"
m = ::Rogue.new
when "Vampire"
m = ::Vampire.new
else
puts "error 503"
end
... #more code
link to full repo: https://github.com/chaseWilliams/textGame

You can use const_get.
class_name = "Rogue"
rogue_class = Object.const_get(class_name) # => Rogue
my_rogue = rogue_class.new # => #<Rogue ...>
In your example, this would look like:
def fight(name_of_monster_to_fight)
monster_class = Object.const_get(name_of_monster_to_fight)
m = monster_class.new
# ... more code
rescue NameError # This is the error thrown when the class doesn't exist
puts "error 503"
end

Related

Ruby iterating in a class method while referencing another class

Solved. I didn't have the initialization right, which gave the noMethodError. Then I was changing an array, but checking a variable that referred to a position in the array, and that variable had not been reassigned.
Edited to initialize bookPagesInfo, bookChaptersInfo, editPagesInfo, and editChapterInfo as suggested. Still gives the same NoMethod Error.
I have a book with page and chapter info, and want to be able to apply edits that change the number of pages, introPages, chapters, and povs.
class Book
attr_accessor :pages, :chapters, :bookPagesInfo, :bookChaptersInfo, :introPages, :povs
def initialize(bookPagesInfo, bookChaptersInfo)
#bookPagesInfo = bookPagesInfo
#bookChaptersInfo = bookChaptersInfo
#pages = bookPagesInfo[0]
#introPages = bookPagesInfo[1]
#chapters = bookChaptersInfo[0]
#povs = bookChaptersInfo[1]
end
def applyEdit(edit)
#pages += edit.new_pages
end
end
class Edit
attr_accessor :new_pages, :new_chapters, :editPagesInfo, :editChaptersInfo, :new_intro_pages, :new_povs
def initialize(editPagesInfo, editChaptersInfo)
#editPagesInfo = editPagesInfo
#editChaptersInfo = editChaptersInfo
#new_pages = editPagesInfo[0]
#new_intro_pages = editPagesInfo[1]
#new_chapters = editChaptersInfo[0]
#new_povs = editChaptersInfo[1]
end
end
The above code works for editing just number of pages. However, if I change my applyEdit method to iterate over the bookPagesInfo array, I can't get it to work. Running applyEdit below gives a nonfatal error.
def applyEdit(edit)
#bookPagesInfo.each_with_index do {|stat, idx| stat += edit.bookPagesInfo[idx]}
end
## gives undefined method `each_with_index' for nil:NilClass (NoMethodError), but
## my understanding is as long as bookPagesInfo was initialized as an array, it
## should be an array, not nilClass
I'm pretty new to classes (and this website, sorry for formatting). Thanks for the help.
You've got attr_accessors defined for :bookPagesInfo and :bookChaptersInfo, which will give you reader and writer methods, but it won't set #bookPagesInfo and #bookChaptersInfo for you in the initialize method - you need to do that yourself. So, when you try to read from the instance variable in applyEdit, you're reading nil.
Try adding
#bookPagesInfo = bookPagesInfo
#bookChaptersInfo = bookChaptersInfo
in Book#initialize.

How do I dynamically constantize the name of a namespaced class?

Information on what's going on here in ruby: http://coderrr.wordpress.com/2008/03/11/constant-name-resolution-in-ruby/
Doesn't help me solve my problem.. but it at least explains they 'why'
I've written the following method:
# delegate to a user permission serializer specific to the given object
# - if a serializer is not found for the given object, check the superclass
#
# #raise [NameError] if none of object, or it's superclasses have a known
# user permission serializer
# #return [UserPermission::*Serializer] returns serialized object
def self.serialized_for(object, user, klass: nil, recursion_limit: 5)
object_class = klass ? klass : object.class
# use demodulize to chop off the namespace and get the generic object name
object_name = object_class.name.demodulize
# bulid serializer name
name = "::UserPermission::#{object_name}Serializer"
begin
binding.pry
permissions = object.user_permissions(user)
return name.constantize.new(permissions)
rescue NameError => e
raise e if recursion_limit < 1
# try with super class
UserPermission.serialized_for(
object,
user,
klass: object_class.superclass,
recursion_limit: recursion_limit - 1
)
end
end
The goal is to be able to retrieve the serializer of any subclass, provided the subclass has a superclass with a serializer already defined. (I'm using ActiveModelSerializers, but that's not important here).
My problem is that I'm receiving a non-namespaced class when name.constantize runs.
My existing classes:
UserPermission
UserPermission::ProposalSerializer
PresentationSerializer < ActiveModel::Serializer
Presentation < Proposal
Proposal < ActiveRecord::Base
What I'm expecting to happen, is that when I call UserPermission.serialized_for with a Presentation, that name.constantize tries to give me a ::UserPermission::PresentationSerializer and then throw a NameError because the class doesn't exist.
What I'm getting instead is ::PresentationSerializer, which is no good - used for a different purpose.
Here is what I came up with for replicating the issue in irb:
(maybe the above context is an overly complicated explanation of this):
class NameSpace; end
class NameSpace::Klass; end
class Klass; end
class SubKlass < Klass; end
Object.const_get "::NameSpace::SubKlass"
=> SubKlass
Object.const_get("::NameSpace").const_get("SubKlass")
=> SubKlass
eval("NameSpace::SubKlass")
(eval):1: warning: toplevel constant SubKlass referenced by NameSpace::SubKlass
=> SubKlass
Is there a way I can constantize "::NameSpace::SubKlass" such that I get a NameError due to NameSpace::SubKlass not existing?
P.S.: I hope the context helps.
Edit: found another problem:
UserPermission::Template < UserPermission::Proposal
UserPermission::Template.superclass
=> Proposal
should be UserPermission::Proposal
UserPermission::Proposal
(pry):9: warning: toplevel constant Proposal referenced by UserPermission::Proposal
=> Proposal
UserPermission::Proposal is a class. So... this is a big problem. o.o
I'm using Ruby 2.1.0
Do not define your classes and modules the short-hand way. You run into scoping issues.
module UserPermission
class Proposal
end
end
module UserPermission
class Template < Proposal
end
end
UserPermission::Template.superclass
# => UserPermission::Proposal

Issue loading classes order EDIT: works, although some odd behavior along the way

I'm working on a project to recreate some of the functionality of ActiveRecord. Here's the portion that isn't working
module Associations
def belongs_to(name, params)
self.class.send(:define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
self.class.send(:define_method, :other_table_name) do |other_class|
other_class.table_name
end
.
.
.
o_c = other_class(name, params)
#puts this and other (working) values in a query
query = <<-SQL
...
SQL
#sends it off with db.execute(query)...
I'm building towards this testing file:
require 'all_files' #holds SQLClass & others
pets_db_file_name = File.expand_path(File.join(File.dirname(__FILE__), "pets.db"))
DBConnection.open(pets_db_file_name)
#class Person
#end
class Pet < SQLClass
set_table_name("pets")
set_attrs(:id, :name, :owner_id)
belongs_to :person, :class_name => "Person", :primary_key => :id, :foreign_key => :owner_id
end
class Person < SQLClass
set_table_name("people")
set_attrs(:id, :name)
has_many :pets, :foreign_key => :owner_id
end
.
.
.
Without any changes I received
.../active_support/inflector/methods.rb:230:in `block in constantize': uninitialized constant Person (NameError)
Just to make sure that it was an issue with the order of loading the classes in the file I began the file with the empty Person class, which, as predicted gave me
undefined method `table_name' for Person:Class (NoMethodError)
Since this is a learning project I don't want to change the test to make my code work (open all the classes, set all the tables/attributes then reopen them them for belongs_to. But, I'm stuck on how else to proceed.)
EDIT SQLClass:
class SQLClass < AssignmentClass
extend SearchMod
extend Associations
def self.set_table_name(table_name)
#table_name = table_name
end
def self.table_name
#table_name
end
#some more methods for finding rows, and creating new rows in existing tables
And the relevant part of AssignmentClass uses send on attr_accessor to give functionality to set_attrs and makes sure that before you initialize a new instance of a class all the names match what was set using set_attrs.
This highlights an important difference between dynamic, interpreted Ruby (et al) and static, compiled languages like Java/C#/C++. In Java, the compiler runs over all your source files, finds all the class/method definitions, and matches them up with usages. Ruby doesn't work like this -- a class "comes into existence" after executing its class block. Before that, the Ruby interpreter doesn't know anything about it.
In your test file, you define Pet first. Within the definition of Pet, you have belongs_to :person. belongs_to does :person.constantize, attempting to get the class object for Person. But Person doesn't exist yet! Its definition comes later in the test file.
There are a couple ways I can think that you could try to resolve this:
One would be to do what Rails does: define each class in its own file, and make the file names conform to some convention. Override constant_missing, and make it automatically load the file which defines the missing class. This will make load order problems resolve themselves automatically.
Another solution would be to make belongs_to lazy. Rather than looking up the Person class object immediately, it could just record the fact that there is an association between Pet and Person. When someone tries to call pet.person, use a missing_method hook to actually define the method. (Presumably, by that time all the class definitions will have been executed.)
Another way would be do something like:
define_method(belongs_to) do
belongs_to_class = belongs_to.constantize
self.class.send(:define_method, belongs_to) do
# put actual definition here
end
self.send(belongs_to)
end
This code is not tested, it's just to give you an idea! Though it's a pretty mind-bending idea, perhaps. Basically, you define a method which redefines itself the first time it is called. Just like using method_missing, this allows you to delay the class lookup until the first time the method is actually used.
If I can say one more thing: though you say you don't want to "overload" method_missing, I don't think that's as much of a problem as you think. It's just a matter of extracting code into helper methods to keep the definition of method_missing manageable. Maybe something like:
def method_missing(name,*a,&b)
if has_belongs_to_association?(name)
invoke_belongs_to_association(name,a,b)
elsif has_has_many_association?(name)
invoke_has_many_association(name,a,b)
# more...
else
super
end
end
Progress! Inspired by Alex D's suggestion to use method_missing to delay the creation I instead used define_methodto create a method for the name, like so:
define_method, :other_class) do |name, params|
(params[:class_name] || name.camelize).constantize
end
define_method(:other_table_name) do |other_class|
other_class.table_name
end
#etc
define_method(name) do #|params| turns out I didn't need to pass in `params` at all but:
#p "---#{params} (This is line 31: when testing this out I got the strangest error
#.rb:31:in `block in belongs_to': wrong number of arguments (0 for 1) (ArgumentError)
#if anyone can explain this I would be grateful.
#I had declared an #params class instance variable and a getter for it,
#but nothing that should make params require an argument
f_k = foreign_key(name, params)
p f_k
o_c = other_class(name, params)
o_t_n = other_table_name(o_c)
p_k = primary_key(params)
query = <<-SQL
SELECT *
FROM #{o_t_n}
WHERE #{p_k} = ?
SQL
row = DBConnection.execute(query, self.send(f_k))
o_c.parse_all(row)
end

ruby, no method error

I am receiving the following error when running my below ruby script:
s3parse.rb:12:in `block in <class:AccountLog>': undefined method `extract_account_id' for AccountLog:Class (NoMethodError)
I dont think it should be a class method, is there a reason its not taking my method into account?
class AccountLog
attr_accessor :bytes, :account_id, :date
def extract_account_id(line)
line.match(%r{accounts/(\d+)}).captures.join.to_i
end
s3log = File.open('vidcoder.txt').each do |line|
account_log = AccountLog.new
account_log.date = line.match(%r{\[[^:]*}).to_s.delete"[" #need to finish this regex to make it work
account_log.account_id = extract_account_id(line)
account_log.bytes = line.match(%r{^.*\s+HTTP.*\s+-\s+(\d+)\s+}).captures.join.to_i
puts "\n"
puts "The api request on #{account_log.date} was fromm account number #{account_log.account_id} and the bytes were #{account_log.bytes}"
end
end
def extract_account_id will define an instance method.
In the way you call it, you need a class method instead.
Define it like this:
def self.extract_account_id(line)
or, as you already have an AccountLog instance, use it to call extract_account_id:
account_log.account_id = account_log.extract_account_id(line)
Please note that with second way you do not need to alter method definition, just call extract_account_id via account_log instance.
And i guess you would want to put s3log = File... outside class definition.
Or use a constant instead: S3log = ...
Then you'll can access it as AccountLog::S3log
Is there any reason you don't think it should be a class method? You are using it in the context of a class method and that's why it it's saying no such method for class AccountLog.
If you name your method as self.extract_account_id(line) I'm sure it will work.
From what you are trying to do I think this is what you are looking for?
class AccountLog
attr_accessor :bytes, :account_id, :date
def self.extract_account_id(line)
line.match(%r{accounts/(\d+)}).captures.join.to_i
end
end
s3log = File.open('vidcoder.txt').each do |line|
account_log = AccountLog.new
account_log.date = line.match(%r{\[[^:]*}).to_s.delete"[" #need to finish this regex to make it work
account_log.account_id = extract_account_id(line)
account_log.bytes = line.match(%r{^.*\s+HTTP.*\s+-\s+(\d+)\s+}).captures.join.to_i
puts "\n"
puts "The api request on #{account_log.date} was fromm account number #{account_log.account_id} and the bytes were #{account_log.bytes}"
end
While you could take the class method approach, there seems to be a little more going on.
You should put the extraction logic in a method in itself rather than let it hangout in your class. Then outside of the class, have an instance of AccountLog where you can call on the methods for log and account id extraction. At that point you can do something with those values.
Class method or not is a detail we can explore after the class is a bit more clean I think.

Marshaling and undefined attributes/classes

I am using ruby marshaling to send data between two clients. Each client has a set of class definitions which they will use to help load the marshaled data. The definitions are stored in an external ruby file which they can load anytime they want (but usually when they start up)
A simple use case would be
Client A marshal dumps the data and sends it to client B
Client B marshal loads the data and then writes it out to a file
However, sometimes one client is sending data that contains objects that isn't defined in the other client's definitions, in which case the other client should update its definitions accordingly.
It might be a new instance variable that should be added to the definition of class xyz or it might be a new class completely.
Marshal#Load currently just throws an exception when it runs into an undefined variable (eg: undefined class/method abc).
Is there a way for me to take this exception and update the definitions accordingly so that the client can happily read the data and write it out?
All classes will contain data that Marshal already knows how to encode/decode, such as strings, arrays, hashes, numbers, etc. There won't be any data that requires custom dump/load methods.
My solution would be to automatically create the class (and constant hierarchy, i.e. Foo::Bar::Baz) and make the class autorespond to attribute access attempts.
class AutoObject
def method_missing(*args,&b)
if args.size == 1
name = args[0]
if instance_variable_defined? "##{name}"
self.class.send :attr_accessor, name
send(*args)
else
super
end
elsif args.size == 2 && args[0].to_s[/=$/]
name = args[0].to_s[0...-1]
if instance_variable_defined? "##{name}"
self.class.send :attr_accessor, name
send(*args)
else
super
end
end
end
end
def Marshal.auto_load(data)
Marshal.load(data)
rescue ArgumentError => e
classname = e.message[%r(^undefined class/module (.+)$), 1]
raise e unless classname
classname.split("::").inject(Object) do |outer, inner|
if !outer.const_defined? inner
outer.const_set inner, Class.new(AutoObject)
else
outer.const_get inner
end
end
retry
end
This could easily be extended to log all classes created, and even to determine what instance variables they might have. Which could then aid you in updating the files, perhaps programatically.

Resources