xero-api 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f58c896bb749d9a48423d4af4b1a209b8ef3cce9aa94459b317e44b7b649f378
4
+ data.tar.gz: f738a3619bc044690bb01582849065b7ec253c183d03719a879af99952d555da
5
+ SHA512:
6
+ metadata.gz: 7b8e83ddc7cb421258f60fe62c1adf38febf43383317ec0994219906414de8106a320527c7478d7d8b0e71f4322326a80f8aeef6d5b263146b0ac62ed2b78611
7
+ data.tar.gz: 2187ac8e2deb3ad9cbd54104e99b4e0673e9676102eb677e916c03071cbc95ed7693e5c8c32c30c3e955bc7311ee57a5146b3ad1df887b81c994a0acfb056049
@@ -0,0 +1,4 @@
1
+ export XERO_API_CONSUMER_KEY=consumer-key-goes-here
2
+ export XERO_API_CONSUMER_SECRET=consumer-secret-goes-here
3
+ export XERO_API_ACCESS_TOKEN=access-token-goes-here
4
+ export XERO_API_ACCESS_TOKEN_SECRET=access-token-secret-goes-here
File without changes
data/.gitignore ADDED
@@ -0,0 +1,23 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /Gemfile.lock
4
+ /_yardoc/
5
+ /coverage/
6
+ /doc/
7
+ /pkg/
8
+ /spec/reports/
9
+ /tmp/
10
+ .env
11
+ .ruby-version
12
+ .ruby-gemset
13
+ .byebug_history
14
+ *.gem
15
+ .rspec
16
+ todo.txt
17
+ *.sess
18
+ *.log
19
+ spec/temp/spec_status.txt
20
+ *.swp
21
+ scrap.txt
22
+ README.md.html
23
+ .DS_Store
data/.travis.yml ADDED
@@ -0,0 +1,15 @@
1
+ sudo: false
2
+ language: ruby
3
+ rvm:
4
+ - 2.4.4
5
+
6
+ before_script:
7
+ - cp .env.example_app.oauth1 .env
8
+
9
+ script:
10
+ - bundle exec rspec spec/
11
+
12
+
13
+ notifications:
14
+ email:
15
+ - christian@minimul.com
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source 'https://rubygems.org'
2
+
3
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2018 Christian Pelczarski
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,214 @@
1
+ # The xero-api gem
2
+
3
+ ### Ruby client for the Xero API version 2.
4
+ - **Pure JSON-in / JSON-out.** No XML support.
5
+ - 4 main methods: **.get, .create, .update, and .delete**
6
+ - No validation rules built into the gem. **Validation comes from API only**.
7
+ - Close to the metal experience.
8
+ - First class logging.
9
+ - Robust error handling.
10
+ - Specs are built using real requests run directly against the Xero Demo Company. Thanks [VCR](https://github.com/vcr/vcr).
11
+ - Built leveraging [Faraday](https://github.com/lostisland/faraday).
12
+ - Built knowing that OAuth2 might be in the not-to-distant future.
13
+
14
+ ## Why another library when there are other more mature, established Ruby Xero libraries?
15
+
16
+ Both of the current de facto Ruby Xero client gems were built 6+ years ago when the Xero API was XML only, therefore, they are loaded with *XML cruft*.
17
+ For example, here are the total code line counts (of `.rb` files):
18
+
19
+ - Total LOC count of :
20
+ - **minimul/xero-api** => **910!** 🌈
21
+ - waynerobinson/xeroizer => 6019
22
+ - xero-gateway/xero_gateway => 5545
23
+
24
+ ## Ruby >= 2.4.0 required
25
+
26
+ ## Current Limitations
27
+
28
+ - Accounting API only.
29
+ - Only for public and partner Xero apps.
30
+
31
+ ## Installation
32
+
33
+ Add this line to your application's Gemfile:
34
+
35
+ ```ruby
36
+ gem 'xero_api'
37
+ ```
38
+
39
+ And then execute:
40
+
41
+ $ bundle
42
+
43
+ Or install it yourself as:
44
+
45
+ $ gem install xero-api
46
+
47
+
48
+ ## Initialize
49
+
50
+ ```ruby
51
+ creds = account.xero_account # or wherever you are storing the OAuth creds
52
+ xero_api = Xero::Api.new(token: creds.token,
53
+ token_secret: creds.secret,
54
+ consumer_key: '*****',
55
+ consumer_secret: '********')
56
+ ```
57
+
58
+ ## .get
59
+
60
+ ### Important
61
+ One queries the Xero API [mostly using URL parameters](https://developer.xero.com/documentation/api/requests-and-responses) so `xero-api` doesn't do a lot of "hand holding" and has the `params` argument enabling you to craft your queries as you see fit. Likewise, the `path` argument allows you to forge the URL path you desire. The `params` and `path` arguments are available on `.create`, `.update`, `delete`, and `.upload_attachment` as well.
62
+
63
+ ```ruby
64
+ # Basic get - retrieves first 100 contacts
65
+ resp = api.get(:contacts)
66
+ # Retrieves all contacts - returns Enumerator so you can do cool stuff
67
+ resp = api.get(:contacts, all: true)
68
+ p resp.count #=> 109
69
+ # Retrieves all contacts modified after a certain date
70
+ resp = api.get(:items, all: true, modified_since: Time.utc(2014, 01, 01))
71
+ # Retrieves only customers
72
+ resp = api.get(:contacts, params: { where: 'IsCustomer=true' })
73
+ # Retrieve by id
74
+ resp = api.get(:contacts, id: '323-43fss-4234dfa-3432233')
75
+ # Retrieve with custom path
76
+ resp = api.get(:users, path: '3138017f-8ddc-420e-a159-e7e1cf9e643d/History')
77
+ ```
78
+
79
+ See all the arguments for the [`.get` method](https://github.com/minimul/xero-api/blob/447170ff1035103ed251bf203cf95450bda0f377/lib/xero/api/methods.rb#L4).
80
+
81
+ ## .create
82
+
83
+ ```ruby
84
+ payload = {
85
+ "Type": "ACCREC",
86
+ "Contact": {
87
+ "ContactID": "f477ad8d-44f2-4bb7-a99b-04f28681e849"
88
+ },
89
+ "DateString": api.standard_date(Time.utc(2009, 05, 27)),
90
+ "DueDateString": api.standard_date(Time.utc(2009, 06, 06)),
91
+ "LineAmountTypes": "Exclusive",
92
+ "LineItems": [
93
+ {
94
+ "Description": "Consulting services as agreed (20% off standard rate)",
95
+ "Quantity": "10",
96
+ "UnitAmount": "100.00",
97
+ "AccountCode": "200",
98
+ "DiscountRate": "20"
99
+ }
100
+ ]
101
+ }
102
+ response = api.create(:invoice, payload: payload)
103
+ inv_num = response.dig("Invoices", 0, "InvoiceNumber")
104
+ p inv_num #=> 'INV-0041'
105
+ ```
106
+
107
+ ##### bulk .create
108
+ ```ruby
109
+ payload = { "Contacts": [] }
110
+ 60.times do
111
+ payload[:Contacts] << { "Name": Faker::Name.unique.name, "IsCustomer": true }
112
+ end
113
+ resp = api.create(:contacts, payload: payload, params: { summarizeErrors: false })
114
+ p resp.dig("Contacts").size #=> 60
115
+ ```
116
+
117
+ See all the arguments for the [`.create` method](https://github.com/minimul/xero-api/blob/447170ff1035103ed251bf203cf95450bda0f377/lib/xero/api/methods.rb#L14).
118
+
119
+ ## .update
120
+
121
+ ```ruby
122
+ payload = {
123
+ "InvoiceNumber": 'INV-0038',
124
+ "Status": 'VOIDED'
125
+ }
126
+ response = api.update(:invoices, id: 'INV-0038', payload: payload)
127
+ p response.dig("Invoices", 0, "Status") #=> VOIDED
128
+ ```
129
+
130
+ See all the arguments for the [`.update` method](https://github.com/minimul/xero-api/blob/447170ff1035103ed251bf203cf95450bda0f377/lib/xero/api/methods.rb#L19).
131
+
132
+ ## .delete
133
+
134
+ ```ruby
135
+ api.delete(:items, id: "e1d100f5-a602-4f0e-94b7-dc12e97b9bc2")
136
+ ```
137
+ See all the arguments for the [`.delete` method](https://github.com/minimul/xero-api/blob/447170ff1035103ed251bf203cf95450bda0f377/lib/xero/api/methods.rb#L25).
138
+
139
+ ## Configuration options
140
+ ```
141
+ - Logging:
142
+ ```ruby
143
+ Xero::Api.log = true
144
+ ```
145
+ - To change logging target from `$stdout` e.g.
146
+ ```ruby
147
+ Xero::Api.logger = Rails.logger
148
+ ```
149
+
150
+ ## Other stuff
151
+
152
+ ### .upload_attachment
153
+ ```ruby
154
+ file_name = 'connect_xero_button_blue_2x.png'
155
+ resp = api.upload_attachment(:invoices, id: '9eb7b996-4ac6-4cf8-8ee8-eb30d6e572e3',
156
+ file_name: file_name, content_type: 'image/png',
157
+ attachment: "#{__dir__}/../../../example/public/#{file_name}")
158
+ ```
159
+
160
+ ### Respond to an error
161
+ ```ruby
162
+ customer = { Name: 'Already Exists', EmailAddress: 'newone@already.com' }
163
+ begin
164
+ response = api.create(:contacts, payload: customer)
165
+ rescue Xero::Api::BadRequest => e
166
+ if e.message =~ /already exists/
167
+ # Query for Id using Name
168
+ resp = api.get(:contacts, params: { where: "Name='Already Exists'" })
169
+ # Do an update instead
170
+ up_resp = api.update(:contacts, id: resp["Id"], payload: payload)
171
+ end
172
+ end
173
+ ```
174
+
175
+ ### Spin up an example
176
+
177
+ 1. Follow and do all in Step 1 from the [Getting Started Guide](https://developer.xero.com/documentation/getting-started/getting-started-guide).
178
+ 1. `git clone git://github.com/minimul/xero-api && cd xero-api`
179
+ 1. `bundle`
180
+ 1. Create a `.env` file
181
+ 1. `cp .env.example_app.oauth1 .env`
182
+ 1. Edit the `.env` file values with `consumer_key` and `consumer_secret`.
183
+ 1. Start up the example app => `ruby example/oauth.rb`
184
+ 1. In browser go to `http://localhost:9393`.
185
+ 1. Use the `Connect to Xero` button to connect to your Xero account.
186
+ 1. After successfully connecting click on the displayed link => `View All Customers`
187
+ 1. Checkout [`example/oauth.rb`](https://github.com/minimul/xero-api/blob/master/example/oauth.rb)
188
+ to see what is going on under the hood.
189
+ - **Important:** In the [`/auth/xero/callback`](https://github.com/minimul/xero-api/blob/master/example/oauth.rb) route there is code there that will automatically update your `.env` file.
190
+
191
+ ### Protip: Once your .env file is completely filled out you can use the console to play around in your sandbox
192
+ ```
193
+ bin/console test
194
+ >> @xero_api.get :contacts, id: '5345-as543-4-afgafadsafsad-45334'
195
+ ```
196
+
197
+ ## Contributing
198
+
199
+ Bug reports and pull requests are welcome on GitHub at https://github.com/minimul/xero-api.
200
+
201
+ #### Running the specs
202
+ - `git clone git://github.com/minimul/xero-api && cd xero-api`
203
+ - `bundle`
204
+ - Create a `.env` file
205
+ - `cp .env.example_app.oauth1 .env`
206
+ - `bundle exec rake`
207
+
208
+ #### Creating new specs or modifying existing spec that have been recorded using the VCR gem.
209
+ - All specs that require interaction with the API must be recorded against the Xero Demo Company.
210
+
211
+ ## License
212
+
213
+ The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
214
+
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require_relative '../lib/xero/api'
5
+
6
+ if ARGV[0] == "test"
7
+ require_relative '../spec/support/credentials'
8
+ ARGV[0] = nil # needed to avoid irb error
9
+ instance_variable_set(:@xero_api, Xero::Api.new(creds.to_h))
10
+ end
11
+
12
+ # You can add fixtures and/or initialization code here to make experimenting
13
+ # with your gem easier. You can also use a different console, if you like.
14
+
15
+ # (If you use this, don't forget to add pry to your Gemfile!)
16
+ # require "pry"
17
+ # Pry.start
18
+
19
+ require "irb"
20
+ IRB.start
data/bin/setup ADDED
@@ -0,0 +1,7 @@
1
+ #!/bin/bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+
5
+ bundle install
6
+
7
+ # Do any other automated setup that you need to do here
data/example/base.rb ADDED
@@ -0,0 +1,36 @@
1
+ BASE_GEMS = proc do
2
+ gem 'xero-api', path: '.'
3
+ # This app
4
+ gem 'sinatra'
5
+ gem 'sinatra-contrib'
6
+
7
+ # Creds from ../.env
8
+ gem 'dotenv'
9
+ end
10
+
11
+ BASE_SETUP = proc do
12
+ # Webhook support
13
+ require 'json'
14
+ require 'openssl'
15
+ require 'base64'
16
+
17
+ Dotenv.load "#{__dir__}/../.env"
18
+ end
19
+
20
+ BASE_APP_CONFIG = proc do
21
+ PORT = ENV.fetch("PORT", 9393)
22
+
23
+ configure do
24
+ $VERBOSE = nil # silence redefined constant warning
25
+ register Sinatra::Reloader
26
+ end
27
+
28
+ set :port, PORT
29
+
30
+ helpers do
31
+ def base_url
32
+ "http://localhost:#{PORT}"
33
+ end
34
+ end
35
+
36
+ end
data/example/oauth.rb ADDED
@@ -0,0 +1,67 @@
1
+ require 'bundler/inline'
2
+
3
+ require File.expand_path(File.join('..', 'base'), __FILE__)
4
+
5
+ install_gems = true
6
+ gemfile(install_gems) do
7
+ source 'https://rubygems.org'
8
+
9
+ instance_eval(&BASE_GEMS)
10
+
11
+ gem 'simple_oauth'
12
+ gem 'omniauth'
13
+ gem 'omniauth-xero'
14
+ end
15
+
16
+ instance_eval(&BASE_SETUP)
17
+
18
+ class OAuthApp < Sinatra::Base
19
+ instance_eval(&BASE_APP_CONFIG)
20
+
21
+ CONSUMER_KEY = ENV['XERO_API_CONSUMER_KEY']
22
+ CONSUMER_SECRET = ENV['XERO_API_CONSUMER_SECRET']
23
+
24
+ use Rack::Session::Cookie, secret: '34233adasfqewrq453agqr9lasfa'
25
+ use OmniAuth::Builder do
26
+ provider :xero, CONSUMER_KEY, CONSUMER_SECRET
27
+ end
28
+
29
+ get '/' do
30
+ @auth_data = oauth_data
31
+ @port = PORT
32
+ erb :index
33
+ end
34
+
35
+ get '/customers' do
36
+ if session[:token]
37
+ api = Xero::Api.new(oauth_data)
38
+ @resp = api.get :contacts, all: true, params: { where: 'isCustomer=true' }
39
+ end
40
+ erb :customers
41
+ end
42
+
43
+ get '/auth/xero/callback' do
44
+ auth = env["omniauth.auth"][:credentials]
45
+ session[:token] = auth[:token]
46
+ session[:secret] = auth[:secret]
47
+ file_name = "#{__dir__}/../.env"
48
+ if env = File.read(file_name)
49
+ res = env.sub(/(XERO_API_ACCESS_TOKEN=)(.*)/, '\1' + session[:token])
50
+ res = res.sub(/(XERO_API_ACCESS_TOKEN_SECRET=)(.*)/, '\1' + session[:secret])
51
+ File.open(file_name, "w") {|file| file.puts res }
52
+ end
53
+ @url = base_url
54
+ erb :callback
55
+ end
56
+
57
+ def oauth_data
58
+ {
59
+ consumer_key: CONSUMER_KEY,
60
+ consumer_secret: CONSUMER_SECRET,
61
+ token: session[:token],
62
+ token_secret: session[:secret]
63
+ }
64
+ end
65
+ end
66
+
67
+ OAuthApp.run!
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <title>Callback page</title>
5
+ </head>
6
+ <body>
7
+ <h3>Redirecting ...</h3>
8
+ <script>
9
+ setTimeout(function(){
10
+ var url = '<%= @url %>';
11
+ if(window.opener == null){
12
+ window.location = url;
13
+ }else{
14
+ window.opener.location = url;
15
+ window.close();
16
+ }
17
+ }, 5000);
18
+ </script>
19
+ </body>
20
+ </html>
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>My Xero Customers</title>
6
+ </head>
7
+ <body>
8
+ <h2>Here's the code that is powering this view.</h2>
9
+ <pre>
10
+ api = Xero::Api.new(oauth_data)
11
+ @resp = api.get :customers, all: true, params: { where: 'isCustomer=true' }
12
+ </pre>
13
+ <h1>Customers within your Xero Account</h1>
14
+ <ul>
15
+ <% @resp.each do |c, index| %>
16
+ <li><strong><%= c['Name'] %></strong></li>
17
+ <% end %>
18
+ </ul>
19
+ </body>
20
+ </html>
@@ -0,0 +1,20 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Xero Connect</title>
6
+ </head>
7
+ <body>
8
+ <a href="<%= base_url %>/auth/xero">
9
+ <img src="/connect_xero_button_blue_2x.png" alt="Xero Blue Connect Button" />
10
+ </a>
11
+ <% if session[:token] %>
12
+ <ul>
13
+ <li>Token: <input type="text" value="<%= session[:token] %>" size="100" /></li>
14
+ <li>Secret: <input type="text" value="<%= session[:secret] %>" size="100" /></li>
15
+ <li><strong><a href="/customers">View All Customers</a></strong></li>
16
+ </ul>
17
+ <% end %>
18
+
19
+ </body>
20
+ </html>
data/lib/xero/api.rb ADDED
@@ -0,0 +1,55 @@
1
+ require 'json'
2
+ require 'uri'
3
+ require 'logger'
4
+ require_relative 'api/version'
5
+ require_relative 'api/configuration'
6
+ require_relative 'api/connection'
7
+ require_relative 'api/error'
8
+ require_relative 'api/raise_http_exception'
9
+ require_relative 'api/util'
10
+ require_relative 'api/attachment'
11
+ require_relative 'api/methods'
12
+
13
+ module Xero
14
+ class Api
15
+ extend Configuration
16
+ include Connection
17
+ include Util
18
+ include Attachment
19
+ include Methods
20
+
21
+ attr_accessor :endpoint
22
+
23
+ V2_ENDPOINT_BASE_URL = 'https://api.xero.com/api.xro/2.0/'
24
+ LOG_TAG = "[xero-api gem]"
25
+
26
+ def initialize(attributes = {})
27
+ raise Xero::Api::Error, "missing or blank keyword: token" unless attributes.key?(:token) and !attributes[:token].nil?
28
+ attributes = default_attributes.merge!(attributes)
29
+ attributes.each do |attribute, value|
30
+ public_send("#{attribute}=", value)
31
+ end
32
+ @endpoint_url = get_endpoint
33
+ end
34
+
35
+ def default_attributes
36
+ {
37
+ endpoint: :accounting
38
+ }
39
+ end
40
+
41
+ def connection(url: endpoint_url)
42
+ @connection ||= authorized_json_connection(url)
43
+ end
44
+
45
+ def endpoint_url
46
+ @endpoint_url.dup
47
+ end
48
+
49
+ private
50
+
51
+ def get_endpoint
52
+ V2_ENDPOINT_BASE_URL
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,26 @@
1
+ class Xero::Api
2
+ module Attachment
3
+
4
+ def upload_attachment(entity, id:, file_name:, content_type:, attachment:, include_online: false)
5
+ url = "#{entity_handler(entity)}/#{id}/Attachments/#{file_name}"
6
+ url += "?IncludeOnline=true" if include_online
7
+ headers = { 'Content-Type' => content_type, 'Accept' => 'application/json' }
8
+ raw_response = attachment_connection(headers: headers).post do |request|
9
+ request.url url
10
+ request.body = Faraday::UploadIO.new(attachment, content_type, file_name)
11
+ end
12
+ response(raw_response, entity: entity)
13
+ end
14
+
15
+ def attachment_connection(headers:)
16
+ build_connection(endpoint_url, headers: headers) do |conn|
17
+ add_authorization_middleware(conn)
18
+ add_exception_middleware(conn)
19
+ conn.request :url_encoded
20
+ add_connection_adapter(conn)
21
+ end
22
+ end
23
+
24
+ end
25
+ end
26
+
@@ -0,0 +1,21 @@
1
+ class Xero::Api
2
+ module Configuration
3
+
4
+ def logger
5
+ @logger ||= ::Logger.new($stdout)
6
+ end
7
+
8
+ def logger=(logger)
9
+ @logger = logger
10
+ end
11
+
12
+ def log
13
+ @log ||= false
14
+ end
15
+
16
+ def log=(value)
17
+ @log = value
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,114 @@
1
+ require 'faraday'
2
+ require 'faraday_middleware'
3
+ require 'faraday/detailed_logger'
4
+
5
+ class Xero::Api
6
+ module Connection
7
+ AUTHORIZATION_MIDDLEWARES = []
8
+
9
+ def Connection.add_authorization_middleware(strategy_name)
10
+ Connection::AUTHORIZATION_MIDDLEWARES << strategy_name
11
+ end
12
+
13
+ def authorized_json_connection(url, headers: nil)
14
+ headers ||= {}
15
+ headers['Accept'] ||= 'application/json' # required "we'll only accept JSON". Can be changed to any `+json` media type.
16
+ headers['Content-Type'] ||= 'application/json;charset=UTF-8' # required when request has a body, else harmless
17
+ build_connection(url, headers: headers) do |conn|
18
+ add_authorization_middleware(conn)
19
+ add_exception_middleware(conn)
20
+ conn.request :url_encoded
21
+ add_connection_adapter(conn)
22
+ end
23
+ end
24
+
25
+ def authorized_multipart_connection(url)
26
+ headers = { 'Content-Type' => 'multipart/form-data' }
27
+ build_connection(url, headers: headers) do |conn|
28
+ add_authorization_middleware(conn)
29
+ add_exception_middleware(conn)
30
+ conn.request :multipart
31
+ add_connection_adapter(conn)
32
+ end
33
+ end
34
+
35
+ def build_connection(url, headers: nil)
36
+ Faraday.new(url: url) { |conn|
37
+ conn.response :detailed_logger, Xero::Api.logger, LOG_TAG if Xero::Api.log
38
+ conn.headers.update(headers) if headers
39
+ yield conn if block_given?
40
+ }
41
+ end
42
+
43
+ def request(method, path:, entity: nil, payload: nil, headers: nil, parse_entity: false)
44
+ raw_response = raw_request(method, conn: connection, path: path, payload: payload, headers: headers)
45
+ response(raw_response, entity: entity, parse_entity: parse_entity)
46
+ end
47
+
48
+ def raw_request(method, conn:, path:, payload: nil, headers: nil)
49
+ conn.public_send(method) do |req|
50
+ req.headers.update(headers) if headers
51
+ case method
52
+ when :get, :delete
53
+ req.url path
54
+ when :post, :put
55
+ req.url path
56
+ req.body = payload.to_json
57
+ else raise Xero::Api::Error, "Unhandled request method '#{method.inspect}'"
58
+ end
59
+ end
60
+ end
61
+
62
+ def response(resp, entity: nil, parse_entity: false)
63
+ data = parse_response_body(resp)
64
+ parse_entity && entity ? entity_response(data, entity) : data
65
+ rescue => e
66
+ msg = "#{LOG_TAG} response parsing error: entity=#{entity.inspect} body=#{resp.body.inspect} exception=#{e.inspect}"
67
+ Xero::Api.logger.debug { msg }
68
+ data
69
+ end
70
+
71
+ def parse_response_body(resp)
72
+ body = resp.body
73
+ case resp.headers['Content-Type']
74
+ when /json/ then JSON.parse(body)
75
+ else body
76
+ end
77
+ end
78
+
79
+ private
80
+
81
+ def entity_response(data, entity)
82
+ entity_name = entity_handler(entity)
83
+ entity_body = data
84
+ entity_body.fetch(entity_name) do
85
+ msg = "#{LOG_TAG} entity name not in that top-level of the response body: entity_name=#{entity_name}"
86
+ Xero::Api.logger.debug { msg }
87
+ data
88
+ end
89
+ end
90
+
91
+ def add_connection_adapter(conn)
92
+ conn.adapter Faraday.default_adapter
93
+ end
94
+
95
+ def add_exception_middleware(conn)
96
+ conn.use FaradayMiddleware::RaiseHttpException
97
+ end
98
+
99
+ def add_authorization_middleware(conn)
100
+ Connection::AUTHORIZATION_MIDDLEWARES.find(proc do
101
+ raise Xero::Api::Error, 'Add a configured authorization_middleware'
102
+ end) do |strategy_name|
103
+ next unless public_send("use_#{strategy_name}_middleware?")
104
+ public_send("add_#{strategy_name}_authorization_middleware", conn)
105
+ true
106
+ end
107
+ end
108
+
109
+ require_relative 'connection/oauth1'
110
+ include OAuth1
111
+ require_relative 'connection/oauth2'
112
+ include OAuth2
113
+ end
114
+ end
@@ -0,0 +1,51 @@
1
+ module Xero
2
+ class Api
3
+ OAUTH1_BASE = 'https://api.xero.com/oauth'
4
+ OAUTH1_UNAUTHORIZED = OAUTH1_BASE + '/RequestToken'
5
+ OAUTH1_REDIRECT = OAUTH1_BASE + '/Authorize'
6
+ OAUTH1_ACCESS_TOKEN = OAUTH1_BASE + '/AccessToken'
7
+
8
+ attr_accessor :token, :token_secret
9
+ attr_accessor :consumer_key, :consumer_secret
10
+
11
+ module Connection::OAuth1
12
+
13
+ def self.included(*)
14
+ Xero::Api::Connection.add_authorization_middleware :oauth1
15
+ super
16
+ end
17
+
18
+ def default_attributes
19
+ super.merge!(
20
+ token: nil, token_secret: nil,
21
+ consumer_key: defined?(CONSUMER_KEY) ? CONSUMER_KEY : nil,
22
+ consumer_secret: defined?(CONSUMER_SECRET) ? CONSUMER_SECRET : nil,
23
+ )
24
+ end
25
+
26
+ def add_oauth1_authorization_middleware(conn)
27
+ gem 'simple_oauth'
28
+ require 'simple_oauth'
29
+ conn.request :oauth, oauth_data
30
+ end
31
+
32
+ def use_oauth1_middleware?
33
+ token != nil
34
+ end
35
+
36
+ private
37
+
38
+ # Use with simple_oauth OAuth1 middleware
39
+ # @see #add_authorization_middleware
40
+ def oauth_data
41
+ {
42
+ consumer_key: @consumer_key,
43
+ consumer_secret: @consumer_secret,
44
+ token: @token,
45
+ token_secret: @token_secret
46
+ }
47
+ end
48
+
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,24 @@
1
+ class Xero::Api
2
+ attr_accessor :access_token
3
+
4
+ module Connection::OAuth2
5
+
6
+ def self.included(*)
7
+ Xero::Api::Connection.add_authorization_middleware :oauth2
8
+ super
9
+ end
10
+
11
+ def default_attributes
12
+ super.merge!(
13
+ access_token: nil
14
+ )
15
+ end
16
+ def add_oauth2_authorization_middleware(conn)
17
+ conn.request :oauth2, access_token, token_type: 'bearer'
18
+ end
19
+
20
+ def use_oauth2_middleware?
21
+ access_token != nil
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,33 @@
1
+
2
+ class Xero::Api
3
+ class Error < StandardError
4
+ attr_reader :fault
5
+ def initialize(errors = nil)
6
+ if errors
7
+ @fault = errors
8
+ super(errors)
9
+ end
10
+ end
11
+ end
12
+
13
+ # Raised on HTTP status code 400
14
+ class BadRequest < Error; end
15
+
16
+ # Raised on HTTP status code 401
17
+ class Unauthorized < Error; end
18
+
19
+ # Raised on HTTP status code 404
20
+ class NotFound < Error; end
21
+
22
+ # Raised on HTTP status code 412
23
+ class PreconditionFailed < Error; end
24
+
25
+ # Raised on HTTP status code 500
26
+ class InternalError < Error; end
27
+
28
+ # Raised on HTTP status code 501
29
+ class NotImplemented < Error; end
30
+
31
+ # Raised on HTTP status code 503
32
+ class ServiceUnavailable < Error; end
33
+ end
@@ -0,0 +1,67 @@
1
+ class Xero::Api
2
+ module Methods
3
+
4
+ def get(entity, all: false, id: nil, params: nil, headers: nil, path: nil, modified_since: nil, parse_entity: true)
5
+ route = build_resource(entity, id: id, params: params, path: path)
6
+ final_headers = handle_headers(headers, modified_since)
7
+ if all
8
+ enumerator = get_all(entity, path: route, headers: final_headers, parse_entity: parse_entity)
9
+ else
10
+ request(:get, path: route, entity: entity, headers: final_headers, parse_entity: parse_entity)
11
+ end
12
+ end
13
+
14
+ def create(entity, payload:, params: nil, path: nil)
15
+ route = build_resource(entity, params: params, path: path)
16
+ request(:put, path: route, entity: entity, payload: payload)
17
+ end
18
+
19
+ def update(entity, id:, payload:, params: nil, path: nil)
20
+ route = build_resource(entity, id: id, params: params, path: path)
21
+ payload.merge!({ "Id": id })
22
+ request(:post, path: route, entity: entity, payload: payload)
23
+ end
24
+
25
+ def delete(entity, id:, params: nil, path: nil)
26
+ route = build_resource(entity, id: id, path: path)
27
+ request(:delete, path: route, entity: entity)
28
+ end
29
+
30
+ private
31
+
32
+ def build_resource(entity, id: nil, params: nil, path: nil)
33
+ route = entity_handler(entity)
34
+ route = "#{route}/#{id}" if id
35
+ route = "#{route}/#{path}" if path
36
+ route = add_params(route: route, params: params) if params
37
+ route
38
+ end
39
+
40
+ def handle_headers(headers, modified_since)
41
+ h = {}
42
+ h.merge!(headers) if headers
43
+ h.merge!(if_modified_hash(modified_since)) if modified_since
44
+ h
45
+ end
46
+
47
+ def if_modified_hash(modified_since)
48
+ { 'If-Modified-Since' => standard_date(modified_since) }
49
+ end
50
+
51
+ def get_all(entity, path:, headers:, parse_entity:)
52
+ max = 100
53
+ Enumerator.new do |enum_yielder|
54
+ number = 0
55
+ begin
56
+ number += 1
57
+ paged_path = add_params(route: path, params: { page: number })
58
+ results = request(:get, path: paged_path, entity: entity, headers: headers, parse_entity: parse_entity)
59
+ results.each do |result|
60
+ enum_yielder.yield(result)
61
+ end if results
62
+ end while (results ? results.size == max : false)
63
+ end
64
+ end
65
+
66
+ end
67
+ end
@@ -0,0 +1,42 @@
1
+ require 'faraday'
2
+
3
+ # @private
4
+ module FaradayMiddleware
5
+ # @private
6
+ class RaiseHttpException < Faraday::Middleware
7
+ def call(env)
8
+ @app.call(env).on_complete do |response|
9
+ case response.status
10
+ when 200
11
+ when 204
12
+ when 400
13
+ raise Xero::Api::BadRequest.new(error_message(response))
14
+ when 401
15
+ raise Xero::Api::Unauthorized.new(error_message(response))
16
+ when 404
17
+ raise Xero::Api::NotFound.new(error_message(response))
18
+ when 412
19
+ raise Xero::Api::PreconditionFailed.new(error_message(response))
20
+ when 500
21
+ raise Xero::Api::InternalError.new(error_message(response))
22
+ when 501
23
+ raise Xero::Api::NotImplemented.new(error_message(response))
24
+ when 503
25
+ raise Xero::Api::ServiceUnavailable.new(error_message(response))
26
+ end
27
+ end
28
+ end
29
+
30
+ def initialize(app)
31
+ super app
32
+ end
33
+
34
+ private
35
+
36
+ def error_message(response)
37
+ error = ::JSON.parse(response.body)
38
+ rescue => e
39
+ response.body
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,54 @@
1
+ class Xero::Api
2
+ module Util
3
+
4
+ def add_params(route:, params:)
5
+ uri = URI.parse(route)
6
+ params.each do |p|
7
+ new_query_ar = URI.decode_www_form(uri.query || '') << p.to_a
8
+ uri.query = URI.encode_www_form(new_query_ar)
9
+ end
10
+ uri.to_s
11
+ end
12
+
13
+ def standard_date(date)
14
+ date.strftime('%Y-%m-%dT%H:%M:%S')
15
+ rescue => e
16
+ raise Xero::Api::Error, date_method_error_msg(e)
17
+ end
18
+
19
+ def json_date(date)
20
+ date.strftime("/Date(%s%L)/")
21
+ rescue => e
22
+ raise Xero::Api::Error, date_method_error_msg(e)
23
+ end
24
+
25
+ def parse_json_date(datestring)
26
+ seconds_since_epoch = datestring.scan(/[0-9]+/)[0].to_i / 1000.0
27
+ Time.at(seconds_since_epoch)
28
+ end
29
+
30
+ def entity_handler(entity)
31
+ if entity.is_a?(Symbol)
32
+ snake_to_camel(entity)
33
+ else
34
+ entity
35
+ end
36
+ end
37
+
38
+ def snake_to_camel(sym)
39
+ sym.to_s.split('_').collect(&:capitalize).join
40
+ end
41
+
42
+ private
43
+
44
+ def date_method_error_msg(e)
45
+ if e.message =~ /undefined method \`strftime/
46
+ "The argument needs to be an instance of Date|Time|DateTime"
47
+ else
48
+ e.message
49
+ end
50
+ end
51
+
52
+ end
53
+ end
54
+
@@ -0,0 +1,5 @@
1
+ module Xero
2
+ class Api
3
+ VERSION = "1.0.0"
4
+ end
5
+ end
data/xero-api.gemspec ADDED
@@ -0,0 +1,33 @@
1
+ # coding: utf-8
2
+ lib = File.expand_path('../lib', __FILE__)
3
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
4
+ require 'xero/api/version'
5
+
6
+ Gem::Specification.new do |spec|
7
+ spec.name = "xero-api"
8
+ spec.version = Xero::Api::VERSION
9
+ spec.authors = ["Christian Pelczarski"]
10
+ spec.email = ["christian@minimul.com"]
11
+
12
+ spec.summary = %q{Ruby JSON-only client for Xero API. }
13
+ spec.homepage = "https://github.com/minimul/xero-api"
14
+ spec.license = "MIT"
15
+
16
+ spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
17
+ spec.bindir = "exe"
18
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
19
+ spec.require_paths = ["lib"]
20
+
21
+ spec.add_development_dependency "bundler", "~> 1.10"
22
+ spec.add_development_dependency "rake", "~> 10.0"
23
+ spec.add_development_dependency "rspec"
24
+ spec.add_development_dependency 'webmock'
25
+ spec.add_development_dependency 'faker'
26
+ spec.add_development_dependency 'simple_oauth'
27
+ spec.add_development_dependency 'dotenv'
28
+ spec.add_development_dependency 'vcr'
29
+ spec.add_development_dependency 'awesome_print'
30
+ spec.add_runtime_dependency 'faraday'
31
+ spec.add_runtime_dependency 'faraday_middleware'
32
+ spec.add_runtime_dependency 'faraday-detailed_logger'
33
+ end
metadata ADDED
@@ -0,0 +1,240 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: xero-api
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Christian Pelczarski
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2018-10-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.10'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.10'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '10.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '10.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rspec
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: webmock
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: faker
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: simple_oauth
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: dotenv
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ - !ruby/object:Gem::Dependency
112
+ name: vcr
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '0'
125
+ - !ruby/object:Gem::Dependency
126
+ name: awesome_print
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - ">="
130
+ - !ruby/object:Gem::Version
131
+ version: '0'
132
+ type: :development
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - ">="
137
+ - !ruby/object:Gem::Version
138
+ version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: faraday
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :runtime
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
153
+ - !ruby/object:Gem::Dependency
154
+ name: faraday_middleware
155
+ requirement: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ type: :runtime
161
+ prerelease: false
162
+ version_requirements: !ruby/object:Gem::Requirement
163
+ requirements:
164
+ - - ">="
165
+ - !ruby/object:Gem::Version
166
+ version: '0'
167
+ - !ruby/object:Gem::Dependency
168
+ name: faraday-detailed_logger
169
+ requirement: !ruby/object:Gem::Requirement
170
+ requirements:
171
+ - - ">="
172
+ - !ruby/object:Gem::Version
173
+ version: '0'
174
+ type: :runtime
175
+ prerelease: false
176
+ version_requirements: !ruby/object:Gem::Requirement
177
+ requirements:
178
+ - - ">="
179
+ - !ruby/object:Gem::Version
180
+ version: '0'
181
+ description:
182
+ email:
183
+ - christian@minimul.com
184
+ executables: []
185
+ extensions: []
186
+ extra_rdoc_files: []
187
+ files:
188
+ - ".env.example_app.oauth1"
189
+ - ".env.example_app.oauth2"
190
+ - ".gitignore"
191
+ - ".travis.yml"
192
+ - Gemfile
193
+ - LICENSE.txt
194
+ - README.md
195
+ - Rakefile
196
+ - bin/console
197
+ - bin/setup
198
+ - example/base.rb
199
+ - example/oauth.rb
200
+ - example/public/connect_xero_button_blue_2x.png
201
+ - example/views/callback.erb
202
+ - example/views/customers.erb
203
+ - example/views/index.erb
204
+ - lib/xero/api.rb
205
+ - lib/xero/api/attachment.rb
206
+ - lib/xero/api/configuration.rb
207
+ - lib/xero/api/connection.rb
208
+ - lib/xero/api/connection/oauth1.rb
209
+ - lib/xero/api/connection/oauth2.rb
210
+ - lib/xero/api/error.rb
211
+ - lib/xero/api/methods.rb
212
+ - lib/xero/api/raise_http_exception.rb
213
+ - lib/xero/api/util.rb
214
+ - lib/xero/api/version.rb
215
+ - xero-api.gemspec
216
+ homepage: https://github.com/minimul/xero-api
217
+ licenses:
218
+ - MIT
219
+ metadata: {}
220
+ post_install_message:
221
+ rdoc_options: []
222
+ require_paths:
223
+ - lib
224
+ required_ruby_version: !ruby/object:Gem::Requirement
225
+ requirements:
226
+ - - ">="
227
+ - !ruby/object:Gem::Version
228
+ version: '0'
229
+ required_rubygems_version: !ruby/object:Gem::Requirement
230
+ requirements:
231
+ - - ">="
232
+ - !ruby/object:Gem::Version
233
+ version: '0'
234
+ requirements: []
235
+ rubyforge_project:
236
+ rubygems_version: 2.7.7
237
+ signing_key:
238
+ specification_version: 4
239
+ summary: Ruby JSON-only client for Xero API.
240
+ test_files: []