instapaper 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (87) hide show
  1. data/{PostInstall.txt → .gemtest} +0 -0
  2. data/.gitignore +11 -0
  3. data/.rspec +3 -0
  4. data/.simplecov +1 -0
  5. data/.yardopts +3 -0
  6. data/Gemfile +2 -0
  7. data/LICENSE.md +20 -0
  8. data/README.md +152 -0
  9. data/Rakefile +13 -4
  10. data/instapaper.gemspec +33 -0
  11. data/lib/faraday/response/raise_http_1xxx.rb +65 -0
  12. data/lib/instapaper.rb +26 -10
  13. data/lib/instapaper/authentication.rb +32 -0
  14. data/lib/instapaper/client.rb +43 -0
  15. data/lib/instapaper/client/account.rb +13 -0
  16. data/lib/instapaper/client/bookmark.rb +81 -0
  17. data/lib/instapaper/client/folder.rb +34 -0
  18. data/lib/instapaper/client/user.rb +14 -0
  19. data/lib/instapaper/configuration.rb +88 -0
  20. data/lib/instapaper/connection.rb +35 -0
  21. data/lib/instapaper/request.rb +22 -9
  22. data/lib/instapaper/version.rb +1 -31
  23. data/spec/faraday/response_spec.rb +22 -0
  24. data/spec/fixtures/access_token.qline +1 -0
  25. data/spec/fixtures/bookmarks_add.json +1 -0
  26. data/spec/fixtures/bookmarks_archive.json +1 -0
  27. data/spec/fixtures/bookmarks_get_text.txt +299 -0
  28. data/spec/fixtures/bookmarks_list.json +5 -0
  29. data/spec/fixtures/bookmarks_move.json +1 -0
  30. data/spec/fixtures/bookmarks_star.json +1 -0
  31. data/spec/fixtures/bookmarks_unarchive.json +1 -0
  32. data/spec/fixtures/bookmarks_unstar.json +1 -0
  33. data/spec/fixtures/bookmarks_update_read_progress.json +1 -0
  34. data/spec/fixtures/folders_add.json +1 -0
  35. data/spec/fixtures/folders_delete.json +1 -0
  36. data/spec/fixtures/folders_list.json +1 -0
  37. data/spec/fixtures/folders_set_order.json +1 -0
  38. data/spec/fixtures/verify_credentials.json +1 -0
  39. data/spec/instapaper/client/account_spec.rb +27 -0
  40. data/spec/instapaper/client/bookmark_spec.rb +234 -0
  41. data/spec/instapaper/client/folder_spec.rb +89 -0
  42. data/spec/instapaper/client/user_spec.rb +28 -0
  43. data/spec/instapaper/client_spec.rb +65 -0
  44. data/spec/instapaper_spec.rb +85 -0
  45. data/spec/spec_helper.rb +44 -0
  46. metadata +230 -99
  47. data/History.txt +0 -3
  48. data/Manifest.txt +0 -43
  49. data/README.rdoc +0 -73
  50. data/config/hoe.rb +0 -73
  51. data/config/requirements.rb +0 -15
  52. data/lib/instapaper/base.rb +0 -113
  53. data/lib/instapaper/constants.rb +0 -37
  54. data/lib/instapaper/exceptions.rb +0 -41
  55. data/lib/instapaper/protocol.rb +0 -47
  56. data/lib/instapaper/request/abstract_request.rb +0 -104
  57. data/lib/instapaper/request/add_url_request.rb +0 -68
  58. data/lib/instapaper/request/authentication_request.rb +0 -60
  59. data/lib/instapaper/request/request_error.rb +0 -34
  60. data/lib/instapaper/request/request_errors.rb +0 -41
  61. data/lib/instapaper/request/request_validation.rb +0 -34
  62. data/lib/instapaper/response.rb +0 -10
  63. data/lib/instapaper/response/abstract_response.rb +0 -38
  64. data/lib/instapaper/response/add_url_success_response.rb +0 -41
  65. data/lib/instapaper/response/authentication_invalid_response.rb +0 -40
  66. data/lib/instapaper/response/authentication_success_response.rb +0 -40
  67. data/lib/instapaper/response/bad_request_response.rb +0 -40
  68. data/lib/instapaper/response/response_builder.rb +0 -61
  69. data/lib/instapaper/response/service_error_response.rb +0 -40
  70. data/script/console +0 -10
  71. data/script/destroy +0 -14
  72. data/script/generate +0 -14
  73. data/script/txt2html +0 -82
  74. data/setup.rb +0 -1585
  75. data/tasks/deployment.rake +0 -34
  76. data/tasks/environment.rake +0 -7
  77. data/tasks/website.rake +0 -17
  78. data/test/test_auth.rb +0 -62
  79. data/test/test_error.rb +0 -81
  80. data/test/test_helper.rb +0 -2
  81. data/test/test_instapaper.rb +0 -9
  82. data/test/test_request.rb +0 -45
  83. data/website/index.html +0 -99
  84. data/website/index.txt +0 -69
  85. data/website/javascripts/rounded_corners_lite.inc.js +0 -285
  86. data/website/stylesheets/screen.css +0 -138
  87. data/website/template.html.erb +0 -48
@@ -0,0 +1,81 @@
1
+ module Instapaper
2
+ class Client
3
+ # Defines methods related to bookmarks
4
+ module Bookmark
5
+
6
+ # Lists the user’s unread bookmarks, and can also synchronize reading positions.
7
+ # @option limit: Optional. A number between 1 and 500, default 25.
8
+ # @option folder_id: Optional. Possible values are unread (default), starred, archive, or a folder_id value from /api/1/folders/list.
9
+ # @option have: Optional. A concatenation of bookmark_id values that the client already has from the specified folder. See below.
10
+ def bookmarks(options={})
11
+ post('bookmarks/list', options)[2..-1]
12
+ end
13
+
14
+ # Updates the user’s reading progress on a single article.
15
+ # @param bookmark_id [String] The id of the bookmark to update.
16
+ # @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.
17
+ # @param progress_timestamp [Integer] The Unix timestamp value of the time that the progress was recorded.
18
+ def update_read_progress(bookmark_id, progress, progress_timestamp=Time.now)
19
+ post('bookmarks/update_read_progress', :bookmark_id => bookmark_id, :progress => progress, :progress_timestamp => progress_timestamp.to_i).first
20
+ end
21
+
22
+ # Adds a new unread bookmark to the user’s account.
23
+ # @param url [String] The url of the bookmark.
24
+ def add_bookmark(url, options={})
25
+ post('bookmarks/add', options.merge(:url => url)).first
26
+ end
27
+
28
+ # Permanently deletes the specified bookmark.
29
+ # This is NOT the same as Archive. Please be clear to users if you’re going to do this.
30
+ # @param bookmark_id [String] The id of the bookmark.
31
+ def delete_bookmark(bookmark_id)
32
+ post('bookmarks/delete', :bookmark_id => bookmark_id)
33
+ end
34
+
35
+ # Stars the specified bookmark.
36
+ # @param bookmark_id [String] The id of the bookmark.
37
+ def star(bookmark_id)
38
+ post('bookmarks/star', :bookmark_id => bookmark_id).first
39
+ end
40
+ alias :star_bookmark :star
41
+
42
+ # Un-stars the specified bookmark.
43
+ # @param bookmark_id [String] The id of the bookmark.
44
+ def unstar(bookmark_id)
45
+ post('bookmarks/unstar', :bookmark_id => bookmark_id).first
46
+ end
47
+ alias :unstar_bookmark :unstar
48
+
49
+ # Moves the specified bookmark to the Archive.
50
+ # @param bookmark_id [String] The id of the bookmark.
51
+ def archive(bookmark_id)
52
+ post('bookmarks/archive', :bookmark_id => bookmark_id).first
53
+ end
54
+ alias :archive_bookmark :archive
55
+
56
+ # Moves the specified bookmark to the top of the Unread folder.
57
+ # @param bookmark_id [String] The id of the bookmark.
58
+ def unarchive(bookmark_id)
59
+ post('bookmarks/unarchive', :bookmark_id => bookmark_id).first
60
+ end
61
+ alias :unarchive_bookmark :unarchive
62
+
63
+ # Moves the specified bookmark to a user-created folder.
64
+ # @param bookmark_id [String] The id of the bookmark.
65
+ # @param folder_id [String] The id of the folder to move the bookmark to.
66
+ def move(bookmark_id, folder_id)
67
+ post('bookmarks/move', :bookmark_id => bookmark_id, :folder_id => folder_id).first
68
+ end
69
+ alias :move_bookmark :move
70
+
71
+ # Returns the specified bookmark’s processed text-view HTML, which is
72
+ # always text/html encoded as UTF-8.
73
+ # @param bookmark_id [String] The id of the bookmark.
74
+ def text(bookmark_id)
75
+ post('bookmarks/get_text', { :bookmark_id => bookmark_id }, true).body
76
+ end
77
+ alias :get_text :text
78
+
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,34 @@
1
+ module Instapaper
2
+ class Client
3
+ # Defines methods related to folders
4
+ module Folder
5
+
6
+ # List the account’s user-created folders.
7
+ # @note This only includes organizational folders and does not include RSS-feed folders or starred-subscription folders
8
+ def folders
9
+ post('folders/list')
10
+ end
11
+
12
+ # Creates an organizational folder.
13
+ # @param title [String] The title of the folder to create
14
+ def add_folder(title)
15
+ post('folders/add', :title => title)
16
+ end
17
+
18
+ # Deletes the folder and moves any articles in it to the Archive.
19
+ # @param folder_id [String] The id of the folder.
20
+ def delete_folder(folder_id)
21
+ post('folders/delete', :folder_id => folder_id)
22
+ end
23
+
24
+ # Re-orders a user’s folders.
25
+ # @param order [Array] An array of folder_id:position pairs joined by commas.
26
+ # @example Ordering folder_ids 100, 200, and 300
27
+ # Instapaper.set_order(['100:1','200:2','300:3'])
28
+ def set_order(order=[])
29
+ post('folders/set_order', :order => order.join(','))
30
+ end
31
+
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,14 @@
1
+ module Instapaper
2
+ class Client
3
+ # Defines methods related to users
4
+ module User
5
+
6
+ # Gets an OAuth access token for a user.
7
+ def access_token(username, password)
8
+ response = post('oauth/access_token', { :x_auth_username => username, :x_auth_password => password, :x_auth_mode => "client_auth"}, true)
9
+ Hash[*response.body.split("&").map {|part| part.split("=") }.flatten]
10
+ end
11
+
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,88 @@
1
+ require 'instapaper/version'
2
+
3
+ module Instapaper
4
+ module Configuration
5
+ # An array of valid keys in the options hash when configuring a {Instapaper::API}
6
+ VALID_OPTIONS_KEYS = [
7
+ :adapter,
8
+ :consumer_key,
9
+ :consumer_secret,
10
+ :endpoint,
11
+ :oauth_token,
12
+ :oauth_token_secret,
13
+ :proxy,
14
+ :version,
15
+ :path_prefix,
16
+ :user_agent,
17
+ :connection_options].freeze
18
+
19
+ # The adapter that will be used to connect if none is set
20
+ #
21
+ # @note The default faraday adapter is Net::HTTP.
22
+ DEFAULT_ADAPTER = :net_http
23
+
24
+ # By default, don't set an application key
25
+ DEFAULT_CONSUMER_KEY = nil
26
+
27
+ # By default, don't set an application secret
28
+ DEFAULT_CONSUMER_SECRET = nil
29
+
30
+ # The endpoint that will be used to connect if none is set
31
+ DEFAULT_ENDPOINT = 'https://www.instapaper.com/'.freeze
32
+
33
+ # The version of the API.
34
+ DEFAULT_VERSION = '1'
35
+
36
+ DEFAULT_PATH_PREFIX = 'api/' + DEFAULT_VERSION + '/'
37
+
38
+ # By default, don't set a user oauth token
39
+ DEFAULT_OAUTH_TOKEN = nil
40
+
41
+ # By default, don't set a user oauth secret
42
+ DEFAULT_OAUTH_TOKEN_SECRET = nil
43
+
44
+ # By default, don't use a proxy server
45
+ DEFAULT_PROXY = nil
46
+
47
+ # The user agent that will be sent to the API endpoint if none is set
48
+ DEFAULT_USER_AGENT = "Instapaper Ruby Gem #{Instapaper::VERSION}".freeze
49
+
50
+ DEFAULT_CONNECTION_OPTIONS = {}
51
+
52
+ # @private
53
+ attr_accessor *VALID_OPTIONS_KEYS
54
+
55
+ # When this module is extended, set all configuration options to their default values
56
+ def self.extended(base)
57
+ base.reset
58
+ end
59
+
60
+ # Convenience method to allow configuration options to be set in a block
61
+ def configure
62
+ yield self
63
+ end
64
+
65
+ # Create a hash of options and their values
66
+ def options
67
+ options = {}
68
+ VALID_OPTIONS_KEYS.each{|k| options[k] = send(k) }
69
+ options
70
+ end
71
+
72
+ # Reset all configuration options to defaults
73
+ def reset
74
+ self.adapter = DEFAULT_ADAPTER
75
+ self.consumer_key = DEFAULT_CONSUMER_KEY
76
+ self.consumer_secret = DEFAULT_CONSUMER_SECRET
77
+ self.endpoint = DEFAULT_ENDPOINT
78
+ self.oauth_token = DEFAULT_OAUTH_TOKEN
79
+ self.oauth_token_secret = DEFAULT_OAUTH_TOKEN_SECRET
80
+ self.proxy = DEFAULT_PROXY
81
+ self.user_agent = DEFAULT_USER_AGENT
82
+ self.version = DEFAULT_VERSION
83
+ self.path_prefix = DEFAULT_PATH_PREFIX
84
+ self.connection_options = DEFAULT_CONNECTION_OPTIONS
85
+ self
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,35 @@
1
+ require 'faraday_middleware'
2
+ require 'faraday/response/raise_http_1xxx'
3
+
4
+ module Instapaper
5
+ # @private
6
+ module Connection
7
+ private
8
+
9
+ def connection(raw=false)
10
+ merged_options = connection_options.merge({
11
+ :headers => {
12
+ 'Accept' => "application/json",
13
+ 'User-Agent' => user_agent
14
+ },
15
+ :proxy => proxy,
16
+ :ssl => {:verify => false},
17
+ :url => api_endpoint
18
+ })
19
+
20
+ Faraday.new(merged_options) do |builder|
21
+ if authenticated?
22
+ builder.use Faraday::Request::OAuth, authentication
23
+ else
24
+ builder.use Faraday::Request::OAuth, consumer_tokens
25
+ end
26
+ builder.use Faraday::Request::Multipart
27
+ builder.use Faraday::Request::UrlEncoded
28
+ builder.use Faraday::Response::Rashify unless raw
29
+ builder.use Faraday::Response::ParseJson unless raw
30
+ builder.use Faraday::Response::RaiseHttp1xxx
31
+ builder.adapter(adapter)
32
+ end
33
+ end
34
+ end
35
+ end
@@ -1,9 +1,22 @@
1
- $:.unshift(File.dirname(__FILE__)) unless
2
- $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__)))
3
-
4
- require 'request/request_error'
5
- require 'request/request_errors'
6
- require 'request/request_validation'
7
- require 'request/abstract_request'
8
- require 'request/authentication_request'
9
- require 'request/add_url_request'
1
+ module Instapaper
2
+ # Defines HTTP request methods
3
+ module Request
4
+
5
+ # Perform an HTTP POST request
6
+ def post(path, options={}, raw=false)
7
+ request(:post, path, options, raw)
8
+ end
9
+
10
+ private
11
+
12
+ # Perform an HTTP request
13
+ def request(method, path, options, raw=false)
14
+ response = connection(raw).send(method) do |request|
15
+ request.path = path_prefix + path
16
+ request.body = options unless options.empty?
17
+ end
18
+ raw ? response : response.body
19
+ end
20
+
21
+ end
22
+ end
@@ -1,33 +1,3 @@
1
- # Copyright (c) 2009 Douglas Willcocks
2
- #
3
- # Permission is hereby granted, free of charge, to any person
4
- # obtaining a copy of this software and associated documentation
5
- # files (the "Software"), to deal in the Software without
6
- # restriction, including without limitation the rights to use,
7
- # copy, modify, merge, publish, distribute, sublicense, and/or sell
8
- # copies of the Software, and to permit persons to whom the
9
- # Software is furnished to do so, subject to the following
10
- # conditions:
11
- #
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
- #
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
17
- # OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
19
- # HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
20
- # WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
21
- # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
22
- # OTHER DEALINGS IN THE SOFTWARE.
23
-
24
1
  module Instapaper
25
- module VERSION #:nodoc:
26
- MAJOR = 0
27
- MINOR = 1
28
- TINY = 0
29
-
30
- STRING = [MAJOR, MINOR, TINY].join('.')
31
- self
32
- end
2
+ VERSION = "0.2.0"
33
3
  end
@@ -0,0 +1,22 @@
1
+ require 'spec_helper'
2
+
3
+ describe Faraday::Response do
4
+ before do
5
+ @client = Instapaper::Client.new
6
+ end
7
+
8
+ [1040, 1041, 1042, 1220, 1221, 1240, 1241, 1242, 1243, 1244, 1245, 1250,
9
+ 1251, 1252, 1500, 1550].each do |status|
10
+ context "when HTTP status is #{status}" do
11
+ before do
12
+ stub_post('folders/list').to_return(:status => status)
13
+ end
14
+
15
+ it "should raise Instapaper::Error error" do
16
+ lambda do
17
+ @client.folders
18
+ end.should raise_error(Instapaper::Error)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1 @@
1
+ oauth_token=aabbccdd&oauth_token_secret=efgh1234
@@ -0,0 +1 @@
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}]
@@ -0,0 +1 @@
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}]
@@ -0,0 +1,299 @@
1
+
2
+ <html>
3
+ <head>
4
+ <title>Ideo&#039;s Axioms for Starting Disruptive New Businesses</title>
5
+ <meta name="viewport" content="width=device-width; initial-scale=1.0; user-scalable=no; minimum-scale=1.0; maximum-scale=1.0;" />
6
+ <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
7
+ <meta name="robots" content="noindex"/>
8
+ <link rel="icon" href="/images/favicon.png"/>
9
+ <!-- IP:TITLE
10
+ Ideo's Axioms for Starting Disruptive New Businesses
11
+ /IP:TITLE -->
12
+ <!-- IP:IMAGES
13
+
14
+ /IP:IMAGES -->
15
+ <style type="text/css">
16
+ body {
17
+ font-family: Georgia;
18
+ font-size: 16px;
19
+ margin: 0px auto 0px auto;
20
+ width: 500px word-wrap: break-word;
21
+ }
22
+
23
+ h1 { font-size: 1.3em; }
24
+ h2 { font-size: 1.15em; }
25
+ h3, h4, h5, h6, h7 { font-size: 1.0em; }
26
+
27
+ img { border: 0; display: block; margin: 0.5em 0; }
28
+ pre, code { overflow: scroll; }
29
+ #story {
30
+ clear: both; padding: 0 10px; overflow: hidden; margin-bottom: 40px;
31
+ }
32
+
33
+ .bar {
34
+ color: #555;
35
+ font-family: 'Helvetica';
36
+ font-size: 11pt;
37
+ margin: 0 -20px;
38
+ padding: 10px 0;
39
+ }
40
+ .top { border-bottom: 2px solid #000; }
41
+
42
+ .top a {
43
+ display: block;
44
+ float: right;
45
+ text-decoration: none;
46
+ font-size: 11px;
47
+ background-color: #eee;
48
+ -webkit-border-radius: 8px;
49
+ -moz-border-radius: 8px;
50
+ padding: 2px 15px;
51
+ }
52
+
53
+ #story div {
54
+ margin: 1em 0;
55
+ }
56
+
57
+ .bottom {
58
+ border-top: 2px solid #000;
59
+ color: #555;
60
+ }
61
+
62
+ .bar a { color: #444; }
63
+
64
+ blockquote {
65
+ border-top: 1px solid #bbb;
66
+ border-bottom: 1px solid #bbb;
67
+ margin: 1.5em 0;
68
+ padding: 0.5em 0;
69
+ }
70
+ blockquote.short { font-style: italic; }
71
+
72
+ pre {
73
+ white-space: pre-wrap;
74
+ }
75
+
76
+ ul.bodytext, ol.bodytext {
77
+ list-style: none;
78
+ margin-left: 0;
79
+ padding-left: 0em;
80
+ }
81
+
82
+ </style>
83
+ </head>
84
+ <body onload="loadFont();">
85
+ <div class="bar top">
86
+ <a href="http://www.fastcodesign.com/1662169/ideos-axioms-for-starting-disruptive-new-businesses">View original</a>
87
+ <div class="sm">fastcodesign.com</div>
88
+ </div>
89
+
90
+ <div id="editing_controls" style="float: right; padding-top: 2px;">
91
+ </div>
92
+
93
+ <div id="story">
94
+
95
+
96
+ <div>
97
+ <div>
98
+ <div><a href="http://www.fastcodesign.com/1662169/ideos-axioms-for-starting-disruptive-new-businesses"><span>Back</span> to Fast Company</a></div>
99
+ </div>
100
+
101
+
102
+
103
+
104
+ <div>
105
+
106
+
107
+ <div><a href="http://www.fastcodesign.com/">Fast Company</a></div>
108
+ <div>
109
+
110
+
111
+ <div><a href="http://www.fastcodesign.com/12072010-tue-0" title="Issue Flag"><strong>Dec 04,
112
+ 2010</strong></a></div>
113
+ <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" />
114
+ <div>
115
+
116
+ <div>
117
+ <h1><span>Ideo&#8217;s Axioms for Starting Disruptive New
118
+ Businesses</span></h1>
119
+
120
+ <div>
121
+ <div>Don&#8217;t wait for perfection: Launch and
122
+ learn.</div>
123
+ <p><em>This is the first piece in our PATTERNS
124
+ 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">
125
+ here</a>.</em></p>
126
+ <p>How do you build a business in an unproven market? How do you
127
+ figure out what customers need when you’re delivering an
128
+ experience they’ve never seen before? You begin where service
129
+ and software companies have begun, by conducting fast, cheap
130
+ experiments that help you understand your customers. You build on
131
+ what you learn. In short, you prototype.</p>
132
+ <p>With ever-increasing competition, innovative businesses are
133
+ finding that in order to stay competitive their offerings need to
134
+ constantly evolve. And that to improve their offerings is to
135
+ encourage consumer participation. This helps them build a
136
+ competitive advantage by constantly revisiting what they deliver
137
+ and how they deliver it. They know that traditional market testing
138
+ will only validate their past successes. To understand the next big
139
+ thing, companies have to engage with customers and react to their
140
+ needs.</p>
141
+
142
+ <h2><strong>TAKE ACTION: Designing for Life&#8217;s Changes</strong></h2>
143
+ <p><br /><br /><strong>1. Go early, go often</strong><br />
144
+ Building experimentation into your business is harder than you
145
+ think. Start small and stay focused. Try everything, but
146
+ don’t try it all in one prototype.<br /><strong><br />
147
+ 2. Learning by doing</strong><br />
148
+ Build value for the business as you prototype. If you fail, what
149
+ will you have learned? What will you salvage?</p>
150
+ <p><strong>3. Inspiration through constraint</strong><br />
151
+ Don’t exhaust yourself searching for money and resources. The
152
+ tighter your constraints, the more creative your prototypes will
153
+ be.</p>
154
+ <p><strong>4. Open to opportunity</strong><br />
155
+ Look for unanticipated ways customers are using your offering.
156
+ Their improvisations may be the future of your business.</p>
157
+ <h2><strong>THE EVIDENCE: Stories from Around the
158
+ Globe</strong></h2>
159
+ <img src="http://images.fastcompany.com/upload/business-beta-platform.jpg" alt="" style="border: 0px;" />
160
+ <strong>Platform for Change</strong><br />
161
+ Companies like Apple and Facebook have learned to harness the
162
+ energy of outside developers to create new applications. By
163
+ allowing thousands of new applications to run on their platforms,
164
+ they create a Darwinian environment where only the fittest survive.
165
+ <p>Jeff, a developer, noticed that Facebook lacked reminders around
166
+ birthdays, so he created Birthday Alert. Customers quickly made it
167
+ one of the hottest apps on the platform. Instead of trying to guess
168
+ what type of functionality users wanted, Facebook just tapped into
169
+ smart developers like Jeff who built it for them.</p>
170
+ <p>Through this process, Facebook learned it needed a mechanism to
171
+ foster developers who could improve the overall ecosystem. Now the
172
+ Facebook Fund supports enterprising developers who are eager to
173
+ build their ideas.<br /><em><br />
174
+ How can you engage your customers and partners to help you
175
+ prototype new offerings?<br /></em></p>
176
+ <p><strong>Front-of-house Flexibility</strong><br />
177
+ The secret behind the unique feel of Whole Foods and Trader
178
+ Joe’s is how employees are empowered to cocreate the customer
179
+ experience. Each store establishes teams to figure out the best way
180
+ to serve customers, from the products they offer to the way
181
+ sections are organized. Each week, employees can see the results of
182
+ their experiments in the aisles.</p>
183
+ <p>Jesse recently joined the cheese department at Whole Foods and
184
+ one of his favorite jobs is to select the cheeses that customers
185
+ sample. He feels it helps set the mood of the entire store, and
186
+ when he nails the selection, the store usually sells the entire
187
+ stock. Giving teams the tools to constantly improve the business
188
+ creates an engaging and successful environment.<br /><em><br />
189
+ What control should you give up so your team is empowered?</em></p>
190
+ <img src="http://images.fastcompany.com/upload/business-beta-grassroots.jpg" alt="image" />
191
+ <br /><strong>Grassroots Growth</strong><br />
192
+ Ayr is a former scientist with an MBA from Harvard. After several
193
+ years with McKinsey, he decided to follow his dream to create a
194
+ chain of fast and friendly vegetarian restaurants.
195
+ <p>He could have hired a chef and tested his menu with focus
196
+ groups, but instead he decided that it would be better to run a lot
197
+ of experiments at low cost. So he launched his restaurant from a
198
+ food truck parked outside the MIT campus, updating his customers
199
+ about daily specials through text messages and blog posts.</p>
200
+ <p>After six months, the results have been phenomenal. By starting
201
+ small and prototyping, Ayr is learning while he shapes his
202
+ business. He’s adding additional trucks, developing permanent
203
+ spaces, and has begun to cater special events. Each experiment
204
+ brings him closer to his ultimate goal.</p>
205
+ <p><em>How can you intentionally limit your resources to create a
206
+ more inspired offering?</em></p>
207
+ <p><strong>Making Lemonade</strong><br />
208
+ Like many fashion houses, Gucci and Ann Taylor were hit hard during
209
+ the recent recession. The nation’s sudden shopping withdrawal
210
+ left many designers with too few retail orders to manufacture their
211
+ line. In similar circumstances, manufacturers will order the
212
+ additional garments and offer excess inventory in outlet malls and
213
+ discount retailers. This time things were different, demand was
214
+ much lower so the fashion houses got creative. Taking advantage of
215
+ empty retail space many designers negotiated short, temporary
216
+ leases in high-traffic areas. In this short stay space they opened
217
+ pop-up shops to connect with customers. The recession could have
218
+ distanced these designers from their customers but quick, nimble
219
+ moves created new opportunities to engage.<br /><em><br />
220
+ How can you turn your biggest challenge into an opportunity to try
221
+ something new?</em></p>
222
+ <p><strong>Real-time Results</strong><br />
223
+ Internet companies routinely use their constant connections with
224
+ customers to prototype new offerings. Companies like Google and
225
+ Amazon routinely select pools of users and change the functionality
226
+ in their products (e.g., you may be looking at a different Gmail
227
+ interface than your friends). Depending on specific behavioral
228
+ metrics, Google may change a product without ever directly asking
229
+ the customer. Smart and nimble businesses know that always-on and
230
+ always-accessible allows them to learn and evolve.<br /><em><br />
231
+ How can you experiment on the fly and learn without compromising
232
+ experience?</em></p>
233
+ <img src="http://images.fastcompany.com/upload/business-beta-frontofhouse.jpg" alt="image" />
234
+ <br /><strong>Ongoing Experimentation</strong><br />
235
+ McDonald’s has built prototyping into its organization. Since
236
+ the company does not want every employee in every store deviating
237
+ from service patterns, it has set up test restaurants to try new
238
+ menu items, new pricing strategies, and new food preparation
239
+ methods. This flexibility has paid off. McDonald&#8217;s has been able to
240
+ roll out worldwide menu expansions in just a few months&#8212;quite a
241
+ feat for a company that serves 47 million customers a day.
242
+ <p><em>How can you build experimentation into the culture of your
243
+ organization?</em></p>
244
+ <p><strong>Name my Book</strong><br />
245
+ Tim Ferriss loved the playful working title of his first book, Drug
246
+ Dealing for Fun and Profit, but it was too racy for Walmart and
247
+ other retailers. With the success of the book hinging on this
248
+ decision, Tim decided to prototype. He drafted a shortlist of
249
+ titles and bought Google AdWords. Each online click equaled one
250
+ vote. Within a week, he had his title, and <em>The 4-Hour Work
251
+ Week</em> was finally finished.</p>
252
+ <p><em>How much information do you need to make decisions? Can
253
+ prototyping help you get there faster?</em></p>
254
+ <h2><strong>Be a Pattern Spotter</strong></h2>
255
+ <p><br /><br />
256
+ Now that you&#8217;ve been exposed to a few different examples,
257
+ don’t be surprised if you start seeing Life&#8217;s Changes
258
+ patterns all around. Keep your eyes open and let us know what you
259
+ find, especially if it&#8217;s the next new pattern.</p>
260
+ <p><em>PATTERNS are a collection of shared thoughts, insights, and
261
+ observations gathered by IDEO through their work and the world
262
+ around them. Read more pieces from the series <a href="http://www.fastcodesign.com/tag/ideo-patterns">here</a>.</em></p>
263
+ <p><em><a href="http://www.fastcodesign.com/users/craney"><strong>Colin
264
+ Raney</strong></a> leads the Business Design Community within IDEO.
265
+ He specializes in designing new ventures for clients based on new
266
+ technologies or unique insights. He believes that good business is
267
+ good design&#8212;successful businesses require offerings, brands,
268
+ services, and strategies that complement each other.</em></p>
269
+
270
+ <div>
271
+ <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>
272
+ <div>
273
+ <h2><a href="http://www.fastcodesign.com/users/kclark" title="IDEO">IDEO</a></h2>
274
+ <p>IDEO is an award-winning global design firm that takes a
275
+ human-centered approach to helping organizations in the public and
276
+ 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>
277
+ <p>• <a href="http://www.twitter.com/IDEO" title="IDEO's twitter profile">Twitter</a></p>
278
+ </div>
279
+ </div>
280
+
281
+ </div>
282
+
283
+ </div>
284
+
285
+ <div><img src="http://www.fastcodesign.com/sites/fastcodesign.com/themes/co/images/blank.png" alt="image" /></div>
286
+ <div>
287
+ </div>
288
+
289
+ </div>
290
+ </div>
291
+ </div>
292
+ </div>
293
+
294
+ </div>
295
+
296
+ <div class="bar bottom">
297
+ </div>
298
+ </body>
299
+ </html>