free_agent 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (3) hide show
  1. data/README +53 -0
  2. data/lib/free_agent.rb +252 -0
  3. metadata +96 -0
data/README ADDED
@@ -0,0 +1,53 @@
1
+ FreeAgent API
2
+ =============
3
+
4
+ This is just a simple wrapper around the FreeAgent[1] API. The motivation is that I wanted to be able to deal with multiple FreeAgent accounts; most other approaches use ActiveResource-esque mappings that link Projects (for example) to a single account.
5
+
6
+ Example use:
7
+
8
+ require 'rubygems'
9
+ require 'free_agent'
10
+
11
+ Lazyatom = FreeAgent::Company.new("mydomain", "me@email.com", "mypassword")
12
+
13
+ # All my contacts
14
+ Lazyatom.contacts.all
15
+
16
+ # All my unpaid invoices
17
+ Lazyatom.invoices.reject { |i| i.paid? }
18
+
19
+ # All the timeslips for a project
20
+ Lazyatom.projects.first.timeslips.all
21
+
22
+ It's early days yet, but this is a nice start.
23
+
24
+
25
+ Links
26
+ -----
27
+
28
+ [1]: http://www.freeagentcentral.com/?referrer=31h0wcs9
29
+
30
+
31
+
32
+ Copyright (c) 2010 James Adam
33
+ Plenty of help from Free Rangers, incl. Luke Redpath
34
+
35
+ The MIT License
36
+
37
+ Permission is hereby granted, free of charge, to any person obtaining a copy
38
+ of this software and associated documentation files (the "Software"), to deal
39
+ in the Software without restriction, including without limitation the rights
40
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
41
+ copies of the Software, and to permit persons to whom the Software is
42
+ furnished to do so, subject to the following conditions:
43
+
44
+ The above copyright notice and this permission notice shall be included in
45
+ all copies or substantial portions of the Software.
46
+
47
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
48
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
49
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
50
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
51
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
52
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
53
+ THE SOFTWARE.
data/lib/free_agent.rb ADDED
@@ -0,0 +1,252 @@
1
+ require 'restclient'
2
+ require 'crack'
3
+ require 'mash'
4
+ require 'active_support/core_ext/string'
5
+ require 'active_support/core_ext/object/returning'
6
+ require 'active_support/core_ext/hash'
7
+ require 'cgi'
8
+
9
+ RestClient::Resource.class_eval do
10
+ def root
11
+ self.class.new(URI.parse(url).merge('/').to_s, options)
12
+ end
13
+ end
14
+
15
+ module FreeAgent
16
+ class Company
17
+ def initialize(domain, username, password)
18
+ @resource = RestClient::Resource.new(
19
+ "https://#{domain}.freeagentcentral.com",
20
+ :user => username, :password => password
21
+ )
22
+ end
23
+
24
+ def invoices
25
+ @invoices ||= Collection.new(@resource['/invoices'], :entity => :invoice)
26
+ end
27
+
28
+ def contacts
29
+ @contacts ||= Collection.new(@resource['/contacts'], :entity => :contact)
30
+ end
31
+
32
+ def projects
33
+ @projects ||= Collection.new(@resource['/projects'], :entity => :project)
34
+ end
35
+
36
+ def users
37
+ @users ||= Collection.new(@resource['/company/users'], :entity => :user)
38
+ end
39
+
40
+ # Note, this is only for PUT/POSTing to
41
+ def timeslips
42
+ @timeslips ||= Collection.new(@resource['/timeslips'], :entity => :timeslip)
43
+ end
44
+
45
+ def expenses(user_id, options={})
46
+ options.assert_valid_keys(:view, :from, :to)
47
+ options.reverse_merge!(:view => 'recent')
48
+
49
+ if options[:from] && options[:to]
50
+ options[:view] = "#{options[:from].strftime('%Y-%m-%d')}_#{options[:to].strftime('%Y-%m-%d')}"
51
+ end
52
+
53
+ Collection.new(@resource["/users/#{user_id}/expenses?view=#{options[:view]}"], :entity => :expense)
54
+ end
55
+ end
56
+
57
+ class Collection
58
+ include Enumerable
59
+
60
+ def initialize(resource, options={})
61
+ @resource = resource
62
+ @entity = options.delete(:entity)
63
+ @entity_klass = "FreeAgent::#{@entity.to_s.classify}".constantize
64
+ end
65
+
66
+ def url
67
+ @resource.url
68
+ end
69
+
70
+ def find(id=nil)
71
+ if id
72
+ entity_for_id(id).reload
73
+ else
74
+ super
75
+ end
76
+ end
77
+
78
+ def each(&block)
79
+ all.each(&block)
80
+ end
81
+
82
+ def all(params={})
83
+ if params.any?
84
+ # very naive Hash-to-params
85
+ resource = @resource['?'+params.map{|k,v|"#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}"}.join('&')]
86
+ else
87
+ resource = @resource
88
+ end
89
+ case (response = resource.get).code
90
+ when 200
91
+ if entities = Crack::XML.parse(response)[@entity.to_s.pluralize]
92
+ entities.map do |attributes|
93
+ entity_for_id(attributes['id'], attributes)
94
+ end
95
+ else
96
+ []
97
+ end
98
+ end
99
+ end
100
+
101
+ def create(attributes)
102
+ payload = attributes.to_xml(:root => @entity.to_s )
103
+ case (response = @resource.post(payload,
104
+ :content_type => 'application/xml', :accept => 'application/xml')).code
105
+ when 201
106
+ resource_path = URI.parse(response.headers[:location]).path
107
+ @entity_klass.new(@resource.root[resource_path]).reload
108
+ end
109
+ end
110
+
111
+ def update(id, attributes)
112
+ entity_for_id(id).update(attributes, headers)
113
+ end
114
+
115
+ def destroy(id)
116
+ entity_for_id(id).destroy
117
+ end
118
+
119
+ private
120
+
121
+ def entity_for_id(id, attributes={})
122
+ @entity_klass.new(@resource["/#{id}"], attributes)
123
+ end
124
+
125
+ # Treat the collection as if it's an array otherwise
126
+ def method_missing(method, *args, &block)
127
+ all.send(method, *args, &block)
128
+ end
129
+ end
130
+
131
+ class Entity
132
+ def self.has_many(things)
133
+ define_method(things) do
134
+ Collection.new(@resource["/#{things}"], :entity => things.to_s.singularize)
135
+ end
136
+ end
137
+
138
+ def self.belongs_to(thing, *args)
139
+ define_method(thing) do
140
+
141
+ end
142
+ # NOOP right now.
143
+ end
144
+
145
+ def self.xml_name
146
+ name.split("::")[1..-1].join("::").underscore
147
+ end
148
+
149
+ attr_reader :attributes
150
+
151
+ def initialize(resource, attributes = {})
152
+ @resource = resource
153
+ @attributes = attributes.to_mash
154
+ end
155
+
156
+ def id
157
+ @attributes.id
158
+ end
159
+
160
+ def url
161
+ @resource.url
162
+ end
163
+
164
+ def reload
165
+ returning(self) do
166
+ @attributes = Crack::XML.parse(@resource.get)[xml_name].to_mash
167
+ end
168
+ end
169
+
170
+ def update(new_attributes)
171
+ attributes.merge!(new_attributes)
172
+ save
173
+ end
174
+
175
+ def save
176
+ @resource.put(attributes.to_xml(:root => xml_name),
177
+ :content_type =>'application/xml', :accept => 'application/xml')
178
+ end
179
+
180
+ def destroy
181
+ @resource.delete
182
+ end
183
+
184
+ private
185
+
186
+ def xml_name
187
+ self.class.xml_name
188
+ end
189
+
190
+ def method_missing(*args)
191
+ @attributes.send(*args)
192
+ end
193
+ end
194
+
195
+ class User < Entity
196
+ has_many :expenses
197
+ has_many :timeslips
198
+ end
199
+
200
+ class Project < Entity
201
+ has_many :tasks
202
+ has_many :invoices
203
+ has_many :timeslips
204
+ belongs_to :contact
205
+
206
+ def active?
207
+ status == "Active"
208
+ end
209
+ end
210
+
211
+ class Invoice < Entity
212
+ has_many :invoice_items
213
+ belongs_to :project
214
+ belongs_to :contact
215
+ end
216
+
217
+ class InvoiceItem < Entity
218
+ end
219
+
220
+ class Task < Entity
221
+ belongs_to :project
222
+ end
223
+
224
+ class Timeslip < Entity
225
+ def initialize(resource, attributes={})
226
+ # need to convert /projects/123/timeslips/456 into /timeslips/456
227
+ tweaked_resource = RestClient::Resource.new(resource.url.gsub(/\/projects\/\d+\//, "/"), resource.options)
228
+ super(tweaked_resource, attributes)
229
+ end
230
+
231
+ belongs_to :project
232
+ belongs_to :user
233
+ belongs_to :task
234
+ end
235
+
236
+ class Contact < Entity
237
+ has_many :invoices
238
+ has_many :projects
239
+ end
240
+
241
+ class Bill < Entity
242
+ belongs_to :contact
243
+ belongs_to :project, :key => :rebilled_to_project_id
244
+ end
245
+
246
+ class Expense < Entity
247
+ belongs_to :user
248
+ end
249
+
250
+ class Attachment < Entity
251
+ end
252
+ end
metadata ADDED
@@ -0,0 +1,96 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: free_agent
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - James Adam
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2010-02-17 00:00:00 +00:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: rest-client
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: crack
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ - !ruby/object:Gem::Dependency
36
+ name: mash
37
+ type: :runtime
38
+ version_requirement:
39
+ version_requirements: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: "0"
44
+ version:
45
+ - !ruby/object:Gem::Dependency
46
+ name: activesupport
47
+ type: :runtime
48
+ version_requirement:
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
52
+ - !ruby/object:Gem::Version
53
+ version: "0"
54
+ version:
55
+ description:
56
+ email: james@lazyatom.com
57
+ executables: []
58
+
59
+ extensions: []
60
+
61
+ extra_rdoc_files:
62
+ - README
63
+ files:
64
+ - README
65
+ - lib/free_agent.rb
66
+ has_rdoc: true
67
+ homepage: http://interblah.net/freeagent-gem
68
+ licenses: []
69
+
70
+ post_install_message:
71
+ rdoc_options:
72
+ - --main
73
+ - README
74
+ require_paths:
75
+ - lib
76
+ required_ruby_version: !ruby/object:Gem::Requirement
77
+ requirements:
78
+ - - ">="
79
+ - !ruby/object:Gem::Version
80
+ version: "0"
81
+ version:
82
+ required_rubygems_version: !ruby/object:Gem::Requirement
83
+ requirements:
84
+ - - ">="
85
+ - !ruby/object:Gem::Version
86
+ version: "0"
87
+ version:
88
+ requirements: []
89
+
90
+ rubyforge_project: freeagent
91
+ rubygems_version: 1.3.5
92
+ signing_key:
93
+ specification_version: 3
94
+ summary: A small ruby library for accessing information from http://freeagentcentral.com
95
+ test_files: []
96
+