textrepo 0.4.1 → 0.5.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
  SHA256:
3
- metadata.gz: 4d26af6dd86e8b79c9e50300f8c31102bf79d5c751e175740a7225c2ca4a9b33
4
- data.tar.gz: 8d398c0848c0a4b06d2a775f3cb6bf1f49489be7f5ba5d8cf7eab2aad068b5c7
3
+ metadata.gz: dc5cf6089b4883c93dc228e19aa0a149d71a8d9cc26a92e6c75fdc4ee0b2d694
4
+ data.tar.gz: 8ebecace02d486b6b6c12256d52adfd44963a3a14f6d39729b83d426bce3a26e
5
5
  SHA512:
6
- metadata.gz: ef79855cd99ebc3baea4368c00cdc4d439478cfc9733d45eee1e7a08ca005bf0dc15824cb57b46326c982e724acc762b25b722c112f19ae4f67349d575fffdcb
7
- data.tar.gz: 4af657999971e6d3cef41dc5d3ef7ea1017a536b772372d38e79ffb91196c239585a0c932fe10e42f9d7ccbfee8fa477ec31bd97afa9cd4e1c31f0c894772110
6
+ metadata.gz: aef27bf0363a66eeb1676ccda0682c253155875a8d0c16b27189674836edf2563b8df87b7f3b56cdb5d02c895f724f47a956588a5d97d24d83cdddce76fc083e
7
+ data.tar.gz: 81a4f8a76eb0ec8545e29ddbd537d49ec00f6009f5617dad61c61d8a5552c6df090d6d37fd507a7976c5f5f60b7d0c8b43f9a99d98d7efa1e933828270711d87
data/.gitignore CHANGED
@@ -1,8 +1,57 @@
1
- /.bundle/
2
- /.yardoc
3
- /_yardoc/
1
+ Gemfile.lock
2
+ *.gem
3
+ *.rbc
4
+ /.config
4
5
  /coverage/
5
- /doc/
6
+ /InstalledFiles
6
7
  /pkg/
7
8
  /spec/reports/
9
+ /spec/examples.txt
10
+ /test/tmp/
11
+ /test/version_tmp/
8
12
  /tmp/
13
+
14
+ # Used by dotenv library to load environment variables.
15
+ # .env
16
+
17
+ # Ignore Byebug command history file.
18
+ .byebug_history
19
+
20
+ ## Specific to RubyMotion:
21
+ .dat*
22
+ .repl_history
23
+ build/
24
+ *.bridgesupport
25
+ build-iPhoneOS/
26
+ build-iPhoneSimulator/
27
+
28
+ ## Specific to RubyMotion (use of CocoaPods):
29
+ #
30
+ # We recommend against adding the Pods directory to your .gitignore. However
31
+ # you should judge for yourself, the pros and cons are mentioned at:
32
+ # https://guides.cocoapods.org/using/using-cocoapods.html#should-i-check-the-pods-directory-into-source-control
33
+ #
34
+ # vendor/Pods/
35
+
36
+ ## Documentation cache and generated files:
37
+ /.yardoc/
38
+ /_yardoc/
39
+ /doc/
40
+ /rdoc/
41
+
42
+ ## Environment normalization:
43
+ /.bundle/
44
+ /vendor/bundle
45
+ /lib/bundler/man/
46
+
47
+ # for a library or gem, you might want to ignore these files since the code is
48
+ # intended to run in multiple environments; otherwise, check them in:
49
+ # Gemfile.lock
50
+ # .ruby-version
51
+ # .ruby-gemset
52
+
53
+ # unless supporting rvm < 1.11.0 or doing something fancy, ignore this:
54
+ .rvmrc
55
+
56
+ # Used by RuboCop. Remote config files pulled in from inherit_from directive.
57
+ # .rubocop-https?--*
@@ -7,6 +7,11 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
  ## [Unreleased]
8
8
  Nothing to record here.
9
9
 
10
+ ## [0.5.0] - 2020-11-01
11
+ ### Added
12
+ - Add a new API `Repository#search`.
13
+ - Add a new API `Repository#exist?`. (0.4.3)
14
+
10
15
  ## [0.4.0] - 2020-10-14
11
16
  ### Added
12
17
  - Released to rubygems.org.
data/Rakefile CHANGED
@@ -35,3 +35,11 @@ end
35
35
  task :clobber => :clean_sandbox
36
36
  CLOBBER << 'test/fixtures/notes'
37
37
  CLOBBER << 'test/fixtures/test_repo'
38
+
39
+ require "rdoc/task"
40
+
41
+ RDoc::Task.new do |rdoc|
42
+ rdoc.generator = "ri"
43
+ rdoc.rdoc_dir = "doc"
44
+ rdoc.rdoc_files.include("lib/**/*.rb")
45
+ end
@@ -1,12 +1,39 @@
1
1
  module Textrepo
2
+
3
+ ##
4
+ # Following errors might occur in repository operations:
5
+ # +---------------------------------+-----------------------+
6
+ # | operation (args) | error type |
7
+ # +---------------------------------+-----------------------+
8
+ # | create (timestamp, text) | Duplicate timestamp |
9
+ # | | Empty text |
10
+ # +---------------------------------+-----------------------+
11
+ # | read (timestamp) | Missing timestamp |
12
+ # +---------------------------------+-----------------------+
13
+ # | update (timestamp, text) | Mssing timestamp |
14
+ # | | Empty text |
15
+ # +---------------------------------+-----------------------+
16
+ # | delete (timestamp) | Missing timestamp |
17
+ # +---------------------------------+-----------------------+
18
+ # | search (pattern, stamp_pattern) | Invalid search result |
19
+ # +---------------------------------+-----------------------+
20
+
2
21
  class Error < StandardError; end
3
22
 
23
+ # :stopdoc:
4
24
  module ErrMsg
5
25
  UNKNOWN_REPO_TYPE = 'unknown type for repository: %s'
6
26
  DUPLICATE_TIMESTAMP = 'duplicate timestamp: %s'
7
27
  EMPTY_TEXT = 'empty text'
8
28
  MISSING_TIMESTAMP = 'missing timestamp: %s'
29
+ INVALID_TIMESTAMP_STRING = "invalid string as timestamp: %s"
30
+ INVALID_SEARCH_RESULT = "invalid result by searcher: %s"
9
31
  end
32
+ # :startdoc:
33
+
34
+ ##
35
+ # An error raised if unknown type was specified as the repository
36
+ # type.
10
37
 
11
38
  class UnknownRepoTypeError < Error
12
39
  def initialize(type)
@@ -14,20 +41,9 @@ module Textrepo
14
41
  end
15
42
  end
16
43
 
17
- # Following errors might occur in repository operations:
18
- # +--------------------------+---------------------+
19
- # | operation (args) | error type |
20
- # +--------------------------+---------------------+
21
- # | create (timestamp, text) | Duplicate timestamp |
22
- # | | Empty text |
23
- # +--------------------------+---------------------+
24
- # | read (timestamp) | Missing timestamp |
25
- # +--------------------------+---------------------+
26
- # | update (timestamp, text) | Mssing timestamp |
27
- # | | Empty text |
28
- # +--------------------------+---------------------+
29
- # | delete (timestamp) | Missing timestamp |
30
- # +--------------------------+---------------------+
44
+ ##
45
+ # An error raised if the specified timestamp has already exist in
46
+ # the repository.
31
47
 
32
48
  class DuplicateTimestampError < Error
33
49
  def initialize(timestamp)
@@ -35,16 +51,43 @@ module Textrepo
35
51
  end
36
52
  end
37
53
 
54
+ ##
55
+ # An error raised if the given text is empty.
56
+
38
57
  class EmptyTextError < Error
39
58
  def initialize
40
59
  super(ErrMsg::EMPTY_TEXT)
41
60
  end
42
61
  end
43
62
 
63
+ ##
64
+ # An error raised if the given timestamp has not exist in the
65
+ # repository.
66
+
44
67
  class MissingTimestampError < Error
45
68
  def initialize(timestamp)
46
69
  super(ErrMsg::MISSING_TIMESTAMP % timestamp)
47
70
  end
48
71
  end
49
72
 
73
+ ##
74
+ # An error raised if an argument is invalid to convert a
75
+ # Textrepo::Timestamp object.
76
+
77
+ class InvalidTimestampStringError < Error
78
+ def initialize(str)
79
+ super(ErrMsg::INVALID_TIMESTAMP_STRING % str)
80
+ end
81
+ end
82
+
83
+ ##
84
+ # An error raise if the search result is not suitable to use.
85
+ #
86
+
87
+ class InvalidSearchResultError < Error
88
+ def initialize(str)
89
+ super(ErrMsg::INVALID_SEARCH_RESULT % str)
90
+ end
91
+ end
92
+
50
93
  end
@@ -1,26 +1,89 @@
1
1
  require 'fileutils'
2
+ require "open3"
2
3
 
3
4
  module Textrepo
4
- # A concrete repository which uses the default file system as a storage.
5
+
6
+ ##
7
+ # A concrete class which implements Repository interfaces. This
8
+ # repository uses the default file system of the operating system as
9
+ # a text storage.
10
+
5
11
  class FileSystemRepository < Repository
6
- attr_reader :path, :extname
12
+
13
+ ##
14
+ # Repository root.
15
+
16
+ attr_reader :path
17
+
18
+ ##
19
+ # Extension of notes sotres in the repository.
20
+
21
+ attr_reader :extname
22
+
23
+ ##
24
+ # Searcher program name.
25
+
26
+ attr_reader :searcher
27
+
28
+ ##
29
+ # An array of options to pass to the searcher program.
30
+
31
+ attr_reader :searcher_options
32
+
33
+ ##
34
+ # Default name for the repository which uses when no name is
35
+ # specified in the configuration settings.
7
36
 
8
37
  FAVORITE_REPOSITORY_NAME = 'notes'
38
+
39
+ ##
40
+ # Default extension of notes which uses when no extname is
41
+ # specified in the configuration settings.
42
+
9
43
  FAVORITE_EXTNAME = 'md'
10
44
 
11
- # `conf` must be a Hash object. It must hold the follwoing
12
- # values:
45
+ ##
46
+ # Default searcher program to search text in the repository.
47
+
48
+ FAVORITE_SEARCHER = 'grep'
49
+
50
+ ##
51
+ # Creates a new repository object. The argument, `conf` must be a
52
+ # Hash object. It should hold the follwoing values:
13
53
  #
14
- # - :repository_type (:file_system)
15
- # - :repository_name => basename of the root path for the repository
16
- # - :repository_base => the parent directory path for the repository
17
- # - :default_extname => extname for a file stored into in the repository
54
+ # - MANDATORY:
55
+ # - :repository_type => `:file_system`
56
+ # - :repository_base => the parent directory path for the repository
57
+ # - OPTIONAL: (if not specified, default values are used)
58
+ # - :repository_name => basename of the root path for the repository
59
+ # - :default_extname => extname for a file stored into in the repository
60
+ # - :searcher => a program to search like `grep`
61
+ # - :searcher_options => an Array of option to pass to the searcher
18
62
  #
19
63
  # The root path of the repository looks like the following:
20
64
  # - conf[:repository_base]/conf[:repository_name]
21
65
  #
22
- # Default values are set when `repository_name` and `default_extname`
66
+ # Default values are set when `:repository_name` and `:default_extname`
23
67
  # were not defined in `conf`.
68
+ #
69
+ # Be careful to set `:searcher_options`, it must be to specify the
70
+ # searcher behavior equivalent to `grep` with "-inR". The default
71
+ # value for the searcher options is defined for BSD grep (default
72
+ # grep on macOS), GNU grep, and ripgrep (aka rg). They are:
73
+ #
74
+ # "grep" => ["-i", "-n", "-R", "-E"]
75
+ # "egrep" => ["-i", "-n", "-R"]
76
+ # "ggrep" => ["-i", "-n", "-R", "-E"]
77
+ # "gegrep" => ["-i", "-n", "-R"]
78
+ # "rg" => ["-S", "-n", "--no-heading", "--color", "never"]
79
+ #
80
+ # If use those 3 searchers, it is not recommended to set
81
+ # `:searcher_options`. The default value works well in
82
+ # `textrepo`.
83
+ #
84
+ # :call-seq:
85
+ # new(Hash or Hash like object) -> FileSystemRepository
86
+
24
87
  def initialize(conf)
25
88
  super
26
89
  base = conf[:repository_base]
@@ -28,14 +91,17 @@ module Textrepo
28
91
  @path = File.expand_path("#{name}", base)
29
92
  FileUtils.mkdir_p(@path)
30
93
  @extname = conf[:default_extname] || FAVORITE_EXTNAME
94
+ @searcher = find_searcher(conf[:searcher])
95
+ @searcher_options = conf[:searcher_options]
31
96
  end
32
97
 
33
- #
34
- # repository operations
35
- #
36
-
98
+ ##
37
99
  # Creates a file into the repository, which contains the specified
38
100
  # text and is associated to the timestamp.
101
+ #
102
+ # :call-seq:
103
+ # create(Timestamp, Array) -> Timestamp
104
+
39
105
  def create(timestamp, text)
40
106
  abs = abspath(timestamp)
41
107
  raise DuplicateTimestampError, timestamp if FileTest.exist?(abs)
@@ -45,8 +111,13 @@ module Textrepo
45
111
  timestamp
46
112
  end
47
113
 
114
+ ##
48
115
  # Reads the file content in the repository. Then, returns its
49
116
  # content.
117
+ #
118
+ # :call-seq:
119
+ # read(Timestamp) -> Array
120
+
50
121
  def read(timestamp)
51
122
  abs = abspath(timestamp)
52
123
  raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
@@ -57,8 +128,13 @@ module Textrepo
57
128
  content
58
129
  end
59
130
 
131
+ ##
60
132
  # Updates the file content in the repository. A new timestamp
61
133
  # will be attached to the text.
134
+ #
135
+ # :call-seq:
136
+ # update(Timestamp, Array) -> Timestamp
137
+
62
138
  def update(timestamp, text)
63
139
  raise EmptyTextError if text.empty?
64
140
  org_abs = abspath(timestamp)
@@ -75,7 +151,12 @@ module Textrepo
75
151
  new_stamp
76
152
  end
77
153
 
154
+ ##
78
155
  # Deletes the file in the repository.
156
+ #
157
+ # :call-seq:
158
+ # delete(Timestamp) -> Array
159
+
79
160
  def delete(timestamp)
80
161
  abs = abspath(timestamp)
81
162
  raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
@@ -86,7 +167,12 @@ module Textrepo
86
167
  content
87
168
  end
88
169
 
170
+ ##
89
171
  # Finds entries of text those timestamp matches the specified pattern.
172
+ #
173
+ # :call-seq:
174
+ # entries(String = nil) -> Array of Timestamp instances
175
+
90
176
  def entries(stamp_pattern = nil)
91
177
  results = []
92
178
 
@@ -94,7 +180,7 @@ module Textrepo
94
180
  when "yyyymoddhhmiss_lll".size
95
181
  stamp = Timestamp.parse_s(stamp_pattern)
96
182
  if exist?(stamp)
97
- results << stamp.to_s
183
+ results << stamp
98
184
  end
99
185
  when 0, "yyyymoddhhmiss".size, "yyyymodd".size
100
186
  results += find_entries(stamp_pattern)
@@ -117,12 +203,54 @@ module Textrepo
117
203
  results
118
204
  end
119
205
 
206
+ ##
207
+ # Check the existence of text which is associated with the given
208
+ # timestamp.
209
+ #
210
+ # :call-seq:
211
+ # exist?(Timestamp) -> true or false
212
+
213
+ def exist?(timestamp)
214
+ FileTest.exist?(abspath(timestamp))
215
+ end
216
+
217
+ ##
218
+ # Searches a pattern in all text. The given pattern is a word to
219
+ # search or a regular expression. The pattern would be passed to
220
+ # a searcher program as it passed.
221
+ #
222
+ # See the document for Textrepo::Repository#search to know about
223
+ # the search result.
224
+ #
225
+ # :call-seq:
226
+ # search(String for pattern, String for Timestamp pattern) -> Array
227
+
228
+ def search(pattern, stamp_pattern = nil)
229
+ result = nil
230
+ if stamp_pattern.nil?
231
+ result = invoke_searcher_at_repo_root(@searcher, pattern)
232
+ else
233
+ result = invoke_searcher_for_entries(@searcher, pattern, entries(stamp_pattern))
234
+ end
235
+ construct_search_result(result)
236
+ end
237
+
238
+ # :stopdoc:
239
+
120
240
  private
121
241
  def abspath(timestamp)
122
- filename = timestamp.to_pathname + ".#{@extname}"
242
+ filename = timestamp_to_pathname(timestamp) + ".#{@extname}"
123
243
  File.expand_path(filename, @path)
124
244
  end
125
245
 
246
+ # %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
247
+ # "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
248
+ # "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
249
+ def timestamp_to_pathname(timestamp)
250
+ yyyy, mo = Timestamp.split_stamp(timestamp.to_s)[0..1]
251
+ File.join(yyyy, mo, timestamp.to_s)
252
+ end
253
+
126
254
  def write_text(abs, text)
127
255
  FileUtils.mkdir_p(File.dirname(abs))
128
256
  File.open(abs, 'w') { |f|
@@ -134,15 +262,156 @@ module Textrepo
134
262
  File.basename(text_path).delete_suffix(".#{@extname}")
135
263
  end
136
264
 
137
- def exist?(timestamp)
138
- FileTest.exist?(abspath(timestamp))
139
- end
140
-
141
265
  def find_entries(stamp_pattern)
142
266
  Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
143
- timestamp_str(e)
267
+ begin
268
+ Timestamp.parse_s(timestamp_str(e))
269
+ rescue InvalidTimestampStringError => _
270
+ # Just ignore the erroneous entry, since it is not a text in
271
+ # the repository. It may be a garbage, or some kind of
272
+ # hidden stuff of the repository, ... etc.
273
+ nil
274
+ end
275
+ }.compact
276
+ end
277
+
278
+ ##
279
+ # The upper limit of files to search at one time. The value has
280
+ # no reason to select. It seems to me that not too much, not too
281
+ # little to handle in one process to search.
282
+
283
+ LIMIT_OF_FILES = 20
284
+
285
+ ##
286
+ # When no timestamp pattern was given, invoke the searcher with
287
+ # the repository root path as its argument and the recursive
288
+ # searching option. The search could be done in only one process.
289
+
290
+ def invoke_searcher_at_repo_root(searcher, pattern)
291
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
292
+ pattern, @path)
293
+ output = []
294
+ output += o.lines.map(&:chomp) if s.success? && (! o.empty?)
295
+ output
296
+ end
297
+
298
+ ##
299
+ # When a timestamp pattern was given, at first, list target files,
300
+ # then invoke the searcher for those files. Since the number of
301
+ # target files may be so much, it seems to be dangerous to pass
302
+ # all of them to a single search process at one time.
303
+ #
304
+ # One more thing to mention, the searcher, like `grep`, does not
305
+ # add the filename at the beginning of the search result line, if
306
+ # the target is one file. This behavior is not suitable in this
307
+ # purpose. The code below adds the filename when the target is
308
+ # one file.
309
+
310
+ def invoke_searcher_for_entries(searcher, pattern, entries)
311
+ output = []
312
+
313
+ num_of_entries = entries.size
314
+ if num_of_entries == 1
315
+ # If the search taget is one file, the output needs special
316
+ # treatment.
317
+ file = abspath(entries[0])
318
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
319
+ pattern, file)
320
+ if s.success? && (! o.empty)
321
+ output += o.lines.map { |line|
322
+ # add filename at the beginning of the search result line
323
+ [file, line.chomp].join(":")
324
+ }
325
+ end
326
+ elsif num_of_entries > LIMIT_OF_FILES
327
+ output += invoke_searcher_for_entries(searcher, pattern, entries[0..(LIMIT_OF_FILES - 1)])
328
+ output += invoke_searcher_for_entries(searcher, pattern, entries[LIMIT_OF_FILES..-1])
329
+ else
330
+ # When the number of target is less than the upper limit,
331
+ # invoke the searcher with all of target files as its
332
+ # arguments.
333
+ files = find_files(entries)
334
+ o, s = Open3.capture2(searcher, *find_searcher_options(searcher),
335
+ pattern, *files)
336
+ if s.success? && (! o.empty)
337
+ output += o.lines.map(&:chomp)
338
+ end
339
+ end
340
+
341
+ output
342
+ end
343
+
344
+ SEARCHER_OPTS = {
345
+ # case insensitive, print line number, recursive search, work as egrep
346
+ "grep" => ["-i", "-n", "-R", "-E"],
347
+ # case insensitive, print line number, recursive search
348
+ "egrep" => ["-i", "-n", "-R"],
349
+ # case insensitive, print line number, recursive search, work as gegrep
350
+ "ggrep" => ["-i", "-n", "-R", "-E"],
351
+ # case insensitive, print line number, recursive search
352
+ "gegrep" => ["-i", "-n", "-R"],
353
+ # smart case, print line number, no color
354
+ "rg" => ["-S", "-n", "--no-heading", "--color", "never"],
355
+ }
356
+
357
+ def find_searcher_options(searcher)
358
+ @searcher_options || SEARCHER_OPTS[File.basename(searcher)] || ""
359
+ end
360
+
361
+ def find_files(timestamps)
362
+ timestamps.map{|stamp| abspath(stamp)}
363
+ end
364
+
365
+ ##
366
+ # The argument must be an Array contains the searcher output.
367
+ # Each item is constructed from 3 parts:
368
+ # "<pathname>:<integer>:<text>"
369
+ #
370
+ # For example, it may looks like:
371
+ #
372
+ # "/somewhere/2020/11/20201101044300.md:18:foo is foo"
373
+ #
374
+ # Or it may contains more ":" in the text part as:
375
+ #
376
+ # "/somewhere/2020/11/20201101044500.md:119:apple:orange:grape"
377
+ #
378
+ # In the latter case, `split(":")` will split it too much. That is,
379
+ # the result will be:
380
+ #
381
+ # ["/somewhere/2020/11/20201101044500.md", "119", "apple", "orange", "grape"]
382
+ #
383
+ # Text part must be joined with ":".
384
+
385
+ def construct_search_result(output)
386
+ output.map { |line|
387
+ begin
388
+ pathname, num, *match_text = line.split(":")
389
+ [Timestamp.parse_s(timestamp_str(pathname)),
390
+ num.to_i,
391
+ match_text.join(":")]
392
+ rescue InvalidTimestampStringError, TypeError => _
393
+ raise InvalidSearchResultError, [@searcher, @searcher_options.join(" ")].join(" ")
394
+ end
395
+ }.compact
396
+ end
397
+
398
+ def find_searcher(program = nil)
399
+ candidates = [FAVORITE_SEARCHER]
400
+ candidates.unshift(program) unless program.nil? || candidates.include?(program)
401
+ search_paths = ENV["PATH"].split(":")
402
+ candidates.map { |prog|
403
+ find_in_paths(prog, search_paths)
404
+ }[0]
405
+ end
406
+
407
+ def find_in_paths(prog, paths)
408
+ paths.each { |p|
409
+ abspath = File.expand_path(prog, p)
410
+ return abspath if FileTest.exist?(abspath) && FileTest.executable?(abspath)
144
411
  }
412
+ nil
145
413
  end
414
+ # :startdoc:
146
415
 
147
416
  end
148
417
  end
@@ -1,31 +1,72 @@
1
1
  module Textrepo
2
2
  class Repository
3
- attr_reader :type, :name
3
+
4
+ ##
5
+ # Repository type. It specifies which concrete repository class
6
+ # will instantiated. For example, the type `:file_system` specifies
7
+ # `FileSystemRepository`.
8
+
9
+ attr_reader :type
10
+
11
+ ##
12
+ # Repository name. The usage of the value of `name` depends on a
13
+ # concrete repository class. For example, `FileSystemRepository`
14
+ # uses it as a part of the repository path.
15
+
16
+ attr_reader :name
17
+
18
+ ##
19
+ # Create a new repository. The argument must be an object which
20
+ # can be accessed like a Hash object.
4
21
 
5
22
  def initialize(conf)
6
23
  @type = conf[:repository_type]
7
24
  @name = conf[:repository_name]
8
25
  end
9
26
 
27
+ ##
10
28
  # Stores text data into the repository with the specified timestamp.
11
29
  # Returns the timestamp.
30
+ #
31
+ # :call-seq:
32
+ # create(Timestamp, Array) -> Timestamp
33
+
12
34
  def create(timestamp, text); timestamp; end
13
35
 
36
+ ##
14
37
  # Reads text data from the repository, which is associated to the
15
38
  # timestamp. Returns an array which contains the text.
39
+ #
40
+ # :call-seq:
41
+ # read(Timestamp) -> Array
42
+
16
43
  def read(timestamp); []; end
17
44
 
45
+ ##
18
46
  # Updates the content with text in the repository, which is
19
47
  # associated to the timestamp. Returns the timestamp.
48
+ #
49
+ # :call-seq:
50
+ # update(Timestamp, Array) -> Timestamp
51
+
20
52
  def update(timestamp, text); timestamp; end
21
53
 
54
+ ##
22
55
  # Deletes the content in the repository, which is associated to
23
56
  # the timestamp. Returns an array which contains the deleted text.
57
+ #
58
+ # :call-seq:
59
+ # delete(Timestamp) -> Array
60
+
24
61
  def delete(timestamp); []; end
25
62
 
63
+ ##
26
64
  # Finds all entries of text those have timestamps which mathes the
27
65
  # specified pattern of timestamp. Returns an array which contains
28
- # timestamps. A pattern must be one of the following:
66
+ # instances of Timestamp. If none of text was found, an empty
67
+ # array would be returned.
68
+ #
69
+ # A pattern must be one of the following:
29
70
  #
30
71
  # - yyyymoddhhmiss_lll : whole stamp
31
72
  # - yyyymoddhhmiss : omit millisecond part
@@ -37,17 +78,50 @@ module Textrepo
37
78
  # If `stamp_pattern` is omitted, the recent entries will be listed.
38
79
  # Then, how many entries are listed depends on the implementaiton
39
80
  # of the concrete repository class.
81
+ #
82
+ # :call-seq:
83
+ # entries(String) -> Array of Timestamp instances
84
+
40
85
  def entries(stamp_pattern = nil); []; end
41
86
 
87
+ ##
88
+ # Check the existence of text which is associated with the given
89
+ # timestamp.
90
+ #
91
+ # :call-seq:
92
+ # exist?(Timestamp) -> true or false
93
+
94
+ def exist?(timestamp); false; end
95
+
96
+ ##
97
+ # Searches a pattern (word or regular expression) in text those
98
+ # matches to a given timestamp pattern. Returns an Array of
99
+ # search results. If no match, returns an empty Array.
100
+ #
101
+ # See the document for Repository#entries about a timestamp
102
+ # pattern. When nil is passed as a timestamp pattern, searching
103
+ # applies to all text in the repository.
104
+ #
105
+ # Each entry of the result Array is constructed from 3 items, (1)
106
+ # timestamp (Timestamp), (2) line number (Integer), (3) matched
107
+ # line (String).
108
+ #
109
+ # :call-seq:
110
+ # search(String for pattern, String for Timestamp pattern) -> Array
111
+
112
+ def search(pattern, stamp_pattern = nil); []; end
113
+
42
114
  end
43
115
 
44
116
  require_relative 'file_system_repository'
45
117
 
118
+ ##
46
119
  # Returns an instance which derived from Textrepo::Repository class.
47
- # `conf` must be a Hash object which has a value of
48
- # `:repository_type` and `:repository_name` at least. Some concrete
49
- # class derived from Textrepo::Repository may require more key-value
50
- # pairs in `conf`.
120
+ # `conf` must be an object which can be accessed like a Hash object.
121
+ # And it must also has a value of `:repository_type` and
122
+ # `:repository_name` at least. Some concrete class derived from
123
+ # Textrepo::Repository may require more key-value pairs in `conf`.
124
+
51
125
  def init(conf)
52
126
  type = conf[:repository_type]
53
127
  klass_name = type.to_s.split(/_/).map(&:capitalize).join + "Repository"
@@ -1,17 +1,47 @@
1
1
  module Textrepo
2
+ ##
3
+ # Timestamp is generated from a Time object. It converts a time to
4
+ # string in the obvious format, such "20201023122400".
5
+ #
6
+ # Since the obvious format contains only year, month, day, hour,
7
+ # minute, and second, the resolution of time is a second. That is,
8
+ # two Time object those are different only in second will generates
9
+ # equal Timestamp objects.
10
+ #
11
+ # If a client program of Textrepo::Timestamp wants to distinguish
12
+ # those Time objects, an attribute `suffix` could be used.
13
+ #
14
+ # For example, the `suffix` will be converted into a 3 character
15
+ # string, such "012", "345", "678", ... etc. So, millisecond part
16
+ # of a Time object will be suitable to pass as `suffix` when
17
+ # creating a Timestamp object.
18
+
2
19
  class Timestamp
3
20
  include Comparable
4
21
 
5
- attr_reader :time, :suffix
22
+ ##
23
+ # Time object which generates the Timestamp object.
24
+
25
+ attr_reader :time
26
+
27
+ ##
28
+ # An integer specified in `new` method to create the Timestamp object.
29
+
30
+ attr_reader :suffix
31
+
32
+ ##
33
+ # Creates a Timestamp object from a Time object. In addition, an
34
+ # Integer can be passed as a suffix use.
35
+ #
36
+ # :call-seq:
37
+ # new(Time, Integer = nil) -> Timestamp
6
38
 
7
- # time: a Time instance
8
- # suffix: an Integer instance
9
39
  def initialize(time, suffix = nil)
10
40
  @time = time
11
41
  @suffix = suffix
12
42
  end
13
43
 
14
- def <=>(other)
44
+ def <=>(other) # :nodoc:
15
45
  result = (self.time <=> other.time)
16
46
 
17
47
  sfx = self.suffix || 0
@@ -20,44 +50,58 @@ module Textrepo
20
50
  result == 0 ? (sfx <=> osfx) : result
21
51
  end
22
52
 
23
- # %Y %m %d %H %M %S suffix
24
- # "2020-12-30 12:34:56 (0 | nil)" => "20201230123456"
25
- # "2020-12-30 12:34:56 (7)" => "20201230123456_007"
53
+ ##
54
+ # Generate an obvious time string.
55
+ #
56
+ # %Y %m %d %H %M %S suffix
57
+ # "2020-12-30 12:34:56 (0 | nil)" -> "20201230123456"
58
+ # "2020-12-30 12:34:56 (7)" -> "20201230123456_007"
59
+
26
60
  def to_s
27
61
  s = @time.strftime("%Y%m%d%H%M%S")
28
62
  s += "_#{"%03u" % @suffix}" unless @suffix.nil? || @suffix == 0
29
63
  s
30
64
  end
31
65
 
32
- # %Y %m %d %H %M %S suffix %Y/%m/ %Y%m%d%H%M%S %L
33
- # "2020-12-30 12:34:56 (0 | nil)" => "2020/12/20201230123456"
34
- # "2020-12-30 12:34:56 (7)" => "2020/12/20201230123456_007"
35
- def to_pathname
36
- @time.strftime("%Y/%m/") + self.to_s
37
- end
38
-
39
66
  class << self
40
- # yyyymoddhhmiss sfx yyyy mo dd hh mi ss sfx
41
- # "20201230123456" => "2020", "12", "30", "12", "34", "56"
42
- # "20201230123456_789" => "2020", "12", "30", "12", "34", "56", "789"
67
+ ##
68
+ # Splits a string which represents a timestamp into components.
69
+ # Each component represents a part of constructs to instantiate
70
+ # a Time object.
71
+ #
72
+ # yyyymoddhhmiss sfx yyyy mo dd hh mi ss sfx
73
+ # "20201230123456" -> "2020", "12", "30", "12", "34", "56"
74
+ # "20201230123456_789" -> "2020", "12", "30", "12", "34", "56", "789"
75
+ #
76
+ # Raises InvalidTimestampStringError if nil was passed as an arguemnt.
77
+
43
78
  def split_stamp(stamp_str)
79
+ raise InvalidTimestampStringError, stamp_str if stamp_str.nil?
44
80
  # yyyy mo dd hh mi ss sfx
45
81
  a = [0..3, 4..5, 6..7, 8..9, 10..11, 12..13, 15..17].map {|r| stamp_str[r]}
46
82
  a[-1].nil? ? a[0..-2] : a
47
83
  end
48
84
 
85
+ ##
86
+ # Generate a Timestamp object from a string which represents a
87
+ # timestamp, such "20201028163400".
88
+ #
89
+ # Raises InvalidTimestampStringError if cannot convert the
90
+ # argument into a Timestamp object.
91
+ #
92
+ # :call-seq:
93
+ # parse_s("20201028163400") -> Timestamp
94
+ # parse_s("20201028163529_034") -> Timestamp
95
+
49
96
  def parse_s(stamp_str)
50
- year, mon, day, hour, min, sec , sfx = split_stamp(stamp_str).map(&:to_i)
51
- Timestamp.new(Time.new(year, mon, day, hour, min, sec), sfx)
97
+ begin
98
+ ye, mo, da, ho, mi, se, sfx = split_stamp(stamp_str).map(&:to_i)
99
+ Timestamp.new(Time.new(ye, mo, da, ho, mi, se), sfx)
100
+ rescue InvalidTimestampStringError, ArgumentError => _
101
+ raise InvalidTimestampStringError, stamp_str
102
+ end
52
103
  end
53
104
 
54
- # (-2)
55
- # 0 8 |(-1)
56
- # V V VV
57
- # "2020/12/20201230123456" => "2020-12-30 12:34:56"
58
- def parse_pathname(pathname)
59
- parse_s(pathname[8..-1])
60
- end
61
105
  end
62
106
  end
63
107
  end
@@ -1,3 +1,3 @@
1
1
  module Textrepo
2
- VERSION = '0.4.1'
2
+ VERSION = '0.5.0'
3
3
  end
@@ -7,7 +7,7 @@ Gem::Specification.new do |spec|
7
7
  spec.email = ["mnbi@users.noreply.github.com"]
8
8
 
9
9
  spec.summary = %q{A repository to store text with timestamp.}
10
- spec.description = %q{Textrepo is a repository to store text with timestamp. It can manage text with the attached timestamp (read/update/delte).}
10
+ spec.description = %q{Textrepo is a repository to store text with timestamp. It can manage text with the attached timestamp (create/read/update/delete).}
11
11
  spec.homepage = "https://github.com/mnbi/textrepo"
12
12
  spec.license = "MIT"
13
13
  spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: textrepo
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.1
4
+ version: 0.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - mnbi
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2020-10-15 00:00:00.000000000 Z
11
+ date: 2020-11-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -25,7 +25,7 @@ dependencies:
25
25
  - !ruby/object:Gem::Version
26
26
  version: '2.1'
27
27
  description: Textrepo is a repository to store text with timestamp. It can manage
28
- text with the attached timestamp (read/update/delte).
28
+ text with the attached timestamp (create/read/update/delete).
29
29
  email:
30
30
  - mnbi@users.noreply.github.com
31
31
  executables: []
@@ -36,7 +36,6 @@ files:
36
36
  - ".travis.yml"
37
37
  - CHANGELOG.md
38
38
  - Gemfile
39
- - Gemfile.lock
40
39
  - LICENSE.txt
41
40
  - README.md
42
41
  - Rakefile
@@ -1,22 +0,0 @@
1
- PATH
2
- remote: .
3
- specs:
4
- textrepo (0.4.1)
5
-
6
- GEM
7
- remote: https://rubygems.org/
8
- specs:
9
- minitest (5.14.2)
10
- rake (13.0.1)
11
-
12
- PLATFORMS
13
- ruby
14
-
15
- DEPENDENCIES
16
- bundler (~> 2.1)
17
- minitest (~> 5.0)
18
- rake (~> 13.0)
19
- textrepo!
20
-
21
- BUNDLED WITH
22
- 2.1.4