« Xero API + Ruby Revisited | Main | To Snapper or not to Snapper »
Wednesday
30Jul2008

Connecting to the Xero API: Part 1 [Ruby, GET, Contacts]

Hey hey hey, so I finally get an excuse to post some ruby code, yay!

As part of my foray into the Xero API I wanted to get a feel for how easy it would be to connect from non .net languages and as my Ruby was getting a bit rusty I thought I’d pull out the old text editor and write up some simple methods and models for interacting with the API, so here’s the first part. How to connect to the GET methods for contacts.

The first step was to write down the specifications in RSpec, I’m glad to say they’re all green and for your reading pleasure here they are:

RSpec Results

26 examples, 0 failures

Finished in 3.567 seconds

Address
should have correct country
should have correct postal code
should have correct region
should have correct city
should have correct address line 4
should have correct address line 3
should have correct address line 2
should have correct address line 1
should have correct type
Phone
should have correct country code
should have correct area code
should have correct number
should have correct type
Contact fetch without specifying a contact id should fetch all
should fetch the correct number of contacts
Contact fetch, specifying an id
should populate phone numbers
should populate addresses
should fetch the correct contact
Contact
should populate phone data correctly
should have the correct number of phone numbers
should have the correct address
should have the correct number of addresses
should have the correct updated date
should have the correct email address
should have the correct status
should have the correct xero id
should have the correct name

Ok once those were defined it was time to build up the models, I cheated and used Rails to create some ActiveRecord classes really easily, the migrations look like this:

Phones Migration

class CreatePhones < ActiveRecord::Migration
def self.up
create_table :phones do |t|
t.string :phone_type
t.string :number
t.string :area_code
t.string :country_code

t.timestamps
end
end

def self.down
drop_table :phones
end
end


Addresses Migration



class CreateAddresses < ActiveRecord::Migration
def self.up
create_table :addresses do |t|
t.string :addresstype
t.string :line1
t.string :line2
t.string :line3
t.string :line4
t.string :city
t.string :region
t.string :postal_code
t.string :country

t.timestamps
end
end

def self.down
drop_table :addresses
end
end


Contacts Migration



class CreateContacts < ActiveRecord::Migration
def self.up
create_table :contacts do |t|
t.string :name
t.string :xero_id
t.string :status
t.string :email_address
t.datetime :updated_date_utc

t.timestamps
end
end

def self.down
drop_table :contacts
end
end


 



this should give you your 3 models, but of course we don’t want them to be populated from the database we want them to be populated from the xero api, as such each model has a from_xml method defined, which takes in the xml for the contact/address/phone as appropriate



Also the contact model has a fetch method which actually goes away and gets the xml stream either for an individual contact (if :xero_id is passed in) otherwise for all contacts



address.rb



require 'hpricot'
class Address < ActiveRecord::Base
include XmlHelper
has_one :contact
def self.from_xml(address_xml)
doc = Hpricot(address_xml)
mappings = {
:addresstype => "/address/addresstype",
:line1 => "/address/addressline1",
:line2 => "/address/addressline2",
:line3 => "/address/addressline3",
:line4 => "/address/addressline4",
:city => "/address/city",
:region => "/address/region",
:postal_code => "/address/postalcode",
:country => "/address/country",
}
attr = XmlHelper.populate_from_xml(doc,mappings)
return Address.new(attr)
end
end


phone.rb



require "hpricot"
class Phone < ActiveRecord::Base
include XmlHelper
has_one :contact
def self.from_xml(phone_xml)
doc = Hpricot(phone_xml)
mappings = {
:phone_type => "/phone/phonetype",
:number => "/phone/phonenumber",
:area_code => "/phone/phoneareacode",
:country_code => "/phone/phonecountrycode",
}
attr=XmlHelper.populate_from_xml(doc,mappings)
return Phone.new(attr)
end
end


contact.rb



require 'hpricot'
require 'open-uri'
class Contact < ActiveRecord::Base
include XmlHelper

has_many :addresses
has_many :phones
def self.from_xml(contact_xml)
doc = Hpricot(contact_xml)
mappings = {
:name=>"/contact/name",
:xero_id=>"/contact/contactid",
:status=>"/contact/contactstatus",
:email_address=>"/contact/emailaddress",
:updated_date_utc=>"/contact/updateddateutc"
}
attr = XmlHelper.populate_from_xml(doc,mappings)
contact = Contact.new(attr)
(doc/"/contact/addresses/address").each do |address_xml|
contact.addresses << Address.from_xml(address_xml.to_html)
end
(doc/"/contact/phones/phone").each do |phone_xml|
contact.phones << Phone.from_xml(phone_xml.to_html)
end
return contact
end
def self.fetch(params)
if(params[:xero_id]!=nil)
response=Hpricot(open("#{params[:url]}/contact?contactID=#{params[:xero_id]}&apiKey=#{params[:api_key]}&xeroKey=#{params[:customer_key]}"))
return Contact.from_xml(response.at("/response/contact").to_html)
else
contacts = Array.new
response=Hpricot(open("#{params[:url]}/contacts?apiKey=#{params[:api_key]}&xeroKey=#{params[:customer_key]}"))
(response/"/response/contacts/contact").each do |contact_xml|
contacts << Contact.from_xml(contact_xml.to_html)
end
return contacts
end
end
end


There’s also a helper module that takes in a hash of property names mapped to the xml path for the property, and defaults to an empty string if the xml isn’t found



xml_helper.rb



module XmlHelper
def self.populate_from_xml(xml,mappings)
params={}

mappings.each { |key,value|
if(!xml.at(value).nil?)
params[key]=xml.at(value).inner_text
else
params[key]=""
end
}
return params
end
end




And here’s the specs that should show how everything is used, and glues together (the xml is contained in fixture files to remove the need to go to the api each time)



address_spec.rb



require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe Address do
before(:each) do
@xml = File.read(RAILS_ROOT+"/spec/fixtures/xero_xml_objects/address.xml")
@address = Address.from_xml(@xml)
end

it "should have correct type" do
@address.addresstype.should=="POBOX"
end
it "should have correct address line 1" do
@address.line1.should=="line 1"
end
it "should have correct address line 2" do
@address.line2.should=="line 2"
end
it "should have correct address line 3" do
@address.line3.should=="line 3 text"
end
it "should have correct address line 4" do
@address.line4.should=="line 4"
end
it "should have correct city" do
@address.city.should=="Wellington"
end
it "should have correct region" do
@address.region.should=="Region"
end
it "should have correct postal code" do
@address.postal_code.should=="6011"
end
it "should have correct country" do
@address.country.should=="COUNTRY/NZ"
end

end


phone_spec.rb



require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe Phone do

before(:each) do
@xml = File.read(RAILS_ROOT+"/spec/fixtures/xero_xml_objects/phone.xml")
@phone = Phone.from_xml(@xml)

end

it "should have correct type" do
@phone.phone_type.should == "DDI"
end
it "should have correct number" do
@phone.number.should =="1223 (2304)"
end
it "should have correct area code" do
@phone.area_code.should == "(1023)"
end
it "should have correct country code" do
@phone.country_code.should == "10"
end
end


contact_spec.rb



require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe Contact do
before(:each) do
@xml = File.read(RAILS_ROOT+"/spec/fixtures/xero_xml_objects/contact.xml")
@contact = Contact.from_xml(@xml)
end

it "should have the correct name" do
@contact.name.should == "A. Dutchess"
end
it "should have the correct xero id" do
@contact.xero_id.should == "bd2270c3-8706-4c11-9cfb-000b551c3f51"
end
it "should have the correct status" do
@contact.status.should == "ACTIVE"
end
it "should have the correct email address" do
@contact.email_address.should == "user@domain.com"
end
it "should have the correct updated date" do
@contact.updated_date_utc.should == DateTime.parse("2008-07-12T01:44:26.747")
end
it "should have the correct number of addresses" do
@contact.addresses.size.should == 1
end
it "should have the correct address" do
@contact.addresses[0].line1.should == "P O Box 123"
end
it "should have the correct number of phone numbers" do
@contact.phones.size.should == 4
end
it "should populate phone data correctly" do
@contact.phones[0].number.should == "12345690"
end
describe "fetch, specifying an id" do
before(:each) do
@xero_contact_id="bd2270c3-8706-4c11-9cfb-000b551c3f51"
@base_url="http://networkurl"
@api_key="B5920D82BC0A40C89C16E8E2217875"
@customer_key = "BF7BC56EE7D9457BB6F7685B00A955"
@params={
:xero_id => @xero_contact_id,
:url => @base_url,
:api_key=>@api_key,
:customer_key=>@customer_key
}
xml=File.read(RAILS_ROOT+"/spec/fixtures/xero_api_responses/get_contact_response.xml")
Contact.should_receive(:open).with("http://networkurl/contact?contactID=bd2270c3-8706-4c11-9cfb-000b551c3f51&apiKey=B5920D82BC0A40C89C16E8E2217875&xeroKey=BF7BC56EE7D9457BB6F7685B00A955").and_return(xml)
@contact = Contact.fetch(@params)

end
it "should fetch the correct contact" do
@contact.xero_id.should == "bd2270c3-8706-4c11-9cfb-000b551c3f51"
end
it "should populate addresses" do
@contact.addresses.size.should == 1
end
it "should populate phone numbers" do
@contact.phones.size.should == 4
end

end
describe "fetch without specifying a contact id should fetch all" do
before(:each) do
@base_url="http://networkurl"
@api_key="B5920D82BC0A40C89C16E8E2217875"
@customer_key = "BF7BC56EE7D9457BB6F7685B00A955"
@params={
:url => @base_url,
:api_key=>@api_key,
:customer_key=>@customer_key
}
xml=File.read(RAILS_ROOT+"/spec/fixtures/xero_api_responses/get_contacts_response.xml")
Contact.should_receive(:open).with("http://networkurl/contacts?apiKey=B5920D82BC0A40C89C16E8E2217875&xeroKey=BF7BC56EE7D9457BB6F7685B00A955").and_return(xml)
@contacts = Contact.fetch(@params)
end
it "should fetch the correct number of contacts" do
@contacts.size.should == 63
end
end
end


Wow that’s a lot of code for one post and may not make sense, as such i’m going to see if Xero will let me borrow some equipment and record a screen cast to show how i got to this point from the beginning.



Attached is the full rails project for this so you can peruse at your leisure.



If you’re interested in becoming a Xero Network partner and have a compelling solution, check out http://network.xero.com and apply to become part of our beta program.



Next I’ll go through modifying and adding a new contact to Xero



then the same treatment for invoices.



Also I want to try this out in other languages, might have to dust of my Java as it’s been a while, are there any other examples people want?



Source Files

Reader Comments (5)

I really don't have time to look much deeper, but you may be missing some foreign keys in your migrations.

August 9, 2008 | Unregistered CommenterAdam

Hey, thanks for this. I look forward to making use of it once we're ready to integrate with Xero.

Cheers, Galen

August 9, 2008 | Unregistered CommenterGalen King

true phone and addresses need to hold the contact id.

Anyway in my follow up post with HTTParty you don't need to rely on these migrations and the API is much much easier.

August 9, 2008 | Unregistered Commenterbuildmaster

I've taken a slightly different approach and written a xero_gateway GEM.

You can do stuff like
result xero_gateway.get_invoice("INV-0001")
if result.success?
result.invoice.phone.number
end

The GEM can be found here
http://github.com/tlconnor/xero_gateway" rel="nofollow">http://github.com/tlconnor/xero_gateway

December 1, 2008 | Unregistered CommenterTim

Different point of view from that post. Interesting to say the least.

June 11, 2009 | Unregistered CommenterChina Travel Deals

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
Post:
 
Some HTML allowed: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <code> <em> <i> <strike> <strong>