Implementing accessor methods in Ruby - ruby

This is a follow up question to: ruby variable scoping across classes. The solution makes sense to me conceptually, but I can't get it to work. Thought maybe with more code someone could help me.
I have a class Login that declares a new IMAP class, authenticates, and picks a mailbox.
I then am trying to create a separate class that will "do stuff" in the mailbox. For example, calculate the number of emails received. The problem is that the #imap instance of Net::IMAP doesn't pass from the Login class to the Stat class -- I'm getting no method errors for imap.search in the new class. I don't want to re-log in and re-authenticate each time I need to "do some stuff" with the mailbox. I'm trying to implement the solution in the other thread, but can't seem to get it to work.
Here's the Login class:
class Login
def initialize(user, domain, pass)
#username = user
#domain = domain
#pass = pass
#check if gmail or other domain
gmail_test = #domain.include? "gmail.com"
if gmail_test == true
#imap = Net::IMAP.new('imap.gmail.com',993,true,nil,false)
#imap.login(#username + "#" + #domain, #pass)
else
#imap = Net::IMAP.new("mail." + #domain)
#imap.authenticate('LOGIN', #username + "#" + #domain, #pass)
end
return self
end
#enable mailbox select
def mailbox(box)
#mailbox = box
#mailbox_array = #imap.list('','*').collect{ |mailbox| mailbox.name } #create array of mailboxes
#matching_mailbox = #mailbox_array.grep(/#{#mailbox}/i) #search for mailbox along list
if #matching_mailbox.empty? == true #if no results open INBOX
#mailbox = "INBOX"
#imap.examine(#mailbox)
else
#imap.examine(#matching_mailbox.first.to_s) #if multiple results, select first and examine
#mailbox = #matching_mailbox.first.to_s
end
return self
end
end
I want to be able to say:
Login.new("user", "domain", "pass").mailbox("in")
and then something like:
class Stat
def received_today()
#emails received today
#today = Date.today
#received_today = #imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
puts #domain + " " + #mailbox.to_s + ": " + #received_today + " -- Today\n\n" #(" + #today.strftime("%d-%b-%Y") + ")
end
end
And be able to call
Stat.new.received_today and have it not throw a "no method search" error. Again, the other question contains pseudo_code and a high level explanation of how to use an accessor method to do this, but I can't implement it regardless of how many hours I've tried (been up all night)...
All I can think is that I am doing this wrong at a high level, and the stat calculation needs to be a method for the Login class, not a separate class. I really wanted to make it a separate class, however, so I could more easily compartmentalize...
Thanks!

Another approach that works and doesn't require defining get_var methods:
b.instance_variable_get("#imap") # where b = class instance of login

OK --- After much head banging on the wall, I got this to work.
Added these three methods to class Login:
def get_imap
#imap
end
def get_domain
#domain
end
def get_mailbox
#mailbox
end
Changed class Stat to:
class Stat
def received_today(login)
#emails received today
#today = Date.today
#received_today = login.get_imap.search(["SINCE", #today.strftime("%d-%b-%Y")]).count.to_s
# puts #received_today
puts login.get_domain + " " + login.get_mailbox.to_s + ": " + #received_today + " -- Today\n\n"
end
end
Now this actually works, and doesn't say undefined method search or imap:
b = Login.new("user", "domain", "pass").mailbox("box")
c = Stat.new
c.received_today(b)
I'm pretty sure there is a way to use attr_accessor to do this as well, but couldn't figure out the syntax. Anyway, this works and enables me to use the #imap var from class Login in class Stat so I can write methods to "do_stuff" with it. Thanks for the help and please don't hesitate to tell me this is horrible Ruby or not best practices. I'd love to hear the Ruby way to accomplish this.
Edit for attr_accessor or attr_reader use:
Just add it to class Login and then can say login.imap.search#stuff in class Stat with no problem.

Related

Ruby Class Output

I am trying my hands at Ruby, below is the code that I am writing in 2 different ways to understand Ruby Classes. In first block I am using accessor method (combination of accessor read & write) and I want to print final line as "lord of the rings is written by Tolkien and has 400 pages". How can I make that happen? I understand that adding string and integer will throw an error. I can get them to print on separate lines, its just that I can't get them in a sentence.
class Book
attr_accessor :title, :author, :pages
end
book1 = Book.new()
book1.title = 'lord of the rings'
book1.author = 'Tolkien'
book1.pages = 400
puts book1.title
puts book1.author
puts book1.pages
#puts book1.title + " is written by " + book1.author + " and has " + book1.pages + " pages" <<<this errors out for known reason>>>
Second piece of code doing the same thing but I am using instance variable and have figured out how to get desired output. However, please advise if there's a better way of doing this?
class Novel
def initialize(title, author, pages)
#title = title
#author = author
#pages = pages
end
def inspect
"#{#title} is written by #{#author} and has #{#pages} pages"
end
end
novel1 = Novel.new('harry potter', 'JK Rowling', 300)
puts novel1.inspect
In your first example you are providing access the info you want and leaving it up to the client to format the output. For example you could have gotten what you wanted by adding this line in place of your commented line.
puts "#{book1.title} is written by #{book1.author} and has #{book1.pages} pages"
In your second example you are "pushing" that code down into the Novel class and proving a method to produce the output you want. BTW, don't use inspect as a method name, inspect is already a defined method
For example the following will print the same info twice.
class Novel
attr_accessor :title, :author, :pages
def initialize(title, author, pages)
#title = title
#author = author
#pages = pages
end
def info
"#{#title} is written by #{#author} and has #{#pages} pages"
end
end
novel = Novel.new('harry potter', 'JK Rowling', 300)
puts novel.info
puts "#{novel.title} is written by #{novel.author} and has #{novel.pages} pages"

How to stub class instantiated inside tested class in rspec

I have problem stubbing external api, following is the example
require 'rspec'
require 'google/apis/storage_v1'
module Google
class Storage
def upload file
puts '#' * 90
puts "File #{file} is uploaded to google cloud"
end
end
end
class UploadWorker
include Sidekiq::Worker
def perform
Google::Storage.new.upload 'test.txt'
end
end
RSpec.describe UploadWorker do
it 'uploads to google cloud' do
google_cloud_instance = double(Google::Storage, insert_object: nil)
expect(google_cloud_instance).to receive(:upload)
worker = UploadWorker.new
worker.perform
end
end
I'm trying to stub Google::Storage class. This class is instantiated inside the object being tested. How can I verify the message expectation on this instance?
When I run above example, I get following output, and it seems logical, my double is not used by tested object
(Double Google::Storage).upload(*(any args))
expected: 1 time with any arguments
received: 0 times with any arguments
I'm new to Rspec and having hard time with this, any help will be appreciated.
Thanks!
Reaching for DI is always a good idea (https://stackoverflow.com/a/51401376/299774) but there are sometimes reasons you can't so it, so here's another way to stub it without changing the "production" code.
1. expect_any_instance_of
it 'uploads to google cloud' do
expect_any_instance_of(Google::Storage).to receive(:insert_object)
worker = UploadWorker.new
worker.perform
end
In case you just want to test that the method calls the method on any such objects.
2. bit more elaborated setup
In case you want to control or set up more expectations, you can do this
it 'uploads to google cloud' do
the_double = instance_double(Google::Storage)
expect(Google::Storage).to receive(:new).and_return(the_double)
# + optional `.with` in case you wanna assert stuff passed to the constructor
expect(the_double).to receive(:insert_object)
worker = UploadWorker.new
worker.perform
end
Again - Dependency Injection is clearer, and you should aim for it. This is presented as another possibility.
I would consider reaching for dependency injection, such as:
class UploadWorker
def initialize(dependencies = {})
#storage = dependencies.fetch(:storage) { Google::Storage }
end
def perform
#storage.new.upload 'test.txt'
end
end
Then in the spec you can inject a double:
storage = double
expect(storage).to receive(...) # expection
worker = UploadWorker.new(storage: storage)
worker.perform
If using the initializer is not an option then you could use getter/setter method to inject the dependency:
def storage=(new_storage)
#storage = new_storage
end
def storage
#storage ||= Google::Storage
end
and in the specs:
storage = double
worker.storage = storage

Variable URL with Instance Variables

I know I'm being an idiot here, but I can't think of how this is done. I am creating an app with certain interests and am using a a Wikipedia scrape set up using Nokogiri. I have two inputs: Title and Wikipedia, but want to fill Summary and Content in the data model using the scrape. I want to use the Wikipedia attribute as a variable in a url within a method, but keep getting the error dynamic constant assignment PAGE_URL = "http://en.wikipedia.org/w/i....
I thought that the methods should go in the model, with reference to them in the Create definition under the controller, but this doesn't seem to work.
EDIT
I've just tried taking the constants out of the methods as suggested, but I am still getting a dynamic constant assignment error. My model currently looks like this:
PAGE_URL1 = "http://en.wikipedia.org/w/index.php?title="
PAGE_URL2 = "&printable=yes"
def get_PAGE_URL
PAGE_URL = PAGE_URL1 + self.wikipedia + PAGE_URL2
end
def get_page
page = Nokogiri::HTML(open(PAGE_URL))
end
def get_summary
get_PAGE_URL
self.summary = page.css("p")[0].text
end
def get_full_page
get_PAGE_URL
puts page.css('div#content.mw-body div#bodyContent div#mw-content-text.mw-content-ltr p').each do |p|
self.content = puts p.text
end
end
Constants can't go inside of methods, they must be defined inside of the class' direct scope.
Edit:
For example:
class WikiScraper
PAGE_URL = "http://www.wikipedia.org/"
def scrape
page_num = '5'
my_url = PAGE_URL + page_num
end
end

Ruby Devise 2.2 add email address along with the user.email

Currently I am using ruby devise gem 2.2.3. And I tried to customize the confirmation_instructions for adding couple of email ids with the user email.
app/mailers/my_devise_mailer.rb
class MyDeviseMailer < Devise::Mailer
include Devise::Mailers::Helpers
def confirmation_instructions(record, opts={})
opts[:to] = "example1#mail.com, example2#mail.com"
super
end
end
config/initializers/devise.rb
config.mailer = "MyDeviseMailer"
And, I ran the following in my console
user = User.first
MyDeviseMailer.delay.confirmation_instructions(user)
I got a output without body message. PFA
Correct
Wrong
Can anyone tell me what I missed to add/configure?
You could create a new mailer instead and modify the headers of that one:
# app/mailers/my_mailer.rb
class MyMailer < Devise::Mailer
def headers_for(action, opts)
if action == :confirmation_instructions
super.merge!(to: ['example1#mail.com', 'example2#mail.com'])
else
super
end
end
end
Then tell Devise to use your mailer:
# config/initializers/devise.rb
config.mailer = MyMailer
super.merge!" will not work. Because it'll replace the value for the given key (:to). But, the requirement is to add two mail id's with 'To'. The following gist is working fine.
class MyMailer < Devise::Mailer
include Devise::Mailers::Helpers
def headers_for(action, opts={})
begin
super.merge!(to: [super[:to], 'example1#mail.com', 'example2#mail.com'], template_path: ["devise/mailer"]) if action == :confirmation_instructions
rescue Exception => e
super
end
end
end
Happy Coding!!

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.

Resources