Rails pagination in API using Her, Faraday - ruby

I've been trying to figure this out all day, and it's driving me crazy.
I have two rails apps, ServerApp and ClientApp. ClientApp gets data from ServerApp through an API, using the Her gem. Everything was great until I needed pagination information.
This is the method I am using to get the orders (this uses kamainari for pagination and ransack for search):
# ServerApp
def search
#search = Order.includes(:documents, :client).order('id desc').search(params[:q])
#orders = #search.result(distinct: true).page(params[:page]).per(params[:per])
respond_with #orders.as_json(include: :documents)
end
It returns an array of hashes in json, which Her uses as a collection of orders. That works fine.
# Response
[
{
"client_id": 239,
"created_at": "2013-05-15T15:37:03-07:00",
"id": 2422,
"ordered_at": "2013-05-15T15:37:03-07:00",
"origin": "online",
"updated_at": "2013-05-15T15:37:03-07:00",
"documents": [
{ ... }
]
},
...
]
But I needed pagination information. It looked like I needed to send it as metadata with my json. So I change my response to this:
respond_to do |format|
format.json do
render json: { orders: #orders.as_json(include: :documents), metadata: 'sent' }
end
end
This does indeed send over metadata, so in my ClientApp I can write #orders.metadata and get 'sent'. But now my orders are nested in an array inside of 'orders', so I need to use #orders.orders, and then it treats it like an array instead of a Her collection.
After doing some reading, it seemed sending pagination info through headers was the way a lot of other people did this (I was able to get the headers set up in an after_filter using this guide). But I am even more lost on how to get those response headers in my ClientApp - I believe I need a Faraday Middleware but I just am having no luck getting this to work.
If anyone knows how I can just get this done, I would be very grateful. I can't take another day of banging my head against the wall on this, but I feel like I am just one vital piece of info away from solving this!

I encountered the same issue and solved it by adding my own middleware and rewriting the "parse" and "on_complete" methods without that much hassle and avoiding the use of global variables.
Here's the code:
class CustomParserMiddleware < Her::Middleware::DefaultParseJSON
def parse(env)
json = parse_json(env[:body])
pagination = parse_json(env[:response_headers][:pagination_key]) || {}
errors = json.delete(:errors) || {}
metadata = json.delete(:metadata) || {}
{
:data => json,
:errors => errors,
:metadata => {
:pagination => pagination,
:additional_metadata => metadata
},
end
def on_complete(env)
env[:body] = case env[:status]
when 204
parse('{}')
else
parse(env)
end
end
end
then, you can access the pagination as follows:
model = Model.all
model.metadata[:pagination]

I finally got this working. The trick was to use a global variable in the faraday on_complete - I tried to find a better solution but this was the best I could do. Once again, I got the header code from here. Here's the full guide to how to get pagination working with Her:
First, on my server side, I have the Kaminari gem, and I pass page and per as params to the server from the client. (This is also using ransack for searching)
def search
#search = Order.order('id desc').search(params[:q])
#orders = #search.result(distinct: true).page(params[:page]).per(params[:per])
respond_with #orders.as_json(include: :items)
end
My client makes the request like so:
#orders = Order.search(q: { client_id_eq: #current_user.id }, page: params[:page], per: 3)`
Back on the server, I have this in my ApiController (app controller for api):
protected
def self.set_pagination_headers(name, options = {})
after_filter(options) do |controller|
results = instance_variable_get("##{name}")
headers["X-Pagination"] = {
total_count: results.total_count,
offset_value: results.offset_value
}.to_json
end
end
In the server orders_controller.rb, I set the pagination headers for the search method:
class OrdersController < ApiController
set_pagination_headers :orders, only: [:search]
...
end
Now to receive the headers we need a Faraday middleware in Her on the client.
# config/initializers/her.rb
Her::API.setup url: Constants.api.url do |c|
c.use TokenAuthentication
c.use HeaderParser # <= This is my middleware for headers
c.use Faraday::Request::UrlEncoded
c.use Her::Middleware::DefaultParseJSON
c.use Faraday::Adapter::NetHttp
c.use Faraday::Response::RaiseError
end
# lib/header_parser.rb
# don't forget to load this file in application.rb with something like:
# config.autoload_paths += Dir[File.join(Rails.root, "lib", "*.rb")].each { |l| require l }
class HeaderParser < Faraday::Response::Middleware
def on_complete(env)
unless env[:response_headers]['x-pagination'].nil?
# Set the global var for pagination
$pagination = JSON.parse(env[:response_headers]['x-pagination'], symbolize_names: true)
end
end
end
Now back in your client controller, you have a global variable of hash called $pagination; mine looks like this:
$pagintation = { total_count: 0, offset_value: 0 }`
Finally, I added Kaminari gem to my client app to paginate the array and get those easy pagination links:
#orders = Kaminari.paginate_array(#orders, total_count: $pagination[:total_count]).page(params[:page]).per(params[:per_page])`
I hope this can help someone else, and if anyone knows a better way to do this, let me know!

You can pass header options to Faraday when setting up the connection, see the docs at http://rubydoc.info/gems/faraday/0.8.7/Faraday/Connection:initialize
Sometimes it helps to do a curl request first, esp. use -vv option for verbose output where you will see all headers. (Maybe you can attach some log outputs from the Server too)
You can use e.g. clogger (http://clogger.rubyforge.org/) do monitor header information on the Rails server side

Related

Testing External Api in ruby using rspec

I am working on a project where I have to test an external api which is making call to github.
Let's take the code below as example, the class below does not have intializer
Class CodeReviewSignedOff
def pullrequestreview(github)
#the following line return a json response, it require two parameter a string and a number
github.client.pull_request_reviews("string",number).each do |github|
#block of code which perform some operation
end
return count #return integer response
end
end
As you can see my class CodeReviewSignedOff relies on the github object which is defined in my existing project and using the object to make external API call.
But the thing is, I do not want to actually call the API.
I want stub request github.client.pull_request_reviews
allow(github).to receive_message_chain(:client,:pull_request_review_requests).and_return json
My question here is,
How should I return the json response because if I include it in the double quote it will be interpreted as string.
My second question
How do I test my pullrequestreview function.
github=double #I am creating a fake github
object=CodeReviewSignedOff.new
expect(pullrequestreview).with(double).and_return(3)
Is the above logic correct?
Good question!
Disclaimer: I'm still learning about RoR, but I can try to answer this question as best as I can. If anyone has suggestions about how I can improve my response, please let me know!
Testing a class method (using stubs)
So, in order to test your method called "pullrequestreview", you'll need to stub a method within the Github::Client class. I personally have not worked with GitHub's API, so let's assume that it is a static class with a few class methods (including pull_request_reviews).
(Note: I renamed "pullrequestreview" to pull_request_review, because it is more idiomatic in Ruby)
In the example below, I stubbed Github::Client.pull_request_reviews, so that it will return res_data whenever it is called:
Github::Client.stubs(:pull_request_reviews).returns(res_data)
Next, I use a "factory" to generate a github_client argument to pass into code_review_sign_off.pull_request_review. Since the GitHub method is stubbed, it doesn't really matter what I pass into it. In more complex test scenarios, it might be a good idea to make use of a factory bot gem to build out different GitHub related objects (with different trait options). For example, using a factory, you could define a new github_client with a "pull_request" trait like so:
github_client = create(:github_client, :with_pull_request)
In your post, code_review_sign_off.pull_request_review is expecting an iterable object from GitHub. Let's assume that code_review_sign_off.pull_request_review returns the number of elements within the array. Since I have control over GitHub's response, I know that only one element will be returned, so the result should be 1, which I assert using:
assert_equal pull_request_review_response, 1
So, in general when testing, you want to issolate the function that you're interested in and use stubs to keep everything else constant.
require 'test_helper'
class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest
test 'with CodeReviewSignedOff method' do
res_data = [{
:id => 1,
:data => "some_data"
}]
Github::Client.stubs(:pull_request_reviews).returns(res_data)
github_client = create(:github_client, :with_pull_request)
code_review_sign_off = CodeReviewSignedOff.new
pull_request_review_response = code_review_sign_off.pull_request_review(github_client)
...
assert_equal pull_request_review_response, 1
end
end
(Note: I'm using the mocha/minitest in the examples)
Bonus: Mocking an API endpoint
So, first, I am going to assume you have your test environment for RSpec properly configured (you ran rails generate rspec:install command) and you have the appropriate /spec directory.
The first step to mocking an API endpoint is to stub the calls to the actual endpoints by using a gem like WebMock. So, install the gem and add require 'webmock/rspec' to /spec/spec_helper.rb
Within /spec/spec_helper.rb you'll need to define a few things,
First, add the following to ensure that you're not continuing to make real calls to the API:
WebMock.disable_net_connect!(allow_localhost: true)
Next, you'll need a stub for each API endpoint that you're mocking. For example, in your case, you'd need to define something like:
RSpec.configure do |config|
config.before(:each) do
pull_request_review_response = [
{
"id": 80,
"node_id": "MDE3OlB1bGxSZXF1ZXN0UmV2aWV3ODA=",
"user": {
"login": "octocat",
"id": 1,
"node_id": "MDQ6VXNlcjE=",
"avatar_url": "https://github.com/images/error/octocat_happy.gif",
"gravatar_id": "",
"url": "https://api.github.com/users/octocat",
"html_url": "https://github.com/octocat",
"followers_url": "https://api.github.com/users/octocat/followers",
"following_url": "https://api.github.com/users/octocat/following{/other_user}",
"gists_url": "https://api.github.com/users/octocat/gists{/gist_id}",
"starred_url": "https://api.github.com/users/octocat/starred{/owner}{/repo}",
"subscriptions_url": "https://api.github.com/users/octocat/subscriptions",
"organizations_url": "https://api.github.com/users/octocat/orgs",
"repos_url": "https://api.github.com/users/octocat/repos",
"events_url": "https://api.github.com/users/octocat/events{/privacy}",
"received_events_url": "https://api.github.com/users/octocat/received_events",
"type": "User",
"site_admin": false
},
"body": "Here is the body for the review.",
"state": "APPROVED",
"html_url": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80",
"pull_request_url": "https://api.github.com/repos/octocat/Hello-World/pulls/12",
"_links": {
"html": {
"href": "https://github.com/octocat/Hello-World/pull/12#pullrequestreview-80"
},
"pull_request": {
"href": "https://api.github.com/repos/octocat/Hello-World/pulls/12"
}
},
"submitted_at": "2019-11-17T17:43:43Z",
"commit_id": "ecdd80bb57125d7ba9641ffaa4d7d2c19d3f3091",
"author_association": "COLLABORATOR"
}
]
owner = 'octocat',
repo = 'hello-world',
pull_number = 42
stub_request(:get, "https://api.github.com/repos/#{owner}/#{repo}/pulls/#{pull_number}/reviews").
to_return(status: 200, body: pull_request_review_response.to_json,
headers: {'Accept'=>'*/*', 'User-Agent'=>'Ruby'})
end
# ....
end
The important part of this code is stub_request which takes an HTTP request method, a URL path, and it returns the arguments passed into to_return (as an HTTP response). The response object will include a status, body, and a header.
I used the examples from the WebMock README and the API doc from GitHub to produce the example above (Note: It is untested).
Now, you can define a new test that calls the Mock endpoint:
require 'spec_helper'
class CodeReviewSignedOffTest < ActionDispatch::IntegrationTest
test 'GET pull_request_review_response endpoint from GitHub mock' do
uri = URI('https://api.github.com/repos/octocat/hello-world/pulls/42/reviews')
response = Net::HTTP.get(uri)
data = JSON.parse(response&.body)
...
end
end
Hopefully you'll see the data you defined within your mock API come through. I haven't tested this code, so it might require some debugging.
Let me know if you have any questions or run into trouble writing your tests! Good luck.

Post to Sinatra API with hyperresource

I built an app in Sinatra with Roar (hal/JSON) and now I'm trying to post a new item from my client to this API.
In my Sinatra app I have routes like these:
get '/todos/new' do
#pagetitle = 'New Todo'
#todo = Todo.new
#todo.extend(TodoRepresenter)
#todo.to_json
erb :'todos/new'
end
post "/todos" do
#todo = Todo.new(params[:todo])
if #todo.save
redirect "todos/#{#todo.id}"
else
erb :"todos/new"
end
end
And my client.rb looks like this:
require 'hyperresource'
class ApiRequest < HyperResource
api = ApiRequest.new(root: 'http://127.0.0.1:9393',
headers: {'Accept' => 'application/vnd.http://127.0.0.1:9393.v1+json'})
api.post '/todos/new', { :title => "a"}
This doesn't work. The only way of get a working client is the get function:
require 'hyperresource'
class ApiRequest < HyperResource
api = ApiRequest.new(root: 'http://127.0.0.1:9393/todos/13',
headers: {'Accept' => 'application/vnd.http://127.0.0.1:9393.v1+json'})
todo = api.get
output = todo.body
puts output
I don't know how to solve this, and the Github page doesn't tell me either.
I changed the API slightly:
get '/todos' do
#pagetitle = 'New Todo'
#todo = Todo.new
erb :'todos/new'
end
post "/todos/new" do
#todo = Todo.new(params[:todo])
#todo.extend(TodoRepresenter)
#todo.to_json
if #todo.save
redirect "todos/#{#todo.id}"
else
erb :"todos/new"
end
end
and also the way of posting in my client:
todo = api.get.new_todo.post(title: "Test")
In my API console I now get:
D, [2014-11-04T17:11:41.875218 #11111] DEBUG -- : Todo Load (0.1ms) SELECT "todos".* FROM "todos" WHERE "todos"."id" = ? LIMIT 1 [["id", 0]]
ActiveRecord::RecordNotFound - Couldn't find Todo with 'id'=new:
and a lot of other code.
In my client console I get a lot of code, with a hyper resource server error and a lot of other code.

Ruby stubbing with faraday, can't get it to work

Sorry for the title, I'm too frustrated to come up with anything better right now.
I have a class, Judge, which has a method #stats. This stats method is supposed to send a GET request to an api and get some data as response. I'm trying to test this and stub the stats method so that I don't perform an actual request. This is what my test looks like:
describe Judge do
describe '.stats' do
context 'when success' do
subject { Judge.stats }
it 'returns stats' do
allow(Faraday).to receive(:get).and_return('some data')
expect(subject.status).to eq 200
expect(subject).to be_success
end
end
end
end
This is the class I'm testing:
class Judge
def self.stats
Faraday.get "some-domain-dot-com/stats"
end
end
This currently gives me the error: Faraday does not implement: get
So How do you stub this with faraday? I have seen methods like:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get('http://stats-api.com') { [200, {}, 'Lorem ipsum'] }
end
But I can't seem to apply it the right way. What am I missing here?
Note that Faraday.new returns an instance of Faraday::Connection, not Faraday. So you can try using
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return("some data")
Note that I don't know if returning "some data" as shown in your question is correct, because Faraday::Connection.get should return a response object, which would include the body and status code instead of a string. You might try something like this:
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double("response", status: 200, body: "some data")
)
Here's a rails console that shows the class you get back from Faraday.new
$ rails c
Loading development environment (Rails 4.1.5)
2.1.2 :001 > fara = Faraday.new
=> #<Faraday::Connection:0x0000010abcdd28 #parallel_manager=nil, #headers={"User-Agent"=>"Faraday v0.9.1"}, #params={}, #options=#<Faraday::RequestOptions (empty)>, #ssl=#<Faraday::SSLOptions (empty)>, #default_parallel_manager=nil, #builder=#<Faraday::RackBuilder:0x0000010abcd990 #handlers=[Faraday::Request::UrlEncoded, Faraday::Adapter::NetHttp]>, #url_prefix=#<URI::HTTP:0x0000010abcd378 URL:http:/>, #proxy=nil>
2.1.2 :002 > fara.class
=> Faraday::Connection
Coming to this late, but incase anyone else is too, this is what worked for me - a combination of the approaches above:
let(:json_data) { File.read Rails.root.join("..", "fixtures", "ror", "501100000267.json") }
before do
allow_any_instance_of(Faraday::Connection).to receive(:get).and_return(
double(Faraday::Response, status: 200, body: json_data, success?: true)
)
end
Faraday the class has no get method, only the instance does. Since you are using this in a class method what you can do is something like this:
class Judge
def self.stats
connection.get "some-domain-dot-com/stats"
end
def self.connection=(val)
#connection = val
end
def self.connection
#connection ||= Faraday.new(some stuff to build up connection)
end
end
Then in your test you can just set up a double:
let(:connection) { double :connection, get: nil }
before do
allow(connection).to receive(:get).with("some-domain-dot-com/stats").and_return('some data')
Judge.connection = connection
end
I ran into the same problem with Faraday::Adapter::Test::Stubs erroring with Faraday does not implement: get. It seems you need to set stubs to a Faraday adapter, like so:
stubs = Faraday::Adapter::Test::Stubs.new do |stub|
stub.get("some-domain-dot-com/stats") { |env| [200, {}, 'egg'] }
end
test = Faraday.new do |builder|
builder.adapter :test, stubs
end
allow(Faraday).to receive(:new).and_return(test)
expect(Judge.stats.body).to eq "egg"
expect(Judge.stats.status).to eq 200
A better way to do this, rather than using allow_any_instance_of, is to set the default connection for Faraday, so that Faraday.get will use the connection you setup in your tests.
For example:
let(:stubs) { Faraday::Adapter::Test::Stubs.new }
let(:conn) { Faraday.new { |b| b.adapter(:test, stubs) } }
before do
stubs.get('/maps/api/place/details/json') do |_env|
[
200,
{ 'Content-Type': 'application/json' },
{ 'result' => { 'photos' => [] } }.to_json
]
end
Faraday.default_connection = conn
end
after do
Faraday.default_connection = nil
end

How Do I Get Response Headers From ActiveResource?

I'm using ActiveResource 4.0, and I need to get pagination working. I've set the response headers on the server end, but I cannot read them on the client end.
I'm using this great blog post:
http://javiersaldana.com/2013/04/29/pagination-with-activeresource.html
And I'm trying to read the headers from the response:
ActiveResource::Base.connection.response
But I'm getting this error:
undefined method 'response' for #<ActiveResource::Connection:0x007f9a4f9692b8>
How can I get the response headers?
gem "activeresource-response" to the rescue .
https://github.com/Fivell/activeresource-response
Example, lets say server returns headers for pagination
X-limit,X-offset,X-total
class Order < ActiveResource::Base
self.format = :json
self.site = 'http://0.0.0.0:3000/'
self.element_name = "order"
add_response_method :http_response # our new method for returned objects
end
class OrdersController < ApplicationController
def index
orders = Order.all(:params=>params)
#orders = Kaminari::PaginatableArray.new(
orders,{
:limit => orders.http_response['X-limit'].to_i,
:offset =>orders.http_response['X-offset'].to_i,
:total_count => orders.http_response['X-total'].to_i
})
end
end

HTTParty options parameter not functioning properly

I'm setting up an application which can make LastFM API Requests.
These are simple get requests and I'm using the HTTParty gem.
My function is as follows:
def get_albums
self.class.base_uri "http://ws.audioscrobbler.com/2.0/"
options = {
:user => "Gerard1992",
:method => "user.gettopalbums",
:api_key => Constants::LASTFM_API_KEY,
:format => "json"
}
puts options.to_query
self.class.get "/?#{options.to_query}", {} #options don't work
end
This piece of code that's shown above works. The get request returns a set of JSON. My problem is that this /?#{options.to_query} doesn't look that neat. And neither does the actual (now empty {}) options parameter. How do I get the HTTParty options parameter to work like it should?
This is what I've tried, but both cases failed:
self.class.get "/", options
self.class.get "/", options => options
I appreciate the help.
The correct option for query parameters in HTTParty is :query, so what you want is:
self.class.get "/", query: options
You can see all the available parameters in the docs.
Send :verify => false in options hash

Resources