From 837e6b2a7a31d722bd73dd0c9033f647d4f3641e Mon Sep 17 00:00:00 2001 From: Susan Potter Date: Sat, 7 Aug 2010 12:41:48 -0500 Subject: [PATCH] Added v 0.2.3 snapshot. --- CHANGES | 102 ++++++ MIT-LICENSE | 20 ++ README | 29 ++ Rakefile | 24 ++ config/rcov_morpher.rb | 58 ++++ config/rdoc_template.rb | 572 ++++++++++++++++++++++++++++++++ config/templates/rcov.rhtml | 585 +++++++++++++++++++++++++++++++++ config/tidy.yml.conf | 2 + config/twitter.yml.conf | 4 + examples/configure.rb | 36 ++ examples/friendship.rb | 32 ++ examples/messaging.rb | 38 +++ examples/model.rb | 60 ++++ examples/status.rb | 24 ++ examples/timeline.rb | 66 ++++ examples/user.rb | 34 ++ lib/twitter.rb | 25 ++ lib/twitter/client.rb | 18 + lib/twitter/client/base.rb | 81 +++++ lib/twitter/client/friendship.rb | 35 ++ lib/twitter/client/messaging.rb | 79 +++++ lib/twitter/client/status.rb | 45 +++ lib/twitter/client/timeline.rb | 71 ++++ lib/twitter/client/user.rb | 54 +++ lib/twitter/config.rb | 71 ++++ lib/twitter/console.rb | 28 ++ lib/twitter/core.rb | 136 ++++++++ lib/twitter/ext.rb | 2 + lib/twitter/ext/stdlib.rb | 32 ++ lib/twitter/extras.rb | 39 +++ lib/twitter/meta.rb | 56 ++++ lib/twitter/model.rb | 331 +++++++++++++++++++ lib/twitter/version.rb | 19 ++ pkg-info.yml | 25 ++ spec/spec_helper.rb | 133 ++++++++ spec/twitter/client/base_spec.rb | 232 +++++++++++++ spec/twitter/client/friendship_spec.rb | 76 +++++ spec/twitter/client/messaging_spec.rb | 118 +++++++ spec/twitter/client/status_spec.rb | 92 ++++++ spec/twitter/client/timeline_spec.rb | 79 +++++ spec/twitter/client/user_spec.rb | 220 +++++++++++++ spec/twitter/client_spec.rb | 2 + spec/twitter/config_spec.rb | 86 +++++ spec/twitter/console_spec.rb | 15 + spec/twitter/core_spec.rb | 127 +++++++ spec/twitter/ext/stdlib_spec.rb | 42 +++ spec/twitter/extras_spec.rb | 46 +++ spec/twitter/meta_spec.rb | 90 +++++ spec/twitter/model_spec.rb | 488 +++++++++++++++++++++++++++ spec/twitter/version_spec.rb | 19 ++ tasks/clean.rake | 4 + tasks/doc.rake | 14 + tasks/find.rake | 31 ++ tasks/pkg.rake | 10 + tasks/rubyforge.rake | 23 ++ tasks/spec.rake | 25 ++ tasks/stats.rake | 45 +++ tasks/web.rake | 16 + test/sanity_test.rb | 79 +++++ 59 files changed, 4945 insertions(+) create mode 100644 CHANGES create mode 100644 MIT-LICENSE create mode 100644 README create mode 100644 Rakefile create mode 100644 config/rcov_morpher.rb create mode 100644 config/rdoc_template.rb create mode 100644 config/templates/rcov.rhtml create mode 100644 config/tidy.yml.conf create mode 100644 config/twitter.yml.conf create mode 100644 examples/configure.rb create mode 100644 examples/friendship.rb create mode 100644 examples/messaging.rb create mode 100644 examples/model.rb create mode 100644 examples/status.rb create mode 100644 examples/timeline.rb create mode 100644 examples/user.rb create mode 100644 lib/twitter.rb create mode 100644 lib/twitter/client.rb create mode 100644 lib/twitter/client/base.rb create mode 100644 lib/twitter/client/friendship.rb create mode 100644 lib/twitter/client/messaging.rb create mode 100644 lib/twitter/client/status.rb create mode 100644 lib/twitter/client/timeline.rb create mode 100644 lib/twitter/client/user.rb create mode 100644 lib/twitter/config.rb create mode 100644 lib/twitter/console.rb create mode 100644 lib/twitter/core.rb create mode 100644 lib/twitter/ext.rb create mode 100644 lib/twitter/ext/stdlib.rb create mode 100644 lib/twitter/extras.rb create mode 100644 lib/twitter/meta.rb create mode 100644 lib/twitter/model.rb create mode 100644 lib/twitter/version.rb create mode 100644 pkg-info.yml create mode 100644 spec/spec_helper.rb create mode 100644 spec/twitter/client/base_spec.rb create mode 100644 spec/twitter/client/friendship_spec.rb create mode 100644 spec/twitter/client/messaging_spec.rb create mode 100644 spec/twitter/client/status_spec.rb create mode 100644 spec/twitter/client/timeline_spec.rb create mode 100644 spec/twitter/client/user_spec.rb create mode 100644 spec/twitter/client_spec.rb create mode 100644 spec/twitter/config_spec.rb create mode 100644 spec/twitter/console_spec.rb create mode 100644 spec/twitter/core_spec.rb create mode 100644 spec/twitter/ext/stdlib_spec.rb create mode 100644 spec/twitter/extras_spec.rb create mode 100644 spec/twitter/meta_spec.rb create mode 100644 spec/twitter/model_spec.rb create mode 100644 spec/twitter/version_spec.rb create mode 100644 tasks/clean.rake create mode 100644 tasks/doc.rake create mode 100644 tasks/find.rake create mode 100644 tasks/pkg.rake create mode 100644 tasks/rubyforge.rake create mode 100644 tasks/spec.rake create mode 100644 tasks/stats.rake create mode 100644 tasks/web.rake create mode 100755 test/sanity_test.rb diff --git a/CHANGES b/CHANGES new file mode 100644 index 0000000..2db9524 --- /dev/null +++ b/CHANGES @@ -0,0 +1,102 @@ += CHANGES + +Catalog(ue) of changes for Twitter4R 0.1.x releases including Retrospectiva ticket cross-reference numbers. Refer to http://retro.tautology.net/projects/twitter4r/tickets for more information. + +== 0.2.3 Changes + +=== 2007-07-22 +* Fixed defect #31 such that passing string screen name as for user argument is handled correctly. +* Fixed #30 typo: respond_to -> respond_to? +* Added relevant exception handling for #message(:post, ...) case (#32) +* Add ability to pass in Twitter::User object to Twitter::Client#user(...) #33 +* Added stats Rake task +* Updated RDoc for Twitter::Client#user to warn against using it to get followers of authenticated user and updated ArgumentError raising logic as per #29. + +== 0.2.2 Changes + +=== 2007-07-18 +* Fixed URI paths for user, messaging and friendship APIs (#25) +* Added action checks for Twitter::Client methods: #user, #my, #message, #messages, #status, #timeline, #friend (#26) +* Added 'source' configuration documentation. +* Added missing attributes for Twitter::User (#28) + +== 0.2.1 Changes + +=== 2007-07-17 +* Added 'source' feature and configurability. + +== 0.2.0 Changes + +=== 2007-07-08 +* Added featured users API as an "extra" (#19). +* Productionized website for publishing. +* Published Ruby Gem on Rubyforge. + +=== 2007-07-07 +* Refactored Twitter4R API to be more consistent, by grouping APIs (#6): + - Messaging APIs: direct_messages, new, destroy, replies + - Friendship APIs: create, destroy +* Added following features (#7): + - Retrieving direct messages + - User APIs: friends, followers, show +* Updated documentation and example code. + +=== 2007-07-06 +* Refactored Twitter4R API to be more consistent, by grouping APIs (#6): + - Status APIs: show, update, destroy + - User APIs: friends, followers, show +* Added X-Twitter-Client HTTP headers and Twitter::Config options (#16) +* Removed redundant feature (#8): + - Followers timeline +* Refactored HTTP request/response code to DRY up code. +* Fix REST error handling to use #is_a?(HTTPSuccess) instead of code in ['200', '201'] to determine REST error (#15). + +=== 2007-06-25 +* Updated example documentation (#14) +* Refactored marshaling unmarshaling code (#13) + +=== 2007-06-20 +* Added proxy user/pass support. Tested only via endo-testing. (No system/integration testing behind real proxy as I do not have that environment). + + +=== 2007-06-17 +* Refactored Twitter4R API to be more consistent, by grouping APIs (#6): + - Timeline APIs: public, friends, user + +=== 2007-06-13 +* Added RSpec Autotest integration +* Fixed Twitter::Meta generation of spec for hash values +* Added HTTP header to each request including generated User-Agent header +* Added RCovMorpher and template to restyle RCov output upon release +* Added Gemspec dependencies and requirements +* Added default tidy YAML configuration file for RCovMorpher +* Added Contributors list and updated external dependencies list to README +* Removed shebang from examples + +=== 2007-06-12 +* Added proxy support as per Kaiichi Matsunaga submitted patch (#11). +* Added SSL support (#12) + +=== 2007-05-19 +* Translated RSpec specifications from 0.8.2 compliant to 1.0.0 (#10) + +== 0.1.1 Changes + +=== 2007-06-25 +* Added SSL support (#12) +* Added Proxy support (#11) + +== 0.1.0 Changes + +=== 2007-05-08 +* Added Google Analytics Javascript code to website pages (#5) + +=== 2007-05-07 +* Fixed errors in online sample code documentation and redeployed website (#2 and #3) +* Created more consistent RDoc theme to go more with website home page (#4) + +=== 2007-05-06 +* Initial revision of codebase commited; includes: + - Achieved 80% Twitter API feature-completeness + - Attained 100% RSpec C0 code coverage + - Rake tasks for: RSpec, RCov, RDoc, Gem, Rubyforge Publishing, etc. diff --git a/MIT-LICENSE b/MIT-LICENSE new file mode 100644 index 0000000..682e0bd --- /dev/null +++ b/MIT-LICENSE @@ -0,0 +1,20 @@ +Copyright (c) 2006-2007 Susan Potter . + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README b/README new file mode 100644 index 0000000..fd0ad5f --- /dev/null +++ b/README @@ -0,0 +1,29 @@ += Twitter4R + +* Project Website - http://twitter4r.rubyforge.org +* Mailing List - http://groups.google.com/group/twitter4r-users + +== Developers +* {Susan Potter}[http://SusanPotter.NET] + +== Contributors +* Kaiichi Matsunaga + +== Description +Twitter4R provides an object based API to query or update your Twitter account via pure Ruby. It hides the ugly HTTP/REST code from your code. + +== External Dependencies +* Ruby 1.8 (tested with 1.8.5) +* RSpec gem 1.0.0+ (tested with 1.0.4) +* JSON gem 0.4.3+ (tested with versions: 0.4.3, and 0.1.1) +* jcode (for unicode support) + +== Usage Examples +Twitter4R starting with version 0.1.1 and above is organized into seven parts: +* {Configuration API}[link:files/examples/configure_rb.html] +* {Friendship API}[link:files/examples/friendship_rb.html] +* {Messaging API}[link:files/examples/messaging_rb.html] +* {Model API}[link:files/examples/model_rb.html] +* {Status API}[link:files/examples/status_rb.html] +* {Timeline API}[link:files/examples/timeline_rb.html] +* {User API}[link:files/examples/user_rb.html] diff --git a/Rakefile b/Rakefile new file mode 100644 index 0000000..57582ee --- /dev/null +++ b/Rakefile @@ -0,0 +1,24 @@ +$:.unshift('lib') + +require('rubygems') +gem('rspec', '>=1.0.0') + +require('rake') +require('twitter') + +import('tasks/clean.rake') +import('tasks/doc.rake') +import('tasks/find.rake') +import('tasks/pkg.rake') +import('tasks/rubyforge.rake') +import('tasks/spec.rake') +import('tasks/stats.rake') +import('tasks/web.rake') + +task :default => [:coverage] + +namespace :spec do + task :autotest do + Autotest::Rspec.run + end +end diff --git a/config/rcov_morpher.rb b/config/rcov_morpher.rb new file mode 100644 index 0000000..c1b7f4e --- /dev/null +++ b/config/rcov_morpher.rb @@ -0,0 +1,58 @@ +module RCov; end + +require('erb') +require('tidy') +require('hpricot') + +require(File.join(File.dirname(__FILE__), '..', 'lib', 'twitter')) + +class File #:nodoc: + class << self + # Reads in local file from current directory. + def read_local(file) + self.read(self.join(self.dirname(__FILE__), file)) + end + end +end + +# Follows Strategy design pattern +class RCov::OutputMorpher + # transforms given list of files based on new template. + def transform(files, output_dir = '../web/rcov/', template = 'templates/rcov.rhtml') + files.each do |file| + data = File.read(file) + fname = File.basename(file) + xml = Tidy.open(:show_warnings => true) do |tidy| + tidy.options.output_xml = true + tidy.clean(data) + end + + rhtml = ERB.new(File.read_local(template), 0) + parser = Hpricot.parse(xml) + legend_content = parser.find_element('pre') + table_content = parser.find_element('table') + code_content = table_content.next_sibling + index = (fname == 'index.html') + version = Twitter::Version.to_version + File.delete(fname) if File.exists?(fname) + File.open("#{output_dir}/#{fname}", 'w') do |f| + f.puts(rhtml.result(binding)) + end + end + end +end + +module RCov + class << self + # returns html files to morph based on tidy configuration file + def configure_morpher(tidy_conf = 'tidy.yml') + overrides = YAML.load(File.read_local('tidy.yml')) + Tidy.path = overrides['path'] || '/usr/lib/libtidy-0.99.so.0' + Dir.glob(overrides['html_glob'] || 'doc/rcov/**/*.html') + end + end +end + +if __FILE__ == $0 + RCov::OutputMorpher.new.transform(RCov.configure_morpher) +end diff --git a/config/rdoc_template.rb b/config/rdoc_template.rb new file mode 100644 index 0000000..8f44bf9 --- /dev/null +++ b/config/rdoc_template.rb @@ -0,0 +1,572 @@ +module RDoc + module Page + + FONTS = "\"Bitstream Vera Sans\", Verdana, Arial, Helvetica, sans-serif" + + STYLE = < pre { + font-family: "Courier New", monospace; + padding: 0.5em; + border: 1px dotted #000; + color: #222; + background: #ffd; +} + +.entries { + margin: 0.25em 1em 0 1em; + font-size: x-small; +} +a { + white-space: nowrap; +} + +CSS + + XHTML_PREAMBLE = %{ + +} + + HEADER = XHTML_PREAMBLE + < + + %title% + + + + + + +ENDHEADER + + FILE_PAGE = < + + + + +
File
%short_name%
+ + + + + + + + + +
Path:%full_path% +IF:cvsurl +  (CVS) +ENDIF:cvsurl +
Modified:%dtm_modified%
+
+ +
+HTML + +################################################################### + + CLASS_PAGE = < + %classmod%
%full_name% + + + + + + +IF:parent + + + + +ENDIF:parent +
In: +START:infiles +HREF:full_path_url:full_path: +IF:cvsurl + (CVS) +ENDIF:cvsurl +END:infiles +
Parent: +IF:par_url + +ENDIF:par_url +%parent% +IF:par_url + +ENDIF:par_url +
+ + + +HTML + +################################################################### + + METHOD_LIST = < +IF:diagram +
+ %diagram% +
+ENDIF:diagram + +IF:description +
%description%
+ENDIF:description + +IF:requires +
Required Files
+
    +START:requires +
  • HREF:aref:name:
  • +END:requires +
+ENDIF:requires + +IF:toc +
Contents
+ +ENDIF:toc + +IF:methods +
Methods
+
    +START:methods +
  • HREF:aref:name:
  • +END:methods +
+ENDIF:methods + +IF:includes +
Included Modules
+
    +START:includes +
  • HREF:aref:name:
  • +END:includes +
+ENDIF:includes + +START:sections +IF:sectitle + +IF:seccomment +
+%seccomment% +
+ENDIF:seccomment +ENDIF:sectitle + +IF:classlist +
Classes and Modules
+ %classlist% +ENDIF:classlist + +IF:constants +
Constants
+ +START:constants + + + + + +IF:desc + + + + +ENDIF:desc +END:constants +
%name%=%value%
 %desc%
+ENDIF:constants + +IF:attributes +
Attributes
+ +START:attributes + + + + + +END:attributes +
+IF:rw +[%rw%] +ENDIF:rw + %name%%a_desc%
+ENDIF:attributes + +IF:method_list +START:method_list +IF:methods +
%type% %category% methods
+START:methods +
+
+IF:callseq + %callseq% +ENDIF:callseq +IFNOT:callseq + %name%%params% +ENDIF:callseq +IF:codeurl +[ source ] +ENDIF:codeurl +
+IF:m_desc +
+ %m_desc% +
+ENDIF:m_desc +IF:aka +
+ This method is also aliased as +START:aka + %name% +END:aka +
+ENDIF:aka +IF:sourcecode +
+ +
+
+%sourcecode%
+
+
+
+ENDIF:sourcecode +
+END:methods +ENDIF:methods +END:method_list +ENDIF:method_list +END:sections + +HTML + + FOOTER = < + + + + +ENDFOOTER + + BODY = HEADER + < + +
+ #{METHOD_LIST} +
+ + #{FOOTER} +ENDBODY + +########################## Source code ########################## + + SRC_PAGE = XHTML_PREAMBLE + < +%title% + + + + +
%code%
+ + +HTML + +########################## Index ################################ + + FR_INDEX_BODY = < + + + + + + + +
+START:entries +%name%
+END:entries +
+ +HTML + + CLASS_INDEX = FILE_INDEX + METHOD_INDEX = FILE_INDEX + + INDEX = XHTML_PREAMBLE + < + + %title% + + + + + + + + + +IF:inline_source + +ENDIF:inline_source +IFNOT:inline_source + + + + +ENDIF:inline_source + + <body bgcolor="white"> + Click <a href="html/index.html">here</a> for a non-frames + version of this page. + </body> + + + + +HTML + end +end + diff --git a/config/templates/rcov.rhtml b/config/templates/rcov.rhtml new file mode 100644 index 0000000..813e78a --- /dev/null +++ b/config/templates/rcov.rhtml @@ -0,0 +1,585 @@ + + + + + Twitter4R - Open Source Ruby Project Code Coverage (RCov C0) + + + + + + + +
+ +
+ +

Code Coverage Metrics (C0)

+
+ <% unless index %> + [Back to coverage index] + <% end %> +
+

<%= legend_content %>

+ <%= table_content %> +

<%= code_content %>

+
+ +
+ + + + diff --git a/config/tidy.yml.conf b/config/tidy.yml.conf new file mode 100644 index 0000000..7b06d17 --- /dev/null +++ b/config/tidy.yml.conf @@ -0,0 +1,2 @@ +path: /usr/lib/libtidy-0.99.so.0 +html_glob: doc/rdoc/**/*.html diff --git a/config/twitter.yml.conf b/config/twitter.yml.conf new file mode 100644 index 0000000..232c432 --- /dev/null +++ b/config/twitter.yml.conf @@ -0,0 +1,4 @@ +test: + login: twitter4r + password: rubyr0cks + diff --git a/examples/configure.rb b/examples/configure.rb new file mode 100644 index 0000000..99514cf --- /dev/null +++ b/examples/configure.rb @@ -0,0 +1,36 @@ +# = Configuration API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# Here we setup configuration for all Twitter::Client instances +# This looks much like Rails' Initializer code that can be found +# in config/environment in Rails applications by design. +# Twitter::Client.configure do |conf| +# # We can set Twitter4R to use :ssl or :http to connect to the Twitter API. +# # Defaults to :ssl +# conf.protocol = :ssl +# +# # We can set Twitter4R to use another host name (perhaps for internal +# # testing purposes). +# # Defaults to 'twitter.com' +# conf.host = 'twitter.com' +# +# # We can set Twitter4R to use another port (also for internal +# # testing purposes). +# # Defaults to 443 +# conf.port = 443 +# +# # We can set proxy information for Twitter4R +# # By default all following values are set to nil. +# conf.proxy_host = 'myproxy.host' +# conf.proxy_port = 8080 +# conf.proxy_user = 'myuser' +# conf.proxy_pass = 'mypass' +# +# # We can also change the User-Agent and X-Twitter-Client* HTTP headers +# conf.user_agent = 'MyAppAgentName' +# conf.application_name = 'MyAppName' +# conf.application_version = 'v1.5.6' +# conf.application_url = 'http://myapp.url' +# end diff --git a/examples/friendship.rb b/examples/friendship.rb new file mode 100644 index 0000000..8a09d4f --- /dev/null +++ b/examples/friendship.rb @@ -0,0 +1,32 @@ +# = Friendship API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# The following is only required if you want to use some configuration +# helper methods like Twitte4R::Client.from_config for +# sensitive/instance context. +# require 'twitter/console' +# config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +# +# To override client connection configuration please refer to +# the {configure.rb}[file:configure_rb.html] example code. +# twitter = Twitter::Client.from_config(config_file) +# +# To add a friend to the authenticated user's list of friends using the +# client context object you can do: +# twitter.friend(:add, 'otherlogin') +# You may also pass in the unique integer user ID or the Twitter::User object +# representation of the user you wish to 'friend'. For example: +# user = twitter.user('otherlogin') +# twitter.friend(:add, user) +# OR +# twitter.friend(:add, user.id) +# +# To remove a friend from the authenticated user's list of friends using the +# client context object you can do: +# twitter.friend(:remove, 'otherlogin') +# As with the case of adding a new friend, you can use the unique integer user +# ID or the Twitter::User object representation of the user you wish to remove +# as a friend. See above "add" examples and replace ":add" with ":remove" for +# desired effect. diff --git a/examples/messaging.rb b/examples/messaging.rb new file mode 100644 index 0000000..cc949df --- /dev/null +++ b/examples/messaging.rb @@ -0,0 +1,38 @@ +# = Messaging API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# The following is only required if you want to use some configuration +# helper methods like Twitte4R::Client.from_config for +# sensitive/instance context. +# require 'twitter/console' +# config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +# +# To override client connection configuration please refer to +# the {configure.rb}[file:configure_rb.html] example code. +# twitter = Twitter::Client.from_config(config_file) +# +# To retrieve a list of direct messages received by the authenticated user +# you may do the following: +# messages = twitter.messages(:received) +# +# To retrieve a list of direct messages sent by the authenticated user +# you may do the following: +# messages = twitter.messages(:sent) +# +# To send a direct message to another user, you can do the following: +# text = 'Do you want to meet me at our favorite coffeeshop at 3pm?' +# message = twitter.message(:post, text, 'myfriend') +# As with most methods that accept the user's screen name you can also use in +# it's place either the unique integer user ID or the Twitter::User +# object representation of the desired recipient user. For example, +# friend = Twitter::User.find('myfriend', twitter) +# message = twitter.message(:post, text, friend) +# OR +# message = twitter.message(:post, text, friend.id) +# +# To delete a direct message you can use the following code: +# twitter.message(:delete, message) +# You may also pass in the unique integer message ID instead like: +# twitter.message(:delete, message.id) diff --git a/examples/model.rb b/examples/model.rb new file mode 100644 index 0000000..8818b23 --- /dev/null +++ b/examples/model.rb @@ -0,0 +1,60 @@ +# = Model API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# The following is only required if you want to use some configuration +# helper methods like Twitte4R::Client.from_config for +# sensitive/instance context. +# require 'twitter/console' +# config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +# +# To override client connection configuration please refer to +# the {configure.rb}[file:configure_rb.html] example code. +# twitter = Twitter::Client.from_config(config_file) +# +# The purpose of the Model API is to make Twitter REST methods +# integrated into class and instance methods that resemble +# ActiveRecord class API. +# +# == Create Methods +# For example, suppose we want to create a new direct message to a friend: +# message = Twitter::Message.create( +# :text => 'Ich liebe dich!', +# :client => twitter, +# :recipient => 'myspouse') +# +# We can also create a new status message, which updates our (the +# authenticated user's) timeline: +# status = Twitter::Status.create( +# :text => 'Doing early Christmas shopping in July', +# :client => twitter) +# +# Notice we must always pass in a :client key-value pair giving +# the client context object. +# +# There is currently no useful Twitter::User.create implementation as +# Twitter does not provide this server REST API. This is *not* +# a Twitter4R limitation. Please harass the Twitter.com/Obvious +# developer for this not me please. +# +# == Finder Methods +# We can also use finder methods that look very similar to +# ActiveRecord style classes: +# user = Twitter::User.find('myfriend', twitter) +# status = Twitter::Status.find(status.id, twitter) +# +# There is currently no useful Twitter::Message.find implementation as +# Twitter does not provide this server REST API. Ths is *not* +# a Twitter4R limitation. Please harass the Twitter.com/Obvious +# developer for this not me please. +# +# == Domain Specific Methods +# Twitter::User has a few domain specific methods for convenience: +# user.is_me? # => false since user is 'myfriend' not authenticated user +# me = Twitter::User.find('mylogin') +# me.is_me? # => true since me is authenticated user +# me.followers # => return Array of followers for authenticated user (only available for authenticated user) +# me.friends # => returns Array of friends for that user instance +# me.befriend(user) # => user (only available for authenticated user) +# me.defriend(user) # => user (only available for authenticated user) diff --git a/examples/status.rb b/examples/status.rb new file mode 100644 index 0000000..419e3fb --- /dev/null +++ b/examples/status.rb @@ -0,0 +1,24 @@ +# = Status API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# The following is only required if you want to use some configuration +# helper methods like Twitte4R::Client.from_config for +# sensitive/instance context. +# require 'twitter/console' +# config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +# +# To override client connection configuration please refer to +# the {configure.rb}[file:configure_rb.html] example code. +# twitter = Twitter::Client.from_config(config_file) +# +# To post a new status we can do: +# status = twitter.status(:post, 'NOT buying overrated iPhone.') +# +# To retrieve the status from it's unique integer status ID we can do the +# following: +# status = twitter.status(:get, status.id) +# +# To delete a status message we can do: +# twitter.status(:delete, status) diff --git a/examples/timeline.rb b/examples/timeline.rb new file mode 100644 index 0000000..2d6b67b --- /dev/null +++ b/examples/timeline.rb @@ -0,0 +1,66 @@ +# = Timeline API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# The following is only required if you want to use some configuration +# helper methods like Twitte4R::Client.from_config for +# sensitive/instance context. +# require 'twitter/console' +# config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +# +# +# To override client connection configuration please refer to +# the {configure.rb}[file:configure_rb.html] example code. +# twitter = Twitter::Client.from_config(config_file) +# +# This returns the public timeline as an Array. +# +# We pass a block in to do something with each status returned inline. +# However, we still get to keep the public_timeline array after +# the method returns, for our application's safe keeping. +# +# It is not necessary to pass in a block to this method, so don't worry if +# you don't want to do that. All calls to Twitter::Client#timeline_for +# can pass in a block that works the same way as explained here. +# public_timeline = twitter.timeline_for(:public) do |status| +# puts status.user.screen_name, status.text +# end +# +# This returns the timeline for all your friends as an Array. +# See public timeline example above for discussion about passing in block, +# which is possible to all calls to Twitter::Client#timeline_for. +# all_friends_timeline = twitter.timeline_for(:friends) +# +# This returns the timeline for a particular friend. +# myfriend_timeline = twitter.timeline_for(:friend, :id => 'dictionary') +# +# This returns the timeline for a particular user (who may or may not be a friend). +# user_timeline = twitter.timeline_for(:user, :id => 'twitter4r') +# +# This returns the authenticated user's timeline +# +# We can also use the following code snippet, which is equivalent to the example below: +# my_timeline = twitter.my(:timeline) +# See Status API examples in status.rb file. +# my_timeline = twitter.timeline_for(:me) +# +# Below we demonstrate how to use more interesting parameters to the +# Twitter::Client#timeline_for method +# +# Returns all public statuses since the status id returned by: +# public_timeline.first.id +# latest_timeline = twitter.timeline_for(:public, :since_id => public_timeline.first.id) +# +# Returns all friend statuses in the last 24 hours: +# yesterday = Time.now - 60*60*24 +# new_friends_timeline = twitter.timeline_for(:friends, :since => yesterday) +# +# Returns last three statuses from user 'twitter4r': +# latest_twitter4r_timeline = twitter.timeline_for(:user, :id => 'twitter4', :count => 3) +# +# Returns timeline from user 'dictionary' since yesterday at this time, +# with block: +# new_dictionary_timeline = twitter.timeline_for(:user, :id => 'dictionary', :since => yesterday) do |status| +# puts status.text +# end diff --git a/examples/user.rb b/examples/user.rb new file mode 100644 index 0000000..a92cd75 --- /dev/null +++ b/examples/user.rb @@ -0,0 +1,34 @@ +# = User API Examples +# require('rubygems') +# gem('twitter4r', '0.2.0') +# require('twitter') +# +# The following is only required if you want to use some configuration +# helper methods like Twitte4R::Client.from_config for +# sensitive/instance context. +# require 'twitter/console' +# config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +# +# To override client connection configuration please refer to +# the {configure.rb}[file:configure_rb.html] example code. +# twitter = Twitter::Client.from_config(config_file) +# +# To get the Twitter::User object representation of a Twitter user we can do: +# user = twitter.user('otherlogin') +# +# To get a list of friends of a specific user on Twitter we can do: +# friends = twitter.user('otherlogin', :friends) +# See the {Model API}[link:files/examples/model_rb.html] for related methods. +# +# To get the authenticated user's Twitter::User object representation we can +# do: +# me = twitter.my(:info) +# +# To get the authenticated user's followers (only available for authenticated +# user): +# followers = twitter.my(:followers) +# OR +# myuser = twitter.my(:info) +# followers = myuser.followers +# See the {Model API}[link:files/examples/model_rb.html] for more information +# on this. diff --git a/lib/twitter.rb b/lib/twitter.rb new file mode 100644 index 0000000..1f8daa9 --- /dev/null +++ b/lib/twitter.rb @@ -0,0 +1,25 @@ +# + +module Twitter; end + +def require_local(suffix) + require(File.expand_path(File.join(File.dirname(__FILE__), suffix))) +end + +# For better unicode support +$KCODE = 'u' +require 'jcode' + +# External requires +require('net/https') +require('uri') +require('json') + +# Ordering matters...pay attention here! +require_local('twitter/ext') +require_local('twitter/version') +require_local('twitter/meta') +require_local('twitter/core') +require_local('twitter/model') +require_local('twitter/config') +require_local('twitter/client') diff --git a/lib/twitter/client.rb b/lib/twitter/client.rb new file mode 100644 index 0000000..95cb02d --- /dev/null +++ b/lib/twitter/client.rb @@ -0,0 +1,18 @@ +# client.rb contains the classes, methods and extends Twitter4R +# features to define client calls to the Twitter REST API. +# +# See: +# * Twitter::Client + +# Used to query or post to the Twitter REST API to simplify code. +class Twitter::Client + include Twitter::ClassUtilMixin +end + +require('twitter/client/base.rb') +require('twitter/client/timeline.rb') +require('twitter/client/status.rb') +require('twitter/client/friendship.rb') +require('twitter/client/messaging.rb') +require('twitter/client/user.rb') + diff --git a/lib/twitter/client/base.rb b/lib/twitter/client/base.rb new file mode 100644 index 0000000..279f630 --- /dev/null +++ b/lib/twitter/client/base.rb @@ -0,0 +1,81 @@ +class Twitter::Client + protected + attr_accessor :login, :password + + # Returns the response of the HTTP connection. + def http_connect(body = nil, require_auth = true, &block) + require_block(block_given?) + connection = create_http_connection + connection.start do |connection| + request = yield connection if block_given? + request.basic_auth(@login, @password) if require_auth + response = connection.request(request, body) + handle_rest_response(response) + response + end + end + + # "Blesses" model object with client information + def bless_model(model) + model.bless(self) if model + end + + def bless_models(list) + return bless_model(list) if list.respond_to?(:client=) + list.collect { |model| bless_model(model) } if list.respond_to?(:collect) + end + + private + @@http_header = nil + + def raise_rest_error(response, uri = nil) + raise Twitter::RESTError.new(:code => response.code, + :message => response.message, + :uri => uri) + end + + def handle_rest_response(response, uri = nil) + unless response.is_a?(Net::HTTPSuccess) + raise_rest_error(response, uri) + end + end + + def create_http_connection + conn = Net::HTTP.new(@@config.host, @@config.port, + @@config.proxy_host, @@config.proxy_port, + @@config.proxy_user, @@config.proxy_pass) + if @@config.protocol == :ssl + conn.use_ssl = true + conn.verify_mode = OpenSSL::SSL::VERIFY_NONE + end + conn + end + + def http_header + # can cache this in class variable since all "variables" used to + # create the contents of the HTTP header are determined by other + # class variables that are not designed to change after instantiation. + @@http_header ||= { + 'User-Agent' => "Twitter4R v#{Twitter::Version.to_version} [#{@@config.user_agent}]", + 'Accept' => 'text/x-json', + 'X-Twitter-Client' => @@config.application_name, + 'X-Twitter-Client-Version' => @@config.application_version, + 'X-Twitter-Client-URL' => @@config.application_url, + } + @@http_header + end + + def create_http_get_request(uri, params = {}) + path = (params.size > 0) ? "#{uri}?#{params.to_http_str}" : uri + Net::HTTP::Get.new(path, http_header) + end + + def create_http_post_request(uri) + Net::HTTP::Post.new(uri, http_header) + end + + def create_http_delete_request(uri, params = {}) + path = (params.size > 0) ? "#{uri}?#{params.to_http_str}" : uri + Net::HTTP::Delete.new(path, http_header) + end +end diff --git a/lib/twitter/client/friendship.rb b/lib/twitter/client/friendship.rb new file mode 100644 index 0000000..98efa66 --- /dev/null +++ b/lib/twitter/client/friendship.rb @@ -0,0 +1,35 @@ +class Twitter::Client + @@FRIENDSHIP_URIS = { + :add => '/friendships/create', + :remove => '/friendships/destroy', + } + + # Provides access to the Twitter Friendship API. + # + # You can add and remove friends using this method. + # + # action can be any of the following values: + # * :add - to add a friend, you would use this action value + # * :remove - to remove an existing friend from your friends list use this. + # + # The value must be either the user to befriend or defriend's + # screen name, integer unique user ID or Twitter::User object representation. + # + # Examples: + # screen_name = 'dictionary' + # client.friend(:add, 'dictionary') + # client.friend(:remove, 'dictionary') + # id = 1260061 + # client.friend(:add, id) + # client.friend(:remove, id) + # user = Twitter::User.find(id, client) + # client.friend(:add, user) + # client.friend(:remove, user) + def friend(action, value) + raise ArgumentError, "Invalid friend action provided: #{action}" unless @@FRIENDSHIP_URIS.keys.member?(action) + value = value.to_i unless value.is_a?(String) + uri = "#{@@FRIENDSHIP_URIS[action]}/#{value}.json" + response = http_connect {|conn| create_http_get_request(uri) } + bless_model(Twitter::User.unmarshal(response.body)) + end +end diff --git a/lib/twitter/client/messaging.rb b/lib/twitter/client/messaging.rb new file mode 100644 index 0000000..669db4f --- /dev/null +++ b/lib/twitter/client/messaging.rb @@ -0,0 +1,79 @@ +class Twitter::Client + + @@MESSAGING_URIS = { + :received => '/direct_messages.json', + :sent => '/direct_messages/sent.json', + :post => '/direct_messages/new.json', + :delete => '/direct_messages/destroy', + } + + # Provides access to Twitter's Messaging API for received and + # sent direct messages. + # + # Example: + # received_messages = @twitter.messages(:received) + # + # An ArgumentError will be raised if an invalid action + # is given. Valid actions are: + # * +:received+ + # * +:sent+ + def messages(action) + raise ArgumentError, "Invalid messaging action: #{action}" unless [:sent, :received].member?(action) + uri = @@MESSAGING_URIS[action] + response = http_connect {|conn| create_http_get_request(uri) } + bless_models(Twitter::Message.unmarshal(response.body)) + end + + # Provides access to Twitter's Messaging API for sending and deleting + # direct messages to other users. + # + # action can be: + # * :post - to send a new direct message, value, to user given. + # * :delete - to delete direct message with message ID value. + # + # value should be: + # * String when action is :post. Will be the message text sent to given user. + # * Integer or Twitter::Message object when action is :delete. Will refer to the unique message ID to delete. When passing in an instance of Twitter::Message that Status will be + # + # user should be: + # * Twitter::User, Integer or String object when action is :post. The Integer must be the unique ID of the Twitter user you wish to send the direct message to and any Strings passed in must be the screen name of the user you wish to send the direct message to. + # * totally ignore when action is :delete. It has no purpose in this use case scenario. + # + # Examples: + # The example below sends the message text 'Are you coming over at 6pm for the BBQ tonight?' to user with screen name 'myfriendslogin'... + # @twitter.message(:post, 'Are you coming over at 6pm for the BBQ tonight?', 'myfriendslogin') + # The example below sends the same message text as above to user with unique integer ID of 1234567890... + # the example below sends the same message text as above to user represented by user object instance of Twitter::User... + # @twitter.message(:post, 'Are you coming over at 6pm for the BBQ tonight?', user) + # message = @twitter.message(:post, 'Are you coming over at 6pm for the BBQ tonight?', 1234567890) + # the example below delete's the message send directly above to user with unique ID 1234567890... + # @twitter.message(:delete, message) + # Or the following can also be done... + # @twitter.message(:delete, message.id) + # + # In both scenarios (action is :post or + # :delete) a blessed Twitter::Message object is + # returned that represents the newly posted or newly deleted message. + # + # An ArgumentError will be raised if an invalid action + # is given. Valid actions are: + # * +:post+ + # * +:delete+ + # + # An ArgumentError is also raised when no user argument is + # supplied when action is +:post+. + def message(action, value, user = nil) + raise ArgumentError, "Invalid messaging action: #{action}" unless [:post, :delete].member?(action) + raise ArgumentError, "User argument must be supplied for :post case" if action.eql?(:post) and user.nil? + uri = @@MESSAGING_URIS[action] + user = user.to_i if user and user.is_a?(Twitter::User) + case action + when :post + response = http_connect({:text => value, :user => user, :source => @@config.source}.to_http_str) {|conn| create_http_post_request(uri) } + when :delete + response = http_connect {|conn| create_http_delete_request(uri, :id => value.to_i) } + end + message = Twitter::Message.unmarshal(response.body) + bless_model(message) + end +end diff --git a/lib/twitter/client/status.rb b/lib/twitter/client/status.rb new file mode 100644 index 0000000..dce4be7 --- /dev/null +++ b/lib/twitter/client/status.rb @@ -0,0 +1,45 @@ +class Twitter::Client + @@STATUS_URIS = { + :get => '/statuses/show.json', + :post => '/statuses/update.json', + :delete => '/statuses/destroy.json', + } + + # Provides access to individual statuses via Twitter's Status APIs + # + # action can be of the following values: + # * :get to retrieve status content. Assumes value given responds to :to_i message in meaningful way to yield intended status id. + # * :post to publish a new status + # * :delete to remove an existing status. Assumes value given responds to :to_i message in meaningful way to yield intended status id. + # + # value should be set to: + # * the status identifier for :get case + # * the status text message for :post case + # * none necessary for :delete case + # + # Examples: + # twitter.status(:get, 107786772) + # twitter.status(:post, "New Ruby open source project Twitter4R version 0.2.0 released.") + # twitter.status(:delete, 107790712) + # + # An ArgumentError will be raised if an invalid action + # is given. Valid actions are: + # * +:get+ + # * +:post+ + # * +:delete+ + def status(action, value) + raise ArgumentError, "Invalid status action: #{action}" unless @@STATUS_URIS.keys.member?(action) + return nil unless value + uri = @@STATUS_URIS[action] + response = nil + case action + when :get + response = http_connect {|conn| create_http_get_request(uri, :id => value.to_i) } + when :post + response = http_connect({:status => value, :source => @@config.source}.to_http_str) {|conn| create_http_post_request(uri) } + when :delete + response = http_connect {|conn| create_http_delete_request(uri, :id => value.to_i) } + end + bless_model(Twitter::Status.unmarshal(response.body)) + end +end diff --git a/lib/twitter/client/timeline.rb b/lib/twitter/client/timeline.rb new file mode 100644 index 0000000..1916ab1 --- /dev/null +++ b/lib/twitter/client/timeline.rb @@ -0,0 +1,71 @@ +class Twitter::Client + @@TIMELINE_URIS = { + :public => '/statuses/public_timeline.json', + :friends => '/statuses/friends_timeline.json', + :friend => '/statuses/friends_timeline.json', + :user => '/statuses/user_timeline.json', + :me => '/statuses/user_timeline.json', + } + + # Provides access to Twitter's Timeline APIs + # + # Returns timeline for given type. + # + # type can take the following values: + # * public + # * friends or friend + # * user or me + # + # :id is on key applicable to be defined in options: + # * the id or screen name (aka login) for :friends + # * the id or screen name (aka login) for :user + # * meaningless for the :me case, since twitter.timeline_for(:user, 'mylogin') and twitter.timeline_for(:me) are the same assuming 'mylogin' is the authenticated user's screen name (aka login). + # + # Examples: + # # returns the public statuses since status with id of 6543210 + # twitter.timeline_for(:public, id => 6543210) + # # returns the statuses for friend with user id 43210 + # twitter.timeline_for(:friend, :id => 43210) + # # returns the statuses for friend with screen name (aka login) of 'otherlogin' + # twitter.timeline_for(:friend, :id => 'otherlogin') + # # returns the statuses for user with screen name (aka login) of 'otherlogin' + # twitter.timeline_for(:user, :id => 'otherlogin') + # + # options can also include the following keys: + # * :id is the user ID, screen name of Twitter::User representation of a Twitter user. + # * :since is a Time object specifying the date-time from which to return results for. Applicable for the :friend, :friends, :user and :me cases. + # * :count specifies the number of statuses to retrieve. Only applicable for the :user case. + # * since_id is the status id of the public timeline from which to retrieve statuses for :public. Only applicable for the :public case. + # + # You can also pass this method a block, which will iterate through the results + # of the requested timeline and apply the block logic for each status returned. + # + # Example: + # twitter.timeline_for(:public) do |status| + # puts status.user.screen_name, status.text + # end + # + # twitter.timeline_for(:friend, :id => 'myfriend', :since => 30.minutes.ago) do |status| + # puts status.user.screen_name, status.text + # end + # + # timeline = twitter.timeline_for(:me) do |status| + # puts status.text + # end + # + # An ArgumentError will be raised if an invalid type + # is given. Valid types are: + # * +:public+ + # * +:friends+ + # * +:friend+ + # * +:user+ + # * +:me+ + def timeline_for(type, options = {}, &block) + raise ArgumentError, "Invalid timeline type: #{type}" unless @@TIMELINE_URIS.keys.member?(type) + uri = @@TIMELINE_URIS[type] + response = http_connect {|conn| create_http_get_request(uri, options) } + timeline = Twitter::Status.unmarshal(response.body) + timeline.each {|status| bless_model(status); yield status if block_given? } + timeline + end +end diff --git a/lib/twitter/client/user.rb b/lib/twitter/client/user.rb new file mode 100644 index 0000000..5307a9e --- /dev/null +++ b/lib/twitter/client/user.rb @@ -0,0 +1,54 @@ +class Twitter::Client + @@USER_URIS = { + :info => '/users/show', + :friends => '/statuses/friends.json', + :followers => '/statuses/followers.json', + } + + # Provides access to Twitter's User APIs + # + # Returns user instance for the id given. The id + # can either refer to the numeric user ID or the user's screen name. + # + # For example, + # @twitter.user(234943) #=> Twitter::User object instance for user with numeric id of 234943 + # @twitter.user('mylogin') #=> Twitter::User object instance for user with screen name 'mylogin' + # + # An ArgumentError will be raised if an invalid action + # is given. Valid actions are: + # * +:info+ + # * +:friends+ + # + # +Note:+ You should not use this method to attempt to retrieve the + # authenticated user's followers. Please use any of the following + # ways of accessing this list: + # followers = client.my(:followers) + # OR + # followers = client.my(:info).followers + def user(id, action = :info) + raise ArgumentError, "Invalid user action: #{action}" unless @@USER_URIS.keys.member?(action) + raise ArgumentError, "Unable to retrieve followers for user: #{id}" if action.eql?(:followers) and not id.eql?(@login) + id = id.to_i if id.is_a?(Twitter::User) + response = http_connect {|conn| create_http_get_request(@@USER_URIS[action], :id => id) } + bless_models(Twitter::User.unmarshal(response.body)) + end + + # Syntactic sugar for queries relating to authenticated user in Twitter's User API + # + # When action is: + # * :info - Returns user instance for the authenticated user. + # * :friends - Returns Array of users that are authenticated user's friends + # * :followers - Returns Array of users that are authenticated user's followers + # + # An ArgumentError will be raised if an invalid action + # is given. Valid actions are: + # * +:info+ + # * +:friends+ + # * +:followers+ + def my(action) + raise ArgumentError, "Invalid user action: #{action}" unless @@USER_URIS.keys.member?(action) + response = http_connect {|conn| create_http_get_request(@@USER_URIS[action], :id => @login) } + users = Twitter::User.unmarshal(response.body) + bless_models(users) + end +end diff --git a/lib/twitter/config.rb b/lib/twitter/config.rb new file mode 100644 index 0000000..061b64e --- /dev/null +++ b/lib/twitter/config.rb @@ -0,0 +1,71 @@ +# config.rb contains classes, methods and extends existing Twitter4R classes +# to provide easy configuration facilities. + +module Twitter + # Represents global configuration for Twitter::Client. + # Can override the following configuration options: + # * protocol - :http, :https or :ssl supported. :ssl is an alias for :https. Defaults to :ssl + # * host - hostname to connect to for the Twitter service. Defaults to 'twitter.com'. + # * port - port to connect to for the Twitter service. Defaults to 443. + # * proxy_host - proxy host to use. Defaults to nil. + # * proxy_port - proxy host to use. Defaults to nil. + # * proxy_user - proxy username to use. Defaults to nil. + # * proxy_pass - proxy password to use. Defaults to nil. + # * user_agent - user agent string to use for each request of the HTTP header. + # * application_name - name of your client application. Defaults to 'Twitter4R' + # * application_version - version of your client application. Defaults to current Twitter::Version.to_version. + # * application_url - URL of your client application. Defaults to http://twitter4r.rubyforge.org. + # * source - the source id given to you by Twitter to identify your application in their web interface. Note: you must contact Twitter.com developer directly so they can configure their servers appropriately. + class Config + include ClassUtilMixin + @@ATTRIBUTES = [ + :protocol, + :host, + :port, + :proxy_host, + :proxy_port, + :proxy_user, + :proxy_pass, + :user_agent, + :application_name, + :application_version, + :application_url, + :source, + ] + attr_accessor *@@ATTRIBUTES + + # Override of Object#eql? to ensure RSpec specifications run + # correctly. Also done to follow Ruby best practices. + def eql?(other) + return true if self == other + @@ATTRIBUTES.each do |att| + return false unless self.send(att).eql?(other.send(att)) + end + true + end + end + + class Client + @@defaults = { :host => 'twitter.com', + :port => 443, + :protocol => :ssl, + :proxy_host => nil, + :proxy_port => nil, + :user_agent => "default", + :application_name => 'Twitter4R', + :application_version => Twitter::Version.to_version, + :application_url => 'http://twitter4r.rubyforge.org', + :source => 'twitter4r', + } + @@config = Twitter::Config.new(@@defaults) + + # Twitter::Client class methods + class << self + # Yields to given block to configure the Twitter4R API. + def configure(&block) + raise ArgumentError, "Block must be provided to configure" unless block_given? + yield @@config + end # configure + end # class << self + end # Client class +end # Twitter module diff --git a/lib/twitter/console.rb b/lib/twitter/console.rb new file mode 100644 index 0000000..2245ff6 --- /dev/null +++ b/lib/twitter/console.rb @@ -0,0 +1,28 @@ +# Contains hooks for the twitter console + +module Twitter + class Client + class << self + # Helper method mostly for irb shell prototyping. + # + # Reads in login/password Twitter credentials from YAML file + # found at the location given by config_file that has + # the following format: + # envname: + # login: mytwitterlogin + # password: mytwitterpassword + # + # Where envname is the name of the environment like 'test', + # 'dev' or 'prod'. The env argument defaults to 'test'. + # + # To use this in the shell you would do something like the following + # examples: + # twitter = Twitter::Client.from_config('config/twitter.yml', 'dev') + # twitter = Twitter::Client.from_config('config/twitter.yml') + def from_config(config_file, env = 'test') + yaml_hash = YAML.load(File.read(config_file)) + self.new yaml_hash[env] + end + end # class << self + end +end diff --git a/lib/twitter/core.rb b/lib/twitter/core.rb new file mode 100644 index 0000000..825108d --- /dev/null +++ b/lib/twitter/core.rb @@ -0,0 +1,136 @@ +# The Twitter4R API provides a nicer Ruby object API to work with +# instead of coding around the REST API. + +# Module to encapsule the Twitter4R API. +module Twitter + # Mixin module for classes that need to have a constructor similar to + # Rails' models, where a Hash is provided to set attributes + # appropriately. + # + # To define a class that uses this mixin, use the following code: + # class FilmActor + # include ClassUtilMixin + # end + module ClassUtilMixin #:nodoc: + def self.included(base) #:nodoc: + base.send(:include, InstanceMethods) + end + + # Instance methods defined for Twitter::ModelMixin module. + module InstanceMethods #:nodoc: + # Constructor/initializer that takes a hash of parameters that + # will initialize *members* or instance attributes to the + # values given. For example, + # + # class FilmActor + # include Twitter::ClassUtilMixin + # attr_accessor :name + # end + # + # class Production + # include Twitter::ClassUtilMixin + # attr_accessor :title, :year, :actors + # end + # + # # Favorite actress... + # jodhi = FilmActor.new(:name => "Jodhi May") + # jodhi.name # => "Jodhi May" + # + # # Favorite actor... + # robert = FilmActor.new(:name => "Robert Lindsay") + # robert.name # => "Robert Lindsay" + # + # # Jane is also an excellent pick...gotta love her accent! + # jane = FilmActor.new(name => "Jane Horrocks") + # jane.name # => "Jane Horrocks" + # + # # Witty BBC series... + # mrs_pritchard = Production.new(:title => "The Amazing Mrs. Pritchard", + # :year => 2005, + # :actors => [jodhi, jane]) + # mrs_pritchard.title # => "The Amazing Mrs. Pritchard" + # mrs_pritchard.year # => 2005 + # mrs_pritchard.actors # => [#, + # ] + # # Any Ros Pritchard's out there to save us from the Tony Blair + # # and Gordon Brown *New Labour* debacle? You've got my vote! + # + # jericho = Production.new(:title => "Jericho", + # :year => 2005, + # :actors => [robert]) + # jericho.title # => "Jericho" + # jericho.year # => 2005 + # jericho.actors # => [#] + # + # Assuming class FilmActor includes + # Twitter::ClassUtilMixin in the class definition + # and has an attribute of name, then that instance + # attribute will be set to "Jodhi May" for the actress + # object during object initialization (aka construction for + # you Java heads). + def initialize(params = {}) + params.each do |key,val| + self.send("#{key}=", val) if self.respond_to? key + end + self.send(:init) if self.respond_to? :init + end + + protected + # Helper method to provide an easy and terse way to require + # a block is provided to a method. + def require_block(block_given) + raise ArgumentError, "Must provide a block" unless block_given + end + end + end # ClassUtilMixin + + # Exception subclass raised when there is an error encountered upon + # querying or posting to the remote Twitter REST API. + # + # To consume and query any RESTError raised by Twitter4R: + # begin + # # Do something with your instance of Twitter::Client. + # # Maybe something like: + # timeline = twitter.timeline_for(:public) + # rescue RESTError => re + # puts re.code, re.message, re.uri + # end + # Which on the code raising a RESTError will output something like: + # 404 + # Resource Not Found + # /i_am_crap.json + class RESTError < Exception + include ClassUtilMixin + attr_accessor :code, :message, :uri + + # Returns string in following format: + # "HTTP #{@code}: #{@message} at #{@uri}" + # For example, + # "HTTP 404: Resource Not Found at /i_am_crap.json" + def to_s + "HTTP #{@code}: #{@message} at #{@uri}" + end + end # RESTError + + # Remote REST API interface representation + # + class RESTInterfaceSpec + include ClassUtilMixin + + end + + # Remote REST API method representation + # + class RESTMethodSpec + include ClassUtilMixin + attr_accessor :uri, :method, :parameters + end + + # Remote REST API method parameter representation + # + class RESTParameterSpec + include ClassUtilMixin + attr_accessor :name, :type, :required + def required?; @required; end + end +end diff --git a/lib/twitter/ext.rb b/lib/twitter/ext.rb new file mode 100644 index 0000000..92edbd1 --- /dev/null +++ b/lib/twitter/ext.rb @@ -0,0 +1,2 @@ + +require_local('twitter/ext/stdlib') diff --git a/lib/twitter/ext/stdlib.rb b/lib/twitter/ext/stdlib.rb new file mode 100644 index 0000000..1bd7975 --- /dev/null +++ b/lib/twitter/ext/stdlib.rb @@ -0,0 +1,32 @@ +# Contains Ruby standard library extensions specific to Twitter4R library. + +# Extension to Hash to create URL encoded string from key-values +class Hash + # Returns string formatted for HTTP URL encoded name-value pairs. + # For example, + # {:id => 'thomas_hardy'}.to_http_str + # # => "id=thomas_hardy" + # {:id => 23423, :since => Time.now}.to_http_str + # # => "since=Thu,%2021%20Jun%202007%2012:10:05%20-0500&id=23423" + def to_http_str + result = '' + return result if self.empty? + self.each do |key, val| + result << "#{key}=#{URI.encode(val.to_s)}&" + end + result.chop # remove the last '&' character, since it can be discarded + end +end + +# Extension to Time that outputs RFC2822 compliant string on #to_s +class Time + alias :old_to_s :to_s + # Returns RFC2822 compliant string for Time object. + # For example, + # # Tony Blair's last day in office (hopefully) + # best_day_ever = Time.local(2007, 6, 27) + # best_day_ever.to_s # => "Wed, 27 Jun 2007 00:00:00 +0100" + def to_s + self.rfc2822 + end +end diff --git a/lib/twitter/extras.rb b/lib/twitter/extras.rb new file mode 100644 index 0000000..c1b39a4 --- /dev/null +++ b/lib/twitter/extras.rb @@ -0,0 +1,39 @@ +# extra.rb contains features that are not considered part of the core library. +# This file is not imported by doing require('twitter'), so you will +# need to import this file separately like: +# require('twitter') +# require('twitter/extras') + +require('twitter') + +class Twitter::Client + @@FEATURED_URIS = { + :users => 'http://twitter.com/statuses/featured.json' + } + + # Provides access to the Featured Twitter API. + # + # Currently the only value for type accepted is :users, + # which will return an Array of blessed Twitter::User objects that + # represent Twitter's featured users. + def featured(type) + uri = @@FEATURED_URIS[type] + response = http_connect {|conn| create_http_get_request(uri) } + bless_models(Twitter::User.unmarshal(response.body)) + end +end + +class Twitter::User + class << self + # Provides access to the Featured Twitter API via the Twitter4R Model + # interface. + # + # The following lines of code are equivalent to each other: + # users1 = Twitter::User.features(client) + # users2 = client.featured(:users) + # where users1 and users2 would be logically equivalent. + def featured(client) + client.featured(:users) + end + end +end diff --git a/lib/twitter/meta.rb b/lib/twitter/meta.rb new file mode 100644 index 0000000..3f9968a --- /dev/null +++ b/lib/twitter/meta.rb @@ -0,0 +1,56 @@ +# meta.rb contains Twitter::Meta and related classes that +# help define the metadata of the Twitter4R project. + +require('rubygems') +require('erb') + +class Twitter::Meta #:nodoc: + attr_accessor :root_dir + attr_reader :gem_spec, :project_files, :spec_files + + # Initializer for Twitter::Meta class. Takes root_dir as parameter. + def initialize(root_dir) + @root_dir = root_dir + end + + # Returns package information defined in root_dir/pkg-info.yml + def pkg_info + yaml_file = File.join(@root_dir, 'pkg-info.yml') + ryaml = ERB.new(File.read(yaml_file), 0) + s = ryaml.result(binding) + YAML.load(s) + end + + # Returns RubyGems spec information + def spec_info + self.pkg_info['spec'] if self.pkg_info + end + + # Returns list of project files + def project_files + @project_files ||= Dir.glob(File.join(@root_dir, 'lib/**/*.rb')) + @project_files + end + + # Returns list of specification files + def spec_files + @spec_files ||= Dir.glob(File.join(@root_dir, 'spec/**/*.rb')) + @spec_files + end + + # Returns RubyGem specification for Twitter4R project + def gem_spec + @gem_spec ||= Gem::Specification.new do |spec| + self.spec_info.each do |key, val| + if val.is_a?(Hash) + val.each do |k, v| + spec.send(key, k, v) + end + else + spec.send("#{key}=", val) + end + end + end + @gem_spec + end +end diff --git a/lib/twitter/model.rb b/lib/twitter/model.rb new file mode 100644 index 0000000..a285e79 --- /dev/null +++ b/lib/twitter/model.rb @@ -0,0 +1,331 @@ +# Contains Twitter4R Model API. + +module Twitter + # Mixin module for model classes. Includes generic class methods like + # unmarshal. + # + # To create a new model that includes this mixin's features simply: + # class NewModel + # include Twitter::ModelMixin + # end + # + # This mixin module automatically includes Twitter::ClassUtilMixin + # features. + # + # The contract for models to use this mixin correctly is that the class + # including this mixin must provide an class method named attributes + # that will return an Array of attribute symbols that will be checked + # in #eql? override method. The following would be sufficient: + # def self.attributes; @@ATTRIBUTES; end + module ModelMixin #:nodoc: + def self.included(base) #:nodoc: + base.send(:include, Twitter::ClassUtilMixin) + base.send(:include, InstanceMethods) + base.extend(ClassMethods) + end + + # Class methods defined for Twitter::ModelMixin module. + module ClassMethods #:nodoc: + # Unmarshal object singular or plural array of model objects + # from JSON serialization. Currently JSON is only supported + # since this is all Twitter4R needs. + def unmarshal(raw) + input = JSON.parse(raw) + def unmarshal_model(hash) + self.new(hash) + end + return unmarshal_model(input) if input.is_a?(Hash) # singular case + result = [] + input.each do |hash| + model = unmarshal_model(hash) if hash.is_a?(Hash) + result << model + end if input.is_a?(Array) + result # plural case + end + end + + # Instance methods defined for Twitter::ModelMixin module. + module InstanceMethods #:nodoc: + attr_accessor :client + # Equality method override of Object#eql? default. + # + # Relies on the class using this mixin to provide a attributes + # class method that will return an Array of attributes to check are + # equivalent in this #eql? override. + # + # It is by design that the #eql? method will raise a NoMethodError + # if no attributes class method exists, to alert you that + # you must provide it for a meaningful result from this #eql? override. + # Otherwise this will return a meaningless result. + def eql?(other) + attrs = self.class.attributes + attrs.each do |att| + return false unless self.send(att).eql?(other.send(att)) + end + true + end + + # Returns integer representation of model object instance. + # + # For example, + # status = Twitter::Status.new(:id => 234343) + # status.to_i #=> 234343 + def to_i + @id + end + + # Returns string representation of model object instance. + # + # For example, + # status = Twitter::Status.new(:text => 'my status message') + # status.to_s #=> 'my status message' + # + # If a model class doesn't have a @text attribute defined + # the default Object#to_s will be returned as the result. + def to_s + self.respond_to?(:text) ? @text : super.to_s + end + + # Returns hash representation of model object instance. + # + # For example, + # u = Twitter::User.new(:id => 2342342, :screen_name => 'tony_blair_is_the_devil') + # u.to_hash #=> {:id => 2342342, :screen_name => 'tony_blair_is_the_devil'} + # + # This method also requires that the class method attributes be + # defined to return an Array of attributes for the class. + def to_hash + attrs = self.class.attributes + result = {} + attrs.each do |att| + value = self.send(att) + value = value.to_hash if value.respond_to?(:to_hash) + result[att] = value if value + end + result + end + + # "Blesses" model object. + # + # Should be overridden by model class if special behavior is expected + # + # Expected to return blessed object (usually self) + def bless(client) + self.basic_bless(client) + end + + protected + # Basic "blessing" of model object + def basic_bless(client) + self.client = client + self + end + end + end + + module AuthenticatedUserMixin + def self.included(base) + base.send(:include, InstanceMethods) + end + + module InstanceMethods + # Returns an Array of user objects that represents the authenticated + # user's friends on Twitter. + def followers + @client.my(:followers) + end + + # Adds given user as a friend. Returns user object as given by + # Twitter REST server response. + # + # For user argument you may pass in the unique integer + # user ID, screen name or Twitter::User object representation. + def befriend(user) + @client.friend(:add, user) + end + + # Removes given user as a friend. Returns user object as given by + # Twitter REST server response. + # + # For user argument you may pass in the unique integer + # user ID, screen name or Twitter::User object representation. + def defriend(user) + @client.friend(:remove, user) + end + end + end + + # Represents a Twitter user + class User + include ModelMixin + @@ATTRIBUTES = [:id, :name, :description, :location, :screen_name, :url, :profile_image_url, :protected] + attr_accessor *@@ATTRIBUTES + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + + # Returns user model object with given id using the configuration + # and credentials of the client object passed in. + # + # You can pass in either the user's unique integer ID or the user's + # screen name. + def find(id, client) + client.user(id) + end + end + + # Override of ModelMixin#bless method. + # + # Adds #followers instance method when user object represents + # authenticated user. Otherwise just do basic bless. + # + # This permits applications using Twitter4R to write + # Rubyish code like this: + # followers = user.followers if user.is_me? + # Or: + # followers = user.followers if user.respond_to?(:followers) + def bless(client) + basic_bless(client) + self.instance_eval(%{ + self.class.send(:include, Twitter::AuthenticatedUserMixin) + }) if self.is_me? and not self.respond_to?(:followers) + self + end + + # Returns whether this Twitter::User model object + # represents the authenticated user of the client + # that blessed it. + def is_me? + # TODO: Determine whether we should cache this or not? + # Might be dangerous to do so, but do we want to support + # the edge case where this would cause a problem? i.e. + # changing authenticated user after initial use of + # authenticated API. + # TBD: To cache or not to cache. That is the question! + # Since this is an implementation detail we can leave this for + # subsequent 0.2.x releases. It doesn't have to be decided before + # the 0.2.0 launch. + @screen_name == @client.instance_eval("@login") + end + + # Returns an Array of user objects that represents the authenticated + # user's friends on Twitter. + def friends + @client.user(@id, :friends) + end + end # User + + # Represents a status posted to Twitter by a Twitter user. + class Status + include ModelMixin + @@ATTRIBUTES = [:id, :text, :created_at, :user] + attr_accessor *@@ATTRIBUTES + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + + # Returns status model object with given status using the + # configuration and credentials of the client object passed in. + def find(id, client) + client.status(:get, id) + end + + # Creates a new status for the authenticated user of the given + # client context. + # + # You MUST include a valid/authenticated client context + # in the given params argument. + # + # For example: + # status = Twitter::Status.create( + # :text => 'I am shopping for flip flops', + # :client => client) + # + # An ArgumentError will be raised if no valid client context + # is given in the params Hash. For example, + # status = Twitter::Status.create(:text => 'I am shopping for flip flops') + # The above line of code will raise an ArgumentError. + # + # The same is true when you do not provide a :text key-value + # pair in the params argument given. + # + # The Twitter::Status object returned after the status successfully + # updates on the Twitter server side is returned from this method. + def create(params) + client, text = params[:client], params[:text] + raise ArgumentError, 'Valid client context must be provided' unless client.is_a?(Twitter::Client) + raise ArgumentError, 'Must provide text for the status to update' unless text.is_a?(String) + client.status(:post, text) + end + end + + protected + # Constructor callback + def init + @user = User.new(@user) if @user.is_a?(Hash) + @created_at = Time.parse(@created_at) if @created_at.is_a?(String) + end + end # Status + + # Represents a direct message on Twitter between Twitter users. + class Message + include ModelMixin + @@ATTRIBUTES = [:id, :recipient, :sender, :text, :created_at] + attr_accessor *@@ATTRIBUTES + + class << self + # Used as factory method callback + def attributes; @@ATTRIBUTES; end + + # Raises NotImplementedError because currently + # Twitter doesn't provide a facility to retrieve + # one message by unique ID. + def find(id, client) + raise NotImplementedError, 'Twitter has yet to implement a REST API for this. This is not a Twitter4R library limitation.' + end + + # Creates a new direct message from the authenticated user of the + # given client context. + # + # You MUST include a valid/authenticated client context + # in the given params argument. + # + # For example: + # status = Twitter::Message.create( + # :text => 'I am shopping for flip flops', + # :receipient => 'anotherlogin', + # :client => client) + # + # An ArgumentError will be raised if no valid client context + # is given in the params Hash. For example, + # status = Twitter::Status.create(:text => 'I am shopping for flip flops') + # The above line of code will raise an ArgumentError. + # + # The same is true when you do not provide any of the following + # key-value pairs in the params argument given: + # * text - the String that will be the message text to send to user + # * recipient - the user ID, screen_name or Twitter::User object representation of the recipient of the direct message + # + # The Twitter::Message object returned after the direct message is + # successfully sent on the Twitter server side is returned from + # this method. + def create(params) + client, text, recipient = params[:client], params[:text], params[:recipient] + raise ArgumentError, 'Valid client context must be given' unless client.is_a?(Twitter::Client) + raise ArgumentError, 'Message text must be supplied to send direct message' unless text.is_a?(String) + raise ArgumentError, 'Recipient user must be specified to send direct message' unless [Twitter::User, Integer, String].member?(recipient.class) + client.message(:post, text, recipient) + end + end + + protected + # Constructor callback + def init + @sender = User.new(@sender) if @sender.is_a?(Hash) + @recipient = User.new(@recipient) if @recipient.is_a?(Hash) + @created_at = Time.parse(@created_at) if @created_at.is_a?(String) + end + end # Message +end # Twitter diff --git a/lib/twitter/version.rb b/lib/twitter/version.rb new file mode 100644 index 0000000..17b7ac2 --- /dev/null +++ b/lib/twitter/version.rb @@ -0,0 +1,19 @@ +# version.rb contains Twitter::Version that provides helper +# methods related to versioning of the Twitter4R project. + +module Twitter::Version #:nodoc: + MAJOR = 0 + MINOR = 2 + REVISION = 3 + class << self + # Returns X.Y.Z formatted version string + def to_version + "#{MAJOR}.#{MINOR}.#{REVISION}" + end + + # Returns X-Y-Z formatted version name + def to_name + "#{MAJOR}_#{MINOR}_#{REVISION}" + end + end +end diff --git a/pkg-info.yml b/pkg-info.yml new file mode 100644 index 0000000..5c0ad36 --- /dev/null +++ b/pkg-info.yml @@ -0,0 +1,25 @@ +<% require('pathname') %> +spec: + name: twitter4r + version: <%= Twitter::Version.to_version %> + summary: A clean Twitter client API in pure Ruby. Will include Twitter add-ons also in Ruby. + require_path: lib + has_rdoc: true + autorequire: twitter + bindir: bin +# executables: +# - twitter4r + add_dependency: + json: >=0.4.3 + requirements: + - Ruby 1.8.4+ + - json gem, version 0.4.3 or higher + - jcode (for unicode support) + required_ruby_version: >=1.8.2 + author: Susan Potter + email: twitter4r-users@googlegroups.com + homepage: http://twitter4r.rubyforge.org + rubyforge_project: twitter4r + files: <% (self.project_files + self.spec_files).each do |file| %> + - <%= Pathname.new(file).relative_path_from(Pathname.new(@root_dir)) %> +<% end %> diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..ebf7d8b --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,133 @@ +require 'spec' +require 'twitter' +require 'twitter/console' +require 'twitter/extras' + +# Add helper methods here if relevant to multiple _spec.rb files + +# Spec helper that sets attribute att for given objects obj +# and other to given value. +def equalizer(obj, other, att, value) + setter = "#{att}=" + obj.send(setter, value) + other.send(setter, value) +end + +# Spec helper that nil-izes objects passed in +def nilize(*objects) + objects.each {|obj| obj = nil } +end + +# Returns default client context object +def client_context(file = 'config/twitter.yml') + Twitter::Client.from_config(file) +end + +# Spec helper that returns a mocked Twitter::Config object +# with stubbed attributes and attrs for overriding attribute +# values. +def stubbed_twitter_config(config, attrs = {}) + opts = { + :protocol => :ssl, + :host => 'twitter.com', + :port => 443, + :proxy_host => 'proxy.host', + :proxy_port => 8080, + }.merge(attrs) + config.stub!(:protocol).and_return(opts[:protocol]) + config.stub!(:host).and_return(opts[:host]) + config.stub!(:port).and_return(opts[:port]) + config.stub!(:proxy_host).and_return(opts[:proxy_host]) + config.stub!(:proxy_port).and_return(opts[:proxy_port]) + config +end + +def mas_twitter_config(attrs = {}) + config = mock(Twitter::Config) + stubbed_twitter_conf(config, attrs) +end + +# Spec helper that returns the project root directory as absolute path string +def project_root_dir + File.expand_path(File.join(File.dirname(__FILE__), '..')) +end + +# Spec helper that returns stubbed Net::HTTP object +# with given response and obj_stubs. +# The host and port are used to initialize +# the Net::HTTP object. +def stubbed_net_http(response, obj_stubs = {}, host = 'twitter.com', port = 80) + http = Net::HTTP.new(host, port) + Net::HTTP.stub!(:new).and_return(http) + http.stub!(:request).and_return(response) + http +end + +# Spec helper that returns a mocked Net::HTTP object and +# stubs out the request method to return the given +# response +def mas_net_http(response, obj_stubs = {}) + http = mock(Net::HTTP, obj_stubs) + Net::HTTP.stub!(:new).and_return(http) + http.stub!(:request).and_return(response) + http.stub!(:start).and_yield(http) + http.stub!(:use_ssl=) + http.stub!(:verify_mode=) + http +end + +# Spec helper that returns a mocked Net::HTTP::Get object and +# stubs relevant class methods and given obj_stubs +# for endo-specing +def mas_net_http_get(obj_stubs = {}) + request = Spec::Mocks::Mock.new(Net::HTTP::Get) + Net::HTTP::Get.stub!(:new).and_return(request) + obj_stubs.each do |method, value| + request.stub!(method).and_return(value) + end + request +end + +# Spec helper that returns a mocked Net::HTTP::Post object and +# stubs relevant class methods and given obj_stubs +# for endo-specing +def mas_net_http_post(obj_stubs = {}) + request = Spec::Mocks::Mock.new(Net::HTTP::Post) + Net::HTTP::Post.stub!(:new).and_return(request) + obj_stubs.each do |method, value| + request.stub!(method).and_return(value) + end + request +end + +# Spec helper that returns a mocked Net::HTTPResponse object and +# stubs given obj_stubs for endo-specing. +# +def mas_net_http_response(status = :success, + body = '', + obj_stubs = {}) + response = Spec::Mocks::Mock.new(Net::HTTPResponse) + response.stub!(:body).and_return(body) + case status + when :success || 200 + _create_http_response(response, "200", "OK") + when :created || 201 + _create_http_response(response, "201", "Created") + when :redirect || 301 + _create_http_response(response, "301", "Redirect") + when :not_authorized || 403 + _create_http_response(response, "403", "Not Authorized") + when :file_not_found || 404 + _create_http_response(response, "404", "File Not Found") + when :server_error || 500 + _create_http_response(response, "500", "Server Error") + end + response +end + +# Local helper method to DRY up code. +def _create_http_response(mock_response, code, message) + mock_response.stub!(:code).and_return(code) + mock_response.stub!(:message).and_return(message) + mock_response.stub!(:is_a?).and_return(true) if ["200", "201"].member?(code) +end diff --git a/spec/twitter/client/base_spec.rb b/spec/twitter/client/base_spec.rb new file mode 100644 index 0000000..b2e2b10 --- /dev/null +++ b/spec/twitter/client/base_spec.rb @@ -0,0 +1,232 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe "Twitter::Client" do + before(:each) do + @init_hash = { :login => 'user', :password => 'pass' } + end + + it ".new should accept login and password as initializer hash keys and set the values to instance values" do + client = nil + lambda do + client = Twitter::Client.new(@init_hash) + end.should_not raise_error + client.send(:login).should eql(@init_hash[:login]) + client.send(:password).should eql(@init_hash[:password]) + end +end + +describe Twitter::Client, "#http_header" do + before(:each) do + @user_agent = 'myapp' + @application_name = @user_agent + @application_version = '1.2.3' + @application_url = 'http://myapp.url' + Twitter::Client.configure do |conf| + conf.user_agent = @user_agent + conf.application_name = @application_name + conf.application_version = @application_version + conf.application_url = @application_url + end + @expected_headers = { + 'Accept' => 'text/x-json', + 'X-Twitter-Client' => @application_name, + 'X-Twitter-Client-Version' => @application_version, + 'X-Twitter-Client-URL' => @application_url, + 'User-Agent' => "Twitter4R v#{Twitter::Version.to_version} [#{@user_agent}]", + } + @twitter = client_context + # resent @@http_header class variable in Twitter::Client class + Twitter::Client.class_eval("@@http_header = nil") + end + + it "should always return expected HTTP headers" do + headers = @twitter.send(:http_header) + headers.should eql(@expected_headers) + end + + it "should cache HTTP headers Hash in class variable after first invocation" do + cache = Twitter::Client.class_eval("@@http_header") + cache.should be_nil + @twitter.send(:http_header) + cache = Twitter::Client.class_eval("@@http_header") + cache.should_not be_nil + cache.should eql(@expected_headers) + end + + after(:each) do + nilize(@user_agent, @application_name, @application_version, @application_url, @twitter, @expected_headers) + end +end + +describe Twitter::Client, "#create_http_get_request" do + before(:each) do + @uri = '/some/path' + @expected_get_request = mock(Net::HTTP::Get) + @twitter = client_context + @default_header = @twitter.send(:http_header) + end + + it "should create new Net::HTTP::Get object with expected initialization arguments" do + Net::HTTP::Get.should_receive(:new).with(@uri, @default_header).and_return(@expected_get_request) + @twitter.send(:create_http_get_request, @uri) + end + + after(:each) do + nilize(@twitter, @uri, @expected_get_request, @default_header) + end +end + +describe Twitter::Client, "#create_http_post_request" do + before(:each) do + @uri = '/some/path' + @expected_post_request = mock(Net::HTTP::Post) + @twitter = client_context + @default_header = @twitter.send(:http_header) + end + + it "should create new Net::HTTP::Post object with expected initialization arguments" do + Net::HTTP::Post.should_receive(:new).with(@uri, @default_header).and_return(@expected_post_request) + @twitter.send(:create_http_post_request, @uri) + end + + after(:each) do + nilize(@twitter, @uri, @expected_post_request, @default_header) + end +end + +describe Twitter::Client, "#create_http_delete_request" do + before(:each) do + @uri = '/a/stupid/path/that/is/not/restful/since/twitter.com/cannot/do/consistent/restful/apis' + @expected_delete_request = mock(Net::HTTP::Delete) + @twitter = client_context + @default_header = @twitter.send(:http_header) + end + + it "should create new Net::HTTP::Delete object with expected initialization arguments" do + Net::HTTP::Delete.should_receive(:new).with(@uri, @default_header).and_return(@expected_delete_request) + @twitter.send(:create_http_delete_request, @uri) + end + + after(:each) do + nilize(@twitter, @uri, @expected_delete_request, @default_header) + end +end + +describe Twitter::Client, "#http_connect" do + before(:each) do + @request = mas_net_http_get(:basic_auth => nil) + @good_response = mas_net_http_response(:success) + @bad_response = mas_net_http_response(:server_error) + @http_stubs = {:is_a? => true} + @block = Proc.new do |conn| + conn.is_a?(Net::HTTP).should be(true) + @has_yielded = true + @request + end + @twitter = client_context + @has_yielded = false + end + + def generate_bad_response + @http = mas_net_http(@bad_response, @http_stubs) + Net::HTTP.stub!(:new).and_return(@http) + end + + def generate_good_response + @http = mas_net_http(@good_response, @http_stubs) + Net::HTTP.stub!(:new).and_return(@http) + end + + it "should yield HTTP connection when response is good" do + generate_good_response + @http.should_receive(:is_a?).with(Net::HTTP).and_return(true) + lambda do + @twitter.send(:http_connect, &@block) + end.should_not raise_error + @has_yielded.should be(true) + end + + it "should yield HTTP connection when response is bad" do + generate_bad_response + @http.should_receive(:is_a?).with(Net::HTTP).and_return(true) + lambda { + @twitter.send(:http_connect, &@block) + }.should raise_error(Twitter::RESTError) + @has_yielded.should be(true) + end + + after(:each) do + nilize(@good_response, @bad_response, @http) + end +end + +describe Twitter::Client, "#bless_model" do + before(:each) do + @twitter = client_context + @model = Twitter::User.new + end + + it "should recieve #client= message on given model to self" do + @model.should_receive(:client=).with(@twitter) + model = @twitter.send(:bless_model, @model) + end + + it "should set client attribute on given model to self" do + model = @twitter.send(:bless_model, @model) + model.client.should eql(@twitter) + end + + # if model is nil, it doesn't not necessarily signify an exceptional case for this method's usage. + it "should return nil when receiving nil and not raise any exceptions" do + model = @twitter.send(:bless_model, nil) + model.should be_nil + end + + # needed to alert developer that the model needs to respond to #client= messages appropriately. + it "should raise an error if passing in a non-nil object that doesn't not respond to the :client= message" do + lambda { + @twitter.send(:bless_model, Object.new) + }.should raise_error(NoMethodError) + end + + after(:each) do + nilize(@twitter) + end +end + +describe Twitter::Client, "#bless_models" do + before(:each) do + @twitter = client_context + @models = [ + Twitter::Status.new(:text => 'message #1'), + Twitter::Status.new(:text => 'message #2'), + ] + end + + it "should set client attributes for each model in given Array to self" do + models = @twitter.send(:bless_models, @models) + models.each {|model| model.client.should eql(@twitter) } + end + + it "should set client attribute for singular model given to self" do + model = @twitter.send(:bless_models, @models[0]) + model.client.should eql(@twitter) + end + + it "should delegate to bless_model for singular model case" do + model = @models[0] + @twitter.should_receive(:bless_model).with(model).and_return(model) + @twitter.send(:bless_models, model) + end + + it "should return nil when receiving nil and not raise any exceptions" do + lambda { + value = @twitter.send(:bless_models, nil) + value.should be_nil + }.should_not raise_error + end + + after(:each) do + nilize(@twitter, @models) + end +end diff --git a/spec/twitter/client/friendship_spec.rb b/spec/twitter/client/friendship_spec.rb new file mode 100644 index 0000000..162e210 --- /dev/null +++ b/spec/twitter/client/friendship_spec.rb @@ -0,0 +1,76 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe Twitter::Client, "#friend" do + before(:each) do + @twitter = client_context + @id = 1234567 + @screen_name = 'dummylogin' + @friend = Twitter::User.new(:id => @id, :screen_name => @screen_name) + @uris = Twitter::Client.class_eval("@@FRIENDSHIP_URIS") + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success) + @connection = mas_net_http(@response) + Net::HTTP.stub!(:new).and_return(@connection) + Twitter::User.stub!(:unmarshal).and_return(@friend) + end + + def create_uri(action, id) + "#{@uris[action]}/#{id}.json" + end + + it "should create expected HTTP GET request for :add case using integer user ID" do + # the integer user ID scenario... + @twitter.should_receive(:create_http_get_request).with(create_uri(:add, @id)).and_return(@request) + @twitter.friend(:add, @id) + end + + it "should create expected HTTP GET request for :add case using screen name" do + # the screen name scenario... + @twitter.should_receive(:create_http_get_request).with(create_uri(:add, @screen_name)).and_return(@request) + @twitter.friend(:add, @screen_name) + end + + it "should create expected HTTP GET request for :add case using Twitter::User object" do + # the Twitter::User object scenario... + @twitter.should_receive(:create_http_get_request).with(create_uri(:add, @friend.to_i)).and_return(@request) + @twitter.friend(:add, @friend) + end + + it "should create expected HTTP GET request for :remove case using integer user ID" do + # the integer user ID scenario... + @twitter.should_receive(:create_http_get_request).with(create_uri(:remove, @id)).and_return(@request) + @twitter.friend(:remove, @id) + end + + it "should create expected HTTP GET request for :remove case using screen name" do + # the screen name scenario... + @twitter.should_receive(:create_http_get_request).with(create_uri(:remove, @screen_name)).and_return(@request) + @twitter.friend(:remove, @screen_name) + end + + it "should create expected HTTP GET request for :remove case using Twitter::User object" do + # the Twitter::User object scenario... + @twitter.should_receive(:create_http_get_request).with(create_uri(:remove, @friend.to_i)).and_return(@request) + @twitter.friend(:remove, @friend) + end + + it "should bless user model returned for :add case" do + @twitter.should_receive(:bless_model).with(@friend) + @twitter.friend(:add, @friend) + end + + it "should bless user model returned for :remove case" do + @twitter.should_receive(:bless_model).with(@friend) + @twitter.friend(:remove, @friend) + end + + it "should raise ArgumentError if action given is not valid" do + lambda { + @twitter.friend(:crap, @friend) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter, @id, @uris, @request, @response, @connection) + end +end diff --git a/spec/twitter/client/messaging_spec.rb b/spec/twitter/client/messaging_spec.rb new file mode 100644 index 0000000..562876a --- /dev/null +++ b/spec/twitter/client/messaging_spec.rb @@ -0,0 +1,118 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe Twitter::Client, "#messages" do + before(:each) do + @twitter = client_context + @uris = Twitter::Client.class_eval("@@MESSAGING_URIS") + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, "[]") + @connection = mas_net_http(@response) + Net::HTTP.stub!(:new).and_return(@connection) + @messages = [] + Twitter::Message.stub!(:unmarshal).and_return(@messages) + end + + it "should create expected HTTP GET request for :received case" do + @twitter.should_receive(:create_http_get_request).with(@uris[:received]).and_return(@request) + @twitter.messages(:received) + end + + it "should bless the Array returned from Twitter for :received case" do + @twitter.should_receive(:bless_models).with(@messages).and_return(@messages) + @twitter.messages(:received) + end + + it "should create expected HTTP GET request for :sent case" do + @twitter.should_receive(:create_http_get_request).with(@uris[:sent]).and_return(@request) + @twitter.messages(:sent) + end + + it "should bless the Array returned from Twitter for :sent case" do + @twitter.should_receive(:bless_models).with(@messages).and_return(@messages) + @twitter.messages(:sent) + end + + it "should raise an ArgumentError when giving an invalid messaging action" do + lambda { + @twitter.messages(:crap) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter, @uris, @request, @response, @connection, @messages) + end +end + +describe Twitter::Client, "#message" do + before(:each) do + @twitter = client_context + @attributes = { + :id => 34324, + :text => 'Randy, are you coming over later?', + :sender => {:id => 123, :screen_name => 'mylogin'}, + :recipient => {:id => 1234, :screen_name => 'randy'}, + } + @message = Twitter::Message.new(@attributes) + @uris = Twitter::Client.class_eval("@@MESSAGING_URIS") + @request = mas_net_http_get(:basic_auth => nil) + @json = JSON.unparse(@attributes) + @response = mas_net_http_response(:success, @json) + @connection = mas_net_http(@response) + @source = Twitter::Client.class_eval("@@defaults[:source]") + + Net::HTTP.stub!(:new).and_return(@connection) + Twitter::Message.stub!(:unmarshal).and_return(@message) + end + + it "should invoke #http_connect with expected arguments for :post case" do + @twitter.should_receive(:http_connect).with({:text => @message.text, :user => @message.recipient.to_i, :source => @source}.to_http_str).and_return(@response) + @twitter.message(:post, @message.text, @message.recipient) + end + + it "should create expected HTTP POST request for :post case" do + @twitter.should_receive(:create_http_post_request).with(@uris[:post]).and_return(@request) + @twitter.message(:post, @message.text, @message.recipient) + end + + it "should bless returned Twitter::Message object for :post case" do + @twitter.should_receive(:bless_model).with(@message) + @twitter.message(:post, @message.text, @message.recipient) + end + + it "should create expected HTTP DELETE request for :delete case" do + @twitter.should_receive(:create_http_delete_request).with(@uris[:delete], {:id => @message.to_i}).and_return(@request) + @twitter.message(:delete, @message) + end + + it "should bless returned Twitter::Message object for :delete case" do + @twitter.should_receive(:bless_model).with(@message) + @twitter.message(:delete, @message) + end + + it "should invoke #to_i on message object passed in for :delete case" do + @message.should_receive(:to_i).and_return(@message.id) + @twitter.message(:delete, @message) + end + + it "should raise an ArgumentError when giving an invalid messaging action" do + lambda { + @twitter.message(:crap, @message) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError for :post case if user argument is not supplied" do + lambda { + @twitter.message(:post, @message) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError for :post case if user argument is nil" do + lambda { + @twitter.message(:post, @message, nil) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter, @uris, @request, @response, @connection, @sender, @recipient, @message, @attributes) + end +end diff --git a/spec/twitter/client/status_spec.rb b/spec/twitter/client/status_spec.rb new file mode 100644 index 0000000..925088b --- /dev/null +++ b/spec/twitter/client/status_spec.rb @@ -0,0 +1,92 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe Twitter::Client, "#status" do + before(:each) do + @twitter = client_context + @message = 'This is my unique message' + @uris = Twitter::Client.class_eval("@@STATUS_URIS") + @options = {:id => 666666} + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, '{}') + @connection = mas_net_http(@response) + @float = 43.3434 + @status = Twitter::Status.new(:id => 2349343) + @source = Twitter::Client.class_eval("@@defaults[:source]") + end + + it "should return nil if nil is passed as value argument for :get case" do + status = @twitter.status(:get, nil) + status.should be_nil + end + + it "should not call @twitter#http_connect when passing nil for value argument in :get case" do + @twitter.should_not_receive(:http_connect) + @twitter.status(:get, nil) + end + + it "should create expected HTTP GET request for :get case" do + @twitter.should_receive(:create_http_get_request).with(@uris[:get], @options).and_return(@request) + @twitter.status(:get, @options[:id]) + end + + it "should invoke @twitter#create_http_get_request with given parameters equivalent to {:id => value.to_i} for :get case" do + # Float case + @twitter.should_receive(:create_http_get_request).with(@uris[:get], {:id => @float.to_i}).and_return(@request) + @twitter.status(:get, @float) + + # Twitter::Status object case + @twitter.should_receive(:create_http_get_request).with(@uris[:get], {:id => @status.to_i}).and_return(@request) + @twitter.status(:get, @status) + end + + it "should return nil if nil is passed as value argument for :post case" do + status = @twitter.status(:post, nil) + status.should be_nil + end + + it "should not call @twitter#http_connect when passing nil for value argument in :post case" do + @twitter.should_not_receive(:http_connect) + @twitter.status(:post, nil) + end + + it "should create expected HTTP POST request for :post case" do + @twitter.should_receive(:create_http_post_request).with(@uris[:post]).and_return(@request) + @connection.should_receive(:request).with(@request, {:status => @message, :source => @source}.to_http_str).and_return(@response) + @twitter.status(:post, @message) + end + + it "should return nil if nil is passed as value argument for :delete case" do + status = @twitter.status(:delete, nil) + status.should be_nil + end + + it "should not call @twitter#http_connect when passing nil for value argument in :delete case" do + @twitter.should_not_receive(:http_connect) + @twitter.status(:delete, nil) + end + + it "should create expected HTTP DELETE request for :delete case" do + @twitter.should_receive(:create_http_delete_request).with(@uris[:delete], @options).and_return(@request) + @twitter.status(:delete, @options[:id]) + end + + it "should invoke @twitter#create_http_get_request with given parameters equivalent to {:id => value.to_i} for :delete case" do + # Float case + @twitter.should_receive(:create_http_delete_request).with(@uris[:delete], {:id => @float.to_i}).and_return(@request) + @twitter.status(:delete, @float) + + # Twitter::Status object case + @twitter.should_receive(:create_http_delete_request).with(@uris[:delete], {:id => @status.to_i}).and_return(@request) + @twitter.status(:delete, @status) + end + + it "should raise an ArgumentError when given an invalid status action" do + lambda { + @twitter.status(:crap, nil) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter) + end +end diff --git a/spec/twitter/client/timeline_spec.rb b/spec/twitter/client/timeline_spec.rb new file mode 100644 index 0000000..45d8c39 --- /dev/null +++ b/spec/twitter/client/timeline_spec.rb @@ -0,0 +1,79 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe Twitter::Client, "Timeline API" do + before(:each) do + @client = client_context + @uris = Twitter::Client.class_eval("@@TIMELINE_URIS") + @user = Twitter::User.new(:screen_name => 'mylogin') + @status = Twitter::Status.new(:id => 23343443, :text => 'I love Lucy!', :user => @user) + @timeline = [@status] + @json = JSON.unparse([@status.to_hash]) + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, @json) + @connection = mas_net_http(@response) + @params = { + :public => {:since_id => 3249328}, + :friends => {:id => 'myfriend'}, + :user => {:id => 'auser'}, + :me => {}, + } + end + + it "should respond to instance method #timeline_for" do + @client.should respond_to(:timeline_for) + end + + it "should call #http_get with expected parameters for :public case" do + @client.should_receive(:http_connect).and_return(mas_net_http_response(:success, @json)) + @client.timeline_for(:public) + end + + it "should yield to block for each status in timeline" do + @client.should_receive(:http_connect).and_return(mas_net_http_response(:success, @json)) + Twitter::Status.should_receive(:unmarshal).and_return(@timeline) + count = 0 + @client.timeline_for(:public) do |status| + status.should eql(@status) + count += 1 + end + count.should eql(@timeline.size) + end + + it "should generate expected HTTP GET request for generic :public case" do + @client.should_receive(:create_http_get_request).with(@uris[:public], {}).and_return(@request) + timeline = @client.timeline_for(:public) + timeline.should eql(@timeline) + end + + it "should generate expected HTTP GET request for :public case with expected parameters" do + @client.should_receive(:create_http_get_request).with(@uris[:public], @params[:public]).and_return(@request) + timeline = @client.timeline_for(:public, @params[:public]) + timeline.should eql(@timeline) + end + + it "should generate expected HTTP GET request for generic :friends case" do + @client.should_receive(:create_http_get_request).with(@uris[:friends], {}).and_return(@request) + timeline = @client.timeline_for(:friends) + timeline.should eql(@timeline) + end + + it "should generate expected HTTP GET request for :friends case with expected parameters" do + @client.should_receive(:create_http_get_request).with(@uris[:friends], @params[:friends]).and_return(@request) + timeline = @client.timeline_for(:friends, @params[:friends]) + timeline.should eql(@timeline) + end + + it "should raise an ArgumentError if type given is not valid" do + lambda { + @client.timeline_for(:crap) + }.should raise_error(ArgumentError) + + lambda { + @client.timeline_for(:crap, @params[:friends]) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@client) + end +end diff --git a/spec/twitter/client/user_spec.rb b/spec/twitter/client/user_spec.rb new file mode 100644 index 0000000..8e45d38 --- /dev/null +++ b/spec/twitter/client/user_spec.rb @@ -0,0 +1,220 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe Twitter::Client, "#user(id, :followers)" do + before(:each) do + @twitter = client_context + @id = 395783 + end + + it "should raise ArgumentError" do + lambda { + @twitter.user(@id, :followers) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter, @id) + end +end + +describe Twitter::Client, "#user(id, :info)" do + before(:each) do + @twitter = client_context + @id = 395783 + @screen_name = 'boris_johnson_is_funny_as_hell' + @user = Twitter::User.new( + :id => @id, + :screen_name => @screen_name, + :location => 'London' + ) + @json = JSON.unparse(@user.to_hash) + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, @json) + @connection = mas_net_http(@response) + @uris = Twitter::Client.class_eval("@@USER_URIS") + @twitter.stub!(:create_http_get_request).and_return(@request) + Twitter::User.stub!(:unmarshal).and_return(@user) + Net::HTTP.stub!(:new).and_return(@connection) + end + + it "should create expected HTTP GET request when giving numeric user id" do + @twitter.should_receive(:create_http_get_request).with(@uris[:info], {:id => @id}).and_return(@request) + @twitter.user(@id) + end + + it "should create expected HTTP GET request when giving screen name" do + @twitter.should_receive(:create_http_get_request).with(@uris[:info], {:id => @screen_name}).and_return(@request) + @twitter.user(@screen_name) + end + + it "should bless model returned when giving numeric user id" do + @twitter.should_receive(:bless_model).with(@user).and_return(@user) + @twitter.user(@id) + end + + it "should bless model returned when giving screen name" do + @twitter.should_receive(:bless_model).with(@user).and_return(@user) + @twitter.user(@screen_name) + end + + after(:each) do + nilize(@request, @response, @connection, @twitter, @id, @screen_name, @user) + end +end + +# TODO: Add specs for new Twitter::Client#user(id, :friends) and +# Twitter::Client#user(id, :followers) use cases. +describe Twitter::Client, "#user(id, :friends)" do + before(:each) do + @twitter = client_context + @id = 395784 + @screen_name = 'cafe_paradiso' + @user = Twitter::User.new( + :id => @id, + :screen_name => @screen_name, + :location => 'Urbana, IL' + ) + @json = JSON.unparse(@user.to_hash) + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, @json) + @connection = mas_net_http(@response) + @uris = Twitter::Client.class_eval("@@USER_URIS") + @twitter.stub!(:create_http_get_request).and_return(@request) + Twitter::User.stub!(:unmarshal).and_return(@user) + Net::HTTP.stub!(:new).and_return(@connection) + end + + it "should create expected HTTP GET request when giving numeric user id" do + @twitter.should_receive(:create_http_get_request).with(@uris[:friends], {:id => @id}).and_return(@request) + @twitter.user(@id, :friends) + end + + it "should invoke #to_i on Twitter::User objecct given" do + @user.should_receive(:to_i).and_return(@id) + @twitter.user(@user, :friends) + end + + it "should create expected HTTP GET request when giving Twitter::User object" do + @twitter.should_receive(:create_http_get_request).with(@uris[:friends], {:id => @user.to_i}).and_return(@request) + @twitter.user(@user, :friends) + end + + it "should create expected HTTP GET request when giving screen name" do + @twitter.should_receive(:create_http_get_request).with(@uris[:friends], {:id => @screen_name}).and_return(@request) + @twitter.user(@screen_name, :friends) + end + + it "should bless model returned when giving numeric id" do + @twitter.should_receive(:bless_model).with(@user).and_return(@user) + @twitter.user(@id, :friends) + end + + it "should bless model returned when giving Twitter::User object" do + @twitter.should_receive(:bless_model).with(@user).and_return(@user) + @twitter.user(@user, :friends) + end + + it "should bless model returned when giving screen name" do + @twitter.should_receive(:bless_model).with(@user).and_return(@user) + @twitter.user(@screen_name, :friends) + end + + after(:each) do + nilize(@request, @response, @connection, @twitter, @id, @screen_name, @user) + end +end + +describe Twitter::Client, "#my(:info)" do + before(:each) do + @twitter = client_context + @screen_name = @twitter.instance_eval("@login") + @user = Twitter::User.new( + :id => 2394393, + :screen_name => @screen_name, + :location => 'Glamorous Urbana' + ) + @json = JSON.unparse(@user.to_hash) + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, @json) + @connection = mas_net_http(@response) + @uris = Twitter::Client.class_eval("@@USER_URIS") + @twitter.stub!(:create_http_get_request).and_return(@request) + Net::HTTP.stub!(:new).and_return(@connection) + Twitter::User.stub!(:unmarshal).and_return(@user) + end + + it "should create expected HTTP GET request" do + @twitter.should_receive(:create_http_get_request).with(@uris[:info], :id => @screen_name).and_return(@request) + @twitter.my(:info) + end + + it "should bless the model object returned" do + @twitter.should_receive(:bless_models).with(@user).and_return(@user) + @twitter.my(:info) + end + + it "should return expected user object" do + user = @twitter.my(:info) + user.should eql(@user) + end + + after(:each) do + nilize(@request, @response, @connection, @twitter, @user, @screen_name) + end +end + +describe Twitter::Client, "#my(:friends)" do + before(:each) do + @twitter = client_context + @screen_name = @twitter.instance_eval("@login") + @friends = [ + Twitter::User.new(:screen_name => 'lucy_snowe'), + Twitter::User.new(:screen_name => 'jane_eyre'), + Twitter::User.new(:screen_name => 'tess_derbyfield'), + Twitter::User.new(:screen_name => 'elizabeth_jane_newson'), + ] + @json = JSON.unparse(@friends.collect {|f| f.to_hash }) + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success, @json) + @connection = mas_net_http(@response) + @uris = Twitter::Client.class_eval("@@USER_URIS") + @twitter.stub!(:create_http_get_request).and_return(@request) + Twitter::User.stub!(:unmarshal).and_return(@friends) + Net::HTTP.stub!(:new).and_return(@connection) + end + + it "should create expected HTTP GET request" do + @twitter.should_receive(:create_http_get_request).with(@uris[:friends], :id => @screen_name).and_return(@request) + @twitter.my(:friends) + end + + it "should bless models returned" do + @twitter.should_receive(:bless_models).with(@friends).and_return(@friends) + @twitter.my(:friends) + end + + it "should return expected Array of friends" do + friends = @twitter.my(:friends) + friends.should eql(@friends) + end + + after(:each) do + nilize(@request, @response, @connection, @twitter, @friends, @screen_name) + end +end + +describe Twitter::Client, "#my(:invalid_action)" do + before(:each) do + @twitter = client_context + end + + it "should raise ArgumentError for invalid user action" do + lambda { + @twitter.my(:crap) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter) + end +end diff --git a/spec/twitter/client_spec.rb b/spec/twitter/client_spec.rb new file mode 100644 index 0000000..28723b5 --- /dev/null +++ b/spec/twitter/client_spec.rb @@ -0,0 +1,2 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + diff --git a/spec/twitter/config_spec.rb b/spec/twitter/config_spec.rb new file mode 100644 index 0000000..7089921 --- /dev/null +++ b/spec/twitter/config_spec.rb @@ -0,0 +1,86 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe Twitter::Client, ".configure" do + it "should respond to :configure class method" do + Twitter::Client.respond_to?(:configure).should be(true) + end + + it "should not accept calls that do not specify blocks" do + lambda { + Twitter::Client.configure() + }.should raise_error(ArgumentError) + end +end + +describe Twitter::Client, ".configure with mocked @config" do + before(:each) do + @block_invoked = false + @conf_yielded = false + @conf = mock(Twitter::Config) + @block = Proc.new do |conf| + @block_invoked = true + @conf_yielded = true if conf.is_a?(Twitter::Config) + end + Twitter::Config.stub!(:new).and_return(@conf) + end + + it "should not raise an error when passing block" do + lambda { + Twitter::Client.configure(&@block) + }.should_not raise_error + end + + it "should yield a Twitter::Client object to block" do + Twitter::Client.configure(&@block) + @block_invoked.should be(true) + @conf_yielded.should be(true) + end + + after(:each) do + nilize(@block, @block_invoked, @conf, @conf_yielded) + end +end + +describe Twitter::Config, "#eql?" do + before(:each) do + @protocol = :ssl + @host = 'twitter.com' + @port = 443 + @proxy_host = 'myproxy.host' + @proxy_port = 8080 + attrs = { + :protocol => @protocol, + :host => @host, + :port => @port, + :proxy_host => @proxy_host, + :proxy_port => @proxy_port, + } + @obj = stubbed_twitter_config(Twitter::Config.new, attrs) + @other = stubbed_twitter_config(Twitter::Config.new, attrs) + + @different = stubbed_twitter_config(Twitter::Config.new, attrs.merge(:proxy_host => 'different.proxy')) + @same = @obj + end + + it "should return true for two logically equivalent objects" do + @obj.should be_eql(@other) + @other.should be_eql(@obj) + end + + it "should return false for two logically different objects" do + @obj.should_not be_eql(@different) + @different.should_not be_eql(@obj) + @other.should_not be_eql(@different) + @different.should_not be_eql(@other) + end + + it "should return true for references to the same object in memory" do + @obj.should eql(@same) + @same.should eql(@obj) + @other.should eql(@other) + end + + after(:each) do + nilize(@protocol, @host, @port, @proxy_host, @proxy_port, @obj, @other, @different, @same) + end +end diff --git a/spec/twitter/console_spec.rb b/spec/twitter/console_spec.rb new file mode 100644 index 0000000..ddce31a --- /dev/null +++ b/spec/twitter/console_spec.rb @@ -0,0 +1,15 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe Twitter::Client, ".from_config" do + before(:each) do + + end + + it "should load YAML file for instance configuration" do + + end + + after(:each) do + + end +end diff --git a/spec/twitter/core_spec.rb b/spec/twitter/core_spec.rb new file mode 100644 index 0000000..5e032e4 --- /dev/null +++ b/spec/twitter/core_spec.rb @@ -0,0 +1,127 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe "Twitter::ClassUtilMixin mixed-in class" do + before(:each) do + class TestClass + include Twitter::ClassUtilMixin + attr_accessor :var1, :var2, :var3 + end + @init_hash = { :var1 => 'val1', :var2 => 'val2', :var3 => 'val3' } + end + + it "should have Twitter::ClassUtilMixin as an included module" do + TestClass.included_modules.member?(Twitter::ClassUtilMixin).should be(true) + end + + it "should set attributes passed in the hash to TestClass.new" do + test = TestClass.new(@init_hash) + @init_hash.each do |key, val| + test.send(key).should eql(val) + end + end + + it "should not set attributes passed in the hash that are not attributes in TestClass.new" do + test = nil + lambda { test = TestClass.new(@init_hash.merge(:var4 => 'val4')) }.should_not raise_error + test.respond_to?(:var4).should be(false) + end +end + +describe "Twitter::RESTError#to_s" do + before(:each) do + @hash = { :code => 200, :message => 'OK', :uri => 'http://test.host/bla' } + @error = Twitter::RESTError.new(@hash) + @expected_message = "HTTP #{@hash[:code]}: #{@hash[:message]} at #{@hash[:uri]}" + end + + it "should return @expected_message" do + @error.to_s.should eql(@expected_message) + end +end + +describe "Twitter::Status#eql?" do + before(:each) do + @id = 34329594003 + @attr_hash = { :text => 'Status', :id => @id, + :user => { :name => 'Tess', + :description => "Unfortunate D'Urberville", + :location => 'Dorset', + :url => nil, + :id => 34320304, + :screen_name => 'maiden_no_more' }, + :created_at => 'Wed May 02 03:04:54 +0000 2007'} + @obj = Twitter::Status.new @attr_hash + @other = Twitter::Status.new @attr_hash + end + + it "should return true when non-transient object attributes are eql?" do + @obj.should eql(@other) + end + + it "should return false when not all non-transient object attributes are eql?" do + @other.created_at = Time.now.to_s + @obj.should_not eql(@other) + end + + it "should return true when comparing same object to itself" do + @obj.should eql(@obj) + @other.should eql(@other) + end +end + +describe "Twitter::User#eql?" do + before(:each) do + @attr_hash = { :name => 'Elizabeth Jane Newson-Henshard', + :description => "Wronged 'Daughter'", + :location => 'Casterbridge', + :url => nil, + :id => 6748302, + :screen_name => 'mayors_daughter_or_was_she?' } + @obj = Twitter::User.new @attr_hash + @other = Twitter::User.new @attr_hash + end + + it "should return true when non-transient object attributes are eql?" do + @obj.should eql(@other) + end + + it "should return false when not all non-transient object attributes are eql?" do + @other.id = 1 + @obj.should_not eql(@other) + @obj.eql?(@other).should be(false) + end + + it "should return true when comparing same object to itself" do + @obj.should eql(@obj) + @other.should eql(@other) + end +end + +describe "Twitter::ClassUtilMixin#require_block" do + before(:each) do + class TestClass + include Twitter::ClassUtilMixin + end + @test_subject = TestClass.new + end + + it "should respond to :require_block" do + @test_subject.should respond_to(:require_block) + end + + it "should raise ArgumentError when block not given" do + lambda { + @test_subject.send(:require_block, false) + }.should raise_error(ArgumentError) + end + + it "should not raise ArgumentError when block is given" do + lambda { + @test_subject.send(:require_block, true) + }.should_not raise_error(ArgumentError) + end + + after(:each) do + @test_subject = nil + end +end diff --git a/spec/twitter/ext/stdlib_spec.rb b/spec/twitter/ext/stdlib_spec.rb new file mode 100644 index 0000000..8e11e6a --- /dev/null +++ b/spec/twitter/ext/stdlib_spec.rb @@ -0,0 +1,42 @@ +require File.join(File.dirname(__FILE__), '..', '..', 'spec_helper') + +describe Hash, "#to_http_str" do + before(:each) do + @http_params = {:id => 'otherlogin', :since_id => 3953743, :full_name => 'Lucy Cross'} + @id_regexp = Regexp.new("id=#{URI.encode(@http_params[:id].to_s)}") + @since_id_regexp = Regexp.new("since_id=#{URI.encode(@http_params[:since_id].to_s)}") + @full_name_regexp = Regexp.new("full_name=#{URI.encode(@http_params[:full_name].to_s)}") + end + + it "should generate expected URL encoded string" do + http_str = @http_params.to_http_str + http_str.should match(@id_regexp) + http_str.should match(@since_id_regexp) + http_str.should match(@full_name_regexp) + end + + after(:each) do + @http_params = nil + @id_kv_str, @since_id_kv_str, @full_name_kv_str = nil + end +end + +describe Time, "#to_s" do + before(:each) do + @time = Time.now + @expected_string = @time.rfc2822 + end + + it "should output RFC2822 compliant string" do + time_string = @time.to_s + time_string.should eql(@expected_string) + end + + it "should respond to #old_to_s" do + @time.should respond_to(:old_to_s) + end + + after(:each) do + nilize(@time, @expected_string) + end +end diff --git a/spec/twitter/extras_spec.rb b/spec/twitter/extras_spec.rb new file mode 100644 index 0000000..dbb9cf0 --- /dev/null +++ b/spec/twitter/extras_spec.rb @@ -0,0 +1,46 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +describe Twitter::Client, "#featured(:users)" do + before(:each) do + @twitter = client_context + @uris = Twitter::Client.class_eval("@@FEATURED_URIS") + @request = mas_net_http_get(:basic_auth => nil) + @response = mas_net_http_response(:success) + @connection = mas_net_http(@response) + Net::HTTP.stub!(:new).and_return(@connection) + @users = [ + Twitter::User.new(:screen_name => 'twitter4r'), + Twitter::User.new(:screen_name => 'dictionary'), + ] + Twitter::User.stub!(:unmarshal).and_return(@users) + end + + it "should create expected HTTP GET request" do + @twitter.should_receive(:create_http_get_request).with(@uris[:users]).and_return(@request) + @twitter.featured(:users) + end + + it "should bless Twitter::User models returned" do + @twitter.should_receive(:bless_models).with(@users).and_return(@users) + @twitter.featured(:users) + end + + after(:each) do + nilize(@twitter, @uris, @request, @response, @connection) + end +end + +describe Twitter::User, ".featured" do + before(:each) do + @twitter = client_context + end + + it "should delegate #featured(:users) message to given client context" do + @twitter.should_receive(:featured).with(:users).and_return([]) + Twitter::User.featured(@twitter) + end + + after(:each) do + nilize(@twitter) + end +end diff --git a/spec/twitter/meta_spec.rb b/spec/twitter/meta_spec.rb new file mode 100644 index 0000000..9e7e51e --- /dev/null +++ b/spec/twitter/meta_spec.rb @@ -0,0 +1,90 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +def glob_files(*path_elements) + Dir.glob(File.join(*path_elements)) +end + +def load_erb_yaml(path, context) + ryaml = ERB.new(File.read(path), 0) + YAML.load(ryaml.result(context)) +end + +module ERBMetaMixin + # Needed to make the YAML load work... + def project_files + glob_files(@root_dir, 'lib', '**/*.rb') + end + + # Needed to make the YAML load work... + def spec_files + glob_files(@root_dir, 'spec', '**/*.rb') + end +end + +describe "Twitter::Meta cache policy" do + include ERBMetaMixin + before(:each) do + @root_dir = project_root_dir + @meta = Twitter::Meta.new(@root_dir) + @expected_pkg_info = load_erb_yaml(File.join(@root_dir, 'pkg-info.yml'), binding) + @expected_project_files = project_files + @expected_spec_files = spec_files + end + + it "should store value returned from project_files in @project_files after first glob" do + @meta.instance_eval("@project_files").should eql(nil) + @meta.project_files + @meta.instance_eval("@project_files").should eql(@expected_project_files) + @meta.project_files + @meta.instance_eval("@project_files").should eql(@expected_project_files) + end + + it "should store value returned from spec_files in @spec_files after first glob" do + @meta.instance_eval("@spec_files").should eql(nil) + @meta.spec_files + @meta.instance_eval("@spec_files").should eql(@expected_spec_files) + @meta.spec_files + @meta.instance_eval("@spec_files").should eql(@expected_spec_files) + end +end + +describe "Twitter::Meta" do + include ERBMetaMixin + before(:each) do + @root_dir = project_root_dir + @meta = Twitter::Meta.new(@root_dir) + @expected_yaml_hash = load_erb_yaml(File.join(@root_dir, 'pkg-info.yml'), binding) + @expected_project_files = project_files + @expected_spec_files = spec_files + end + + it "should load and return YAML file into Hash object upon #pkg_info call" do + yaml_hash = @meta.pkg_info + yaml_hash.should.eql? @expected_yaml_hash + end + + it "should return the embedded hash responding to key 'spec' of #pkg_info call upon #spec_info call" do + yaml_hash = @meta.spec_info + yaml_hash.should.eql? @expected_yaml_hash['spec'] + end + + it "should return list of files matching ROOT_DIR/lib/**/*.rb upon #project_files call" do + project_files = @meta.project_files + project_files.should.eql? @expected_project_files + end + + it "should return list of files matching ROOT_DIR/spec/**/*.rb upon #spec_files call" do + spec_files = @meta.spec_files + spec_files.should.eql? @expected_spec_files + end + + it "should return Gem specification based on YAML file contents and #project_files and #spec_files return values" do + spec = @meta.gem_spec + expected_spec_hash = @expected_yaml_hash['spec'] + expected_spec_hash.each do |key, val| + unless val.is_a?(Hash) + spec.send(key).should.eql? expected_spec_hash[key] + end + end + end +end diff --git a/spec/twitter/model_spec.rb b/spec/twitter/model_spec.rb new file mode 100644 index 0000000..fe4f240 --- /dev/null +++ b/spec/twitter/model_spec.rb @@ -0,0 +1,488 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +module Test + class Model + include Twitter::ModelMixin + end +end + +describe Twitter::Status, "unmarshaling" do + before(:each) do + @json_hash = { "text" => "Thinking Zipcar is lame...", + "id" => 46672912, + "user" => {"name" => "Angie", + "description" => "TV junkie...", + "location" => "NoVA", + "profile_image_url" => "http:\/\/assets0.twitter.com\/system\/user\/profile_image\/5483072\/normal\/eye.jpg?1177462492", + "url" => nil, + "id" => 5483072, + "protected" => false, + "screen_name" => "ang_410"}, + "created_at" => "Wed May 02 03:04:54 +0000 2007"} + @user = Twitter::User.new @json_hash["user"] + @status = Twitter::Status.new @json_hash + @status.user = @user + end + + it "should respond to unmarshal class method" do + Twitter::Status.should respond_to(:unmarshal) + end + + it "should return expected Twitter::Status object for singular case" do + status = Twitter::Status.unmarshal(JSON.unparse(@json_hash)) + status.should_not be(nil) + status.should eql(@status) + end + + it "should return expected array of Twitter::Status objects for plural case" do + statuses = Twitter::Status.unmarshal(JSON.unparse([@json_hash])) + statuses.should_not be(nil) + statuses.should have(1).entries + statuses.first.should eql(@status) + end +end + +describe Twitter::User, "unmarshaling" do + before(:each) do + @json_hash = { "name" => "Lucy Snowe", + "description" => "School Mistress Entrepreneur", + "location" => "Villette", + "url" => "http://villetteschoolforgirls.com", + "id" => 859303, + "protected" => true, + "screen_name" => "LucyDominatrix", } + @user = Twitter::User.new @json_hash + end + + it "should respond to unmarshal class method" do + Twitter::User.should respond_to(:unmarshal) + end + + it "should return expected arry of Twitter::User objects for plural case" do + users = Twitter::User.unmarshal(JSON.unparse([@json_hash])) + users.should have(1).entries + users.first.should eql(@user) + end + + it "should return expected Twitter::User object for singular case" do + user = Twitter::User.unmarshal(JSON.unparse(@json_hash)) + user.should_not be(nil) + user.should eql(@user) + end +end + +describe "Twitter::ModelMixin#to_hash" do + before(:all) do + class Model + include Twitter::ModelMixin + @@ATTRIBUTES = [:id, :name, :value, :unused_attr] + attr_accessor *@@ATTRIBUTES + def self.attributes; @@ATTRIBUTES; end + end + + class Hash + def eql?(other) + return false unless other # trivial nil case. + return false unless self.keys.eql?(other.keys) + self.each do |key,val| + return false unless self[key].eql?(other[key]) + end + true + end + end + end + + before(:each) do + @attributes = {:id => 14, :name => 'State', :value => 'Illinois'} + @model = Model.new(@attributes) + end + + it "should return expected hash representation of given model object" do + @model.to_hash.should eql(@attributes) + end + + after(:each) do + nilize(@attributes, @model) + end +end + +describe Twitter::User, ".find" do + before(:each) do + @twitter = Twitter::Client.from_config 'config/twitter.yml' + @id = 2423423 + @screen_name = 'ascreenname' + @expected_user = Twitter::User.new(:id => @id, :screen_name => @screen_name) + end + + it "should invoke given Twitter::Client's #user method with expected arguments" do + # case where id => @id + @twitter.should_receive(:user).with(@id).and_return(@expected_user) + user = Twitter::User.find(@id, @twitter) + user.should eql(@expected_user) + + # case where id => @screen_name, which is also valid + @twitter.should_receive(:user).with(@screen_name).and_return(@expected_user) + user = Twitter::User.find(@screen_name, @twitter) + user.should eql(@expected_user) + end + + after(:each) do + nilize(@twitter, @id, @screen_name, @expected_user) + end +end + +describe Twitter::Status, ".find" do + before(:each) do + @twitter = Twitter::Client.from_config 'config/twitter.yml' + @id = 9439843 + @text = 'My crummy status message' + @user = Twitter::User.new(:id => @id, :screen_name => @screen_name) + @expected_status = Twitter::Status.new(:id => @id, :text => @text, :user => @user) + end + + it "should invoke given Twitter::Client's #status method with expected arguments" do + @twitter.should_receive(:status).with(:get, @id).and_return(@expected_status) + status = Twitter::Status.find(@id, @twitter) + status.should eql(@expected_status) + end + + after(:each) do + nilize(@twitter, @id, @text, @user, @expected_status) + end +end + +describe Test::Model, "#bless" do + before(:each) do + @twitter = Twitter::Client.from_config('config/twitter.yml') + @model = Test::Model.new + end + + it "should delegate to #basic_bless" do + @model.should_receive(:basic_bless).and_return(@twitter) + @model.bless(@twitter) + end + + it "should set client attribute of self" do + @model.should_receive(:client=).once + @model.bless(@twitter) + end + + after(:each) do + nilize(@model, @twitter) + end +end + +describe Twitter::User, "#is_me?" do + before(:each) do + @twitter = Twitter::Client.from_config('config/twitter.yml') + @user_not_me = Twitter::User.new(:screen_name => 'notmylogin') + @user_me = Twitter::User.new(:screen_name => @twitter.instance_eval("@login")) + @user_not_me.bless(@twitter) + @user_me.bless(@twitter) + end + + it "should return true when Twitter::User object represents authenticated user of client context" do + @user_me.is_me?.should be_true + end + + it "should return false when Twitter::User object does not represent authenticated user of client context" do + @user_not_me.is_me?.should be_false + end + + after(:each) do + nilize(@twitter, @user_not_me, @user_me) + end +end + +describe Twitter::User, "#bless(client)" do + before(:each) do + @twitter = Twitter::Client.from_config('config/twitter.yml') + @user_not_me = Twitter::User.new(:screen_name => 'notmylogin') + @user_me = Twitter::User.new(:screen_name => @twitter.instance_eval("@login")) + end + + it "should add a followers method" do + @user_me.should_not respond_to?(:followers) + @user_me.bless(@twitter) + @user_me.should respond_to?(:followers) + end + + it "should not add a followers method" do + @user_not_me.should_not respond_to?(:followers) + @user_not_me.bless(@twitter) + @user_not_me.should_not respond_to?(:followers) + end + + after(:each) do + nilize(@twitter, @user_not_me, @user_me) + end +end + +describe Twitter::User, "#friends" do + before(:each) do + @twitter = Twitter::Client.from_config('config/twitter.yml') + @id = 5701682 + @user = Twitter::User.new(:id => @id, :screen_name => 'twitter4r') + @user.bless(@twitter) + end + + it "should delegate to @client.user(@id, :friends)" do + @twitter.should_receive(:user).with(@id, :friends) + @user.friends + end + + after(:each) do + nilize(@twitter, @id, @user) + end +end + +describe Twitter::User, "#followers" do + before(:each) do + @twitter = Twitter::Client.from_config('config/twitter.yml') + @id = 5701682 + @user = Twitter::User.new(:id => @id, :screen_name => 'twitter4r') + @user.bless(@twitter) + end + + it "should delegate to @client.my(:followers)" do + @twitter.should_receive(:my).with(:followers) + @user.followers + end + + after(:each) do + nilize(@twitter, @id, @user) + end +end + +describe Test::Model, "#to_i" do + before(:each) do + @id = 234324285 + class Test::Model + attr_accessor :id + end + @model = Test::Model.new(:id => @id) + end + + it "should return @id attribute" do + @model.to_i.should eql(@id) + end + + after(:each) do + nilize(@model, @id) + end +end + +describe Test::Model, "#to_s" do + before(:each) do + class Test::Model + attr_accessor :text + end + @text = 'Some text for the message body here' + @model = Test::Model.new(:text => @text) + end + + it "should return expected text when a @text attribute exists for the model" do + @model.to_s.should eql(@text) + end + + after(:each) do + nilize(@model) + end +end + +describe Twitter::Message, ".find" do + it "should raise NotImplementedError due to Twitter (as opposed to Twitter4R) API limitation" do + lambda { + Twitter::Message.find(123, nil) + }.should raise_error(NotImplementedError) + end +end + +describe Twitter::Status, ".create" do + before(:each) do + @twitter = client_context + @text = 'My status update' + @status = Twitter::Status.new(:text => @text, :client => @twitter) + end + + it "should invoke #status(:post, text) on client context given" do + @twitter.should_receive(:status).with(:post, @text).and_return(@status) + Twitter::Status.create(:text => @text, :client => @twitter) + end + + it "should raise an ArgumentError when no client is given in params" do + lambda { + Twitter::Status.create(:text => @text) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError when no text is given in params" do + @twitter.should_receive(:is_a?).with(Twitter::Client) + lambda { + Twitter::Status.create(:client => @twitter) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError when text given in params is not a String" do + lambda { + Twitter::Status.create(:client => @twitter, :text => 234493) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError when client context given in params is not a Twitter::Client object" do + lambda { + Twitter::Status.create(:client => 'a string instead of a Twitter::Client', :text => @text) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter, @text, @status) + end +end + +describe Twitter::Message, ".create" do + before(:each) do + @twitter = client_context + @text = 'Just between you and I, Lantana and Gosford Park are two of my favorite movies' + @recipient = Twitter::User.new(:id => 234958) + @message = Twitter::Message.new(:text => @text, :recipient => @recipient) + end + + it "should invoke #message(:post, text, recipient) on client context given" do + @twitter.should_receive(:message).with(:post, @text, @recipient).and_return(@message) + Twitter::Message.create(:client => @twitter, :text => @text, :recipient => @recipient) + end + + it "should raise an ArgumentError if no client context is given in params" do + lambda { + Twitter::Message.create(:text => @text, :recipient => @recipient) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError if client conext given in params is not a Twitter::Client object" do + lambda { + Twitter::Message.create( + :client => 3.14159, + :text => @text, + :recipient => @recipient) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError if no text is given in params" do + @twitter.should_receive(:is_a?).with(Twitter::Client) + lambda { + Twitter::Message.create( + :client => @twitter, + :recipient => @recipient) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError if text given in params is not a String" do + @twitter.should_receive(:is_a?).with(Twitter::Client) + lambda { + Twitter::Message.create( + :client => @twitter, + :text => Object.new, + :recipient => @recipient) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError if no recipient is given in params" do + @text.should_receive(:is_a?).with(String) + lambda { + Twitter::Message.create( + :client => @twitter, + :text => @text) + }.should raise_error(ArgumentError) + end + + it "should raise an ArgumentError if recipient given in params is not a Twitter::User, Integer or String object" do + @text.should_receive(:is_a?).with(String) + lambda { + Twitter::Message.create( + :client => @twitter, + :text => @text, + :recipient => 3.14159) + }.should raise_error(ArgumentError) + end + + after(:each) do + nilize(@twitter, @text, @recipient, @message) + end +end + +describe Twitter::User, "#befriend" do + before(:each) do + @twitter = client_context + @user = Twitter::User.new( + :id => 1234, + :screen_name => 'mylogin', + :client => @twitter) + @friend = Twitter::User.new( + :id => 5678, + :screen_name => 'friend', + :client => @twitter) + end + + it "should invoke #friend(:add, user) on client context" do + @twitter.should_receive(:friend).with(:add, @friend).and_return(@friend) + @user.befriend(@friend) + end + + after(:each) do + nilize(@twitter, @user, @friend) + end +end + +describe Twitter::User, "#defriend" do + before(:each) do + @twitter = client_context + @user = Twitter::User.new( + :id => 1234, + :screen_name => 'mylogin', + :client => @twitter) + @friend = Twitter::User.new( + :id => 5678, + :screen_name => 'friend', + :client => @twitter) + end + + it "should invoke #friend(:remove, user) on client context" do + @twitter.should_receive(:friend).with(:remove, @friend).and_return(@friend) + @user.defriend(@friend) + end + + after(:each) do + nilize(@twitter, @user, @friend) + end +end + +describe Twitter::Status, "#to_s" do + before(:each) do + @text = 'Aloha' + @status = Twitter::Status.new(:text => @text) + end + + it "should render text attribute" do + @status.to_s.should be(@text) + end + + after(:each) do + nilize(@text, @status) + end +end + +describe Twitter::Message, "#to_s" do + before(:each) do + @text = 'Aloha' + @message = Twitter::Message.new(:text => @text) + end + + it "should render text attribute" do + @message.to_s.should be(@text) + end + + after(:each) do + nilize(@text, @message) + end +end diff --git a/spec/twitter/version_spec.rb b/spec/twitter/version_spec.rb new file mode 100644 index 0000000..075c792 --- /dev/null +++ b/spec/twitter/version_spec.rb @@ -0,0 +1,19 @@ +require File.join(File.dirname(__FILE__), '..', 'spec_helper') + +VERSION_LIST = [Twitter::Version::MAJOR, Twitter::Version::MINOR, Twitter::Version::REVISION] + +EXPECTED_VERSION = VERSION_LIST.join('.') +EXPECTED_NAME = VERSION_LIST.join('_') + +describe Twitter::Version, ".to_version" do + it "should return #{EXPECTED_VERSION}" do + Twitter::Version.to_version.should eql(EXPECTED_VERSION) + end +end + +describe Twitter::Version, ".to_name" do + it "should return #{EXPECTED_NAME}" do + Twitter::Version.to_name.should eql(EXPECTED_NAME) + end +end + diff --git a/tasks/clean.rake b/tasks/clean.rake new file mode 100644 index 0000000..e43c457 --- /dev/null +++ b/tasks/clean.rake @@ -0,0 +1,4 @@ +task :clobber do + rm_rf 'doc/output' +end + diff --git a/tasks/doc.rake b/tasks/doc.rake new file mode 100644 index 0000000..91f0e1e --- /dev/null +++ b/tasks/doc.rake @@ -0,0 +1,14 @@ +require 'rake/rdoctask' + +desc 'Generate RDoc' +Rake::RDocTask.new do |rdoc| + rdoc.rdoc_dir = 'doc/rdoc' + rdoc.title = "Twitter4R v#{Twitter::Version.to_version}: Open Source Ruby Client Library for the Twitter REST API" + rdoc.template = 'config/rdoc_template.rb' + rdoc.options << '--line-numbers' << '--inline-source' << '--main' << 'README' << '--line-numbers' + rdoc.rdoc_files.include('README') + rdoc.rdoc_files.include('CHANGES') + rdoc.rdoc_files.include('MIT-LICENSE') + rdoc.rdoc_files.include('lib/**/*.rb') + rdoc.rdoc_files.include('examples/**/*.rb') +end diff --git a/tasks/find.rake b/tasks/find.rake new file mode 100644 index 0000000..d3cb891 --- /dev/null +++ b/tasks/find.rake @@ -0,0 +1,31 @@ + +def egrep(pattern) + Dir.glob('**/*.rb').each do |name| + count = 0 + open(name) do |f| + while line = f.gets + count += 1 + if line =~ pattern + puts "#{name}:#{count}:#{line}" + end + end + end + end +end + +namespace :find do + desc "Find TODO tags in the code" + task :todos do + egrep /(TODO)/ + end + + desc "Find FIXME tags in the code" + task :fixmes do + egrep /(FIXME)/ + end + + desc "Find TBD tags in the code" + task :tbds do + egrep /(TBD)/ + end +end diff --git a/tasks/pkg.rake b/tasks/pkg.rake new file mode 100644 index 0000000..78cdf58 --- /dev/null +++ b/tasks/pkg.rake @@ -0,0 +1,10 @@ +require('rake/gempackagetask') + +meta = Twitter::Meta.new(File.join(File.dirname(__FILE__), '..')) +namespace :package do + desc "Create Gem Packages" + Rake::GemPackageTask.new(meta.gem_spec) do |pkg| + pkg.need_zip = true + pkg.need_tar = true + end +end diff --git a/tasks/rubyforge.rake b/tasks/rubyforge.rake new file mode 100644 index 0000000..9bba95d --- /dev/null +++ b/tasks/rubyforge.rake @@ -0,0 +1,23 @@ +require('rake/contrib/rubyforgepublisher') + +def rf_publisher + rf_user = ENV['RUBYFORGE_USER'] + publisher = Rake::CompositePublisher.new +# publisher.add(Rake::RubyForgePublisher.new('twitter4r', rf_user)) + publisher.add(Rake::SshDirPublisher.new("#{rf_user}@rubyforge.org", + "/var/www/gforge-projects/twitter4r/", + "doc")) + publisher.add(Rake::SshDirPublisher.new("#{rf_user}@rubyforge.org", + "/var/www/gforge-projects/twitter4r/", + "../web")) + + + publisher +end + +namespace :publish do + task :web do + rf_publisher.upload + end +end + diff --git a/tasks/spec.rake b/tasks/spec.rake new file mode 100644 index 0000000..0f6a369 --- /dev/null +++ b/tasks/spec.rake @@ -0,0 +1,25 @@ +gem 'rspec', '>=1.0.0' +require('spec') +require('spec/rake/spectask') +require('spec/rake/verify_rcov') + +gem 'ZenTest' +require('autotest') +require('autotest/rspec') + +desc "Run specs" +Spec::Rake::SpecTask.new(:spec) do |t| + t.spec_files = 'spec/**/*.rb' + t.spec_opts = ['--color', '--format', 'html'] + t.out = 'doc/spec/index.html' + t.rcov = true + t.rcov_opts = ['--html', '--exclude', "#{ENV['HOME']}/.autotest,spec/"] #, '--xrefs'] + t.rcov_dir = 'doc/rcov' + t.fail_on_error = true +end + +desc "Run specs with coverage verification" +RCov::VerifyTask.new(:coverage => :spec) do |t| + t.threshold = 100 + t.index_html = 'doc/rcov/index.html' +end diff --git a/tasks/stats.rake b/tasks/stats.rake new file mode 100644 index 0000000..13a035e --- /dev/null +++ b/tasks/stats.rake @@ -0,0 +1,45 @@ +require('code_statistics') + +class CodeStatistics + TEST_TYPES = %w(Specs) + + def to_embedded_html + output = "\n" + output << "\t\n" + @pairs.each { |p| output << html_row(p) } + output << "
NameLinesLOCClassesMethodsM/CLOC/M
" + output + end + + private + def html_row(pair) + stats = @statistics[pair.first] + methods = stats["methods"] + classes = stats["classes"] + loc = stats["codelines"] + lines = stats["lines"] + mpc = methods/classes + lpm = loc/methods + + "\t#{pair.first}#{lines}#{loc}#{classes}#{methods}#{mpc}#{lpm}\n" + end +end + +namespace :stats do + STATS_DIRECTORIES = [ + %w(Library\ Code lib), + %w(Specs spec) + ].collect { |name, dir| [ name, "./#{dir}" ] }.select { |name, dir| File.directory?(dir) } + + desc "Report code statistics (KLOCs, etc) for code" + task :default do + verbose = true + CodeStatistics.new(*STATS_DIRECTORIES).to_s + end + + desc "Report code statistics (KLOCs, etc) for code in HTML" + task :html do + puts CodeStatistics.new(*STATS_DIRECTORIES).to_embedded_html + end +end + diff --git a/tasks/web.rake b/tasks/web.rake new file mode 100644 index 0000000..132ae33 --- /dev/null +++ b/tasks/web.rake @@ -0,0 +1,16 @@ +require('rake/contrib/rubyforgepublisher') + +namespace :web do + desc "Build the website, but do not publish it" + task :build => [:clobber, :coverage, 'spec:html', :webgen, :failing_examples_with_html, :examples_specdoc, :rdoc, :rdoc_rails] + + desc "Upload Website to RubyForge" + task :publish => [:verify_user, :website] do + publisher = Rake::SshDirPublisher.new( + "rspec-website@rubyforge.org", + "/var/www/gforge-projects/#{PKG_NAME}", + "../doc/output" + ) + publisher.upload + end +end diff --git a/test/sanity_test.rb b/test/sanity_test.rb new file mode 100755 index 0000000..d176a58 --- /dev/null +++ b/test/sanity_test.rb @@ -0,0 +1,79 @@ +#!/usr/bin/env ruby + +require('twitter') +require('twitter/console') + +version = Twitter::Version.to_version +puts "Sanity testing #{version}" +config_file = File.join(File.dirname(__FILE__), '..', 'config', 'twitter.yml') +twitter = Twitter::Client.from_config(config_file) + +def expect(n, m, message = 'A potential error') + puts "WARNING: #{message}:\n\t => #{m} instead of #{n}" unless n.eql?(m) +end + +puts "Public timeline sanity check" +timeline = twitter.timeline_for(:public) +count = timeline.size +expect 20, count, "Retrieved the last #{count} statuses from the public timeline" + +sleep(5) +puts "Friends timeline sanity check" +timeline = twitter.timeline_for(:friends) +count = timeline.size +expect 20, count, "Retrieved the last #{count} statuses from all my friends' timeline" + +sleep(5) +puts "User timeline sanity check" +timeline = twitter.timeline_for(:user, + :id => 'mbbx6spp', + :count => 5) +count = timeline.size +expect 5, count, "Retrieved the last #{count} statuses from one friend" + +sleep(5) +puts "User lookup sanity check" +screen_name = 'mbbx6spp' +user = twitter.user(screen_name) +expect screen_name, user.screen_name, 'Retrieved a different user' + +sleep(5) +puts "User#friends sanity check" +friends = twitter.user(screen_name, :friends) +expect Array, friends.class, + 'Did not retrieve an Array of users for #user(..., :friends)' + +sleep(5) +puts "My user info sanity check" +followers = twitter.my(:info).followers +expect Array, followers.class, + 'Did not retrieve an Array of users for #my(:followers)' + +sleep(5) +puts "Status posting sanity check" +posted_status = twitter.status(:post, "Testing Twitter4R v#{version} - http://twitter4r.rubyforge.org") +timeline = twitter.timeline_for(:me, :count => 1) +expect Twitter::Status, posted_status.class, 'Did not return newly posted status' +expect 1, timeline.size, 'Did not retrieve only last status' +expect Array, timeline.class, 'Did not retrieve an Array' +expect timeline[0], posted_status, 'Did not successfully post new status' + +sleep(5) +puts "Status retrieval sanity check" +status = twitter.status(:get, posted_status) +expect posted_status, status, 'Did not get proper status' + +sleep(5) +puts "Status deletion sanity check" +deleted_status = twitter.status(:delete, posted_status.id) +expect posted_status, deleted_status, 'Did not delete same status' + +sleep(5) +puts "Direct messaging sanity check" +text = 'This is a test direct message for sanity test script purposes' +message = twitter.message(:post, text, user) +expect text, message.text, + 'Did not post expected text' +expect user.screen_name, message.recipient.screen_name, + 'Did not post to expected recipient' + -- 2.11.4.GIT