xero-min 0.0.9

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ *.gem
2
+ .bundle
3
+ .rvmrc
4
+ Gemfile.lock
5
+ pkg/*
6
+ keys/
7
+ data/
@@ -0,0 +1,8 @@
1
+ # A sample Guardfile
2
+ # More info at https://github.com/guard/guard#readme
3
+
4
+ guard 'rspec', version: 2 do
5
+ watch(%r{^spec/.+_spec\.rb})
6
+ watch(%r{^lib/(.+)\.rb}) { |m| "spec/#{m[1]}_spec.rb" }
7
+ watch('spec/spec_helper.rb') { "spec" }
8
+ end
@@ -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
+
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,2 @@
1
+ require 'xero-min/client'
2
+ require 'xero-min/erb'
@@ -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
@@ -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
@@ -0,0 +1,3 @@
1
+ module XeroMin
2
+ VERSION = "0.0.9"
3
+ end
@@ -0,0 +1,5 @@
1
+ require 'rspec'
2
+ require 'wrong/adapters/rspec'
3
+ RSpec.configure do |config|
4
+ config.mock_with :mocha
5
+ end
@@ -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
@@ -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