basecamp_wrapper 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.
data/Manifest ADDED
@@ -0,0 +1,5 @@
1
+ README
2
+ Rakefile
3
+ lib/basecamp.rb
4
+ lib/basecamp_wrapper.rb
5
+ Manifest
data/README ADDED
@@ -0,0 +1,13 @@
1
+ Based on anibalcucco's basecamp-wrapper library. Updated to work with REST API for People.
2
+
3
+ = Using the Basecamp API with Ruby
4
+
5
+ This is the basecamp wrapper provided by 37Signals to access their API with a little change to obtain the API token.
6
+
7
+ This is useful when you only have username/password and you want to obtain the token automatically to use that in future calls.
8
+
9
+ = Usage
10
+
11
+ >> Basecamp.establish_connection!('<subdomain>.basecamphq.com', <username>, <password>, true)
12
+ >> Basecamp.get_token
13
+ => "the token"
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'echoe'
4
+ Echoe.new('basecamp_wrapper', '0.1.0') do |p|
5
+ p.description = "A wrapper for the 37 Signals Basecamp API"
6
+ p.url = "http://www.github.com/fmiopensource/basecamp_wrapper"
7
+ p.author = "Chelsea Robb"
8
+ p.email = "chelsea@fluidmedia.com"
9
+ p.ignore_pattern = ["tmp/*", "script/*"]
10
+ p.development_dependencies = []
11
+ end
12
+
13
+ Dir["#{File.dirname(__FILE__)}/tasks/*.rake"].sort.each { |ext| load ext }
@@ -0,0 +1,30 @@
1
+ # -*- encoding: utf-8 -*-
2
+
3
+ Gem::Specification.new do |s|
4
+ s.name = %q{basecamp_wrapper}
5
+ s.version = "0.1.0"
6
+
7
+ s.required_rubygems_version = Gem::Requirement.new(">= 1.2") if s.respond_to? :required_rubygems_version=
8
+ s.authors = ["Chelsea Robb"]
9
+ s.date = %q{2010-07-30}
10
+ s.description = %q{A wrapper for the 37 Signals Basecamp API}
11
+ s.email = %q{chelsea@fluidmedia.com}
12
+ s.extra_rdoc_files = ["README", "lib/basecamp.rb", "lib/basecamp_wrapper.rb"]
13
+ s.files = ["README", "Rakefile", "lib/basecamp.rb", "lib/basecamp_wrapper.rb", "Manifest", "basecamp_wrapper.gemspec"]
14
+ s.homepage = %q{http://www.github.com/fmiopensource/basecamp_wrapper}
15
+ s.rdoc_options = ["--line-numbers", "--inline-source", "--title", "Basecamp_wrapper", "--main", "README"]
16
+ s.require_paths = ["lib"]
17
+ s.rubyforge_project = %q{basecamp_wrapper}
18
+ s.rubygems_version = %q{1.3.6}
19
+ s.summary = %q{A wrapper for the 37 Signals Basecamp API}
20
+
21
+ if s.respond_to? :specification_version then
22
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
23
+ s.specification_version = 3
24
+
25
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
26
+ else
27
+ end
28
+ else
29
+ end
30
+ end
data/lib/basecamp.rb ADDED
@@ -0,0 +1,667 @@
1
+ require 'net/https'
2
+ require 'yaml'
3
+ require 'date'
4
+ require 'time'
5
+
6
+ begin
7
+ require 'xmlsimple'
8
+ rescue LoadError
9
+ begin
10
+ require 'rubygems'
11
+ require 'xmlsimple'
12
+ rescue LoadError
13
+ abort <<-ERROR
14
+ The 'xml-simple' library could not be loaded. If you have RubyGems installed
15
+ you can install xml-simple by doing "gem install xml-simple".
16
+ ERROR
17
+ end
18
+ end
19
+
20
+ begin
21
+ require 'active_resource'
22
+ rescue LoadError
23
+ begin
24
+ require 'rubygems'
25
+ require 'active_resource'
26
+ rescue LoadError
27
+ abort <<-ERROR
28
+ The 'activeresource' library could not be loaded. If you have RubyGems
29
+ installed you can install ActiveResource by doing "gem install activeresource".
30
+ ERROR
31
+ end
32
+ end
33
+
34
+ # = A Ruby library for working with the Basecamp web-services API.
35
+ #
36
+ # For more information about the Basecamp web-services API, visit:
37
+ #
38
+ # http://developer.37signals.com/basecamp
39
+ #
40
+ # NOTE: not all of Basecamp's web-services are accessible via REST. This
41
+ # library provides access to RESTful services via ActiveResource. Services not
42
+ # yet upgraded to REST are accessed via the Basecamp class. Continue reading
43
+ # for more details.
44
+ #
45
+ #
46
+ # == Establishing a Connection
47
+ #
48
+ # The first thing you need to do is establish a connection to Basecamp. This
49
+ # requires your Basecamp site address and your login credentials. Example:
50
+ #
51
+ # Basecamp.establish_connection!('you.grouphub.com', 'username', 'password')
52
+ #
53
+ # This is the same whether you're accessing using the ActiveResource interface,
54
+ # or the legacy interface.
55
+ #
56
+ #
57
+ # == Using the REST interface via ActiveResource
58
+ #
59
+ # The REST interface is accessed via ActiveResource, a popular Ruby library
60
+ # that implements object-relational mapping for REST web-services. For more
61
+ # information on working with ActiveResource, see:
62
+ #
63
+ # * http://api.rubyonrails.org/files/activeresource/README.html
64
+ # * http://api.rubyonrails.org/classes/ActiveResource/Base.html
65
+ #
66
+ #
67
+ # === Finding a Resource
68
+ #
69
+ # Find a specific resource using the +find+ method. Attributes of the resource
70
+ # are available as instance methods on the resulting object. For example, to
71
+ # find a message with the ID of 8675309 and access its title attribute, you
72
+ # would do the following:
73
+ #
74
+ # m = Basecamp::Message.find(8675309)
75
+ # m.title # => 'Jenny'
76
+ #
77
+ # To find all messages for a given project, use find(:all), passing the
78
+ # project_id as a parameter to find. Example:
79
+ #
80
+ # messages = Basecamp::Message.find(:all, params => { :project_id => 1037 })
81
+ # messages.size # => 25
82
+ #
83
+ #
84
+ # === Creating a Resource
85
+ #
86
+ # Create a resource by making a new instance of that resource, setting its
87
+ # attributes, and saving it. If the resource requires a prefix to identify
88
+ # it (as is the case with resources that belong to a sub-resource, such as a
89
+ # project), it should be specified when instantiating the object. Examples:
90
+ #
91
+ # m = Basecamp::Message.new(:project_id => 1037)
92
+ # m.category_id = 7301
93
+ # m.title = 'Message in a bottle'
94
+ # m.body = 'Another lonely day, with no one here but me'
95
+ # m.save # => true
96
+ #
97
+ # c = Basecamp::Comment.new(:post_id => 25874)
98
+ # c.body = 'Did you get those TPS reports?'
99
+ # c.save # => true
100
+ #
101
+ # You can also create a resource using the +create+ method, which will create
102
+ # and save it in one step. Example:
103
+ #
104
+ # Basecamp::TodoItem.create(:todo_list_id => 3422, :contents => 'Do it')
105
+ #
106
+ #
107
+ # === Updating a Resource
108
+ #
109
+ # To update a resource, first find it by its id, change its attributes, and
110
+ # save it. Example:
111
+ #
112
+ # m = Basecamp::Message.find(8675309)
113
+ # m.body = 'Changed'
114
+ # m.save # => true
115
+ #
116
+ #
117
+ # === Deleting a Resource
118
+ #
119
+ # To delete a resource, use the +delete+ method with the ID of the resource
120
+ # you want to delete. Example:
121
+ #
122
+ # Basecamp::Message.delete(1037)
123
+ #
124
+ #
125
+ # === Attaching Files to a Resource
126
+ #
127
+ # If the resource accepts file attachments, the +attachments+ parameter should
128
+ # be an array of Basecamp::Attachment objects. Example:
129
+ #
130
+ # a1 = Basecamp::Attachment.create('primary', File.read('primary.doc'))
131
+ # a2 = Basecamp::Attachment.create('another', File.read('another.doc'))
132
+ #
133
+ # m = Basecamp::Message.new(:project_id => 1037)
134
+ # ...
135
+ # m.attachments = [a1, a2]
136
+ # m.save # => true
137
+ #
138
+ #
139
+ # = Using the non-REST inteface
140
+ #
141
+ # The non-REST interface is accessed via instance methods on the Basecamp
142
+ # class. Ensure you've established a connection, then create a new Basecamp
143
+ # instance and call methods on it. Object attributes are accessible as methods.
144
+ # Example:
145
+ #
146
+ # session = Basecamp.new
147
+ # person = session.person(93832) # => #<Record(person)..>
148
+ # person.first_name # => "Jason"
149
+ #
150
+ class Basecamp
151
+ class Connection #:nodoc:
152
+ def initialize(master)
153
+ @master = master
154
+ @connection = Net::HTTP.new(master.site, master.use_ssl ? 443 : 80)
155
+ @connection.use_ssl = master.use_ssl
156
+ @connection.verify_mode = OpenSSL::SSL::VERIFY_NONE if master.use_ssl
157
+ end
158
+
159
+ def post(path, body, headers = {})
160
+ request = Net::HTTP::Post.new(path, headers.merge('Accept' => 'application/xml'))
161
+ request.basic_auth(@master.user, @master.password)
162
+ @connection.request(request, body)
163
+ end
164
+
165
+ def get(path, headers = {})
166
+ request = Net::HTTP::Get.new(path, headers.merge('Accept' => 'application/xml'))
167
+ request.basic_auth(@master.user, @master.password)
168
+ @connection.request(request)
169
+ end
170
+ end
171
+
172
+ class Resource < ActiveResource::Base #:nodoc:
173
+ class << self
174
+ def parent_resources(*parents)
175
+ @parent_resources = parents
176
+ end
177
+
178
+ def element_name
179
+ name.split(/::/).last.underscore
180
+ end
181
+
182
+ def prefix_source
183
+ if @parent_resources
184
+ @parent_resources.map { |resource| "/#{resource.to_s.pluralize}/:#{resource}_id" }.join + '/'
185
+ else
186
+ '/'
187
+ end
188
+ end
189
+
190
+ def prefix(options = {})
191
+ if options.any?
192
+ options.map { |name, value| "/#{name.to_s.chomp('_id').pluralize}/#{value}" }.join + '/'
193
+ else
194
+ '/'
195
+ end
196
+ end
197
+ end
198
+
199
+ def prefix_options
200
+ id ? {} : super
201
+ end
202
+ end
203
+
204
+ class Project < Resource
205
+ def self.all
206
+ find(:all)
207
+ end
208
+ end
209
+
210
+ class Company < Resource
211
+ parent_resources :project
212
+
213
+ def self.on_project(project_id, options = {})
214
+ find(:all, :params => options.merge(:project_id => project_id))
215
+ end
216
+
217
+ end
218
+
219
+ # == Creating different types of categories
220
+ #
221
+ # The type parameter is required when creating a category. For exampe, to
222
+ # create an attachment category for a particular project:
223
+ #
224
+ # c = Basecamp::Category.new(:project_id => 1037)
225
+ # c.type = 'attachment'
226
+ # c.name = 'Pictures'
227
+ # c.save # => true
228
+ #
229
+ class Category < Resource
230
+ parent_resources :project
231
+
232
+ def self.all(project_id, options = {})
233
+ find(:all, :params => options.merge(:project_id => project_id))
234
+ end
235
+
236
+ def self.post_categories(project_id, options = {})
237
+ find(:all, :params => options.merge(:project_id => project_id, :type => 'post'))
238
+ end
239
+
240
+ def self.attachment_categories(project_id, options = {})
241
+ find(:all, :params => options.merge(:project_id => project_id, :type => 'attachment'))
242
+ end
243
+ end
244
+
245
+ class Message < Resource
246
+ parent_resources :project
247
+ set_element_name 'post'
248
+
249
+ # Returns the most recent 25 messages in the given project (and category,
250
+ # if specified). If you need to retrieve older messages, use the archive
251
+ # method instead. Example:
252
+ #
253
+ # Basecamp::Message.recent(1037)
254
+ # Basecamp::Message.recent(1037, :category_id => 7301)
255
+ #
256
+ def self.recent(project_id, options = {})
257
+ find(:all, :params => options.merge(:project_id => project_id))
258
+ end
259
+
260
+ # Returns a summary of all messages in the given project (and category, if
261
+ # specified). The summary is simply the title and category of the message,
262
+ # as well as the number of attachments (if any). Example:
263
+ #
264
+ # Basecamp::Message.archive(1037)
265
+ # Basecamp::Message.archive(1037, :category_id => 7301)
266
+ #
267
+ def self.archive(project_id, options = {})
268
+ find(:all, :params => options.merge(:project_id => project_id), :from => :archive)
269
+ end
270
+
271
+ def comments(options = {})
272
+ @comments ||= Comment.find(:all, :params => options.merge(:post_id => id))
273
+ end
274
+ end
275
+
276
+ # == Creating comments for multiple resources
277
+ #
278
+ # Comments can be created for messages, milestones, and to-dos, identified
279
+ # by the <tt>post_id</tt>, <tt>milestone_id</tt>, and <tt>todo_item_id</tt>
280
+ # params respectively.
281
+ #
282
+ # For example, to create a comment on the message with id #8675309:
283
+ #
284
+ # c = Basecamp::Comment.new(:post_id => 8675309)
285
+ # c.body = 'Great tune'
286
+ # c.save # => true
287
+ #
288
+ # Similarly, to create a comment on a milestone:
289
+ #
290
+ # c = Basecamp::Comment.new(:milestone_id => 8473647)
291
+ # c.body = 'Is this done yet?'
292
+ # c.save # => true
293
+ #
294
+ class Comment < Resource
295
+ parent_resources :post, :milestone, :todo_item
296
+ end
297
+
298
+ class TodoList < Resource
299
+ parent_resources :project
300
+
301
+ # Returns all lists for a project. If complete is true, only completed lists
302
+ # are returned. If complete is false, only uncompleted lists are returned.
303
+ def self.all(project_id, complete = nil)
304
+ filter = case complete
305
+ when nil then "all"
306
+ when true then "finished"
307
+ when false then "pending"
308
+ else raise ArgumentError, "invalid value for `complete'"
309
+ end
310
+
311
+ find(:all, :params => { :project_id => project_id, :filter => filter })
312
+ end
313
+
314
+ def todo_items(options = {})
315
+ begin
316
+ @todo_items ||= TodoItem.find(:all, :params => options.merge(:todo_list_id => id))
317
+ rescue
318
+ @todo_items = []
319
+ end
320
+ end
321
+ end
322
+
323
+ class TodoItem < Resource
324
+ parent_resources :todo_list
325
+
326
+ def todo_list(options = {})
327
+ @todo_list ||= TodoList.find(todo_list_id, options)
328
+ end
329
+
330
+ def time_entries(options = {})
331
+ @time_entries ||= TimeEntry.find(:all, :params => options.merge(:todo_item_id => id))
332
+ end
333
+
334
+ def comments(options = {})
335
+ @comments ||= Comment.find(:all, :params => options.merge(:todo_item_id => id))
336
+ end
337
+
338
+ def complete!
339
+ put(:complete)
340
+ end
341
+
342
+ def uncomplete!
343
+ put(:uncomplete)
344
+ end
345
+ end
346
+
347
+ class TimeEntry < Resource
348
+ parent_resources :project, :todo_item
349
+
350
+ def self.all(project_id, page = 0)
351
+ find(:all, :params => { :project_id => project_id, :page => page })
352
+ end
353
+
354
+ def self.report(options={})
355
+ find(:all, :from => :report, :params => options)
356
+ end
357
+ end
358
+
359
+ class Person < Resource
360
+ parent_resources :company
361
+ def self.all(company_id)
362
+ find(:all, :params => {:company_id => company_id})
363
+ end
364
+ end
365
+
366
+ class Category < Resource
367
+ parent_resources :project
368
+ end
369
+
370
+ class Attachment
371
+ attr_accessor :id, :filename, :content
372
+
373
+ def self.create(filename, content)
374
+ returning new(filename, content) do |attachment|
375
+ attachment.save
376
+ end
377
+ end
378
+
379
+ def initialize(filename, content)
380
+ @filename, @content = filename, content
381
+ end
382
+
383
+ def attributes
384
+ { :file => id, :original_filename => filename }
385
+ end
386
+
387
+ def to_xml(options = {})
388
+ { :file => attributes }.to_xml(options)
389
+ end
390
+
391
+ def inspect
392
+ to_s
393
+ end
394
+
395
+ def save
396
+ response = Basecamp.connection.post('/upload', content, 'Content-Type' => 'application/octet-stream')
397
+
398
+ if response.code == '200'
399
+ self.id = Hash.from_xml(response.body)['upload']['id']
400
+ true
401
+ else
402
+ raise "Could not save attachment: #{response.message} (#{response.code})"
403
+ end
404
+ end
405
+ end
406
+
407
+
408
+ class Record #:nodoc:
409
+ attr_reader :type
410
+
411
+ def initialize(type, hash)
412
+ @type, @hash = type, hash
413
+ end
414
+
415
+ def [](name)
416
+ name = dashify(name)
417
+
418
+ case @hash[name]
419
+ when Hash then
420
+ @hash[name] = if (@hash[name].keys.length == 1 && @hash[name].values.first.is_a?(Array))
421
+ @hash[name].values.first.map { |v| Record.new(@hash[name].keys.first, v) }
422
+ else
423
+ Record.new(name, @hash[name])
424
+ end
425
+ else
426
+ @hash[name]
427
+ end
428
+ end
429
+
430
+ def id
431
+ @hash['id']
432
+ end
433
+
434
+ def attributes
435
+ @hash.keys
436
+ end
437
+
438
+ def respond_to?(sym)
439
+ super || @hash.has_key?(dashify(sym))
440
+ end
441
+
442
+ def method_missing(sym, *args)
443
+ if args.empty? && !block_given? && respond_to?(sym)
444
+ self[sym]
445
+ else
446
+ super
447
+ end
448
+ end
449
+
450
+ def to_s
451
+ "\#<Record(#{@type}) #{@hash.inspect[1..-2]}>"
452
+ end
453
+
454
+ def inspect
455
+ to_s
456
+ end
457
+
458
+ private
459
+
460
+ def dashify(name)
461
+ name.to_s.tr("_", "-")
462
+ end
463
+ end
464
+
465
+ attr_accessor :use_xml
466
+
467
+ class << self
468
+ attr_reader :site, :user, :password, :use_ssl
469
+
470
+ def establish_connection!(site, user, password, use_ssl = false)
471
+ @site = site
472
+ @user = user
473
+ @password = password
474
+ @use_ssl = use_ssl
475
+
476
+ Resource.user = user
477
+ Resource.password = password
478
+ Resource.site = (use_ssl ? "https" : "http") + "://" + site
479
+
480
+ @connection = Connection.new(self)
481
+ end
482
+
483
+ def connection
484
+ @connection || raise('No connection established')
485
+ end
486
+
487
+ def get_token
488
+ response = @connection.get('/me.xml')
489
+ xml = XmlSimple.xml_in(response.body)
490
+ xml['token'][0]
491
+ end
492
+ end
493
+
494
+ def initialize
495
+ @use_xml = false
496
+ end
497
+
498
+ # ==========================================================================
499
+ # PEOPLE
500
+ # ==========================================================================
501
+
502
+ # Return an array of the people in the given company. If the project-id is
503
+ # given, only people who have access to the given project will be returned.
504
+ def people(company_id, project_id=nil)
505
+ url = project_id ? "/projects/#{project_id}" : ""
506
+ url << "/contacts/people/#{company_id}"
507
+ records "person", url
508
+ end
509
+
510
+ # Return information about the person with the given id
511
+ def person(id)
512
+ record "/contacts/person/#{id}"
513
+ end
514
+
515
+ # ==========================================================================
516
+ # MILESTONES
517
+ # ==========================================================================
518
+
519
+ # Returns a list of all milestones for the given project, optionally filtered
520
+ # by whether they are completed, late, or upcoming.
521
+ def milestones(project_id, find = 'all')
522
+ records "milestone", "/projects/#{project_id}/milestones/list", :find => find
523
+ end
524
+
525
+ # Create a new milestone for the given project. +data+ must be hash of the
526
+ # values to set, including +title+, +deadline+, +responsible_party+, and
527
+ # +notify+.
528
+ def create_milestone(project_id, data)
529
+ create_milestones(project_id, [data]).first
530
+ end
531
+
532
+ # As #create_milestone, but can create multiple milestones in a single
533
+ # request. The +milestones+ parameter must be an array of milestone values as
534
+ # described in #create_milestone.
535
+ def create_milestones(project_id, milestones)
536
+ records "milestone", "/projects/#{project_id}/milestones/create", :milestone => milestones
537
+ end
538
+
539
+ # Updates an existing milestone.
540
+ def update_milestone(id, data, move = false, move_off_weekends = false)
541
+ record "/milestones/update/#{id}", :milestone => data,
542
+ :move_upcoming_milestones => move,
543
+ :move_upcoming_milestones_off_weekends => move_off_weekends
544
+ end
545
+
546
+ # Destroys the milestone with the given id.
547
+ def delete_milestone(id)
548
+ record "/milestones/delete/#{id}"
549
+ end
550
+
551
+ # Complete the milestone with the given id
552
+ def complete_milestone(id)
553
+ record "/milestones/complete/#{id}"
554
+ end
555
+
556
+ # Uncomplete the milestone with the given id
557
+ def uncomplete_milestone(id)
558
+ record "/milestones/uncomplete/#{id}"
559
+ end
560
+
561
+ private
562
+
563
+ # Make a raw web-service request to Basecamp. This will return a Hash of
564
+ # Arrays of the response, and may seem a little odd to the uninitiated.
565
+ def request(path, parameters = {})
566
+ response = Basecamp.connection.post(path, convert_body(parameters), "Content-Type" => content_type)
567
+
568
+ if response.code.to_i / 100 == 2
569
+ result = XmlSimple.xml_in(response.body, 'keeproot' => true, 'contentkey' => '__content__', 'forcecontent' => true)
570
+ typecast_value(result)
571
+ else
572
+ raise "#{response.message} (#{response.code})"
573
+ end
574
+ end
575
+
576
+ # A convenience method for wrapping the result of a query in a Record
577
+ # object. This assumes that the result is a singleton, not a collection.
578
+ def record(path, parameters={})
579
+ result = request(path, parameters)
580
+ (result && !result.empty?) ? Record.new(result.keys.first, result.values.first) : nil
581
+ end
582
+
583
+ # A convenience method for wrapping the result of a query in Record
584
+ # objects. This assumes that the result is a collection--any singleton
585
+ # result will be wrapped in an array.
586
+ def records(node, path, parameters={})
587
+ result = request(path, parameters).values.first or return []
588
+ result = result[node] or return []
589
+ result = [result] unless Array === result
590
+ result.map { |row| Record.new(node, row) }
591
+ end
592
+
593
+ def convert_body(body)
594
+ body = use_xml ? body.to_legacy_xml : body.to_yaml
595
+ end
596
+
597
+ def content_type
598
+ use_xml ? "application/xml" : "application/x-yaml"
599
+ end
600
+
601
+ def typecast_value(value)
602
+ case value
603
+ when Hash
604
+ if value.has_key?("__content__")
605
+ content = translate_entities(value["__content__"]).strip
606
+ case value["type"]
607
+ when "integer" then content.to_i
608
+ when "boolean" then content == "true"
609
+ when "datetime" then Time.parse(content)
610
+ when "date" then Date.parse(content)
611
+ else content
612
+ end
613
+ # a special case to work-around a bug in XmlSimple. When you have an empty
614
+ # tag that has an attribute, XmlSimple will not add the __content__ key
615
+ # to the returned hash. Thus, we check for the presense of the 'type'
616
+ # attribute to look for empty, typed tags, and simply return nil for
617
+ # their value.
618
+ elsif value.keys == %w(type)
619
+ nil
620
+ elsif value["nil"] == "true"
621
+ nil
622
+ # another special case, introduced by the latest rails, where an array
623
+ # type now exists. This is parsed by XmlSimple as a two-key hash, where
624
+ # one key is 'type' and the other is the actual array value.
625
+ elsif value.keys.length == 2 && value["type"] == "array"
626
+ value.delete("type")
627
+ typecast_value(value)
628
+ else
629
+ value.empty? ? nil : value.inject({}) do |h,(k,v)|
630
+ h[k] = typecast_value(v)
631
+ h
632
+ end
633
+ end
634
+ when Array
635
+ value.map! { |i| typecast_value(i) }
636
+ case value.length
637
+ when 0 then nil
638
+ when 1 then value.first
639
+ else value
640
+ end
641
+ else
642
+ raise "can't typecast #{value.inspect}"
643
+ end
644
+ end
645
+
646
+ def translate_entities(value)
647
+ value.gsub(/&lt;/, "<").
648
+ gsub(/&gt;/, ">").
649
+ gsub(/&quot;/, '"').
650
+ gsub(/&apos;/, "'").
651
+ gsub(/&amp;/, "&")
652
+ end
653
+ end
654
+
655
+ # A minor hack to let Xml-Simple serialize symbolic keys in hashes
656
+ class Symbol
657
+ def [](*args)
658
+ to_s[*args]
659
+ end
660
+ end
661
+
662
+ class Hash
663
+ def to_legacy_xml
664
+ XmlSimple.xml_out({:request => self}, 'keeproot' => true, 'noattr' => true)
665
+ end
666
+ end
667
+
@@ -0,0 +1 @@
1
+ require 'basecamp.rb'
metadata ADDED
@@ -0,0 +1,75 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: basecamp_wrapper
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 1
8
+ - 0
9
+ version: 0.1.0
10
+ platform: ruby
11
+ authors:
12
+ - Chelsea Robb
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2010-07-30 00:00:00 -04:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description: A wrapper for the 37 Signals Basecamp API
22
+ email: chelsea@fluidmedia.com
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files:
28
+ - README
29
+ - lib/basecamp.rb
30
+ - lib/basecamp_wrapper.rb
31
+ files:
32
+ - README
33
+ - Rakefile
34
+ - lib/basecamp.rb
35
+ - lib/basecamp_wrapper.rb
36
+ - Manifest
37
+ - basecamp_wrapper.gemspec
38
+ has_rdoc: true
39
+ homepage: http://www.github.com/fmiopensource/basecamp_wrapper
40
+ licenses: []
41
+
42
+ post_install_message:
43
+ rdoc_options:
44
+ - --line-numbers
45
+ - --inline-source
46
+ - --title
47
+ - Basecamp_wrapper
48
+ - --main
49
+ - README
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ segments:
57
+ - 0
58
+ version: "0"
59
+ required_rubygems_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ segments:
64
+ - 1
65
+ - 2
66
+ version: "1.2"
67
+ requirements: []
68
+
69
+ rubyforge_project: basecamp_wrapper
70
+ rubygems_version: 1.3.6
71
+ signing_key:
72
+ specification_version: 3
73
+ summary: A wrapper for the 37 Signals Basecamp API
74
+ test_files: []
75
+