xero-api 1.0.0

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.
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: []