What's the best way to use SOAP with Ruby? - ruby-on-rails

A client of mine has asked me to integrate a 3rd party API into their Rails app. The only problem is that the API uses SOAP. Ruby has basically dropped SOAP in favor of REST. They provide a Java adapter that apparently works with the Java-Ruby bridge, but we'd like to keep it all in Ruby, if possible. I looked into soap4r, but it seems to have a slightly bad reputation.
So what's the best way to integrate SOAP calls into a Rails app?

I built Savon to make interacting with SOAP webservices via Ruby as easy as possible.
I'd recommend you check it out.

We used the built in soap/wsdlDriver class, which is actually SOAP4R.
It's dog slow, but really simple. The SOAP4R that you get from gems/etc is just an updated version of the same thing.
Example code:
require 'soap/wsdlDriver'
client = SOAP::WSDLDriverFactory.new( 'http://example.com/service.wsdl' ).create_rpc_driver
result = client.doStuff();
That's about it

We switched from Handsoap to Savon.
Here is a series of blog posts comparing the two client libraries.

I also recommend Savon. I spent too many hours trying to deal with Soap4R, without results. Big lack of functionality, no doc.
Savon is the answer for me.

Try SOAP4R
SOAP4R
Getting Started with SOAP4R
And I just heard about this on the Rails Envy Podcast (ep 31):
WS-Deathstar SOAP walkthrough

Just got my stuff working within 3 hours using Savon.
The Getting Started documentation on Savon's homepage was really easy to follow - and actually matched what I was seeing (not always the case)

Kent Sibilev from Datanoise had also ported the Rails ActionWebService library to Rails 2.1 (and above).
This allows you to expose your own Ruby-based SOAP services.
He even has a scaffold/test mode which allows you to test your services using a browser.

I have used HTTP call like below to call a SOAP method,
require 'net/http'
class MyHelper
def initialize(server, port, username, password)
#server = server
#port = port
#username = username
#password = password
puts "Initialised My Helper using #{#server}:#{#port} username=#{#username}"
end
def post_job(job_name)
puts "Posting job #{job_name} to update order service"
job_xml ="<soapenv:Envelope xmlns:soapenv=\"http://schemas.xmlsoap.org/soap/envelope/\" xmlns:ns=\"http://test.com/Test/CreateUpdateOrders/1.0\">
<soapenv:Header/>
<soapenv:Body>
<ns:CreateTestUpdateOrdersReq>
<ContractGroup>ITE2</ContractGroup>
<ProductID>topo</ProductID>
<PublicationReference>#{job_name}</PublicationReference>
</ns:CreateTestUpdateOrdersReq>
</soapenv:Body>
</soapenv:Envelope>"
#http = Net::HTTP.new(#server, #port)
puts "server: " + #server + "port : " + #port
request = Net::HTTP::Post.new(('/XISOAPAdapter/MessageServlet?/Test/CreateUpdateOrders/1.0'), initheader = {'Content-Type' => 'text/xml'})
request.basic_auth(#username, #password)
request.body = job_xml
response = #http.request(request)
puts "request was made to server " + #server
validate_response(response, "post_job_to_pega_updateorder job", '200')
end
private
def validate_response(response, operation, required_code)
if response.code != required_code
raise "#{operation} operation failed. Response was [#{response.inspect} #{response.to_hash.inspect} #{response.body}]"
end
end
end
/*
test = MyHelper.new("mysvr.test.test.com","8102","myusername","mypassword")
test.post_job("test_201601281419")
*/
Hope it helps. Cheers.

I have used SOAP in Ruby when i've had to make a fake SOAP server for my acceptance tests. I don't know if this was the best way to approach the problem, but it worked for me.
I have used Sinatra gem (I wrote about creating mocking endpoints with Sinatra here) for server and also Nokogiri for XML stuff (SOAP is working with XML).
So, for the beginning I have create two files (e.g. config.rb and responses.rb) in which I have put the predefined answers that SOAP server will return.
In config.rb I have put the WSDL file, but as a string.
##wsdl = '<wsdl:definitions name="StockQuote"
targetNamespace="http://example.com/stockquote.wsdl"
xmlns:tns="http://example.com/stockquote.wsdl"
xmlns:xsd1="http://example.com/stockquote.xsd"
xmlns:soap="http://schemas.xmlsoap.org/wsdl/soap/"
xmlns="http://schemas.xmlsoap.org/wsdl/">
.......
</wsdl:definitions>'
In responses.rb I have put samples for responses that SOAP server will return for different scenarios.
##login_failure = "<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Body>
<LoginResponse xmlns="http://tempuri.org/">
<LoginResult xmlns:a="http://schemas.datacontract.org/2004/07/WEBMethodsObjects" xmlns:i="http://www.w3.org/2001/XMLSchema-instance">
<a:Error>Invalid username and password</a:Error>
<a:ObjectInformation i:nil="true"/>
<a:Response>false</a:Response>
</LoginResult>
</LoginResponse>
</s:Body>
</s:Envelope>"
So now let me show you how I have actually created the server.
require 'sinatra'
require 'json'
require 'nokogiri'
require_relative 'config/config.rb'
require_relative 'config/responses.rb'
after do
# cors
headers({
"Access-Control-Allow-Origin" => "*",
"Access-Control-Allow-Methods" => "POST",
"Access-Control-Allow-Headers" => "content-type",
})
# json
content_type :json
end
#when accessing the /HaWebMethods route the server will return either the WSDL file, either and XSD (I don't know exactly how to explain this but it is a WSDL dependency)
get "/HAWebMethods/" do
case request.query_string
when 'xsd=xsd0'
status 200
body = ##xsd0
when 'wsdl'
status 200
body = ##wsdl
end
end
post '/HAWebMethods/soap' do
request_payload = request.body.read
request_payload = Nokogiri::XML request_payload
request_payload.remove_namespaces!
if request_payload.css('Body').text != ''
if request_payload.css('Login').text != ''
if request_payload.css('email').text == some username && request_payload.css('password').text == some password
status 200
body = ##login_success
else
status 200
body = ##login_failure
end
end
end
end
I hope you'll find this helpful!

I was having the same issue, switched to Savon and then just tested it on an open WSDL (I used http://www.webservicex.net/geoipservice.asmx?WSDL) and so far so good!
https://github.com/savonrb/savon

Related

What are some guidelines for creating HTTP requests in a Rails app?

I am relatively new to writing code against REST APIs. It is possible I am analyzing the wrong problem here, so feel free to give big picture advice. The other twist is that the API I want to use is not yet configured, so I can't test.
I need to write some Rails code to create webhooks on the Jive API. Jive's docs show how to register the webservice via a curl request. I want to build the code as an admin function of my app in case we need to recreate the webhook for any reason.
Here are the Jive Docs.
Based on this guide, I'm thinking I need something like (I expect this example to sent a POST request to "sample.jiveon.com/api/core/v3/webhooks"):
#host = "sample.jiveon.com/api/core/v3"
#port = "443"
#post_ws = "/webhooks"
#payload ={
"events" => "document",
"callback" => "my_app/jive_listener",
"object" => "my/jive/space"
}.to_json
def post
req = Net::HTTP::Post.new(#post_ws, initheader = {'Content-Type' =>'application/json'})
req['Authorization'] = "Bearer my_key"
req.body = #payload
response = Net::HTTP.new(#host, #port).start {|http| http.request(req) }
end
end
Thanks.
It would be better to use gem like 'rest-client(https://github.com/rest-client/rest-client)'
Above gem does the many stuff, which you might be doing manually using bare ruby library. It depends on need of yours.

Query server with PUT method

I will replace my command line
`curl -XPUT 'host:port/url' -d '{"val": "some_json"}'̀
by a Rails command, and get the result...
Somewhere like this :
response = call('put', 'host:port/url', '{"val" : "some_json"}')
Is there any predefined method to do this in Rails, or some gem ?
I know the command get of HTTP, but I will do a 'PUT' method.
Net::HTTP.get(URI.parse('host:port/url'))
Thanks for your replies
You can use Net::HTTP to send any standard http requests.
Here is a way, you can connect to any url ( http / https ), with any valid http methods with or without parameters.
def universal_connector(api_url, api_parameters={}, method="Get")
# Do raise Error, if url is invalid and Method is invalid
uri = URI(api_url)
req = eval("Net::HTTP::#{method.capitalize}.new('#{uri}')")
req.set_form_data(api_parameters)
Net::HTTP.start(uri.host, uri.port,:use_ssl => uri.scheme == 'https') do |http|
response = http.request(req)
return response.body
end
end
There are many alternatives available as well. Specifically, Faraday. Also, read this before making a choice.
#get is just a simple shortcut for the whole code (Net::HTTP Ruby library tends to be very verbose). However, Net::HTTP perfectly supports PUT requests.
Another alternative is to use an HTTP client as a wrapper. The most common alternatives are HTTParty and Faraday.
HTTParty.put('host:port/url', { body: {"val" : "some_json"} })
As a side note, please keep in mind that Rails is a framework, not a programming language. Your question is about how to perform an HTTP PUT request in Ruby, not Rails. It's important to understand the difference.

Ruby SOAP Client using SAVON not working whereas PHP SOAP client works

Hello there I am testing few web services and I am trying to write a client using SAVON with my WSDL. I have one available operation named log_process and I am trying to access that but getting errors. I have a similar script written in PHP and it is working fine. I have tried
require 'net/http'
require "uri"
require 'savon'
client = Savon.client(wsdl: "http://somedomain.com/projects/shared/abc.wsdl")
#a=client.operations
puts #a
ary={0 =>"art", 1 =>"bac", 2 =>"arr"}
#result = client.call(:log_process, message:{0 =>"asdf", 1 =>"qwer", 2 =>"arr"})
puts #result
and getting following error
raise_soap_and_http_errors!': (SOAP-ENV:Client) Bad Request (Savon::SOAPFault)
My php working solution looks like this
$result = $client->log_process(array(0=>$user_name,1=>$user_pwd,2=>$display_type));
any idea what will be the ruby equivalent to this or am I calling the operation in correct manner?
I know this is late, but I was having the exact same issue trying to set up a soap request using savon to a soap server I have worked with extensively using PHP Soap server. I found another post related to this, and it seem that adding the message_tag option fixed it.
This is because in my case the WSDL was expecting functionNameRequest in the xml, but savon as only sending funcionName by setting message_tag to functionNameRequest the >soap server was able to correctly map the function that was being requested.
This was the thread that helped me out https://github.com/savonrb/savon/issues/520 Relevant code quoted below:
Hi,
I'm just sharing this in case it's useful.
I'm using savon 2.3.0 and I guess the gem had some problems identifying parameters >automatically from my wsdl. I have no idea about SOAP and this is the first time I'm >actually using it.
I'm dealing with TradeTracker's WSDL
With the following code I got it working:
client = Savon.client do
wsdl "http://ws.tradetracker.com/soap/affiliate?wsdl"
namespace_identifier :ns1
end
credentials = {
customerID: 123123,
passphrase: "123123123"
}
response = client.call(:authenticate, message_tag: :authenticate, message: credentials)
Try:
#result = client.call(:log_process, message:["asdf", "asg", "arr"])
In the PHP code, you are sending only 1 parameter, its an array

Ruby Proxy Authentication GET/POST with OpenURI or net/http

I'm using ruby 1.9.3 and trying to use open-uri to get a url and try posting using Net:HTTP
Im trying to use proxy authentication for both:
Trying to do a POST request with net/http:
require 'net/http'
require 'open-uri'
http = Net::HTTP.new("google.com", 80)
headers = { 'User-Agent' => 'Ruby 193'}
resp, data = http.post("/", "name1=value1&name2=value2", headers)
puts data
And for open-uri which I can't get to do POST I use:
data = open("http://google.com/","User-Agent"=> "Ruby 193").read
How would I modify these to use a proxy with HTTP Authentication
I've tried (for open-uri)
data = open("http://google.com/","User-Agent"=> "Ruby 193", :proxy_http_basic_authentication => ["http://proxy.com:8000/", "proxy-user", "proxy-password"]).read
However all I will get is a OpenURI::HTTPError: 407 Proxy Authentication Required. I've verified all and it works in the browser with the same authentication and proxy details but I can't get ruby to do it.
How would I modify the code above to add http authentication properly? Has anyone gone through this atrocity?
Try:
require "open-uri"
proxy_uri = URI.parse("http://proxy.com:8000")
data = open("http://www.whatismyipaddress.com/", :proxy_http_basic_authentication => [proxy_uri, "username", "password"]).read
puts data
As for Net::HTTP, I recently implemented support for proxies with http authentication into a Net::HTTP wrapper library called http. If you look at my last pull-request, you'll see the basic implementation.
EDIT: Hopefully this will get you moving in the right direction.
Net::HTTP::Proxy(proxy_uri.host, proxy_uri.port,"username","password").start('whatismyipaddress.com') do |http|
puts http.get('/').body
end
EDIT 11/24/2020: Net::HTTP::Proxy is now considered obsolete. You can now configure proxies when creating a new instance of Net::HTTP. See the documentation for Net::HTTP.new for more details.

Trying to connect to a "digest authentication" webservice using HTTParty or Net:HTTP (or etc)

I have been trying to connect to a web service that is using digest authentication.
I am able to connect in Safari using user:password#service.site.com/endpoint
I have tried in Ruby and Rails to connect using HTTParty and Net:HTTP using the "basic"auth" options, but have not had any luck.
Wondering if the HTTParty/Net:HTTP "basic_auth" option is not going to be compatible with a "digest auth" service?
If not, is there another way that I might connect?
HTTParty basic auth is apparently not compatible with digest_auth. I found this Net:HTTP extension: https://codesnippets.joyent.com/posts/show/1075 and am writing a method to handle this, with the help of the Crack gem http://github.com/jnunemaker/crack:
def self.decode vin
url = URI.parse(APP_CONFIG[:vinlink_url])
Net::HTTP.start(url.host) do |http|
res = http.head(url.request_uri)
req = Net::HTTP::Get.new("/report?type=basic&vin=#{vin}")
req.digest_auth(APP_CONFIG[:vinlink_login], APP_CONFIG[:vinlink_password], res)
#response = http.request(req)
end
if #response.code == "200"
hash = Crack::XML.parse(#response.body).recursive_downcase_keys!.recursive_symbolize_keys!
end
end
Wasn't able to get to the codesnippets link given above today, but code is also available here https://gist.github.com/73102. I've used this successfully for digest authentication, but ran into problems with multiple request, getting 'Stale client nonce' errors - resolved by generating a new nonce within the digest_auth function each time it was called. Didn't find much on that when I looked, so hope this helps someone.

Resources