ruby_pocket 0.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (45) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +191 -0
  4. data/Rakefile +51 -0
  5. data/config/config.rb +14 -0
  6. data/config/database.rb +14 -0
  7. data/db/migrations/001_create_tags.rb +15 -0
  8. data/db/migrations/002_create_favorites.rb +17 -0
  9. data/db/migrations/003_create_favorites_tags.rb +20 -0
  10. data/db/pocket_development.db +0 -0
  11. data/db/pocket_test.db +0 -0
  12. data/lib/ruby_pocket/all.rb +5 -0
  13. data/lib/ruby_pocket/cli/add_action.rb +12 -0
  14. data/lib/ruby_pocket/cli/delete_action.rb +16 -0
  15. data/lib/ruby_pocket/cli/list_action.rb +39 -0
  16. data/lib/ruby_pocket/cli/open_action.rb +12 -0
  17. data/lib/ruby_pocket/cli/options.rb +23 -0
  18. data/lib/ruby_pocket/cli.rb +8 -0
  19. data/lib/ruby_pocket/environment.rb +11 -0
  20. data/lib/ruby_pocket/favorite.rb +36 -0
  21. data/lib/ruby_pocket/favorite_creator.rb +62 -0
  22. data/lib/ruby_pocket/favorite_query.rb +34 -0
  23. data/lib/ruby_pocket/tag.rb +32 -0
  24. data/lib/ruby_pocket/version.rb +3 -0
  25. data/lib/ruby_pocket/web_page.rb +33 -0
  26. data/lib/ruby_pocket.rb +37 -0
  27. data/test/ruby_pocket/cli/options_test.rb +49 -0
  28. data/test/ruby_pocket/favorite_creator_test.rb +80 -0
  29. data/test/ruby_pocket/favorite_query_test.rb +98 -0
  30. data/test/ruby_pocket/favorite_test.rb +76 -0
  31. data/test/ruby_pocket/features/add_test.rb +40 -0
  32. data/test/ruby_pocket/features/delete_test.rb +45 -0
  33. data/test/ruby_pocket/features/list_test.rb +75 -0
  34. data/test/ruby_pocket/tag_test.rb +53 -0
  35. data/test/ruby_pocket/web_page_test.rb +68 -0
  36. data/test/support/add_feature_mocker.rb +37 -0
  37. data/test/support/custom_assertions.rb +9 -0
  38. data/test/support/database_test_case.rb +11 -0
  39. data/test/support/default_test_case.rb +9 -0
  40. data/test/support/favorite_factory.rb +14 -0
  41. data/test/support/feature_assertions.rb +22 -0
  42. data/test/support/feature_test_case.rb +32 -0
  43. data/test/support/webmock.rb +3 -0
  44. data/test/test_helper.rb +19 -0
  45. 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
+ [![Code Climate](https://codeclimate.com/github/thiagoa/ruby_pocket/badges/gpa.svg)](https://codeclimate.com/github/thiagoa/ruby_pocket)
4
+ [![Test Coverage](https://codeclimate.com/github/thiagoa/ruby_pocket/badges/coverage.svg)](https://codeclimate.com/github/thiagoa/ruby_pocket/coverage)
5
+ [![Travis CI](https://travis-ci.org/thiagoa/ruby_pocket.svg)](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'
@@ -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,15 @@
1
+ Sequel.migration do
2
+ up do
3
+ unless table_exists?(:tags)
4
+ create_table :tags do
5
+ primary_key :id
6
+
7
+ String :name, unique: true
8
+ end
9
+ end
10
+ end
11
+
12
+ down do
13
+ drop_table :tags
14
+ end
15
+ 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,5 @@
1
+ require 'ruby_pocket'
2
+
3
+ Dir.glob(File.join(__dir__, '**', '*.rb')).each do |file|
4
+ require file
5
+ end
@@ -0,0 +1,12 @@
1
+ module RubyPocket
2
+ module Cli
3
+ class AddAction
4
+ def call(options)
5
+ creator = FavoriteCreator.new(options.values)
6
+ creator.save
7
+
8
+ puts "Favorite '#{creator.name}' created!"
9
+ end
10
+ end
11
+ end
12
+ end
@@ -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,12 @@
1
+ module RubyPocket
2
+ module Cli
3
+ class OpenAction
4
+ def call(options)
5
+ favorite = Favorite[options.values[:id]]
6
+ fail ArgumentError, 'Favorite not found!' unless favorite
7
+
8
+ %x(open #{favorite.url})
9
+ end
10
+ end
11
+ end
12
+ 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,8 @@
1
+ module RubyPocket
2
+ module Cli
3
+ def self.dispatch(options)
4
+ action = const_get("#{options.action.to_s.capitalize}Action")
5
+ action.new.call options
6
+ end
7
+ end
8
+ end
@@ -0,0 +1,11 @@
1
+ module RubyPocket
2
+ class Environment < String
3
+ ALL = %w(PRODUCTION DEVELOPMENT TEST)
4
+
5
+ ALL.each do |env|
6
+ define_method "#{env.downcase}?" do
7
+ self == env
8
+ end
9
+ end
10
+ end
11
+ 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
@@ -0,0 +1,3 @@
1
+ module RubyPocket
2
+ VERSION = '0.1.1'
3
+ end