fmylife 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,9 @@
1
+ === 0.5.0 / 2009-03-25
2
+
3
+ * Initial release
4
+
5
+ * Full access to the API
6
+ * Read all categories, "top" FMLs, "flop" FMLs, paginated results
7
+ * Vote on FMLs, moderate pending FMLs
8
+ * Comment submission and FML submissions aren't working, but this is a result of the API itself being busted
9
+ * Swap out XML parsers. Choose from REXML, Hpricot, Nokogiri, or libxml-ruby
@@ -0,0 +1,7 @@
1
+ History.txt
2
+ Manifest.txt
3
+ README.txt
4
+ Rakefile
5
+ lib/fmylife.rb
6
+ lib/xmlstruct.rb
7
+ test/test_fmylife.rb
@@ -0,0 +1,81 @@
1
+ = FMyLife
2
+
3
+ * http://beforefilter.blogspot.com/
4
+
5
+ == DESCRIPTION:
6
+
7
+ This gem allows the user to access the fmylife.com API, which includes
8
+ reading stories, reading comments, moderating stories that are submitted,
9
+ submitting stories, submitting comments, and searching for stories.
10
+
11
+ In addition, this gem lets you swap in and out which XML parser you use.
12
+ Since not everyone can take advantage of compiled xml parsers,
13
+ the typically built-in REXML library is available as well.
14
+
15
+ == FEATURES/PROBLEMS:
16
+
17
+ * Full access to the fmylife.com API, including reading and submitting
18
+ stories and submissions
19
+ * Swap out which XML parser you want to use:
20
+ * libxml-ruby
21
+ * hpricot
22
+ * nokogiri
23
+ * REXML
24
+
25
+ == SYNOPSIS:
26
+
27
+ The following code will connect with your API key, authenticate as a user,
28
+ grab a page of the latest stories and then download all the comments for the
29
+ very latest story. Then, it will print the names of each comment's author.
30
+
31
+ acc = FMyLife::Account.new("yourapikey")
32
+ acc.authenticate(username,password)
33
+ latest = acc.latest.first # get the latest story
34
+ acc.story(latest) # pull comments
35
+ acc.comments.each do |comm|
36
+ puts comm.author
37
+ end
38
+
39
+ More interestingly, we should be able to submit stories!
40
+
41
+ story = FMyLife::Story.new(:text => "my life sucks. FML",
42
+ :author => "Anonymous",
43
+ :category => :love)
44
+
45
+ See the individual docs for more examples.
46
+
47
+ == REQUIREMENTS:
48
+
49
+ * none, though one of the following is recommended:
50
+ * hpricot
51
+ * nokogiri
52
+ * libxml-ruby
53
+
54
+ == INSTALL:
55
+
56
+ * sudo gem install fmylife
57
+
58
+ == LICENSE:
59
+
60
+ (The MIT License)
61
+
62
+ Copyright (c) 2009 Michael J. Edgar
63
+
64
+ Permission is hereby granted, free of charge, to any person obtaining
65
+ a copy of this software and associated documentation files (the
66
+ 'Software'), to deal in the Software without restriction, including
67
+ without limitation the rights to use, copy, modify, merge, publish,
68
+ distribute, sublicense, and/or sell copies of the Software, and to
69
+ permit persons to whom the Software is furnished to do so, subject to
70
+ the following conditions:
71
+
72
+ The above copyright notice and this permission notice shall be
73
+ included in all copies or substantial portions of the Software.
74
+
75
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
76
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
77
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
78
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
79
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
80
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
81
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,12 @@
1
+ # -*- ruby -*-
2
+
3
+ require 'rubygems'
4
+ require 'hoe'
5
+ require './lib/fmylife.rb'
6
+
7
+ Hoe.new('fmylife', FMyLife::VERSION) do |p|
8
+ p.rubyforge_name = 'fmylife' # if different than lowercase project name
9
+ p.developer('Michael J. Edgar', 'edgar@triqweb.com')
10
+ end
11
+
12
+ # vim: syntax=Ruby
@@ -0,0 +1,473 @@
1
+ require 'net/http'
2
+ require 'uri'
3
+ require 'digest'
4
+ require 'time'
5
+ require File.dirname(__FILE__)+'/xmlstruct.rb'
6
+ # = FMyLife
7
+ #
8
+ # This gem allows the user to access the fmylife.com API, which includes
9
+ # reading stories, reading comments, moderating stories that are submitted,
10
+ # submitting stories, submitting comments, and searching for stories.
11
+ #
12
+ # In addition, this gem lets you swap in and out which XML parser you use.
13
+ # Since not everyone can take advantage of compiled xml parsers,
14
+ # the typically built-in REXML library is available.
15
+ #
16
+ # The following code will connect with your API key, authenticate as a user,
17
+ # grab a page of the latest stories and then download all the comments for the
18
+ # very latest story. Then, it will print the names of each comment's author.
19
+ #
20
+ # acc = FMyLife::Account.new("yourapikey")
21
+ # acc.authenticate(username,password)
22
+ # latest = acc.latest.first # get the latest story
23
+ # acc.story(latest) # pull comments
24
+ # acc.comments.each do |comm|
25
+ # puts comm.author
26
+ # end
27
+ #
28
+ # More interestingly, we should be able to submit stories!
29
+ #
30
+ # story = FMyLife::Story.new(:text => "my life sucks. FML",
31
+ # :author => "Anonymous",
32
+ # :category => :love)
33
+ #
34
+ # Change parsers as such (you _*MUST*_ pick one before you start accessing the API!)
35
+ #
36
+ # FMyLife.parser = :nokogiri
37
+ # FMyLife.parser = :hpricot
38
+ # FMyLife.parser = :rexml
39
+ # FMyLife.parser = :libxml
40
+ #
41
+ # See the individual docs for more examples.
42
+ #
43
+ module FMyLife
44
+ VERSION = '0.5.0'
45
+
46
+ API_ROOT = 'api.betacie.com'
47
+ SANDBOX_ROOT = 'sandbox.betacie.com'
48
+
49
+ # Categories you are allowed to search and submit with
50
+ AVAILABLE_CATEGORIES = [ :love , :money , :kids , :work , :health , :sex , :miscellaneous ]
51
+
52
+ class AuthenticationError < StandardError; end
53
+ class RetrievalError < StandardError; end
54
+ class VotingError < StandardError; end
55
+ class InvalidCategoryError < ArgumentError; end
56
+ class VotingTypeError < ArgumentError; end
57
+ class StoryTooLongError < ArgumentError; end
58
+ class UnsupportedParserError < ArgumentError; end
59
+
60
+ AVAILABLE_PARSERS = [:nokogiri, :hpricot, :rexml, :libxml]
61
+ def self.parser=(val)
62
+ raise UnsupportedParserError.new("An unsupported parser was provided: #{val}") unless AVAILABLE_PARSERS.include? val
63
+ @@parser = val
64
+ case @@parser
65
+ when :nokogiri
66
+ require 'nokogiri'
67
+ when :hpricot
68
+ require 'hpricot'
69
+ when :rexml
70
+ require 'rexml/document'
71
+ when :libxml
72
+ require 'libxml'
73
+ end
74
+ end
75
+ def self.parser
76
+ @@parser
77
+ end
78
+
79
+
80
+ # = Account
81
+ # An account/connection to FMyLife.com. This class provides access to all of the
82
+ # features of the FML api.
83
+ #
84
+ class Account
85
+ include CanParse
86
+
87
+ attr_accessor :auth_token
88
+ attr_accessor :api_key
89
+ attr_accessor :sandbox_mode
90
+
91
+ # Creates an FMyLife with the given developer key. The language can also be 'fr'
92
+ # for French. Sandbox mode defaults to being off.
93
+ def initialize(key = 'readonly', language='en')
94
+ @api_key, @language = key, language
95
+ @sandbox_mode = false
96
+ end
97
+
98
+ # Logs the connection in with the provided username and password
99
+ def authenticate(username, password)
100
+ path = "/account/login/#{username}/#{Digest::MD5.hexdigest(password)}"
101
+ doc = http_post(path, '')
102
+
103
+ case code(doc)
104
+ when 0
105
+ raise FMyLife::AuthenticationError.new("Error logging into FMyLife: #{error(doc)}")
106
+ when 1
107
+ @auth_token = xml_content xpath(doc,'//token').first
108
+ end
109
+ self
110
+ end
111
+
112
+ # Logs the user out of fmylife.com.
113
+ def logout
114
+ raise FMyLife::AuthenticationError.new("You aren't logged in.") if @auth_token.nil?
115
+ path = "/account/logout/#{@auth_token}"
116
+ doc = http_get(path)
117
+ case code(doc)
118
+ when 0
119
+ raise FMyLife::AuthenticationError.new("Error logging out: #{error(doc)}")
120
+ when 1
121
+ @auth_token = nil
122
+ end
123
+ end
124
+
125
+ # Returns the most recent stories, paginated (starting at 0). Maximum return of 15 stories.
126
+ def latest(page=0)
127
+ path = "/view/last/#{page}"
128
+ retrieve_stories(path)
129
+ end
130
+
131
+ # Returns a page of the top stories. interval can be "day", "week", or "month", to change
132
+ # how far back to look. Paginated to 15 entries, and +page+ starts at 0.
133
+ def top(interval = nil, page=0)
134
+ param = case interval
135
+ when nil
136
+ "top"
137
+ when :day
138
+ "top_day"
139
+ when :week
140
+ "top_week"
141
+ when :month
142
+ "top_month"
143
+ else
144
+ raise ArgumentError.new("Invalid top-fml interval: #{interval}")
145
+ end
146
+ path = "/view/#{param}/#{page}"
147
+ retrieve_stories(path)
148
+ end
149
+
150
+ # Returns a page of the "flop" (lowest rated) stories. interval can be "day", "week", or "month",
151
+ # to change how far back to look. Paginated to 15 entries, and +page+ starts at 0.
152
+ def flop(interval = nil, page=0)
153
+ param = case interval.to_sym
154
+ when nil
155
+ "flop"
156
+ when :day
157
+ "flop_day"
158
+ when :week
159
+ "flop_week"
160
+ when :month
161
+ "flop_month"
162
+ else
163
+ raise ArgumentError.new("Invalid flop-fml interval: #{interval}")
164
+ end
165
+ path = "/view/#{param}/#{page}"
166
+ retrieve_stories(path)
167
+ end
168
+
169
+ # Returns a page of stories with the provided category. You may abbreviate "miscellaneous" as "misc".
170
+ def category(category, page=0)
171
+ category = :miscellaneous if category.to_s =~ /misc/ # poor spellers unite!
172
+ unless FMyLife::AVAILABLE_CATEGORIES.include?(category.to_sym)
173
+ raise InvalidCategoryError.new("You provided an invalid category: #{category}")
174
+ end
175
+ path = "/view/#{category}/#{page}"
176
+ retrieve_stories(path)
177
+ end
178
+
179
+ # Gets a random FML story, with or without comments.
180
+ def random(get_comments = true)
181
+ path = "/view/random"
182
+ retrieve_story(path,get_comments)
183
+ end
184
+
185
+ # Gets a specific +story+, with or without comments. The +_story_+ parameter may be either a
186
+ # FMyLife::Story object, or an ID number.
187
+ def story(_story, get_comments = true)
188
+ number = (_story.is_a? FMyLife::Story) ? _story.id : _story
189
+ path = "/view/#{number}"
190
+ path = path + "/nocomment" unless get_comments
191
+ retrieve_story(path,get_comments)
192
+ end
193
+
194
+ # Gets the stories that the currently logged-in user has not seen.
195
+ def new
196
+ path = "/view/new"
197
+ retrieve_stories(path,true)
198
+ end
199
+
200
+ # Gets the currently logged-in user's favorite stories.
201
+ def favorites
202
+ path = "/view/favorites"
203
+ retrieve_stories(path,true)
204
+ end
205
+
206
+ def search(term)
207
+ path = "/view/search/"
208
+ retrieve_stories(path,false,{:search => term})
209
+ end
210
+
211
+ # Helper method for retrieving a single story with comments. Internal use only.
212
+ def retrieve_story(path, get_comments = true)
213
+ doc = http_get(path)
214
+
215
+ case code(doc)
216
+ when 0
217
+ raise FMyLife::RetrievalError.new("Error retrieving story ##{number}: #{error(doc)}")
218
+ when 1
219
+ story = FMyLife::Story.new(:xml => xpath(doc,'//items/item').first)
220
+ if get_comments
221
+ comments = []
222
+ xpath(doc,'//comments/comment').each do |entry|
223
+ comments << FMyLife::Comment.new(:xml => entry)
224
+ end
225
+ story.comments = comments
226
+ end
227
+ story
228
+ end
229
+ end
230
+
231
+ # Submits a Story to fmylife.com. You must be logged in (with +authenticate+). The +story+ parameter
232
+ # can be either an FMyLife::Story, or a hash with the following values:
233
+ #
234
+ # [:author] The name of the story's author
235
+ # [:text] The text of the story. Maximum 300 characters.
236
+ # [:cat] The category to use. Must be one of AVAILABLE_CATEGORIES.
237
+ #
238
+ def submit(story)
239
+ raise FMyLife::AuthenticationError.new("You aren't logged in.") if @auth_token.nil?
240
+ params = (story.is_a? FMyLife::Story) ? {:author => story.author, :text => story.text, :cat => story.category} : story
241
+ params[:cat] = params.delete :category if params[:category]
242
+ raise FMyLife::StoryTooLongError.new("Stories can be no longer than 300 characters.") if params[:text].size > 300
243
+ params[:token] = @auth_token
244
+ path = "/submit"
245
+ doc = http_get(path, params)
246
+ case code(doc)
247
+ when 0
248
+ raise VotingError.new("Error while submitting story with text \"#{params[:text]}\": #{error(doc)}")
249
+ when 1
250
+ true
251
+ end
252
+ end
253
+
254
+ # Votes for a story. +type+ must be :agree or :deserved . Story can be either a FMyLife::Story or an ID number.
255
+ def vote(story, type)
256
+ raise VotingTypeError.new("Your vote must be either 'agree' or 'deserved'.") unless [:agree, :deserved].include? type.to_sym
257
+ id = (story.is_a? FMyLife::Story) ? story.id : story
258
+ path = "/vote/#{id}/#{type}"
259
+ doc = http_get(path)
260
+ case code(doc)
261
+ when 0
262
+ raise VotingError.new("Error while voting on story ##{story.id}: #{error(doc)}")
263
+ when 1
264
+ true
265
+ end
266
+ end
267
+
268
+ # Submits a Comment to fmylife.com. You must be logged in (with +authenticate+). The +story+ parameter
269
+ # can be either an FMyLife::Story, or an ID number. +arg+ can be an FMyLife::Comment or a hash with
270
+ # the following values:
271
+ #
272
+ # [:text] The text of the story. Maximum 300 characters.
273
+ # [:url] The url for the currently logged-in user.
274
+ #
275
+ def comment(story, arg)
276
+ raise FMyLife::AuthenticationError.new("You aren't logged in.") if @auth_token.nil?
277
+ params = (arg.is_a? FMyLife::Comment) ? {:text => arg.text, :url => arg.author_url} : arg
278
+ params[:id] = (story.is_a? FMyLife::Story) ? story.id : story
279
+ params[:token] = @auth_token
280
+ path = "/comment"
281
+ doc = http_get(path, params)
282
+ case code(doc)
283
+ when 0
284
+ raise VotingError.new("Error while voting on story ##{params[:id]}: #{error(doc)}")
285
+ when 1
286
+ true
287
+ end
288
+ end
289
+
290
+ # Returns an FMyLife::Developer object with information on the current developer (determined
291
+ # by the id provided when the FMyLife was instantiated)
292
+ def developer_info
293
+ path = "/dev"
294
+ doc = http_get(path)
295
+ case code(doc)
296
+ when 0
297
+ raise RetrievalError.new("Error retrieving developer info: #{error(doc)}")
298
+ when 1
299
+ Developer.new(:xml => doc)
300
+ end
301
+ end
302
+
303
+ # Returns all unmoderated stories.
304
+ def all_unmoderated
305
+ path = "/mod/view"
306
+ doc = http_get(path)
307
+ results = []
308
+ xpath(doc,"//items/item").each do |item|
309
+ results << xml_content(item).to_i
310
+ end
311
+ results
312
+ end
313
+
314
+ # Returns a single unmoderated story, based on its ID number.
315
+ def unmoderated(id)
316
+ path = "/mod/view/#{id}"
317
+ retrieve_stories(path, true)
318
+ end
319
+
320
+ # Returns the last story to be moderated.
321
+ def last_moderated
322
+ path = "/mod/last"
323
+ retrieve_stories(path, true)
324
+ end
325
+
326
+ # Moderates a given story with a "yes" or "no" answer. ID can be either an FMyLife::Story object
327
+ # or an ID number. +type+ should be :yes or :no .
328
+ def moderate(story, type)
329
+ raise VotingTypeError.new("Your vote must be either 'yes' or 'no'.") unless [:yes, :no].include? type.to_sym
330
+ id = (story.is_a? Story) ? story.id : story
331
+ path = "/mod/#{type}/#{id}"
332
+ doc = http_get(path, :token => @auth_token)
333
+ case code(doc)
334
+ when 0
335
+ raise VotingError.new("Error while moderating story ##{story.id}: #{error(doc)}")
336
+ when 1
337
+ true
338
+ end
339
+ end
340
+
341
+ # Helper method for retreiving multiple stories, without comments. Internal Use Only.
342
+ def retrieve_stories(path, pass_token = false, params = {})
343
+ params.merge!({:token => @auth_token}) if pass_token
344
+ doc = http_get(path, params)
345
+ case code(doc)
346
+ when 0
347
+ raise FMyLife::RetrievalError.new("Error retrieving stories: #{error(doc)}")
348
+ when 1
349
+ stories = []
350
+ xpath(doc,'//items/item').each do |entry|
351
+ story = FMyLife::Story.new(:xml => entry)
352
+ stories << story
353
+ end
354
+ stories
355
+ end
356
+ end
357
+
358
+ # Helper method for retrieving URLs via GET while escaping parameters and including API-specific
359
+ # parameters
360
+ def http_get(path, query_params = {})
361
+ query_params.merge!(:key => @api_key, :language => @language)
362
+ http = Net::HTTP.new((@sandbox_mode) ? SANDBOX_ROOT : API_ROOT)
363
+ path = path + "?" + URI.escape(query_params.map {|k,v| "#{k}=#{v}"}.join("&"), /[^-_!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n)
364
+ puts path
365
+ resp = http.get(path)
366
+ xml_doc(resp.body)
367
+ end
368
+
369
+ # Helper method for retrieving URLs via POST while escaping parameters and including API-specific
370
+ # parameters
371
+ def http_post(path, data, query_params = {})
372
+ query_params.merge!(:key => @api_key, :language => @language)
373
+ http = Net::HTTP.new((@sandbox_mode) ? SANDBOX_ROOT : API_ROOT)
374
+ path = path + "?" + URI.escape(query_params.map {|k,v| "#{k}=#{v}"}.join("&"), /[^-_!~*'()a-zA-Z\d;\/?:@&=+$,\[\]]/n)
375
+ resp = http.post(path, data)
376
+ xml_doc(resp.body)
377
+ end
378
+
379
+
380
+
381
+ # Helper method that finds the error code from an XML document from fmylife
382
+ def code(doc)
383
+ xml_content(xpath(doc,'//code').first).to_i
384
+ end
385
+
386
+ # Helper method that finds the error string from an XML document from fmylife
387
+ def error(doc)
388
+ puts doc.inspect
389
+ begin
390
+ xml_content xpath(doc,'//error').first
391
+ rescue
392
+ xml_content xpath(doc,'//errors').first
393
+ end
394
+ end
395
+ end
396
+
397
+ # = Story
398
+ # Encapsulates a story on fmylife.com. These are primary used for reading stories
399
+ # off the site, but it can be used to submit stories as well. An example:
400
+ #
401
+ # fml = FMyLife.new("apikeyhere")
402
+ # story = FMyLife::Story.new(:author => "Anonymous",
403
+ # :category => :love,
404
+ # :text => "blah blah blah. FML")
405
+ # fml.submit(story)
406
+ #
407
+ # To read one, simple use it's variables:
408
+ # story = fml.latest.first
409
+ # author = story.author
410
+ # text = story.text
411
+ #
412
+ class Story < XMLStruct
413
+ attr_accessor :comments
414
+
415
+ field :id, :attribute, "id", "."
416
+ field :author, :node, "author"
417
+ field :category, :node, "category"
418
+ field :date, :proc, lambda { |entry| Time.xmlschema(xml_content(xpath(entry,"date").first)) }
419
+ field :agreed, :node, "agree"
420
+ field :deserved, :node, "deserved"
421
+ field :num_comments, :node, "comments"
422
+ field :text, :node, "text"
423
+ field :comments_flag, :node, "comments_flag"
424
+
425
+ # Initialize a new story. Available options are:
426
+ #
427
+ # [:author] The name of the author
428
+ # [:category] The category (must be from FMyLife::AVAILABLE_CATEGORIES)
429
+ # [:text] The text of the submissions
430
+ def initialize(opts = {}); super(opts); end
431
+ end
432
+
433
+ # = Comment
434
+ # Encapsulates a comment on fmylife.com. Belongs to a story. This class
435
+ # can be used for submitting a comment, or for reading them. To submit one,
436
+ # initialize them as follows:
437
+ #
438
+ # comment = FMyLife::Comment.new(:text => "good story")
439
+ # fml.comment(story, comment)
440
+ #
441
+ class Comment < XMLStruct
442
+ field :id, :attribute, "id", "."
443
+ field :order, :attribute, "pub_id", "."
444
+ field :staff, :proc, Proc.new {|entry| (xml_attribute(entry,"staff").to_i == 1)}
445
+ field :author, :node, "author"
446
+ field :author_url, :attribute, "url", "author"
447
+ field :date, :proc, lambda {|entry| Time.xmlschema(xml_content(xpath(entry,"date").first))}
448
+ field :text, :node, "text"
449
+
450
+ # Initialize a new comment. Available options are:
451
+ #
452
+ # [:text] The text of the submission
453
+ #
454
+ # All others will have no effect upon submitting the comment.
455
+ def initialize(opts={}); super(opts); end
456
+ end
457
+
458
+ # = Developer
459
+ #
460
+ # This class holds the developer's information that one can request using the
461
+ # FMyLife::Account#developer_info method. All information is read-only.
462
+ #
463
+ class Developer < XMLStruct
464
+ field :name, :node, "//infos/name"
465
+ field :project, :node, "//infos/project"
466
+ field :description, :node, "//infos/description"
467
+ field :url, :node, "//infos/url"
468
+ field :email, :node, "//infos/mail"
469
+ field :last24h, :node, "//actions/last24h"
470
+ field :alltime, :node, "//actions/alltime"
471
+ field :tokens, :node, "//tokens"
472
+ end
473
+ end
@@ -0,0 +1,111 @@
1
+ # Methods for parser-agnostic parsing
2
+ module CanParse
3
+ # Helper method that encapsulates a string into an XML document
4
+ def xml_doc(body)
5
+ case FMyLife.parser
6
+ when :nokogiri
7
+ Nokogiri::XML(body)
8
+ when :hpricot
9
+ Hpricot(body)
10
+ when :rexml
11
+ REXML::Document.new(body)
12
+ when :libxml
13
+ LibXML::XML::Parser.string(body).parse
14
+ end
15
+ end
16
+
17
+ def xpath(element,path)
18
+ case FMyLife.parser
19
+ when :nokogiri
20
+ element.xpath(path)
21
+ when :hpricot
22
+ puts "in hpricot"
23
+ element/path #force the //
24
+ when :rexml
25
+ REXML::XPath.match(element,path)
26
+ when :libxml
27
+ element.find(path)
28
+ end
29
+ end
30
+
31
+ def xml_content(element)
32
+ case FMyLife.parser
33
+ when :nokogiri
34
+ element.content
35
+ when :hpricot
36
+ element.inner_text
37
+ when :rexml
38
+ element.text
39
+ when :libxml
40
+ element.content
41
+ end
42
+ end
43
+
44
+ def xml_attribute(element,attribute)
45
+ case FMyLife.parser
46
+ when :nokogiri
47
+ element[attribute]
48
+ when :hpricot
49
+ element.get_attribute(attribute)
50
+ when :rexml
51
+ element.attributes[attribute]
52
+ when :nokogiri
53
+ element.attributes[attribute]
54
+ end
55
+ end
56
+ end
57
+
58
+ # Generic XML Structure
59
+ class XMLStruct
60
+
61
+ include CanParse
62
+ extend CanParse
63
+
64
+ class << self
65
+
66
+ attr_accessor :fields
67
+
68
+ def field(name, type, getter, path="")
69
+ @fields ||= {}
70
+ if type == :attribute
71
+ @fields[name.to_sym] = {:type => type, :getter => getter, :path => path}
72
+ else
73
+ @fields[name.to_sym] = {:type => type, :getter => getter}
74
+ end
75
+ attr_accessor name
76
+
77
+ end
78
+ end
79
+
80
+ def post_initialize; end
81
+
82
+ def initialize(opts = {})
83
+ if opts[:xml]
84
+ parse_xml opts[:xml]
85
+ else
86
+ opts.each do |key, value|
87
+ instance_variable_set("@#{key}".to_sym, value)
88
+ end
89
+ end
90
+ post_initialize
91
+ self
92
+ end
93
+
94
+ def parse_xml(entry)
95
+ myfields = self.class.fields
96
+ myfields.each do |k,v|
97
+ if v[:getter].is_a?(String) || v[:getter].is_a?(Symbol)
98
+ if v[:type] == :attribute
99
+ instance_variable_set("@#{k}".to_sym, xml_attribute(xpath(entry,v[:path]).first, v[:getter].to_s))
100
+ elsif v[:type] == :node
101
+ node = xpath(entry, v[:getter].to_s).first
102
+ if node
103
+ instance_variable_set "@#{k}".to_sym, xml_content(node)
104
+ end
105
+ end
106
+ elsif v[:getter].is_a?(Proc)
107
+ instance_variable_set "@#{k}".to_sym, v[:getter].call(entry)
108
+ end
109
+ end
110
+ end
111
+ end
@@ -0,0 +1,8 @@
1
+ require "test/unit"
2
+ require "fmylife"
3
+
4
+ class TestFMyLife < Test::Unit::TestCase
5
+ def test_sanity
6
+ flunk "write tests or I will kneecap you"
7
+ end
8
+ end
metadata ADDED
@@ -0,0 +1,72 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fmylife
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.5.0
5
+ platform: ruby
6
+ authors:
7
+ - Michael J. Edgar
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-03-27 00:00:00 -04:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: hoe
17
+ type: :development
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: 1.11.0
24
+ version:
25
+ description: This gem allows the user to access the fmylife.com API, which includes reading stories, reading comments, moderating stories that are submitted, submitting stories, submitting comments, and searching for stories. In addition, this gem lets you swap in and out which XML parser you use. Since not everyone can take advantage of compiled xml parsers, the typically built-in REXML library is available as well.
26
+ email:
27
+ - edgar@triqweb.com
28
+ executables: []
29
+
30
+ extensions: []
31
+
32
+ extra_rdoc_files:
33
+ - History.txt
34
+ - Manifest.txt
35
+ - README.txt
36
+ files:
37
+ - History.txt
38
+ - Manifest.txt
39
+ - README.txt
40
+ - Rakefile
41
+ - lib/fmylife.rb
42
+ - lib/xmlstruct.rb
43
+ - test/test_fmylife.rb
44
+ has_rdoc: true
45
+ homepage: http://beforefilter.blogspot.com/
46
+ post_install_message:
47
+ rdoc_options:
48
+ - --main
49
+ - README.txt
50
+ require_paths:
51
+ - lib
52
+ required_ruby_version: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: "0"
57
+ version:
58
+ required_rubygems_version: !ruby/object:Gem::Requirement
59
+ requirements:
60
+ - - ">="
61
+ - !ruby/object:Gem::Version
62
+ version: "0"
63
+ version:
64
+ requirements: []
65
+
66
+ rubyforge_project: fmylife
67
+ rubygems_version: 1.3.1
68
+ signing_key:
69
+ specification_version: 2
70
+ summary: This gem allows the user to access the fmylife.com API, which includes reading stories, reading comments, moderating stories that are submitted, submitting stories, submitting comments, and searching for stories
71
+ test_files:
72
+ - test/test_fmylife.rb