commit c34b46b95befa3d9b53939115850e1a6ad79ad40 Author: Steve Agalloco Date: Thu Dec 1 23:07:24 2011 -0500 initial commit 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