Ruby Nokogiri XML formatting for post request - ruby-on-rails

I am setting up a method in a Rails app which posts 2 bits of data to an endpoint api.
Using Nokogiri to parse the data, and Faraday to send request, I have got very close to what is needed for successful post of multiple order items data, excepting a key issue.
This is that I am getting unwanted repeats of the top level node, because it is inside an each loop, which is needed for the items, whereas we only need that once. Below is the method, whereby an object is parsed and the order items put in to place in xml with Nokogiri and here we are just accessing the closed orders in the data object by definition, and just sending the id and quantity for each of them.
def update_backoffice_closed_orders(bj_update)
bj_update.each do |item|
if item['order_status'] == 'closed'
id = item['order_number']
del = item['quantity_fulfilled']
# Set up the connection to the Backoffice API and items to be updated
#xml_post = Nokogiri::XML::Builder.new do |xml|
xml.supplier {
xml.sid SID
xml.password PASSWORD
xml.order {
xml.id id
xml.volume_Delivered del
}
}
end
# Send the request to the Backoffice API
#response = CONNECTION.post do |req|
req.url '/suppliers/orderDataUpdate.php'
req.body = #xml_post.to_xml
end
end
end
end
This is working OK except failing at api because the top level nodes repeat like so:
<?xml version="1.0"?>
<supplier>
<sid>344</sid>
<password>4657gdhtey6</password>
in each order, whereas all are only needed once at top, with supplier closing tag after all the orders in between, as in the example below.
<?xml version="1.0"?>
<supplier>
<sid>344</sid>
<pw>4657gdhtey6</pw>
<order>
<id>3686414</id>
<volume_delivered>480</volume_delivered>
</order>
<order>
<id>3686425</id>
<volume_delivered>480</volume_delivered>
</order>
<order>
<id>3686443</id>
<volume_delivered>480</volume_delivered>
</order>
</supplier>
CONNECTION is a Faraday constant and works fine, as are SID and PASSWORD, if I can get the Nokogiri #xml_post like above, then it is exactly as needed and used in Postman tests for the api, and should work. If anyone knows how to adjust the xml builder to achieve that in this method, please share?
Biggest problem seems the <?xml opening tag which is default with Nokogiri.
Many Thanks

Related

Cannot call web service methods via SOAP Connection (SAVON) in Ruby on Rails. Where am i going wrong?

I seem to be getting this error message:
(a:ActionNotSupported) The message with Action 'GetServices' cannot be
processed at the receiver, due to a ContractFilter mismatch at the
EndpointDispatcher. This may be because of either a contract mismatch
(mismatched Actions between sender and receiver) or a binding/security
mismatch between the sender and the receiver. Check that sender and
receiver have the same contract and the same binding (including
security requirements, e.g. Message, Transport, None).
I assume it is something to do with the security/binding setup.
My connection uses HTTP, with basichttpbinding. I've done a lot of searching for the answer, as I always do, but am unable to fix it, and no one here has expertise on Ruby on Rails.
Help would be appreciated.
Below is my code, in Ruby on Rails, which initialises the service and then calls it. Note: I can connect to it fine. It has successfully reported the available methods. Just calling the methods seems to be the problem. I have successfully connected to online test services using the same code. And I use Savon.
def test
puts "web_service: IN"
client = Savon::Client.new do
wsdl.document = "http://hidden.co.uk/myService.svc?wsdl"
end
#response = client.request "GetServices", :xmlns => "http://tempuri.org/" do
soap.header = {}
soap.body = {
"CostCentreNo" => 1,
"filter" => 0
}
end
puts '##########################'
puts #response.to_hash;
end
Below is what my Ruby on Rails sends:
<?xml version="1.0" encoding="UTF-8"?>
<env:Envelope xmlns:wsdl="http://tempuri.org/" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:env="http://schemas.xmlsoap.org/soap/envelope/">
<env:Body>
<GetServices xmlns="http://tempuri.org/">
<CostCentreNo>1</CostCentreNo>
<filter>0</filter>
</GetServices>
</env:Body>
</env:Envelope>
This is what WCF test client sends, (which works)
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/">
<s:Header>
<Action s:mustUnderstand="1" xmlns="http://schemas.microsoft.com/ws/2005/05/addressing/none">http://tempuri.org/IIBCSServices/GetServices</Action>
</s:Header>
<s:Body>
<GetServices xmlns="http://tempuri.org/">
<CostCentreNo>0</CostCentreNo>
<filter>0</filter>
</GetServices>
</s:Body>
</s:Envelope>
It seems to be the way it was being called... Something so simple.
The override Stated on the SAVON Tutorial, recommended if you have an uppercase starting camelcase doesnt work. Maybe the tutorial is outdated. (Note, :wsdl IS required in my case)
So this was not working:
response = client.request :wsdl, "GetCustomerCentreDetails"
Changing it to:
response = client.request :wsdl, :get_customer_centre_details
Then obviously I need a body added to it, and header etc.
The assumption that caused me confusion : Being able to get the WSDL does not mean you are connected to the webservice.
it seems you're missing this part
<Action s:mustUnderstand="1" ...>
you should insert something like the following into your request
soap.header = {"Action" =>
{'env:mustUnderstand' =>
'http://tempuri.org/IIBCSServices/GetServices',
attributes! => { 'mustUnderstand' => "1", 'xmlns' => "..." }
}

Microsoft Translator API answers 500 internal server error

I'm trying to use Microsoft's Translator API in my Rails app. Unfortunately and mostly unexpected, the server answers always with an internal server error. I also tried it manually with Poster[1] and I get the same results.
In more detail, what am I doing? I'm creating an XML string which goes into the body of the request. I used the C# Example of the API documentation. Well, and then I'm just invoking the RESTservice.
My code looks like this:
xmlns1 = "http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2"
xmlns2 = "http://schemas.microsoft.com/2003/10/Serialization/Arrays"
xml_builder = Nokogiri::XML::Builder.new(:encoding => 'UTF-8') do |xml|
xml.TranslateArrayRequest("xmlns:ms" => xmlns1, "xmlns:arr" => xmlns2) {
xml.AppId token #using temporary token instead of appId
xml.From source
xml.To target
xml.Options {
xml["ms"].ContentType {
xml.text "text/html"
}
}
xml.Texts {
translate.each do |key,val|
xml["arr"].string {
xml.text CGI::unescape(val)
}
end
}
}
end
headers = {
'Content-Type' => 'text/xml'
}
uri = URI.parse(##msTranslatorBase + "/TranslateArray" + "?appId=" + token)
req = Net::HTTP::Post.new(uri.path, headers)
req.body = xml_builder.to_xml
response = Net::HTTP.start(uri.host, uri.port) { |http| http.request(req) }
# [...]
The xml_builder produces something like the following XML. Differently to the example from the API page, I'm defining two namespaces instead of referencing them on the certain tags (mainly because I wanted to reduces the overhead) -- but this doesn't seem to be a problem, when I do it like the docu-example I also get an internal server error.
<?xml version="1.0" encoding="UTF-8"?>
<TranslateArrayRequest xmlns:ms="http://schemas.datacontract.org/2004/07/Microsoft.MT.Web.Service.V2" xmlns:arr="http://schemas.microsoft.com/2003/10/Serialization/Arrays">
<AppId>TX83NVx0MmIxxCzHjPwo2_HgYN7lmWIBqyjruYm7YzCpwnkZL5wtS5oucxqlEFKw9</AppId>
<From>de</From>
<To>en</To>
<Options>
<ms:ContentType>text/html</ms:ContentType>
</Options>
<Texts>
<arr:string>Bitte übersetze diesen Text.</arr:string>
<arr:string>Das hier muss auch noch übersetzt werden.</arr:string>
</Texts>
</TranslateArrayRequest>
Every time I request the service it answers with
#<Net::HTTPInternalServerError 500 The server encountered an error processing the request. Please see the server logs for more details.>
... except I do some unspecified things, like using GET instead of POST, then it answers with something like "method not allowed".
I thought it might be something wrong with the XML stuff, because I can request an AppIdToken and invoke the Translate method without problems. But to me, the XML looks just fine. The documentation states that there is a schema for the expected XML:
The request body is a xml string generated according to the schema specified at http:// api.microsofttranslator.com/v2/Http.svc/help
Unfortunately, I cannot find anything on that.
So now my question(s): Am I doing something wrong? Maybe someone experienced similar situations and can report on solutions or work-arounds?
[1] Poster FF plugin > addons.mozilla.org/en-US/firefox/addon/poster/
Well, after lot's of trial-and-error I think I made it. So in case someone has similar problems, here is how I fixed this:
Apparently, the API is kind of fussy with the incoming XML. But since there is no schema (or at least I couldn't find the one specified in the documentation) it's kind of hard to do it the right way: the ordering of the tags is crucial!
<TranslateArrayRequest>
<AppId/>
<From/>
<Options />
<Texts/>
<To/>
</TranslateArrayRequest>
When the XML has this ordering it works. Otherwise you'll only see the useless internal server error response. Furthermore, I read a couple of times that the API also breaks if the XML contains improper UTF-8. One can force untrusted UTF-8 (e.g. coming from a user form) this way:
ic = Iconv.new('UTF-8//IGNORE', 'UTF-8')
valid_string = ic.iconv(untrusted_string + ' ')[0..-2]

Trouble handling HTTP responses and parsing JSON data

I am using Ruby on Rails 3 and I would like to solve an issue with the following code where a web client application receive back some JSON data from a web service application that uses a Rack middleware in order to respond.
In the web client app model I have
response_parsed = JSON.parse(response.body)
if response_parsed["account"]
...
else
return response
end
In the above code the response.body come back from the web service app that uses a Rack middleware to respond to the web client:
accounts = Account.where(:id => ids)
[200, {'Content-Type' => 'application/json'}, accounts.to_json] # That is, response.body = accounts.to_json
Data transmission is ok, but I get the following error
TypeError
can't convert String into Integer
*Application Trace*
lib/accounts.rb:107:in `[]'
The line 107 corresponds to
if response_parsed["account"]
...
Where and what is the problem? How to solve that?
If I try to debug the respons.body I get
# Note: this is an array!
"[{\"account\":{\"firstname\":\"Semio\",\"lastname\":\"Iaven\"\"}}]"
If I'm saying something you already realize, forgive me.
It looks like your response is a one-element array with a hash in it as the first element. Because the response is an array, when you use the [] it is expecting a integer representing the index of the item in the array you'd like to access, and that is what the error message means--it expected that you'd tell it the integer value of the item you wanted, but instead you gave it a string.
If you instead do:
response_parsed[0]['account']
It seems like you'd get what you want.

How do I post XML to RESTFUL Web Service using Net::HTTP::Post?

I am having trouble getting this to work so any help would be appreciated! Basically, the request.body contains valid XML for the Web Service like so:
<somedata>
<name>Test Name 1</name>
<description>Some data for Unit testing</description>
</somedata>
...but the service returns empty XML. Note that the id field is returned suggesting that it does actually hit the database, but the name and description fields are nil:
<somedata>
<id type='integer'>1</id>
<name nil='true'></name>
<description nil='true'></description>
</somedata>
I have manually tested the RESTFUL service using Poster and it works fine.
Here is the code:
url = URI.parse('http://localhost:3000/someservice/')
request = Net::HTTP::Post.new(url.path)
request.body = "<?xml version='1.0' encoding='UTF-8'?><somedata><name>Test Name 1</name><description>Some data for Unit testing</description></somedata>"
response = Net::HTTP.start(url.host, url.port) {|http| http.request(request)}
#Note this test PASSES!
assert_equal '201 Created', response.get_fields('Status')[0]
Does anyone have any clues why the data in the XML post is not persisted?
Without knowing anything about the service, this is just a guess, but… is the service expecting a specific header that Poster is setting and you aren't?
If I were you, I'd use Wireshark, tcpflow or some other sniffer to look at the exact data Poster and your app are sending, and make sure they're identical. I've seen services that were sensitive to the weirdest things, like whitespace or user agent.

What's the best way to use SOAP with Ruby?

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

Resources