textrepo 0.4.1 → 0.5.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
  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