Automatically encode Rack output with JSON when Content-Type is application/json - ruby

I've got a modular Sinatra app and I'd like to encode the output as JSON when the content-type dictates. At the moment I'm doing so manually in my routes:
get 'someroute' do
# content-type is actually set with a before filter
# included only for clarity
content_type 'application/json', :charset => 'utf-8'
# .. #
{:success=>true}.to_json
end
I'd prefer it to look like this:
get 'someroute' do
content_type 'application/json', :charset => 'utf-8'
# .. #
{:success=>true}
end
And I'd like to use Rack middleware to do the encoding if it detects the appropriate content-type.
I've been trying to get the following to work, but to no avail (the content-length gets borked - returns content-length of original content and not JSON encoded content):
require 'init'
module Rack
class JSON
def initialize app
#app = app
end
def call env
#status, #headers, #body = #app.call env
if json_response?
#body = #body.to_json
end
[#status, #headers, #body]
end
def json_response?
#headers['Content-Type'] == 'application/json'
end
end
end
use Rack::JSON
MyApp.set :run, false
MyApp.set :environment, ENV['RACK_ENV'].to_sym
run MyApp
Any pointers to get me back on track?

You’ve got everything right but one thing: Rack expects the body to be an object that responds to each but is not a string. Just put your body inside an array.
It’s probably not necessary, but if you want to set the content length by hand, just add it to the headers.
if json_response?
#body = [#body.to_json]
#headers["Content-Length"] = #body[0].size.to_s
end

Related

Httparty - appending to url and adding headers to get request via Ruby class

I'm currently working with Httparty to make a GET Seamless.giv API which returns field from a form. In the requests there are Authentication headers that need to be passed in order to access the API. But the request has to be made to a specific form. Thats where the issue lays should this be in the base URI or appended?
Here is the curl example of the request:
curl -X GET -H "AuthDate: 1531236159"\
-H "Authorization: HMAC-SHA256 api_key=XXXXXXXX nonce=12345 Signature=XXXXXXXXXXX"\
-d 'false' https://nycopp.seamlessdocs.com/api/form/:form_id/elements
and this is the approach im currently taking:
class SeamlessGov
include HTTParty
base_uri "https://nycopp.seamlessdocs.com/api"
def initialize(args={})
#api_key = args[:api_key]
#nonce = args[:nonce]
#signature = generate_signature
end
def form(form_id)
#form = form_id
end
def headers(headers={})
#headers = headers
end
def generate_signature
# bash command
end
end
Is the best practice to append it or put it in the base_uri for example:
base_uri "https://nycopp.seamlessdocs.com/api/form/:form_id/elements" or created a method to append to the base_uri for example:
def append_form(form)
"/form/#{form}/elements"
end
What would the best approach be? So that when I call
#form = SeamlessGov.new(params, headers) works.
If I understand what you're asking correctly, you would write a method like:
def create_form
get("/form/#{form_id}/elements", headers)
end
Which you can then call like:
#form = SeamlessGov.new(params, headers).create_form

Ruby/Sinatra - How can I call post in lambda class?

I'm make a little program in sinatra and I'm wanted to perfom some dynamic call of post, with diynamic uri, so I make a Connexion class like this:
class Connexion
def initialize(path)
#path = path
end
def sinatraPost
post "/#{#path}" do
# some code
end
end
end
But when I'm launch sinatraPost, I've got this error:
undefined method `post' for #<Connexion:0x000000026206b8> (NoMethodError)
How can I call the sinatra post method in my class ?
EDIT: Okay ! So, I change my strategy, I have this following code:
class Webhook < Sinatra::Base
get '/:name' do
# compare with names array
end
end
Webhook.run!
Thank's to everyone !
It looks like you're going about this the wrong way. If you want to set up your app to receive a POST request, you'll need routing logic in your controller. Sinatra controllers normally look like this:
require 'sinatra'
get '/route1' do
# do stuff
end
post '/route2' do
# do stuff
end
If you're using a modular app, you'll want to have your app inherit from Sinatra::Base. See the Sinatra docs for more.
Making a post request is different, and doesn't rely on Sinatra methods.
require 'net/http'
uri = URI("http://google.com")
headers = {}
http = Net::HTTP.new(uri.host, uri.port)
request = Net::HTTP::Post.new(uri.request_uri, headers)
response = http.request(request)
Or something like that. Good luck!

Rack middleware server crash

I am trying to write simple rack middleware like this:
class NewMiddleWare
NEW_STRING = <<BEGIN
my new content
BEGIN
def initialize(app)
#app = app
end
def call(env)
status, headers, response = #app.call(env)
response_body = ""
response.each {|part| response_body += part}
response_body += "#{NEW_STRING}"
headers["Content-Length"] = response_body.length.to_s
[status, headers, response_body]
end
end
when I run rackup, I got:
Unexpected error while processing request: undefined methodeachfor #<String:0x007fad313bdb30> in terminal.
I couldn't figure out the reason for this error. Response object
should be able to respond each right? Coz I saw some sample code doing
that.
From the Rack spec:
The Body must respond to each and must only yield String values. The Body itself should not be an instance of String, as this will break in Ruby 1.9.
In Ruby 1.8 Strings did respond to each, but that changed in 1.9.
The simplest solution would be to just return an array containing the string:
[status, headers, [response_body]]

Sinatra JSON.parse undefined method when moving helper to another file

I have a route helper that works fine when it's in a "helpers do" block in my route file, but when I move the helper to my helpers.rb file, I get an error. Here is the helper:
def processPutRequest(patient_id, request, _class, foreign_key, route_fragment)
p = Patient.find(patient_id)
data = request.body.read
puts 'request body' + data
data = JSON.parse(data)
host_with_port = request.host_with_port
puts 'hash data: ' + data.inspect
saveListData(params[:id], data, _class, foreign_key)
url = {'url' => "http://#{host_with_port}/patients/#{params[:id]}#{route_fragment}"}.to_json
Rack::Response.new(url)
end
Here is the logging when the helper is in the route file:
request body{"list_data":[{"id":8440,"value":"Removal of ear wax (procedure)"},{"id":9827,"value":"Foreign body in nose (disorder)"}]}
hash data: {"list_data"=>[{"id"=>8440, "value"=>"Removal of ear wax (procedure)"}, {"id"=>9827, "value"=>"Foreign body in nose (disorder)"}]}
And when I move it to the helpers file:
request body{"list_data":[{"id":8440,"value":"Removal of ear wax (procedure)"},{"id":9827,"value":"Foreign body in nose (disorder)"}]}
NoMethodError - undefined method `parse' for Sinatra::JSON:Module:
Am I missing something really simple?
EDIT, got the debugger working. "data" after request.body.read in routes file:
"{"list_data":[{"id":8440,"value":"Removal of ear wax (procedure)"},{"id":9827,"value":"Foreign body in nose (disorder)"}]}"
in helpers file:
"{"list_data":[{"id":8440,"value":"Removal of ear wax (procedure)"},{"id":9827,"value":"Foreign body in nose (disorder)"}]}"
So the content looks identical to me. I can literally cut and paste this method between the two files, it works fine in the routes file, fails with undefined method parse in the helpers file. I'm guessing I've somehow defined that module improperly or have a dandling or missing character, but RubyMine is showing no errors, and the method does at least partially execute, so the method is getting sourced.
Complete helpers file:
module Sinatra
module DynFormat
CONTENT_TYPES={'xml' => 'text/xml','json' => 'application/json'}
def dynamicFormat(data,format=params[:format])
content_type CONTENT_TYPES[format], :charset => 'utf-8'
case format
when 'xml'
data.to_xml
when 'json'
data.to_json
end
end
end
helpers DynFormat
module RouteHelpers
def processPutRequest(patient_id, request, _class, foreign_key, route_fragment)
p = Patient.find(patient_id)
data = request.body.read
puts 'request body' + data
data = JSON.parse(data)
host_with_port = request.host_with_port
puts 'hash data: ' + data.inspect
saveListData(params[:id], data, _class, foreign_key)
url = {'url' => "http://#{host_with_port}/patients/#{params[:id]}#{route_fragment}"}.to_json
Rack::Response.new(url)
end
def saveListData(patient_id, list_data, _class, foreign_key)
p = Patient.find(patient_id)
_class = eval _class
list_data = list_data['list_data']
list_data.each do |x|
_class.create(:patient_id => patient_id, foreign_key => x['id'] )
end
end
end
helpers RouteHelpers
end
When you put your method in the external file, you are putting into a module under the Sinatra namespace:
module Sinatra
module DynFormat
#...
end
module RouteHelpers
def processPutRequest(patient_id, request, _class, foreign_key, route_fragment)
# code that refers to JSON...
end
end
end
When your method calls JSON.parse it ends up finding the Sinatra::JSON module, not the top level JSON module that you intended, and so you get the error undefined method `parse' for Sinatra::JSON:Module.
When you include the method in a helpers block in your main file the method isn’t defined under the Sinatra module, so JSON refers to the correct top level module.
If you need to include Sinatra::JSON, you can explicitly refer to the top level module using :::
data = ::JSON.parse(data)
Also note that you don’t necessarily need to define your helper modules under Sinatra (though the docs do suggest you do). You can move them out of the Sinata module, and then use e.g. Sinatra.helpers RouteHelpers to register them as helpers.
I was requiring both json and sinatra/json. No idea why the last one didn't win all the time, but removing sinatra/json resolved the issue. If something else breaks, I'll spend the time deciding between the two.

How to get ALL of the URL parameters in a Sinatra app

Using the following Sinatra app
get '/app' do
content_type :json
{"params" => params}.to_json
end
Invoking:
/app?param1=one&param2=two&param2=alt
Gives the following result:
{"params":{"param1":"one","param2":"alt"}}
Params has only two keys, param1 & param2.
I understand Sinatra is setting params as a hash, but it does not represent all of the URL request.
Is there a way in Sinatra to get a list of all URL parameters sent in the request?
Any request in rack
get '/app' do
params = request.env['rack.request.query_hash']
end
I believe by default params of the same name will be overwritten by the param that was processed last.
You could either setup params2 as an array of sorts
...&param2[]=two&param2[]=alt
Or parse the query string vs the Sinatra provided params hash.
kwon suggests to parse the query string.
You can use CGI to parse it as follows:
require 'cgi'
get '/app' do
content_type :json
{"params" => CGI::parse(request.query_string)}.to_json
end
Invoking:
/app?param1=one&param2=two&param2=alt
Gives the following result:
{"params":{"param1":["one"],"param2":["two","alt"]}}
You can create a helper to make the process more friendly:
require 'cgi'
helpers do
def request_params_repeats
params = {}
request.env["rack.input"].read.split('&').each do |pair|
kv = pair.split('=').map{|v| CGI.unescape(v)}
params.merge!({kv[0]=> kv.length > 1 ? kv[1] : nil }) {|key, o, n| o.is_a?(Array) ? o << n : [o,n]}
end
params
end
end
You can then access the parameters in your get block:
get '/app' do
content_type :json
request_params_repeats.to_json
end

Resources