From c34b46b95befa3d9b53939115850e1a6ad79ad40 Mon Sep 17 00:00:00 2001 From: Steve Agalloco Date: Thu, 1 Dec 2011 23:07:24 -0500 Subject: [PATCH] initial commit --- .gemtest | 0 .gitignore | 11 + .rspec | 3 + .simplecov | 1 + .yardopts | 3 + Gemfile | 2 + LICENSE.md | 20 ++ README.md | 126 ++++++++ Rakefile | 13 + instapaper.gemspec | 33 ++ lib/faraday/response/raise_http_1xxx.rb | 65 ++++ lib/instapaper.rb | 26 ++ lib/instapaper/authentication.rb | 32 ++ lib/instapaper/client.rb | 43 +++ lib/instapaper/client/account.rb | 13 + lib/instapaper/client/bookmark.rb | 81 +++++ lib/instapaper/client/folder.rb | 34 ++ lib/instapaper/client/user.rb | 14 + lib/instapaper/configuration.rb | 88 ++++++ lib/instapaper/connection.rb | 35 ++ lib/instapaper/request.rb | 22 ++ lib/instapaper/version.rb | 3 + spec/faraday/response_spec.rb | 22 ++ spec/fixtures/access_token.qline | 1 + spec/fixtures/bookmarks_add.json | 1 + spec/fixtures/bookmarks_archive.json | 1 + spec/fixtures/bookmarks_get_text.txt | 299 ++++++++++++++++++ spec/fixtures/bookmarks_list.json | 5 + spec/fixtures/bookmarks_move.json | 1 + spec/fixtures/bookmarks_star.json | 1 + spec/fixtures/bookmarks_unarchive.json | 1 + spec/fixtures/bookmarks_unstar.json | 1 + .../bookmarks_update_read_progress.json | 1 + spec/fixtures/folders_add.json | 1 + spec/fixtures/folders_delete.json | 1 + spec/fixtures/folders_list.json | 1 + spec/fixtures/folders_set_order.json | 1 + spec/fixtures/verify_credentials.json | 1 + spec/instapaper/client/account_spec.rb | 27 ++ spec/instapaper/client/bookmark_spec.rb | 234 ++++++++++++++ spec/instapaper/client/folder_spec.rb | 89 ++++++ spec/instapaper/client/user_spec.rb | 28 ++ spec/instapaper/client_spec.rb | 65 ++++ spec/instapaper_spec.rb | 85 +++++ spec/spec_helper.rb | 44 +++ 45 files changed, 1579 insertions(+) create mode 100644 .gemtest create mode 100644 .gitignore create mode 100644 .rspec create mode 100644 .simplecov create mode 100644 .yardopts create mode 100644 Gemfile create mode 100644 LICENSE.md create mode 100644 README.md create mode 100755 Rakefile create mode 100644 instapaper.gemspec create mode 100644 lib/faraday/response/raise_http_1xxx.rb create mode 100644 lib/instapaper.rb create mode 100644 lib/instapaper/authentication.rb create mode 100644 lib/instapaper/client.rb create mode 100644 lib/instapaper/client/account.rb create mode 100644 lib/instapaper/client/bookmark.rb create mode 100644 lib/instapaper/client/folder.rb create mode 100644 lib/instapaper/client/user.rb create mode 100644 lib/instapaper/configuration.rb create mode 100644 lib/instapaper/connection.rb create mode 100644 lib/instapaper/request.rb create mode 100644 lib/instapaper/version.rb create mode 100644 spec/faraday/response_spec.rb create mode 100644 spec/fixtures/access_token.qline create mode 100644 spec/fixtures/bookmarks_add.json create mode 100644 spec/fixtures/bookmarks_archive.json create mode 100644 spec/fixtures/bookmarks_get_text.txt create mode 100644 spec/fixtures/bookmarks_list.json create mode 100644 spec/fixtures/bookmarks_move.json create mode 100644 spec/fixtures/bookmarks_star.json create mode 100644 spec/fixtures/bookmarks_unarchive.json create mode 100644 spec/fixtures/bookmarks_unstar.json create mode 100644 spec/fixtures/bookmarks_update_read_progress.json create mode 100644 spec/fixtures/folders_add.json create mode 100644 spec/fixtures/folders_delete.json create mode 100644 spec/fixtures/folders_list.json create mode 100644 spec/fixtures/folders_set_order.json create mode 100644 spec/fixtures/verify_credentials.json create mode 100644 spec/instapaper/client/account_spec.rb create mode 100644 spec/instapaper/client/bookmark_spec.rb create mode 100644 spec/instapaper/client/folder_spec.rb create mode 100644 spec/instapaper/client/user_spec.rb create mode 100644 spec/instapaper/client_spec.rb create mode 100644 spec/instapaper_spec.rb create mode 100644 spec/spec_helper.rb diff --git a/.gemtest b/.gemtest new file mode 100644 index 0000000..e69de29 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1af5a9b --- /dev/null +++ b/.gitignore @@ -0,0 +1,11 @@ +*.gem +*.rbc +.DS_Store +.bundle +.rvmrc +.yardoc +Gemfile.lock +coverage/* +doc/* +log/* +pkg/* \ No newline at end of file diff --git a/.rspec b/.rspec new file mode 100644 index 0000000..bb259fe --- /dev/null +++ b/.rspec @@ -0,0 +1,3 @@ +--color +--format=nested +--backtrace diff --git a/.simplecov b/.simplecov new file mode 100644 index 0000000..34fc1b2 --- /dev/null +++ b/.simplecov @@ -0,0 +1 @@ +SimpleCov.start \ No newline at end of file diff --git a/.yardopts b/.yardopts new file mode 100644 index 0000000..dbcb341 --- /dev/null +++ b/.yardopts @@ -0,0 +1,3 @@ +--markup markdown +- +LICENSE.md diff --git a/Gemfile b/Gemfile new file mode 100644 index 0000000..e45e65f --- /dev/null +++ b/Gemfile @@ -0,0 +1,2 @@ +source :rubygems +gemspec diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000..3c817b1 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,20 @@ +Copyright (c) 2011 Steve Agalloco + +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.md b/README.md new file mode 100644 index 0000000..c08fcc7 --- /dev/null +++ b/README.md @@ -0,0 +1,126 @@ +Instapaper +========= +Instapaper is a ruby wrapper for interacting with [Instapaper's Full Developer API](http://www.instapaper.com/api/full). Note that access to the Full API is restricted to Instapaper subscribers only. + +Installation +------------ + gem install instapaper + +Usage +----- + +Instapaper offers full support for all methods exposed through the Full API. + +Configuration +------------- + +Retrieve a list of bookmarks: +```ruby +Instapaper.bookmarks +``` + +Add a new bookmark: +```ruby +Instapaper.add_bookmark('http://someurl.com', :title => 'This is the title', :description => 'This is the description') +``` + +Remove a bookmark: +```ruby +Instapaper.delete_bookmark(bookmark_id) +``` + +Update read progress: +```ruby +Instapaper.update_read_progress(bookmark_id, 0.5) +``` + +Star/Un-star a bookmark: +```ruby +Instapaper.star(bookmark_id) +Instapaper.unstar(bookmark_id) +``` + +Archive/Un-archive a bookmark: +```ruby +Instapaper.archive(bookmark_id) +Instapaper.unarchive(bookmark_id) +``` + +Move a bookmark to a folder: +```ruby +Instapaper.move(bookmark_id, folder_id) +``` + +Obtain the text of a bookmark: +```ruby +Instapaper.text(bookmark_id) +``` + +Bookmark Operations +------------------- + +Folder Operations +----------------- + +To obtain the list of folders: +```ruby +Instapaper.folders +``` + +You can add by passing a name: +```ruby +Instapaper.add_folder('eventmachine') +``` + +And remove folders by referencing a folder by it's id. +```ruby +Instapaper.delete_folder(folder_id) +``` + +Lastly, the folders can be reordered by + + +Other Operations +---------------- + +To obtain an access token via xAuth: +```ruby +Instapaper.access_token(username, password) +``` + +You can also verify credentials once you have received tokens: +```ruby +Instapaper.verify_credentials +``` + +Restrictions +------------ + +Users without an Instapaper Subscription may only invoke the following calls: +```ruby +Instapaper.access_token +Instapaper.verify_credentials +Instapaper.add_bookmark +Instapaper.folders +``` + +Documentation +------------- + +[http://rdoc.info/gems/instapaper](http://rdoc.info/gems/instapaper) + +Note on Patches/Pull Requests +----------------------------- + +* Fork the project. +* Make your feature addition or bug fix. +* Add tests for it. This is important so I don't break it in a + future version unintentionally. +* Commit, do not mess with rakefile, version, or history. + (if you want to have your own version, that is fine but bump version in a commit by itself I can ignore when I pull) +* Send me a pull request. Bonus points for topic branches. + +Copyright +--------- + +Copyright (c) 2011 Steve Agalloco. See [LICENSE](https://github.com/spagalloco/instapaper/blob/master/LICENSE.md) for details. diff --git a/Rakefile b/Rakefile new file mode 100755 index 0000000..d18b887 --- /dev/null +++ b/Rakefile @@ -0,0 +1,13 @@ +#!/usr/bin/env rake + +require 'bundler' +Bundler::GemHelper.install_tasks + +require 'rspec/core/rake_task' +RSpec::Core::RakeTask.new(:spec) + +task :test => :spec +task :default => :spec + +require 'yard' +YARD::Rake::YardocTask.new \ No newline at end of file diff --git a/instapaper.gemspec b/instapaper.gemspec new file mode 100644 index 0000000..fe6c127 --- /dev/null +++ b/instapaper.gemspec @@ -0,0 +1,33 @@ +# -*- encoding: utf-8 -*- +$:.push File.expand_path("../lib", __FILE__) +require "instapaper/version" + +Gem::Specification.new do |gem| + gem.name = "instapaper" + gem.version = Instapaper::VERSION + + gem.author = "Steve Agalloco" + gem.email = "steve.agalloco@gmail.com" + gem.homepage = "https://github.com/spagalloco/instapaper" + gem.summary = %q{Ruby Instapaper Client} + gem.description = %q{Ruby Instapaper Client} + + gem.add_development_dependency('rake', '~> 0.9') + gem.add_development_dependency('rdiscount', '~> 1.6') + gem.add_development_dependency('rspec', '~> 2.7') + gem.add_development_dependency('simplecov', '~> 0.5') + gem.add_development_dependency('yard', '~> 0.7') + gem.add_development_dependency('json', '>= 0') + gem.add_development_dependency('webmock', '~> 1.7') + + gem.add_runtime_dependency('hashie', '~> 1.1.0') + gem.add_runtime_dependency('faraday_middleware', '~> 0.7') + gem.add_runtime_dependency('multi_json', '~> 1.0.3') + gem.add_runtime_dependency('rash', '~> 0.3') + gem.add_runtime_dependency('simple_oauth', '~> 0.1') + + gem.files = `git ls-files`.split("\n") + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") + gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) } + gem.require_paths = ["lib"] +end diff --git a/lib/faraday/response/raise_http_1xxx.rb b/lib/faraday/response/raise_http_1xxx.rb new file mode 100644 index 0000000..58101b3 --- /dev/null +++ b/lib/faraday/response/raise_http_1xxx.rb @@ -0,0 +1,65 @@ +require 'faraday' + +# @private +module Faraday + # @private + class Response::RaiseHttp1xxx < Response::Middleware + def on_complete(env) + case env[:status].to_i + + # general errors + + when 1040 + raise Instapaper::Error.new(error_message(env, "Rate-limit exceeded.")) + when 1041 + raise Instapaper::Error.new(error_message(env, "Subscription account required.")) + when 1042 + raise Instapaper::Error.new(error_message(env, "Application is suspended.")) + + # bookmark errors + + when 1220 + raise Instapaper::Error.new(error_message(env, "Domain requires full content to be supplied.")) + when 1221 + raise Instapaper::Error.new(error_message(env, "Domain has opted out of Instapaper compatibility.")) + when 1240 + raise Instapaper::Error.new(error_message(env, "Invalid URL specified.")) + when 1241 + raise Instapaper::Error.new(error_message(env, "Invalid or missing bookmark_id.")) + when 1242 + raise Instapaper::Error.new(error_message(env, "Invalid or missing folder_id.")) + when 1243 + raise Instapaper::Error.new(error_message(env, "Invalid or missing progress.")) + when 1244 + raise Instapaper::Error.new(error_message(env, "Invalid or missing progress_timestamp.")) + when 1245 + raise Instapaper::Error.new(error_message(env, "Private bookmarks require supplied content.")) + when 1250 + raise Instapaper::Error.new(error_message(env, "Unexpected error when saving bookmark.")) + + # folder errors + + when 1250 + raise Instapaper::Error.new(error_message(env, "Invalid or missing title.")) + when 1251 + raise Instapaper::Error.new(error_message(env, "User already has a folder with this title.")) + when 1252 + raise Instapaper::Error.new(error_message(env, "Cannot add bookmarks to this folder.")) + + # operational errors + + when 1500 + raise Instapaper::Error.new(error_message(env, "Unexpected service error.")) + when 1550 + raise Instapaper::Error.new(error_message(env, "Error generating text version of this URL.")) + + end + end + + private + + def error_message(env, body=nil) + "#{env[:method].to_s.upcase} #{env[:url].to_s}: #{[env[:status].to_s + ':', body].compact.join(' ')}." + end + end +end \ No newline at end of file diff --git a/lib/instapaper.rb b/lib/instapaper.rb new file mode 100644 index 0000000..68b992c --- /dev/null +++ b/lib/instapaper.rb @@ -0,0 +1,26 @@ +require 'instapaper/configuration' +require 'instapaper/client' + +module Instapaper + extend Configuration + + # Alias for Instapaper::Client.new + # + # @return [Instapaper::Client] + def self.client(options={}) + Instapaper::Client.new(options) + end + + # Delegate to Instapaper::Client + def self.method_missing(method, *args, &block) + return super unless client.respond_to?(method) + client.send(method, *args, &block) + end + + def self.respond_to?(method, include_private = false) + client.respond_to?(method, include_private) || super(method, include_private) + end + + # Custom error class for rescuing from all Instapaper errors + class Error < StandardError; end +end diff --git a/lib/instapaper/authentication.rb b/lib/instapaper/authentication.rb new file mode 100644 index 0000000..b035f69 --- /dev/null +++ b/lib/instapaper/authentication.rb @@ -0,0 +1,32 @@ +module Instapaper + # @private + module Authentication + private + + # Authentication hash + # + # @return [Hash] + def authentication + { + :consumer_key => consumer_key, + :consumer_secret => consumer_secret, + :token => oauth_token, + :token_secret => oauth_token_secret + } + end + + def consumer_tokens + { + :consumer_key => consumer_key, + :consumer_secret => consumer_secret + } + end + + # Check whether user is authenticated + # + # @return [Boolean] + def authenticated? + authentication.values.all? + end + end +end diff --git a/lib/instapaper/client.rb b/lib/instapaper/client.rb new file mode 100644 index 0000000..c3b4dcf --- /dev/null +++ b/lib/instapaper/client.rb @@ -0,0 +1,43 @@ +require 'instapaper/connection' +require 'instapaper/request' +require 'instapaper/authentication' + +module Instapaper + # Wrapper for the Instapaper REST API + class Client + # @private + attr_accessor *Configuration::VALID_OPTIONS_KEYS + + alias :api_endpoint :endpoint + alias :api_version :version + + # Creates a new API + def initialize(options={}) + options = Instapaper.options.merge(options) + Configuration::VALID_OPTIONS_KEYS.each do |key| + send("#{key}=", options[key]) + end + end + + def endpoint_with_prefix + api_endpoint + path_prefix + end + + include Connection + include Request + include Authentication + + # Require client method modules after initializing the Client class in + # order to avoid a superclass mismatch error, allowing those modules to be + # Client-namespaced. + require 'instapaper/client/account' + require 'instapaper/client/user' + require 'instapaper/client/bookmark' + require 'instapaper/client/folder' + + include Instapaper::Client::Account + include Instapaper::Client::User + include Instapaper::Client::Bookmark + include Instapaper::Client::Folder + end +end diff --git a/lib/instapaper/client/account.rb b/lib/instapaper/client/account.rb new file mode 100644 index 0000000..27c4417 --- /dev/null +++ b/lib/instapaper/client/account.rb @@ -0,0 +1,13 @@ +module Instapaper + class Client + # Defines methods related to accounts + module Account + + # Returns the currently logged in user. + def verify_credentials + post('account/verify_credentials') + end + + end + end +end diff --git a/lib/instapaper/client/bookmark.rb b/lib/instapaper/client/bookmark.rb new file mode 100644 index 0000000..49458f8 --- /dev/null +++ b/lib/instapaper/client/bookmark.rb @@ -0,0 +1,81 @@ +module Instapaper + class Client + # Defines methods related to bookmarks + module Bookmark + + # Lists the user’s unread bookmarks, and can also synchronize reading positions. + # @option limit: Optional. A number between 1 and 500, default 25. + # @option folder_id: Optional. Possible values are unread (default), starred, archive, or a folder_id value from /api/1/folders/list. + # @option have: Optional. A concatenation of bookmark_id values that the client already has from the specified folder. See below. + def bookmarks(options={}) + post('bookmarks/list', options)[2..-1] + end + + # Updates the user’s reading progress on a single article. + # @param bookmark_id [String] The id of the bookmark to update. + # @param progress [Float] The user’s progress, as a floating-point number between 0.0 and 1.0, defined as the top edge of the user’s current viewport, expressed as a percentage of the article’s total length. + # @param progress_timestamp [Integer] The Unix timestamp value of the time that the progress was recorded. + def update_read_progress(bookmark_id, progress, progress_timestamp=Time.now) + post('bookmarks/update_read_progress', :bookmark_id => bookmark_id, :progress => progress, :progress_timestamp => progress_timestamp.to_i).first + end + + # Adds a new unread bookmark to the user’s account. + # @param url [String] The url of the bookmark. + def add_bookmark(url, options={}) + post('bookmarks/add', options.merge(:url => url)).first + end + + # Permanently deletes the specified bookmark. + # This is NOT the same as Archive. Please be clear to users if you’re going to do this. + # @param bookmark_id [String] The id of the bookmark. + def delete_bookmark(bookmark_id) + post('bookmarks/delete', :bookmark_id => bookmark_id) + end + + # Stars the specified bookmark. + # @param bookmark_id [String] The id of the bookmark. + def star(bookmark_id) + post('bookmarks/star', :bookmark_id => bookmark_id).first + end + alias :star_bookmark :star + + # Un-stars the specified bookmark. + # @param bookmark_id [String] The id of the bookmark. + def unstar(bookmark_id) + post('bookmarks/unstar', :bookmark_id => bookmark_id).first + end + alias :unstar_bookmark :unstar + + # Moves the specified bookmark to the Archive. + # @param bookmark_id [String] The id of the bookmark. + def archive(bookmark_id) + post('bookmarks/archive', :bookmark_id => bookmark_id).first + end + alias :archive_bookmark :archive + + # Moves the specified bookmark to the top of the Unread folder. + # @param bookmark_id [String] The id of the bookmark. + def unarchive(bookmark_id) + post('bookmarks/unarchive', :bookmark_id => bookmark_id).first + end + alias :unarchive_bookmark :unarchive + + # Moves the specified bookmark to a user-created folder. + # @param bookmark_id [String] The id of the bookmark. + # @param folder_id [String] The id of the folder to move the bookmark to. + def move(bookmark_id, folder_id) + post('bookmarks/move', :bookmark_id => bookmark_id, :folder_id => folder_id).first + end + alias :move_bookmark :move + + # Returns the specified bookmark’s processed text-view HTML, which is + # always text/html encoded as UTF-8. + # @param bookmark_id [String] The id of the bookmark. + def text(bookmark_id) + post('bookmarks/get_text', { :bookmark_id => bookmark_id }, true).body + end + alias :get_text :text + + end + end +end diff --git a/lib/instapaper/client/folder.rb b/lib/instapaper/client/folder.rb new file mode 100644 index 0000000..60a862b --- /dev/null +++ b/lib/instapaper/client/folder.rb @@ -0,0 +1,34 @@ +module Instapaper + class Client + # Defines methods related to folders + module Folder + + # List the account’s user-created folders. + # @note This only includes organizational folders and does not include RSS-feed folders or starred-subscription folders + def folders + post('folders/list') + end + + # Creates an organizational folder. + # @param title [String] The title of the folder to create + def add_folder(title) + post('folders/add', :title => title) + end + + # Deletes the folder and moves any articles in it to the Archive. + # @param folder_id [String] The id of the folder. + def delete_folder(folder_id) + post('folders/delete', :folder_id => folder_id) + end + + # Re-orders a user’s folders. + # @param order [Array] An array of folder_id:position pairs joined by commas. + # @example Ordering folder_ids 100, 200, and 300 + # Instapaper.set_order(['100:1','200:2','300:3']) + def set_order(order=[]) + post('folders/set_order', :order => order.join(',')) + end + + end + end +end diff --git a/lib/instapaper/client/user.rb b/lib/instapaper/client/user.rb new file mode 100644 index 0000000..a0c338e --- /dev/null +++ b/lib/instapaper/client/user.rb @@ -0,0 +1,14 @@ +module Instapaper + class Client + # Defines methods related to users + module User + + # Gets an OAuth access token for a user. + def access_token(username, password) + response = post('oauth/access_token', { :x_auth_username => username, :x_auth_password => password, :x_auth_mode => "client_auth"}, true) + Hash[*response.body.split("&").map {|part| part.split("=") }.flatten] + end + + end + end +end diff --git a/lib/instapaper/configuration.rb b/lib/instapaper/configuration.rb new file mode 100644 index 0000000..1c3ad9f --- /dev/null +++ b/lib/instapaper/configuration.rb @@ -0,0 +1,88 @@ +require 'instapaper/version' + +module Instapaper + module Configuration + # An array of valid keys in the options hash when configuring a {Instapaper::API} + VALID_OPTIONS_KEYS = [ + :adapter, + :consumer_key, + :consumer_secret, + :endpoint, + :oauth_token, + :oauth_token_secret, + :proxy, + :version, + :path_prefix, + :user_agent, + :connection_options].freeze + + # The adapter that will be used to connect if none is set + # + # @note The default faraday adapter is Net::HTTP. + DEFAULT_ADAPTER = :net_http + + # By default, don't set an application key + DEFAULT_CONSUMER_KEY = nil + + # By default, don't set an application secret + DEFAULT_CONSUMER_SECRET = nil + + # The endpoint that will be used to connect if none is set + DEFAULT_ENDPOINT = 'https://www.instapaper.com/'.freeze + + # The version of the API. + DEFAULT_VERSION = '1' + + DEFAULT_PATH_PREFIX = 'api/' + DEFAULT_VERSION + '/' + + # By default, don't set a user oauth token + DEFAULT_OAUTH_TOKEN = nil + + # By default, don't set a user oauth secret + DEFAULT_OAUTH_TOKEN_SECRET = nil + + # By default, don't use a proxy server + DEFAULT_PROXY = nil + + # The user agent that will be sent to the API endpoint if none is set + DEFAULT_USER_AGENT = "Instapaper Ruby Gem #{Instapaper::VERSION}".freeze + + DEFAULT_CONNECTION_OPTIONS = {} + + # @private + attr_accessor *VALID_OPTIONS_KEYS + + # When this module is extended, set all configuration options to their default values + def self.extended(base) + base.reset + end + + # Convenience method to allow configuration options to be set in a block + def configure + yield self + end + + # Create a hash of options and their values + def options + options = {} + VALID_OPTIONS_KEYS.each{|k| options[k] = send(k) } + options + end + + # Reset all configuration options to defaults + def reset + self.adapter = DEFAULT_ADAPTER + self.consumer_key = DEFAULT_CONSUMER_KEY + self.consumer_secret = DEFAULT_CONSUMER_SECRET + self.endpoint = DEFAULT_ENDPOINT + self.oauth_token = DEFAULT_OAUTH_TOKEN + self.oauth_token_secret = DEFAULT_OAUTH_TOKEN_SECRET + self.proxy = DEFAULT_PROXY + self.user_agent = DEFAULT_USER_AGENT + self.version = DEFAULT_VERSION + self.path_prefix = DEFAULT_PATH_PREFIX + self.connection_options = DEFAULT_CONNECTION_OPTIONS + self + end + end +end diff --git a/lib/instapaper/connection.rb b/lib/instapaper/connection.rb new file mode 100644 index 0000000..0ad3486 --- /dev/null +++ b/lib/instapaper/connection.rb @@ -0,0 +1,35 @@ +require 'faraday_middleware' +require 'faraday/response/raise_http_1xxx' + +module Instapaper + # @private + module Connection + private + + def connection(raw=false) + merged_options = connection_options.merge({ + :headers => { + 'Accept' => "application/json", + 'User-Agent' => user_agent + }, + :proxy => proxy, + :ssl => {:verify => false}, + :url => api_endpoint + }) + + Faraday.new(merged_options) do |builder| + if authenticated? + builder.use Faraday::Request::OAuth, authentication + else + builder.use Faraday::Request::OAuth, consumer_tokens + end + builder.use Faraday::Request::Multipart + builder.use Faraday::Request::UrlEncoded + builder.use Faraday::Response::Rashify unless raw + builder.use Faraday::Response::ParseJson unless raw + builder.use Faraday::Response::RaiseHttp1xxx + builder.adapter(adapter) + end + end + end +end diff --git a/lib/instapaper/request.rb b/lib/instapaper/request.rb new file mode 100644 index 0000000..d344c56 --- /dev/null +++ b/lib/instapaper/request.rb @@ -0,0 +1,22 @@ +module Instapaper + # Defines HTTP request methods + module Request + + # Perform an HTTP POST request + def post(path, options={}, raw=false) + request(:post, path, options, raw) + end + + private + + # Perform an HTTP request + def request(method, path, options, raw=false) + response = connection(raw).send(method) do |request| + request.path = path_prefix + path + request.body = options unless options.empty? + end + raw ? response : response.body + end + + end +end diff --git a/lib/instapaper/version.rb b/lib/instapaper/version.rb new file mode 100644 index 0000000..29cadb3 --- /dev/null +++ b/lib/instapaper/version.rb @@ -0,0 +1,3 @@ +module Instapaper + VERSION = "0.2.0" +end diff --git a/spec/faraday/response_spec.rb b/spec/faraday/response_spec.rb new file mode 100644 index 0000000..4dd720c --- /dev/null +++ b/spec/faraday/response_spec.rb @@ -0,0 +1,22 @@ +require 'spec_helper' + +describe Faraday::Response do + before do + @client = Instapaper::Client.new + end + + [1040, 1041, 1042, 1220, 1221, 1240, 1241, 1242, 1243, 1244, 1245, 1250, + 1251, 1252, 1500, 1550].each do |status| + context "when HTTP status is #{status}" do + before do + stub_post('folders/list').to_return(:status => status) + end + + it "should raise Instapaper::Error error" do + lambda do + @client.folders + end.should raise_error(Instapaper::Error) + end + end + end +end diff --git a/spec/fixtures/access_token.qline b/spec/fixtures/access_token.qline new file mode 100644 index 0000000..fe3cff7 --- /dev/null +++ b/spec/fixtures/access_token.qline @@ -0,0 +1 @@ +oauth_token=aabbccdd&oauth_token_secret=efgh1234 \ No newline at end of file diff --git a/spec/fixtures/bookmarks_add.json b/spec/fixtures/bookmarks_add.json new file mode 100644 index 0000000..0e38ba2 --- /dev/null +++ b/spec/fixtures/bookmarks_add.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":169529989,"url":"http:\/\/www.fastcodesign.com\/1662169\/ideos-axioms-for-starting-disruptive-new-businesses","title":"Ideo's Axioms for Starting Disruptive New Businesses | Co.Design","description":"www.fastcodesign.com","time":1307586766,"starred":"0","private_source":"","hash":"9GZzaC8U","progress":"0","progress_timestamp":1307585389}] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_archive.json b/spec/fixtures/bookmarks_archive.json new file mode 100644 index 0000000..afd2e9d --- /dev/null +++ b/spec/fixtures/bookmarks_archive.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":169529989,"url":"http:\/\/www.fastcodesign.com\/1662169\/ideos-axioms-for-starting-disruptive-new-businesses","title":"Ideo's Axioms for Starting Disruptive New Businesses | Co.Design","description":"www.fastcodesign.com","time":1306963988,"starred":"0","private_source":"","hash":"v27qHZc2","progress":"0","progress_timestamp":1307145892}] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_get_text.txt b/spec/fixtures/bookmarks_get_text.txt new file mode 100644 index 0000000..954b0c2 --- /dev/null +++ b/spec/fixtures/bookmarks_get_text.txt @@ -0,0 +1,299 @@ + + + + Ideo's Axioms for Starting Disruptive New Businesses + + + + + + + + + +
+ View original +
fastcodesign.com
+
+ +
+
+ +
+ + +
+
+
Back to Fast Company
+
+ + + + +
+ + +
Fast Company
+
+ + + +Ideo's Axioms for Starting Disruptive New Businesses +
+ +
+

Ideo’s Axioms for Starting Disruptive New +Businesses

+ +
+
Don’t wait for perfection: Launch and +learn.
+

This is the first piece in our PATTERNS +series by IDEO. Read more about the series +here.

+

How do you build a business in an unproven market? How do you +figure out what customers need when you’re delivering an +experience they’ve never seen before? You begin where service +and software companies have begun, by conducting fast, cheap +experiments that help you understand your customers. You build on +what you learn. In short, you prototype.

+

With ever-increasing competition, innovative businesses are +finding that in order to stay competitive their offerings need to +constantly evolve. And that to improve their offerings is to +encourage consumer participation. This helps them build a +competitive advantage by constantly revisiting what they deliver +and how they deliver it. They know that traditional market testing +will only validate their past successes. To understand the next big +thing, companies have to engage with customers and react to their +needs.

+ +

TAKE ACTION: Designing for Life’s Changes

+



1. Go early, go often
+Building experimentation into your business is harder than you +think. Start small and stay focused. Try everything, but +don’t try it all in one prototype.

+2. Learning by doing

+Build value for the business as you prototype. If you fail, what +will you have learned? What will you salvage?

+

3. Inspiration through constraint
+Don’t exhaust yourself searching for money and resources. The +tighter your constraints, the more creative your prototypes will +be.

+

4. Open to opportunity
+Look for unanticipated ways customers are using your offering. +Their improvisations may be the future of your business.

+

THE EVIDENCE: Stories from Around the +Globe

+ +Platform for Change
+Companies like Apple and Facebook have learned to harness the +energy of outside developers to create new applications. By +allowing thousands of new applications to run on their platforms, +they create a Darwinian environment where only the fittest survive. +

Jeff, a developer, noticed that Facebook lacked reminders around +birthdays, so he created Birthday Alert. Customers quickly made it +one of the hottest apps on the platform. Instead of trying to guess +what type of functionality users wanted, Facebook just tapped into +smart developers like Jeff who built it for them.

+

Through this process, Facebook learned it needed a mechanism to +foster developers who could improve the overall ecosystem. Now the +Facebook Fund supports enterprising developers who are eager to +build their ideas.

+How can you engage your customers and partners to help you +prototype new offerings?

+

Front-of-house Flexibility
+The secret behind the unique feel of Whole Foods and Trader +Joe’s is how employees are empowered to cocreate the customer +experience. Each store establishes teams to figure out the best way +to serve customers, from the products they offer to the way +sections are organized. Each week, employees can see the results of +their experiments in the aisles.

+

Jesse recently joined the cheese department at Whole Foods and +one of his favorite jobs is to select the cheeses that customers +sample. He feels it helps set the mood of the entire store, and +when he nails the selection, the store usually sells the entire +stock. Giving teams the tools to constantly improve the business +creates an engaging and successful environment.

+What control should you give up so your team is empowered?

+image +
Grassroots Growth
+Ayr is a former scientist with an MBA from Harvard. After several +years with McKinsey, he decided to follow his dream to create a +chain of fast and friendly vegetarian restaurants. +

He could have hired a chef and tested his menu with focus +groups, but instead he decided that it would be better to run a lot +of experiments at low cost. So he launched his restaurant from a +food truck parked outside the MIT campus, updating his customers +about daily specials through text messages and blog posts.

+

After six months, the results have been phenomenal. By starting +small and prototyping, Ayr is learning while he shapes his +business. He’s adding additional trucks, developing permanent +spaces, and has begun to cater special events. Each experiment +brings him closer to his ultimate goal.

+

How can you intentionally limit your resources to create a +more inspired offering?

+

Making Lemonade
+Like many fashion houses, Gucci and Ann Taylor were hit hard during +the recent recession. The nation’s sudden shopping withdrawal +left many designers with too few retail orders to manufacture their +line. In similar circumstances, manufacturers will order the +additional garments and offer excess inventory in outlet malls and +discount retailers. This time things were different, demand was +much lower so the fashion houses got creative. Taking advantage of +empty retail space many designers negotiated short, temporary +leases in high-traffic areas. In this short stay space they opened +pop-up shops to connect with customers. The recession could have +distanced these designers from their customers but quick, nimble +moves created new opportunities to engage.

+How can you turn your biggest challenge into an opportunity to try +something new?

+

Real-time Results
+Internet companies routinely use their constant connections with +customers to prototype new offerings. Companies like Google and +Amazon routinely select pools of users and change the functionality +in their products (e.g., you may be looking at a different Gmail +interface than your friends). Depending on specific behavioral +metrics, Google may change a product without ever directly asking +the customer. Smart and nimble businesses know that always-on and +always-accessible allows them to learn and evolve.

+How can you experiment on the fly and learn without compromising +experience?

+image +
Ongoing Experimentation
+McDonald’s has built prototyping into its organization. Since +the company does not want every employee in every store deviating +from service patterns, it has set up test restaurants to try new +menu items, new pricing strategies, and new food preparation +methods. This flexibility has paid off. McDonald’s has been able to +roll out worldwide menu expansions in just a few months—quite a +feat for a company that serves 47 million customers a day. +

How can you build experimentation into the culture of your +organization?

+

Name my Book
+Tim Ferriss loved the playful working title of his first book, Drug +Dealing for Fun and Profit, but it was too racy for Walmart and +other retailers. With the success of the book hinging on this +decision, Tim decided to prototype. He drafted a shortlist of +titles and bought Google AdWords. Each online click equaled one +vote. Within a week, he had his title, and The 4-Hour Work +Week was finally finished.

+

How much information do you need to make decisions? Can +prototyping help you get there faster?

+

Be a Pattern Spotter

+



+Now that you’ve been exposed to a few different examples, +don’t be surprised if you start seeing Life’s Changes +patterns all around. Keep your eyes open and let us know what you +find, especially if it’s the next new pattern.

+

PATTERNS are a collection of shared thoughts, insights, and +observations gathered by IDEO through their work and the world +around them. Read more pieces from the series here.

+

Colin +Raney leads the Business Design Community within IDEO. +He specializes in designing new ventures for clients based on new +technologies or unique insights. He believes that good business is +good design—successful businesses require offerings, brands, +services, and strategies that complement each other.

+ +
+IDEO +
+

IDEO

+

IDEO is an award-winning global design firm that takes a +human-centered approach to helping organizations in the public and +private sectors innovate and grow. We identify … Read more

+

Twitter

+
+
+ +
+ +
+ +
image
+
+
+ +
+
+
+
+ +
+ +
+
+ + diff --git a/spec/fixtures/bookmarks_list.json b/spec/fixtures/bookmarks_list.json new file mode 100644 index 0000000..99216a8 --- /dev/null +++ b/spec/fixtures/bookmarks_list.json @@ -0,0 +1,5 @@ +[{"type":"meta"}, +{"type":"user","user_id":1075837,"username":"steve.agalloco@gmail.com","subscription_is_active":"1"}, +{"type":"bookmark","bookmark_id":170939225,"url":"http:\/\/www.igvita.com\/2010\/11\/17\/routing-with-ruby-zeromq-devices\/","title":"Routing with Ruby & ZeroMQ Devices","description":"ZeroMQ sockets provide message-oriented messaging, support for multiple transports, transparent setup and teardown, and an entire array of routing patterns via different socket types","time":1307319089,"starred":"0","private_source":"","hash":"PGU0MMPw","progress":0,"progress_timestamp":0}, +{"type":"bookmark","bookmark_id":169529989,"url":"http:\/\/www.fastcodesign.com\/1662169\/ideos-axioms-for-starting-disruptive-new-businesses","title":"Ideo's Axioms for Starting Disruptive New Businesses | Co.Design","description":"www.fastcodesign.com","time":1306963988,"starred":"0","private_source":"","hash":"v27qHZc2","progress":"0","progress_timestamp":1307145892} +] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_move.json b/spec/fixtures/bookmarks_move.json new file mode 100644 index 0000000..0e38ba2 --- /dev/null +++ b/spec/fixtures/bookmarks_move.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":169529989,"url":"http:\/\/www.fastcodesign.com\/1662169\/ideos-axioms-for-starting-disruptive-new-businesses","title":"Ideo's Axioms for Starting Disruptive New Businesses | Co.Design","description":"www.fastcodesign.com","time":1307586766,"starred":"0","private_source":"","hash":"9GZzaC8U","progress":"0","progress_timestamp":1307585389}] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_star.json b/spec/fixtures/bookmarks_star.json new file mode 100644 index 0000000..5c018d5 --- /dev/null +++ b/spec/fixtures/bookmarks_star.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":170939225,"url":"http:\/\/www.igvita.com\/2010\/11\/17\/routing-with-ruby-zeromq-devices\/","title":"Routing with Ruby & ZeroMQ Devices","description":"ZeroMQ sockets provide message-oriented messaging, support for multiple transports, transparent setup and teardown, and an entire array of routing patterns via different socket types","time":1307319089,"starred":"1","private_source":"","hash":"PGU0MMPw","progress":0,"progress_timestamp":0}] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_unarchive.json b/spec/fixtures/bookmarks_unarchive.json new file mode 100644 index 0000000..4e6e840 --- /dev/null +++ b/spec/fixtures/bookmarks_unarchive.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":169529989,"url":"http:\/\/www.fastcodesign.com\/1662169\/ideos-axioms-for-starting-disruptive-new-businesses","title":"Ideo's Axioms for Starting Disruptive New Businesses | Co.Design","description":"www.fastcodesign.com","time":1307507347,"starred":"0","private_source":"","hash":"v27qHZc2","progress":"0","progress_timestamp":1307145892}] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_unstar.json b/spec/fixtures/bookmarks_unstar.json new file mode 100644 index 0000000..302ed81 --- /dev/null +++ b/spec/fixtures/bookmarks_unstar.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":170939225,"url":"http:\/\/www.igvita.com\/2010\/11\/17\/routing-with-ruby-zeromq-devices\/","title":"Routing with Ruby & ZeroMQ Devices","description":"ZeroMQ sockets provide message-oriented messaging, support for multiple transports, transparent setup and teardown, and an entire array of routing patterns via different socket types","time":1307319089,"starred":"0","private_source":"","hash":"PGU0MMPw","progress":0,"progress_timestamp":0}] \ No newline at end of file diff --git a/spec/fixtures/bookmarks_update_read_progress.json b/spec/fixtures/bookmarks_update_read_progress.json new file mode 100644 index 0000000..d706583 --- /dev/null +++ b/spec/fixtures/bookmarks_update_read_progress.json @@ -0,0 +1 @@ +[{"type":"bookmark","bookmark_id":169529989,"url":"http:\/\/www.fastcodesign.com\/1662169\/ideos-axioms-for-starting-disruptive-new-businesses","title":"Ideo's Axioms for Starting Disruptive New Businesses | Co.Design","description":"www.fastcodesign.com","time":1307507347,"starred":"0","private_source":"","hash":"6n9G6qFt","progress":"0.5","progress_timestamp":1307585332}] \ No newline at end of file diff --git a/spec/fixtures/folders_add.json b/spec/fixtures/folders_add.json new file mode 100644 index 0000000..7094bdc --- /dev/null +++ b/spec/fixtures/folders_add.json @@ -0,0 +1 @@ +[{"type":"folder","folder_id":1121141,"title":"Ruby","sync_to_mobile":"1","position":1307330035}] \ No newline at end of file diff --git a/spec/fixtures/folders_delete.json b/spec/fixtures/folders_delete.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/spec/fixtures/folders_delete.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/spec/fixtures/folders_list.json b/spec/fixtures/folders_list.json new file mode 100644 index 0000000..86b8189 --- /dev/null +++ b/spec/fixtures/folders_list.json @@ -0,0 +1 @@ +[{"type":"folder","folder_id":1121173,"title":"Ruby","sync_to_mobile":"1","position":1307331865},{"type":"folder","folder_id":1121174,"title":"JavaScript","sync_to_mobile":"1","position":1307331865}] \ No newline at end of file diff --git a/spec/fixtures/folders_set_order.json b/spec/fixtures/folders_set_order.json new file mode 100644 index 0000000..384ff7b --- /dev/null +++ b/spec/fixtures/folders_set_order.json @@ -0,0 +1 @@ +[{"type":"folder","folder_id":1121174,"title":"JavaScript","sync_to_mobile":"1","position":1},{"type":"folder","folder_id":1121173,"title":"Ruby","sync_to_mobile":"1","position":2}] \ No newline at end of file diff --git a/spec/fixtures/verify_credentials.json b/spec/fixtures/verify_credentials.json new file mode 100644 index 0000000..918b0e5 --- /dev/null +++ b/spec/fixtures/verify_credentials.json @@ -0,0 +1 @@ +[{"type":"user","user_id":54321,"username":"TestUserOMGLOL"}] \ No newline at end of file diff --git a/spec/instapaper/client/account_spec.rb b/spec/instapaper/client/account_spec.rb new file mode 100644 index 0000000..972f5dc --- /dev/null +++ b/spec/instapaper/client/account_spec.rb @@ -0,0 +1,27 @@ +require 'spec_helper' + +describe Instapaper::Client::Account do + before(:each) do + @client = Instapaper::Client.new + end + + describe '.verify_credentials' do + before do + stub_post("account/verify_credentials"). + to_return(:body => fixture("verify_credentials.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.verify_credentials + a_post("account/verify_credentials"). + should have_been_made + end + + it "should return the user" do + user = @client.verify_credentials.first + user.should be_a Hashie::Rash + user.username.should == 'TestUserOMGLOL' + end + end + +end \ No newline at end of file diff --git a/spec/instapaper/client/bookmark_spec.rb b/spec/instapaper/client/bookmark_spec.rb new file mode 100644 index 0000000..483dbd7 --- /dev/null +++ b/spec/instapaper/client/bookmark_spec.rb @@ -0,0 +1,234 @@ +require 'spec_helper' + +describe Instapaper::Client::Bookmark do + before(:each) do + @client = Instapaper::Client.new(:consumer_key => 'CK', :consumer_secret => 'CS', :oauth_token => 'OT', :oauth_token_secret => 'OS') + end + + describe '.bookmarks' do + before do + stub_post("bookmarks/list"). + to_return(:body => fixture("bookmarks_list.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.bookmarks + a_post("bookmarks/list"). + should have_been_made + end + + it "should return an array containing bookmarks on success" do + bookmarks = @client.bookmarks + bookmarks.should be_an Array + bookmarks.size.should == 2 + end + + it "should remove the meta and current user objects from the array" do + bookmarks = @client.bookmarks + bookmarks.each do |bookmark| + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + end + end + end + + describe '.update_read_progress' do + before do + @time = Time.now + stub_post("bookmarks/update_read_progress"). + to_return(:body => fixture("bookmarks_update_read_progress.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.update_read_progress(123, 0.5, @time) + a_post("bookmarks/update_read_progress").with(:body => {:bookmark_id => "123", :progress => '0.5', :progress_timestamp => @time.to_i.to_s }). + should have_been_made + end + + it "should return an array containing bookmarks on success" do + bookmark = @client.update_read_progress(123, 0.5, @time) + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + bookmark.progress.should == "0.5" + end + end + + describe '.add_bookmark' do + before do + stub_post("bookmarks/add"). + to_return(:body => fixture('bookmarks_add.json'), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.add_bookmark('http://someurl.com', :title => 'This is the title', :description => 'This is the description') + a_post("bookmarks/add").with(:body => {:url => "http://someurl.com", :title => 'This is the title', :description => 'This is the description' }). + should have_been_made + end + + it "should return the bookmark on success" do + bookmark = @client.add_bookmark('http://someurl.com', :title => 'This is the title', :description => 'This is the description') + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + end + end + + describe '.delete_bookmark' do + before do + stub_post("bookmarks/delete"). + to_return(:body => '[]', :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.delete_bookmark(123) + a_post("bookmarks/delete").with(:body => {:bookmark_id => "123" }). + should have_been_made + end + + it "should return an array containing bookmarks on success" do + confirm = @client.delete_bookmark(123) + confirm.should be_an Array + confirm.should be_empty + end + end + + describe '.star' do + before do + stub_post("bookmarks/star"). + to_return(:body => fixture("bookmarks_star.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.star(123) + a_post("bookmarks/star").with(:body => {:bookmark_id => "123" }). + should have_been_made + end + + it "should return a starred bookmark on success" do + bookmark = @client.star(123) + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + bookmark.starred.should == '1' + end + + it 'should be aliased as .star_bookmark' do + @client.star(123).should == @client.star_bookmark(123) + end + end + + describe '.unstar' do + before do + stub_post("bookmarks/unstar"). + to_return(:body => fixture("bookmarks_unstar.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.unstar(123) + a_post("bookmarks/unstar").with(:body => {:bookmark_id => "123" }). + should have_been_made + end + + it "should return an unstarred bookmark on success" do + bookmark = @client.unstar(123) + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + bookmark.starred.should == '0' + end + + it 'should be aliased as .unstar_bookmark' do + @client.unstar(123).should == @client.unstar_bookmark(123) + end + end + + describe '.archive' do + before do + stub_post("bookmarks/archive"). + to_return(:body => fixture("bookmarks_archive.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.archive(123) + a_post("bookmarks/archive").with(:body => {:bookmark_id => "123" }). + should have_been_made + end + + it "should return the bookmark on success" do + bookmark = @client.archive(123) + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + end + + it 'should be aliased as .archive_bookmark' do + @client.archive(123).should == @client.archive_bookmark(123) + end + end + + describe '.unarchive' do + before do + stub_post("bookmarks/unarchive"). + to_return(:body => fixture("bookmarks_unarchive.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.unarchive(123) + a_post("bookmarks/unarchive").with(:body => {:bookmark_id => "123" }). + should have_been_made + end + + it "should return the bookmark on success" do + bookmark = @client.unarchive(123) + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + end + + it 'should be aliased as .unarchive_bookmark' do + @client.unarchive(123).should == @client.unarchive_bookmark(123) + end + end + + describe '.move' do + before do + stub_post("bookmarks/move"). + to_return(:body => fixture("bookmarks_move.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.move(123, 12345) + a_post("bookmarks/move").with(:body => {:bookmark_id => "123", :folder_id => "12345" }). + should have_been_made + end + + it "should return the bookmark on success" do + bookmark = @client.move(123, 12345) + bookmark.should be_a Hashie::Rash + bookmark.type.should == 'bookmark' + end + + it 'should be aliased as .move_bookmark' do + @client.move(123, 12345).should == @client.move_bookmark(123, 12345) + end + end + + describe '.text' do + before do + stub_post("bookmarks/get_text"). + to_return(:body => fixture("bookmarks_get_text.txt"), :headers => {:content_type => "text/html; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.text(123) + a_post("bookmarks/get_text").with(:body => {:bookmark_id => "123" }). + should have_been_made + end + + it "should return the bookmark's html on success" do + bookmark = @client.text(123) + bookmark.length.should > 0 + bookmark.should include("Ideo's Axioms for Starting Disruptive New Businesses") + end + + it 'should be aliased as .get_text' do + @client.text(123).should == @client.get_text(123) + end + end + +end \ No newline at end of file diff --git a/spec/instapaper/client/folder_spec.rb b/spec/instapaper/client/folder_spec.rb new file mode 100644 index 0000000..e7ca6d0 --- /dev/null +++ b/spec/instapaper/client/folder_spec.rb @@ -0,0 +1,89 @@ +require 'spec_helper' + +describe Instapaper::Client::Folder do + before(:each) do + @client = Instapaper::Client.new + end + + describe '.folders' do + before do + stub_post("folders/list"). + to_return(:body => fixture("folders_list.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.folders + a_post("folders/list"). + should have_been_made + end + + it "should return an array containing folders on success" do + folders = @client.folders + folders.should be_an Array + folders.size.should == 2 + folders.first.should be_a Hashie::Rash + folders.first['title'].should == 'Ruby' + end + end + + describe '.add_folder' do + before do + stub_post("folders/add").with(:body => {:title => "Ruby" }). + to_return(:body => fixture("folders_add.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.add_folder("Ruby") + a_post("folders/add"). + should have_been_made + end + + it "should return an array containing the new folder on success" do + folders = @client.add_folder("Ruby") + folders.should be_an Array + folders.should_not be_empty + folders.first.should be_a Hashie::Rash + folders.first['title'].should == 'Ruby' + end + end + + describe '.delete_folder' do + before do + stub_post("folders/delete"). with(:body => {:folder_id => "1" }). + to_return(:body => fixture("folders_delete.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.delete_folder("1") + a_post("folders/delete"). + should have_been_made + end + + it "should return an empty array on success" do + confirm = @client.delete_folder("1") + confirm.should be_an Array + confirm.should be_empty + end + end + + describe '.set_order' do + before do + stub_post("folders/set_order"). with(:body => {:order => "1121173:2,1121174:1" }). + to_return(:body => fixture("folders_set_order.json"), :headers => {:content_type => "application/json; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.set_order(['1121173:2','1121174:1']) + a_post("folders/set_order"). + should have_been_made + end + + it "should return an array reflecting the new order on success" do + folders = @client.set_order(['1121173:2','1121174:1']) + folders.should be_an Array + folders.first.should be_a Hashie::Rash + folders.first['position'].should == 1 + end + end + +end \ No newline at end of file diff --git a/spec/instapaper/client/user_spec.rb b/spec/instapaper/client/user_spec.rb new file mode 100644 index 0000000..52d0bbd --- /dev/null +++ b/spec/instapaper/client/user_spec.rb @@ -0,0 +1,28 @@ +require 'spec_helper' + +describe Instapaper::Client::User do + before(:each) do + @client = Instapaper::Client.new + end + + describe '.access_token' do + before do + stub_post("oauth/access_token").with(:body => { :x_auth_username => 'ohai', :x_auth_password => 'p455w0rd', :x_auth_mode => 'client_auth'}). + to_return(:body => fixture("access_token.qline"), :headers => {:content_type => "text/plain; charset=utf-8"}) + end + + it "should get the correct resource" do + @client.access_token('ohai', 'p455w0rd') + a_post("oauth/access_token"). + should have_been_made + end + + it "should return the a hash containing an oauth token and secret" do + tokens = @client.access_token('ohai', 'p455w0rd') + tokens.should be_a Hash + tokens.key?('oauth_token').should be_true + tokens.key?('oauth_token_secret').should be_true + end + end + +end diff --git a/spec/instapaper/client_spec.rb b/spec/instapaper/client_spec.rb new file mode 100644 index 0000000..54cec04 --- /dev/null +++ b/spec/instapaper/client_spec.rb @@ -0,0 +1,65 @@ +require 'spec_helper' + +describe Instapaper::Client do + + before do + @options = { :adapter => :em_synchrony, :user_agent => 'Instapaper::Client spec' } + @keys = Instapaper::Configuration::VALID_OPTIONS_KEYS + end + + describe '.new' do + before(:each) do + @keys.each do |key| + Instapaper.send("#{key}=", key) + end + end + + after do + Instapaper.reset + end + + context 'with module configuration' do + it "should inherit module configuration" do + api = Instapaper::Client.new + @keys.each do |key| + api.send(key).should eq(key) + end + end + end + + context 'with class configuration' do + context "during initialization" do + it "should override module configuration" do + api = Instapaper::Client.new(@options) + @keys.each do |key| + h = @options.has_key?(key) ? @options : Instapaper.options + api.send(key).should eq(h[key]) + end + end + end + + context "after initialization" do + it "should override module configuration after initialization" do + api = Instapaper::Client.new + @options.each do |key, value| + api.send("#{key}=", value) + end + @keys.each do |key| + h = @options.has_key?(key) ? @options : Instapaper.options + api.send(key).should eq(h[key]) + end + end + end + end + end + + describe '.endpoint_with_prefix' do + before(:each) do + @client = Instapaper::Client.new + end + + it 'should return the ' do + @client.endpoint_with_prefix.should == Instapaper.endpoint + Instapaper.path_prefix + end + end +end \ No newline at end of file diff --git a/spec/instapaper_spec.rb b/spec/instapaper_spec.rb new file mode 100644 index 0000000..7d7434f --- /dev/null +++ b/spec/instapaper_spec.rb @@ -0,0 +1,85 @@ +require 'spec_helper' + +describe Instapaper do + after do + Instapaper.reset + end + + describe '.respond_to?' do + it 'takes an optional include private argument' do + Instapaper.respond_to?(:client, true).should be_true + end + end + + describe ".client" do + it "should be a Instapaper::Client" do + Instapaper.client.should be_a Instapaper::Client + end + end + + describe ".adapter" do + it "should return the default adapter" do + Instapaper.adapter.should == Instapaper::Configuration::DEFAULT_ADAPTER + end + end + + describe ".adapter=" do + it "should set the adapter" do + Instapaper.adapter = :typhoeus + Instapaper.adapter.should == :typhoeus + end + end + + describe ".endpoint" do + it "should return the default endpoint" do + Instapaper.endpoint.should == Instapaper::Configuration::DEFAULT_ENDPOINT + end + end + + describe ".endpoint=" do + it "should set the endpoint" do + Instapaper.endpoint = 'http://tumblr.com/' + Instapaper.endpoint.should == 'http://tumblr.com/' + end + end + + describe ".user_agent" do + it "should return the default user agent" do + Instapaper.user_agent.should == Instapaper::Configuration::DEFAULT_USER_AGENT + end + end + + describe ".user_agent=" do + it "should set the user_agent" do + Instapaper.user_agent = 'Custom User Agent' + Instapaper.user_agent.should == 'Custom User Agent' + end + end + + describe ".version" do + it "should return the default version" do + Instapaper.version.should == Instapaper::Configuration::DEFAULT_VERSION + end + end + + describe ".version=" do + it "should set the user_agent" do + Instapaper.version = '2' + Instapaper.version.should == '2' + end + end + + describe ".configure" do + + Instapaper::Configuration::VALID_OPTIONS_KEYS.each do |key| + + it "should set the #{key}" do + Instapaper.configure do |config| + config.send("#{key}=", key) + Instapaper.send(key).should == key + end + end + end + end + +end \ No newline at end of file diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb new file mode 100644 index 0000000..a786bab --- /dev/null +++ b/spec/spec_helper.rb @@ -0,0 +1,44 @@ +require 'simplecov' +require File.expand_path('../../lib/instapaper', __FILE__) +require 'rspec' +require 'webmock/rspec' + +def a_delete(path) + a_request(:delete, Instapaper.endpoint_with_prefix + path) +end + +def a_get(path) + a_request(:get, Instapaper.endpoint_with_prefix + path) +end + +def a_post(path) + a_request(:post, Instapaper.endpoint_with_prefix + path) +end + +def a_put(path) + a_request(:put, Instapaper.endpoint_with_prefix + path) +end + +def stub_delete(path) + stub_request(:delete, Instapaper.endpoint_with_prefix + path) +end + +def stub_get(path) + stub_request(:get, Instapaper.endpoint_with_prefix + path) +end + +def stub_post(path) + stub_request(:post, Instapaper.endpoint_with_prefix + path) +end + +def stub_put(path) + stub_request(:put, Instapaper.endpoint_with_prefix + path) +end + +def fixture_path + File.expand_path("../fixtures", __FILE__) +end + +def fixture(file) + File.new(fixture_path + '/' + file) +end