rtmapi 0.5

Sign up to get free protection for your applications and to get access to all the features.
Files changed (70) hide show
  1. data/README +236 -0
  2. data/gpl.txt +339 -0
  3. data/lib/rtmapi.rb +597 -0
  4. data/test/data/test_get_method_exceptions.1.xml +2 -0
  5. data/test/data/test_get_method_exceptions.2.xml +2 -0
  6. data/test/data/test_get_method_exceptions.3.xml +2 -0
  7. data/test/data/test_rtm_auth_checkToken_bad.1.xml +2 -0
  8. data/test/data/test_rtm_auth_checkToken_good.1.xml +2 -0
  9. data/test/data/test_rtm_contacts_delete_and_add.1.xml +2 -0
  10. data/test/data/test_rtm_contacts_delete_and_add.2.xml +2 -0
  11. data/test/data/test_rtm_contacts_delete_and_add.3.xml +2 -0
  12. data/test/data/test_rtm_contacts_delete_and_add.4.xml +2 -0
  13. data/test/data/test_rtm_contacts_delete_and_add.5.xml +2 -0
  14. data/test/data/test_rtm_contacts_delete_and_add.6.xml +2 -0
  15. data/test/data/test_rtm_contacts_getList.1.xml +2 -0
  16. data/test/data/test_rtm_get_frob.1.xml +2 -0
  17. data/test/data/test_rtm_get_task_and_is_complete.1.xml +2 -0
  18. data/test/data/test_rtm_groups_add_and_delete.1.xml +2 -0
  19. data/test/data/test_rtm_groups_add_and_delete.2.xml +2 -0
  20. data/test/data/test_rtm_groups_add_and_delete.3.xml +2 -0
  21. data/test/data/test_rtm_groups_add_and_delete.4.xml +2 -0
  22. data/test/data/test_rtm_groups_add_and_delete.5.xml +2 -0
  23. data/test/data/test_rtm_groups_add_and_delete.6.xml +2 -0
  24. data/test/data/test_rtm_groups_add_and_delete.7.xml +2 -0
  25. data/test/data/test_rtm_groups_add_and_delete.8.xml +2 -0
  26. data/test/data/test_rtm_groups_add_and_delete.9.xml +2 -0
  27. data/test/data/test_rtm_groups_getList.1.xml +2 -0
  28. data/test/data/test_rtm_lists_getList.1.xml +2 -0
  29. data/test/data/test_rtm_lists_setName.1.xml +2 -0
  30. data/test/data/test_rtm_lists_setName.2.xml +2 -0
  31. data/test/data/test_rtm_lists_setName.3.xml +2 -0
  32. data/test/data/test_rtm_lists_setName.4.xml +2 -0
  33. data/test/data/test_rtm_lists_setName.5.xml +2 -0
  34. data/test/data/test_rtm_lists_setName.6.xml +2 -0
  35. data/test/data/test_rtm_reflection_getMethods.1.xml +2 -0
  36. data/test/data/test_rtm_settings_getList.1.xml +2 -0
  37. data/test/data/test_rtm_tasks_add.1.xml +2 -0
  38. data/test/data/test_rtm_tasks_add.2.xml +2 -0
  39. data/test/data/test_rtm_tasks_add.3.xml +2 -0
  40. data/test/data/test_rtm_tasks_getList.1.xml +2 -0
  41. data/test/data/test_rtm_tasks_getList_all.1.xml +2 -0
  42. data/test/data/test_rtm_tasks_getList_from_smartlist.1.xml +2 -0
  43. data/test/data/test_rtm_tasks_setDueDate.1.xml +2 -0
  44. data/test/data/test_rtm_tasks_setDueDate.2.xml +2 -0
  45. data/test/data/test_rtm_tasks_setDueDate.3.xml +2 -0
  46. data/test/data/test_rtm_tasks_setDueDate.4.xml +2 -0
  47. data/test/data/test_rtm_tasks_setDueDate.5.xml +2 -0
  48. data/test/data/test_rtm_tasks_setDueDate.6.xml +2 -0
  49. data/test/data/test_rtm_tasks_setRecurrence.1.xml +2 -0
  50. data/test/data/test_rtm_tasks_setRecurrence.2.xml +2 -0
  51. data/test/data/test_rtm_tasks_setRecurrence.3.xml +2 -0
  52. data/test/data/test_rtm_tasks_setRecurrence.4.xml +2 -0
  53. data/test/data/test_rtm_test_echo.1.xml +2 -0
  54. data/test/data/test_rtm_time_parse.1.xml +2 -0
  55. data/test/data/test_rtm_timelines_and_transactions_with_priorities.1.xml +2 -0
  56. data/test/data/test_rtm_timelines_and_transactions_with_priorities.2.xml +2 -0
  57. data/test/data/test_rtm_timelines_and_transactions_with_priorities.3.xml +2 -0
  58. data/test/data/test_rtm_timelines_and_transactions_with_priorities.4.xml +2 -0
  59. data/test/data/test_rtm_timelines_and_transactions_with_priorities.5.xml +2 -0
  60. data/test/data/test_rtm_timelines_and_transactions_with_priorities.6.xml +2 -0
  61. data/test/data/test_rtm_timelines_and_transactions_with_tags.1.xml +2 -0
  62. data/test/data/test_rtm_timelines_and_transactions_with_tags.2.xml +2 -0
  63. data/test/data/test_rtm_timelines_and_transactions_with_tags.3.xml +2 -0
  64. data/test/data/test_rtm_timelines_and_transactions_with_tags.4.xml +2 -0
  65. data/test/data/test_rtm_timelines_and_transactions_with_tags.5.xml +2 -0
  66. data/test/data/test_rtm_timelines_and_transactions_with_tags.6.xml +2 -0
  67. data/test/data/test_rtm_timezones_getList.1.xml +2 -0
  68. data/test/data/test_rtm_user.1.xml +2 -0
  69. data/test/test-rtmapi.rb +506 -0
  70. metadata +131 -0
@@ -0,0 +1,597 @@
1
+ # This file is part of the RTM Ruby API Wrapper.
2
+ #
3
+ # The RTM Ruby API Wrapper is free software; you can redistribute it and/or modify
4
+ # it under the terms of the GNU General Public License as
5
+ # published by the Free Software Foundation; either version 2 of the
6
+ # License, or (at your option) any later version.
7
+ #
8
+ # The RTM Ruby API Wrapper is distributed in the hope that it will be useful,
9
+ # but WITHOUT ANY WARRANTY; without even the implied warranty of
10
+ # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11
+ # GNU General Public License for more details.
12
+ #
13
+ # You should have received a copy of the GNU General Public License
14
+ # along with the RTM Ruby API Wrapper; if not, write to the Free Software
15
+ # Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
16
+ #
17
+ # (c) 2006, QuantumFoam.org, Inc.
18
+
19
+
20
+ require 'uri'
21
+ require 'md5'
22
+ require 'cgi'
23
+ require 'net/http'
24
+ require 'date'
25
+ require 'time'
26
+ require 'parsedate'
27
+ require 'rubygems'
28
+ require 'xml/libxml'
29
+
30
+
31
+ #TODO: allow specifying whether retval should be indexed by rtm_id or list name for lists
32
+
33
+ class RememberTheMilk
34
+ RUBY_API_VERSION = '0.5'
35
+ # you can just put set these here so you don't have to pass them in with
36
+ # every constructor call
37
+ API_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'
38
+ API_SHARED_SECRET = 'xxxxxxxxxxxxxxxx'
39
+
40
+
41
+ Element = 0
42
+ CloseTag = 1
43
+ Tag = 2
44
+ Attributes = 3
45
+ #SelfContainedElement = 4
46
+ TextNode = 4
47
+
48
+ TagName = 0
49
+ TagHash = 1
50
+
51
+
52
+ attr_accessor :debug, :auth_token, :return_raw_response, :api_key, :shared_secret, :max_connection_attempts, :use_user_tz
53
+
54
+ def user
55
+ @user_info_cache[auth_token] ||= auth.checkToken.user
56
+ end
57
+
58
+ def user_settings
59
+ @user_settings_cache[auth_token]
60
+ end
61
+
62
+ def time_to_user_tz( time )
63
+ return time unless(use_user_tz && auth_token && defined?(TZInfo::Timezone.get))
64
+ begin
65
+ unless defined?(@user_settings_cache[auth_token]) && defined?(@user_settings_cache[auth_token][:tz])
66
+ @user_settings_cache[auth_token] = settings.getList
67
+ @user_settings_cache[auth_token][:tz] = TZInfo::Timezone.get(@user_settings_cache[auth_token].timezone)
68
+ end
69
+ debug "returning time in local zone(%s/%s)", @user_settings_cache[auth_token].timezone, @user_settings_cache[auth_token][:tz]
70
+ @user_settings_cache[auth_token][:tz].utc_to_local(time)
71
+ rescue Exception => err
72
+ debug "unable to read local timezone for auth_token<%s>, ignoring timezone. err<%s>", auth_token, err
73
+ time
74
+ end
75
+ end
76
+
77
+ def logout_user(auth_token)
78
+ @auth_token = nil if @auth_token == auth_token
79
+ @user_settings_cache.delete(auth_token)
80
+ @user_info_cache.delete(auth_token)
81
+ end
82
+
83
+ # TODO: test efficacy of using https://www.rememberthemilk.com/services/rest/
84
+ def initialize( api_key = API_KEY, shared_secret = API_SHARED_SECRET, endpoint = 'http://www.rememberthemilk.com/services/rest/' )
85
+ @max_connection_attempts = 3
86
+ @debug = false
87
+ @api_key = api_key
88
+ @shared_secret = shared_secret
89
+ @uri = URI.parse(endpoint)
90
+ @auth_token = nil
91
+ @return_raw_response = false
92
+ @use_user_tz = true
93
+ @user_settings_cache = {}
94
+ @user_info_cache = {}
95
+ @xml_parser = XML::Parser.new
96
+ end
97
+
98
+ def version() RUBY_API_VERSION end
99
+
100
+ def debug(*args)
101
+ return unless @debug
102
+ if defined?(RAILS_DEFAULT_LOGGER)
103
+ RAILS_DEFAULT_LOGGER.warn( sprintf(*args) )
104
+ else
105
+ $stderr.puts(sprintf(*args))
106
+ end
107
+ end
108
+
109
+ def auth_url( perms = 'delete' )
110
+ auth_url = 'http://www.rememberthemilk.com/services/auth/'
111
+ args = { 'api_key' => @api_key, 'perms' => perms }
112
+ args['api_sig'] = sign_request(args)
113
+ return auth_url + '?' + args.keys.collect {|k| "#{k}=#{args[k]}"}.join('&')
114
+ end
115
+
116
+ # this is a little fragile. it assumes we are being invoked with RTM api calls
117
+ # (which are two levels deep)
118
+ # e.g.,
119
+ # rtm = RememberTheMilk.new
120
+ # data = rtm.reflection.getMethodInfo('method_name' => 'rtm.test.login')
121
+ # the above line gets turned into two calls, the first to this, which returns
122
+ # an RememberTheMilkAPINamespace object, which then gets *its* method_missing
123
+ # invoked with 'getMethodInfo' and the above args
124
+ # i.e.,
125
+ # rtm.foo.bar
126
+ # rtm.foo() => a
127
+ # a.bar
128
+
129
+ def method_missing( symbol, *args )
130
+ rtm_namespace = symbol.id2name
131
+ debug("method_missing called with namespace <%s>", rtm_namespace)
132
+ RememberTheMilkAPINamespace.new( rtm_namespace, self )
133
+ end
134
+
135
+ def xml_node_to_hash( node, recursion_level = 0 )
136
+ result = xml_attributes_to_hash( node.properties )
137
+ if node.element? == false
138
+ result[node.name.to_sym] = node.content
139
+ else
140
+ node.each do |child|
141
+ name = child.name.to_sym
142
+ value = xml_node_to_hash( child, recursion_level+1 )
143
+
144
+ # if we have the same node name appear multiple times, we need to build up an array
145
+ # of the converted nodes
146
+ if !result.has_key?(name)
147
+ result[name] = value
148
+ elsif result[name].class != Array
149
+ result[name] = [result[name], value]
150
+ else
151
+ result[name] << value
152
+ end
153
+ end
154
+ end
155
+
156
+ # top level nodes should be a hash no matter what
157
+ (recursion_level == 0 || result.values.size > 1) ? result : result.values[0]
158
+ end
159
+
160
+ def xml_attributes_to_hash( attributes, class_name = RememberTheMilkHash )
161
+ hash = class_name.send(:new)
162
+ attributes.each {|a| hash[a.name.to_sym] = a.value} if attributes.respond_to?(:each)
163
+ return hash
164
+ end
165
+
166
+ def index_data_into_hash( data, key )
167
+ new_hash = RememberTheMilkHash.new
168
+
169
+ if data.class == Array
170
+ data.each {|datum| new_hash[datum[key]] = datum }
171
+ else
172
+ new_hash[data[key]] = data
173
+ end
174
+
175
+ new_hash
176
+ end
177
+
178
+ def parse_response(response,method,args)
179
+ # groups -- an array of group obj
180
+ # group -- some attributes and a possible contacts array
181
+ # contacts -- an array of contact obj
182
+ # contact -- just attributes
183
+ # lists -- array of list obj
184
+ # list -- attributes and possible filter obj, and a set of taskseries objs?
185
+ # task sereies obj are always wrapped in a list. why?
186
+ # taskseries -- set of attributes, array of tags, an rrule, participants array of contacts, notes,
187
+ # and task. created and modified are time obj,
188
+ # task -- attributes, due/added are time obj
189
+ # note -- attributes and a body of text, with created and modified time obj
190
+ # time -- convert to a time obj
191
+ # timeline -- just has a body of text
192
+ return true unless response.keys.size > 1 # empty response (stat only)
193
+
194
+ rtm_transaction = nil
195
+ if response.has_key?(:transaction)
196
+ # debug("got back <%s> elements in my transaction", response[:transaction].keys.size)
197
+ # we just did a write operation, got back a transaction AND some data.
198
+ # Now, we will do some fanciness.
199
+ rtm_transaction = response[:transaction]
200
+ end
201
+
202
+ response_types = response.keys - [:stat, :transaction]
203
+
204
+ if response.has_key?(:api_key) # echo call, we assume
205
+ response_type = :echo
206
+ data = response
207
+ elsif response_types.size > 1
208
+ error = RememberTheMilkAPIError.new({:code => "666", :msg=>"found more than one response type[#{response_types.join(',')}]"},method,args)
209
+ debug( "%s", error )
210
+ raise error
211
+ else
212
+ response_type = response_types[0] || :transaction
213
+ data = response[response_type]
214
+ end
215
+
216
+
217
+ case response_type
218
+ when :auth
219
+ when :frob
220
+ when :echo
221
+ when :transaction
222
+ when :timeline
223
+ when :methods
224
+ when :settings
225
+ when :contact
226
+ when :group
227
+ # no op
228
+
229
+ when :tasks
230
+ new_hash = RememberTheMilkHash.new
231
+ if data.class == Array # a bunch of lists
232
+ data.each do |list|
233
+ if list.class == String # empty list, just an id, so we create a stub
234
+ new_list = RememberTheMilkHash.new
235
+ new_list[:id] = list
236
+ list = new_list
237
+ end
238
+ new_hash[list[:id]] = process_task_list( list[:id], list.arrayify_value(:taskseries) )
239
+ end
240
+ data = new_hash
241
+ elsif data.class == RememberTheMilkHash # only one list
242
+ data = process_task_list( data[:id], data[:taskseries] )
243
+ elsif data.class == NilClass # empty list
244
+ data = new_hash
245
+ else # who knows...
246
+ debug( "got a class of (%s) when processing tasks. passing it on through", data.class )
247
+ end
248
+ when :groups
249
+ # contacts expected to be array, so look at each group and fix it's contact
250
+ data = [data] unless data.class == Array # won't be array if there's only one group. normalize here
251
+ data.each do |datum|
252
+ datum.arrayify_value( :contacts )
253
+ end
254
+ data = index_data_into_hash( data, :id )
255
+ when :time
256
+ data = time_to_user_tz( Time.parse(data[:text]) )
257
+ when :timezones
258
+ data = index_data_into_hash( data, :name )
259
+ when :lists
260
+ data = index_data_into_hash( data, :id )
261
+ when :contacts
262
+ data = [data].compact unless data.class == Array
263
+ when :list
264
+ # rtm.tasks.add returns one of these, which looks like this:
265
+ # <rsp stat='ok'><transaction id='978920558' undoable='0'/><list id='761280'><taskseries name='Try out Remember The Milk' modified='2006-12-19T22:07:50Z' url='' id='1939553' created='2006-12-19T22:07:50Z' source='api'><tags/><participants/><notes/><task added='2006-12-19T22:07:50Z' completed='' postponed='0' priority='N' id='2688677' has_due_time='0' deleted='' estimate='' due=''/></taskseries></list></rsp>
266
+ # rtm.lists.add also returns this, but it looks like this:
267
+ # <rsp stat='ok'><transaction id='978727001' undoable='0'/><list name='PersonalClone2' smart='0' id='761266' archived='0' deleted='0' position='0' locked='0'/></rsp>
268
+ # so we can look for a name attribute
269
+ if !data.has_key?(:name)
270
+ data = process_task_list( data[:id], data.arrayify_value(:taskseries) )
271
+ data = data.values[0] if data.values.size == 1
272
+ end
273
+ else
274
+ throw "Unsupported reply type<#{response_type}>#{response.inspect}"
275
+ end
276
+
277
+ if rtm_transaction
278
+ if !data.respond_to?(:keys)
279
+ new_hash = RememberTheMilkHash.new
280
+ new_hash[response_type] = data
281
+ data = new_hash
282
+ end
283
+
284
+ if data.keys.size == 0
285
+ data = rtm_transaction
286
+ else
287
+ data[:rtm_transaction] = rtm_transaction if rtm_transaction
288
+ end
289
+ end
290
+ return data
291
+ end
292
+
293
+
294
+ def process_task_list( list_id, list )
295
+ return {} unless list
296
+ tasks = RememberTheMilkHash.new
297
+
298
+ list.each do |taskseries_as_hash|
299
+ taskseries = RememberTheMilkTask.new.merge(taskseries_as_hash)
300
+
301
+ taskseries[:parent_list] = list_id # parent pointers are nice
302
+ taskseries[:tasks] = taskseries.arrayify_value(:task)
303
+ taskseries.arrayify_value(:tags)
304
+ taskseries.arrayify_value(:participants)
305
+
306
+ # TODO is there a ruby lib that speaks rrule?
307
+ taskseries[:recurrence] = nil
308
+ if taskseries[:rrule]
309
+ taskseries[:recurrence] = taskseries[:rrule]
310
+ taskseries[:recurrence][:rule] = taskseries[:rrule][:text]
311
+ end
312
+
313
+ taskseries[:completed] = nil
314
+ taskseries.tasks.each do |item|
315
+ if item.has_key?(:due) && item.due != ''
316
+ item.due = time_to_user_tz( Time.parse(item.due) )
317
+ end
318
+
319
+ if item.has_key?(:completed) && item.completed != '' && taskseries[:completed] == nil
320
+ taskseries[:completed] = true
321
+ else # once we set it to false, it can't get set to true
322
+ taskseries[:completed] = false
323
+ end
324
+ end
325
+
326
+ # TODO: support past tasks?
327
+ tasks[taskseries[:id]] = taskseries
328
+ end
329
+
330
+ return tasks
331
+ end
332
+
333
+ def call_api_method( method, args={} )
334
+
335
+ unless args.has_key?(:test_data)
336
+ args['method'] = "rtm.#{method}"
337
+ args['api_key'] = @api_key
338
+ args['auth_token'] ||= @auth_token if @auth_token
339
+
340
+ # make sure everything in our arguments is a string
341
+ args.each do |key,value|
342
+ key_s = key.to_s
343
+ args.delete(key) if key.class != String
344
+ args[key_s] = value.to_s
345
+ end
346
+
347
+ args['api_sig'] = sign_request(args)
348
+
349
+ debug( 'rtm.%s(%s)', method, args.inspect )
350
+ end
351
+
352
+ attempts_left = @max_connection_attempts
353
+
354
+ begin
355
+ if args.has_key?(:test_data)
356
+ @xml_parser.string = args[:test_data]
357
+ else
358
+ attempts_left -= 1
359
+ response = Net::HTTP.get_response(@uri.host, "#{@uri.path}?#{args.keys.collect {|k| "#{CGI::escape(k).gsub(/ /,'+')}=#{CGI::escape(args[k]).gsub(/ /,'+')}"}.join('&')}")
360
+ debug('RESPONSE code: %s\n%sEND RESPONSE\n', response.code, response.body)
361
+ @xml_parser.string = response.body
362
+ end
363
+
364
+ raw_data = @xml_parser.parse
365
+ data = xml_node_to_hash( raw_data.root )
366
+
367
+ if data[:stat] != 'ok'
368
+ debug( "processed into data<#{data.inspect}>")
369
+ error = RememberTheMilkAPIError.new(data[:err],method,args)
370
+ debug( "%s", error )
371
+ raise error
372
+ end
373
+ return return_raw_response ? @xml_parser.string : parse_response(data,method,args)
374
+ rescue XML::Parser::ParseError => err
375
+ debug("Unable to parse document.\nGot response:%s\nGot Error:\n", response.body, err.to_s)
376
+ raise err
377
+ rescue Timeout::Error => timeout
378
+ $stderr.puts "Timed out to<#{endpoint}>, trying #{attempts_left} more times"
379
+ if attempts_left > 0
380
+ retry
381
+ else
382
+ raise timeout
383
+ end
384
+ end
385
+ end
386
+
387
+ def sign_request( args )
388
+ return MD5.md5(@shared_secret + args.sort.flatten.join).to_s
389
+ end
390
+ end
391
+
392
+
393
+ ## a pretty crappy exception class, but it should be sufficient for bubbling
394
+ ## up errors returned by the RTM API (website)
395
+ class RememberTheMilkAPIError < RuntimeError
396
+ attr_reader :response, :error_code, :error_message
397
+
398
+ def initialize(error, method, args_to_method)
399
+ @method_name = method
400
+ @args_to_method = args_to_method
401
+ @error_code = error[:code].to_i
402
+ @error_message = error[:msg]
403
+ end
404
+
405
+ def to_s
406
+ "Calling rtm.#{@method_name}(#{@args_to_method.inspect}) produced => <#{@error_code}>: #{@error_message}"
407
+ end
408
+ end
409
+
410
+
411
+ ## this is just a helper class so that you can do things like
412
+ ## rtm.test.echo. the method_missing in RememberTheMilkAPI returns one of
413
+ ## these.
414
+ ## this class is the "test" portion of the programming. its method_missing then
415
+ ## get invoked with "echo" as the symbol. it has stored a reference to the original
416
+ ## rtm object, so it can then invoke call_api_method
417
+ class RememberTheMilkAPINamespace
418
+ def initialize(namespace, rtm)
419
+ @namespace = namespace
420
+ @rtm = rtm
421
+ end
422
+
423
+ def method_missing( symbol, *args )
424
+ method_name = symbol.id2name
425
+ @rtm.call_api_method( "#{@namespace}.#{method_name}", *args)
426
+ end
427
+ end
428
+
429
+ ## a standard hash with some helper methods
430
+ class RememberTheMilkHash < Hash
431
+ attr_accessor :rtm
432
+
433
+ @@strict_keys = true
434
+ def self.strict_keys=( value )
435
+ @@strict_keys = value
436
+ end
437
+
438
+ def initialize(rtm_object = nil)
439
+ super
440
+ @rtm = rtm_object
441
+ end
442
+
443
+ def id
444
+ rtm_id || object_id
445
+ end
446
+
447
+ def rtm_id
448
+ self[:id]
449
+ end
450
+
451
+ # guarantees that a given key corresponds to an array, even if it's an empty array
452
+ def arrayify_value( key )
453
+ if !self.has_key?(key)
454
+ self[key] = []
455
+ elsif self[key].class != Array
456
+ self[key] = [ self[key] ].compact
457
+ else
458
+ self[key]
459
+ end
460
+ end
461
+
462
+
463
+ def method_missing( key, *args )
464
+ name = key.to_s
465
+
466
+ setter = false
467
+ if name[-1,1] == '='
468
+ name = name.chop
469
+ setter = true
470
+ end
471
+
472
+ if name == ""
473
+ name = "rtm_nil".to_sym
474
+ else
475
+ name = name.to_sym
476
+ end
477
+
478
+
479
+ # TODO: should we allow the blind setting of values? (i.e., only do this test
480
+ # if setter==false )
481
+ raise "unknown hash key<#{name}> requested for #{self.inspect}" if @@strict_keys && !self.has_key?(name)
482
+
483
+ if setter
484
+ self[name] = *args
485
+ else
486
+ self[name]
487
+ end
488
+ end
489
+ end
490
+
491
+
492
+ ## DateSet is to manage rrules
493
+ ## this comes from the iCal ruby module as mentioned here:
494
+ ## http://www.macdevcenter.com/pub/a/mac/2003/09/03/rubycocoa.html
495
+
496
+ class DateSet
497
+
498
+ def initialize(startDate, rule)
499
+ @startDate = startDate
500
+ @frequency = nil
501
+ @count = nil
502
+ @untilDate = nil
503
+ @byMonth = nil
504
+ @byDay = nil
505
+ @starts = nil
506
+ if not rule.nil? then
507
+ @starts = rule.every == 1 ? 'every' : 'after'
508
+ parseRecurrenceRule(rule.rule)
509
+ end
510
+ end
511
+
512
+ def parseRecurrenceRule(rule)
513
+
514
+ if rule =~ /FREQ=(.*?);/ then
515
+ @frequency = $1
516
+ end
517
+
518
+ if rule =~ /COUNT=(\d*)/ then
519
+ @count = $1.to_i
520
+ end
521
+
522
+ if rule =~ /UNTIL=(.*?)[;\r]/ then
523
+ @untilDate = DateParser.parse($1)
524
+ end
525
+
526
+ if rule =~ /INTERVAL=(\d*)/ then
527
+ @interval = $1.to_i
528
+ end
529
+
530
+ if rule =~ /BYMONTH=(.*?);/ then
531
+ @byMonth = $1
532
+ end
533
+
534
+ if rule =~ /BYDAY=(.*?);/ then
535
+ @byDay = $1
536
+ #puts "byDay = #{@byDay}"
537
+ end
538
+ end
539
+
540
+ def to_s
541
+ # after/every FREQ
542
+ puts "UNIMPLETEMENT"
543
+ # puts "#<DateSet: starts: #{@startDate.strftime("%m/%d/%Y")}, occurs: #{@frequency}, count: #{@count}, until: #{@untilDate}, byMonth: #{@byMonth}, byDay: #{@byDay}>"
544
+ end
545
+
546
+ def includes?(date)
547
+ return true if date == @startDate
548
+ return false if @untilDate and date > @untilDate
549
+
550
+ case @frequency
551
+ when 'DAILY'
552
+ #if @untilDate then
553
+ # return (@startDate..@untilDate).include?(date)
554
+ #end
555
+ increment = @interval ? @interval : 1
556
+ d = @startDate
557
+ counter = 0
558
+ until d > date
559
+
560
+ if @count then
561
+ counter += 1
562
+ if counter >= @count
563
+ return false
564
+ end
565
+ end
566
+
567
+ d += (increment * SECONDS_PER_DAY)
568
+ if d.day == date.day and
569
+ d.year == date.year and
570
+ d.month == date.month then
571
+ puts "true for start: #{@startDate}, until: #{@untilDate}"
572
+ return true
573
+ end
574
+
575
+ end
576
+
577
+ when 'WEEKLY'
578
+ return true if @startDate.wday == date.wday
579
+
580
+ when 'MONTHLY'
581
+
582
+ when 'YEARLY'
583
+
584
+ end
585
+
586
+ false
587
+ end
588
+
589
+ attr_reader :frequency
590
+ attr_accessor :startDate
591
+ end
592
+
593
+
594
+ class RememberTheMilkTask < RememberTheMilkHash
595
+ # The API is aware it's creating tasks. You may want to add semantics to a "task"
596
+ # elsewhere in your program. This gives you that flexibility
597
+ end