turingstudio-freshbooks-rb 3.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt ADDED
@@ -0,0 +1,4 @@
1
+ == 0.0.1 2009-02-18
2
+
3
+ * 1 major enhancement:
4
+ * Initial release
data/README.rdoc ADDED
@@ -0,0 +1,65 @@
1
+ = freshbooks-rb
2
+
3
+ http://github.com/turingstudio/freshbooks-rb/
4
+
5
+ == DESCRIPTION:
6
+
7
+ A Ruby interface to the FreshBooks API. It exposes easy-to-use classes and methods for interacting with your FreshBooks account.
8
+
9
+ == SYNOPSIS:
10
+
11
+ Initialization:
12
+
13
+ FreshBooks::Base.setup('sample.freshbooks.com', 'mytoken')
14
+
15
+ Updating a client name:
16
+
17
+ clients = FreshBooks::Client.list
18
+ client = clients[0]
19
+ client.first_name = 'Suzy'
20
+ client.update
21
+
22
+ Updating an invoice:
23
+
24
+ invoice = FreshBooks::Invoice.get(4)
25
+ invoice.lines[0].quantity += 1
26
+ invoice.update
27
+
28
+ Creating a new item
29
+
30
+ item = FreshBooks::Item.new
31
+ item.name = 'A sample item'
32
+ item.create
33
+
34
+ == REQUIREMENTS:
35
+
36
+ activesupport >= 2.2.2
37
+
38
+ == INSTALL:
39
+
40
+ gem install turingstudio-freshbooks-rb
41
+
42
+ == LICENSE:
43
+
44
+ (The MIT License)
45
+
46
+ Copyright (c) 2009 The Turing Studio, Inc.
47
+
48
+ Permission is hereby granted, free of charge, to any person obtaining
49
+ a copy of this software and associated documentation files (the
50
+ 'Software'), to deal in the Software without restriction, including
51
+ without limitation the rights to use, copy, modify, merge, publish,
52
+ distribute, sublicense, and/or sell copies of the Software, and to
53
+ permit persons to whom the Software is furnished to do so, subject to
54
+ the following conditions:
55
+
56
+ The above copyright notice and this permission notice shall be
57
+ included in all copies or substantial portions of the Software.
58
+
59
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
60
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
61
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
62
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
63
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
64
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
65
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ %w[rubygems rake rake/clean fileutils newgem rubigen].each { |f| require f }
2
+ require File.dirname(__FILE__) + '/lib/freshbooks/version'
3
+
4
+ # Generate all the Rake tasks
5
+ # Run 'rake -T' to see list of generated tasks (from gem root directory)
6
+ $hoe = Hoe.new('freshbooks-rb', FreshBooks::VERSION) do |p|
7
+ p.developer('The Turing Studio, Inc.', 'support@turingstudio.com')
8
+ p.changes = p.paragraphs_of("History.txt", 0..1).join("\n\n")
9
+ p.extra_deps = [
10
+ ['xml-simple','>= 1.0.11'],
11
+ ['activesupport','>= 2.2.2']
12
+ ]
13
+ p.extra_dev_deps = [
14
+ ['newgem', ">= #{::Newgem::VERSION}"]
15
+ ]
16
+ p.clean_globs |= %w[**/.DS_Store tmp *.log]
17
+ end
18
+
19
+ require 'newgem/tasks' # load /tasks/*.rake
20
+ Dir['tasks/**/*.rake'].each { |t| load t }
data/lib/freshbooks.rb ADDED
@@ -0,0 +1,41 @@
1
+ #
2
+ # Usage:
3
+ #
4
+ # FreshBooks::Base.setup('sample.freshbooks.com', 'mytoken')
5
+ #
6
+ # clients = FreshBooks::Client.list
7
+ # client = clients[0]
8
+ # client.first_name = 'Suzy'
9
+ # client.update
10
+ #
11
+ # invoice = FreshBooks::Invoice.get(4)
12
+ # invoice.lines[0].quantity += 1
13
+ # invoice.update
14
+ #
15
+ # item = FreshBooks::Item.new
16
+ # item.name = 'A sample item'
17
+ # item.create
18
+ #
19
+
20
+ $:.unshift(File.dirname(__FILE__))
21
+
22
+ require 'net/https'
23
+ require 'rexml/document'
24
+ require 'activesupport'
25
+
26
+ require 'freshbooks/version'
27
+ require 'freshbooks/response'
28
+ require 'freshbooks/base'
29
+ require 'freshbooks/category'
30
+ require 'freshbooks/client'
31
+ require 'freshbooks/estimate'
32
+ require 'freshbooks/expense'
33
+ require 'freshbooks/invoice'
34
+ require 'freshbooks/item'
35
+ require 'freshbooks/line'
36
+ require 'freshbooks/payment'
37
+ require 'freshbooks/project'
38
+ require 'freshbooks/recurring'
39
+ require 'freshbooks/staff'
40
+ require 'freshbooks/task'
41
+ require 'freshbooks/time_entry'
@@ -0,0 +1,286 @@
1
+ module FreshBooks
2
+ class Base
3
+ class InternalError < Exception; end;
4
+ class AuthenticationError < Exception; end;
5
+ class UnknownSystemError < Exception; end;
6
+ class InvalidParameterError < Exception; end;
7
+
8
+ attr_accessor :resp
9
+
10
+ def initialize(data = {})
11
+ @old_values = {}
12
+ @changed_fields = []
13
+ data.each do |name, value|
14
+ send("#{name}=", value) if self.class.attributes[name]
15
+ end
16
+ end
17
+
18
+ # Convert an instance of this class to an XML element
19
+ def to_xml(elem_name = nil)
20
+ root = REXML::Element.new(node_name)
21
+
22
+ self.class.attributes.each do |name, options|
23
+ next if options[:read_only] || !send("#{name}_changed?")
24
+
25
+ value = send(name)
26
+
27
+ if self.class.method_defined?("#{name}_to_xml")
28
+ send("#{name}_to_xml", root)
29
+ elsif value.is_a?(Array)
30
+ unless value.empty?
31
+ node = root.add_element(name.to_s)
32
+ value.each do |item|
33
+ node.add_element(item.to_xml)
34
+ end
35
+ end
36
+ elsif !value.nil?
37
+ root.add_element(name.to_s).text = value
38
+ end
39
+ end
40
+
41
+ root
42
+ end
43
+
44
+ def create
45
+ self.class.ensure_allowed! :create
46
+ resp = Base.call_api("#{node_name}.create", node_name => self)
47
+ if resp.success?
48
+ self.send("#{node_name}_id=", resp.elements[1].text.to_i)
49
+ end
50
+ resp.success? ? self.send("#{node_name}_id") : nil
51
+ end
52
+
53
+ def update
54
+ self.class.ensure_allowed! :update
55
+ resp = Base.call_api("#{node_name}.update", node_name => self)
56
+ resp.success?
57
+ end
58
+
59
+ def delete
60
+ self.class.delete(self.send("#{node_name}_id"))
61
+ end
62
+
63
+ def send_by_email
64
+ self.class.send_by_email(self.send("#{node_name}_id"))
65
+ end
66
+
67
+ def send_by_snail_mail
68
+ self.class.send_by_snail_mail(self.send("#{node_name}_id"))
69
+ end
70
+
71
+ def node_name
72
+ self.class.node_name
73
+ end
74
+
75
+ class << self
76
+ def setup(account_url, auth_token, request_headers = {})
77
+ @@account_url = account_url
78
+ @@auth_token = auth_token
79
+ @@request_headers = request_headers
80
+ @@response = nil
81
+ end
82
+
83
+ def list(options = {})
84
+ ensure_allowed! :list
85
+ resp = Base.call_api("#{node_name}.list", options)
86
+ return nil unless resp.success?
87
+
88
+ elems = resp.elements[1].elements
89
+ elems.map { |elem| self.new_from_xml(elem) }
90
+ end
91
+
92
+ def get(id)
93
+ ensure_allowed! :get
94
+ resp = Base.call_api("#{node_name}.get", "#{node_name}_id" => id)
95
+ resp.success? ? self.new_from_xml(resp.elements[1]) : nil
96
+ end
97
+
98
+ def delete(id)
99
+ ensure_allowed! :delete
100
+ resp = Base.call_api("#{node_name}.delete", "#{node_name}_id" => id)
101
+ resp.success?
102
+ end
103
+
104
+ def send_by_email(id)
105
+ ensure_allowed! :send_by_email
106
+ resp = Base.call_api("#{node_name}.sendByEmail", "#{node_name}_id" => id)
107
+ resp.success?
108
+ end
109
+
110
+ def send_by_snail_mail(id)
111
+ ensure_allowed! :send_by_snail_mail
112
+ resp = Base.call_api("#{node_name}.sendBySnailMail", "#{node_name}_id" => id)
113
+ resp.success?
114
+ end
115
+
116
+ def node_name
117
+ self.to_s.split('::').last.underscore
118
+ end
119
+
120
+ #
121
+ # Defines structure attribute. Generates attr_accessor and #{name}_changed? method
122
+ #
123
+ # class Test < Base
124
+ # attribute :test_id, :integer
125
+ # end
126
+ #
127
+ # test = Test.new
128
+ # test.test_id_changed? # false
129
+ # test.test_id = 1
130
+ # test.test_id_changed? # true
131
+ # test.test_id # 1
132
+ #
133
+ def attribute(name, type, options = {})
134
+ self.attributes[name] = options.update(:type => type)
135
+
136
+ define_method("#{name}=") do |value|
137
+ @changed_fields << name
138
+ @old_values[name] = send(name) if @old_values[name].nil?
139
+ instance_variable_set("@#{name}", value)
140
+ end
141
+
142
+ define_method("#{name}_changed?") do
143
+ @changed_fields.include?(name)
144
+ end
145
+
146
+ self.send(:attr_reader, name)
147
+ end
148
+
149
+ def attributes
150
+ @attributes ||= {}
151
+ end
152
+
153
+ #
154
+ # Generates association methods.
155
+ #
156
+ # class Test < Base
157
+ # belongs_to :project
158
+ # has_many :items, :lines
159
+ # end
160
+ #
161
+ # test = Test.new
162
+ # test.project # Project.get(test.project_id)
163
+ # test.items # Item.list(:test_id => test.test_id)
164
+ # test.lines # Line.list(:test_id => test.test_id)
165
+ #
166
+ def has_many(*args)
167
+ args.each do |name|
168
+ class_eval "def #{name}(options = {}); @#{name} ||= #{name.to_s.classify}.list(options.merge('#{node_name}_id' => #{node_name}_id)); end"
169
+ end
170
+ end
171
+
172
+ def belongs_to(*args)
173
+ args.each do |name|
174
+ class_eval "def #{name}(options = {}); @#{name} ||= #{name.to_s.classify}.get(#{name}_id); end"
175
+ end
176
+ end
177
+
178
+ #
179
+ # Defines allowed API methods.
180
+ #
181
+ # class Test < Base
182
+ # method :list
183
+ # end
184
+ #
185
+ # Test.list # [...]
186
+ # Test.get(1) # "Error: Method get is not allowed for Test"
187
+ #
188
+ def method(*args)
189
+ @methods = (methods + args).uniq
190
+ end
191
+
192
+ def methods
193
+ @methods ||= []
194
+ end
195
+
196
+ def ensure_allowed!(name)
197
+ unless methods.include?(name)
198
+ raise "Error: Method #{name} is not allowed for #{self}"
199
+ end
200
+ end
201
+
202
+ # Create a new instance of this class from an XML element
203
+ def new_from_xml(xml_root)
204
+ object = self.new
205
+ attributes.each do |name, options|
206
+ node = xml_root.elements[name.to_s]
207
+ next if node.nil?
208
+
209
+ case options[:type]
210
+ when :integer
211
+ value = node.text.to_i
212
+ when :float
213
+ value = node.text.to_f
214
+ when :array
215
+ value = node.elements.map do |item|
216
+ FreshBooks::const_get(item.name.classify)::new_from_xml(item)
217
+ end
218
+ else
219
+ value = node.text.to_s
220
+ end
221
+
222
+ object.send("#{name}=", value)
223
+ end
224
+ object
225
+ end
226
+
227
+ def call_api(method, elems = [])
228
+ doc = REXML::Document.new '<?xml version="1.0" encoding="UTF-8"?>'
229
+ request = doc.add_element 'request'
230
+ request.attributes['method'] = method
231
+
232
+ elems.each do |key, value|
233
+ if value.is_a?(Base)
234
+ elem = value.to_xml
235
+ request.add_element(elem)
236
+ else
237
+ request.add_element(REXML::Element.new(key)).text = value.to_s
238
+ end
239
+ end
240
+
241
+ result = self.post(request.to_s)
242
+
243
+ @@response = Response.new(result)
244
+
245
+ #
246
+ # Failure
247
+ #
248
+ if @@response.fail?
249
+ error_msg = @@response.error_msg
250
+
251
+ raise InternalError.new, error_msg if error_msg =~ /not formatted correctly/
252
+ raise AuthenticationError.new, error_msg if error_msg =~ /[Aa]uthentication failed/
253
+ raise UnknownSystemError.new, error_msg if error_msg =~ /does not exist/
254
+ raise InvalidParameterError.new, error_msg if error_msg =~ /Invalid parameter: (.*)/
255
+
256
+ # Raise an exception for unexpected errors
257
+ raise error_msg
258
+ end
259
+
260
+ @@response
261
+ end
262
+
263
+ def response
264
+ @@response
265
+ end
266
+
267
+ def post(body)
268
+ connection = Net::HTTP.new(@@account_url, 443)
269
+ connection.use_ssl = true
270
+ connection.verify_mode = OpenSSL::SSL::VERIFY_NONE
271
+
272
+ request = Net::HTTP::Post.new(FreshBooks::SERVICE_URL)
273
+ request.basic_auth @@auth_token, 'X'
274
+ request.body = body
275
+ request.content_type = 'application/xml'
276
+ @@request_headers.each_pair do |name, value|
277
+ request[name.to_s] = value
278
+ end
279
+
280
+ result = connection.start { |http| http.request(request) }
281
+
282
+ result.body
283
+ end
284
+ end
285
+ end
286
+ end
@@ -0,0 +1,11 @@
1
+ module FreshBooks
2
+ class Category < Base
3
+ attribute :category_id, :integer
4
+ attribute :name, :string
5
+ attribute :tax1, :integer
6
+ attribute :tax2, :integer
7
+
8
+ has_many :expenses
9
+ method :list, :get, :create, :update, :delete
10
+ end
11
+ end
@@ -0,0 +1,32 @@
1
+ module FreshBooks
2
+ class Client < Base
3
+ attribute :client_id, :integer
4
+ attribute :first_name, :string
5
+ attribute :last_name, :string
6
+ attribute :organization, :string
7
+ attribute :email, :string
8
+ attribute :username, :string
9
+ attribute :password, :string
10
+ attribute :work_phone, :string
11
+ attribute :home_phone, :string
12
+ attribute :mobile, :string
13
+ attribute :fax, :string
14
+ attribute :notes, :string
15
+ attribute :p_street1, :string
16
+ attribute :p_street2, :string
17
+ attribute :p_city, :string
18
+ attribute :p_state, :string
19
+ attribute :p_country, :string
20
+ attribute :p_code, :string
21
+ attribute :s_street1, :string
22
+ attribute :s_street2, :string
23
+ attribute :s_city, :string
24
+ attribute :s_state, :string
25
+ attribute :s_country, :string
26
+ attribute :s_code, :string
27
+ attribute :url, :string, :read_only => true
28
+
29
+ has_many :expenses, :estimates, :invoices, :payments, :projects, :recurrings
30
+ method :list, :get, :create, :update, :delete
31
+ end
32
+ end