Connecting to the Xero API: Part 1 [Ruby, GET, Contacts]
Wednesday, July 30, 2008 at 5:39PM 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?
Owen Evans |
5 Comments |
Reader Comments (5)
I really don't have time to look much deeper, but you may be missing some foreign keys in your migrations.
Hey, thanks for this. I look forward to making use of it once we're ready to integrate with Xero.
Cheers, Galen
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.
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
Different point of view from that post. Interesting to say the least.