#290 SOAP with Savon
- Download:
- source codeProject Files in Zip (77.4 KB)
- mp4Full Size H.264 Video (23.8 MB)
- m4vSmaller H.264 Video (13.3 MB)
- webmFull Size VP8 Video (13.4 MB)
- ogvFull Size Theora Video (34.1 MB)
A todo programador web le acaba tocando trabajar contra una interfaz SOAP; ese momento en que el sencillo código Ruby debe comunicarse con complejas aplicaciones .Net o Java. Por suerte tenemos ayuda a la hora de acometer esta tarea (potencialmente incómoda). Savon es una gema Ruby escrita por Daniel Harrington que proporciona una bonita interfaz Ruby para comunicarnos con las APIs SOAP. En este episodio veremos cómo funciona.
Búsquedas de código postal
Debajo se muestra una página de una aplicación que tiene un sencillo formulario que recibe un código postal. Queremos que los usuarios introduzcan un código postal y vean la información devuelta para dicho código.
Como puede verse, la aplicación todavía no funciona. Tenemos el sitio para la información pero no se muestra nada, dado que tenemos que comunicarnos con un Web Service externo para recuperar dicha información. Lo haremos de la web de WebserviceX.NET. Este sitio dispone de una API SOAP que proporciona mucha información útil como valores bursátiles, conversión de monedas, información meteorológica y muchas más cosas. La web tiene una página con un formulario con el cual podemos probar el servicio web. Si introducimos un código postal válido en el formulario veremos que se nos devuelven ciertos valores XML con datos acerca de dicho código postal. Si, por ejemplo, introducimos 90210, veremos la siguiente respuesta:
<NewDataSet> <Table> <CITY>Beverly Hills</CITY> <STATE>CA</STATE> <ZIP>90210</ZIP> <AREA_CODE>310</AREA_CODE> <TIME_ZONE>P</TIME_ZONE> </Table> </NewDataSet>
Uso de soapUI para testear las llamadas SOAP
Antes de empezar a usar un API SOAP en una aplicación es buena idea experimentar un poco con ella para asegurarnos de que funciona tal y como esperamos. Para esto nos puede ser de ayuda una aplicación Java llamada soapUI que sirve para probar servicios web. Tras su instalación podemos ejecutarla y crear un nuevo proyecto. Aparecerá una caja de diálogo que nos pedirá la URL de un WSDL, entre otras cosas. La página que vimos antes dispone de esta información y la URL que debemos introducir es http://www.webservicex.net/uszip.asmx?WSDL
.
Un WSDL nos dice qué acciones hay disponibles en una URL dada. Tras introducir la URL del WSDL en soapUI veremos un listado de dichas aciones. Si expandimos la acción GetInfoByZip
y hacemos clic en el campo “Request 1” que aparece veremos el XML necesario para realizar la petición.
Esta es la información que tenemos que sacar de soapUI para poder usarla con Savon en nuestra aplicación Rails. Si cambiamos la interrogación en el elemento <web:USZip>
y ponemos un código postal de verdad, cuando pulsemos la flecha verde veremos la respuesta en XML.
Comencemos con Savon
Ahora que tenemos una manera de testear las peticiones SOAP podemos empezar a utilizar Savon en nuestra aplicación. Como es habitual, se instala añadiendo la gema al Gemfile y ejecutando bundle
.
source 'http://rubygems.org' gem 'rails', '3.1.1' gem 'sqlite3' # Gems used only for assets and not required # in production environments by default. group :assets do gem 'sass-rails', '~> 3.1.4' gem 'coffee-rails', '~> 3.1.1' gem 'uglifier', '>= 1.0.3' end gem 'jquery-rails' gem 'savon'
Para ver cómo funciona Savon podemos experimentar en la consola. Lo primero que tenemos que hacer es crear un nuevo Savon::Client
, pasándole la URL con la que nos queremos comunicar.
> client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL")
Al método new
se le puede pasar un bloque si nos hace falta alguna configuración extra, pero para nuestros propósitos bastará dejarlo tal y como está. Lo siguiente que haremos será recuperar el listado de las acciones disponibles.
> client.wsdl.soap_actions HTTPI executes HTTP GET using the net_http adapter => [:get_info_by_zip, :get_info_by_city, :get_info_by_state, :get_info_by_area_code]
Una de las acciones devueltas es precisamente la que estamos buscando (obsérvese que Savon ha cambiado el nombre de GetInfoByZip
a la nomenclatura más habitual de Ruby get_info_by_zip
)
Hacemos las peticiones invocando a client.request
y pasando la acción que queremos invocar. También podemos pasar un espacio de nombres como primer argumento opcional. Si vemos el XML generado por soapUI veremos que se está usando el espacio de nombres web
, por tanto tendremos que pasarlo. Por último tenemos que añadir algunas opciones, y aquí tambíen podríamos usar un bloque pero en su lugar pasaremos la opción body
, esta opción recibe un hash donde le ponemos los parámetros que vimos en la petición XML. En soapUI el código zip se pasó en el elemento <web:USZip>
. Ya hemos especificado el espacio de nombres, así que tan sólo tenemos que pasar el código postal en la forma us_zip
. (Aquí Savon cambia también las mayúsculas y minúsculas del parámetro).
> client.request :web, :get_info_by_zip, body: { us_zip: "90210" }
La respuesta a esta petición será bastante extensa. Por lo general esto sale en el log de desarrollo por lo que si queremos depurar las llamadas SOAP tendremos que ver qué peticiones y respuestas han ocurrido ahí. Si examinamos el XML de la respuesta veremos que no contiene la información que buscábamos:
<?xml version="1.0" encoding="utf-8"?> <soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetInfoByZIPResponse xmlns="http://www.webserviceX.NET" /> </soap:Body> </soap:Envelope>
Parece que algo ha ido mal con nuestra petición. En este caso el problema es que en SOAP los nombres de parámetro distinguen entre mayúsculas y minúsculas y, si vemos la petición que enviamos en XML, vemos que el parámetro USZip
se envió como usZip
.
<web:usZip>90210</web:usZip>
Para hacer que Savon preserve las mayúsculas y minúsculas podemos pasar el nombre de parámetro como una cadena.
> client.request :web, :get_info_by_zip, body: { "USZip" => "90210" }
Cuando enviemos esta petición veremos que la respuesta contiene lo que esperábamos.
<soap:Envelope xmlns:soap="http://schemas.xmlsoap.org/soap/envelope/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:xsd="http://www.w3.org/2001/XMLSchema"> <soap:Body> <GetInfoByZIPResponse xmlns="http://www.webserviceX.NET"> <GetInfoByZIPResult> <NewDataSet xmlns=""> <Table> <CITY>Beverly Hills</CITY> <STATE>CA</STATE> <ZIP>90210</ZIP> <AREA_CODE>310</AREA_CODE> <TIME_ZONE>P</TIME_ZONE> </Table> </NewDataSet> </GetInfoByZIPResult> </GetInfoByZIPResponse> </soap:Body> </soap:Envelope>
Podemos asignar el objeto de respuesta recibido a una variable en la consola utilizando el guión bajo, que captura lo último que ha sido devuelto.
> response = _
Si a este objeto le invocamos to_hash
, tendremos un hash de Ruby muy fácil de analizar.
> response.to_hash => {:get_info_by_zip_response=> {:get_info_by_zip_result=> {:new_data_set=>{:table=> {:city=>"Beverly Hills", :state=>"CA", :zip=>"90210", :area_code=>"310", :time_zone=>"P"}, :@xmlns=>""}}, :@xmlns=>"http://www.webserviceX.NET"}}
La información que buscamos está anidada muy profundamente en este hash pero aún así podemos llegar a ella fácilmente. Ahora que ya sabemos cómo funciona Savon veremos cómo usarlo en nuestras aplicaciones Rails.
Uso de Savon con aplicaciones Rails
Nuestra aplicación tiene una clase de modelo ZipCode
. No se trata de un modelo de ActiveRecord, sino tan sólo una clase Ruby, con algunos atributos y un inicializador que recibe un código postal.
class ZipCode attr_reader :state, :city, :area_code, :time_zone def initialize(zip) end end
Cuando se instancie un nuevo objeto ZipCode
queremos establecer los atributos con los valores que recuperemos de la llamada SOAP, así que el código que tenemos que añadir a la clase es similar que el que hemos ejecutado en consola.
class ZipCode attr_reader :state, :city, :area_code, :time_zone def initialize(zip) client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL") response = client.request :web, :get_info_by_zip, body: { "USZip" => zip } data = response.to_hash[:get_info_by_zip_response][:get_info_by_zip_result][:new_data_set][:table] @state = data[:state] @city = data[:city] @area_code = data[:area_code] @time_zone = data[:time_zone] end end
Este código hace la petición e invoca to_hash
sobre su respuesta, recorriendo el hash hasta llegar al atributo relevante. Podemos hacer la prueba ahora mismo, si metemos un código postal y hacemos clic en “Lookup” se ejecutará la llamada SOAP y veremos la información en la página, lo que quiere decir que nuestro código funciona.
Gestión de errores
Todavía tenemos que modificar nuestro código para gestionar los errores. Si tal y como está ahora introducimos un código postal incorrecto y tratamos de buscarlo, la aplicación eleva una excepción porque la respuesta no incluye el hash que esperamos.
Una forma de gestionar estos casos es comprobar que la respuesta es correcta antes de intentar establecer los atributos. Podríamos utilizar response.success?
para esto pero, aunque sería útil, en nuestro caso no nos ayudaría porque la respuesta es correcta incluso cuando se introduce un código incorrecto, el problema es que la respuesta viene vacía.
Para ayudarnos con esto Savon dispone del método to_array
que podemos usar en lugar de to_hash
. Podemos pasar una lista de claves de hash y en lugar de elevar una excepción si no existen las claves, devolverá un array vacío. En este caso sólo tenemos que invocar first
para encontrar los resultados que queramos, y si no viene ninguno recibiremos nil
, lo que podremos comprobar en nuestro código.
def initialize(zip) client = Savon::Client.new("http://www.webservicex.net/uszip.asmx?WSDL") response = client.request :web, :get_info_by_zip, body: { "USZip" => zip } if response.success? data = response.to_array(:get_info_by_zip_response, :get_info_by_zip_result, :new_data_set, :table).first if data @state = data[:state] @city = data[:city] @area_code = data[:area_code] @time_zone = data[:time_zone] end end end
Si recargamos la página veremos un resultado vacío en lugar de la excepción anterior.
Todavía nos queda trabajo por hacer porque en este caso deberíamos mostrar un mensaje de error, pero no lo haremos.
Consejos finales
Vamos a terminar este episodio con un par de consejos. Si la mayoría de atributos vienen con la primera letra en mayúsculas y terminamos usando muchas cadenas podemos añadir la siguiente línea en nuestra aplicación para que se utilice UpperCamelCase
en lugar de lowerCamelCase
, con lo que si la API que estamos usando utiliza este tipo de combinación de mayúsculas y minúsculas podremos usar símbolos en lugar de cadenas en nuestros argumentos.
Gyoku.convert_symbols_to :camelcase
Los ficheros WSDL no deberían cambiar con frecuencia por lo que es buena idea cachearlos en lugar de descargarlos cada vez. Podemos descargar el fichero WSDL, almacenarlo localmente y luego utilizar una referencia al archivo para crear el Savon::Client
en lugar de una referencia a la web.
Con esto cerramos este episodio acerca de Savon, que nos facilita la comunicación con APIs SOAP. Si lo usamos en nuestras aplicaciones nos podrían interesar un par de proyectos relacionados. El primero de ellos es Savon Model, que proporciona un DSL para configurar el cliente desde dentro de una clase y probablemente iría bien con nuestra clase ZipCode
lo que nos permitiría limpiar bastante el código.
El otro proyecto es Savon Spec que nos puede ayudar en nuestras pruebas generando peticiones SOAP ficticias. Esto está bien para tests de bajo nivel, pero para hacer pruebas intensivas deberíamos tener tests de integración a más alto nivel que ataquen a la API real. De esta forma si la API cambia, los tests fallarán. La gema VCR es de gran ayuda, y hablaremos de ella en el episodio de pago de esta semana.