I'm using the Etsy gem, which as a Listing module.
I also have a corresponding Listing model in my app.
I'm trying to set up a sidekiq worker to work with the gem, call upon some methods and update the corresponding Listing row, but because I have include Etsy so that I can use the gem, rails gets confused and thinks I'm referring to the module instead of the model.
Here's the code:
class ListingWorker
include Sidekiq::Worker
include Etsy
def perform(seller, shop)
access_token = {access_token: seller.oauth_token,
access_secret: seller.request_secret}
myself = Etsy.myself(access_token[:access_token], access_token[:access_secret])
limit = 100
offset = 0
total_results = myself.shop.active_listings_count
until offset > total_results
listings = Etsy::Request.get("/shops/#{shop.shop_id}/listings/active",
access_token.merge(limit: limit.to_s,
offset: offset.to_s,
include_private: 'true',
includes: 'Images:1:0'))
.to_hash
offset += limit
listings['results'].each do |l|
listing = Listing.find_by(listing_id: l['listing_id'])
end
end
Here's the error I get:
undefined method `find_by' for Etsy::Listing:Class
How do I differentiate between the module and the model and make rails understand I mean a db table?
Thanks in advance!
The short answer to your problem is simply not to include Esty:
class ListingWorker
include Sidekiq::Worker
def perform(seller, shop)
# ...
end
end
The purpose of include is to add methods defined with the module into your class.
For example, if I defined a module:
module Tom
def hello
puts "Hello!"
end
end
And you wanted to call hello directly within your ListingWorker class, then you'd need to include Tom.
On the other hand, suppose I just define a "namespaced" method within a module, such as:
module Maayan
def self.example
puts "Example"
end
end
Then, this should be invoked by referencing the module - i.e. Maayan.example.
You can do this from anywhere (provided the file containing this code is loaded); you don't need to include anything.
And that's what you're doing here -- in order to run code such as Etsy.myself or Etsy::Request.get, you don't need to include the module. You're calling methods on the module directly, not mixing the module's methods into your own class.
However, if you did find yourself in such a situation, where you have a nested class which conflicts with the top-level definition, note that ruby lets you explicitly access the globally scoped class by prepending :: to the class name.
In other words, you can use ::Listing to explicitly reference your own class.
Related
TL;DR;
I need to make some ##vars of a static method (extends) in one module visible to a instance method in another module(includes).
How to accomplish that once only setting ##var=value was not enough to make it visible?
Maybe you can just read my capitalized comment bellow and jump to question 4.
Hi, I would like to add an method to my models to index some data in a mysql table with some full text search fields.
In order to accomplish that, I created the following module:
module ElasticFakeIndexing
module IndexingTarget
#instance method to be called on model to get data to save
def build_index_data
{
entity_id: self.id,
entity_type: self.class.name,
#UNABLE TO ACCESS IF SET ONLY WITH ##var=value. Why?
#AND ALMOST SURE THAT USING class_variable_set IS THE CAUSE OF CONFIGURATION OF ONE MODULE MESSING UP WITH ANOTHER'S
title: ##title_fields.collect{|prop| self.send(prop.to_sym)}.join(" || "),
description: ##description_fields.collect{|prop| self.send(prop.to_sym)}.join(" || "),
}
end
def self.included(base)
base.extend ClassMethods
end
module ClassMethods
#class method to declare/call at a given model
def elastic_fake(options = {})
#Make sure we always get an array so we can use 'join'
title_arg = Array(options[:title])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##title_fields, title_arg)
description_arg = Array(options[:description])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##description_fields, description_arg)
extra_arg = Array(options[:extra])
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##extra_args, extra_arg)
end
end
end
end
And I use it this way at my models:
class SomeModel < ApplicationRecord
#includes the module
include ElasticFakeIndexing::IndexingTarget
...
# 'static' method call to configure to all classes of this model
elastic_fake(title: "prop_a", description: ["prop_b", "prop_c", "prop_d"])
end
And at some point of my code something like this will be called:
index_data = some_model_instance.build_index_data
save_on_mysql_text_search_fields(index_data)
But I got some problems. And have some questions:
when I use/include my module in a second model, looks like the configuration of one model is being visible to the other. And I got 'invalid fields' like errors. I guess it happens because of this, for example:
ElasticFakeIndexing::IndexingTarget.class_variable_set(:##title_fields, title_arg)
But I got to this this because only set ##title_fields wasn't enough to make title_fields visible at build_index_data instance method. Why?
Why using only #title_fields isn't enough too to make it visible at build_index_data?
How to design it in a way that the set of fields are set in a 'static' variable for each model, and visible inside the instance method build_index_data? Or as a possible solution, the fields could live in a instance variable and be visible. But I think it should live in a 'static' variable because the fields will not change from one instance of the model to another...
Any thoughts? What am I missing about the variables scopes/visibility?
Thank you
Read the following articles on Ruby variables:
Ruby Variable Scope
Understanding Scope in Ruby
quick reminder: ##title_fields, class variable, must be initialized at creation time, while #title_fields, instance variable, hasn't such requirement.
Instead of relying on class variables I recommend using class side instance variables. Class variables will easily be overwritten between individual models including the module. Class side instance variables however are save.
Using some of the syntactic sugar (namely concern and class_attribute) rails offers you could write something like
module ElasticFakeIndexing
extend ActiveSupport::Concern
included do
class_attribute :title_fields,
:description_fields,
:extra_args
end
class_methods do
def elastic_fake(options = {})
...
self.title_fields = Array(options[:title])
...
end
end
def build_index_data
...
title: self.class.title_fields ...
...
end
end
I'm in the process of writing an Importable concern for my rails project. This concern will provide a generic way for me to import a csv file into any model that includes Importable.
I need a way for each model to specify which field the import code should use to find existing records. Are there any recommended ways of adding this type of configuring for a concern?
A slightly more "vanilla-looking" solution, we do this (coincidentally, for the exactly some csv import issue) to avoid the need for passing arguments to the Concern. I am sure there are pros and cons to the error-raising abstract method, but it keeps all the code in the app folder and the models where you expect to find it.
In the "concern" module, just the basics:
module CsvImportable
extend ActiveSupport::Concern
# concern methods, perhaps one that calls
# some_method_that_differs_by_target_class() ...
def some_method_that_differs_by_target_class()
raise 'you must implement this in the target class'
end
end
And in the model having the concern:
class Exemption < ActiveRecord::Base
include CsvImportable
# ...
private
def some_method_that_differs_by_target_class
# real implementation here
end
end
Rather than including the concern in each model, I'd suggest creating an ActiveRecord submodule and extend ActiveRecord::Base with it, and then add a method in that submodule (say include_importable) that does the including. You can then pass the field name as an argument to that method, and in the method define an instance variable and accessor (say for example importable_field) to save the field name for reference in your Importable class and instance methods.
So something like this:
module Importable
extend ActiveSupport::Concern
module ActiveRecord
def include_importable(field_name)
# create a reader on the class to access the field name
class << self; attr_reader :importable_field; end
#importable_field = field_name.to_s
include Importable
# do any other setup
end
end
module ClassMethods
# reference field name as self.importable_field
end
module InstanceMethods
# reference field name as self.class.importable_field
end
end
You'll then need to extend ActiveRecord with this module, say by putting this line in an initializer (config/initializers/active_record.rb):
ActiveRecord::Base.extend(Importable::ActiveRecord)
(If the concern is in your config.autoload_paths then you shouldn't need to require it here, see the comments below.)
Then in your models, you would include Importable like this:
class MyModel
include_importable 'some_field'
end
And the imported_field reader will return the name of the field:
MyModel.imported_field
#=> 'some_field'
In your InstanceMethods, you can then set the value of the imported field in your instance methods by passing the name of the field to write_attribute, and get the value using read_attribute:
m = MyModel.new
m.write_attribute(m.class.imported_field, "some value")
m.some_field
#=> "some value"
m.read_attribute(m.class.importable_field)
#=> "some value"
Hope that helps. This is just my personal take on this, though, there are other ways to do it (and I'd be interested to hear about them too).
I am including two different module libraries in my class. Both have the method test_method. How do I explicitly use one over the other?
class User
include Calculus::Math #a module
include Algebra::Math::Misc #a module
#perform_test is defined in both Calculus::Math and Algebra::Math::Misc
perform_test: 1
#Calculus::Math::perform_test: 1 #This doesn't work
end
Thanks
You'll have to do a bit if meta programming:
(Calculus::Math).method(:perform_test).bind(self).call 1
or
include Calculus::Math
alias :foo :perform_test
include ...
I haven't tested either of these and might have made some small errors.
Try turning perform_test into a module function on Calculus::Math.
Calculus::Math.module_eval do
module_function(:perform_test)
public :perform_test
end
class User
include Calculus::Math #a module
include Algebra::Math::Misc #a module
Calculus::Math.perform_test(1)
end
I created a file so I can share a method amongst many models in lib/foo/bar_woo.rb. Inside of bar_woo.rb I defined the following:
module BarWoo
def hello
puts "hello"
end
end
Then in my model I'm doing something like:
def MyModel < ActiveRecord::Base
include Foo::BarWoo
def some_method
Foo::BarWoo.hello
end
end
The interpreter is complaining that it expected bar_woo.rb to define Foo::BarWoo.
The Agile Web Development with Rails book states that if files contain classes or modules and the files are named using the lowercase form of the class or module name, then Rails will load the file automatically. I didn't require it because of this.
What is the correct way to define the code and what is the right way to call it in my model?
You might want to try:
module Foo
module BarWoo
def hello
puts "hello"
end
end
end
Also for calling you won't call it with Foo::BarWhoo.hello - that would have to make it a class method. However includeing the module should enable you to call it with just hello.
Files in subdirectories of /lib are not automatically require'd by default. The cleanest way to handle this is to add a new initializer under config/initializers that loads your library module for you.
In: config/initializers/load_my_libraries.rb Pick whatever name you want.
require(File.join(RAILS_ROOT, "lib", "foo", "bar_woo"))
Once it has been require'd, you should be able to include it at will.
The issue is twofold.
You need to use the outer Foo scope to define BarWoo
You have defined hello as an instance method, then tried to call it on the class.
Define your method using def self.hello instead of def hello
module Foo
module BarWoo
def self.hello
puts "hello"
end
end
end
You can also do
module Foo::Barwoo; end;
I'm working on a Rails app and am looking to include some functionality from "Getting the Hostname or IP in Ruby on Rails" that I asked.
I'm having problems getting it to work. I was under the impression that I should just make a file in the lib directory, so I named it 'get_ip.rb', with the contents:
require 'socket'
module GetIP
def local_ip
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true # turn off reverse DNS resolution temporarily
UDPSocket.open do |s|
s.connect '64.233.187.99', 1
s.addr.last
end
ensure
Socket.do_not_reverse_lookup = orig
end
end
I had also tried defining GetIP as a class but when I do the usual ruby script/console, I'm not able to use the local_ip method at all. Any ideas?
require will load a file. If that file contains any class/module definitions, then your other code will now be able to use them. If the file just contains code which is not in any modules, it will get run as if it were in the same place as your 'require' call (like PHP include)
include is to do with modules.
It takes all the methods in the module, and adds them to your class. Like this:
class Orig
end
Orig.new.first_method # no such method
module MyModule
def first_method
end
end
class Orig
include MyModule
end
Orig.new.first_method # will now run first_method as it's been added.
There's also extend which works like include does, but instead of adding the methods as instance methods, adds them as class methods, like this:
Note above, how when I wanted to access first_method, I created a new object of Orig class. That's what I mean by instance method.
class SecondClass
extend MyModule
end
SecondClass.first_method # will call first_method
Note that in this example I'm not making any new objects, just calling the method directly on the class, as if it had been defined as self.first_method all along.
So there you go :-)
You haven't described how you're trying to use the method, so I apologize in advance if this is stuff you already know.
The methods on a module never come into use unless the module is included into a class. Instance methods on a class require there to be an instance of the class. You probably want a class method instead. And the file itself should be loaded, generally through the require statement.
If the following code is in the file getip.rb,
require 'socket'
class GetIP
def self.local_ip
orig, Socket.do_not_reverse_lookup = Socket.do_not_reverse_lookup, true
UDPSocket.open do |s|
s.connect '64.233.187.99', 1
s.addr.last
end
ensure
Socket.do_not_reverse_lookup = orig
end
end
Then you should be able to run it by saying,
require 'getip'
GetIP.local_ip
require and include are two different things.
require is to strictly load a file once from a load path. The loadpath is a string and this is the key used to determine if the file has already been loaded.
include is used to "mix-in" modules into other classes. include is called on a module and the module methods are included as instance methods on the class.
module MixInMethods
def mixed_in_method
"I'm a part of #{self.class}"
end
end
class SampleClass
include MixInMethods
end
mixin_class = SampleClass.new
puts my_class.mixed_in_method # >> I'm a part of SampleClass
But many times the module you want to mix in is not in the same file as the target class. So you do a require 'module_file_name' and then inside the class you do an include module.