initial commit

This commit is contained in:
Steve Agalloco 2011-12-01 23:07:24 -05:00
commit c34b46b95b
45 changed files with 1579 additions and 0 deletions

0
.gemtest Normal file
View file

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
*.gem
*.rbc
.DS_Store
.bundle
.rvmrc
.yardoc
Gemfile.lock
coverage/*
doc/*
log/*
pkg/*

3
.rspec Normal file
View file

@ -0,0 +1,3 @@
--color
--format=nested
--backtrace

1
.simplecov Normal file
View file

@ -0,0 +1 @@
SimpleCov.start

3
.yardopts Normal file
View file

@ -0,0 +1,3 @@
--markup markdown
-
LICENSE.md

2
Gemfile Normal file
View file

@ -0,0 +1,2 @@
source :rubygems
gemspec

20
LICENSE.md Normal file
View file

@ -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.

126
README.md Normal file
View file

@ -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.

13
Rakefile Executable file
View file

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

33
instapaper.gemspec Normal file
View file

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

View file

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

26
lib/instapaper.rb Normal file
View file

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

View file

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

43
lib/instapaper/client.rb Normal file
View file

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

View file

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

View file

@ -0,0 +1,81 @@
module Instapaper
class Client
# Defines methods related to bookmarks
module Bookmark
# Lists the users 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 users reading progress on a single article.
# @param bookmark_id [String] The id of the bookmark to update.
# @param progress [Float] The users progress, as a floating-point number between 0.0 and 1.0, defined as the top edge of the users current viewport, expressed as a percentage of the articles 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 users 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 youre 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 bookmarks 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

View file

@ -0,0 +1,34 @@
module Instapaper
class Client
# Defines methods related to folders
module Folder
# List the accounts 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 users 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

View file

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

View file

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

View file

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

22
lib/instapaper/request.rb Normal file
View file

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

View file

@ -0,0 +1,3 @@
module Instapaper
VERSION = "0.2.0"
end

View file

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

1
spec/fixtures/access_token.qline vendored Normal file
View file

@ -0,0 +1 @@
oauth_token=aabbccdd&oauth_token_secret=efgh1234

1
spec/fixtures/bookmarks_add.json vendored Normal file
View file

@ -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}]

1
spec/fixtures/bookmarks_archive.json vendored Normal file
View file

@ -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}]

299
spec/fixtures/bookmarks_get_text.txt vendored Normal file
View file

@ -0,0 +1,299 @@
<html>
<head>
<title>Ideo&#039;s Axioms for Starting Disruptive New Businesses</title>
<meta name="viewport" content="width=device-width; initial-scale=1.0; user-scalable=no; minimum-scale=1.0; maximum-scale=1.0;" />
<meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
<meta name="robots" content="noindex"/>
<link rel="icon" href="/images/favicon.png"/>
<!-- IP:TITLE
Ideo's Axioms for Starting Disruptive New Businesses
/IP:TITLE -->
<!-- IP:IMAGES
/IP:IMAGES -->
<style type="text/css">
body {
font-family: Georgia;
font-size: 16px;
margin: 0px auto 0px auto;
width: 500px word-wrap: break-word;
}
h1 { font-size: 1.3em; }
h2 { font-size: 1.15em; }
h3, h4, h5, h6, h7 { font-size: 1.0em; }
img { border: 0; display: block; margin: 0.5em 0; }
pre, code { overflow: scroll; }
#story {
clear: both; padding: 0 10px; overflow: hidden; margin-bottom: 40px;
}
.bar {
color: #555;
font-family: 'Helvetica';
font-size: 11pt;
margin: 0 -20px;
padding: 10px 0;
}
.top { border-bottom: 2px solid #000; }
.top a {
display: block;
float: right;
text-decoration: none;
font-size: 11px;
background-color: #eee;
-webkit-border-radius: 8px;
-moz-border-radius: 8px;
padding: 2px 15px;
}
#story div {
margin: 1em 0;
}
.bottom {
border-top: 2px solid #000;
color: #555;
}
.bar a { color: #444; }
blockquote {
border-top: 1px solid #bbb;
border-bottom: 1px solid #bbb;
margin: 1.5em 0;
padding: 0.5em 0;
}
blockquote.short { font-style: italic; }
pre {
white-space: pre-wrap;
}
ul.bodytext, ol.bodytext {
list-style: none;
margin-left: 0;
padding-left: 0em;
}
</style>
</head>
<body onload="loadFont();">
<div class="bar top">
<a href="http://www.fastcodesign.com/1662169/ideos-axioms-for-starting-disruptive-new-businesses">View original</a>
<div class="sm">fastcodesign.com</div>
</div>
<div id="editing_controls" style="float: right; padding-top: 2px;">
</div>
<div id="story">
<div>
<div>
<div><a href="http://www.fastcodesign.com/1662169/ideos-axioms-for-starting-disruptive-new-businesses"><span>Back</span> to Fast Company</a></div>
</div>
<div>
<div><a href="http://www.fastcodesign.com/">Fast Company</a></div>
<div>
<div><a href="http://www.fastcodesign.com/12072010-tue-0" title="Issue Flag"><strong>Dec 04,
2010</strong></a></div>
<img src="http://www.fastcodesign.com/multisite_files/codesign/imagecache/article-feature/business-beta-frontofhouseB.jpg" title="" alt="Ideo's Axioms for Starting Disruptive New Businesses" />
<div>
<div>
<h1><span>Ideo&#8217;s Axioms for Starting Disruptive New
Businesses</span></h1>
<div>
<div>Don&#8217;t wait for perfection: Launch and
learn.</div>
<p><em>This is the first piece in our PATTERNS
series by IDEO. Read more about the series <a href="http://www.fastcodesign.com/1662168/introducing-ideos-new-column-patterns-affecting-business-and-design-today">
here</a>.</em></p>
<p>How do you build a business in an unproven market? How do you
figure out what customers need when youre delivering an
experience theyve 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.</p>
<p>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.</p>
<h2><strong>TAKE ACTION: Designing for Life&#8217;s Changes</strong></h2>
<p><br /><br /><strong>1. Go early, go often</strong><br />
Building experimentation into your business is harder than you
think. Start small and stay focused. Try everything, but
dont try it all in one prototype.<br /><strong><br />
2. Learning by doing</strong><br />
Build value for the business as you prototype. If you fail, what
will you have learned? What will you salvage?</p>
<p><strong>3. Inspiration through constraint</strong><br />
Dont exhaust yourself searching for money and resources. The
tighter your constraints, the more creative your prototypes will
be.</p>
<p><strong>4. Open to opportunity</strong><br />
Look for unanticipated ways customers are using your offering.
Their improvisations may be the future of your business.</p>
<h2><strong>THE EVIDENCE: Stories from Around the
Globe</strong></h2>
<img src="http://images.fastcompany.com/upload/business-beta-platform.jpg" alt="" style="border: 0px;" />
<strong>Platform for Change</strong><br />
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.
<p>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.</p>
<p>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.<br /><em><br />
How can you engage your customers and partners to help you
prototype new offerings?<br /></em></p>
<p><strong>Front-of-house Flexibility</strong><br />
The secret behind the unique feel of Whole Foods and Trader
Joes 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.</p>
<p>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.<br /><em><br />
What control should you give up so your team is empowered?</em></p>
<img src="http://images.fastcompany.com/upload/business-beta-grassroots.jpg" alt="image" />
<br /><strong>Grassroots Growth</strong><br />
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.
<p>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.</p>
<p>After six months, the results have been phenomenal. By starting
small and prototyping, Ayr is learning while he shapes his
business. Hes adding additional trucks, developing permanent
spaces, and has begun to cater special events. Each experiment
brings him closer to his ultimate goal.</p>
<p><em>How can you intentionally limit your resources to create a
more inspired offering?</em></p>
<p><strong>Making Lemonade</strong><br />
Like many fashion houses, Gucci and Ann Taylor were hit hard during
the recent recession. The nations 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.<br /><em><br />
How can you turn your biggest challenge into an opportunity to try
something new?</em></p>
<p><strong>Real-time Results</strong><br />
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.<br /><em><br />
How can you experiment on the fly and learn without compromising
experience?</em></p>
<img src="http://images.fastcompany.com/upload/business-beta-frontofhouse.jpg" alt="image" />
<br /><strong>Ongoing Experimentation</strong><br />
McDonalds 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&#8217;s has been able to
roll out worldwide menu expansions in just a few months&#8212;quite a
feat for a company that serves 47 million customers a day.
<p><em>How can you build experimentation into the culture of your
organization?</em></p>
<p><strong>Name my Book</strong><br />
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 <em>The 4-Hour Work
Week</em> was finally finished.</p>
<p><em>How much information do you need to make decisions? Can
prototyping help you get there faster?</em></p>
<h2><strong>Be a Pattern Spotter</strong></h2>
<p><br /><br />
Now that you&#8217;ve been exposed to a few different examples,
dont be surprised if you start seeing Life&#8217;s Changes
patterns all around. Keep your eyes open and let us know what you
find, especially if it&#8217;s the next new pattern.</p>
<p><em>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 <a href="http://www.fastcodesign.com/tag/ideo-patterns">here</a>.</em></p>
<p><em><a href="http://www.fastcodesign.com/users/craney"><strong>Colin
Raney</strong></a> 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&#8212;successful businesses require offerings, brands,
services, and strategies that complement each other.</em></p>
<div>
<a href="http://www.fastcodesign.com/users/kclark" title="IDEO"><img src="http://www.fastcodesign.com/multisite_files/codesign/imagecache/124x124/IDEO_square_bio.jpg" alt="IDEO" title="IDEO" /></a>
<div>
<h2><a href="http://www.fastcodesign.com/users/kclark" title="IDEO">IDEO</a></h2>
<p>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 &#8230; <a href="http://www.fastcodesign.com/users/kclark" title="Read more by IDEO">Read more</a></p>
<p>• <a href="http://www.twitter.com/IDEO" title="IDEO's twitter profile">Twitter</a></p>
</div>
</div>
</div>
</div>
<div><img src="http://www.fastcodesign.com/sites/fastcodesign.com/themes/co/images/blank.png" alt="image" /></div>
<div>
</div>
</div>
</div>
</div>
</div>
</div>
<div class="bar bottom">
</div>
</body>
</html>

5
spec/fixtures/bookmarks_list.json vendored Normal file
View file

@ -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}
]

1
spec/fixtures/bookmarks_move.json vendored Normal file
View file

@ -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}]

1
spec/fixtures/bookmarks_star.json vendored Normal file
View file

@ -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}]

View file

@ -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}]

1
spec/fixtures/bookmarks_unstar.json vendored Normal file
View file

@ -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}]

View file

@ -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}]

1
spec/fixtures/folders_add.json vendored Normal file
View file

@ -0,0 +1 @@
[{"type":"folder","folder_id":1121141,"title":"Ruby","sync_to_mobile":"1","position":1307330035}]

1
spec/fixtures/folders_delete.json vendored Normal file
View file

@ -0,0 +1 @@
[]

1
spec/fixtures/folders_list.json vendored Normal file
View file

@ -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}]

1
spec/fixtures/folders_set_order.json vendored Normal file
View file

@ -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}]

1
spec/fixtures/verify_credentials.json vendored Normal file
View file

@ -0,0 +1 @@
[{"type":"user","user_id":54321,"username":"TestUserOMGLOL"}]

View file

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

View file

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

View file

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

View file

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

View file

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

85
spec/instapaper_spec.rb Normal file
View file

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

44
spec/spec_helper.rb Normal file
View file

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