Pretty print webservice response on ruby - ruby-on-rails

I have to call a webservice but I don't know the format of the response. In any case (xml, json or html) I have to pretty print the response.
For example, if it is a xml I have to indent and show it properly. Same thing if it is a json. I have two problems here:
Detecting the format
Apply a format depending on the type.
I think that (1) is the most challenging problem.
Any help?

As several of the comments have suggested, the http header will contain the content type.
net/http has methods for this: http://ruby-doc.org/stdlib-2.0.0/libdoc/net/http/rdoc/Net/HTTP.html#method-i-head
require 'net/http'
require 'json'
require 'rexml/document'
response = nil
Net::HTTP.start('www.google.com', 80) {|http|
response = http.get('/index.html')
}
header = response['content-type'].split(';').first # => "text/html"
body = response.read_body
then you can conditionally operate:
if header == "text/html"
puts response.read_body
elsif header == "application/json"
puts JSON.pretty_generate(JSON.parse(body))
elsif header == "text/xml"
xml = REXML::Document.new body
out = ""
xml.write(out, 1)
puts out
end
Most of this was pulled form other SO posts:
pretty JSON: How can I "pretty" format my JSON output in Ruby on Rails?
pretty XML: How to beautify xml code in rails application

This is the code that I finally used:
raw_response = response.body
response_html = ''
if response.header['Content-Type'].include? 'application/json'
tokens = CodeRay.scan(raw_response, :json)
response_html = tokens.div
elsif response.header['Content-Type'].include? 'application/xml'
tokens = CodeRay.scan(raw_response, :xml)
response_html = tokens.div
elsif response.header['Content-Type'].include? 'text/html'
tokens = CodeRay.scan(raw_response, :html)
response_html = tokens.div
else
response_html = '<div>' + raw_response + '</div>'
end
It's using the coderay gem.

Related

FTX.com REST API POST Authentication FAILS with Ruby on Rails and net/https

Hoping for some help as this one has me baffled...
I created a user account and API credentials at FTX.com.
They have an interesting Auth setup which is detailed here: https://docs.ftx.com/?python#authentication
They only provide code examples for python, javascript and c#, but I need to implement the integration on a RoR app.
Here's a link which also provides an example for both GET and POST calls: https://blog.ftx.com/blog/api-authentication/
I'm using:
ruby '3.0.1'
gem 'rails', '~> 6.1.4', '>= 6.1.4.1'
also,
require 'uri'
require 'net/https'
require 'net/http'
require 'json'
I got the authentication working for GET calls as follows:
def get_market
get_market_url = 'https://ftx.com/api/markets/BTC-PERP/orderbook?depth=20'
api_get_call(get_market_url)
end
def api_get_call(url)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}GET/api/markets"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
rsp = http.get(uri, headers)
JSON.parse(rsp.body)
end
This works great and I get the correct response:
=>
{"success"=>true,
"result"=>
{"bids"=>
[[64326.0, 2.0309],
...
[64303.0, 3.1067]],
"asks"=>
[[64327.0, 4.647],
...
[64352.0, 0.01]]}}
However, I can't seem to authenticate correctly for POST calls (even though as far as I can tell I am following the instructions correctly). I use the following:
def create_subaccount
create_subaccount_url = 'https://ftx.com/api/subaccounts'
call_body =
{
"nickname": "sub2",
}.to_json
api_post_call(create_subaccount_url, call_body)
end
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s
}
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri, headers)
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
Also tried passing headers via request[] directly:
def api_post_call(url, body)
ts = (Time.now.to_f * 1000).to_i
signature_payload = "#{ts}POST/api/subaccounts#{body}"
key = ENV['FTX_API_SECRET']
data = signature_payload
digest = OpenSSL::Digest.new('sha256')
signature = OpenSSL::HMAC.hexdigest(digest, key, data)
uri = URI.parse(url)
http = Net::HTTP.new(uri.host, uri.port)
http.read_timeout = 1200
http.use_ssl = true
request = Net::HTTP::Post.new(uri)
request['FTX-KEY'] = ENV['FTX_API_KEY']
request['FTX-SIGN'] = signature
request['FTX-TS'] = ts.to_s
request.body = body
response = http.request(request)
JSON.parse(response.body)
end
This is the error response:
=> {"success"=>false, "error"=>"Not logged in: Invalid signature"}
My feeling is the issue is somewhere in adding the body to signature_payload before generating the signature via HMAC here..?:
signature_payload = "#{ts}POST/api/subaccounts#{body}"
Thinking this because, if I leave out #{body} here, like so:
signature_payload = "#{ts}POST/api/subaccounts"
the response is:
=> {"success"=>false, "error"=>"Missing parameter nickname"}
I have tried several iterations of setting up the POST call method using various different net/https examples but have had no luck...
I have also contacted FTX support but have had no response.
Would truly appreciate if anyone has some insight on what I am doing wrong here?
try this headers
headers = {
'FTX-KEY': ENV['FTX_API_KEY'],
'FTX-SIGN': signature,
'FTX-TS': ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
Here's a working example of a class to retrieve FTX subaccounts. Modify for your own purposes. I use HTTParty.
class Balancer
require 'uri'
require "openssl"
include HTTParty
def get_ftx_subaccounts
method = 'GET'
path = '/subaccounts'
url = "#{ENV['FTX_BASE_URL']}#{path}"
return HTTParty.get(url, headers: headers(method, path, ''))
end
def headers(*args)
{
'FTX-KEY' => ENV['FTX_API_KEY'],
'FTX-SIGN' => signature(*args),
'FTX-TS' => ts.to_s,
'Content-Type' => 'application/json',
'Accepts' => 'application/json',
}
end
def signature(*args)
OpenSSL::HMAC.hexdigest(digest, ENV['FTX_API_SECRET'], signature_payload(*args))
end
def signature_payload(method, path, query)
payload = [ts, method.to_s.upcase, "/api", path].compact
if method==:post
payload << query.to_json
elsif method==:get
payload << ("?" + URI.encode_www_form(query))
end unless query.empty?
payload.join.encode("UTF-8")
end
def ts
#ts ||= (Time.now.to_f * 1000).to_i
end
def digest
#digest ||= OpenSSL::Digest.new('sha256')
end
end

JSON parse javascript string

Trying to parse the json string with JSON.parse I get unexpected token error
require "net/http"
require "uri"
url = URI.parse("https://url-goes-here")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
req = Net::HTTP::Get.new(url.request_uri)
req['Accept'] = 'application/json'
res = Net::HTTP.new(url.host, url.port).start do |http|
http.request(req)
end
json = JSON.parse(res)
puts json
Data in response looks like JSONP, rather old method of doing rpc. Instead of returning plain JSON it outputs some_js_callback_function_name({here_goes: the_json}), usually there's also a parameter that controls the function name.
To get json from it - trim the function call before parsing:
json_data = res.body.gsub(/\A[^(]+\(/, '').gsub(/\)\s*\z/, '')
json = JSON.parse(json_data)

Issue looping through a hash, checking for value and saving to empty hash

I'm attempting pull json data from an api, iterate through a hash of JSON data, append the new data to an empty hash and then check the pulled data for whether the key 'next_page' has any value..breaking the loop if the value is returned nill. I'm pulling the first batch of data from the api successfully, but my loop seems to be incorrect as no data is being inserted into the empty hash i've created.
Any ideas would be deeply appreciated. Thanks for giving my question a read!
require 'net/http'
require 'uri'
require 'json'
#imports APR User data from the zendesk api and populates the database with it.
uri = URI.parse("https://tester.zendesk.com/api/v2/users.json")
request = Net::HTTP::Get.new(uri)
request.content_type = "application/json"
request.basic_auth("cbradford#tester.com", "tester32")
req_options = {
use_ssl: uri.scheme == "https",
}
#response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
http.request(request)
end
puts #response.body
puts #response.message
puts #response.code
res = #response.body
users = res["users"]
data = {}
if users["next_page"]
newUri = users.fetch('next_page')
uriLoop = URI.parse(newUri)
request = Net::HTTP::Get.new(uriLoop)
request.content_type = "application/json"
request.basic_auth("cbradford#tester.com", "tester32")
req_options = {
use_ssl: uriLoop.scheme == "https",
}
#responseLoop = Net::HTTP.start(uriLoop.hostname, uriLoop.port, req_options) do |http|
http.request(request)
end
resLoop = JSON.parse(#responseLoop.body)
puts resLoop
users = resLoop["users"]
data.concat(users)
end
puts data
puts "hash created Successfully!"
You always omit the first fetched users. Before checking for the presence of next_page you never store the fetched users, and thus data remains empty.
See here:
users = res["users"]
data = {} ## ---data is initialised but never filled?
if users["next_page"]
Secondly I suspect that res["users"] is an array, and since you write data.concat(users) later that actually data should also be an array.
So your code should be fixed be replacing the above mentioned three lines as follows:
data = res["users"]
if users["next_page"]

Refactorting if-else blocks into more concise code

I have a bunch of if-elsif blocks like this that end in a else statement so my structure looks like this:
if path.end_with?('something')
template_name = 'something.json.erb'
res.body = ERB.new(File.read(File.expand_path("../#{template_name}", __FILE__))).result(binding)
res.status = 200
res['Content-Type'] = 'application/json'
elsif path.end_with?('somethingELSE')
template_name = 'somethingELSE.json.erb'
res.body = ERB.new(File.read(File.expand_path("../#{template_name}", __FILE__))).result(binding)
res.status = 200
res['Content-Type'] = 'application/json'
# a couple more similar if-elsif blocks in here
else
res.status = 400
res['Content-Type'] = 'text/plain'
res.body = "Invalid path"
So there is a lot of repeated code in the section that has blocks of if-elsif that are just repeared. Basically just the line that sets the template_name is necessary and we should be able to factore out the rest of those next three lines but then I have that else at the end that prevents me from doing that.
How would you suggest to refactor this code to be more concise and less repeated code in it?
['something', 'somethingELSE', 'somethingAGAIN'].each DO |match|
substitute = match if path.end.with?(match)
end
if substitute
template_name = "#{substitute}.json.erb"
res.body = ERB.new(File.read(File.expand_path("../#{template_name}", __FILE__))).result(binding)
res.status = 200
res['Content-Type'] = 'application/json'
else
res.status = 400
res['Content-Type'] = 'text/plain'
res.body = "Invalid path"
end
Below is one way to do it.
if path.end_with?('something') || path.end_with?('somethingELSE')
if path.end_with?('something')
template_name = 'something.json.erb'
elsif path.end_with?('somethingELSE')
template_name = 'somethingELSE.json.erb'
# a couple more similar if-elsif blocks in here
end
res.body = ERB.new(File.read(File.expand_path("../#{template_name}", __FILE__))).result(binding)
res.status = 200
res['Content-Type'] = 'application/json'
else
res.status = 400
res['Content-Type'] = 'text/plain'
res.body = "Invalid path"
end
You could probably also parse the something and somethingELSE off of your path to get the template name, simplifying this even further.
Assuming you've got a proper path, a/path/to/something you could do:
template_name = "/assuming/this/is/a/path".split('/').last + '.json.erb'
But I can't say without seeing your other conditionals.

Ruby, forming API request without implicitly stating each parameter

I'm trying to make a request to a web service (fwix), and in my rails app I've created the following initializer, which works... sorta, I have two problems however:
For some reason the values of the parameters need to have +'s as the spaces, is this a standard thing that I can accomplish with ruby? Additionally is this a standard way to form a url? I thought that spaces were %20.
In my code how can I take any of the options sent in and just use them instead of having to state each one like query_items << "api_key=#{options[:api_key]}" if options[:api_key]
The following is my code, the trouble area I'm having are the lines starting with query_items for each parameter in the last method, any ideas would be awesome!
require 'httparty'
module Fwix
class API
include HTTParty
class JSONParser < HTTParty::Parser
def json
JSON.parse(body)
end
end
parser JSONParser
base_uri "http://geoapi.fwix.com"
def self.query(options = {})
begin
query_url = query_url(options)
puts "querying: #{base_uri}#{query_url}"
response = get( query_url )
rescue
raise "Connection to Fwix API failed" if response.nil?
end
end
def self.query_url(input_options = {})
#defaults ||= {
:api_key => "my_api_key",
}
options = #defaults.merge(input_options)
query_url = "/content.json?"
query_items = []
query_items << "api_key=#{options[:api_key]}" if options[:api_key]
query_items << "province=#{options[:province]}" if options[:province]
query_items << "city=#{options[:city]}" if options[:city]
query_items << "address=#{options[:address]}" if options[:address]
query_url += query_items.join('&')
query_url
end
end
end
For 1)
You API provider is expecting '+' because the API is expecting in a CGI formatted string instead of URL formatted string.
require 'cgi'
my_query = "hel lo"
CGI.escape(my_query)
this should give you
"hel+lo"
as you expect
for Question 2) I would do something like
query_items = options.keys.collect { |key| "#{key.to_s}=#{options[key]}" }
def self.query_url(input_options = {})
options = {
:api_key => "my_api_key",
}.merge(input_options)
query_url = "/content.json?"
query_items = []
options.each { |k, v| query_items << "#{k}=#{v.gsub(/\s/, '+')}" }
query_url += query_items.join('&')
end
I'm a developer at Fwix and wanted to help you with your url escaping issue. However, escaping with %20 works for me:
wget 'http://geoapi.fwix.com/content.xml?api_key=mark&province=ca&city=san%20francisco&query=gavin%20newsom'
I was hoping you could provide me with the specific request you're making that you're unable to escape with %20.

Resources