diary-ruby 0.1.0 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
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
-