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.
@@ -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