xero-min 0.0.9
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|