turingstudio-freshbooks-rb 3.0.1

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