mirror of
https://github.com/samsonjs/instapaper.git
synced 2026-03-25 08:55:49 +00:00
initial commit
This commit is contained in:
commit
c34b46b95b
45 changed files with 1579 additions and 0 deletions
0
.gemtest
Normal file
0
.gemtest
Normal file
11
.gitignore
vendored
Normal file
11
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,11 @@
|
||||||
|
*.gem
|
||||||
|
*.rbc
|
||||||
|
.DS_Store
|
||||||
|
.bundle
|
||||||
|
.rvmrc
|
||||||
|
.yardoc
|
||||||
|
Gemfile.lock
|
||||||
|
coverage/*
|
||||||
|
doc/*
|
||||||
|
log/*
|
||||||
|
pkg/*
|
||||||
3
.rspec
Normal file
3
.rspec
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
--color
|
||||||
|
--format=nested
|
||||||
|
--backtrace
|
||||||
1
.simplecov
Normal file
1
.simplecov
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
SimpleCov.start
|
||||||
3
.yardopts
Normal file
3
.yardopts
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
--markup markdown
|
||||||
|
-
|
||||||
|
LICENSE.md
|
||||||
2
Gemfile
Normal file
2
Gemfile
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
source :rubygems
|
||||||
|
gemspec
|
||||||
20
LICENSE.md
Normal file
20
LICENSE.md
Normal 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
126
README.md
Normal 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
13
Rakefile
Executable 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
33
instapaper.gemspec
Normal 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
|
||||||
65
lib/faraday/response/raise_http_1xxx.rb
Normal file
65
lib/faraday/response/raise_http_1xxx.rb
Normal 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
26
lib/instapaper.rb
Normal 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
|
||||||
32
lib/instapaper/authentication.rb
Normal file
32
lib/instapaper/authentication.rb
Normal 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
43
lib/instapaper/client.rb
Normal 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
|
||||||
13
lib/instapaper/client/account.rb
Normal file
13
lib/instapaper/client/account.rb
Normal 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
|
||||||
81
lib/instapaper/client/bookmark.rb
Normal file
81
lib/instapaper/client/bookmark.rb
Normal file
|
|
@ -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
|
||||||
34
lib/instapaper/client/folder.rb
Normal file
34
lib/instapaper/client/folder.rb
Normal file
|
|
@ -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
|
||||||
14
lib/instapaper/client/user.rb
Normal file
14
lib/instapaper/client/user.rb
Normal 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
|
||||||
88
lib/instapaper/configuration.rb
Normal file
88
lib/instapaper/configuration.rb
Normal 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
|
||||||
35
lib/instapaper/connection.rb
Normal file
35
lib/instapaper/connection.rb
Normal 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
22
lib/instapaper/request.rb
Normal 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
|
||||||
3
lib/instapaper/version.rb
Normal file
3
lib/instapaper/version.rb
Normal file
|
|
@ -0,0 +1,3 @@
|
||||||
|
module Instapaper
|
||||||
|
VERSION = "0.2.0"
|
||||||
|
end
|
||||||
22
spec/faraday/response_spec.rb
Normal file
22
spec/faraday/response_spec.rb
Normal 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
1
spec/fixtures/access_token.qline
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
oauth_token=aabbccdd&oauth_token_secret=efgh1234
|
||||||
1
spec/fixtures/bookmarks_add.json
vendored
Normal file
1
spec/fixtures/bookmarks_add.json
vendored
Normal 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
1
spec/fixtures/bookmarks_archive.json
vendored
Normal 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
299
spec/fixtures/bookmarks_get_text.txt
vendored
Normal file
|
|
@ -0,0 +1,299 @@
|
||||||
|
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>Ideo'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’s Axioms for Starting Disruptive New
|
||||||
|
Businesses</span></h1>
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<div>Don’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 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.</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’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
|
||||||
|
don’t 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 />
|
||||||
|
Don’t 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
|
||||||
|
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.</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. He’s 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 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.<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 />
|
||||||
|
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.
|
||||||
|
<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’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.</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—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 … <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
5
spec/fixtures/bookmarks_list.json
vendored
Normal 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
1
spec/fixtures/bookmarks_move.json
vendored
Normal 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
1
spec/fixtures/bookmarks_star.json
vendored
Normal 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}]
|
||||||
1
spec/fixtures/bookmarks_unarchive.json
vendored
Normal file
1
spec/fixtures/bookmarks_unarchive.json
vendored
Normal 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
1
spec/fixtures/bookmarks_unstar.json
vendored
Normal 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}]
|
||||||
1
spec/fixtures/bookmarks_update_read_progress.json
vendored
Normal file
1
spec/fixtures/bookmarks_update_read_progress.json
vendored
Normal 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
1
spec/fixtures/folders_add.json
vendored
Normal 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
1
spec/fixtures/folders_delete.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[]
|
||||||
1
spec/fixtures/folders_list.json
vendored
Normal file
1
spec/fixtures/folders_list.json
vendored
Normal 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
1
spec/fixtures/folders_set_order.json
vendored
Normal 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
1
spec/fixtures/verify_credentials.json
vendored
Normal file
|
|
@ -0,0 +1 @@
|
||||||
|
[{"type":"user","user_id":54321,"username":"TestUserOMGLOL"}]
|
||||||
27
spec/instapaper/client/account_spec.rb
Normal file
27
spec/instapaper/client/account_spec.rb
Normal 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
|
||||||
234
spec/instapaper/client/bookmark_spec.rb
Normal file
234
spec/instapaper/client/bookmark_spec.rb
Normal 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
|
||||||
89
spec/instapaper/client/folder_spec.rb
Normal file
89
spec/instapaper/client/folder_spec.rb
Normal 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
|
||||||
28
spec/instapaper/client/user_spec.rb
Normal file
28
spec/instapaper/client/user_spec.rb
Normal 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
|
||||||
65
spec/instapaper/client_spec.rb
Normal file
65
spec/instapaper/client_spec.rb
Normal 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
85
spec/instapaper_spec.rb
Normal 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
44
spec/spec_helper.rb
Normal 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
|
||||||
Loading…
Reference in a new issue