Why can't I post variables using HTTParty? - ruby

I'm trying to use HTTParty to manage requests to an API. These are the instructions from the documentation:
url: https://www.havelockinvestments.com/r/orderbook
Required Post Variables symbol: "VTX"
Return Data
status: Contains 'ok' or 'error'
message: Contains error message if applicable
bids: Contains Bids array, as price=>amount pairs
asks: Contains Asks array, as price=>amount pairs
This is what I'm putting in my Ruby script:
require 'httparty'
response = HTTParty.post(
'https://www.havelockinvestments.com/r/orderbook',
:query => { :symbol => "VTX" }
)
But I'm getting an error response:
{"status":"error","message":"post:symbol is required"}
What am I doing wrong here when posting the symbol variable?
The original documentation is at: https://www.havelockinvestments.com/apidoc.php

Documentation seems a bit sparse on the HTTParty Github page, but from the examples it looks like you specify the parameters in a hash as a value to the :body key in the options for HTTParty#post
Like so:
response = HTTParty.post('https://www.havelockinvestments.com/r/orderbook', {body: {symbol: "VTX"}})

Related

With octokit.rb (Ruby library for GitHub API) how do you set Accept: header in post request (for API Previews)?

When using Octokit to do post request to the graphql endpoint, how do you set custom Accept: headers in order to turn on a "preview" API?
I need to do a GraphQL query that is part of an API preview, so it requires me to set the Accept header to application/vnd.github.starfox-preview+json (see GitHub API documentation).
I am using Octokit (4.15.0) in a Rails project, and am successfully doing normal queries (not preview mode) using both the v3 and v4 (GraphQL) APIs.
For normal queries (not API preview), my code looks like this, and works flawlessly. Here, the method graphql_query_string forms the correct query string, and github_machine_user is an instance of an Octokit Client object.
Code that works (when custom Accept: header not needed)
def perform_graphql_query(repo_name, org_name, after="")
graphql_query_string = graphql_query(repo_name, org_name, after)
options = { query: graphql_query_string }.to_json
github_machine_user.post '/graphql', options
end
This results in the default Accept header being used, which is, apparently "application/vnd.github.v3+json" (more on how I know this below.)
I tried several ways of adding a custom Accept: header, but none of them work. Here's what I tried:
Unsuccessful Attempt #1:
def perform_graphql_query(repo_name, org_name, after="")
graphql_query_string = graphql_query(repo_name, org_name, after)
options = {:query => graphql_query_string,
:headers => {:accept => "application/vnd.github.starfox-preview+json"}
}.to_json
github_machine_user.post('/graphql', options)
end
Unsuccessful Attempt #2:
def perform_graphql_query(repo_name, org_name, after="")
graphql_query_string = graphql_query(repo_name, org_name, after)
options = {
:query => graphql_query_string,
:accept => "application/vnd.github.starfox-preview+json"
}.to_json
github_machine_user.post('/graphql', options)
end
Neither of theses sets the proper header. From the source code for the post method and the underlying request method, I would have expected this to work. Here's what those look like.
# Make a HTTP POST request
#
# #param url [String] The path, relative to {#api_endpoint}
# #param options [Hash] Body and header params for request
# #return [Sawyer::Resource]
def post(url, options = {})
request :post, url, options
end
def request(method, path, data, options = {})
if data.is_a?(Hash)
options[:query] = data.delete(:query) || {}
options[:headers] = data.delete(:headers) || {}
if accept = data.delete(:accept)
options[:headers][:accept] = accept
end
end
Is it me, or a bug in Octokit?
I can tell the header isn't being set not just because I get results indicating Field 'project' doesn't exist on type 'AddedToProjectEvent', because I also followed the advice in the Octokit documentation about turning on debugging info and I can see the headers on my request.
Here's how they appear (other than the redacted credential)
I, [2020-07-24T12:26:37.989030 #64350] INFO -- request: POST https://api.github.com/graphql
I, [2020-07-24T12:26:37.989109 #64350] INFO -- request: Accept: "application/vnd.github.v3+json"
User-Agent: "Octokit Ruby Gem 4.15.0"
Content-Type: "application/json"
Authorization: "token REDACTED_FOR_STACK_OVERFLOW_POST"
So my requests to set the accept header are not being respected.
I did verify that using curl, I was able to get the query to work properly by passing the correct Accept header. So the graphQl query is properly formed, and when the Accept header is properly put into the query, everything is fine. But I just can't seem to figure out how to get Octokit to respect my wishes in setting the Accept header. I've even looked into the source, and it seems like what I'm doing should work.
Can anyone help?
Update: Posted as an issue on Octokit's GitHub Repo too.
Update #2: I tried removing the to_json. Unfortunately, that gives a stack trace as shown below.
If I remove the to_json and then only pass the {:accept => "application/vnd.github.starfox-preview+json"} then it does set the correct header. But if try including :query in that hash, it results in the error below, unless I have the to_json on the hash. I just can't seem to win.
NoMethodError undefined method `each' for #<String:0x00007fb05be10b00>
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/utils/params_hash.rb:28:in `update'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/sawyer-0.8.2/lib/sawyer/agent.rb:99:in `block in call'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/connection.rb:489:in `block in run_request'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/connection.rb:506:in `block in build_request'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/request.rb:55:in `block in create'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/request.rb:54:in `tap'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/request.rb:54:in `create'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/connection.rb:502:in `build_request'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/connection.rb:484:in `run_request'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/faraday-1.0.0/lib/faraday/connection.rb:279:in `post'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/sawyer-0.8.2/lib/sawyer/agent.rb:94:in `call'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/octokit-4.18.0/lib/octokit/connection.rb:156:in `request'
/Users/pconrad/.rvm/gems/ruby-2.6.5/gems/octokit-4.18.0/lib/octokit/connection.rb:28:in `post'
/Users/pconrad/github/project-anacapa/anacapa-github-linker/app/jobs/course/course_github_repo_get_sdlc_events.rb:105:in `perform_graphql_query'
Update #3: Added my own answer below but it seems hacky. I had to call a private method, which one is not supposed to do, and definitely not supposed to "have to do". So, still hoping for a better solution.
This is what finally worked. I had to get "dirty" and invoke a private method using .send which is not ideal.
graphql_query_string = graphql_query(repo_name, org_name, after).gsub("\n","")
data = {
:query => graphql_query_string,
}.to_json
options = {
:headers => {
:accept => Octokit::Preview::PREVIEW_TYPES[:project_card_events]
}
}
github_machine_user.send :request, :post, '/graphql', data,options
If there is a better way, I'd welcome the suggestion. I agree with the other posters that said it doesn't make any sense to pass to_json on the data part, but without it, I get the stack trace about each not being defined on a string.
Remove calling #to_json on options. You're passing a String into #post as options. options gets put into #request as data. #request will do its thing on data if it was a Hash but since it's a String, it won't populate the new options that is passed to the client in #request.

How do I PUT a file with Typheous?

I am trying to send a file via a HTTP PUT request. Curl allows this like:
http://curl.haxx.se/docs/httpscripting.html#PUT
What's the correct way of doing this with Typheous?
FWIW this is what I think was the complete (but not necessarily shortest) answer to the question.
Curl allows the uploading of files w/ PUT; the invocation is:
$ curl --upload-file filename url
where url may be something like:
http://someurl/script.php?var=value&anothervar=val&...
Typhoeus provides the same functionality, but the right way to pass url, params and their values as well as the file body is buried in the ethon docs:
request = Typhoeus::Request.new(
url, :method => :put, :params => params_hash,
:body => File.open(filename) { |io| io.read })
Use request object to get response, etc.
You couldn't have looked very hard:
Examples:
Make put request.
Typhoeus.put("www.example.com")
Parameters:
base_url (String) — The url to request.
options (options) (defaults to: {}) — The options.
Options Hash (options):
:params (Hash) — Params hash which is attached to the base_url.
:body (Hash) — Body hash which becomes a PUT request body.
http://rubydoc.info/github/typhoeus/typhoeus/Typhoeus/Request/Actions#put-instance_method

How to test a JSON REST API

I'm brand new to ruby (first day working with ruby) so please forgive any novice questions and lack of understanding.
I'm trying to validate the responses to http callouts.
For example, let's say the endpoint is the following:
https://applicationname-api-sbox02.herokuapp.com
And, I'm trying to authenticate a user by sending a get request like this:
get_response = RestClient.get( "https://applicationname-api-sbox02.herokuapp.com/api/v1/users",
{
"Content-Type" => "application/json",
"Authorization" => "token 4d012314b7e46008f215cdb7d120cdd7",
"Manufacturer-Token" => "8d0693ccfe65104600e2555d5af34213"
}
)
Now, I want to validate the response and do the following:
- parse the response to ensure that it is valid JSON
- do some validation and verify the JSON has the correct data (verify that id == 4 for example)
- if an error is encountered, raise an exception using the 'raise' method.
In my first feeble attempt I tried the following:
puts get_response.body
if get_response.code == 200
puts "********* Get current user successful"
else
puts "Get current user failed!!"
end
Now, this returned that getting the current user was successful, but how do I actually parse the json, verify the correct id, and raise an exception if an error occurred?
Instead of raising an exception, write a test.
A straightforward approach, using the json parser and unit test framework from the std lib:
require 'minitest/autorun'
require 'rest_client'
require 'json'
class APITest < MiniTest::Unit::TestCase
def setup
response = RestClient.get("https://applicationname-api-sbox02.herokuapp.com/api/v1/users",
{
"Content-Type" => "application/json",
"Authorization" => "token 4d012314b7e46008f215cdb7d120cdd7",
"Manufacturer-Token" => "8d0693ccfe65104600e2555d5af34213"
}
)
#data = JSON.parse response.body
end
def test_id_correct
assert_equal 4, #data['id']
end
end
Execute with ruby $filename
JSON.parse parses a JSON string into a ruby hash
Getting started with minitest
If you are using ruby 1.8, you'll need to install the json gem and either install the minitest gem, or switch to the older testunit API. If you choose the latter, then you'll need to change require 'minitest/autorun' -> require 'test/unit' and MiniTest::Unit::TestCase -> Test::Unit::TestCase
I'm a little late to the party, but I recently co-created an rspec driven framework called Airborne for just this purpose. Check it out: https://github.com/brooklynDev/airborne
here is an example from our specs so you can see how we test json api:
it 'returns charge' do
get "/charges/#{charge.id}", '', headers
expect(response.status).to eq(200)
expect(response).to match_response_schema(:charge)
expect(response).to match_json(<<-JSON)
{
"id":"{id}",
"email": "{email}",
"ip": "127.0.0.1",
"amount": 10500,
"state": "captured",
"captured_amount": 10500,
}
JSON
end
Lets look at it closely
match_response_schema(:charge)
This matcher checks that json we get in response is in general valid. We use json-schema (json schema validator) for it. Guys from Thoughtbot have a detailed guide how to use json schema validator and create own matcher in this blog post.
Understanding JSON Schema is where I got a lot of useful information on how to create schemas for JSON documents.
match_json
This is our own matcher and we have released match_json gem recently. Using it you can test structure and values of your json. Here are two great features of this matcher:
if you don't know exact values, you can use patterns like {id}, {uuid} {date_time}, etc. we have predefined patterns but you can add your own too.
you get clear failure message what is wrong with your json e.g. "5" was not found in " > array":[1,2,3]
Parsing json can be done with the json gem: http://flori.github.com/json/
Parsed json is accessed through key/value just like in javascript. You can easily verify the values and conditionally raise errors.
Raising errors is done like so:
raise "the ID was #{id} instead of 4"
And writing unit tests can be done with Test::Unit - http://www.ruby-doc.org/stdlib-1.9.3/libdoc/test/unit/rdoc/Test/Unit.html

Yelp JSON output is not converting to ruby hash

Here is my YELP client using signet but once I get response, I'm not able to convert to ruby hash to inspect response element.
require 'rubygems'
require 'json'
require 'net/http'
client = Signet::OAuth1::Client.new(
:client_credential_key =>
'xxxxxxxxxxxxxxxxxxx',
:client_credential_secret =>
'xxxxxxxxxxxxxxxxxxx',
:token_credential_key =>
'xxxxxxxxxxxxxxxxxxx',
:token_credential_secret =>
'xxxxxxxxxxxxxxxxxxx'
)
response = client.fetch_protected_resource(
:uri => 'http://api.yelp.com/v2/search?term=food&location=san+francisco'
)
# The Rack response format is used here
status, headers, body = response
puts body["businesses"]
Error:
`[]': can't convert String into Integer (TypeError)
Body prints fine in nice JSON format but I can do body["businesses"] for instance
JSON.parse(body).inspect is also not working.
Btw body outputs itself appears as JSON format but JSON.parse(body) doesn't produce hash
puts body
{"region":{"span":{"latitude_delta":0.0,"longitude_delta":0.0},"center":{"latitude":37.660418999999997,"longitude":-121.876508}},"total":853,"businesses":[{"rating":4.0,"mobile_url":"http://m.yelp.com/biz/TT1t4oHeZmqkoiuwgCN4bQ","rating_img_url":"http://media2.ak.yelpcdn.com/static/201012164084228337/img/ico/stars/stars_4.png","review_count":150,"name":"India Garden","rating_img_url_small":"http://media2.ak.yelpcdn.com/static/20101216418129184/img/ico/stars/stars_small_4.png","url":"http://www.yelp.com/biz/india-garden-pleasanton-2","phone":"9254854800","snippet_text":"We went to this place without seeing any reviews while we returning to San Jose from Cache Creek in Brooks. This place looks like a house which was...","image_url":"http://s3-media4.ak.yelpcdn.com/bphoto/8iFj1S9YaU5IdUazwZOG8A/ms.jpg","snippet_image_url":"http://s3-media3.ak.yelpcdn.com/photo/d2TovvsTn2eUw4xqTB4jyw/ms.jpg","display_phone":"+1-925-485-4800","rating_img_url_large":"http://media4.ak.yelpcdn.com/static/20101216169592178/img/ico/stars/stars_large_4.png","id":"india-garden-pleasanton-2","categories":[["Indian","indpak"],["Pakistani","pakistani"]],"location":{"cross_streets":"Main St & Neal St","city":"Pleasanton","display_address":["210 Rose Ave","(b/t Main St & Neal St)","Pleasanton, CA 94566"],"geo_accuracy":8,"postal_code":"94566","country_code":"US","address":["210 Rose Ave"],"coordinate":{"latitude":37.660418999999997,"longitude":-121.876508},"state_code":"CA"}}]}
Actually i'm pretty sure, that body is an Array at this point, since response contains four parts and not three, so the last two parts (an array) are put into the body-object.
Also Array is the only core-object i know, which complains about an [] parameter being not an integer. If it was a string, it would try a regex/contain match.
So to sum up, body is an Array with only one Value containing a String. So to get your Hash (from JSON) you have to real_body = JSON.parse body[0]. Then you should get your hash and
real_body["businesses"] puts your businesses (the output is rather long so i will not be posting it here)
body is a string at this point, not a hash. The [] operator is complaining because the string [] operator only takes an integer, so it's trying to turn your string into an integer and failing.
edit: you can test this by just printing out body.class

How to Send a Multidimensional Array using Rack::Test post

I am sending a multidimensional array using the post method given by the Rack::Test like
post "#{url}.json",:data => [["Company","Website"],["this is the dummy text, with,comma","www.technology.com"],["some company","www.url.com"]]
But in my controller when check my params params[:data] i am receiving it as a single dimensioned array ["Company", "Website", "this is the dummy text, with comma", "www.technology.com", "some company", "www.url.com"]
But i want it to retain it's property as a multi-dimensional array. I need help to do this.
The Rack::Test methods all have the same signature... and the second param is a hash of params
i.e.
post '/path', params={}, rack_env={}
This is because they're just URL params - which are typical key/value structures (i.e. a hash)
Why do you need it to be a multi-dimensional array?
EDIT: oh, I get it - you have a single hash with one key (:data)
If it's still causing you grief you could explicitly call to_param in there
ruby-1.9.2-p180 :003 > h = {:data => [["Company","Website"],["this is the dummy text, with,comma","www.technology.com"],["some company","www.url.com"]]}
=> {:data=>[["Company", "Website"], ["this is the dummy text, with,comma", "www.technology.com"], ["some company", "www.url.com"]]}
ruby-1.9.2-p180 :004 > h.to_param
=> "data[][]=Company&data[][]=Website&data[][]=this+is+the+dummy+text%2C+with%2Ccomma&data[][]=www.technology.com&data[][]=some+company&data[][]=www.url.com"
A workaround if you really need nested arrays is to change the request content type to JSON:
post url, JSON.dump([[1, 2], [3, 4]]), { "CONTENT_TYPE" => "application/json" }
This will correctly send a nested array through to the rack app.
Neither of the above worked too well for me but this did (see my original answer here).
The problem is body is sent as application/x-www-form-urlencoded by default, which doesn't handle multi-dimensional arrays too well. You can send it as application/json, but Sinatra probably won't merge the data into the request params. I use a middleware from rack-contrib which parses a json body from a POST request and merges it for you:
# Gemfile
`gem 'rack-contrib'`
then config.ru:
require 'rack/contrib'
require './app'
use Rack::PostBodyContentTypeParser
run Sinatra::Application
This won't be used in testing by default, but you can specify it:
# spec_helper.rb
OUTER_APP = Rack::Builder.parse_file("config.ru").first
module RSpecMixin
include Rack::Test::Methods
def app
OUTER_APP # typically this might just be Sinatra::Application
end
end
RSpec.configure do |config|
config.include RSpecMixin
end
And example usage:
it 'is ok' do
post '/', { key: 'value' }.to_json, { 'CONTENT_TYPE' => 'application/json' }
expect(last_response).to be_ok
end

Resources