rtmapi 0.5

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