ruby_pocket 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +191 -0
- data/Rakefile +51 -0
- data/config/config.rb +14 -0
- data/config/database.rb +14 -0
- data/db/migrations/001_create_tags.rb +15 -0
- data/db/migrations/002_create_favorites.rb +17 -0
- data/db/migrations/003_create_favorites_tags.rb +20 -0
- data/db/pocket_development.db +0 -0
- data/db/pocket_test.db +0 -0
- data/lib/ruby_pocket/all.rb +5 -0
- data/lib/ruby_pocket/cli/add_action.rb +12 -0
- data/lib/ruby_pocket/cli/delete_action.rb +16 -0
- data/lib/ruby_pocket/cli/list_action.rb +39 -0
- data/lib/ruby_pocket/cli/open_action.rb +12 -0
- data/lib/ruby_pocket/cli/options.rb +23 -0
- data/lib/ruby_pocket/cli.rb +8 -0
- data/lib/ruby_pocket/environment.rb +11 -0
- data/lib/ruby_pocket/favorite.rb +36 -0
- data/lib/ruby_pocket/favorite_creator.rb +62 -0
- data/lib/ruby_pocket/favorite_query.rb +34 -0
- data/lib/ruby_pocket/tag.rb +32 -0
- data/lib/ruby_pocket/version.rb +3 -0
- data/lib/ruby_pocket/web_page.rb +33 -0
- data/lib/ruby_pocket.rb +37 -0
- data/test/ruby_pocket/cli/options_test.rb +49 -0
- data/test/ruby_pocket/favorite_creator_test.rb +80 -0
- data/test/ruby_pocket/favorite_query_test.rb +98 -0
- data/test/ruby_pocket/favorite_test.rb +76 -0
- data/test/ruby_pocket/features/add_test.rb +40 -0
- data/test/ruby_pocket/features/delete_test.rb +45 -0
- data/test/ruby_pocket/features/list_test.rb +75 -0
- data/test/ruby_pocket/tag_test.rb +53 -0
- data/test/ruby_pocket/web_page_test.rb +68 -0
- data/test/support/add_feature_mocker.rb +37 -0
- data/test/support/custom_assertions.rb +9 -0
- data/test/support/database_test_case.rb +11 -0
- data/test/support/default_test_case.rb +9 -0
- data/test/support/favorite_factory.rb +14 -0
- data/test/support/feature_assertions.rb +22 -0
- data/test/support/feature_test_case.rb +32 -0
- data/test/support/webmock.rb +3 -0
- data/test/test_helper.rb +19 -0
- metadata +305 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: f563c1c1e135a1c47d268ecfdb1756b725024d90
|
4
|
+
data.tar.gz: d0d152035dcbd56324cc78c95b6c67595f8449ea
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: c51b5f705315b878fccc9791acf391e57e5f916eefb71fd2f6b8af84a6d77e1419d4d7d78dbca4c660f2e21b4a05479808c1af0896539ac282b6ae735f0e996c
|
7
|
+
data.tar.gz: 550e7671b2f70a3a98e5f307f90b12451b1b6df892711fae04941e4026a7d4876ff91dd895fedf81c52a6bf027d44f07540902537d39fcc33dca0840465f9bc5
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2014 YOURNAME
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,191 @@
|
|
1
|
+
# Ruby Pocket
|
2
|
+
|
3
|
+
[](https://codeclimate.com/github/thiagoa/ruby_pocket)
|
4
|
+
[](https://codeclimate.com/github/thiagoa/ruby_pocket/coverage)
|
5
|
+
[](https://travis-ci.org/thiagoa/ruby_pocket)
|
6
|
+
|
7
|
+
*A warm place to store your precious development references.*
|
8
|
+
|
9
|
+
Although it has *pocket* in its name, **Ruby Pocket** is not a **Pocket** nor a
|
10
|
+
**Readability** clone. It is a tool designed to store useful all-around
|
11
|
+
development articles and reference URLs. You can store any kind of URL, but its
|
12
|
+
main appeal is for developers, or for those who need to store technical
|
13
|
+
articles or references of any kind.
|
14
|
+
|
15
|
+
Why would you want to use something like that? Having made this tool for
|
16
|
+
myself, here are some reasons why you might want to use it too:
|
17
|
+
|
18
|
+
- You feel like work and technical references should not be mixed with other
|
19
|
+
content types.
|
20
|
+
- You don't want to clutter your bookmarks with technical references.
|
21
|
+
- You don't want to clutter your **Pocket** or **Readability** account; they're
|
22
|
+
meant to be read later lists, not permanent article stores.
|
23
|
+
- **You want to be the real owner, and have full control over your own data**.
|
24
|
+
**Ruby Pocket** uses **SQLite3**; that means you're not limited to weird
|
25
|
+
export formats, or to no export formats at all; you can do whatever you want
|
26
|
+
with your data.
|
27
|
+
- You are a heavy command line user and lover, and don't want to break the flow
|
28
|
+
when looking for a reference.
|
29
|
+
- You enjoy a clean, text-based, clutter-free list of favorites.
|
30
|
+
- You want to have your URLs at your disposal, in all your computers, or even
|
31
|
+
have a private URL server. With a private URL server hosted in some place
|
32
|
+
like **Heroku**, you can also consume your content on mobile devices (*not
|
33
|
+
ready yet*).
|
34
|
+
- You want power searching (*not ready yet*).
|
35
|
+
|
36
|
+
If you're fine with your current method, whatever it is, just go with it :-)
|
37
|
+
|
38
|
+
## Current Features
|
39
|
+
|
40
|
+
Currently the app has a very simple feature set:
|
41
|
+
|
42
|
+
- Simple and delightful command line interface.
|
43
|
+
- Tagging support. Each favorite can have one or more tags.
|
44
|
+
- Add favorites.
|
45
|
+
- List favorites.
|
46
|
+
- Search favorites by tags.
|
47
|
+
- Delete favorites
|
48
|
+
- Open favorites in the default browser (currently for **OS X**)
|
49
|
+
|
50
|
+
## How to install
|
51
|
+
|
52
|
+
There is no RubyGem yet. Just clone the repo and install the dependencies:
|
53
|
+
|
54
|
+
```sh
|
55
|
+
bundle install
|
56
|
+
```
|
57
|
+
|
58
|
+
The binary is located in `bin/pocket`.
|
59
|
+
|
60
|
+
## Usage
|
61
|
+
|
62
|
+
Note: there are verbose versions for all commands presented here. See all
|
63
|
+
available options with the following command:
|
64
|
+
|
65
|
+
```sh
|
66
|
+
$ pocket -h
|
67
|
+
```
|
68
|
+
|
69
|
+
Add a favorite, fetching its name over the internet:
|
70
|
+
|
71
|
+
```sh
|
72
|
+
$ pocket -a https://github.com/jlevy/the-art-of-command-line
|
73
|
+
|
74
|
+
Favorite 'jlevy/the-art-of-command-line · GitHub' created!
|
75
|
+
```
|
76
|
+
|
77
|
+
Add a favorite, but specify its name:
|
78
|
+
|
79
|
+
```sh
|
80
|
+
$ pocket -a https://github.com/jlevy/the-art-of-command-line -t 'The Art of Command Line'
|
81
|
+
|
82
|
+
Favorite 'The Art of Command Line' created!
|
83
|
+
```
|
84
|
+
|
85
|
+
Add a favorite with tags. If more than one tag, separate with commas:
|
86
|
+
|
87
|
+
```sh
|
88
|
+
$ pocket -a https://github.com/jlevy/the-art-of-command-line -t command-line
|
89
|
+
|
90
|
+
Favorite 'jlevy/the-art-of-command-line · GitHub' created!
|
91
|
+
```
|
92
|
+
|
93
|
+
List all favorites:
|
94
|
+
|
95
|
+
```sh
|
96
|
+
$ pocket -l
|
97
|
+
|
98
|
+
+----+------------------------------------------------------------------+--------------------+
|
99
|
+
| ID | Name | Tags |
|
100
|
+
+----+------------------------------------------------------------------+--------------------+
|
101
|
+
| 1 | Stronger Shell | shell-script |
|
102
|
+
| 2 | The Art of Command Line | command-line |
|
103
|
+
+----+------------------------------------------------------------------+--------------------+
|
104
|
+
```
|
105
|
+
|
106
|
+
Filter favorites by tag. Can filter by more than one tag:
|
107
|
+
|
108
|
+
```sh
|
109
|
+
$ pocket -l -t command-line
|
110
|
+
|
111
|
+
+----+------------------------------------------------------------------+--------------------+
|
112
|
+
| ID | Name | Tags |
|
113
|
+
+----+------------------------------------------------------------------+--------------------+
|
114
|
+
| 2 | The Art of Command Line | command-line |
|
115
|
+
+----+------------------------------------------------------------------+--------------------+
|
116
|
+
```
|
117
|
+
|
118
|
+
Open a favorite in your default browser. Use the ID of the favorite:
|
119
|
+
|
120
|
+
```sh
|
121
|
+
$ pocket -o 2
|
122
|
+
```
|
123
|
+
|
124
|
+
Delete favorites. Use the IDs of the favorites you want to delete, separated by
|
125
|
+
commas:
|
126
|
+
|
127
|
+
```sh
|
128
|
+
$ pocket -d 2
|
129
|
+
|
130
|
+
Favorite 'The Art of Command Line' deleted
|
131
|
+
```
|
132
|
+
|
133
|
+
## Planned Features
|
134
|
+
|
135
|
+
- Scrape more web page information: probably the main content of the page.
|
136
|
+
- Edit favorites.
|
137
|
+
- List available tags.
|
138
|
+
- Interactive command mode.
|
139
|
+
- Search favorites with partial tag matching.
|
140
|
+
- Search favorites by name or by content, with regex support.
|
141
|
+
- Group favorites by tag, and other useful view modes.
|
142
|
+
- Link database to another folder, such as **Dropbox**. That action shall be
|
143
|
+
automated with a command line flag.
|
144
|
+
- Remote backup support.
|
145
|
+
- **Linux** support for opening favorites.
|
146
|
+
- **Sinatra** web app to consume the content on mobile devices (as another project, which uses the gem).
|
147
|
+
- **Sinatra** API to consume the content (as another project, which uses the gem).
|
148
|
+
- **Alfred** workflow (as another project).
|
149
|
+
|
150
|
+
## Developer information
|
151
|
+
|
152
|
+
This project is built with **Ruby**. The name **Ruby**, from **Ruby
|
153
|
+
Pocket**, doesn't necessarily refer to the language itself :-)
|
154
|
+
|
155
|
+
### Running the tests
|
156
|
+
|
157
|
+
Unit tests:
|
158
|
+
|
159
|
+
```sh
|
160
|
+
rake test
|
161
|
+
```
|
162
|
+
|
163
|
+
Feature tests:
|
164
|
+
|
165
|
+
```sh
|
166
|
+
rake test:feature
|
167
|
+
```
|
168
|
+
|
169
|
+
All tests:
|
170
|
+
|
171
|
+
```sh
|
172
|
+
rake test:all
|
173
|
+
|
174
|
+
# Works too
|
175
|
+
rake
|
176
|
+
```
|
177
|
+
|
178
|
+
Run an IRB console:
|
179
|
+
|
180
|
+
```sh
|
181
|
+
rake console
|
182
|
+
```
|
183
|
+
|
184
|
+
## Contributing
|
185
|
+
|
186
|
+
- Fork the project
|
187
|
+
- Create a feature branch
|
188
|
+
- Make your code changes with tests
|
189
|
+
- Make a Pull-Request
|
190
|
+
|
191
|
+
This project uses MIT\_LICENSE
|
data/Rakefile
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
require 'pathname'
|
2
|
+
require 'rake'
|
3
|
+
|
4
|
+
%w(config lib test).each do |dir|
|
5
|
+
$LOAD_PATH << Pathname(__dir__).join(dir)
|
6
|
+
end
|
7
|
+
|
8
|
+
def run_tests(test_files)
|
9
|
+
require 'minitest'
|
10
|
+
|
11
|
+
test_files.each { |file | require file.expand_path }
|
12
|
+
|
13
|
+
Minitest.run
|
14
|
+
end
|
15
|
+
|
16
|
+
desc 'Run unit tests'
|
17
|
+
task :test do
|
18
|
+
all = Pathname.glob('test/**/*_test.rb')
|
19
|
+
|
20
|
+
run_tests(all.reject { |f| f.to_s =~ /features/ })
|
21
|
+
end
|
22
|
+
|
23
|
+
namespace :test do
|
24
|
+
desc 'Run feature tests'
|
25
|
+
task :feature do
|
26
|
+
feature = Pathname.glob('test/ruby_pocket/features/**/*_test.rb')
|
27
|
+
|
28
|
+
run_tests(feature)
|
29
|
+
end
|
30
|
+
|
31
|
+
desc 'Run all tests'
|
32
|
+
task :all do
|
33
|
+
all = Pathname.glob('test/**/*_test.rb')
|
34
|
+
|
35
|
+
run_tests(all)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
desc 'Run an IRB console'
|
40
|
+
task :console do |t|
|
41
|
+
require 'irb'
|
42
|
+
require 'ruby_pocket'
|
43
|
+
require 'config'
|
44
|
+
require 'ruby_pocket/all'
|
45
|
+
|
46
|
+
ARGV.clear
|
47
|
+
|
48
|
+
IRB.start
|
49
|
+
end
|
50
|
+
|
51
|
+
task default: 'test:all'
|
data/config/config.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
RubyPocket.environment = ENV['RUBY_POCKET_ENV'] if ENV['RUBY_POCKET_ENV']
|
2
|
+
RubyPocket.setup_data_dir
|
3
|
+
|
4
|
+
require 'forwardable'
|
5
|
+
require 'sqlite3'
|
6
|
+
require 'sequel'
|
7
|
+
require 'terminal-table'
|
8
|
+
|
9
|
+
if RubyPocket.environment.test? && ENV['MOCK_FEATURE'] == 'add'
|
10
|
+
require_relative '../test/support/add_feature_mocker'
|
11
|
+
AddFeatureMocker.new.run
|
12
|
+
end
|
13
|
+
|
14
|
+
require_relative 'database'
|
data/config/database.rb
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
db_path = if RubyPocket.environment.production?
|
2
|
+
[RubyPocket.data_dir, 'pocket_production.db']
|
3
|
+
else
|
4
|
+
['db', "pocket_#{RubyPocket.environment.downcase}.db"]
|
5
|
+
end
|
6
|
+
|
7
|
+
DB = Sequel.connect("sqlite://#{File.join(*db_path)}")
|
8
|
+
|
9
|
+
Sequel::Model :validation_helpers
|
10
|
+
Sequel.extension :migration
|
11
|
+
|
12
|
+
unless Sequel::Migrator.is_current?(DB, 'db/migrations')
|
13
|
+
Sequel::Migrator.run(DB, 'db/migrations')
|
14
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
unless table_exists?(:favorites)
|
4
|
+
create_table :favorites do
|
5
|
+
primary_key :id
|
6
|
+
|
7
|
+
String :name, text: true
|
8
|
+
String :summary, text: true
|
9
|
+
String :url, text: true, unique: true
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
down do
|
15
|
+
drop_table :favorites
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
Sequel.migration do
|
2
|
+
up do
|
3
|
+
run <<-SQL
|
4
|
+
CREATE TABLE IF NOT EXISTS favorites_tags(
|
5
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
6
|
+
tag_id INTEGER,
|
7
|
+
favorite_id INTEGER,
|
8
|
+
FOREIGN KEY(tag_id) REFERENCES tags(id) ON DELETE CASCADE,
|
9
|
+
FOREIGN KEY(favorite_id) REFERENCES favorites(id) ON DELETE CASCADE,
|
10
|
+
UNIQUE(tag_id, favorite_id) ON CONFLICT REPLACE
|
11
|
+
)
|
12
|
+
SQL
|
13
|
+
end
|
14
|
+
|
15
|
+
down do
|
16
|
+
run <<-SQL
|
17
|
+
DROP TABLE favorites_tags
|
18
|
+
SQL
|
19
|
+
end
|
20
|
+
end
|
Binary file
|
data/db/pocket_test.db
ADDED
Binary file
|
@@ -0,0 +1,16 @@
|
|
1
|
+
module RubyPocket
|
2
|
+
module Cli
|
3
|
+
class DeleteAction
|
4
|
+
def call(options)
|
5
|
+
options.values[:ids].each do |id|
|
6
|
+
favorite = Favorite[id]
|
7
|
+
|
8
|
+
next puts "Favorite with ID #{id} not found!" unless favorite
|
9
|
+
|
10
|
+
favorite.destroy
|
11
|
+
puts "Favorite '#{favorite.name}' deleted"
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
require 'ruby_pocket/favorite_query'
|
2
|
+
|
3
|
+
module RubyPocket
|
4
|
+
module Cli
|
5
|
+
class ListAction
|
6
|
+
def call(options)
|
7
|
+
favorites = FavoriteQuery.where(options.values).all
|
8
|
+
render favorites
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
|
13
|
+
def render(favorites)
|
14
|
+
return puts 'Your Ruby Pocket is empty' if favorites.empty?
|
15
|
+
|
16
|
+
headings = %w(ID Name Tags)
|
17
|
+
rows = table_rows(favorites)
|
18
|
+
|
19
|
+
puts Terminal::Table.new headings: headings, rows: rows
|
20
|
+
end
|
21
|
+
|
22
|
+
def table_rows(favorites)
|
23
|
+
rows = favorites.map do |f|
|
24
|
+
[f.id, f.name, f.tags.map(&:name).join(',')]
|
25
|
+
end
|
26
|
+
|
27
|
+
add_placeholders(rows)
|
28
|
+
end
|
29
|
+
|
30
|
+
def add_placeholders(rows)
|
31
|
+
rows.each do |row|
|
32
|
+
row.map! do |value|
|
33
|
+
(value.to_s.empty? && '-') || value
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
module RubyPocket
|
2
|
+
module Cli
|
3
|
+
class Options
|
4
|
+
attr_reader :action, :values
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
@values = {}
|
8
|
+
end
|
9
|
+
|
10
|
+
def action=(action)
|
11
|
+
if @action
|
12
|
+
fail ArgumentError, "Can't #{action} and #{@action} at the same time"
|
13
|
+
end
|
14
|
+
|
15
|
+
@action = action
|
16
|
+
end
|
17
|
+
|
18
|
+
def validate!
|
19
|
+
fail ArgumentError, 'You need to supply an action' unless action
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'ruby_pocket/tag'
|
2
|
+
|
3
|
+
module RubyPocket
|
4
|
+
class Favorite < Sequel::Model
|
5
|
+
plugin :validation_helpers
|
6
|
+
|
7
|
+
many_to_many :tags
|
8
|
+
|
9
|
+
attr_writer :tag_names
|
10
|
+
|
11
|
+
def tag_names
|
12
|
+
@tag_names ||= [*@tag_names]
|
13
|
+
end
|
14
|
+
|
15
|
+
private
|
16
|
+
|
17
|
+
def validate
|
18
|
+
validates_presence :url
|
19
|
+
validates_unique :url
|
20
|
+
end
|
21
|
+
|
22
|
+
def before_save
|
23
|
+
name.strip! if name
|
24
|
+
end
|
25
|
+
|
26
|
+
def after_create
|
27
|
+
create_tags
|
28
|
+
end
|
29
|
+
|
30
|
+
def create_tags
|
31
|
+
tag_names.each do |name|
|
32
|
+
add_tag Tag.find_or_create(name: name)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
require 'ruby_pocket/favorite'
|
2
|
+
require 'delegate'
|
3
|
+
|
4
|
+
module RubyPocket
|
5
|
+
ValidationError = Class.new RubyPocketError
|
6
|
+
|
7
|
+
class FavoriteCreator
|
8
|
+
extend Forwardable
|
9
|
+
|
10
|
+
attr_accessor :web_page
|
11
|
+
|
12
|
+
delegate :name => :favorite
|
13
|
+
|
14
|
+
def initialize(params)
|
15
|
+
@params = params
|
16
|
+
end
|
17
|
+
|
18
|
+
def favorite=(favorite)
|
19
|
+
fail 'Must be a new favorite' unless favorite.new?
|
20
|
+
@favorite = favorite
|
21
|
+
end
|
22
|
+
|
23
|
+
def save
|
24
|
+
fetch_missing_data
|
25
|
+
assign_params
|
26
|
+
save_favorite
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def fetch_missing_data
|
32
|
+
return if @params[:name]
|
33
|
+
return unless web_page_contents
|
34
|
+
|
35
|
+
@params[:name] = web_page_contents.title
|
36
|
+
end
|
37
|
+
|
38
|
+
def web_page_contents
|
39
|
+
return unless @params[:url]
|
40
|
+
|
41
|
+
@page ||= web_page.for(@params[:url])
|
42
|
+
end
|
43
|
+
|
44
|
+
def assign_params
|
45
|
+
favorite.set_all @params
|
46
|
+
end
|
47
|
+
|
48
|
+
def save_favorite
|
49
|
+
favorite.save
|
50
|
+
rescue Sequel::ValidationFailed => e
|
51
|
+
raise ValidationError, e.message
|
52
|
+
end
|
53
|
+
|
54
|
+
def favorite
|
55
|
+
@favorite ||= Favorite.new
|
56
|
+
end
|
57
|
+
|
58
|
+
def web_page
|
59
|
+
@web_page ||= WebPage
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'ruby_pocket/favorite'
|
2
|
+
|
3
|
+
module RubyPocket
|
4
|
+
class FavoriteQuery
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
def self.where(options)
|
8
|
+
new.where(options)
|
9
|
+
end
|
10
|
+
|
11
|
+
delegate :all => :@scope
|
12
|
+
|
13
|
+
def initialize(scope = nil)
|
14
|
+
@scope = scope || Favorite
|
15
|
+
end
|
16
|
+
|
17
|
+
def where(options)
|
18
|
+
if options[:tag_names]
|
19
|
+
tags = Tag.find_all(options[:tag_names])
|
20
|
+
@scope = where_tags(tags)
|
21
|
+
end
|
22
|
+
|
23
|
+
self
|
24
|
+
end
|
25
|
+
|
26
|
+
private
|
27
|
+
|
28
|
+
def where_tags(tags)
|
29
|
+
tags.reduce(@scope) do |scope, tag|
|
30
|
+
scope.where(tags: tag)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
module RubyPocket
|
2
|
+
class Tag < Sequel::Model
|
3
|
+
plugin :validation_helpers
|
4
|
+
|
5
|
+
def self.find_all(names)
|
6
|
+
names.map do |name|
|
7
|
+
find(name: name).tap do |tag|
|
8
|
+
fail ArgumentError, "Tag #{name} not found" unless tag
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def name=(name)
|
14
|
+
super parameterize_name(name)
|
15
|
+
end
|
16
|
+
|
17
|
+
private
|
18
|
+
|
19
|
+
def parameterize_name(name)
|
20
|
+
name
|
21
|
+
.downcase
|
22
|
+
.strip
|
23
|
+
.gsub(/\s+/, ' ')
|
24
|
+
.gsub(/[^a-z]/, '-')
|
25
|
+
.gsub(/-+/, '-')
|
26
|
+
end
|
27
|
+
|
28
|
+
def validate
|
29
|
+
validates_unique :name
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|