I have a method on my model and it actually was created as advised(with self.method_name). However, when I go to my console and try to test the method I get an undefined method error.
Does anyone know why?
my Model
class Country < ApplicationRecord
has_many :facts
has_many :dishes
has_many :touristic_places
include HTTParty
base_uri 'restcountries.eu/rest/v2/region/africa'
def self.save_data_from_api
response = HTTParty.get(base_uri)
country_data = JSON.parse(response)
countries = country_data.map do |line|
c = Country.new
c.name = line.name
c.save
c
end
countries.select(&:persisted?)
end
On my Controller
def save_data_from_api
countrie = Country.save_data_from_api
end
Test on Rails console error:
> Country.save_data_from_api
Traceback (most recent call last):
2: from (irb):5
1: from (irb):5:in `rescue in irb_binding'
NoMethodError (undefined method `save_data_from_api' for #<Class:0x00007fc9ec71edd8>)
Don't do HTTP calls in your model. Even without any code your model already has a ton of responsibilities that it gets from ActiveRecord::Base:
validations
assocations
persistence
callbacks
naming
i18n
Instead create a separate client object that fetches the data from the API. This gives you an object which just does one job which is both easy to test and stub out:
# app/clients/rest_countries_client.rb
# HTTP client for the restcountries.eu API
class RestCountriesClient
include HTTParty
base_uri 'restcountries.eu/rest/v2'
format :json # this will automatically parse the response
def self.region(region)
get("/region/#{region}")
end
end
This lets you just test the API call from the console by calling RestCountriesClient.region('africa') and you can see the returned hash without any side-effects.
To actually do the call and persist the objects you want to use a service object or ActiveJob:
# app/jobs/country_importer_job.rb
# Persists countries from the restcountries.eu API
class CountryImporterJob < ApplicationJob
def perform(region = 'africa')
response = RestCountriesClient.region(region)
return unless response.success?
response.map do |line|
Country.create(name: line["name"])
end.select(&:persisted?)
end
end
You would then call this job from the controller:
CountryImporterJob.perform_now('africa')
Quit the console and start it again. Auto-reload only works in the server, not the console.
Related
Background: I'm bringing to life a 6-year-old Rails project and haven't touched the framework since then. Thus, I'm re-learning many things.
I'm trying to understand the best approach to mock an API call that needs to be done synchronously. An Order has_one Invoice, and Invoice must get a reference from an external service. An Order is useless without an Invoice.
Below is a simple version of the application. The Order model is core to the application.
Open questions:
Is the best practise to globally mock SDKs in spec_helper.rb? Which would contain my allow_any_instance_of(InvoiceServiceSdk)
I have an Order factory, used almost everywhere in my tests. But I'm confused if I can loop in an Invoice factory as well. FactoryBot feels quite alien to me at the moment.
# app/models/order.rb
class Order < ApplicationRecord
has_one :invoice, autosave: true
before_create :build_invoice
def build_invoice
self.invoice = Invoice.new
end
end
# app/models/invoice.rb
class Invoice < ApplicationRecord
belongs_to :order
before_create :generate
def generate
invoice_service = InvoiceServiceSdk.new
self.external_id = invoice_service.fetch
end
end
# app/models/invoice_service_sdk.rb
require 'uri'
require 'net/http'
class InvoiceServiceSdk
def fetch
uri = URI('https://example.com/') # Real HTTP request
res = Net::HTTP.get_response(uri)
SecureRandom.urlsafe_base64 # "ID" that API "provides"
end
end
# spec/models/order.rb
require 'rails_helper'
RSpec.describe Order, type: :model do
before do
allow_any_instance_of(InvoiceServiceSdk).to receive(:fetch).and_return('super random external invoice ID')
end
context "new order + invoice" do
it {
o = Order.new
o.save
expect(o.invoice.external_id).to eq 'super random external invoice ID'
}
end
end
The rspec-mocks documentation discourages the use of allow_any_instance:
The rspec-mocks API is designed for individual object instances, but this feature operates on entire classes of objects. As a result there are some semantically confusing edge cases. For example, in expect_any_instance_of(Widget).to receive(:name).twice it isn't clear whether a specific instance is expected to receive name twice, or if two receives total are expected. (It's the former.)
Using this feature is often a design smell. It may be that your test is trying to do too much or that the object under test is too complex.
You can avoid it completely by just adding a factory method to your service objects:
class MyService
def intialize(**kwargs)
#options = kwargs
end
def call
do_something_awesome(#options[:foo])
end
def self.call(**kwargs)
new(**kwargs).call
end
end
allow(MyService).to recieve(:call).and_return([:foo, :bar, :baz])
Is it smelly to Stub the request in spec_helper?
Not necissarily. You can avoid a bit of overhead by refactoring the code as indicated above and stubbing the factory method. It also makes it so that your stubs are not coupled to the inner workings of the service object.
I would be more worried about the fact that this code does one thing right by using a service object and then immediately cancels that out by calling it in a model callback.
I am getting a error using action cable,
NameError (undefined local variable or method `connections_info' for MicropostNotificationsChannel:Class):
app/channels/micropost_notifications_channel.rb:12:in `notify'
app/models/notification.rb:8:in `block in <class:Notification>'
app/controllers/likes_controller.rb:11:in `create'
Rendering C:/RailsInstaller/Ruby2.2.0/lib/ruby/gems/2.2.0/gems/actionpack-5.0.0.1/lib/action_dispatch/middleware/templates/rescues/diagnostics.text.erb
...
To my knowledge and from using controllers and models I can call the class method after_commit -> {MicropostNotificationsChannel.notify(self)} and then get to the self.notifiy(notification) and then as connections_info is a instance method I should be able to call it inside that class and carry out my code but I get the error here?
class Notification < ApplicationRecord
...
after_commit -> {MicropostNotificationsChannel.notify(self)}
end
The action cable micropost notification channel
class MicropostNotificationsChannel < ApplicationCable::Channel
def subscribe
...
end
def unsubscribe
...
end
def self.notifiy(notification)
connection_results = connections_info
puts connection_results.inspect
end
end
Channel.rb
module ApplicationCable
class Channel < ActionCable::Channel::Base
def connections_info
# do stuff here
end
end
end
You've defined connections_info as an instance method in ApplicationCable::Channel, but notify is a class method and so it's looking for methods at the class level instead of the instance level. Sometimes classes will get around this by using method_missing, but it doesn't look like Action Cable does that at a glance. Without knowing more about what you're trying to do, it's hard to say whether to change connections_info to a class method, notify to an instance method, or something else.
This also happens when your redis configuration is not correct.
So I'm wanting to parse a table on about 10 websites, so I want to create a new thread for each site. However, I'm not exactly sure how to return the data from this type of request.
Here's one class:
class TestRequest
def initialize
end
def start
urls = ['site1','site2','site3']
existing_data = Data.pluck(:symbol, :page)
data = GetData.pool(size: 10)
urls.each do |url|
data.async.perform_requests(url, existing_data)
end
end
end
and then GetData class looks like this:
require 'celluloid/current'
class GetData
include Celluloid
def perform_requests(url, existing_data)
# perform HTTP request
# parse HTTP response
# return returned data ???
end
end
What I'd ultimately like to do is have an instance variable in TestRequest class and simply add the returned value from GetData into that instance variable from the TestRequest class. After the threads are finished, I want to perform another action using the data in the instance variable.
I tried playing around with attr_reader, but it doesn't seem to play in my favor.
I tried this:
class TestRequest
def initialize
end
def start
#returned_data = []
urls = ['site1','site2','site3']
existing_data = Data.pluck(:symbol, :page)
data = GetData.pool(size: 10)
urls.each do |url|
data.async.perform_requests(url, existing_data)
end
end
attr_reader :returned_data
end
and then
require 'celluloid/current'
class GetData
include Celluloid
def perform_requests(tr, existing_data)
# perform HTTP request
# parse HTTP response
t = TestData.new
t.returned_data << "value"
end
end
but this doesn't work either.
Multi-threading and Ruby on Rails don't mix very well.
However, you should consider using the ActiveJob documentation (http://guides.rubyonrails.org/active_job_basics.html).
With ActiveJob, you can enqueue jobs and have them executed in the background. There are hooks method defined as well to notify you when a job is about to start, is running or have finished.
Hi there im trying to create a model for a runescape item. Using there api and httparty. Im having a number of issues. But this one is regarding the use of overriding the initialize method saying it has the wrong number of arguments.
class Item < ApplicationRecord
require 'json'
include HTTParty
base_uri 'http://services.runescape.com/m=itemdb_oldschool/'
attr_accessor :name, :description, :price, :icon_url
def initialize(name, description, price, icon_url)
super
self.name = name
self.description = description
self.price = price
self.icon_url = icon_url
end
def self.find(name)
response = get("/api/catalogue/detail.json?item=#{name}")
if response.success?
parsed = JSON.parse(response)
self.new(
parsed["item"]["name"],
parsed["item"]["description"],
parsed["item"]["current"]["price"],
parsed["item"]["icon_large"]
)
else
# this just raises the net/http response that was raised
raise response.response
end
end
end
So in rails console i run the following command to test it:
item_test = Item.find("227")
ArgumentError: wrong number of arguments (given 4, expected 0..1)
from /Users/jacksharville/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.1/lib/active_record/core.rb:312:in `initialize'
from /Users/jacksharville/Desktop/OSCRUDDY/app/models/item.rb:13:in `initialize'
from /Users/jacksharville/.rvm/gems/ruby-2.3.1/gems/activerecord-5.0.1/lib/active_record/inheritance.rb:65:in `new'
But the initialize takes 4 arguments. When reduce it to one then it says it requires 4. Which leaves me very confused.
Im not even sure that overriding the base initialise is the way forward to do something like this. So if you have a better idea please let me know i'm new to this.
In conclusion my question is why is my object not being created correctly? Secondly is this the right approach for creating the object?
The problem I see here is that you are trying to mixin HTTParty with an ActiveRecord subclass, when really you should have two separate classes. The Item class should be responsible for interfacing with your database. You should create another class, i.e. ItemResource with Httparty, which will have the responsibility of connecting to the Runescape resource and respond with a response. You should use this to grab the data from the resource, and create an Item record with that data. Single Responsibility Principle
One thing to keep in mind is that you should almost never be redefining initialize for any models that inherit from ApplicationRecord. Checkout the API
EDIT
This code should get you relatively close
#app/models/item.rb
class Item < ApplicationRecord; end
#app/models/item_resource.rb | app/resources/item_resource.rb or something similar
require 'json'
class ItemResource
include HTTParty
self.base_uri 'http://services.runescape.com/m=itemdb_oldschool/'
def find name
response = get("/api/catalogue/detail.json?item=#{name}")
if response.success?
parsed = JSON.parse(response)
Item.new(
name: parsed["item"]["name"],
description: parsed["item"]["description"],
price: parsed["item"]["current"]["price"],
icon_large: parsed["item"]["icon_large"]
)
else
raise response.response
end
end
end
#somewhere else
item = ItemResource.find 'Elysian Spirit Shield'
if item.save
#do stuff
else
#handle failure
end
I trying to use sidekiq in my app, but when I write this simple worker
class ParseWorker
include Sidekiq::Worker
def perform(instance)
instance.spideys << Spidey.create
end
end
and I use this worker here
def create
#user_link = UserLink.new(user_link_params)
if #user_link.save
binding.pry
ParseWorker.perform_async(#user_link)
redirect_to results_user_links_path
end
end
was returned the error
2015-04-09T13:14:56.757Z 11644 TID-ay1nc ERROR: Actor crashed!
NoMethodError: undefined method `spideys' for "#<UserLink:0x007f65f00d99b0>":String
/home/weare138/simple-parser/app/workers/parse_worker.rb:7:in `perform'
but why? #user_link is not a string
how fix?
upd
def perform(id)
user_link = UserLink.find(id)
user_link.spideys << Spidey.create
end
error
2015-04-09T14:15:21.889Z 11644 TID-ay1nc ERROR: Actor crashed!
NoMethodError: undefined method `spideys' for 39:Fixnum
upd2
class ParseWorker
include Sidekiq::Worker
require 'open-uri'
def perform(id)
user_link = UserLink.find(id)
user_link.spideys << Spidey.create
end
end
Sidekiq uses Redis, so when you pass to the worker an object, it serializes it to JSON.
From the Sidekiq documentation:
This means the arguments to your worker must be simple JSON datatypes
(numbers, strings, boolean, array, hash). Complex Ruby objects (e.g.
Date, Time, ActiveRecord instances) will not serialize properly.
Instead you should do
def create
...
ParseWorker.perform_async(#user_link.id)
end
def perform(id)
UserLink.find(id).spideys << Spidey.create
end