free_agent 0.1.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.
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
+