diary-ruby 0.1.0 → 0.2.0

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 43c0fd4d3d17a68ab7a208cb55d3ba585d02a714
4
- data.tar.gz: 89d2f6271d276120406f36f9383b9a527b54776e
3
+ metadata.gz: 2f7c7aa9aab6f4119776f1ec8e197487beddc6b0
4
+ data.tar.gz: d706e3f057f2ba5911e79bf8273057127b21db19
5
5
  SHA512:
6
- metadata.gz: 4f74fcb7dc21a055383fdfec15e2ad122e007dd74bc2829c5d20951208065f028ce27cc1af44f902fc01c3963d39cb170c3f8e0759aebba2d0d5de7771a02119
7
- data.tar.gz: 0763ab123ad356789eab5b90b1b95afac7abeb3a6e11a395feb845d60e44bda874964466335fcf86467f51d8d6a3f78ae9aac852821c02e1f95e04d69dab3039
6
+ metadata.gz: 1b43c04f2ae1cd94d0e2df2ea58c351a3969b65cf95778f1018bf1d90d586f337cee7cbb8b2be4dd9e39cb435956f10f8b334e79c27881dbc595c9929b9ea88d
7
+ data.tar.gz: 82806e6364fb1405b7c521769a1cfac4b2cab2a8da4cc495e42f0e7cc4e1f0ad10bb69d6fc7d7702cf042eb5a29aa4881dd4d5620c89817b3d745117560785aa
data/.gitignore CHANGED
@@ -7,5 +7,7 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ /*.db
10
11
  /*.store
11
12
  /.diaryrb/
13
+ /.*~
@@ -2,3 +2,5 @@ language: ruby
2
2
  rvm:
3
3
  - 2.2.1
4
4
  before_install: gem install bundler -v 1.10.5
5
+ notifications:
6
+ email: false
data/README.md CHANGED
@@ -1,4 +1,4 @@
1
- **DON'T USE THIS YET**
1
+ ![travisci](https://travis-ci.org/abachman/diary-ruby.svg?branch=master)
2
2
 
3
3
  # diary-ruby
4
4
 
@@ -36,4 +36,5 @@ Gem::Specification.new do |spec|
36
36
  spec.add_dependency 'rdiscount', '~> 2.1'
37
37
  spec.add_dependency 'slop', '~> 4.2'
38
38
  spec.add_dependency 'launchy', '~> 2.4'
39
+ spec.add_dependency 'sqlite3', '~> 1.3'
39
40
  end
@@ -7,21 +7,23 @@ require 'thread'
7
7
 
8
8
  DEFAULT_DIARY = "diaryrb.store"
9
9
 
10
- prompt_for_password = false
10
+ # prompt_for_password = false
11
11
  opts = Slop.parse do |o|
12
- # o.string '-c', '--configuration', 'config file location'
13
12
  o.string '-d', '--diary', "choose diary storage file (leave blank for default, #{DEFAULT_DIARY})"
14
- o.string '-p', '--passphrase', 'Use given encryption passphrase or prompt if option is used but no passphrase is given.', default: false do |v|
15
- if v.start_with?('-') || v.nil? || v.strip.size == 0
16
- prompt_for_password = true
17
- end
18
- end
13
+ # o.string '-c', '--configuration', 'config file location'
14
+ # o.string '-p', '--passphrase', 'Use given encryption passphrase or prompt if option is used but no passphrase is given.', default: false do |v|
15
+ # if v.start_with?('-') || v.nil? || v.strip.size == 0
16
+ # prompt_for_password = true
17
+ # end
18
+ # end
19
19
 
20
20
  # usage modes
21
21
  o.separator ''
22
22
  o.separator "Actions (can't be used in combination):"
23
+ o.bool '-x', '--export', 'export all entries immediately to JSON'
23
24
  o.string '-e', '--edit', 'edit a specific post'
24
25
  o.bool '-l', '--list', 'list all posts by date'
26
+ o.string '-t', '--tag', 'list entries, filtered by tag'
25
27
  o.bool '-s', '--serve', 'start Diary webserver'
26
28
 
27
29
  o.separator ''
@@ -57,50 +59,50 @@ end
57
59
 
58
60
  Diary::Configuration.current_diary = _diary
59
61
 
60
- _passphrase = nil
61
- if prompt_for_password
62
- require 'io/console'
63
- print "Enter passphrase (leave blank for none): "
64
- _passphrase = STDIN.noecho {|io| io.gets}.chomp
65
- elsif opts[:passphrase] && opts[:passphrase].strip.size > 0
66
- _passphrase = opts[:passphrase]
67
- elsif ENV['PASSPHRASE']
68
- _passphrase = ENV['PASSPHRASE']
69
- elsif Diary::Configuration.passphrase
70
- _passphrase = Diary::Configuration.passphrase
71
- end
72
-
73
62
  diary_path = Diary::Configuration.path || Diary::Configuration.current_diary
74
- Diary.debug "LOADING DIARY #{ Diary::Configuration.current_diary } AT PATH #{ diary_path }"
75
- if _passphrase.nil? || _passphrase.size == 0
76
- Diary.debug "LOADING WITH NO PASSPHRASE!"
77
- $store = Diary::Store.new(diary_path)
78
- else
79
- Diary.debug "LOADING WITH PASSPHRASE #{ _passphrase.gsub(/./, '*') }"
80
- $store = Diary::SecureStore.new(diary_path, _passphrase)
81
- end
63
+ database = Diary::Database.new(diary_path)
82
64
 
83
- # this is like rake db:migrate
84
- $store.write do |db|
85
- db[:entries] ||= []
86
- db[:entries] = db[:entries].compact.uniq.sort
87
- db[:tags] ||= []
88
- end
65
+ # make sure we're always up to date
66
+ migrator = Diary::Migrator.new(database)
67
+ migrator.migrate!
89
68
 
90
- if opts.list?
91
- entries = $store.read(:entries)
69
+ # initialize ORM
70
+ Diary::Model.connection = database
71
+
72
+ if opts.export?
92
73
  puts ''
93
74
 
94
- if entries.nil? || entries.size == 0
75
+ if Diary::Entry.count == 0
95
76
  puts "No entries"
96
77
  exit
97
78
  else
98
- entries.uniq.sort.reverse.each do |entry_key|
99
- puts "#{ entry_key } #{ $store.read(entry_key)[:text][0..40].gsub("\n", ' ') }..."
79
+ require 'json'
80
+
81
+ output = []
82
+
83
+ Diary::Entry.order('created_at DESC').each do |entry|
84
+ output << entry.to_hash
85
+ end
86
+
87
+ puts JSON.pretty_generate(output)
88
+ end
89
+ elsif opts.list?
90
+ puts ''
91
+ if Diary::Entry.count == 0
92
+ puts "No entries"
93
+ exit
94
+ else
95
+ Diary::Entry.order('created_at DESC').each do |entry|
96
+ puts entry.summary
100
97
  end
101
98
  end
99
+ elsif opts[:tag] && (tag_id = Diary::Model.select_value('select rowid from tags where name = ?', opts[:tag]))
100
+ entry_ids = Diary::Model.select_values('select entry_id from taggings where tag_id = ?', tag_id)
101
+ Diary::Entry.where(date_key: entry_ids).each do |entry|
102
+ puts entry.summary
103
+ end
102
104
  elsif opts.serve?
103
- Diary::Server.store = $store
105
+ # Diary::Server.store = $store
104
106
  t = Thread.new do
105
107
  Diary::Server.run!
106
108
  end
@@ -114,7 +116,7 @@ else
114
116
 
115
117
  def parse_and_store(file)
116
118
  diary_entry = Diary::Parser.parse_file(file)
117
- $store.write_entry(diary_entry)
119
+ diary_entry.save!
118
120
  end
119
121
 
120
122
  # create a tempfile to store entry in progress in EDITOR
@@ -127,18 +129,18 @@ else
127
129
  time: Time.now.strftime("%T"),
128
130
  tags: "",
129
131
  title: "",
130
- text: "text goes here"
132
+ body: "text goes here"
131
133
  }
132
134
 
133
- # if --edit option is used with a valid entry, load it
134
- if opts[:edit] && (entry_hash = $store.read(opts[:edit]))
135
- entry = Diary::Entry.from_store(entry_hash)
136
- entry_source = entry.to_hash
137
- entry_source[:tags] = entry_source[:tags].join(', ')
135
+ # # if --edit option is used with a valid entry, load it
136
+ if opts[:edit] && Diary::Entry.exists?(date_key: opts[:edit])
137
+ entry = Diary::Entry.find(date_key: opts[:edit])
138
+ entry_source = entry.to_hash if entry
139
+ # FIXME: set tags
138
140
  end
139
141
 
140
142
  # prepare entry and launch editor
141
- tmpl = Diary::Entry.generate(entry_source, $store)
143
+ tmpl = Diary::Entry.generate(entry_source)
142
144
  file.write(tmpl)
143
145
 
144
146
  ed = "vim -f"
@@ -149,12 +151,17 @@ else
149
151
  end
150
152
 
151
153
  pid = fork do
152
- exec("#{ ed } #{ file.path }")
154
+ # split the editor into a separate process
155
+ command = if /%s/ =~ ed
156
+ ed % [file.path]
157
+ else
158
+ "#{ ed } #{ file.path }"
159
+ end
160
+ exec(command)
153
161
  end
154
162
 
155
163
  # wait for child to finish, exit when the editor exits
156
164
  exit_signal = Queue.new
157
-
158
165
  trap("CLD") do
159
166
  Diary.log "CHILD PID #{pid} TERMINATED"
160
167
  exit_signal.push(true)
@@ -1,6 +1,10 @@
1
1
  require "diary-ruby/version"
2
- require "diary-ruby/store"
3
- require "diary-ruby/entry"
2
+ # require "diary-ruby/store"
3
+ require "diary-ruby/database"
4
+ require "diary-ruby/database/migrator"
5
+ require "diary-ruby/database/query"
6
+ require "diary-ruby/model"
7
+ require "diary-ruby/models/entry"
4
8
  require "diary-ruby/parser"
5
9
  require "diary-ruby/configuration"
6
10
  require "diary-ruby/server/server"
@@ -0,0 +1,22 @@
1
+ require 'sqlite3'
2
+ require 'diary-ruby/database/query'
3
+
4
+ module Diary
5
+ class Database
6
+ attr_reader :database
7
+
8
+ def initialize(path)
9
+ @database = SQLite3::Database.new(path)
10
+ end
11
+
12
+ def execute(*query)
13
+ if block_given?
14
+ @database.execute(*query) do |row|
15
+ yield row
16
+ end
17
+ else
18
+ @database.execute(*query)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,90 @@
1
+ MIGRATIONS = {}
2
+
3
+ INITIALIZE = %[
4
+ CREATE TABLE IF NOT EXISTS `versions` (
5
+ `version` TEXT NOT NULL,
6
+ `migrated_at` TEXT DEFAULT NULL
7
+ );
8
+ ]
9
+
10
+ ## MIGRATION FORMAT:
11
+ # MIGRATIONS[number] = array of sql statements
12
+
13
+ # 001 - create initial tables
14
+ MIGRATIONS['001'] = [%[
15
+ CREATE TABLE IF NOT EXISTS `entries` (
16
+ `date_key` TEXT NOT NULL PRIMARY KEY,
17
+ `day` TEXT NOT NULL,
18
+ `time` TEXT NOT NULL,
19
+ `title` TEXT,
20
+ `link` TEXT,
21
+ `body` TEXT,
22
+ `created_at` TEXT NOT NULL,
23
+ `updated_at` TEXT DEFAULT NULL
24
+ );
25
+ ], %[
26
+ CREATE INDEX IF NOT EXISTS `index_entries_on_key` on `entries` (`date_key`);
27
+ ], %[
28
+ CREATE TABLE IF NOT EXISTS `tags` (
29
+ `name` TEXT DEFAULT NULL
30
+ );
31
+ ], %[
32
+ CREATE TABLE IF NOT EXISTS `taggings` (
33
+ `tag_id` INTEGER NOT NULL,
34
+ `entry_id` TEXT NOT NULL
35
+ );
36
+ ], %[
37
+ CREATE INDEX IF NOT EXISTS `index_taggings_on_tag_id` on `taggings` (`tag_id`);
38
+ ], %[
39
+ CREATE INDEX IF NOT EXISTS `index_taggings_on_entry_id` on `taggings` (`entry_id`);
40
+ ]]
41
+
42
+ MIGRATION_VERSIONS = MIGRATIONS.keys.sort
43
+
44
+ module Diary
45
+ class Migrator
46
+ attr_reader :db
47
+
48
+ def initialize(db)
49
+ @db = db.database
50
+ end
51
+
52
+ def migrate!
53
+ exists = false
54
+ db.execute( "SELECT name FROM sqlite_master WHERE type='table' AND name='versions';" ) do |row|
55
+ if row
56
+ exists = true
57
+ end
58
+ end
59
+
60
+ if !exists
61
+ db.execute(INITIALIZE)
62
+ end
63
+
64
+ MIGRATION_VERSIONS.each do |version|
65
+ exists = false
66
+ on_date = nil
67
+ db.execute( "select rowid, migrated_at from versions WHERE version = '#{version}'" ) do |row|
68
+ if row
69
+ exists = true
70
+ on_date = row[1]
71
+ end
72
+ end
73
+
74
+ if !exists
75
+ Diary.debug("UPDATING DATABASE TO VERSION #{ version }")
76
+ if MIGRATIONS[version].is_a?(Array)
77
+ MIGRATIONS[version].each do |statement|
78
+ db.execute(statement)
79
+ end
80
+ else
81
+ db.execute(MIGRATIONS[version])
82
+ end
83
+ db.execute("INSERT INTO versions VALUES ('#{version}', strftime('%Y-%m-%dT%H:%M:%S+0000'));")
84
+ else
85
+ Diary.debug("AT #{ version } SINCE #{ on_date }")
86
+ end
87
+ end
88
+ end
89
+ end
90
+ end
@@ -0,0 +1,269 @@
1
+
2
+ module Diary
3
+ module Query
4
+ class Select
5
+ def initialize(table, context=nil)
6
+ @table_name = table
7
+ @context = context
8
+ @additions = []
9
+ end
10
+
11
+ def context
12
+ @context
13
+ end
14
+
15
+ ## evaluation conditions, called when Select is given a context
16
+ # FIXME: it's gross that Query::Select knows about Model
17
+
18
+ def execute_in_context(sql)
19
+ Diary.debug("[Query::Select execute_in_context] connection.execute(#{ sql.inspect })")
20
+ if Array === sql
21
+ context.connection.execute(*sql)
22
+ else
23
+ context.connection.execute(sql)
24
+ end
25
+ end
26
+
27
+ def first
28
+ return self unless context
29
+ result = execute_in_context(self.limit(1).to_sql)
30
+ context.materialize(result)[0]
31
+ end
32
+
33
+ def all
34
+ return self unless context
35
+ result = execute_in_context(self.to_sql)
36
+ context.materialize(result)
37
+ end
38
+
39
+ def each(&block)
40
+ return self unless context
41
+ result = execute_in_context(self.to_sql)
42
+ context.materialize(result).each(&block)
43
+ end
44
+
45
+ def map(&block)
46
+ return self unless context
47
+ result = execute_in_context(self.to_sql)
48
+ context.materialize(result).map(&block)
49
+ end
50
+
51
+ def size
52
+ return self unless context
53
+ result = execute_in_context(self.to_sql)
54
+ result.size
55
+ end
56
+ alias :count :size
57
+
58
+ ##
59
+
60
+ def select(column_query)
61
+ @column_query = column_query
62
+ end
63
+
64
+ def exists?(*conditions)
65
+ if conditions.size > 0
66
+ @additions = []
67
+ @additions << Where.new(*conditions)
68
+ @additions << Limit.new(1)
69
+ end
70
+ c = self.count
71
+ c && c > 0
72
+ end
73
+
74
+ def where(*conditions)
75
+ # multiple wheres are OR'd
76
+ @additions << Where.new(*conditions)
77
+ self
78
+ end
79
+
80
+ def order(*conditions)
81
+ @additions << Order.new(*conditions)
82
+ self
83
+ end
84
+
85
+ def limit(*conditions)
86
+ @additions << Limit.new(*conditions)
87
+ self
88
+ end
89
+
90
+ def group_by(*conditions)
91
+ @additions << GroupBy.new(*conditions)
92
+ self
93
+ end
94
+
95
+ def to_sql
96
+ # combine @additions in order: WHERE () GROUP BY () ORDER () LIMIT ()
97
+
98
+ sql_string = []
99
+ bind_vars = []
100
+
101
+ wheres = @additions.select {|a| Where === a}
102
+ group_bys = @additions.select {|a| GroupBy === a}
103
+ orders = @additions.select {|a| Order === a}
104
+ limits = @additions.select {|a| Limit === a}
105
+
106
+ if wheres.size > 0
107
+ sql_string << "WHERE"
108
+
109
+ where_params = []
110
+
111
+ wheres = wheres.each do |w|
112
+ if w.has_bound_vars?
113
+ bind_vars << w.prepared_statement.bind_vars
114
+ end
115
+
116
+ where_params << w.prepared_statement.sql_string
117
+ end
118
+
119
+ sql_string << where_params.map {|wp|
120
+ "(#{ wp })"
121
+ }.join(' OR ')
122
+ end
123
+
124
+ if group_bys.size > 0
125
+ sql_string << "GROUP BY #{group_bys.map {|gb| gb.prepared_statement.sql_string}.join(', ')}"
126
+ end
127
+
128
+ if orders.size > 0
129
+ sql_string << "ORDER BY #{orders.map {|ord| ord.prepared_statement.sql_string}.join(', ')}"
130
+ end
131
+
132
+ if limits.size > 0
133
+ # only 1 allowed, last takes precedence
134
+ limit = limits.last
135
+ sql_string << "LIMIT #{limit.prepared_statement.sql_string}"
136
+ end
137
+
138
+ query = [
139
+ "SELECT #{ @column_query || '*' }",
140
+ "FROM `#{ @table_name }`",
141
+ sql_string
142
+ ].join(' ')
143
+
144
+ # once to_sql is called, the Query is reset
145
+ @additions = []
146
+
147
+ # return sqlite compatible SQL
148
+ returning = if bind_vars.size > 0
149
+ [query, bind_vars.flatten]
150
+ else
151
+ query
152
+ end
153
+ returning
154
+ end
155
+ end
156
+
157
+ class SQLBoundParams
158
+ def initialize(left, right)
159
+ @for_sql_query = [left, right]
160
+ end
161
+
162
+ def sql_string
163
+ @for_sql_query[0]
164
+ end
165
+
166
+ def bind_vars
167
+ @for_sql_query[1]
168
+ end
169
+ end
170
+
171
+ class SQLString
172
+ def initialize(value)
173
+ @for_sql_query = value
174
+ end
175
+
176
+ def sql_string
177
+ @for_sql_query
178
+ end
179
+ end
180
+
181
+ class Node
182
+ def string_or_symbol?(value)
183
+ String === value || Symbol === value
184
+ end
185
+
186
+ def prepared_statement
187
+ @sql_result
188
+ end
189
+
190
+ def has_bound_vars?
191
+ SQLBoundParams === prepared_statement
192
+ end
193
+ end
194
+
195
+ class Where < Node
196
+ # convert conditions to AND'd list
197
+ # returns either string or (string, bind_params) 2-tuple
198
+ def initialize(*conditions)
199
+ @sql_result = if Hash === conditions[0]
200
+ attrs = conditions[0]
201
+
202
+ keys = attrs.keys
203
+ vals = keys.map {|k| attrs[k]}
204
+
205
+ and_string = keys.map do |k|
206
+ if attrs[k].is_a?(Array)
207
+ bind_hold = attrs[k].map {|_| '?'}.join(',')
208
+ "`#{k}` in (#{bind_hold})"
209
+ else
210
+ "`#{k}` = ?"
211
+ end
212
+ end.join(' AND ')
213
+
214
+ # (string, bind)
215
+ SQLBoundParams.new(and_string, vals.flatten)
216
+ elsif conditions.size > 1 && String === conditions[0]
217
+ # assume (string, bind) given
218
+ SQLBoundParams.new(conditions[0], conditions[1..-1])
219
+ elsif conditions.size == 1 && String === conditions[0]
220
+ SQLString.new(conditions[0])
221
+ end
222
+ end
223
+ end
224
+
225
+ class Order < Node
226
+ def initialize(*conditions)
227
+ sql_string = if conditions.size == 1
228
+ if string_or_symbol?(conditions[0])
229
+ conditions[0]
230
+ elsif Array === conditions[0]
231
+ conditions.join(', ')
232
+ else
233
+ conditions[0].to_s
234
+ end
235
+ elsif conditions.size > 1
236
+ conditions.join(', ')
237
+ end
238
+
239
+ @sql_result = SQLString.new(sql_string)
240
+ end
241
+ end
242
+
243
+ class Limit < Node
244
+ def initialize(*conditions)
245
+ sql_string = if conditions.size == 1
246
+ conditions[0]
247
+ elsif conditions.size > 1
248
+ conditions.join(', ')
249
+ end
250
+ @sql_result = SQLString.new(sql_string)
251
+ end
252
+ end
253
+
254
+ class GroupBy < Node
255
+ def initialize(*conditions)
256
+ sql_string = if conditions.size == 1
257
+ conditions[0]
258
+ elsif conditions.size > 1
259
+ conditions.join(', ')
260
+ end
261
+ @sql_result = SQLString.new(sql_string)
262
+ end
263
+ end
264
+
265
+ # class Table
266
+ # extend Select
267
+ # end
268
+ end
269
+ end
@@ -0,0 +1,166 @@
1
+ # taken from: https://github.com/rails/rails/blob/master/activesupport/lib/active_support/concern.rb
2
+ #
3
+ # Copyright (c) 2005-2016 David Heinemeier Hansson
4
+ #
5
+ # Permission is hereby granted, free of charge, to any person obtaining
6
+ # a copy of this software and associated documentation files (the
7
+ # "Software"), to deal in the Software without restriction, including
8
+ # without limitation the rights to use, copy, modify, merge, publish,
9
+ # distribute, sublicense, and/or sell copies of the Software, and to
10
+ # permit persons to whom the Software is furnished to do so, subject to
11
+ # the following conditions:
12
+ #
13
+ # The above copyright notice and this permission notice shall be
14
+ # included in all copies or substantial portions of the Software.
15
+ #
16
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+ #
24
+ module ActiveSupport
25
+ # A typical module looks like this:
26
+ #
27
+ # module M
28
+ # def self.included(base)
29
+ # base.extend ClassMethods
30
+ # base.class_eval do
31
+ # scope :disabled, -> { where(disabled: true) }
32
+ # end
33
+ # end
34
+ #
35
+ # module ClassMethods
36
+ # ...
37
+ # end
38
+ # end
39
+ #
40
+ # By using <tt>ActiveSupport::Concern</tt> the above module could instead be
41
+ # written as:
42
+ #
43
+ # require 'active_support/concern'
44
+ #
45
+ # module M
46
+ # extend ActiveSupport::Concern
47
+ #
48
+ # included do
49
+ # scope :disabled, -> { where(disabled: true) }
50
+ # end
51
+ #
52
+ # class_methods do
53
+ # ...
54
+ # end
55
+ # end
56
+ #
57
+ # Moreover, it gracefully handles module dependencies. Given a +Foo+ module
58
+ # and a +Bar+ module which depends on the former, we would typically write the
59
+ # following:
60
+ #
61
+ # module Foo
62
+ # def self.included(base)
63
+ # base.class_eval do
64
+ # def self.method_injected_by_foo
65
+ # ...
66
+ # end
67
+ # end
68
+ # end
69
+ # end
70
+ #
71
+ # module Bar
72
+ # def self.included(base)
73
+ # base.method_injected_by_foo
74
+ # end
75
+ # end
76
+ #
77
+ # class Host
78
+ # include Foo # We need to include this dependency for Bar
79
+ # include Bar # Bar is the module that Host really needs
80
+ # end
81
+ #
82
+ # But why should +Host+ care about +Bar+'s dependencies, namely +Foo+? We
83
+ # could try to hide these from +Host+ directly including +Foo+ in +Bar+:
84
+ #
85
+ # module Bar
86
+ # include Foo
87
+ # def self.included(base)
88
+ # base.method_injected_by_foo
89
+ # end
90
+ # end
91
+ #
92
+ # class Host
93
+ # include Bar
94
+ # end
95
+ #
96
+ # Unfortunately this won't work, since when +Foo+ is included, its <tt>base</tt>
97
+ # is the +Bar+ module, not the +Host+ class. With <tt>ActiveSupport::Concern</tt>,
98
+ # module dependencies are properly resolved:
99
+ #
100
+ # require 'active_support/concern'
101
+ #
102
+ # module Foo
103
+ # extend ActiveSupport::Concern
104
+ # included do
105
+ # def self.method_injected_by_foo
106
+ # ...
107
+ # end
108
+ # end
109
+ # end
110
+ #
111
+ # module Bar
112
+ # extend ActiveSupport::Concern
113
+ # include Foo
114
+ #
115
+ # included do
116
+ # self.method_injected_by_foo
117
+ # end
118
+ # end
119
+ #
120
+ # class Host
121
+ # include Bar # It works, now Bar takes care of its dependencies
122
+ # end
123
+ module Concern
124
+ class MultipleIncludedBlocks < StandardError #:nodoc:
125
+ def initialize
126
+ super "Cannot define multiple 'included' blocks for a Concern"
127
+ end
128
+ end
129
+
130
+ def self.extended(base) #:nodoc:
131
+ base.instance_variable_set(:@_dependencies, [])
132
+ end
133
+
134
+ def append_features(base)
135
+ if base.instance_variable_defined?(:@_dependencies)
136
+ base.instance_variable_get(:@_dependencies) << self
137
+ return false
138
+ else
139
+ return false if base < self
140
+ @_dependencies.each { |dep| base.send(:include, dep) }
141
+ super
142
+ base.extend const_get(:ClassMethods) if const_defined?(:ClassMethods)
143
+ base.class_eval(&@_included_block) if instance_variable_defined?(:@_included_block)
144
+ end
145
+ end
146
+
147
+ def included(base = nil, &block)
148
+ if base.nil?
149
+ raise MultipleIncludedBlocks if instance_variable_defined?(:@_included_block)
150
+
151
+ @_included_block = block
152
+ else
153
+ super
154
+ end
155
+ end
156
+
157
+ def class_methods(&class_methods_module_definition)
158
+ mod = const_defined?(:ClassMethods, false) ?
159
+ const_get(:ClassMethods) :
160
+ const_set(:ClassMethods, Module.new)
161
+
162
+ mod.module_eval(&class_methods_module_definition)
163
+ end
164
+ end
165
+ end
166
+
@@ -0,0 +1,98 @@
1
+ require 'diary-ruby/ext/concern'
2
+
3
+ module Diary
4
+ module ModelQuery
5
+ extend ActiveSupport::Concern
6
+
7
+ # Instance Methods
8
+ def timestamp_sql
9
+ "strftime('%Y-%m-%dT%H:%M:%S+0000')"
10
+ end
11
+
12
+ module ClassMethods
13
+ def columns
14
+ @columns ||= connection.execute("PRAGMA table_info(#{table_name})")
15
+ end
16
+
17
+ def column_names
18
+ @column_names ||= columns.map {|col_info| col_info[1]}
19
+ end
20
+
21
+ def results_to_hashes(array_of_rows)
22
+ array_of_rows.map do |row|
23
+ Hash[ column_names.zip(row) ]
24
+ end
25
+ end
26
+
27
+ def materialize(array_of_rows)
28
+ results_to_hashes(array_of_rows).map do |record_hash|
29
+ if respond_to?(:from_hash)
30
+ from_hash(record_hash)
31
+ else
32
+ record_hash
33
+ end
34
+ end
35
+ end
36
+
37
+ def find(attrs)
38
+ where(attrs).first
39
+ end
40
+
41
+ def new_select_relation
42
+ Diary::Query::Select.new(table_name, self)
43
+ end
44
+
45
+ %w(where order group_by limit exists?).each do |q_type|
46
+ define_method(q_type.to_sym) do |*conditions|
47
+ new_select_relation.send(q_type.to_sym, *conditions)
48
+ end
49
+ end
50
+
51
+ %w(all first count).each do |q_type|
52
+ define_method(q_type.to_sym) do
53
+ new_select_relation.send(q_type.to_sym)
54
+ end
55
+ end
56
+
57
+ %w(each map).each do |q_type|
58
+ define_method(q_type.to_sym) do |&block|
59
+ new_select_relation.send(q_type.to_sym, &block)
60
+ end
61
+ end
62
+
63
+ end
64
+ end
65
+
66
+ class Model
67
+ include ModelQuery
68
+
69
+ class << self
70
+ def connection=(db)
71
+ @@connection = db
72
+ end
73
+
74
+ def connection
75
+ @@connection
76
+ end
77
+
78
+ # one-off queries
79
+ def execute(sql, *binds)
80
+ Diary.debug("[Model execute] #{ sql } #{ binds.inspect }")
81
+ connection.execute(sql, binds)
82
+ end
83
+
84
+ # one-off queries
85
+ def select_rows(sql, *binds)
86
+ execute(sql, *binds)
87
+ end
88
+
89
+ def select_values(sql, *binds)
90
+ select_rows(sql, *binds).map {|row| row[0]}
91
+ end
92
+
93
+ def select_value(sql, *binds)
94
+ select_values(sql, *binds).first
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,151 @@
1
+ TEMPLATE = "# last entry posted at %{last_update_at}
2
+
3
+ DAY %{day}
4
+ TIME %{time}
5
+ TAGS %{tags}
6
+ TITLE %{title}
7
+
8
+ ---
9
+
10
+ %{body}
11
+ "
12
+
13
+ require 'rdiscount'
14
+ require 'diary-ruby/model'
15
+
16
+ module Diary
17
+ class Entry < Model
18
+ attr_accessor :day, :time, :tags, :body, :link, :title, :date_key
19
+
20
+ def self.table_name
21
+ 'entries'
22
+ end
23
+
24
+ def self.from_hash(record)
25
+ entry = self.new(
26
+ day: record['day'],
27
+ time: record['time'],
28
+ body: record['body'],
29
+ title: record['title'],
30
+ date_key: record['date_key'],
31
+ )
32
+
33
+ # taggings!
34
+ begin
35
+ tag_ids = select_values('select tag_id from taggings where entry_id = ?', [entry.identifier])
36
+ bind_hold = tag_ids.map {|_| '?'}.join(',')
37
+ entry.tags = select_values("select name from tags where rowid in (#{ bind_hold })", *tag_ids)
38
+ rescue => ex
39
+ Diary.debug "FAILED TO LOAD TAGS. #{ ex.message }"
40
+ end
41
+
42
+ entry
43
+ end
44
+
45
+ def self.keygen(day, time)
46
+ "%s-%s" % [day, time]
47
+ end
48
+
49
+ def self.generate(options={})
50
+ options[:last_update_at] = connection.execute("select max(updated_at) from #{table_name}")[0] || ''
51
+
52
+ # convert Arrays to dumb CSV
53
+ options.each do |(k, v)|
54
+ if v.is_a?(Array)
55
+ options[k] = v.join(', ')
56
+ end
57
+ end
58
+
59
+ TEMPLATE % options
60
+ end
61
+
62
+ def initialize(options={})
63
+ @day = options[:day]
64
+ @time = options[:time]
65
+ @tags = options[:tags] || []
66
+ @body = options[:body]
67
+ @title = options[:title]
68
+
69
+ if options[:date_key].nil?
70
+ @date_key = identifier
71
+ else
72
+ @date_key = options[:date_key]
73
+ end
74
+ end
75
+
76
+ def identifier
77
+ self.class.keygen(day, time)
78
+ end
79
+
80
+ def formatted_body
81
+ RDiscount.new(body).to_html
82
+ end
83
+
84
+ def truncated_body
85
+ _truncated = body
86
+ if _truncated.size > 40
87
+ _truncated = "#{ _truncated[0..40] }..."
88
+ end
89
+ _truncated
90
+ end
91
+
92
+ def summary
93
+ "%-24s%-46s%s" % [date_key, truncated_body, tags.join(', ')]
94
+ end
95
+
96
+ def to_hash
97
+ {
98
+ day: day,
99
+ time: time,
100
+ tags: tags,
101
+ body: body,
102
+ title: title,
103
+ date_key: date_key,
104
+ }
105
+ end
106
+
107
+ def save!
108
+ if self.class.find(date_key: date_key)
109
+ # update record
110
+ sql = "UPDATE entries SET day=?, time=?, body=?, link=?, title=?, updated_at=#{timestamp_sql} WHERE date_key=?"
111
+ self.class.execute(sql, day, time, body, link, title, date_key)
112
+ else
113
+ # insert
114
+ sql = %[INSERT INTO entries (day, time, body, link, title, date_key, created_at, updated_at)
115
+ VALUES (?, ?, ?, ?, ?, ?, #{timestamp_sql}, #{timestamp_sql})]
116
+ self.class.execute(sql, day, time, body, link, title, date_key)
117
+ end
118
+
119
+ begin
120
+ update_tags!
121
+ rescue => ex
122
+ Diary.debug "FAILED TO UPDATE TAGS #{ tags.inspect }. #{ ex.message }"
123
+ end
124
+ end
125
+
126
+ def update_tags!
127
+ # clean out existing
128
+ Diary.debug "CLEANING `taggings`"
129
+ self.class.execute('delete from taggings where entry_id = ?', [identifier])
130
+
131
+ # add back
132
+ tags.each do |tag|
133
+ # is tag in db?
134
+ tag_id = self.class.select_value('select rowid from tags where name = ?', tag)
135
+
136
+ if tag_id.nil?
137
+ Diary.debug "CREATING TAG #{ tag.inspect }"
138
+
139
+ # exists
140
+ Diary.debug self.class.select_rows('PRAGMA table_info(tags)').inspect
141
+ self.class.execute("insert into tags (name) values (?)", [tag])
142
+ tag_id = self.class.select_value('select last_insert_rowid()')
143
+ end
144
+
145
+ Diary.debug "CREATING tagging"
146
+ self.class.execute('insert into taggings (tag_id, entry_id) values (?, ?)', [tag_id, identifier])
147
+ end
148
+ end
149
+ end
150
+ end
151
+
@@ -46,11 +46,10 @@ module Diary
46
46
  Diary.debug "BODY #{ body.join(" ") }"
47
47
 
48
48
  return Entry.new(
49
- nil,
50
49
  day: metadata['day'],
51
50
  time: metadata['time'],
52
51
  tags: metadata['tags'],
53
- text: body.join("\n").strip,
52
+ body: body.join("\n").strip,
54
53
  title: metadata['title'],
55
54
  key: key,
56
55
  )
@@ -22,27 +22,10 @@ module Diary
22
22
  end
23
23
 
24
24
  get '/' do
25
- keys = store.read(:entries)
26
-
27
- if keys.nil?
28
- store.write do |db|
29
- db[:entries] = []
30
- end
31
-
32
- keys = []
25
+ @entries = Entry.order('created_at DESC').map do |entry_hash|
26
+ Entry.from_hash(entry_hash)
33
27
  end
34
28
 
35
- @entries = keys.uniq.map {|entry_key|
36
- entry = store.read(entry_key)
37
-
38
- if entry
39
- logger.debug "LOAD #{ entry }"
40
- Entry.from_store(entry)
41
- else
42
- nil
43
- end
44
- }.compact
45
-
46
29
  logger.info "returning keys: #{ keys }"
47
30
  logger.info "returning entries: #{ @entries }"
48
31
 
@@ -53,16 +36,13 @@ module Diary
53
36
  content_type :json
54
37
 
55
38
  key = params[:key]
56
- entry_hash = store.read(key)
57
- entry = Entry.from_store(entry_hash)
58
-
59
- content = RDiscount.new entry.text
60
- entry_hash[:formatted] = content.to_html
39
+ entry_hash = Entry.find(date_key: key)
61
40
 
62
- if entry
41
+ if entry_hash
42
+ entry = Entry.from_store(entry_hash)
43
+ content = RDiscount.new entry['body']
44
+ entry_hash[:formatted] = content.to_html
63
45
  entry_hash.to_json
64
- else
65
- {}
66
46
  end
67
47
  end
68
48
 
@@ -81,7 +61,7 @@ module Diary
81
61
  day: params[:day],
82
62
  time: params[:time],
83
63
  tags: tags,
84
- text: params[:text],
64
+ body: params[:body],
85
65
  )
86
66
 
87
67
  store.write_entry(entry)
@@ -45,7 +45,7 @@
45
45
 
46
46
  <label>
47
47
  <span>Text</span>
48
- <textarea class='text' name='text'></textarea>
48
+ <textarea class='text' name='body'></textarea>
49
49
  </label>
50
50
 
51
51
  <input type='submit' value='commit' />
@@ -1,3 +1,3 @@
1
1
  module Diary
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: diary-ruby
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Adam Bachman
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2016-01-07 00:00:00.000000000 Z
11
+ date: 2016-01-24 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -122,6 +122,20 @@ dependencies:
122
122
  - - "~>"
123
123
  - !ruby/object:Gem::Version
124
124
  version: '2.4'
125
+ - !ruby/object:Gem::Dependency
126
+ name: sqlite3
127
+ requirement: !ruby/object:Gem::Requirement
128
+ requirements:
129
+ - - "~>"
130
+ - !ruby/object:Gem::Version
131
+ version: '1.3'
132
+ type: :runtime
133
+ prerelease: false
134
+ version_requirements: !ruby/object:Gem::Requirement
135
+ requirements:
136
+ - - "~>"
137
+ - !ruby/object:Gem::Version
138
+ version: '1.3'
125
139
  description: 'A command line diary: diaryrb'
126
140
  email:
127
141
  - adam.bachman@gmail.com
@@ -144,9 +158,14 @@ files:
144
158
  - exe/diaryrb
145
159
  - lib/diary-ruby.rb
146
160
  - lib/diary-ruby/configuration.rb
147
- - lib/diary-ruby/entry.rb
161
+ - lib/diary-ruby/database.rb
162
+ - lib/diary-ruby/database/migrator.rb
163
+ - lib/diary-ruby/database/query.rb
164
+ - lib/diary-ruby/ext/concern.rb
148
165
  - lib/diary-ruby/ext/encryptor.rb
149
166
  - lib/diary-ruby/ext/secure_pstore.rb
167
+ - lib/diary-ruby/model.rb
168
+ - lib/diary-ruby/models/entry.rb
150
169
  - lib/diary-ruby/parser.rb
151
170
  - lib/diary-ruby/server/public/script.js
152
171
  - lib/diary-ruby/server/public/style.css
@@ -1,85 +0,0 @@
1
- TEMPLATE = "# last entry posted at %{last_update_at}
2
-
3
- DAY %{day}
4
- TIME %{time}
5
- TAGS %{tags}
6
- TITLE %{title}
7
-
8
- ---
9
-
10
- %{text}
11
- "
12
-
13
- require 'rdiscount'
14
-
15
- module Diary
16
- class Entry
17
- attr_accessor :version, :day, :time, :tags, :text, :title, :key
18
-
19
- CURRENT_VERSION = 1
20
-
21
- def self.from_store(record)
22
- if record[:version] == 1
23
- self.new(
24
- record[:version],
25
- day: record[:day],
26
- time: record[:time],
27
- tags: record[:tags],
28
- text: record[:text],
29
- title: record[:title],
30
- key: record[:key],
31
- )
32
- end
33
- end
34
-
35
- def self.keygen(day, time)
36
- "%s-%s" % [day, time]
37
- end
38
-
39
- def self.generate(options={}, store)
40
- options[:last_update_at] = store.read(:last_update_at)
41
-
42
- # convert Arrays to dumb CSV
43
- options.each do |(k, v)|
44
- if v.is_a?(Array)
45
- options[k] = v.join(', ')
46
- end
47
- end
48
-
49
- TEMPLATE % options
50
- end
51
-
52
- def initialize(version, options={})
53
- @version = version || CURRENT_VERSION
54
-
55
- @day = options[:day]
56
- @time = options[:time]
57
- @tags = options[:tags] || []
58
- @text = options[:text]
59
- @title = options[:title]
60
-
61
- if options[:key].nil?
62
- @key = Entry.keygen(day, time)
63
- else
64
- @key = options[:key]
65
- end
66
- end
67
-
68
- def formatted_text
69
- RDiscount.new(text).to_html
70
- end
71
-
72
- def to_hash
73
- {
74
- version: version,
75
- day: day,
76
- time: time,
77
- tags: tags,
78
- text: text,
79
- title: '',
80
- key: key,
81
- }
82
- end
83
- end
84
- end
85
-