xero-min 0.0.9
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/Guardfile +8 -0
- data/README.md +88 -0
- data/Rakefile +2 -0
- data/lib/xero-min.rb +2 -0
- data/lib/xero-min/client.rb +145 -0
- data/lib/xero-min/erb.rb +42 -0
- data/lib/xero-min/templates/contact.xml.erb +7 -0
- data/lib/xero-min/templates/invoice.xml.erb +19 -0
- data/lib/xero-min/urls.rb +16 -0
- data/lib/xero-min/version.rb +3 -0
- data/spec/spec_helper.rb +5 -0
- data/spec/xero-min/client_spec.rb +143 -0
- data/spec/xero-min/erb_spec.rb +30 -0
- data/spec/xero-min/urls_spec.rb +12 -0
- data/spec/xero-min/utils_spec.rb +15 -0
- data/xero-min.gemspec +30 -0
- metadata +126 -0
data/.gitignore
ADDED
data/Guardfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,88 @@
|
|
1
|
+
tiny lib for xero
|
2
|
+
|
3
|
+
existing ruby xero libraires are crowded with models : Payroll, Contact, Invoice that map each xml structure available through api
|
4
|
+
|
5
|
+
But what if I dont care with Payroll or I have already one ... Will I have to build an XXX::Invoice with mine ?
|
6
|
+
|
7
|
+
Here is the minimal workflow for a POST request to xero :
|
8
|
+
|
9
|
+
* send a http request, with xml in params (in body for PUT, ohnoes oauth)
|
10
|
+
* sign request with oauth
|
11
|
+
* parse response
|
12
|
+
|
13
|
+
What this library does is the minimal wire, that is a functional api to GET|POST|PUT any xero call, with the following workflow
|
14
|
+
|
15
|
+
* use your model to build proper xml
|
16
|
+
* call xero-min
|
17
|
+
* parse response to get data your app need
|
18
|
+
|
19
|
+
Library was built and tested for a private app
|
20
|
+
|
21
|
+
Abstract
|
22
|
+
========
|
23
|
+
Uses
|
24
|
+
|
25
|
+
* typhoeus, to configure uri, headers, body, params
|
26
|
+
* nokogiri to parse response
|
27
|
+
|
28
|
+
Get some data
|
29
|
+
=============
|
30
|
+
You will get a Nokogiri node.
|
31
|
+
|
32
|
+
Then you can scrap it and extract what you require, no more, no less
|
33
|
+
|
34
|
+
extract [id, name] for each contact
|
35
|
+
-----------------------------------
|
36
|
+
doc = client.get! :contacts
|
37
|
+
doc.xpath('//Contact').map{|c| ['ContactID', 'Name'].map{|e| c.xpath("./#{e}").text}}
|
38
|
+
|
39
|
+
Post! some data
|
40
|
+
===============
|
41
|
+
lib is raw : you have to post well xml, as it is what xero understand
|
42
|
+
|
43
|
+
client.post! :contacts, body: xml
|
44
|
+
|
45
|
+
client.post! 'https://api.xero.com/api.xro/2.0/contacts', body: xml
|
46
|
+
|
47
|
+
What xml to post! or put! ?
|
48
|
+
---------------------------
|
49
|
+
XeroMin::Erb implements basic xml building
|
50
|
+
|
51
|
+
bill = {id: '4d73c0f91c94a2c47500000a', name: 'Billy', first_name: 'Bill', last_name: 'Kid', email: 'bill@kid.com'}
|
52
|
+
xml=erb.render contact: bill
|
53
|
+
|
54
|
+
and xml should be
|
55
|
+
|
56
|
+
<Contact>
|
57
|
+
<ContactNumber>4d73c0f91c94a2c47500000a</ContactNumber>
|
58
|
+
<Name>Billy</Name>
|
59
|
+
<FirstName>Bill</FirstName>
|
60
|
+
<LastName>Kid</LastName>
|
61
|
+
<EmailAddress>bill@kid.com</EmailAddress>
|
62
|
+
</Contact>
|
63
|
+
|
64
|
+
see XeroMin::Erb source code for precisions, templates for example, documentation
|
65
|
+
|
66
|
+
Use anything else you feel more comfortable with
|
67
|
+
|
68
|
+
Get!
|
69
|
+
====
|
70
|
+
doc = client.get! :contacts
|
71
|
+
|
72
|
+
doc = 'https://api.xero.com/api.xro/2.0/invoices'
|
73
|
+
|
74
|
+
doc = client.get! "#{client.url_for(:contacts)}/#{bill.id}"
|
75
|
+
|
76
|
+
|
77
|
+
What is the return value from post! or get! ?
|
78
|
+
=============================================
|
79
|
+
It is a Nokogiri node if post is success, extract what you need
|
80
|
+
|
81
|
+
invoice.ref = node.xpath('/Response/Invoices/Invoice/InvoiceNumber').first.content
|
82
|
+
|
83
|
+
Else, it raise a XeroMin::Problem with a message
|
84
|
+
|
85
|
+
Caveats
|
86
|
+
=======
|
87
|
+
use PUT to post data ... or use POST + params: {xml: xml} rather than body: xml with following patch : https://github.com/oauth/oauth-ruby/pull/24
|
88
|
+
|
data/Rakefile
ADDED
data/lib/xero-min.rb
ADDED
@@ -0,0 +1,145 @@
|
|
1
|
+
require 'oauth'
|
2
|
+
require 'oauth/signature/rsa/sha1'
|
3
|
+
require 'oauth/request_proxy/typhoeus_request'
|
4
|
+
require 'typhoeus'
|
5
|
+
require 'nokogiri'
|
6
|
+
require 'escape_utils'
|
7
|
+
|
8
|
+
require 'xero-min/urls'
|
9
|
+
|
10
|
+
module XeroMin
|
11
|
+
class Client
|
12
|
+
include XeroMin::Urls
|
13
|
+
|
14
|
+
@@signature = {
|
15
|
+
signature_method: 'HMAC-SHA1'
|
16
|
+
}
|
17
|
+
@@options = {
|
18
|
+
site: 'https://api.xero.com/api.xro/2.0',
|
19
|
+
request_token_path: "/oauth/RequestToken",
|
20
|
+
access_token_path: "/oauth/AccessToken",
|
21
|
+
authorize_path: "/oauth/Authorize",
|
22
|
+
headers: {
|
23
|
+
'Accept' => 'text/xml',
|
24
|
+
'Content-Type' => 'application/x-www-form-urlencoded; charset=utf-8'
|
25
|
+
}
|
26
|
+
}.merge(@@signature)
|
27
|
+
|
28
|
+
# Public : body is transformed using body_proc if present
|
29
|
+
# proc has one param
|
30
|
+
# defaults to `lambda{|body| EscapeUtils.escape_url(body)}`, that is url encode body
|
31
|
+
attr_accessor :body_proc
|
32
|
+
|
33
|
+
def initialize(consumer_key=nil, secret_key=nil, options={})
|
34
|
+
@options = @@options.merge(options)
|
35
|
+
@consumer_key, @secret_key = consumer_key , secret_key
|
36
|
+
self.body_proc = lambda{|body| EscapeUtils.escape_url(body)}
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public returns whether it has already requested an access token
|
40
|
+
def token?
|
41
|
+
!!@token
|
42
|
+
end
|
43
|
+
|
44
|
+
# Public : enables client to act as a private application
|
45
|
+
# resets previous access token if any
|
46
|
+
def private!(private_key_file='keys/xero.rsa')
|
47
|
+
@token = nil if token?
|
48
|
+
@options.merge!({signature_method: 'RSA-SHA1', private_key_file: private_key_file})
|
49
|
+
self
|
50
|
+
end
|
51
|
+
|
52
|
+
# Public : creates a signed request
|
53
|
+
# url of request is XeroMin::Urls.url_for sym_or_url, when string_or_url_for is not a String
|
54
|
+
# available options are the one of a Typhoeus::Request
|
55
|
+
# request is yielded to block if present
|
56
|
+
# first request ask for access token
|
57
|
+
def request(string_or_url_for, options={}, &block)
|
58
|
+
url = (string_or_url_for.is_a?(String) ? string_or_url_for : url_for(string_or_url_for))
|
59
|
+
options[:body] = body_proc.call(options[:body]) if (options[:body] and body_proc)
|
60
|
+
accept_option = options.delete(:accept)
|
61
|
+
if accept_option
|
62
|
+
options[:headers] ||= {}
|
63
|
+
options[:headers].merge! 'Accept' => accept_option
|
64
|
+
end
|
65
|
+
req = Typhoeus::Request.new(url, @options.merge(options))
|
66
|
+
|
67
|
+
# sign request with oauth
|
68
|
+
helper = OAuth::Client::Helper.new(req, @options.merge(consumer: token.consumer, token: token, request_uri: url))
|
69
|
+
req.headers.merge!({'Authorization' => helper.header})
|
70
|
+
yield req if block_given?
|
71
|
+
req
|
72
|
+
end
|
73
|
+
|
74
|
+
# Public : runs a request
|
75
|
+
def run(request=nil)
|
76
|
+
queue(request) if request
|
77
|
+
hydra.run
|
78
|
+
end
|
79
|
+
|
80
|
+
# Public : creates and runs a request and parse! its body
|
81
|
+
def request!(sym_or_url, options={}, &block)
|
82
|
+
parse!(request(sym_or_url, options, &block).tap{|r| run(r)}.response)
|
83
|
+
end
|
84
|
+
|
85
|
+
# Public: returns response body if Content-Type is application/pdf or a nokogiri node
|
86
|
+
def self.parse(response)
|
87
|
+
case content_type = response.headers_hash['Content-Type']
|
88
|
+
when 'application/pdf'
|
89
|
+
response.body
|
90
|
+
when %r(^text/xml)
|
91
|
+
Nokogiri::XML(response.body)
|
92
|
+
else
|
93
|
+
raise Problem, "Unsupported Content-Type : #{content_type}"
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
# try to doctorify failing response
|
98
|
+
def self.diagnose(response)
|
99
|
+
diagnosis = case response.code
|
100
|
+
when 400
|
101
|
+
Nokogiri::XML(response.body).xpath('//Message').to_a.map{|e| e.content}.uniq.join("\n")
|
102
|
+
when 401
|
103
|
+
EscapeUtils.unescape_url(response.body).gsub('&', "\n")
|
104
|
+
else
|
105
|
+
response.body
|
106
|
+
end
|
107
|
+
"code=#{response.code}\n#{diagnosis}\nbody=\n#{response.body}"
|
108
|
+
end
|
109
|
+
|
110
|
+
# Public : parse response or die if response fails
|
111
|
+
def parse!(response)
|
112
|
+
response.success?? Client.parse(response) : raise(Problem, Client.diagnose(response))
|
113
|
+
end
|
114
|
+
|
115
|
+
# Public : get, put, and post are shortcut for a request using this verb (question mark available)
|
116
|
+
[:get, :put, :post].each do |method|
|
117
|
+
module_eval <<-EOS, __FILE__, __LINE__ + 1
|
118
|
+
def #{method}(sym_or_url, options={}, &block)
|
119
|
+
request(sym_or_url, {method: :#{method}}.merge(options), &block)
|
120
|
+
end
|
121
|
+
def #{method}!(sym_or_url, options={}, &block)
|
122
|
+
request!(sym_or_url, {method: :#{method}}.merge(options), &block)
|
123
|
+
end
|
124
|
+
EOS
|
125
|
+
end
|
126
|
+
|
127
|
+
private
|
128
|
+
def hydra
|
129
|
+
@hydra ||= Typhoeus::Hydra.new
|
130
|
+
end
|
131
|
+
def queue(request)
|
132
|
+
hydra.queue(request)
|
133
|
+
self
|
134
|
+
end
|
135
|
+
def token
|
136
|
+
@token ||= OAuth::AccessToken.new(OAuth::Consumer.new(@consumer_key, @secret_key, @options),
|
137
|
+
@consumer_key, @secret_key)
|
138
|
+
end
|
139
|
+
def options
|
140
|
+
@options
|
141
|
+
end
|
142
|
+
end
|
143
|
+
class Problem < StandardError
|
144
|
+
end
|
145
|
+
end
|
data/lib/xero-min/erb.rb
ADDED
@@ -0,0 +1,42 @@
|
|
1
|
+
require 'erb'
|
2
|
+
|
3
|
+
module XeroMin
|
4
|
+
class Erb
|
5
|
+
# Public : dir where templates are looked for
|
6
|
+
attr_accessor :template_dir
|
7
|
+
# Public : post processing for raw erb
|
8
|
+
# default compacts xml, removing \n and spaces
|
9
|
+
attr_accessor :post_processing_proc
|
10
|
+
|
11
|
+
def initialize(template_dir=nil)
|
12
|
+
self.template_dir = template_dir || File.expand_path('../templates', __FILE__)
|
13
|
+
self.post_processing_proc = lambda {|xml| xml.gsub(%r((\s*)<), '<')}
|
14
|
+
end
|
15
|
+
|
16
|
+
# Public : renders a single entity with an infered template
|
17
|
+
# eg : render(contact: {first_name: 'me'}) will render #{template_dir}/contact.xml.erb with {first_name: 'me'} as lvar
|
18
|
+
def render(locals={})
|
19
|
+
erb = ERB.new(read_template(infered_template(locals.keys.first)))
|
20
|
+
inject_locals(locals)
|
21
|
+
post_processing_proc.call(self.instance_eval {erb.result binding})
|
22
|
+
end
|
23
|
+
|
24
|
+
def infered_template(sym)
|
25
|
+
"#{sym}.xml.erb"
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
def inject_locals(hash)
|
30
|
+
hash.each_pair do |key, value|
|
31
|
+
symbol = key.to_s
|
32
|
+
class << self;self;end.module_eval("attr_accessor :#{symbol}")
|
33
|
+
self.send "#{symbol}=", value
|
34
|
+
end
|
35
|
+
self
|
36
|
+
end
|
37
|
+
|
38
|
+
def read_template(template)
|
39
|
+
File.read(File.join(template_dir, template))
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<Contact>
|
2
|
+
<ContactNumber><%= contact[:id] %></ContactNumber>
|
3
|
+
<Name><%= contact[:name] %></Name>
|
4
|
+
<FirstName><%= contact[:first_name] %></FirstName>
|
5
|
+
<LastName><%= contact[:last_name] %></LastName>
|
6
|
+
<EmailAddress><%= contact[:email] %></EmailAddress>
|
7
|
+
</Contact>
|
@@ -0,0 +1,19 @@
|
|
1
|
+
<Invoice>
|
2
|
+
<Type>ACCREC</Type>
|
3
|
+
<Contact>
|
4
|
+
<ContactID><%= invoice[:contact][:id] %></ContactID>
|
5
|
+
</Contact>
|
6
|
+
<Date><%= Time.now.strftime('%Y-%m-%d') %></Date>
|
7
|
+
<DueDate><%= Time.now.strftime('%Y-%m-%d') %></DueDate>
|
8
|
+
<LineAmountTypes>Exclusive</LineAmountTypes>
|
9
|
+
<LineItems>
|
10
|
+
<% invoice[:items].each do |item| %>
|
11
|
+
<LineItem>
|
12
|
+
<Description><%= item[:description] %></Description>
|
13
|
+
<Quantity><%= item[:quantity] %></Quantity>
|
14
|
+
<UnitAmount><%= item[:price] %></UnitAmount>
|
15
|
+
<AccountCode><%= item[:ref] %></AccountCode>
|
16
|
+
</LineItem>
|
17
|
+
<% end %>
|
18
|
+
</LineItems>
|
19
|
+
</Invoice>
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module XeroMin
|
2
|
+
module Urls
|
3
|
+
# Public : use plural to get a collection or use singular and value
|
4
|
+
# url_for(:invoices) OR url_for(invoice: 'INV-001')
|
5
|
+
def url_for(sym_or_hash)
|
6
|
+
key, value = case sym_or_hash
|
7
|
+
when Hash
|
8
|
+
sym_or_hash.first
|
9
|
+
else
|
10
|
+
sym_or_hash
|
11
|
+
end
|
12
|
+
base = "https://api.xero.com/api.xro/2.0/#{key.to_s.capitalize}"
|
13
|
+
value ? "#{base}s/#{value}" : base
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,143 @@
|
|
1
|
+
#encoding: utf-8
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'xero-min/client'
|
4
|
+
require 'ostruct'
|
5
|
+
|
6
|
+
def google
|
7
|
+
'http://google.com'
|
8
|
+
end
|
9
|
+
|
10
|
+
def parse_authorization(header)
|
11
|
+
header['Authorization'].split(',').map{|s| s.strip.gsub("\"", '').split('=')}.reduce({}) {|acc, (k,v)| acc[k]=v; acc}
|
12
|
+
end
|
13
|
+
|
14
|
+
describe "#request" do
|
15
|
+
let(:client) {XeroMin::Client.new}
|
16
|
+
let(:xml) {'<Name>Vélo</Name>'}
|
17
|
+
it "yields request to block" do
|
18
|
+
headers = nil
|
19
|
+
request = client.request('https://api.xero.com/api.xro/2.0/Contacts') {|r| headers = r.headers}
|
20
|
+
headers.should == request.headers
|
21
|
+
end
|
22
|
+
it "can use plain url" do
|
23
|
+
client.request(google).url.should == google
|
24
|
+
end
|
25
|
+
it "can use a symbol for an url, using transformation XeroMin::Urls" do
|
26
|
+
client.stubs(:url_for).with(:google).returns(google)
|
27
|
+
client.request(:google).url.should == google
|
28
|
+
end
|
29
|
+
it "can initialize body" do
|
30
|
+
r = client.tap{|c| c.body_proc = nil}.request google, body: xml
|
31
|
+
r.body.should == xml
|
32
|
+
end
|
33
|
+
it "can use xml option to set body with urlencoded xml" do
|
34
|
+
r = client.request google, body: xml
|
35
|
+
r.body.should == '%3CName%3EV%C3%A9lo%3C%2FName%3E'
|
36
|
+
end
|
37
|
+
it "has default headers" do
|
38
|
+
request = client.request google
|
39
|
+
request.headers['Accept'].should == 'text/xml'
|
40
|
+
request.headers['Content-Type'].should == 'application/x-www-form-urlencoded; charset=utf-8'
|
41
|
+
end
|
42
|
+
it "handles :accept option as a shortcut to Accept header" do
|
43
|
+
request = client.request google, accept: 'application/pdf'
|
44
|
+
request.headers['Accept'].should == 'application/pdf'
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#request!" do
|
49
|
+
let(:client) {XeroMin::Client.new}
|
50
|
+
it "runs request and parse it" do
|
51
|
+
request = OpenStruct.new(response: OpenStruct.new(code: 200))
|
52
|
+
client.stubs(:request).with(google, {}).returns(request)
|
53
|
+
client.expects(:run).with(request)
|
54
|
+
client.expects(:parse!).with(request.response)
|
55
|
+
client.request!(google)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
[:get, :put, :post].each do |method|
|
60
|
+
describe "#{method}" do
|
61
|
+
let(:client) {XeroMin::Client.new}
|
62
|
+
it "uses #{method} method" do
|
63
|
+
r = client.send(method, google)
|
64
|
+
r.method.should == method
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
[:get, :put, :post].each do |method|
|
70
|
+
describe "#{method}!" do
|
71
|
+
let(:client) {XeroMin::Client.new}
|
72
|
+
it "executes a #{method} request!" do
|
73
|
+
client.stubs("request!").with(google, {method: method}).returns(404)
|
74
|
+
client.send("#{method}!", google).should == 404
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#body_proc" do
|
80
|
+
let(:client) {XeroMin::Client.new}
|
81
|
+
it "has default value has a url_encode function" do
|
82
|
+
client.body_proc.should_not be_nil
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
describe 'private #token' do
|
87
|
+
let(:key) {'key'}
|
88
|
+
let(:secret) {'secret'}
|
89
|
+
let(:consumer) {Object.new}
|
90
|
+
let(:token) {Object.new}
|
91
|
+
|
92
|
+
it 'lazily initialize token with appropriate parameters' do
|
93
|
+
OAuth::Consumer.stubs(:new).with(key, secret, anything).returns(consumer)
|
94
|
+
OAuth::AccessToken.stubs(:new).with(consumer, key, secret).returns(token)
|
95
|
+
|
96
|
+
XeroMin::Client.new(key, secret).send(:token).should == token
|
97
|
+
end
|
98
|
+
|
99
|
+
it 'reuse existing token' do
|
100
|
+
cli = XeroMin::Client.new
|
101
|
+
cli.send(:token).should be cli.send(:token)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe ".diagnose" do
|
106
|
+
it "reports oauth problem" do
|
107
|
+
body = "oauth_problem=signature_method_rejected&oauth_problem_advice=No%20certificates%20have%20been%20registered%20for%20the%20consumer"
|
108
|
+
response = OpenStruct.new(code: 401, body: body)
|
109
|
+
diagnosis = XeroMin::Client.diagnose(response)
|
110
|
+
assert {diagnosis =~ %r(code=401\noauth_problem=signature_method_rejected\noauth_problem_advice=No certificates have been registered for the consumer)}
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
describe "signature options" do
|
115
|
+
let(:cli) {XeroMin::Client.new}
|
116
|
+
it "default to public app behavior (HMAC-SHA1)" do
|
117
|
+
authorization = parse_authorization(cli.request(google).headers)
|
118
|
+
assert {authorization['oauth_signature_method'] == 'HMAC-SHA1'}
|
119
|
+
end
|
120
|
+
it "#private! resets headers if called after obtaining an access token" do
|
121
|
+
cli.request(google)
|
122
|
+
authorization = parse_authorization(cli.private!.request(google).headers)
|
123
|
+
assert {authorization['oauth_signature_method'] == 'RSA-SHA1'}
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
describe ".parse" do
|
128
|
+
it "returns raw content when ContentType is application/pdf" do
|
129
|
+
body = 'a.pdf'
|
130
|
+
response = OpenStruct.new(code: 401, body: body, headers_hash: {"Content-Type"=>"application/pdf"})
|
131
|
+
XeroMin::Client.parse(response).should be body
|
132
|
+
end
|
133
|
+
it "returns Nokogiri when ContentType is text/xml" do
|
134
|
+
body = '<foo>bar</foo>'
|
135
|
+
response = OpenStruct.new(code: 401, body: body, headers_hash: {"Content-Type"=>"text/xml; charset=utf-8"})
|
136
|
+
content = XeroMin::Client.parse(response)
|
137
|
+
content.should be_a Nokogiri::XML::Document
|
138
|
+
end
|
139
|
+
it "raises elsewhere (argh)" do
|
140
|
+
response = OpenStruct.new(code: 401, headers_hash: {"Content-Type"=>"application/json"})
|
141
|
+
expect {XeroMin::Client.parse(response)}.to raise_error(XeroMin::Problem, 'Unsupported Content-Type : application/json')
|
142
|
+
end
|
143
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
require 'spec_helper'
|
3
|
+
require 'xero-min/erb'
|
4
|
+
|
5
|
+
describe XeroMin::Erb do
|
6
|
+
let(:erb) {XeroMin::Erb.new}
|
7
|
+
describe "#template_dir" do
|
8
|
+
it "is default ./templates from source file" do
|
9
|
+
erb.template_dir.should =~ %r(xero-min/templates$)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe "#infered_template" do
|
14
|
+
[:contact, :invoice].each do |sym|
|
15
|
+
it "is #{sym}.xml.erb for #{sym}" do
|
16
|
+
erb.infered_template(sym).should == "#{sym}.xml.erb"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
describe "#render" do
|
22
|
+
let(:contact) {{name: 'Héloise Dupont', first_name: 'Héloise', last_name: 'Dupont', email: 'heloise@dupont.com'}}
|
23
|
+
it "should be nice" do
|
24
|
+
erb.render(contact: contact).should =~ %r(<Name>Héloise Dupont</Name>)
|
25
|
+
end
|
26
|
+
it "post processes erb output with #post_process_proc, that compacts xml" do
|
27
|
+
erb.render(contact: contact).should =~ %r(^<Contact><ContactNumber>)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
load 'xero-min/urls.rb'
|
3
|
+
|
4
|
+
describe XeroMin::Urls do
|
5
|
+
include XeroMin::Urls
|
6
|
+
{contact: 'Contact', contacts: 'Contacts'}.each do |k,v|
|
7
|
+
specify {url_for(k).should == "https://api.xero.com/api.xro/2.0/#{v}"}
|
8
|
+
end
|
9
|
+
it "can take a hash and and id" do
|
10
|
+
url_for(invoice: 'INV-001').should == "https://api.xero.com/api.xro/2.0/Invoices/INV-001"
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# describe 'extract_invoice_id' do
|
2
|
+
# it 'should extract InvoiceNumber from happy xml, under xpath' do
|
3
|
+
# xml = <<XML
|
4
|
+
# <Response>
|
5
|
+
# <Invoices>
|
6
|
+
# <Invoice>
|
7
|
+
# <InvoiceNumber>INV-0011</InvoiceNumber>
|
8
|
+
# </Invoice>
|
9
|
+
# </Invoices>
|
10
|
+
# </Response>
|
11
|
+
# XML
|
12
|
+
# response = HttpDuck.new(200, xml)
|
13
|
+
# @connector.extract_invoice_id(response).should == 'INV-0011'
|
14
|
+
# end
|
15
|
+
# end
|
data/xero-min.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.unshift File.expand_path("../lib", __FILE__)
|
3
|
+
require "xero-min/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "xero-min"
|
7
|
+
s.version = XeroMin::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["Thierry Henrio"]
|
10
|
+
s.email = ["thierry.henrio@gmail.com"]
|
11
|
+
s.homepage = "https://github.com/thierryhenrio/xero-min"
|
12
|
+
s.summary = <<-EOS
|
13
|
+
Minimal xero lib, no models, just wires
|
14
|
+
EOS
|
15
|
+
s.description = <<-EOS
|
16
|
+
Wires are oauth-ruby, typhoeus, nokogiri
|
17
|
+
EOS
|
18
|
+
s.rubyforge_project = "xero-min"
|
19
|
+
|
20
|
+
s.files = `git ls-files`.split("\n")
|
21
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
22
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
23
|
+
s.require_paths = ["lib"]
|
24
|
+
|
25
|
+
s.add_dependency 'oauth', '~> 0.4'
|
26
|
+
s.add_dependency 'nokogiri', '~> 1'
|
27
|
+
s.add_dependency 'typhoeus', '~> 0.2'
|
28
|
+
s.add_dependency 'escape_utils', '~> 0.2'
|
29
|
+
s.add_development_dependency 'rspec', '~> 2'
|
30
|
+
end
|
metadata
ADDED
@@ -0,0 +1,126 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: xero-min
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.9
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Thierry Henrio
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2011-05-17 00:00:00.000000000 +02:00
|
13
|
+
default_executable:
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: oauth
|
17
|
+
requirement: &2153946460 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ~>
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0.4'
|
23
|
+
type: :runtime
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *2153946460
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: nokogiri
|
28
|
+
requirement: &2153945960 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ~>
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '1'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *2153945960
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: typhoeus
|
39
|
+
requirement: &2153945500 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - ~>
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: '0.2'
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *2153945500
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: escape_utils
|
50
|
+
requirement: &2153945040 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ~>
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0.2'
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *2153945040
|
59
|
+
- !ruby/object:Gem::Dependency
|
60
|
+
name: rspec
|
61
|
+
requirement: &2153944580 !ruby/object:Gem::Requirement
|
62
|
+
none: false
|
63
|
+
requirements:
|
64
|
+
- - ~>
|
65
|
+
- !ruby/object:Gem::Version
|
66
|
+
version: '2'
|
67
|
+
type: :development
|
68
|
+
prerelease: false
|
69
|
+
version_requirements: *2153944580
|
70
|
+
description: ! ' Wires are oauth-ruby, typhoeus, nokogiri
|
71
|
+
|
72
|
+
'
|
73
|
+
email:
|
74
|
+
- thierry.henrio@gmail.com
|
75
|
+
executables: []
|
76
|
+
extensions: []
|
77
|
+
extra_rdoc_files: []
|
78
|
+
files:
|
79
|
+
- .gitignore
|
80
|
+
- Guardfile
|
81
|
+
- README.md
|
82
|
+
- Rakefile
|
83
|
+
- lib/xero-min.rb
|
84
|
+
- lib/xero-min/client.rb
|
85
|
+
- lib/xero-min/erb.rb
|
86
|
+
- lib/xero-min/templates/contact.xml.erb
|
87
|
+
- lib/xero-min/templates/invoice.xml.erb
|
88
|
+
- lib/xero-min/urls.rb
|
89
|
+
- lib/xero-min/version.rb
|
90
|
+
- spec/spec_helper.rb
|
91
|
+
- spec/xero-min/client_spec.rb
|
92
|
+
- spec/xero-min/erb_spec.rb
|
93
|
+
- spec/xero-min/urls_spec.rb
|
94
|
+
- spec/xero-min/utils_spec.rb
|
95
|
+
- xero-min.gemspec
|
96
|
+
has_rdoc: true
|
97
|
+
homepage: https://github.com/thierryhenrio/xero-min
|
98
|
+
licenses: []
|
99
|
+
post_install_message:
|
100
|
+
rdoc_options: []
|
101
|
+
require_paths:
|
102
|
+
- lib
|
103
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
104
|
+
none: false
|
105
|
+
requirements:
|
106
|
+
- - ! '>='
|
107
|
+
- !ruby/object:Gem::Version
|
108
|
+
version: '0'
|
109
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ! '>='
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
version: '0'
|
115
|
+
requirements: []
|
116
|
+
rubyforge_project: xero-min
|
117
|
+
rubygems_version: 1.5.2
|
118
|
+
signing_key:
|
119
|
+
specification_version: 3
|
120
|
+
summary: Minimal xero lib, no models, just wires
|
121
|
+
test_files:
|
122
|
+
- spec/spec_helper.rb
|
123
|
+
- spec/xero-min/client_spec.rb
|
124
|
+
- spec/xero-min/erb_spec.rb
|
125
|
+
- spec/xero-min/urls_spec.rb
|
126
|
+
- spec/xero-min/utils_spec.rb
|