mirror of
https://github.com/samsonjs/instapaper.git
synced 2026-03-25 08:55:49 +00:00
Compare commits
138 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 75e22840a6 | |||
| 5b4f47babf | |||
| 4de5a572c2 | |||
| 3b1e253972 | |||
| 01f1477a23 | |||
| 745b919620 | |||
| 3fda2e49a0 | |||
| a17773ee35 | |||
| dc3f1ab58e | |||
| e119e41ad3 | |||
| 46f789cd4c | |||
| a27a4128cb | |||
| 5c6557a276 | |||
| 07303c9be5 | |||
| 51f01888e0 | |||
| 9f9d59ccee | |||
| f23b579417 | |||
| 679bfec8df | |||
| 382764bb44 | |||
|
|
7371beb78e | ||
|
|
b069aed352 | ||
|
|
60300cffa5 | ||
| 2491f64040 | |||
|
|
1edfbecac6 | ||
|
|
3a6019cbdf | ||
|
|
41001eb745 | ||
|
|
c3664451d5 | ||
|
|
4e766f926c | ||
|
|
b5ef4469d5 | ||
|
|
d79456220e | ||
|
|
ad7769f54a | ||
|
|
bac041cd0f | ||
|
|
ec295145a2 | ||
|
|
32e65b12af | ||
|
|
7656dabac2 | ||
|
|
cb523577a3 | ||
|
|
7233d85c97 | ||
|
|
ea87e11216 | ||
|
|
0219e7e0c4 | ||
|
|
5bb9084f93 | ||
|
|
9c109f0da0 | ||
|
|
546d4ca55b | ||
|
|
19304cc274 | ||
|
|
9def2d016a | ||
|
|
3138d00398 | ||
|
|
298239a144 | ||
|
|
ff211c7700 | ||
|
|
cc49e06d87 | ||
|
|
cb1d2348a6 | ||
|
|
da254e4aab | ||
|
|
658bd54f49 | ||
|
|
3a0ca00e53 | ||
|
|
f2bef30802 | ||
|
|
affd1f775f | ||
|
|
f4cd48a42a | ||
|
|
e3f3f74d64 | ||
|
|
96490cc9db | ||
|
|
1da5c72eda | ||
|
|
0271919e18 | ||
|
|
954b777f67 | ||
|
|
51125c7e71 | ||
|
|
ef7d4458d0 | ||
|
|
0b2e842dcf | ||
|
|
45c110f96d | ||
|
|
13b278932b | ||
|
|
95b6be116b | ||
|
|
b2c0d13f01 | ||
|
|
327b6f4120 | ||
|
|
32512fe4f4 | ||
|
|
770a722c09 | ||
|
|
26288c458b | ||
|
|
97441310c6 | ||
|
|
d64dfa1085 | ||
|
|
c424057b07 | ||
|
|
83a44a6737 | ||
|
|
09f39c2715 | ||
|
|
4c9319b67c | ||
|
|
1c9a381fff | ||
|
|
2e4fbdfc27 | ||
|
|
45e2213afa | ||
|
|
7911a83137 | ||
|
|
f6fca37fd9 | ||
|
|
6509837fc1 | ||
|
|
8733b43acb | ||
|
|
95a2ad91b1 | ||
|
|
dab3c0d9de | ||
|
|
26f526fe07 | ||
|
|
76b09b30ae | ||
|
|
9c2babc55f | ||
|
|
f745e5bc73 | ||
|
|
55af622159 | ||
|
|
d57d1883f3 | ||
|
|
1bd5ce1728 | ||
|
|
c259665346 | ||
|
|
b254d2fd90 | ||
|
|
0a94dce118 | ||
|
|
6e606f516c | ||
|
|
e3bc6d384e | ||
|
|
c078efccdf | ||
|
|
de1196717d | ||
|
|
49ae0497c6 | ||
|
|
d1d493226c | ||
|
|
62098418d6 | ||
|
|
9661c2b879 | ||
|
|
b501a59a3d | ||
|
|
169a5f0dd9 | ||
|
|
366a963be8 | ||
|
|
3cb374cb4a | ||
|
|
000144ec60 | ||
|
|
ed59ddefdf | ||
|
|
9f2f9648ac | ||
|
|
39bc1dc1e7 | ||
|
|
775fdf854e | ||
|
|
9427881029 | ||
|
|
d66ab48a23 | ||
|
|
160b5e32c0 | ||
|
|
6271356847 | ||
|
|
813cfc7426 | ||
|
|
1c4e343b7e | ||
|
|
72a7e27143 | ||
|
|
dd1ad52936 | ||
|
|
4963767d10 | ||
|
|
52830de1fb | ||
|
|
29e49c1ed7 | ||
|
|
5161c68b5a | ||
|
|
af19561e93 | ||
|
|
d3f08bc508 | ||
|
|
da3498ac38 | ||
|
|
231dbf0021 | ||
|
|
f0f408b1c6 | ||
|
|
774de4dfbe | ||
|
|
9b11f2d27e | ||
|
|
f15c3cf1c3 | ||
|
|
72cd097620 | ||
|
|
19336aff26 | ||
|
|
31c6b9002a | ||
|
|
3f7fbdcdd3 | ||
|
|
887c1df76d |
70 changed files with 1956 additions and 1085 deletions
45
.github/workflows/ci.yml
vendored
Normal file
45
.github/workflows/ci.yml
vendored
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
name: CI
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# Set the job key. The key is displayed as the job name
|
||||||
|
# when a job name is not provided
|
||||||
|
tests:
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
ruby: ["4.0", "3.4", "3.3", "3.2"]
|
||||||
|
|
||||||
|
name: Tests - Ruby ${{ matrix.ruby }}
|
||||||
|
# Set the type of machine to run on
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
# Checks out a copy of your repository on the ubuntu-latest machine
|
||||||
|
- name: Checkout code
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- uses: actions/cache@v4
|
||||||
|
with:
|
||||||
|
path: vendor/bundle
|
||||||
|
key: ${{ runner.os }}-gems-${{ hashFiles('**/Gemfile.lock') }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-gems-
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: ${{ matrix.ruby }}
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Bundle install
|
||||||
|
run: |
|
||||||
|
bundle config path vendor/bundle
|
||||||
|
bundle install --jobs 8 --retry 3 --without development
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: |
|
||||||
|
bundle exec rake test
|
||||||
24
.github/workflows/rubocop.yml
vendored
Normal file
24
.github/workflows/rubocop.yml
vendored
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
name: rubocop
|
||||||
|
|
||||||
|
on: [pull_request]
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
rubocop:
|
||||||
|
name: rubocop
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Check out code
|
||||||
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
|
- name: Set up Ruby
|
||||||
|
uses: ruby/setup-ruby@v1
|
||||||
|
with:
|
||||||
|
ruby-version: "4.0"
|
||||||
|
bundler-cache: true
|
||||||
|
|
||||||
|
- name: Run rubocop
|
||||||
|
run: bundle exec rubocop
|
||||||
3
.rspec
3
.rspec
|
|
@ -1,3 +1,4 @@
|
||||||
--color
|
--color
|
||||||
--format=nested
|
|
||||||
--backtrace
|
--backtrace
|
||||||
|
--warnings
|
||||||
|
|
||||||
|
|
|
||||||
70
.rubocop.yml
Normal file
70
.rubocop.yml
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
AllCops:
|
||||||
|
Include:
|
||||||
|
- "./Rakefile"
|
||||||
|
- "instapaper.gemspec"
|
||||||
|
- "lib/**/*.rb"
|
||||||
|
- "spec/**/*.rb"
|
||||||
|
DisplayCopNames: true
|
||||||
|
NewCops: enable
|
||||||
|
|
||||||
|
Gemspec/RequireMFA:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/BlockLength:
|
||||||
|
Max: 36
|
||||||
|
Exclude:
|
||||||
|
- spec/**/*.rb
|
||||||
|
|
||||||
|
Metrics/BlockNesting:
|
||||||
|
Max: 2
|
||||||
|
|
||||||
|
Layout/LineLength:
|
||||||
|
AllowURI: true
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Metrics/MethodLength:
|
||||||
|
CountComments: false
|
||||||
|
Max: 10
|
||||||
|
|
||||||
|
Metrics/ParameterLists:
|
||||||
|
Max: 4
|
||||||
|
CountKeywordArgs: true
|
||||||
|
|
||||||
|
Style/CollectionMethods:
|
||||||
|
PreferredMethods:
|
||||||
|
map: "collect"
|
||||||
|
reduce: "inject"
|
||||||
|
find: "detect"
|
||||||
|
find_all: "select"
|
||||||
|
|
||||||
|
Style/Documentation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/DoubleNegation:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/FrozenStringLiteralComment:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/MutableConstant:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Style/NumericPredicate:
|
||||||
|
Enabled: false
|
||||||
|
|
||||||
|
Layout/SpaceInsideHashLiteralBraces:
|
||||||
|
EnforcedStyle: no_space
|
||||||
|
|
||||||
|
Style/TrailingCommaInArguments:
|
||||||
|
EnforcedStyleForMultiline: "comma"
|
||||||
|
|
||||||
|
Style/TrailingCommaInArrayLiteral:
|
||||||
|
EnforcedStyleForMultiline: "comma"
|
||||||
|
|
||||||
|
Style/TrailingCommaInHashLiteral:
|
||||||
|
EnforcedStyleForMultiline: "comma"
|
||||||
|
|
||||||
|
Naming/FileName:
|
||||||
|
Exclude:
|
||||||
|
- Rakefile
|
||||||
|
- Gemfile
|
||||||
|
|
@ -1,8 +0,0 @@
|
||||||
rvm:
|
|
||||||
- 1.8.7
|
|
||||||
- 1.9.2
|
|
||||||
- 1.9.3
|
|
||||||
- jruby
|
|
||||||
- rbx
|
|
||||||
- ree
|
|
||||||
- ruby-head
|
|
||||||
49
CLAUDE.md
Normal file
49
CLAUDE.md
Normal file
|
|
@ -0,0 +1,49 @@
|
||||||
|
# CLAUDE.md
|
||||||
|
|
||||||
|
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||||
|
|
||||||
|
## Development Commands
|
||||||
|
|
||||||
|
- **Run tests**: `bundle exec rake test` or `bundle exec rspec`
|
||||||
|
- **Run linting**: `bundle exec rubocop`
|
||||||
|
- **Run both tests and linting**: `bundle exec rake` (default task)
|
||||||
|
- **Build gem**: `bundle exec rake build`
|
||||||
|
- **Install dependencies**: `bundle install`
|
||||||
|
- **Generate documentation**: `bundle exec yard`
|
||||||
|
- **Interactive console**: `./script/console`
|
||||||
|
|
||||||
|
## Architecture Overview
|
||||||
|
|
||||||
|
This is a Ruby gem that provides a client library for Instapaper's Full API. The architecture follows a modular design:
|
||||||
|
|
||||||
|
### Core Components
|
||||||
|
|
||||||
|
- **`Instapaper::Client`** - Main entry point that users instantiate with OAuth credentials
|
||||||
|
- **API modules** - Separate modules for each API category (Accounts, Bookmarks, Folders, Highlights, OAuth) mixed into the Client
|
||||||
|
- **HTTP layer** - Custom HTTP handling using the `http.rb` gem with OAuth signature generation
|
||||||
|
- **Response objects** - Custom model classes (Bookmark, Folder, Highlight, etc.) using Virtus for attribute definition
|
||||||
|
|
||||||
|
### Key Patterns
|
||||||
|
|
||||||
|
- **Modular API design**: Each API endpoint category is in its own module (`lib/instapaper/api/`)
|
||||||
|
- **HTTP abstraction**: All API calls go through `Instapaper::HTTP::Request` which handles OAuth signing and response parsing
|
||||||
|
- **Custom response objects**: API responses are parsed into specific model classes rather than raw hashes
|
||||||
|
- **Method naming convention**: API methods are descriptive (`star_bookmark`, `add_folder`) rather than generic (`star`, `add`)
|
||||||
|
|
||||||
|
### Authentication Flow
|
||||||
|
|
||||||
|
Uses OAuth 1.0a with xAuth for obtaining access tokens. The client requires consumer key/secret and access token/secret for API calls.
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
- Uses RSpec with WebMock for stubbing HTTP requests
|
||||||
|
- Fixtures in `spec/fixtures/` contain sample API responses
|
||||||
|
- Test coverage tracked with SimpleCov (disabled in CI)
|
||||||
|
- Tests are organized by API module structure
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- **http.rb** - HTTP client library (replaced Faraday in v1.0)
|
||||||
|
- **virtus** - Attribute definitions for model objects
|
||||||
|
- **simple_oauth** - OAuth signature generation
|
||||||
|
- **addressable** - URI parsing and manipulation
|
||||||
22
Gemfile
22
Gemfile
|
|
@ -1,7 +1,21 @@
|
||||||
source :rubygems
|
source 'https://rubygems.org'
|
||||||
|
|
||||||
platforms :jruby do
|
gem 'rake'
|
||||||
gem 'jruby-openssl', '~> 0.7'
|
gem 'yard'
|
||||||
|
|
||||||
|
gem 'jruby-openssl', platforms: :jruby
|
||||||
|
gem 'json', platforms: :mri_19
|
||||||
|
|
||||||
|
group :development do
|
||||||
|
gem 'bundler'
|
||||||
|
gem 'kramdown'
|
||||||
end
|
end
|
||||||
|
|
||||||
gemspec
|
group :test do
|
||||||
|
gem 'rspec', '~> 3'
|
||||||
|
gem 'rubocop', '>= 0.27'
|
||||||
|
gem 'simplecov'
|
||||||
|
gem 'webmock', '>= 1.22'
|
||||||
|
end
|
||||||
|
|
||||||
|
gemspec
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
Copyright (c) 2011 Steve Agalloco
|
Copyright (c) 2015 Steve Agalloco
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
a copy of this software and associated documentation files (the
|
a copy of this software and associated documentation files (the
|
||||||
|
|
|
||||||
109
README.md
109
README.md
|
|
@ -1,23 +1,48 @@
|
||||||
# Instapaper
|
# 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.
|
> [!NOTE]
|
||||||
|
> This is a fork of the original [stve/instapaper](https://github.com/stve/instapaper) repository and is not available via RubyGems.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Instapaper is a ruby wrapper for interacting with [Instapaper's Full API](https://www.instapaper.com/api/full). Note that access to the Full API is restricted to Instapaper subscribers only.
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
gem install instapaper
|
Add this line to your application's Gemfile:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
gem 'instapaper', github: 'samsonjs/instapaper'
|
||||||
|
```
|
||||||
|
|
||||||
|
And then execute:
|
||||||
|
|
||||||
|
bundle install
|
||||||
|
|
||||||
## Usage
|
## Usage
|
||||||
|
|
||||||
Instapaper offers full support for all methods exposed through the Full API. Note that Instapaer does not support the request-token/authorize workflow. To obtain an access token, use the `access_token` method.
|
This library offers full support for all methods exposed through Instapaper's Full API. Note that Instapaper's API does not support the request-token/authorize workflow. To obtain an access token, use the `access_token` method.
|
||||||
|
|
||||||
|
## Changes in 1.0.0
|
||||||
|
|
||||||
|
If you've used earlier versions of this library, a lot has changed in version `1.x`. While not a total rewrite, I've changed a number of things based on my experience writing API libraries:
|
||||||
|
|
||||||
|
* swapped out Faraday for http.rb
|
||||||
|
* responses now return custom classes instead of Hashie::Rash objects
|
||||||
|
* most API methods are more clear as to their behavior (i.e., `#star_bookmark` instead of just `#star`)
|
||||||
|
* module-based configuration and invocation has been removed, you'll now need to instantiate an `Instapaper::Client` instead (see usage below)
|
||||||
|
* Improved error handling
|
||||||
|
* Updates for version 1.1 of Instapaper's API
|
||||||
|
* Support for Highlights API
|
||||||
|
|
||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.configure do |config|
|
client = Instapaper::Client.new do |client|
|
||||||
config.consumer_key = YOUR_CONSUMER_KEY
|
client.consumer_key = YOUR_CONSUMER_KEY
|
||||||
config.consumer_secret = YOUR_CONSUMER_SECRET
|
client.consumer_secret = YOUR_CONSUMER_SECRET
|
||||||
config.oauth_token = YOUR_OAUTH_TOKEN
|
client.oauth_token = YOUR_OAUTH_TOKEN
|
||||||
config.oauth_token_secret = YOUR_OAUTH_TOKEN_SECRET
|
client.oauth_token_secret = YOUR_OAUTH_TOKEN_SECRET
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
@ -26,13 +51,13 @@ end
|
||||||
To obtain an access token via xAuth:
|
To obtain an access token via xAuth:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.access_token(username, password)
|
client.access_token(username, password)
|
||||||
```
|
```
|
||||||
|
|
||||||
You can also verify credentials once you have received tokens:
|
You can also verify credentials once you have received tokens:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.verify_credentials
|
client.verify_credentials
|
||||||
```
|
```
|
||||||
|
|
||||||
## Bookmark Operations
|
## Bookmark Operations
|
||||||
|
|
@ -40,51 +65,56 @@ Instapaper.verify_credentials
|
||||||
Retrieve a list of bookmarks:
|
Retrieve a list of bookmarks:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.bookmarks
|
client.bookmarks
|
||||||
```
|
```
|
||||||
|
|
||||||
Add a new bookmark:
|
Add a new bookmark:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.add_bookmark('http://someurl.com', :title => 'This is the title', :description => 'This is the description')
|
bookmark = {
|
||||||
|
title: 'This is the title',
|
||||||
|
description: 'This is the description',
|
||||||
|
}
|
||||||
|
|
||||||
|
client.add_bookmark('http://someurl.com', bookmark)
|
||||||
```
|
```
|
||||||
|
|
||||||
Remove a bookmark:
|
Remove a bookmark:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.delete_bookmark(bookmark_id)
|
client.delete_bookmark(bookmark_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
Update read progress:
|
Update read progress:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.update_read_progress(bookmark_id, 0.5)
|
client.update_read_progress(bookmark_id, 0.5)
|
||||||
```
|
```
|
||||||
|
|
||||||
Star/Un-star a bookmark:
|
Star/Un-star a bookmark:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.star(bookmark_id)
|
client.star_bookmark(bookmark_id)
|
||||||
Instapaper.unstar(bookmark_id)
|
client.unstar_bookmark(bookmark_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
Archive/Un-archive a bookmark:
|
Archive/Un-archive a bookmark:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.archive(bookmark_id)
|
client.archive_bookmark(bookmark_id)
|
||||||
Instapaper.unarchive(bookmark_id)
|
client.unarchive_bookmark(bookmark_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
Move a bookmark to a folder:
|
Move a bookmark to a folder:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.move(bookmark_id, folder_id)
|
client.move_bookmark(bookmark_id, folder_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
Obtain the text of a bookmark:
|
Obtain the text of a bookmark:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.text(bookmark_id)
|
client.get_text(bookmark_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Folder Operations
|
## Folder Operations
|
||||||
|
|
@ -93,48 +123,51 @@ Instapaper.text(bookmark_id)
|
||||||
To obtain the list of folders:
|
To obtain the list of folders:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.folders
|
client.folders
|
||||||
```
|
```
|
||||||
|
|
||||||
You can add by passing a name:
|
You can add by passing a name:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.add_folder('eventmachine')
|
client.add_folder('eventmachine')
|
||||||
```
|
```
|
||||||
|
|
||||||
And remove folders by referencing a folder by it's id.
|
And remove folders by referencing a folder by it's id.
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.delete_folder(folder_id)
|
client.delete_folder(folder_id)
|
||||||
```
|
```
|
||||||
|
|
||||||
Lastly, the folders can be reordered:
|
Lastly, the folders can be reordered:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.set_order(['folder_id1:2','folder_id2:1'])
|
client.set_order(['folder_id1:2','folder_id2:1'])
|
||||||
```
|
```
|
||||||
|
|
||||||
## Restrictions
|
## Highlights Operations
|
||||||
|
|
||||||
Users without an Instapaper Subscription may only invoke the following calls:
|
Obtain highlights for a bookmark:
|
||||||
|
|
||||||
```ruby
|
```ruby
|
||||||
Instapaper.access_token
|
client.highlights(bookmark_id)
|
||||||
Instapaper.verify_credentials
|
|
||||||
Instapaper.add_bookmark
|
|
||||||
Instapaper.folders
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## <a name="build"></a>Build Status
|
Add a highlight for a bookmark:
|
||||||
[][travis]
|
|
||||||
|
|
||||||
[travis]: http://travis-ci.org/spagalloco/instapaper
|
```ruby
|
||||||
|
highlight = {
|
||||||
|
text: 'And so we beat on, boats against the current, borne back ceaselessly into the past.',
|
||||||
|
position: 20,
|
||||||
|
}
|
||||||
|
|
||||||
## <a name="dependencies"></a>Dependency Status
|
client.add_highlight(bookmark_id, highlight)
|
||||||
[][gemnasium]
|
```
|
||||||
|
|
||||||
[gemnasium]: https://gemnasium.com/spagalloco/instapaper
|
Remove a highlight:
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
client.delete_highlight(highlight_id)
|
||||||
|
```
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
|
|
@ -146,10 +179,10 @@ Instapaper.folders
|
||||||
* Make your feature addition or bug fix.
|
* Make your feature addition or bug fix.
|
||||||
* Add tests for it. This is important so I don't break it in a
|
* Add tests for it. This is important so I don't break it in a
|
||||||
future version unintentionally.
|
future version unintentionally.
|
||||||
* Commit, do not mess with rakefile, version, or history.
|
* Commit, do not mess with Rakefile, gem 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)
|
(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.
|
* Send me a pull request. Bonus points for topic branches.
|
||||||
|
|
||||||
## Copyright
|
## Copyright
|
||||||
|
|
||||||
Copyright (c) 2011 Steve Agalloco. See [LICENSE](https://github.com/spagalloco/instapaper/blob/master/LICENSE.md) for details.
|
Copyright (c) 2015 Steve Agalloco. See [LICENSE](https://github.com/samsonjs/instapaper/blob/master/LICENSE.md) for details.
|
||||||
|
|
|
||||||
12
Rakefile
12
Rakefile
|
|
@ -1,13 +1,15 @@
|
||||||
#!/usr/bin/env rake
|
|
||||||
|
|
||||||
require 'bundler'
|
require 'bundler'
|
||||||
Bundler::GemHelper.install_tasks
|
Bundler::GemHelper.install_tasks
|
||||||
|
|
||||||
require 'rspec/core/rake_task'
|
require 'rspec/core/rake_task'
|
||||||
RSpec::Core::RakeTask.new(:spec)
|
RSpec::Core::RakeTask.new(:spec)
|
||||||
|
|
||||||
task :test => :spec
|
task test: :spec
|
||||||
task :default => :spec
|
|
||||||
|
require 'rubocop/rake_task'
|
||||||
|
RuboCop::RakeTask.new
|
||||||
|
|
||||||
require 'yard'
|
require 'yard'
|
||||||
YARD::Rake::YardocTask.new
|
YARD::Rake::YardocTask.new
|
||||||
|
|
||||||
|
task default: %i[spec rubocop]
|
||||||
|
|
|
||||||
16
examples/authorization_flow.rb
Normal file
16
examples/authorization_flow.rb
Normal file
|
|
@ -0,0 +1,16 @@
|
||||||
|
require 'instapaper'
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
consumer_key: 'CONSUMER_KEY',
|
||||||
|
consumer_secret: 'CONSUMER_SECRET',
|
||||||
|
}
|
||||||
|
|
||||||
|
client = Instapaper::Client.new(credentials)
|
||||||
|
token = client.access_token('username', 'password')
|
||||||
|
# => #<Instapaper::Credentials oauth_token="710c838347ae178b4a92c6912e7e72e16c7e42f2" oauth_token_secret="XSXuU7TxBCzbKLjHH4R5iv8wfESLjeY9DI9sAdRBmCnui1E64m">
|
||||||
|
|
||||||
|
client.oauth_token = token.oauth_token
|
||||||
|
client.oauth_token_secret = token.oauth_token_secret
|
||||||
|
|
||||||
|
client.verify_credentials
|
||||||
|
# => #<Instapaper::User username="username" user_id=123456 type="user" subscription_is_active=false>
|
||||||
12
examples/obtain_a_list_of_folders.rb
Normal file
12
examples/obtain_a_list_of_folders.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
require 'instapaper'
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
consumer_key: 'CONSUMER_KEY',
|
||||||
|
consumer_secret: 'CONSUMER_SECRET',
|
||||||
|
oauth_token: 'OAUTH_TOKEN',
|
||||||
|
oauth_token_secret: 'OAUTH_TOKEN_SECRET',
|
||||||
|
}
|
||||||
|
|
||||||
|
client = Instapaper::Client.new(credentials)
|
||||||
|
client.folders
|
||||||
|
# => [#<Instapaper::Folder title="Design" display_title="Design" sync_to_mobile=true folder_id=1234567 position="1" type="folder" slug="design">]
|
||||||
10
examples/obtain_an_access_token.rb
Normal file
10
examples/obtain_an_access_token.rb
Normal file
|
|
@ -0,0 +1,10 @@
|
||||||
|
require 'instapaper'
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
consumer_key: 'CONSUMER_KEY',
|
||||||
|
consumer_secret: 'CONSUMER_SECRET',
|
||||||
|
}
|
||||||
|
|
||||||
|
client = Instapaper::Client.new(credentials)
|
||||||
|
client.access_token('username', 'password')
|
||||||
|
# => #<Instapaper::Credentials oauth_token="710c838347ae178b4a92c6912e7e72e16c7e42f2" oauth_token_secret="XSXuU7TxBCzbKLjHH4R5iv8wfESLjeY9DI9sAdRBmCnui1E64m">
|
||||||
12
examples/verify_oauth_credentials.rb
Normal file
12
examples/verify_oauth_credentials.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
require 'instapaper'
|
||||||
|
|
||||||
|
credentials = {
|
||||||
|
consumer_key: 'CONSUMER_KEY',
|
||||||
|
consumer_secret: 'CONSUMER_SECRET',
|
||||||
|
oauth_token: 'OAUTH_TOKEN',
|
||||||
|
oauth_token_secret: 'OAUTH_TOKEN_SECRET',
|
||||||
|
}
|
||||||
|
|
||||||
|
client = Instapaper::Client.new(credentials)
|
||||||
|
client.verify_credentials
|
||||||
|
# => #<Instapaper::User username="username" user_id=123456 type="user" subscription_is_active=false>
|
||||||
|
|
@ -1,32 +1,24 @@
|
||||||
# -*- encoding: utf-8 -*-
|
lib = File.expand_path('lib', __dir__)
|
||||||
$:.push File.expand_path("../lib", __FILE__)
|
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
||||||
require "instapaper/version"
|
require 'instapaper/version'
|
||||||
|
|
||||||
Gem::Specification.new do |gem|
|
Gem::Specification.new do |spec|
|
||||||
gem.name = "instapaper"
|
spec.add_dependency 'addressable', '~> 2.3'
|
||||||
gem.version = Instapaper::VERSION
|
spec.add_dependency 'base64', '~> 0.3'
|
||||||
|
spec.add_dependency 'dry-struct', '~> 1.0'
|
||||||
gem.author = "Steve Agalloco"
|
spec.add_dependency 'dry-types', '~> 1.0'
|
||||||
gem.email = "steve.agalloco@gmail.com"
|
spec.add_dependency 'http', '>= 2', '< 6'
|
||||||
gem.homepage = "https://github.com/spagalloco/instapaper"
|
spec.add_dependency 'multi_json', '~> 1'
|
||||||
gem.summary = %q{Ruby Instapaper Client}
|
spec.author = 'Steve Agalloco'
|
||||||
gem.description = %q{Ruby Instapaper Client}
|
spec.description = "Ruby Client for Instapaper's Full API"
|
||||||
|
spec.email = 'steve.agalloco@gmail.com'
|
||||||
gem.add_development_dependency('rake', '~> 0.9')
|
spec.files = %w[LICENSE.md README.md instapaper.gemspec] + Dir['lib/**/*.rb']
|
||||||
gem.add_development_dependency('rdiscount', '~> 1.6')
|
spec.homepage = 'https://github.com/stve/instapaper'
|
||||||
gem.add_development_dependency('rspec', '~> 2.7')
|
spec.licenses = %w[MIT]
|
||||||
gem.add_development_dependency('simplecov', '~> 0.5')
|
spec.name = 'instapaper'
|
||||||
gem.add_development_dependency('yard', '~> 0.7')
|
spec.require_paths = %w[lib]
|
||||||
gem.add_development_dependency('json', '>= 0')
|
spec.required_ruby_version = '>= 2.0.0'
|
||||||
gem.add_development_dependency('webmock', '~> 1.7')
|
spec.summary = 'Ruby Instapaper Client'
|
||||||
|
spec.version = Instapaper::VERSION
|
||||||
gem.add_runtime_dependency('faraday_middleware', '~> 0.7')
|
spec.metadata['rubygems_mfa_required'] = 'true'
|
||||||
gem.add_runtime_dependency('multi_json', '~> 1')
|
|
||||||
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
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,65 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,26 +1 @@
|
||||||
require 'instapaper/configuration'
|
|
||||||
require 'instapaper/client'
|
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
|
|
||||||
|
|
|
||||||
15
lib/instapaper/api.rb
Normal file
15
lib/instapaper/api.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
require 'instapaper/api/accounts'
|
||||||
|
require 'instapaper/api/bookmarks'
|
||||||
|
require 'instapaper/api/folders'
|
||||||
|
require 'instapaper/api/highlights'
|
||||||
|
require 'instapaper/api/oauth'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module API
|
||||||
|
include Instapaper::API::Accounts
|
||||||
|
include Instapaper::API::Bookmarks
|
||||||
|
include Instapaper::API::Folders
|
||||||
|
include Instapaper::API::Highlights
|
||||||
|
include Instapaper::API::OAuth
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,13 +1,13 @@
|
||||||
module Instapaper
|
require 'instapaper/user'
|
||||||
class Client
|
|
||||||
# Defines methods related to accounts
|
|
||||||
module Account
|
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module API
|
||||||
|
# Defines methods related to accounts
|
||||||
|
module Accounts
|
||||||
# Returns the currently logged in user.
|
# Returns the currently logged in user.
|
||||||
def verify_credentials
|
def verify_credentials
|
||||||
post('account/verify_credentials')
|
perform_post_with_object('/api/1.1/account/verify_credentials', {}, Instapaper::User)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
77
lib/instapaper/api/bookmarks.rb
Normal file
77
lib/instapaper/api/bookmarks.rb
Normal file
|
|
@ -0,0 +1,77 @@
|
||||||
|
require 'instapaper/bookmark'
|
||||||
|
require 'instapaper/bookmark_list'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module API
|
||||||
|
# Defines methods related to bookmarks
|
||||||
|
module Bookmarks
|
||||||
|
# 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.1/folders/list.
|
||||||
|
# @option have: Optional. A concatenation of bookmark_id values that the client already has from the specified folder. See below.
|
||||||
|
# @option highlights: Optional. A '-' delimited list of highlight IDs that the client already has from the specified bookmarks.
|
||||||
|
def bookmarks(options = {})
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/list', options, Instapaper::BookmarkList)
|
||||||
|
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)
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/update_read_progress', {bookmark_id: bookmark_id, progress: progress, progress_timestamp: progress_timestamp.to_i}, Instapaper::Bookmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Adds a new unread bookmark to the user's account.
|
||||||
|
# @param url [String] The url of the bookmark.
|
||||||
|
def add_bookmark(url, options = {})
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/add', options.merge(url: url), Instapaper::Bookmark)
|
||||||
|
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)
|
||||||
|
perform_post_with_objects('/api/1.1/bookmarks/delete', {bookmark_id: bookmark_id}, Array)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Stars the specified bookmark.
|
||||||
|
# @param bookmark_id [String] The id of the bookmark.
|
||||||
|
def star_bookmark(bookmark_id)
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/star', {bookmark_id: bookmark_id}, Instapaper::Bookmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Un-stars the specified bookmark.
|
||||||
|
# @param bookmark_id [String] The id of the bookmark.
|
||||||
|
def unstar_bookmark(bookmark_id)
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/unstar', {bookmark_id: bookmark_id}, Instapaper::Bookmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Moves the specified bookmark to the Archive.
|
||||||
|
# @param bookmark_id [String] The id of the bookmark.
|
||||||
|
def archive_bookmark(bookmark_id)
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/archive', {bookmark_id: bookmark_id}, Instapaper::Bookmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Moves the specified bookmark to the top of the Unread folder.
|
||||||
|
# @param bookmark_id [String] The id of the bookmark.
|
||||||
|
def unarchive_bookmark(bookmark_id)
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/unarchive', {bookmark_id: bookmark_id}, Instapaper::Bookmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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(bookmark_id, folder_id)
|
||||||
|
perform_post_with_object('/api/1.1/bookmarks/move', {bookmark_id: bookmark_id, folder_id: folder_id}, Instapaper::Bookmark)
|
||||||
|
end
|
||||||
|
|
||||||
|
# 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 get_text(bookmark_id)
|
||||||
|
perform_post_with_unparsed_response('/api/1.1/bookmarks/get_text', bookmark_id: bookmark_id)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,34 +1,35 @@
|
||||||
module Instapaper
|
require 'instapaper/folder'
|
||||||
class Client
|
|
||||||
# Defines methods related to folders
|
|
||||||
module Folder
|
|
||||||
|
|
||||||
# List the account’s user-created folders.
|
module Instapaper
|
||||||
|
module API
|
||||||
|
# Defines methods related to folders
|
||||||
|
module Folders
|
||||||
|
# List the account's user-created folders.
|
||||||
# @note This only includes organizational folders and does not include RSS-feed folders or starred-subscription folders
|
# @note This only includes organizational folders and does not include RSS-feed folders or starred-subscription folders
|
||||||
def folders
|
def folders
|
||||||
post('folders/list')
|
perform_post_with_objects('/api/1.1/folders/list', {}, Instapaper::Folder)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Creates an organizational folder.
|
# Creates an organizational folder.
|
||||||
# @param title [String] The title of the folder to create
|
# @param title [String] The title of the folder to create
|
||||||
def add_folder(title)
|
def add_folder(title)
|
||||||
post('folders/add', :title => title)
|
perform_post_with_object('/api/1.1/folders/add', {title: title}, Instapaper::Folder)
|
||||||
end
|
end
|
||||||
|
|
||||||
# Deletes the folder and moves any articles in it to the Archive.
|
# Deletes the folder and moves any articles in it to the Archive.
|
||||||
# @param folder_id [String] The id of the folder.
|
# @param folder_id [String] The id of the folder.
|
||||||
def delete_folder(folder_id)
|
def delete_folder(folder_id) # rubocop:disable Naming/PredicateMethod
|
||||||
post('folders/delete', :folder_id => folder_id)
|
perform_post_with_unparsed_response('/api/1.1/folders/delete', folder_id: folder_id)
|
||||||
|
true
|
||||||
end
|
end
|
||||||
|
|
||||||
# Re-orders a user’s folders.
|
# Re-orders a user's folders.
|
||||||
# @param order [Array] An array of folder_id:position pairs joined by commas.
|
# @param order [Array] An array of folder_id:position pairs joined by commas.
|
||||||
# @example Ordering folder_ids 100, 200, and 300
|
# @example Ordering folder_ids 100, 200, and 300
|
||||||
# Instapaper.set_order(['100:1','200:2','300:3'])
|
# Instapaper.set_order(['100:1','200:2','300:3'])
|
||||||
def set_order(order=[])
|
def set_order(order = [])
|
||||||
post('folders/set_order', :order => order.join(','))
|
perform_post_with_objects('/api/1.1/folders/set_order', {order: order.join(',')}, Instapaper::Folder)
|
||||||
end
|
end
|
||||||
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
33
lib/instapaper/api/highlights.rb
Normal file
33
lib/instapaper/api/highlights.rb
Normal file
|
|
@ -0,0 +1,33 @@
|
||||||
|
require 'instapaper/highlight'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module API
|
||||||
|
# Defines methods related to highlights
|
||||||
|
module Highlights
|
||||||
|
# List highlights for a bookmark
|
||||||
|
# @param bookmark_id [String, Integer]
|
||||||
|
def highlights(bookmark_id)
|
||||||
|
perform_get_with_objects("/api/1.1/bookmarks/#{bookmark_id}/highlights", {}, Instapaper::Highlight)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new highlight
|
||||||
|
# @note Non-subscribers are limited to 5 highlights per month.
|
||||||
|
# @param bookmark_id [String, Integer]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @option options [String] :text The text for the highlight (HTML tags in text parameter should be unescaped.)
|
||||||
|
# @option options [String, Integer] :posiiton The 0-indexed position of text in the content. Defaults to 0.
|
||||||
|
# @return [Instapaper::Highlight]
|
||||||
|
def add_highlight(bookmark_id, options = {})
|
||||||
|
perform_post_with_object("/api/1.1/bookmarks/#{bookmark_id}/highlight", options, Instapaper::Highlight)
|
||||||
|
end
|
||||||
|
|
||||||
|
# Delete a highlight
|
||||||
|
# @param highlight_id [String, Integer]
|
||||||
|
# @return [Boolean]
|
||||||
|
def delete_highlight(highlight_id, options = {}) # rubocop:disable Naming/PredicateMethod
|
||||||
|
perform_post_with_unparsed_response("/api/1.1/highlights/#{highlight_id}/delete", options)
|
||||||
|
true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
lib/instapaper/api/oauth.rb
Normal file
18
lib/instapaper/api/oauth.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
require 'instapaper/credentials'
|
||||||
|
require 'instapaper/http/qline_parser'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module API
|
||||||
|
# Defines methods related to OAuth
|
||||||
|
module OAuth
|
||||||
|
# Gets an OAuth access token for a user.
|
||||||
|
def access_token(username, password)
|
||||||
|
response = perform_post_with_unparsed_response('/api/1.1/oauth/access_token', x_auth_username: username, x_auth_password: password, x_auth_mode: 'client_auth')
|
||||||
|
parsed_response = QLineParser.parse(response)
|
||||||
|
raise Instapaper::Error::OAuthError, parsed_response['error'] if parsed_response.key?('error')
|
||||||
|
|
||||||
|
Instapaper::Credentials.new(parsed_response)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,32 +0,0 @@
|
||||||
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
|
|
||||||
22
lib/instapaper/bookmark.rb
Normal file
22
lib/instapaper/bookmark.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require 'instapaper/types'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
class Bookmark < Dry::Struct
|
||||||
|
include Types
|
||||||
|
|
||||||
|
transform_keys(&:to_sym)
|
||||||
|
|
||||||
|
attribute :type, Types::String
|
||||||
|
attribute :bookmark_id, Types::Integer
|
||||||
|
attribute :url, Types::String
|
||||||
|
attribute :title, Types::String
|
||||||
|
attribute? :description, Types::String
|
||||||
|
attribute? :instapaper_hash, Types::String
|
||||||
|
attribute? :private_source, Types::String
|
||||||
|
attribute? :progress_timestamp, Types::UnixTime
|
||||||
|
attribute? :time, Types::UnixTime
|
||||||
|
attribute? :progress, Types::StringOrInteger
|
||||||
|
attribute? :starred, Types::StringOrInteger
|
||||||
|
end
|
||||||
|
end
|
||||||
22
lib/instapaper/bookmark_list.rb
Normal file
22
lib/instapaper/bookmark_list.rb
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require 'instapaper/types'
|
||||||
|
require 'instapaper/bookmark'
|
||||||
|
require 'instapaper/highlight'
|
||||||
|
require 'instapaper/user'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
class BookmarkList < Dry::Struct
|
||||||
|
include Types
|
||||||
|
|
||||||
|
transform_keys(&:to_sym)
|
||||||
|
|
||||||
|
attribute :user, Instapaper::User
|
||||||
|
attribute :bookmarks, Types::Array.of(Instapaper::Bookmark)
|
||||||
|
attribute :highlights, Types::Array.of(Instapaper::Highlight)
|
||||||
|
attribute? :delete_ids, Types::Array.of(Types::Integer).optional.default([].freeze)
|
||||||
|
|
||||||
|
def each(&block)
|
||||||
|
bookmarks.each(&block)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,43 +1,57 @@
|
||||||
require 'instapaper/connection'
|
require 'instapaper/api'
|
||||||
require 'instapaper/request'
|
require 'instapaper/http/utils'
|
||||||
require 'instapaper/authentication'
|
require 'instapaper/version'
|
||||||
|
|
||||||
module Instapaper
|
module Instapaper
|
||||||
# Wrapper for the Instapaper REST API
|
# Wrapper for the Instapaper REST API
|
||||||
class Client
|
class Client
|
||||||
# @private
|
include Instapaper::API
|
||||||
attr_accessor *Configuration::VALID_OPTIONS_KEYS
|
include Instapaper::HTTP::Utils
|
||||||
|
|
||||||
alias :api_endpoint :endpoint
|
attr_accessor :oauth_token, :oauth_token_secret, :consumer_key, :consumer_secret, :proxy
|
||||||
alias :api_version :version
|
attr_writer :user_agent
|
||||||
|
|
||||||
# Creates a new API
|
# Initializes a new Client object
|
||||||
def initialize(options={})
|
#
|
||||||
options = Instapaper.options.merge(options)
|
# @param options [Hash]
|
||||||
Configuration::VALID_OPTIONS_KEYS.each do |key|
|
# @return [Instapaper::Client]
|
||||||
send("#{key}=", options[key])
|
def initialize(options = {})
|
||||||
end
|
@oauth_token = options[:oauth_token]
|
||||||
|
@oauth_token_secret = options[:oauth_token_secret]
|
||||||
|
@consumer_key = options[:consumer_key]
|
||||||
|
@consumer_secret = options[:consumer_secret]
|
||||||
|
@proxy = options[:proxy]
|
||||||
|
yield(self) if block_given?
|
||||||
end
|
end
|
||||||
|
|
||||||
def endpoint_with_prefix
|
# @return [String]
|
||||||
api_endpoint + path_prefix
|
def user_agent
|
||||||
|
@user_agent ||= "InstapaperRubyGem/#{Instapaper::VERSION}"
|
||||||
end
|
end
|
||||||
|
|
||||||
include Connection
|
# Authentication hash
|
||||||
include Request
|
#
|
||||||
include Authentication
|
# @return [Hash]
|
||||||
|
def credentials
|
||||||
|
{
|
||||||
|
consumer_key: @consumer_key,
|
||||||
|
consumer_secret: @consumer_secret,
|
||||||
|
oauth_token: @oauth_token,
|
||||||
|
oauth_token_secret: @oauth_token_secret,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
# Require client method modules after initializing the Client class in
|
# @return [Hash]
|
||||||
# order to avoid a superclass mismatch error, allowing those modules to be
|
def consumer_credentials
|
||||||
# Client-namespaced.
|
{
|
||||||
require 'instapaper/client/account'
|
consumer_key: @consumer_key,
|
||||||
require 'instapaper/client/user'
|
consumer_secret: @consumer_secret,
|
||||||
require 'instapaper/client/bookmark'
|
}
|
||||||
require 'instapaper/client/folder'
|
end
|
||||||
|
|
||||||
include Instapaper::Client::Account
|
# @return [Boolean]
|
||||||
include Instapaper::Client::User
|
def credentials?
|
||||||
include Instapaper::Client::Bookmark
|
credentials.values.all?
|
||||||
include Instapaper::Client::Folder
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
|
||||||
|
|
@ -1,81 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,19 +0,0 @@
|
||||||
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)
|
|
||||||
params = response.body.split("&")
|
|
||||||
values = params.map {|part| part.split("=") }.flatten
|
|
||||||
if values.length == 1
|
|
||||||
values.unshift('error')
|
|
||||||
end
|
|
||||||
Hash[*values]
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
@ -1,88 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,35 +0,0 @@
|
||||||
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
|
|
||||||
13
lib/instapaper/credentials.rb
Normal file
13
lib/instapaper/credentials.rb
Normal file
|
|
@ -0,0 +1,13 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require 'instapaper/types'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
class Credentials < Dry::Struct
|
||||||
|
include Types
|
||||||
|
|
||||||
|
transform_keys(&:to_sym)
|
||||||
|
|
||||||
|
attribute :oauth_token, Types::String
|
||||||
|
attribute :oauth_token_secret, Types::String
|
||||||
|
end
|
||||||
|
end
|
||||||
115
lib/instapaper/error.rb
Normal file
115
lib/instapaper/error.rb
Normal file
|
|
@ -0,0 +1,115 @@
|
||||||
|
module Instapaper
|
||||||
|
# Custom error class for rescuing from all Instapaper errors
|
||||||
|
class Error < StandardError
|
||||||
|
# @return [Integer]
|
||||||
|
attr_reader :code
|
||||||
|
|
||||||
|
ClientError = Class.new(self)
|
||||||
|
ServerError = Class.new(self)
|
||||||
|
ServiceUnavailableError = Class.new(self)
|
||||||
|
BookmarkError = Class.new(self)
|
||||||
|
FolderError = Class.new(self)
|
||||||
|
HighlightError = Class.new(self)
|
||||||
|
OAuthError = Class.new(self)
|
||||||
|
|
||||||
|
CLIENT_ERRORS = {
|
||||||
|
400 => 'Bad Request',
|
||||||
|
401 => 'Unauthorized',
|
||||||
|
402 => 'Payment Required',
|
||||||
|
403 => 'Forbidden',
|
||||||
|
404 => 'Not Found',
|
||||||
|
405 => 'Method Not Allowed',
|
||||||
|
}
|
||||||
|
|
||||||
|
SERVER_ERRORS = {
|
||||||
|
500 => 'Internal Server Error',
|
||||||
|
501 => 'Not Implemented',
|
||||||
|
502 => 'Bad Gateway',
|
||||||
|
503 => 'Service Unavailable',
|
||||||
|
504 => 'Gateway Timeout',
|
||||||
|
}
|
||||||
|
|
||||||
|
SERVICE_ERRORS = {
|
||||||
|
1040 => 'Rate-limit exceeded',
|
||||||
|
1041 => 'Premium account required',
|
||||||
|
1042 => 'Application is suspended',
|
||||||
|
1500 => 'Unexpected service error',
|
||||||
|
1550 => 'Error generating text version of this URL',
|
||||||
|
}
|
||||||
|
|
||||||
|
BOOKMARK_ERRORS = {
|
||||||
|
1220 => 'Domain requires full content to be supplied',
|
||||||
|
1221 => 'Domain has opted out of Instapaper compatibility',
|
||||||
|
1240 => 'Invalid URL specified',
|
||||||
|
1241 => 'Invalid or missing bookmark_id',
|
||||||
|
1242 => 'Invalid or missing folder_id',
|
||||||
|
1243 => 'Invalid or missing progress',
|
||||||
|
1244 => 'Invalid or missing progress_timestamp',
|
||||||
|
1245 => 'Private bookmarks require supplied content',
|
||||||
|
1250 => 'Unexpected error when saving bookmark',
|
||||||
|
}
|
||||||
|
|
||||||
|
FOLDER_ERRORS = {
|
||||||
|
1250 => 'Invalid or missing title',
|
||||||
|
1251 => 'User already has a folder with this title',
|
||||||
|
1252 => 'Cannot add bookmarks to this folder',
|
||||||
|
}
|
||||||
|
|
||||||
|
HIGHLIGHT_ERRORS = {
|
||||||
|
1600 => 'Cannot create highlight with empty text',
|
||||||
|
1601 => 'Duplicate highlight',
|
||||||
|
}
|
||||||
|
|
||||||
|
CODES = [
|
||||||
|
CLIENT_ERRORS,
|
||||||
|
SERVER_ERRORS,
|
||||||
|
SERVICE_ERRORS,
|
||||||
|
BOOKMARK_ERRORS,
|
||||||
|
FOLDER_ERRORS,
|
||||||
|
HIGHLIGHT_ERRORS,
|
||||||
|
].collect(&:keys).flatten
|
||||||
|
|
||||||
|
HTTP_ERRORS = CLIENT_ERRORS.merge(SERVER_ERRORS)
|
||||||
|
|
||||||
|
# Create a new error from an HTTP response
|
||||||
|
#
|
||||||
|
# @param response [HTTP::Response]
|
||||||
|
# @return [Instapaper::Error]
|
||||||
|
def self.from_response(code, path)
|
||||||
|
if (HTTP_ERRORS.keys + SERVICE_ERRORS.keys).include?(code)
|
||||||
|
from_response_code(code)
|
||||||
|
else
|
||||||
|
case path
|
||||||
|
when /highlights/ then HighlightError.new(HIGHLIGHT_ERRORS[code], code)
|
||||||
|
when /bookmarks/ then BookmarkError.new(BOOKMARK_ERRORS[code], code)
|
||||||
|
when /folders/ then FolderError.new(FOLDER_ERRORS[code], code)
|
||||||
|
else new('Unknown Error', code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Create a new error from an HTTP response code
|
||||||
|
#
|
||||||
|
# @param code [Integer]
|
||||||
|
# @return [Instapaper::Error]
|
||||||
|
def self.from_response_code(code)
|
||||||
|
if CLIENT_ERRORS.keys.include?(code)
|
||||||
|
ClientError.new(CLIENT_ERRORS[code], code)
|
||||||
|
elsif SERVER_ERRORS.keys.include?(code)
|
||||||
|
ServerError.new(SERVER_ERRORS[code], code)
|
||||||
|
elsif SERVICE_ERRORS.keys.include?(code)
|
||||||
|
new(SERVICE_ERRORS[code], code)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Initializes a new Error object
|
||||||
|
#
|
||||||
|
# @param message [Exception, String]
|
||||||
|
# @param code [Integer]
|
||||||
|
# @return [Instapaper::Error]
|
||||||
|
def initialize(message = '', code = nil)
|
||||||
|
super(message)
|
||||||
|
@code = code
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
18
lib/instapaper/folder.rb
Normal file
18
lib/instapaper/folder.rb
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require 'instapaper/types'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
class Folder < Dry::Struct
|
||||||
|
include Types
|
||||||
|
|
||||||
|
transform_keys(&:to_sym)
|
||||||
|
|
||||||
|
attribute :title, Types::String
|
||||||
|
attribute? :display_title, Types::String
|
||||||
|
attribute :sync_to_mobile, Types::BooleanFlag
|
||||||
|
attribute :folder_id, Types::Integer
|
||||||
|
attribute :position, Types::Coercible::Float
|
||||||
|
attribute :type, Types::String
|
||||||
|
attribute? :slug, Types::String
|
||||||
|
end
|
||||||
|
end
|
||||||
17
lib/instapaper/highlight.rb
Normal file
17
lib/instapaper/highlight.rb
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require 'instapaper/types'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
class Highlight < Dry::Struct
|
||||||
|
include Types
|
||||||
|
|
||||||
|
transform_keys(&:to_sym)
|
||||||
|
|
||||||
|
attribute :type, Types::String
|
||||||
|
attribute :highlight_id, Types::Integer
|
||||||
|
attribute :bookmark_id, Types::Integer
|
||||||
|
attribute :text, Types::String
|
||||||
|
attribute :position, Types::Integer
|
||||||
|
attribute :time, Types::UnixTime
|
||||||
|
end
|
||||||
|
end
|
||||||
45
lib/instapaper/http/headers.rb
Normal file
45
lib/instapaper/http/headers.rb
Normal file
|
|
@ -0,0 +1,45 @@
|
||||||
|
require 'addressable/uri'
|
||||||
|
require 'base64'
|
||||||
|
require 'instapaper/oauth'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module HTTP
|
||||||
|
class Headers
|
||||||
|
def initialize(client, request_method, url, options = {})
|
||||||
|
@client = client
|
||||||
|
@request_method = request_method.to_sym
|
||||||
|
@uri = Addressable::URI.parse(url)
|
||||||
|
@options = options
|
||||||
|
end
|
||||||
|
|
||||||
|
def request_headers
|
||||||
|
{
|
||||||
|
user_agent: @client.user_agent,
|
||||||
|
authorization: oauth_header,
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def oauth_header
|
||||||
|
Instapaper::OAuth::Header.new(@request_method, @uri, @options, credentials.merge(ignore_extra_keys: true))
|
||||||
|
end
|
||||||
|
|
||||||
|
# Authentication hash
|
||||||
|
#
|
||||||
|
# @return [Hash]
|
||||||
|
def credentials
|
||||||
|
if @client.credentials?
|
||||||
|
{
|
||||||
|
consumer_key: @client.consumer_key,
|
||||||
|
consumer_secret: @client.consumer_secret,
|
||||||
|
token: @client.oauth_token,
|
||||||
|
token_secret: @client.oauth_token_secret,
|
||||||
|
}
|
||||||
|
else
|
||||||
|
@client.consumer_credentials
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
9
lib/instapaper/http/qline_parser.rb
Normal file
9
lib/instapaper/http/qline_parser.rb
Normal file
|
|
@ -0,0 +1,9 @@
|
||||||
|
module Instapaper
|
||||||
|
class QLineParser
|
||||||
|
def self.parse(response)
|
||||||
|
values = response.split('&').map { |part| part.split('=') }.flatten
|
||||||
|
values.unshift('error') if values.length == 1
|
||||||
|
Hash[*values]
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
41
lib/instapaper/http/request.rb
Normal file
41
lib/instapaper/http/request.rb
Normal file
|
|
@ -0,0 +1,41 @@
|
||||||
|
require 'addressable/uri'
|
||||||
|
require 'http'
|
||||||
|
require 'instapaper/http/headers'
|
||||||
|
require 'instapaper/http/response'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module HTTP
|
||||||
|
class Request
|
||||||
|
BASE_URL = 'https://www.instapaper.com'
|
||||||
|
attr_accessor :client, :headers, :options, :path, :request_method, :uri
|
||||||
|
|
||||||
|
# @param client [Instapaper::Client]
|
||||||
|
# @param request_method [String, Symbol]
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @return [Instapaper::HTTP::Request]
|
||||||
|
def initialize(client, request_method, path, options = {})
|
||||||
|
@client = client
|
||||||
|
@request_method = request_method
|
||||||
|
@uri = Addressable::URI.parse(path.start_with?('http') ? path : BASE_URL + path)
|
||||||
|
@path = uri.path
|
||||||
|
@options = options
|
||||||
|
end
|
||||||
|
|
||||||
|
# @return [Array, Hash]
|
||||||
|
def perform
|
||||||
|
raw = @options.delete(:raw)
|
||||||
|
response = Instapaper::HTTP::Response.new(perform_request, path, raw)
|
||||||
|
response.valid? && response.body
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def perform_request
|
||||||
|
@headers = Instapaper::HTTP::Headers.new(@client, @request_method, @uri, @options).request_headers
|
||||||
|
options_key = @request_method == :get ? :params : :form
|
||||||
|
::HTTP.headers(@headers).public_send(@request_method, @uri.to_s, options_key => @options)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
65
lib/instapaper/http/response.rb
Normal file
65
lib/instapaper/http/response.rb
Normal file
|
|
@ -0,0 +1,65 @@
|
||||||
|
require 'json'
|
||||||
|
require 'instapaper/error'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module HTTP
|
||||||
|
class Response
|
||||||
|
attr_reader :response, :raw_format, :path
|
||||||
|
|
||||||
|
# TODO: Change this to a keyword argument (needs a major version bump)
|
||||||
|
def initialize(response, path, raw_format = false) # rubocop:disable Style/OptionalBooleanParameter
|
||||||
|
@response = response
|
||||||
|
@path = path
|
||||||
|
@raw_format = raw_format
|
||||||
|
end
|
||||||
|
|
||||||
|
def body
|
||||||
|
raw_format ? response.to_s : parsed
|
||||||
|
end
|
||||||
|
|
||||||
|
def valid?
|
||||||
|
!error?
|
||||||
|
end
|
||||||
|
|
||||||
|
def error?
|
||||||
|
fail_if_body_unparseable unless raw_format
|
||||||
|
fail_if_body_contains_error
|
||||||
|
fail_if_http_error
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def parsed
|
||||||
|
@parsed ||= begin
|
||||||
|
response.parse(:json)
|
||||||
|
rescue StandardError
|
||||||
|
response.body
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fail_if_http_error
|
||||||
|
return if response.status.ok?
|
||||||
|
|
||||||
|
if Instapaper::Error::CODES.include?(response.status.code) # rubocop:disable Style/GuardClause
|
||||||
|
raise Instapaper::Error.from_response(response.status.code, path)
|
||||||
|
else
|
||||||
|
raise Instapaper::Error, 'Unknown Error'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def fail_if_body_unparseable
|
||||||
|
response.parse(:json)
|
||||||
|
rescue JSON::ParserError
|
||||||
|
raise Instapaper::Error::ServiceUnavailableError
|
||||||
|
end
|
||||||
|
|
||||||
|
def fail_if_body_contains_error
|
||||||
|
return unless parsed.is_a?(Array)
|
||||||
|
return if parsed.empty?
|
||||||
|
return unless parsed.first['type'] == 'error'
|
||||||
|
|
||||||
|
raise Instapaper::Error.from_response(parsed.first['error_code'], @path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
70
lib/instapaper/http/utils.rb
Normal file
70
lib/instapaper/http/utils.rb
Normal file
|
|
@ -0,0 +1,70 @@
|
||||||
|
require 'instapaper/http/request'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module HTTP
|
||||||
|
module Utils
|
||||||
|
private
|
||||||
|
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @param klass [Class]
|
||||||
|
def perform_post_with_objects(path, options, klass)
|
||||||
|
perform_request_with_objects(:post, path, options, klass)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @param klass [Class]
|
||||||
|
def perform_get_with_objects(path, options, klass)
|
||||||
|
perform_request_with_objects(:get, path, options, klass)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param request_method [Symbol]
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @param klass [Class]
|
||||||
|
def perform_request_with_objects(request_method, path, options, klass)
|
||||||
|
perform_request(request_method, path, options).collect do |element|
|
||||||
|
klass.new(coerce_hash(element))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @param klass [Class]
|
||||||
|
def perform_post_with_object(path, options, klass)
|
||||||
|
perform_request_with_object(:post, path, options, klass)
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param request_method [Symbol]
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
# @param klass [Class]
|
||||||
|
def perform_request_with_object(request_method, path, options, klass)
|
||||||
|
response = perform_request(request_method, path, options)
|
||||||
|
response = response.first if response.is_a?(Array)
|
||||||
|
klass.new(coerce_hash(response))
|
||||||
|
end
|
||||||
|
|
||||||
|
# @param path [String]
|
||||||
|
# @param options [Hash]
|
||||||
|
def perform_post_with_unparsed_response(path, options)
|
||||||
|
perform_request(:post, path, options.merge(raw: true))
|
||||||
|
end
|
||||||
|
|
||||||
|
def perform_request(method, path, options)
|
||||||
|
Instapaper::HTTP::Request.new(self, method, path, options).perform
|
||||||
|
end
|
||||||
|
|
||||||
|
def coerce_hash(response)
|
||||||
|
response['instapaper_hash'] = response.delete('hash') if response.key?('hash')
|
||||||
|
if response.key?('bookmarks')
|
||||||
|
response['bookmarks'] = response['bookmarks'].collect do |bookmark|
|
||||||
|
coerce_hash(bookmark)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
response
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
92
lib/instapaper/oauth.rb
Normal file
92
lib/instapaper/oauth.rb
Normal file
|
|
@ -0,0 +1,92 @@
|
||||||
|
require 'openssl'
|
||||||
|
require 'base64'
|
||||||
|
require 'cgi/escape'
|
||||||
|
require 'securerandom'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module OAuth
|
||||||
|
class Header
|
||||||
|
OAUTH_VERSION = '1.0'
|
||||||
|
OAUTH_SIGNATURE_METHOD = 'HMAC-SHA1'
|
||||||
|
|
||||||
|
attr_reader :method, :url, :params, :credentials
|
||||||
|
|
||||||
|
def initialize(method, url, params, credentials)
|
||||||
|
@method = method.to_s.upcase
|
||||||
|
@url = url.to_s
|
||||||
|
@params = params || {}
|
||||||
|
@credentials = credentials
|
||||||
|
@credentials.delete(:ignore_extra_keys)
|
||||||
|
end
|
||||||
|
|
||||||
|
def to_s
|
||||||
|
"OAuth #{auth_header_params.map { |k, v| %(#{k}="#{escape(v)}") }.join(', ')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
def auth_header_params
|
||||||
|
params = oauth_params.dup
|
||||||
|
params['oauth_signature'] = signature
|
||||||
|
params.sort.to_h
|
||||||
|
end
|
||||||
|
|
||||||
|
def oauth_params
|
||||||
|
{
|
||||||
|
'oauth_consumer_key' => credentials[:consumer_key],
|
||||||
|
'oauth_token' => credentials[:token],
|
||||||
|
'oauth_signature_method' => OAUTH_SIGNATURE_METHOD,
|
||||||
|
'oauth_timestamp' => timestamp,
|
||||||
|
'oauth_nonce' => nonce,
|
||||||
|
'oauth_version' => OAUTH_VERSION,
|
||||||
|
}.compact
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature
|
||||||
|
Base64.strict_encode64(
|
||||||
|
OpenSSL::HMAC.digest(
|
||||||
|
OpenSSL::Digest.new('sha1'),
|
||||||
|
signing_key,
|
||||||
|
signature_base_string,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
end
|
||||||
|
|
||||||
|
def signing_key
|
||||||
|
"#{escape(credentials[:consumer_secret])}&#{escape(credentials[:token_secret] || '')}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def signature_base_string
|
||||||
|
[
|
||||||
|
method,
|
||||||
|
escape(normalized_url),
|
||||||
|
escape(normalized_params),
|
||||||
|
].join('&')
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalized_url
|
||||||
|
uri = URI.parse(url)
|
||||||
|
port = uri.port
|
||||||
|
port = nil if (uri.scheme == 'http' && port == 80) || (uri.scheme == 'https' && port == 443)
|
||||||
|
"#{uri.scheme}://#{uri.host}#{":#{port}" if port}#{uri.path}"
|
||||||
|
end
|
||||||
|
|
||||||
|
def normalized_params
|
||||||
|
all_params = params.merge(oauth_params)
|
||||||
|
all_params.map { |k, v| "#{escape(k)}=#{escape(v)}" }.sort.join('&')
|
||||||
|
end
|
||||||
|
|
||||||
|
def escape(value)
|
||||||
|
CGI.escape(value.to_s).gsub('+', '%20')
|
||||||
|
end
|
||||||
|
|
||||||
|
def timestamp
|
||||||
|
@timestamp ||= Time.now.to_i.to_s
|
||||||
|
end
|
||||||
|
|
||||||
|
def nonce
|
||||||
|
@nonce ||= SecureRandom.hex(16)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
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
|
|
||||||
34
lib/instapaper/types.rb
Normal file
34
lib/instapaper/types.rb
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
require 'dry-types'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
module Types
|
||||||
|
include Dry.Types()
|
||||||
|
|
||||||
|
# Coerces any value to string (replaces custom StringOrInteger union type)
|
||||||
|
StringOrInteger = Types::Coercible::String
|
||||||
|
|
||||||
|
# Handles boolean flags from API that come as "0"/"1" strings or 0/1 integers.
|
||||||
|
BooleanFlag = Types::Constructor(Types::Bool) do |value|
|
||||||
|
case value
|
||||||
|
when '1', 1, 'true', true
|
||||||
|
true
|
||||||
|
when '0', 0, 'false', false, nil
|
||||||
|
false
|
||||||
|
else
|
||||||
|
!!value
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# Converts Unix timestamps to Time objects
|
||||||
|
UnixTime = Types::Time.constructor do |value|
|
||||||
|
case value
|
||||||
|
when ::Time
|
||||||
|
value
|
||||||
|
when nil
|
||||||
|
nil
|
||||||
|
else
|
||||||
|
::Time.at(value.to_i)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
15
lib/instapaper/user.rb
Normal file
15
lib/instapaper/user.rb
Normal file
|
|
@ -0,0 +1,15 @@
|
||||||
|
require 'dry-struct'
|
||||||
|
require 'instapaper/types'
|
||||||
|
|
||||||
|
module Instapaper
|
||||||
|
class User < Dry::Struct
|
||||||
|
include Types
|
||||||
|
|
||||||
|
transform_keys(&:to_sym)
|
||||||
|
|
||||||
|
attribute :username, Types::String
|
||||||
|
attribute :user_id, Types::Integer
|
||||||
|
attribute :type, Types::String
|
||||||
|
attribute? :subscription_is_active, Types::BooleanFlag.optional
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,3 +1,3 @@
|
||||||
module Instapaper
|
module Instapaper
|
||||||
VERSION = "0.3.0"
|
VERSION = '1.0.1'
|
||||||
end
|
end
|
||||||
|
|
|
||||||
15
script/console
Executable file
15
script/console
Executable file
|
|
@ -0,0 +1,15 @@
|
||||||
|
#!/usr/bin/env ruby
|
||||||
|
# Usage: script/console
|
||||||
|
# Starts an IRB console with this library loaded.
|
||||||
|
|
||||||
|
require 'bundler/setup'
|
||||||
|
require 'irb'
|
||||||
|
require 'irb/completion'
|
||||||
|
|
||||||
|
project_lib = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
|
||||||
|
$LOAD_PATH.unshift project_lib unless $LOAD_PATH.include?(project_lib)
|
||||||
|
|
||||||
|
require 'instapaper'
|
||||||
|
|
||||||
|
ARGV.clear
|
||||||
|
IRB.start
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
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
|
|
||||||
6
spec/fixtures/bookmarks_list.json
vendored
6
spec/fixtures/bookmarks_list.json
vendored
|
|
@ -1,5 +1 @@
|
||||||
[{"type":"meta"},
|
{"user":{"type":"user","user_id":1075837,"username":"steve.agalloco@gmail.com","subscription_is_active":"1"},"bookmarks":[{"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}],"highlights":[],"delete_ids":[12, 123, 123]}
|
||||||
{"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}
|
|
||||||
]
|
|
||||||
|
|
|
||||||
2
spec/fixtures/folders_add.json
vendored
2
spec/fixtures/folders_add.json
vendored
|
|
@ -1 +1 @@
|
||||||
[{"type":"folder","folder_id":1121141,"title":"Ruby","sync_to_mobile":"1","position":1307330035}]
|
[{"title": "Ruby", "display_title": "Ruby", "sync_to_mobile": 1, "folder_id": 2735618, "position": 1440901825.167363, "type": "folder", "slug": "ruby"}]
|
||||||
|
|
|
||||||
8
spec/fixtures/highlight.json
vendored
Normal file
8
spec/fixtures/highlight.json
vendored
Normal file
|
|
@ -0,0 +1,8 @@
|
||||||
|
{
|
||||||
|
"type":"highlight",
|
||||||
|
"highlight_id":42,
|
||||||
|
"bookmark_id":123,
|
||||||
|
"text":"example page",
|
||||||
|
"position":0,
|
||||||
|
"time":1394470555
|
||||||
|
}
|
||||||
18
spec/fixtures/highlights_list.json
vendored
Normal file
18
spec/fixtures/highlights_list.json
vendored
Normal file
|
|
@ -0,0 +1,18 @@
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"type":"highlight",
|
||||||
|
"highlight_id":42,
|
||||||
|
"bookmark_id":123,
|
||||||
|
"text":"example page",
|
||||||
|
"position":0,
|
||||||
|
"time":1394470555
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type":"highlight",
|
||||||
|
"highlight_id":43,
|
||||||
|
"bookmark_id":123,
|
||||||
|
"text":"example page 2",
|
||||||
|
"position":1,
|
||||||
|
"time":1394470555
|
||||||
|
}
|
||||||
|
]
|
||||||
23
spec/instapaper/api/accounts_spec.rb
Normal file
23
spec/instapaper/api/accounts_spec.rb
Normal file
|
|
@ -0,0 +1,23 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::Client::Accounts do
|
||||||
|
let(:client) { Instapaper::Client.new }
|
||||||
|
|
||||||
|
describe '#verify_credentials' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/account/verify_credentials')
|
||||||
|
.to_return(body: fixture('verify_credentials.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.verify_credentials
|
||||||
|
expect(a_post('/api/1.1/account/verify_credentials'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the user' do
|
||||||
|
user = client.verify_credentials
|
||||||
|
expect(user).to be_a Instapaper::User
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
204
spec/instapaper/api/bookmarks_spec.rb
Normal file
204
spec/instapaper/api/bookmarks_spec.rb
Normal file
|
|
@ -0,0 +1,204 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::Client::Bookmarks do
|
||||||
|
let(:client) { Instapaper::Client.new(consumer_key: 'CK', consumer_secret: 'CS', oauth_token: 'OT', oauth_token_secret: 'OS') }
|
||||||
|
|
||||||
|
describe '#bookmarks' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/list')
|
||||||
|
.to_return(body: fixture('bookmarks_list.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.bookmarks
|
||||||
|
expect(a_post('/api/1.1/bookmarks/list'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an Instapaper::BookmarkList on success' do
|
||||||
|
list = client.bookmarks
|
||||||
|
expect(list).to be_an Instapaper::BookmarkList
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'includes all objects in the response' do
|
||||||
|
list = client.bookmarks
|
||||||
|
expect(list.user).to be_an Instapaper::User
|
||||||
|
list.bookmarks.each do |bookmark|
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'coerces bookmarks correctly' do
|
||||||
|
list = client.bookmarks
|
||||||
|
expect(list.bookmarks.first.instapaper_hash).to_not be_nil
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#update_read_progress' do
|
||||||
|
before do
|
||||||
|
@time = Time.now
|
||||||
|
stub_post('/api/1.1/bookmarks/update_read_progress')
|
||||||
|
.to_return(body: fixture('bookmarks_update_read_progress.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.update_read_progress(123, 0.5, @time)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/update_read_progress').with(body: {bookmark_id: '123', progress: '0.5', progress_timestamp: @time.to_i.to_s}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array containing bookmarks on success' do
|
||||||
|
bookmark = client.update_read_progress(123, 0.5, @time)
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
expect(bookmark.progress).to eq('0.5')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#add_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/add')
|
||||||
|
.to_return(body: fixture('bookmarks_add.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.add_bookmark('http://someurl.com', title: 'This is the title', description: 'This is the description')
|
||||||
|
expect(a_post('/api/1.1/bookmarks/add').with(body: {url: 'http://someurl.com', title: 'This is the title', description: 'This is the description'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the bookmark on success' do
|
||||||
|
bookmark = client.add_bookmark('http://someurl.com', title: 'This is the title', description: 'This is the description')
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#delete_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/delete')
|
||||||
|
.to_return(body: '[]', headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.delete_bookmark(123)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/delete').with(body: {bookmark_id: '123'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array containing bookmarks on success' do
|
||||||
|
confirm = client.delete_bookmark(123)
|
||||||
|
expect(confirm).to be_an Array
|
||||||
|
expect(confirm).to be_empty
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#star_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/star')
|
||||||
|
.to_return(body: fixture('bookmarks_star.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.star_bookmark(123)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/star').with(body: {bookmark_id: '123'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a starred bookmark on success' do
|
||||||
|
bookmark = client.star_bookmark(123)
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
expect(bookmark.starred).to eq('1')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#unstar_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/unstar')
|
||||||
|
.to_return(body: fixture('bookmarks_unstar.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.unstar_bookmark(123)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/unstar').with(body: {bookmark_id: '123'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an unstarred bookmark on success' do
|
||||||
|
bookmark = client.unstar_bookmark(123)
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
expect(bookmark.starred).to eq('0')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#archive_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/archive')
|
||||||
|
.to_return(body: fixture('bookmarks_archive.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.archive_bookmark(123)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/archive').with(body: {bookmark_id: '123'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the bookmark on success' do
|
||||||
|
bookmark = client.archive_bookmark(123)
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#unarchive_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/unarchive')
|
||||||
|
.to_return(body: fixture('bookmarks_unarchive.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.unarchive_bookmark(123)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/unarchive').with(body: {bookmark_id: '123'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the bookmark on success' do
|
||||||
|
bookmark = client.unarchive_bookmark(123)
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#move_bookmark' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/move')
|
||||||
|
.to_return(body: fixture('bookmarks_move.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.move_bookmark(123, 12_345)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/move').with(body: {bookmark_id: '123', folder_id: '12345'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the bookmark on success' do
|
||||||
|
bookmark = client.move_bookmark(123, 12_345)
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#get_text' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/get_text')
|
||||||
|
.to_return(body: fixture('bookmarks_get_text.txt'), headers: {content_type: 'text/html; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.get_text(123)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/get_text').with(body: {bookmark_id: '123'}))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it "returns the bookmark's html on success" do
|
||||||
|
bookmark = client.get_text(123)
|
||||||
|
expect(bookmark.length).to be > 0
|
||||||
|
expect(bookmark).to include("Ideo's Axioms for Starting Disruptive New Businesses")
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
83
spec/instapaper/api/folders_spec.rb
Normal file
83
spec/instapaper/api/folders_spec.rb
Normal file
|
|
@ -0,0 +1,83 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::Client::Folders do
|
||||||
|
let(:client) { Instapaper::Client.new(consumer_key: 'CK', consumer_secret: 'CS', oauth_token: 'OT', oauth_token_secret: 'OS') }
|
||||||
|
|
||||||
|
describe '#folders' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/folders/list')
|
||||||
|
.to_return(body: fixture('folders_list.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.folders
|
||||||
|
expect(a_post('/api/1.1/folders/list'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array containing folders on success' do
|
||||||
|
folders = client.folders
|
||||||
|
expect(folders).to be_an Array
|
||||||
|
expect(folders.size).to eq(2)
|
||||||
|
expect(folders.first).to be_a Instapaper::Folder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#add_folder' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/folders/add')
|
||||||
|
.with(body: {title: 'Ruby'})
|
||||||
|
.to_return(body: fixture('folders_add.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.add_folder('Ruby')
|
||||||
|
expect(a_post('/api/1.1/folders/add'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array containing the new folder on success' do
|
||||||
|
folder = client.add_folder('Ruby')
|
||||||
|
expect(folder).to be_a Instapaper::Folder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#delete_folder' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/folders/delete')
|
||||||
|
.with(body: {folder_id: '1'})
|
||||||
|
.to_return(body: fixture('folders_delete.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.delete_folder('1')
|
||||||
|
expect(a_post('/api/1.1/folders/delete'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an empty array on success' do
|
||||||
|
confirm = client.delete_folder('1')
|
||||||
|
expect(confirm).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#set_order' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/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 'gets the correct resource' do
|
||||||
|
client.set_order(['1121173:2', '1121174:1'])
|
||||||
|
expect(a_post('/api/1.1/folders/set_order'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array reflecting the new order on success' do
|
||||||
|
folders = client.set_order(['1121173:2', '1121174:1'])
|
||||||
|
expect(folders).to be_an Array
|
||||||
|
expect(folders.first).to be_a Instapaper::Folder
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
58
spec/instapaper/api/highlights_spec.rb
Normal file
58
spec/instapaper/api/highlights_spec.rb
Normal file
|
|
@ -0,0 +1,58 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::Client::Highlights do
|
||||||
|
let(:client) { Instapaper::Client.new(consumer_key: 'CK', consumer_secret: 'CS', oauth_token: 'OT', oauth_token_secret: 'OS') }
|
||||||
|
|
||||||
|
describe '#highlights' do
|
||||||
|
before do
|
||||||
|
stub_get('/api/1.1/bookmarks/123/highlights')
|
||||||
|
.to_return(status: 200, body: fixture('highlights_list.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.highlights(123)
|
||||||
|
expect(a_get('/api/1.1/bookmarks/123/highlights')).to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array containing folders on success' do
|
||||||
|
highlights = client.highlights(123)
|
||||||
|
expect(highlights).to be_an Array
|
||||||
|
expect(highlights.size).to eq(2)
|
||||||
|
expect(highlights.first).to be_an Instapaper::Highlight
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#add_highlight' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/123/highlight')
|
||||||
|
.to_return(status: 200, body: fixture('highlight.json'), headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.add_highlight(123, text: 'This is the highlighted text.', position: 22)
|
||||||
|
expect(a_post('/api/1.1/bookmarks/123/highlight')).to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns an array containing folders on success' do
|
||||||
|
highlight = client.add_highlight(123, text: 'This is the highlighted text.', position: 22)
|
||||||
|
expect(highlight).to be_an Instapaper::Highlight
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#delete_highlight' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/highlights/123/delete')
|
||||||
|
.to_return(status: 200, body: '', headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'posts to the correct resource' do
|
||||||
|
client.delete_highlight(123)
|
||||||
|
expect(a_post('/api/1.1/highlights/123/delete')).to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns true when successful' do
|
||||||
|
response = client.delete_highlight(123)
|
||||||
|
expect(response).to be true
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
32
spec/instapaper/api/oauth_spec.rb
Normal file
32
spec/instapaper/api/oauth_spec.rb
Normal file
|
|
@ -0,0 +1,32 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::Client::OAuth do
|
||||||
|
let(:client) { Instapaper::Client.new }
|
||||||
|
|
||||||
|
describe '#token' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/oauth/access_token')
|
||||||
|
.with(body: {x_auth_username: 'ohai', x_auth_password: 'p455w0rd', x_auth_mode: 'client_auth'})
|
||||||
|
.to_return(body: fixture('access_token.txt'), headers: {content_type: 'text/plain; charset=utf-8'})
|
||||||
|
|
||||||
|
stub_post('/api/1.1/oauth/access_token')
|
||||||
|
.with(body: {x_auth_username: 'inval1d', x_auth_password: 'cr3dentials', x_auth_mode: 'client_auth'})
|
||||||
|
.to_return(body: fixture('invalid_credentials.txt'), headers: {content_type: 'text/plain; charset=utf-8'})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'gets the correct resource' do
|
||||||
|
client.access_token('ohai', 'p455w0rd')
|
||||||
|
expect(a_post('/api/1.1/oauth/access_token'))
|
||||||
|
.to have_been_made
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns the a hash containing an oauth token and secret' do
|
||||||
|
tokens = client.access_token('ohai', 'p455w0rd')
|
||||||
|
expect(tokens).to be_an Instapaper::Credentials
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'returns a hash containing the error on invalid credentials' do
|
||||||
|
expect { client.access_token('inval1d', 'cr3dentials') }.to raise_error(Instapaper::Error::OAuthError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
12
spec/instapaper/bookmark_list_spec.rb
Normal file
12
spec/instapaper/bookmark_list_spec.rb
Normal file
|
|
@ -0,0 +1,12 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::BookmarkList do
|
||||||
|
describe '#each' do
|
||||||
|
it 'yields a list of bookmarks' do
|
||||||
|
list = Instapaper::BookmarkList.new(JSON.parse(fixture('bookmarks_list.json').read))
|
||||||
|
list.each do |bookmark|
|
||||||
|
expect(bookmark).to be_an Instapaper::Bookmark
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,27 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,234 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,89 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
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"})
|
|
||||||
stub_post("oauth/access_token").with(:body => { :x_auth_username => 'inval1d', :x_auth_password => 'cr3dentials', :x_auth_mode => 'client_auth'}).
|
|
||||||
to_return(:body => fixture("invalid_credentials.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
|
|
||||||
|
|
||||||
it "should return a hash containing the error on invalid credentials" do
|
|
||||||
tokens = @client.access_token('inval1d', 'cr3dentials')
|
|
||||||
tokens.should be_a Hash
|
|
||||||
tokens.key?('error').should be_true
|
|
||||||
end
|
|
||||||
end
|
|
||||||
|
|
||||||
end
|
|
||||||
|
|
@ -1,65 +1,23 @@
|
||||||
require 'spec_helper'
|
require 'spec_helper'
|
||||||
|
|
||||||
describe Instapaper::Client do
|
describe Instapaper::Client do
|
||||||
|
let(:client) { Instapaper::Client.new(consumer_key: 'CK', consumer_secret: 'CS', oauth_token: 'OT', oauth_token_secret: 'OS') }
|
||||||
|
|
||||||
before do
|
describe '#credentials' do
|
||||||
@options = { :adapter => :em_synchrony, :user_agent => 'Instapaper::Client spec' }
|
it 'returns the credentials as hash' do
|
||||||
@keys = Instapaper::Configuration::VALID_OPTIONS_KEYS
|
expect(client.credentials).to be_a Hash
|
||||||
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
|
||||||
end
|
end
|
||||||
|
|
||||||
describe '.endpoint_with_prefix' do
|
describe '#credentials?' do
|
||||||
before(:each) do
|
it 'returns true when all credentials are present' do
|
||||||
@client = Instapaper::Client.new
|
expect(client.credentials?).to be true
|
||||||
end
|
|
||||||
|
|
||||||
it 'should return the ' do
|
|
||||||
@client.endpoint_with_prefix.should == Instapaper.endpoint + Instapaper.path_prefix
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
|
||||||
|
describe '#consumer_credentials?' do
|
||||||
|
it 'returns the consumer credentials as hash' do
|
||||||
|
expect(client.consumer_credentials).to be_a Hash
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
|
||||||
109
spec/instapaper/error_spec.rb
Normal file
109
spec/instapaper/error_spec.rb
Normal file
|
|
@ -0,0 +1,109 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::Error do
|
||||||
|
before do
|
||||||
|
@client = Instapaper::Client.new(consumer_key: 'CK', consumer_secret: 'CS', oauth_token: 'AT', oauth_token_secret: 'AS')
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#code' do
|
||||||
|
it 'returns the error code' do
|
||||||
|
error = Instapaper::Error.new('execution expired', 123)
|
||||||
|
expect(error.code).to eq(123)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#message' do
|
||||||
|
it 'returns the error message' do
|
||||||
|
error = Instapaper::Error.new('execution expired')
|
||||||
|
expect(error.message).to eq('execution expired')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Instapaper::Error::CLIENT_ERRORS.each do |status, exception|
|
||||||
|
context "when HTTP status is #{status}" do
|
||||||
|
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/oauth/access_token')
|
||||||
|
.to_return(status: status, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it "raises #{exception}" do
|
||||||
|
expect { @client.access_token('foo', 'bar') }.to raise_error(Instapaper::Error::ClientError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Instapaper::Error::SERVER_ERRORS.each do |status, exception|
|
||||||
|
context "when HTTP status is #{status}" do
|
||||||
|
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/oauth/access_token')
|
||||||
|
.to_return(status: status, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it "raises #{exception}" do
|
||||||
|
expect { @client.access_token('foo', 'bar') }.to raise_error(Instapaper::Error::ServerError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Instapaper::Error::SERVICE_ERRORS.each do |status, exception|
|
||||||
|
context "when HTTP status is #{status}" do
|
||||||
|
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/oauth/access_token')
|
||||||
|
.to_return(status: 200, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it "raises #{exception}" do
|
||||||
|
expect { @client.access_token('foo', 'bar') }.to raise_error(Instapaper::Error)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Instapaper::Error::BOOKMARK_ERRORS.each do |status, exception|
|
||||||
|
context "when HTTP status is #{status}" do
|
||||||
|
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/bookmarks/list')
|
||||||
|
.to_return(status: 200, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it "raises #{exception}" do
|
||||||
|
expect { @client.bookmarks }.to raise_error(Instapaper::Error::BookmarkError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Instapaper::Error::FOLDER_ERRORS.each do |status, exception|
|
||||||
|
context "when HTTP status is #{status}" do
|
||||||
|
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/folders/list')
|
||||||
|
.to_return(status: 200, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it "raises #{exception}" do
|
||||||
|
expect { @client.folders }.to raise_error(Instapaper::Error::FolderError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
Instapaper::Error::HIGHLIGHT_ERRORS.each do |status, exception|
|
||||||
|
context "when HTTP status is #{status}" do
|
||||||
|
let(:response_body) { %([{"type":"error", "error_code":#{status}, "message":"Error Message"}]) }
|
||||||
|
before do
|
||||||
|
stub_get('/api/1.1/bookmarks/123/highlights')
|
||||||
|
.to_return(status: 200, body: response_body, headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it "raises #{exception}" do
|
||||||
|
expect { @client.highlights('123') }.to raise_error(Instapaper::Error::HighlightError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '.from_response' do
|
||||||
|
context 'with null path' do
|
||||||
|
it 'raises an Instapaper::Error' do
|
||||||
|
error = Instapaper::Error.from_response(5000, nil)
|
||||||
|
expect(error).to be_an Instapaper::Error
|
||||||
|
expect(error.message).to eq('Unknown Error')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
27
spec/instapaper/http/request_spec.rb
Normal file
27
spec/instapaper/http/request_spec.rb
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::HTTP::Request do
|
||||||
|
let(:client) { Instapaper::Client.new(consumer_key: 'CK', consumer_secret: 'CS', oauth_token: 'OT', oauth_token_secret: 'OS') }
|
||||||
|
|
||||||
|
describe 'error handling' do
|
||||||
|
context 'when receiving a non-200 response' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/folders/list')
|
||||||
|
.to_return(status: 503, body: '', headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it 'raises a ServerError' do
|
||||||
|
expect { client.folders }.to raise_error(Instapaper::Error::ServiceUnavailableError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when failing to parse json' do
|
||||||
|
before do
|
||||||
|
stub_post('/api/1.1/folders/list')
|
||||||
|
.to_return(status: 200, body: '{"key":"value}', headers: {content_type: 'application/json; charset=utf-8'})
|
||||||
|
end
|
||||||
|
it 'raises a ServiceUnavailableError' do
|
||||||
|
expect { client.folders }.to raise_error(Instapaper::Error::ServiceUnavailableError)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
44
spec/instapaper/http/response_spec.rb
Normal file
44
spec/instapaper/http/response_spec.rb
Normal file
|
|
@ -0,0 +1,44 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
class FakeResponse
|
||||||
|
def initialize(body)
|
||||||
|
@body = body
|
||||||
|
end
|
||||||
|
|
||||||
|
def parse(_)
|
||||||
|
::JSON.parse(@body)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe Instapaper::HTTP::Response do
|
||||||
|
describe '#body' do
|
||||||
|
context 'raw response' do
|
||||||
|
it 'returns the response in raw text' do
|
||||||
|
resp = Instapaper::HTTP::Response.new('foo', '', true)
|
||||||
|
expect(resp.body).to eq('foo')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'regular response' do
|
||||||
|
let(:fake_response) { FakeResponse.new('{"foo":"bar"}') }
|
||||||
|
it 'returns the parsed response' do
|
||||||
|
resp = Instapaper::HTTP::Response.new(fake_response, '')
|
||||||
|
expect(resp.body).to be_a(Hash)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#valid?' do
|
||||||
|
context 'when http error' do
|
||||||
|
it 'should be invalid'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when body unparseable' do
|
||||||
|
it 'should be invalid'
|
||||||
|
end
|
||||||
|
|
||||||
|
context 'when error in body' do
|
||||||
|
it 'should be invalid'
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
154
spec/instapaper/oauth_spec.rb
Normal file
154
spec/instapaper/oauth_spec.rb
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
require 'spec_helper'
|
||||||
|
|
||||||
|
describe Instapaper::OAuth::Header do
|
||||||
|
let(:method) { 'POST' }
|
||||||
|
let(:url) { 'https://www.instapaper.com/api/1/bookmarks/list' }
|
||||||
|
let(:params) { {folder_id: '12345', limit: '10'} }
|
||||||
|
let(:credentials) do
|
||||||
|
{
|
||||||
|
consumer_key: 'test_consumer_key',
|
||||||
|
consumer_secret: 'test_consumer_secret',
|
||||||
|
token: 'test_token',
|
||||||
|
token_secret: 'test_token_secret',
|
||||||
|
}
|
||||||
|
end
|
||||||
|
|
||||||
|
subject { described_class.new(method, url, params, credentials) }
|
||||||
|
|
||||||
|
describe '#initialize' do
|
||||||
|
it 'upcases the HTTP method' do
|
||||||
|
header = described_class.new('get', url, params, credentials)
|
||||||
|
expect(header.method).to eq('GET')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'converts URL to string' do
|
||||||
|
uri = URI.parse(url)
|
||||||
|
header = described_class.new(method, uri, params, credentials)
|
||||||
|
expect(header.url).to eq(url)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'handles nil params' do
|
||||||
|
header = described_class.new(method, url, nil, credentials)
|
||||||
|
expect(header.params).to eq({})
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes ignore_extra_keys from credentials' do
|
||||||
|
creds_with_extra = credentials.merge(ignore_extra_keys: true)
|
||||||
|
header = described_class.new(method, url, params, creds_with_extra)
|
||||||
|
expect(header.credentials).not_to have_key(:ignore_extra_keys)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe '#to_s' do
|
||||||
|
it 'returns a properly formatted OAuth header' do
|
||||||
|
# Stub time and nonce for consistent output
|
||||||
|
allow(subject).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(subject).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
header_string = subject.to_s
|
||||||
|
expect(header_string).to start_with('OAuth ')
|
||||||
|
expect(header_string).to include('oauth_consumer_key="test_consumer_key"')
|
||||||
|
expect(header_string).to include('oauth_nonce="abcdef1234567890"')
|
||||||
|
expect(header_string).to include('oauth_signature=')
|
||||||
|
expect(header_string).to include('oauth_signature_method="HMAC-SHA1"')
|
||||||
|
expect(header_string).to include('oauth_timestamp="1234567890"')
|
||||||
|
expect(header_string).to include('oauth_token="test_token"')
|
||||||
|
expect(header_string).to include('oauth_version="1.0"')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'sorts OAuth parameters alphabetically' do
|
||||||
|
allow(subject).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(subject).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
header_string = subject.to_s
|
||||||
|
params_order = header_string.scan(/oauth_\w+(?==)/)
|
||||||
|
expect(params_order).to eq(params_order.sort)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'signature generation' do
|
||||||
|
it 'generates consistent signatures for the same input' do
|
||||||
|
allow(subject).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(subject).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
header1 = subject.to_s
|
||||||
|
header2 = subject.to_s
|
||||||
|
|
||||||
|
sig1 = header1[/oauth_signature="([^"]+)"/, 1]
|
||||||
|
sig2 = header2[/oauth_signature="([^"]+)"/, 1]
|
||||||
|
|
||||||
|
expect(sig1).to eq(sig2)
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'generates different signatures for different parameters' do
|
||||||
|
allow(subject).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(subject).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
header1 = subject.to_s
|
||||||
|
|
||||||
|
different_params = {folder_id: '67890', limit: '20'}
|
||||||
|
subject2 = described_class.new(method, url, different_params, credentials)
|
||||||
|
allow(subject2).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(subject2).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
header2 = subject2.to_s
|
||||||
|
|
||||||
|
sig1 = header1[/oauth_signature="([^"]+)"/, 1]
|
||||||
|
sig2 = header2[/oauth_signature="([^"]+)"/, 1]
|
||||||
|
|
||||||
|
expect(sig1).not_to eq(sig2)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'URL normalization' do
|
||||||
|
it 'removes default HTTP port 80' do
|
||||||
|
header = described_class.new(method, 'http://example.com:80/path', params, credentials)
|
||||||
|
expect(header.send(:normalized_url)).to eq('http://example.com/path')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'removes default HTTPS port 443' do
|
||||||
|
header = described_class.new(method, 'https://example.com:443/path', params, credentials)
|
||||||
|
expect(header.send(:normalized_url)).to eq('https://example.com/path')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'keeps non-default ports' do
|
||||||
|
header = described_class.new(method, 'https://example.com:8080/path', params, credentials)
|
||||||
|
expect(header.send(:normalized_url)).to eq('https://example.com:8080/path')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'parameter encoding' do
|
||||||
|
it 'properly encodes spaces as %20' do
|
||||||
|
params_with_spaces = {title: 'Hello World'}
|
||||||
|
header = described_class.new(method, url, params_with_spaces, credentials)
|
||||||
|
allow(header).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(header).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
normalized = header.send(:normalized_params)
|
||||||
|
expect(normalized).to include('title=Hello%20World')
|
||||||
|
expect(normalized).not_to include('+')
|
||||||
|
end
|
||||||
|
|
||||||
|
it 'properly encodes special characters' do
|
||||||
|
params_with_special = {title: 'Test & Co.'}
|
||||||
|
header = described_class.new(method, url, params_with_special, credentials)
|
||||||
|
allow(header).to receive(:timestamp).and_return('1234567890')
|
||||||
|
allow(header).to receive(:nonce).and_return('abcdef1234567890')
|
||||||
|
|
||||||
|
normalized = header.send(:normalized_params)
|
||||||
|
expect(normalized).to include('title=Test%20%26%20Co.')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe 'compatibility' do
|
||||||
|
it 'handles missing token credentials (2-legged OAuth)' do
|
||||||
|
two_legged_creds = {
|
||||||
|
consumer_key: 'test_consumer_key',
|
||||||
|
consumer_secret: 'test_consumer_secret',
|
||||||
|
}
|
||||||
|
header = described_class.new(method, url, params, two_legged_creds)
|
||||||
|
expect { header.to_s }.not_to raise_error
|
||||||
|
expect(header.to_s).not_to include('oauth_token=')
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
@ -1,85 +0,0 @@
|
||||||
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
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
unless ENV['CI']
|
unless ENV['CI']
|
||||||
require 'simplecov'
|
require 'simplecov'
|
||||||
SimpleCov.start do
|
SimpleCov.start do
|
||||||
add_filter '.bundle'
|
add_filter '/spec/'
|
||||||
add_group 'Instapaper', 'lib/instapaper'
|
|
||||||
add_group 'Specs', 'spec'
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|
@ -11,42 +9,26 @@ require 'instapaper'
|
||||||
require 'rspec'
|
require 'rspec'
|
||||||
require 'webmock/rspec'
|
require 'webmock/rspec'
|
||||||
|
|
||||||
def a_delete(path)
|
def a_post(path)
|
||||||
a_request(:delete, Instapaper.endpoint_with_prefix + path)
|
a_request(:post, Instapaper::HTTP::Request::BASE_URL + path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def a_get(path)
|
def a_get(path)
|
||||||
a_request(:get, Instapaper.endpoint_with_prefix + path)
|
a_request(:get, Instapaper::HTTP::Request::BASE_URL + 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
|
end
|
||||||
|
|
||||||
def stub_post(path)
|
def stub_post(path)
|
||||||
stub_request(:post, Instapaper.endpoint_with_prefix + path)
|
stub_request(:post, Instapaper::HTTP::Request::BASE_URL + path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def stub_put(path)
|
def stub_get(path)
|
||||||
stub_request(:put, Instapaper.endpoint_with_prefix + path)
|
stub_request(:get, Instapaper::HTTP::Request::BASE_URL + path)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fixture_path
|
def fixture_path
|
||||||
File.expand_path("../fixtures", __FILE__)
|
File.expand_path('fixtures', __dir__)
|
||||||
end
|
end
|
||||||
|
|
||||||
def fixture(file)
|
def fixture(file)
|
||||||
File.new(fixture_path + '/' + file)
|
File.new("#{fixture_path}/#{file}")
|
||||||
end
|
end
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue