freshbooks.rb 3.0.13
Sign up to get free protection for your applications and to get access to all the features.
- data/History.txt +8 -0
- data/LICENSE +10 -0
- data/Manifest.txt +45 -0
- data/README +44 -0
- data/Rakefile +27 -0
- data/lib/freshbooks.rb +94 -0
- data/lib/freshbooks/base.rb +169 -0
- data/lib/freshbooks/category.rb +11 -0
- data/lib/freshbooks/client.rb +22 -0
- data/lib/freshbooks/connection.rb +153 -0
- data/lib/freshbooks/estimate.rb +15 -0
- data/lib/freshbooks/expense.rb +12 -0
- data/lib/freshbooks/invoice.rb +18 -0
- data/lib/freshbooks/item.rb +11 -0
- data/lib/freshbooks/line.rb +10 -0
- data/lib/freshbooks/links.rb +7 -0
- data/lib/freshbooks/list_proxy.rb +80 -0
- data/lib/freshbooks/payment.rb +13 -0
- data/lib/freshbooks/project.rb +12 -0
- data/lib/freshbooks/recurring.rb +15 -0
- data/lib/freshbooks/response.rb +25 -0
- data/lib/freshbooks/schema/definition.rb +20 -0
- data/lib/freshbooks/schema/mixin.rb +40 -0
- data/lib/freshbooks/staff.rb +13 -0
- data/lib/freshbooks/task.rb +12 -0
- data/lib/freshbooks/time_entry.rb +12 -0
- data/lib/freshbooks/xml_serializer.rb +17 -0
- data/lib/freshbooks/xml_serializer/serializers.rb +109 -0
- data/script/console +10 -0
- data/script/destroy +14 -0
- data/script/generate +14 -0
- data/test/fixtures/freshbooks_credentials.sample.yml +3 -0
- data/test/fixtures/invoice_create_response.xml +4 -0
- data/test/fixtures/invoice_get_response.xml +54 -0
- data/test/fixtures/invoice_list_response.xml +109 -0
- data/test/fixtures/success_response.xml +2 -0
- data/test/mock_connection.rb +13 -0
- data/test/schema/test_definition.rb +36 -0
- data/test/schema/test_mixin.rb +39 -0
- data/test/test_base.rb +97 -0
- data/test/test_connection.rb +145 -0
- data/test/test_helper.rb +48 -0
- data/test/test_invoice.rb +125 -0
- data/test/test_list_proxy.rb +60 -0
- data/test/test_page.rb +50 -0
- metadata +148 -0
data/History.txt
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,10 @@
|
|
1
|
+
= License
|
2
|
+
|
3
|
+
Copyright (c) 2007 Ben Vinegar
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
6
|
+
|
7
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
8
|
+
|
9
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
10
|
+
|
data/Manifest.txt
ADDED
@@ -0,0 +1,45 @@
|
|
1
|
+
History.txt
|
2
|
+
LICENSE
|
3
|
+
Manifest.txt
|
4
|
+
README
|
5
|
+
Rakefile
|
6
|
+
lib/freshbooks.rb
|
7
|
+
lib/freshbooks/base.rb
|
8
|
+
lib/freshbooks/category.rb
|
9
|
+
lib/freshbooks/client.rb
|
10
|
+
lib/freshbooks/connection.rb
|
11
|
+
lib/freshbooks/estimate.rb
|
12
|
+
lib/freshbooks/expense.rb
|
13
|
+
lib/freshbooks/invoice.rb
|
14
|
+
lib/freshbooks/item.rb
|
15
|
+
lib/freshbooks/line.rb
|
16
|
+
lib/freshbooks/links.rb
|
17
|
+
lib/freshbooks/list_proxy.rb
|
18
|
+
lib/freshbooks/payment.rb
|
19
|
+
lib/freshbooks/project.rb
|
20
|
+
lib/freshbooks/recurring.rb
|
21
|
+
lib/freshbooks/response.rb
|
22
|
+
lib/freshbooks/schema/definition.rb
|
23
|
+
lib/freshbooks/schema/mixin.rb
|
24
|
+
lib/freshbooks/staff.rb
|
25
|
+
lib/freshbooks/task.rb
|
26
|
+
lib/freshbooks/time_entry.rb
|
27
|
+
lib/freshbooks/xml_serializer.rb
|
28
|
+
lib/freshbooks/xml_serializer/serializers.rb
|
29
|
+
script/console
|
30
|
+
script/destroy
|
31
|
+
script/generate
|
32
|
+
test/fixtures/freshbooks_credentials.sample.yml
|
33
|
+
test/fixtures/invoice_create_response.xml
|
34
|
+
test/fixtures/invoice_get_response.xml
|
35
|
+
test/fixtures/invoice_list_response.xml
|
36
|
+
test/fixtures/success_response.xml
|
37
|
+
test/mock_connection.rb
|
38
|
+
test/schema/test_definition.rb
|
39
|
+
test/schema/test_mixin.rb
|
40
|
+
test/test_base.rb
|
41
|
+
test/test_connection.rb
|
42
|
+
test/test_helper.rb
|
43
|
+
test/test_invoice.rb
|
44
|
+
test/test_list_proxy.rb
|
45
|
+
test/test_page.rb
|
data/README
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
= About
|
2
|
+
|
3
|
+
FreshBooks.rb is a Ruby interface to the FreshBooks API. It exposes easy-to-use classes and methods for interacting with your FreshBooks account.
|
4
|
+
|
5
|
+
NOTE: These examples are out of date and need to be updated. I will be writing documentation for all the updates soon and will be pushing the changes to rubyforge in the near future.
|
6
|
+
|
7
|
+
= Examples
|
8
|
+
|
9
|
+
Initialization:
|
10
|
+
|
11
|
+
FreshBooks::Base.establish_connection('sample.freshbooks.com', 'mytoken')
|
12
|
+
|
13
|
+
Updating a client name:
|
14
|
+
|
15
|
+
clients = FreshBooks::Client.list
|
16
|
+
client = clients[0]
|
17
|
+
client.first_name = 'Suzy'
|
18
|
+
client.update
|
19
|
+
|
20
|
+
Updating an invoice:
|
21
|
+
|
22
|
+
invoice = FreshBooks::Invoice.get(4)
|
23
|
+
invoice.lines[0].quantity += 1
|
24
|
+
invoice.update
|
25
|
+
|
26
|
+
Creating a new item
|
27
|
+
|
28
|
+
item = FreshBooks::Item.new
|
29
|
+
item.name = 'A sample item'
|
30
|
+
item.create
|
31
|
+
|
32
|
+
= License
|
33
|
+
|
34
|
+
This work is distributed under the MIT License. Use/modify the code however you like.
|
35
|
+
|
36
|
+
= Download
|
37
|
+
|
38
|
+
gem sources -a http://gems.github.com
|
39
|
+
sudo gem install bcurren-freshbooks.rb
|
40
|
+
|
41
|
+
= Credits
|
42
|
+
|
43
|
+
FreshBooks.rb is written and maintained by Ben Curren at Outright.com. Ben Vinegar was the original developer and we have taken over maintenance of the gem from now on.
|
44
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
%w[rubygems rake rake/clean fileutils newgem rubigen hoe].each { |f| require f }
|
2
|
+
require File.dirname(__FILE__) + '/lib/freshbooks'
|
3
|
+
|
4
|
+
# Generate all the Rake tasks
|
5
|
+
# Run 'rake -T' to see list of generated tasks (from gem root directory)
|
6
|
+
$hoe = Hoe.spec('freshbooks.rb') do |p|
|
7
|
+
p.developer('Ben Curren', 'ben@outright.com')
|
8
|
+
p.summary = ''
|
9
|
+
p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
|
10
|
+
p.rubyforge_name = p.name # TODO this is default value
|
11
|
+
p.extra_deps = [ ['activesupport', '>= 0'] ]
|
12
|
+
p.extra_dev_deps = [
|
13
|
+
['newgem', ">= #{::Newgem::VERSION}"],
|
14
|
+
['mocha', ">= 0.9.4"]
|
15
|
+
]
|
16
|
+
|
17
|
+
p.clean_globs |= %w[**/.DS_Store tmp *.log]
|
18
|
+
path = (p.rubyforge_name == p.name) ? p.rubyforge_name : "\#{p.rubyforge_name}/\#{p.name}"
|
19
|
+
p.remote_rdoc_dir = File.join(path.gsub(/^#{p.rubyforge_name}\/?/,''), 'rdoc')
|
20
|
+
p.rsync_args = '-av --delete --ignore-errors'
|
21
|
+
end
|
22
|
+
|
23
|
+
require 'newgem/tasks' # load /tasks/*.rake
|
24
|
+
Dir['tasks/**/*.rake'].each { |t| load t }
|
25
|
+
|
26
|
+
# TODO - want other tests/tasks run by default? Add them to the list
|
27
|
+
# task :default => [:spec, :features]
|
data/lib/freshbooks.rb
ADDED
@@ -0,0 +1,94 @@
|
|
1
|
+
$:.unshift(File.dirname(__FILE__)) unless
|
2
|
+
$:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'active_support'
|
6
|
+
rescue LoadError
|
7
|
+
require 'rubygems'
|
8
|
+
gem 'activesupport'
|
9
|
+
require 'active_support'
|
10
|
+
end
|
11
|
+
|
12
|
+
require 'freshbooks/base'
|
13
|
+
require 'freshbooks/category'
|
14
|
+
require 'freshbooks/client'
|
15
|
+
require 'freshbooks/connection'
|
16
|
+
require 'freshbooks/estimate'
|
17
|
+
require 'freshbooks/expense'
|
18
|
+
require 'freshbooks/invoice'
|
19
|
+
require 'freshbooks/item'
|
20
|
+
require 'freshbooks/line'
|
21
|
+
require 'freshbooks/links'
|
22
|
+
require 'freshbooks/list_proxy'
|
23
|
+
require 'freshbooks/payment'
|
24
|
+
require 'freshbooks/project'
|
25
|
+
require 'freshbooks/recurring'
|
26
|
+
require 'freshbooks/response'
|
27
|
+
require 'freshbooks/staff'
|
28
|
+
require 'freshbooks/task'
|
29
|
+
require 'freshbooks/time_entry'
|
30
|
+
|
31
|
+
require 'net/https'
|
32
|
+
require 'rexml/document'
|
33
|
+
require 'logger'
|
34
|
+
|
35
|
+
#------------------------------------------------------------------------------
|
36
|
+
# FreshBooks.rb - Ruby interface to the FreshBooks API
|
37
|
+
#
|
38
|
+
# Copyright (c) 2007-2008 Ben Vinegar (http://www.benlog.org)
|
39
|
+
#
|
40
|
+
# This work is distributed under an MIT License:
|
41
|
+
# http://www.opensource.org/licenses/mit-license.php
|
42
|
+
#
|
43
|
+
#------------------------------------------------------------------------------
|
44
|
+
# Usage:
|
45
|
+
#
|
46
|
+
# FreshBooks.setup('sample.freshbooks.com', 'mytoken')
|
47
|
+
#
|
48
|
+
# clients = FreshBooks::Client.list
|
49
|
+
# client = clients[0]
|
50
|
+
# client.first_name = 'Suzy'
|
51
|
+
# client.update
|
52
|
+
#
|
53
|
+
# invoice = FreshBooks::Invoice.get(4)
|
54
|
+
# invoice.lines[0].quantity += 1
|
55
|
+
# invoice.update
|
56
|
+
#
|
57
|
+
# item = FreshBooks::Item.new
|
58
|
+
# item.name = 'A sample item'
|
59
|
+
# item.create
|
60
|
+
#
|
61
|
+
#==============================================================================
|
62
|
+
module FreshBooks
|
63
|
+
VERSION = '3.0.13' # Gem version
|
64
|
+
API_VERSION = '2.1' # FreshBooks API version
|
65
|
+
SERVICE_URL = "/api/#{API_VERSION}/xml-in"
|
66
|
+
|
67
|
+
class Error < StandardError; end;
|
68
|
+
class InternalError < Error; end;
|
69
|
+
class AuthenticationError < Error; end;
|
70
|
+
class UnknownSystemError < Error; end;
|
71
|
+
class InvalidParameterError < Error; end;
|
72
|
+
class ApiAccessNotEnabledError < Error; end;
|
73
|
+
class InvalidAccountUrlError < Error; end;
|
74
|
+
class AccountDeactivatedError < Error; end;
|
75
|
+
|
76
|
+
class ParseError < StandardError
|
77
|
+
attr_accessor :original_error, :xml
|
78
|
+
|
79
|
+
def initialize(original_error, xml, msg = nil)
|
80
|
+
@original_error = original_error
|
81
|
+
@xml = xml
|
82
|
+
super(msg)
|
83
|
+
end
|
84
|
+
|
85
|
+
def to_s
|
86
|
+
message = super
|
87
|
+
|
88
|
+
"Original Error: #{original_error.to_s}\n" +
|
89
|
+
"XML: #{xml.to_s}\n" +
|
90
|
+
"Message: #{message}\n"
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
@@ -0,0 +1,169 @@
|
|
1
|
+
require 'freshbooks/schema/mixin'
|
2
|
+
require 'freshbooks/xml_serializer'
|
3
|
+
|
4
|
+
module FreshBooks
|
5
|
+
class Base
|
6
|
+
include FreshBooks::Schema::Mixin
|
7
|
+
|
8
|
+
@@connection = nil
|
9
|
+
def self.connection
|
10
|
+
@@connection
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.establish_connection(account_url, auth_token, request_headers = {})
|
14
|
+
@@connection = Connection.new(account_url, auth_token, request_headers)
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.new_from_xml(xml_root)
|
18
|
+
object = self.new
|
19
|
+
|
20
|
+
self.schema_definition.members.each do |member_name, member_options|
|
21
|
+
node = xml_root.elements[member_name]
|
22
|
+
next if node.nil?
|
23
|
+
|
24
|
+
value = FreshBooks::XmlSerializer.to_value(node, member_options[:type])
|
25
|
+
object.send("#{member_name}=", value)
|
26
|
+
end
|
27
|
+
|
28
|
+
return object
|
29
|
+
|
30
|
+
rescue => e
|
31
|
+
raise ParseError.new(e, xml_root.to_s)
|
32
|
+
end
|
33
|
+
|
34
|
+
def to_xml(elem_name = nil)
|
35
|
+
# The root element is the class name underscored
|
36
|
+
elem_name ||= self.class.to_s.split('::').last.underscore
|
37
|
+
root = REXML::Element.new(elem_name)
|
38
|
+
|
39
|
+
# Add each member to the root elem
|
40
|
+
self.schema_definition.members.each do |member_name, member_options|
|
41
|
+
value = self.send(member_name)
|
42
|
+
next if member_options[:read_only] || value.nil?
|
43
|
+
|
44
|
+
element = FreshBooks::XmlSerializer.to_node(member_name, value, member_options[:type])
|
45
|
+
root.add_element(element) if element != nil
|
46
|
+
end
|
47
|
+
|
48
|
+
root.to_s
|
49
|
+
end
|
50
|
+
|
51
|
+
def primary_key
|
52
|
+
"#{self.class.api_class_name}_id"
|
53
|
+
end
|
54
|
+
|
55
|
+
def primary_key_value
|
56
|
+
send(primary_key)
|
57
|
+
end
|
58
|
+
|
59
|
+
def primary_key_value=(value)
|
60
|
+
send("#{primary_key}=", value)
|
61
|
+
end
|
62
|
+
|
63
|
+
def self.api_class_name
|
64
|
+
klass = class_of_freshbooks_base_descendant(self)
|
65
|
+
|
66
|
+
# Remove module, underscore between words, lowercase
|
67
|
+
klass.name.
|
68
|
+
gsub(/^.*::/, "").
|
69
|
+
gsub(/([A-Z]+)([A-Z][a-z])/,'\1_\2').
|
70
|
+
gsub(/([a-z\d])([A-Z])/,'\1_\2').
|
71
|
+
downcase
|
72
|
+
end
|
73
|
+
|
74
|
+
def self.class_of_freshbooks_base_descendant(klass)
|
75
|
+
if klass.superclass == Base
|
76
|
+
klass
|
77
|
+
elsif klass.superclass.nil?
|
78
|
+
raise "#{name} doesn't belong in a hierarchy descending from ActiveRecord"
|
79
|
+
else
|
80
|
+
self.class_of_freshbooks_base_descendant(klass.superclass)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def self.define_class_method(symbol, &block)
|
85
|
+
self.class.send(:define_method, symbol, &block)
|
86
|
+
end
|
87
|
+
|
88
|
+
|
89
|
+
def self.actions(*operations)
|
90
|
+
operations.each do |operation|
|
91
|
+
method_name = operation.to_s
|
92
|
+
api_action_name = method_name.camelize(:lower)
|
93
|
+
|
94
|
+
case method_name
|
95
|
+
when "list"
|
96
|
+
define_class_method(method_name) do |*args|
|
97
|
+
args << {} if args.empty? # first param is optional and default to empty hash
|
98
|
+
api_list_action(api_action_name, *args)
|
99
|
+
end
|
100
|
+
when "get"
|
101
|
+
define_class_method(method_name) do |object_id|
|
102
|
+
api_get_action(api_action_name, object_id)
|
103
|
+
end
|
104
|
+
when "create"
|
105
|
+
define_method(method_name) do
|
106
|
+
api_create_action(api_action_name)
|
107
|
+
end
|
108
|
+
when "update"
|
109
|
+
define_method(method_name) do
|
110
|
+
api_update_action(api_action_name)
|
111
|
+
end
|
112
|
+
else
|
113
|
+
define_method(method_name) do
|
114
|
+
api_action(api_action_name)
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
def self.api_list_action(action_name, options = {})
|
121
|
+
# Create the proc for the list proxy to retrieve the next page
|
122
|
+
list_page_proc = proc do |page|
|
123
|
+
options["page"] = page
|
124
|
+
response = FreshBooks::Base.connection.call_api("#{api_class_name}.#{action_name}", options)
|
125
|
+
|
126
|
+
raise FreshBooks::InternalError.new(response.error_msg) unless response.success?
|
127
|
+
|
128
|
+
root = response.elements[1]
|
129
|
+
array = root.elements.map { |item| self.new_from_xml(item) }
|
130
|
+
|
131
|
+
current_page = Page.new(root.attributes['page'], root.attributes['per_page'], root.attributes['total'], array.size)
|
132
|
+
|
133
|
+
[array, current_page]
|
134
|
+
end
|
135
|
+
|
136
|
+
ListProxy.new(list_page_proc)
|
137
|
+
end
|
138
|
+
|
139
|
+
def self.api_get_action(action_name, object_id)
|
140
|
+
response = FreshBooks::Base.connection.call_api(
|
141
|
+
"#{api_class_name}.#{action_name}",
|
142
|
+
"#{api_class_name}_id" => object_id)
|
143
|
+
response.success? ? self.new_from_xml(response.elements[1]) : nil
|
144
|
+
end
|
145
|
+
|
146
|
+
def api_action(action_name)
|
147
|
+
response = FreshBooks::Base.connection.call_api(
|
148
|
+
"#{self.class.api_class_name}.#{action_name}",
|
149
|
+
"#{self.class.api_class_name}_id" => primary_key_value)
|
150
|
+
response.success?
|
151
|
+
end
|
152
|
+
|
153
|
+
def api_create_action(action_name)
|
154
|
+
response = FreshBooks::Base.connection.call_api(
|
155
|
+
"#{self.class.api_class_name}.#{action_name}",
|
156
|
+
self.class.api_class_name => self)
|
157
|
+
self.primary_key_value = response.elements[1].text.to_i if response.success?
|
158
|
+
response.success?
|
159
|
+
end
|
160
|
+
|
161
|
+
def api_update_action(action_name)
|
162
|
+
response = FreshBooks::Base.connection.call_api(
|
163
|
+
"#{self.class.api_class_name}.#{action_name}",
|
164
|
+
self.class.api_class_name => self)
|
165
|
+
response.success?
|
166
|
+
end
|
167
|
+
|
168
|
+
end
|
169
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module FreshBooks
|
2
|
+
class Client < FreshBooks::Base
|
3
|
+
define_schema do |s|
|
4
|
+
s.string :first_name, :last_name, :organization, :email
|
5
|
+
s.string :username, :password, :work_phone, :home_phone
|
6
|
+
s.string :mobile, :fax, :notes, :p_street1, :p_street2, :p_city
|
7
|
+
s.string :p_state, :p_country, :p_code, :s_street1, :s_street2
|
8
|
+
s.string :s_city, :s_state, :s_country, :s_code
|
9
|
+
s.float :credit
|
10
|
+
s.date_time :updated, :read_only => true
|
11
|
+
s.fixnum :client_id
|
12
|
+
s.object :links, :read_only => true
|
13
|
+
end
|
14
|
+
|
15
|
+
actions :list, :get, :create, :update, :delete
|
16
|
+
|
17
|
+
def invoices(options = {})
|
18
|
+
options.merge!('client_id' => self.client_id)
|
19
|
+
Invoice::list(options)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,153 @@
|
|
1
|
+
require 'net/https'
|
2
|
+
require 'rexml/document'
|
3
|
+
require 'logger'
|
4
|
+
|
5
|
+
module FreshBooks
|
6
|
+
class Connection
|
7
|
+
attr_reader :account_url, :auth_token, :request_headers
|
8
|
+
|
9
|
+
@@logger = Logger.new(STDOUT)
|
10
|
+
def logger
|
11
|
+
@@logger
|
12
|
+
end
|
13
|
+
|
14
|
+
def self.log_level=(level)
|
15
|
+
@@logger.level = level
|
16
|
+
end
|
17
|
+
self.log_level = Logger::WARN
|
18
|
+
|
19
|
+
def initialize(account_url, auth_token, request_headers = {})
|
20
|
+
raise InvalidAccountUrlError.new unless account_url =~ /^[0-9a-zA-Z\-_]+\.freshbooks\.com$/
|
21
|
+
|
22
|
+
@account_url = account_url
|
23
|
+
@auth_token = auth_token
|
24
|
+
@request_headers = request_headers
|
25
|
+
|
26
|
+
@start_session_count = 0
|
27
|
+
end
|
28
|
+
|
29
|
+
def call_api(method, elements = [])
|
30
|
+
request = create_request(method, elements)
|
31
|
+
result = post(request)
|
32
|
+
Response.new(result)
|
33
|
+
end
|
34
|
+
|
35
|
+
def start_session(&block)
|
36
|
+
@connection = obtain_connection if @start_session_count == 0
|
37
|
+
@start_session_count = @start_session_count + 1
|
38
|
+
|
39
|
+
begin
|
40
|
+
block.call(@connection)
|
41
|
+
ensure
|
42
|
+
@start_session_count = @start_session_count - 1
|
43
|
+
close if @start_session_count == 0
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
protected
|
48
|
+
|
49
|
+
def create_request(method, elements = [])
|
50
|
+
doc = REXML::Document.new '<?xml version="1.0" encoding="UTF-8"?>'
|
51
|
+
request = doc.add_element('request')
|
52
|
+
request.attributes['method'] = method
|
53
|
+
|
54
|
+
elements.each do |element|
|
55
|
+
if element.kind_of?(Hash)
|
56
|
+
element = element.to_a
|
57
|
+
end
|
58
|
+
key = element.first
|
59
|
+
value = element.last
|
60
|
+
|
61
|
+
if value.kind_of?(Base)
|
62
|
+
request.add_element(REXML::Document.new(value.to_xml))
|
63
|
+
else
|
64
|
+
request.add_element(REXML::Element.new(key.to_s)).text = value.to_s
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
doc.to_s
|
69
|
+
end
|
70
|
+
|
71
|
+
def obtain_connection(force = false)
|
72
|
+
return @connection if @connection && !force
|
73
|
+
|
74
|
+
@connection = Net::HTTP.new(@account_url, 443)
|
75
|
+
@connection.use_ssl = true
|
76
|
+
@connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
77
|
+
@connection.start
|
78
|
+
end
|
79
|
+
|
80
|
+
def reconnect
|
81
|
+
close
|
82
|
+
obtain_connection(true)
|
83
|
+
end
|
84
|
+
|
85
|
+
def close
|
86
|
+
begin
|
87
|
+
@connection.finish if @connection
|
88
|
+
rescue => e
|
89
|
+
logger.error("Error closing connection: " + e.message)
|
90
|
+
end
|
91
|
+
@connection = nil
|
92
|
+
end
|
93
|
+
|
94
|
+
def post(request_body)
|
95
|
+
result = nil
|
96
|
+
request = Net::HTTP::Post.new(FreshBooks::SERVICE_URL)
|
97
|
+
request.basic_auth @auth_token, 'X'
|
98
|
+
request.body = request_body
|
99
|
+
request.content_type = 'application/xml'
|
100
|
+
@request_headers.each_pair do |name, value|
|
101
|
+
request[name.to_s] = value
|
102
|
+
end
|
103
|
+
|
104
|
+
result = post_request(request)
|
105
|
+
|
106
|
+
if logger.debug?
|
107
|
+
logger.debug "Request:"
|
108
|
+
logger.debug request_body
|
109
|
+
logger.debug "Response:"
|
110
|
+
logger.debug result.body
|
111
|
+
end
|
112
|
+
|
113
|
+
check_for_api_error(result)
|
114
|
+
end
|
115
|
+
|
116
|
+
# For connections that take a long time, we catch EOFError's and reconnect seamlessly
|
117
|
+
def post_request(request)
|
118
|
+
response = nil
|
119
|
+
has_reconnected = false
|
120
|
+
start_session do |connection|
|
121
|
+
begin
|
122
|
+
response = connection.request(request)
|
123
|
+
rescue EOFError => e
|
124
|
+
raise e if has_reconnected
|
125
|
+
|
126
|
+
has_reconnected = true
|
127
|
+
connection = reconnect
|
128
|
+
retry
|
129
|
+
end
|
130
|
+
end
|
131
|
+
response
|
132
|
+
end
|
133
|
+
|
134
|
+
def check_for_api_error(result)
|
135
|
+
return result.body if result.kind_of?(Net::HTTPSuccess)
|
136
|
+
|
137
|
+
case result
|
138
|
+
when Net::HTTPRedirection
|
139
|
+
if result["location"] =~ /loginSearch/
|
140
|
+
raise UnknownSystemError.new("Account does not exist")
|
141
|
+
elsif result["location"] =~ /deactivated/
|
142
|
+
raise AccountDeactivatedError.new("Account is deactivated")
|
143
|
+
end
|
144
|
+
when Net::HTTPUnauthorized
|
145
|
+
raise AuthenticationError.new("Invalid API key.")
|
146
|
+
when Net::HTTPBadRequest
|
147
|
+
raise ApiAccessNotEnabledError.new("API not enabled.")
|
148
|
+
end
|
149
|
+
|
150
|
+
raise InternalError.new("Invalid HTTP code: #{result.class}")
|
151
|
+
end
|
152
|
+
end
|
153
|
+
end
|