textrepo 0.4.1

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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 4d26af6dd86e8b79c9e50300f8c31102bf79d5c751e175740a7225c2ca4a9b33
4
+ data.tar.gz: 8d398c0848c0a4b06d2a775f3cb6bf1f49489be7f5ba5d8cf7eab2aad068b5c7
5
+ SHA512:
6
+ metadata.gz: ef79855cd99ebc3baea4368c00cdc4d439478cfc9733d45eee1e7a08ca005bf0dc15824cb57b46326c982e724acc762b25b722c112f19ae4f67349d575fffdcb
7
+ data.tar.gz: 4af657999971e6d3cef41dc5d3ef7ea1017a536b772372d38e79ffb91196c239585a0c932fe10e42f9d7ccbfee8fa477ec31bd97afa9cd4e1c31f0c894772110
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
@@ -0,0 +1,6 @@
1
+ ---
2
+ language: ruby
3
+ cache: bundler
4
+ rvm:
5
+ - 2.7.2
6
+ before_install: gem install bundler -v 2.1.4
@@ -0,0 +1,40 @@
1
+ # Change Log
2
+ All notable changes to this project will be documented in this file.
3
+
4
+ The format is based on [Keep a Changelog](https://keepachangelog.com/)
5
+ and this project adheres to [Semantic Versioning](https://semver.org/).
6
+
7
+ ## [Unreleased]
8
+ Nothing to record here.
9
+
10
+ ## [0.4.0] - 2020-10-14
11
+ ### Added
12
+ - Released to rubygems.org.
13
+
14
+ ### Changed
15
+ - Rename the method, `Repository#notes` to `entries`.
16
+ - Modify the instruction to install
17
+
18
+ ## [0.3.0] - 2020-10-11
19
+ ### Added
20
+ - Go public onto GitHub.
21
+ - Add an example (`rbnotes`) to demonstrate how to use `textrepo`.
22
+
23
+ ### Changed
24
+ - Modify not to handle fraction of time in Timestamp.
25
+ - Instead, Timestamp can have a suffix to distinguish 2 stamps those
26
+ represent the same time.
27
+
28
+ ## [0.2.0] - 2020-09-28
29
+ ### Changed
30
+ - Modify to handle millisecond in Timestamp
31
+
32
+ ## [0.1.0] - 2020-09-23
33
+ ### Added
34
+ - Add some target in Rakefile to run test easily.
35
+ - Add error classes
36
+ - Add Timestamp class, it will be an identifier of each text in the repo.
37
+ - Add Repository class (an abstract base class for concrete repository
38
+ implementations)
39
+ - Implement API for FileSystemRepository class (create/read/update/delete).
40
+ - Add tests.
data/Gemfile ADDED
@@ -0,0 +1,7 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in textrepo.gemspec
4
+ gemspec
5
+
6
+ gem "rake", "~> 13.0"
7
+ gem "minitest", "~> 5.0"
@@ -0,0 +1,22 @@
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
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2020 mnbi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
@@ -0,0 +1,77 @@
1
+ # Textrepo
2
+
3
+ [![Build Status](https://travis-ci.org/mnbi/textrepo.svg?branch=main)](https://travis-ci.org/mnbi/textrepo)
4
+
5
+ Textrepo is a repository to store a note with a timestamp. Each note
6
+ in the repository operates with the associated timestamp.
7
+
8
+ ## Installation
9
+
10
+ Add this line to your application's Gemfile:
11
+
12
+ ```ruby
13
+ gem 'textrepo'
14
+ ```
15
+
16
+ And then execute:
17
+
18
+ $ bundle install
19
+
20
+ Or install it yourself as:
21
+
22
+ $ gem install textrepo
23
+
24
+ ## Usage
25
+
26
+ Here is a very short sample to use `textrepo`. It will make
27
+ `~/textrepo_sample` directory and store some text into it.
28
+
29
+ ``` ruby
30
+ #!/usr/bin/env ruby
31
+
32
+ require "textrepo"
33
+
34
+ conf = {
35
+ :repository_type => :file_system,
36
+ :repository_name => "textrepo_sample",
37
+ :repository_base => File.expand_path("~"),
38
+ }
39
+
40
+ repo = Textrepo.init(conf)
41
+
42
+ t0 = Time.now
43
+
44
+ stamps = []
45
+ stamps << repo.create(Textrepo::Timestamp.new(t0), ["jan", "feb", "mar"])
46
+ stamps << repo.create(Textrepo::Timestamp.new(t0, 1), ["apr", "may", "jun"])
47
+ stamps << repo.create(Textrepo::Timestamp.new(t0, 2), ["jul", "aug", "sep"])
48
+ stamps << repo.create(Textrepo::Timestamp.new(t0, 3), ["oct", "nov", "dec"])
49
+
50
+ entries = repo.notes
51
+ puts entries
52
+
53
+ stamps.each { |stamp|
54
+ text = repo.read(stamp)
55
+ puts "----"
56
+ puts stamp
57
+ puts text
58
+ }
59
+ ```
60
+
61
+ Also see `examples` directory. There is a small tool to demonstrate
62
+ how to use `textrepo`.
63
+
64
+ ## Development
65
+
66
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
67
+
68
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
69
+
70
+ ## Contributing
71
+
72
+ Bug reports and pull requests are welcome on GitHub at https://github.com/mnbi/textrepo.
73
+
74
+
75
+ ## License
76
+
77
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,37 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
11
+
12
+ desc 'Setup test data'
13
+ task :setup_test do
14
+ print "Setting up to execute tests..."
15
+ load('test/fixtures/setup_test_repo.rb', true)
16
+ puts "done."
17
+ end
18
+
19
+ task test: [:setup_test, :clean_sandbox]
20
+
21
+ require "fileutils"
22
+
23
+ desc "Clean up test/sandbox"
24
+ task :clean_sandbox do
25
+ sandbox = File.expand_path('test/sandbox', __dir__)
26
+ entries = Dir.entries(sandbox).filter_map { |e|
27
+ File.expand_path(e, sandbox) unless e == '.' or e == '..'
28
+ }
29
+ entries.each { |e|
30
+ next if File.basename(e) == '.gitkeep'
31
+ FileUtils.remove_entry_secure(e) if FileTest.exist?(e)
32
+ }
33
+ end
34
+
35
+ task :clobber => :clean_sandbox
36
+ CLOBBER << 'test/fixtures/notes'
37
+ CLOBBER << 'test/fixtures/test_repo'
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "textrepo"
5
+
6
+ # You can add fixtures and/or initialization code here to make experimenting
7
+ # with your gem easier. You can also use a different console, if you like.
8
+
9
+ # (If you use this, don't forget to add pry to your Gemfile!)
10
+ # require "pry"
11
+ # Pry.start
12
+
13
+ require "irb"
14
+ IRB.start(__FILE__)
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,8 @@
1
+ require "textrepo"
2
+
3
+ module Rbnotes
4
+ class Error < StandardError; end
5
+
6
+ require_relative "rbnotes/version"
7
+ require_relative "rbnotes/commands"
8
+ end
@@ -0,0 +1,118 @@
1
+ module Rbnotes
2
+ module Commands
3
+ class Command
4
+ def execute(args, conf)
5
+ Builtins::DEFAULT_CMD.new.execute(args, conf)
6
+ end
7
+ end
8
+
9
+ # Built-in commands:
10
+ # - repo: prints the absolute path of the repository.
11
+ # - conf: prints all of the current configuration settings.
12
+ # - stamp: converts given TIME_STR into a timestamp.
13
+ # - time: converts given STAMP into a time string.
14
+ module Builtins
15
+ class Help < Command
16
+ def execute(_, _)
17
+ puts <<USAGE
18
+ usage: rbnotes [command] [args]
19
+
20
+ command:
21
+ import FILE : import a FILE into the repository
22
+ list NUM : list NUM notes
23
+ show STAMP : show the note specified with STAMP
24
+
25
+ repo : print the repository path
26
+ stamp TIME_STR : convert TIME_STR into a timestamp
27
+ time STAMP : convert STAMP into a time string
28
+ version : print version
29
+ help : show help
30
+ USAGE
31
+ end
32
+ end
33
+
34
+ class Version < Command
35
+ def execute(_, _)
36
+ rbnotes_version = "rbnotes #{Rbnotes::VERSION} (#{Rbnotes::RELEASE})"
37
+ textrepo_version = "textrepo #{Textrepo::VERSION}"
38
+ puts "#{rbnotes_version} (#{textrepo_version})"
39
+ end
40
+ end
41
+
42
+ class Repo < Command
43
+ def execute(_, conf)
44
+ name = conf[:repository_name]
45
+ base = conf[:repository_base]
46
+ type = conf[:repository_type]
47
+
48
+ puts case type
49
+ when :file_system
50
+ "#{base}/#{name}"
51
+ else
52
+ "#{base}/#{name}"
53
+ end
54
+ end
55
+ end
56
+
57
+ class Conf < Command
58
+ def execute(_, conf)
59
+ conf.keys.sort.each { |k|
60
+ puts "#{k}=#{conf[k]}"
61
+ }
62
+ end
63
+ end
64
+
65
+ require "time"
66
+
67
+ class Stamp < Command
68
+ def execute(args, _)
69
+ time_str = args.shift
70
+ unless time_str.nil?
71
+ puts Textrepo::Timestamp.new(::Time.parse(time_str)).to_s
72
+ else
73
+ puts "not specified TIME_STR"
74
+ super
75
+ end
76
+ end
77
+ end
78
+
79
+ class Time < Command
80
+ def execute(args, _)
81
+ stamp = args.shift
82
+ unless stamp.nil?
83
+ puts ::Time.new(*Textrepo::Timestamp.split_stamp(stamp).map(&:to_i)).to_s
84
+ else
85
+ puts "not specified STAMP"
86
+ super
87
+ end
88
+ end
89
+ end
90
+
91
+ DEFAULT_CMD = Help
92
+ end
93
+
94
+ DEFAULT_CMD_NAME = "help"
95
+
96
+ class << self
97
+ def load(cmd_name)
98
+ cmd_name ||= DEFAULT_CMD_NAME
99
+ klass_name = cmd_name.capitalize
100
+
101
+ klass = nil
102
+ if Builtins.const_defined?(klass_name, false)
103
+ klass = Builtins::const_get(klass_name, false)
104
+ else
105
+ begin
106
+ require_relative "commands/#{cmd_name}"
107
+ klass = const_get(klass_name, false)
108
+ rescue LoadError => _
109
+ STDERR.puts "unknown command: #{cmd_name}"
110
+ klass = Builtins::DEFAULT_CMD
111
+ end
112
+ end
113
+ klass.new
114
+ end
115
+ end
116
+
117
+ end
118
+ end
@@ -0,0 +1,62 @@
1
+ module Rbnotes
2
+ class Commands::Import < Commands::Command
3
+ def execute(args, conf)
4
+ file = args.shift
5
+ unless file.nil?
6
+ st = File::Stat.new(file)
7
+ btime = st.respond_to?(:birthtime) ? st.birthtime : st.mtime
8
+ stamp = Textrepo::Timestamp.new(btime)
9
+ puts "Import [%s] (timestamp [%s]) ..." % [file, stamp]
10
+
11
+ repo = Textrepo.init(conf)
12
+ content = nil
13
+ File.open(file, "r") {|f| content = f.readlines(chomp: true)}
14
+
15
+ count = 0
16
+ while count <= 999
17
+ begin
18
+ repo.create(stamp, content)
19
+ break # success to create a note
20
+ rescue Textrepo::DuplicateTimestampError => e
21
+ puts "A text with the timestamp [%s] has been already exists" \
22
+ " in the repository." % stamp
23
+
24
+ repo_text = repo.read(stamp)
25
+ if content == repo_text
26
+ # if the content is the same to the target file,
27
+ # the specified file has been already imported.
28
+ # Then, there is nothing to do. Just exit.
29
+ puts "The note [%s] in the repository exactly matches" \
30
+ " the specified file." % stamp
31
+ puts "It seems there is no need to import the file [%s]." % file
32
+ exit # normal end
33
+ else
34
+ puts "The text in the repository does not match the" \
35
+ " specified file."
36
+ count += 1
37
+ stamp = Textrepo::Timestamp.new(stamp.time, count)
38
+ puts "Try to create a note again with a new " \
39
+ "timestamp [%s]." % stamp
40
+ end
41
+ rescue Textrepo::EmptyTextError => e
42
+ puts "... aborted."
43
+ puts "The specified file is empyt."
44
+ exit 1 # error
45
+ end
46
+ end
47
+ if count > 999
48
+ puts "Cannot create a text into the repository with the" \
49
+ " specified file [%s]." % file
50
+ puts "For, the birthtime [%s] is identical to some notes" \
51
+ " already exists in the reopsitory." % btime
52
+ puts "Change the birthtime of the target file, then retry."
53
+ else
54
+ puts "... Done."
55
+ end
56
+ else
57
+ puts "not supecified FILE"
58
+ super
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,60 @@
1
+ require "io/console/size"
2
+
3
+ module Rbnotes
4
+ class Commands::List < Commands::Command
5
+ def execute(args, conf)
6
+ @row, @column = IO.console_size
7
+ max = (args.shift || @row - 3).to_i
8
+
9
+ @repo = Textrepo.init(conf)
10
+ notes = @repo.entries.sort{|a, b| b <=> a}
11
+ notes[0, max].each { |timestamp_str|
12
+ puts make_headline(timestamp_str)
13
+ }
14
+ end
15
+
16
+ private
17
+ TIMESTAMP_STR_MAX_WIDTH = "yyyymoddhhmiss_sfx".size
18
+ # Makes a headline with the timestamp and subject of the notes, it
19
+ # looks like as follows:
20
+ #
21
+ # |<------------------ console column size --------------------->|
22
+ # +-- timestamp ---+ +--- subject (the 1st line of each note) --+
23
+ # | | | |
24
+ # 20101010001000_123: # I love Macintosh. [EOL]
25
+ # 20100909090909_999: # This is very very long long loooong subje[EOL]
26
+ # ++
27
+ # ^--- delimiter (2 characters)
28
+ #
29
+ # The subject part will truncate when it is long.
30
+ def make_headline(timestamp_str)
31
+
32
+ delimiter = ": "
33
+ subject_width = @column - TIMESTAMP_STR_MAX_WIDTH - delimiter.size - 1
34
+
35
+ subject = @repo.read(Textrepo::Timestamp.parse_s(timestamp_str))[0]
36
+ prefix = '# '
37
+ subject = prefix + subject.lstrip if subject[0, 2] != prefix
38
+
39
+ ts_part = "#{timestamp_str} "[0..(TIMESTAMP_STR_MAX_WIDTH - 1)]
40
+ sj_part = truncate_str(subject, subject_width)
41
+
42
+ ts_part + delimiter + sj_part
43
+ end
44
+
45
+ def truncate_str(str, size)
46
+ count = 0
47
+ result = ""
48
+ str.each_char { |c|
49
+ # TODO: fix
50
+ # This code is ugly. It assumes that each non-ascii character
51
+ # always occupy the width of 2 ascii characters in a terminal.
52
+ # I am not sure the assumption is appropriate or not.
53
+ count += c.ascii_only? ? 1 : 2
54
+ break if count > size
55
+ result << c
56
+ }
57
+ result
58
+ end
59
+ end
60
+ end
@@ -0,0 +1,25 @@
1
+ module Rbnotes
2
+ class Commands::Show < Commands::Command
3
+ def execute(args, conf)
4
+ stamp_str = args.shift
5
+ unless stamp_str.nil?
6
+ repo = Textrepo.init(conf)
7
+ stamp = Textrepo::Timestamp.parse_s(stamp_str)
8
+ content = repo.read(stamp)
9
+
10
+ pager = conf[:pager]
11
+ unless pager.nil?
12
+ require 'open3'
13
+ Open3.pipeline_w(pager) { |stdin|
14
+ stdin.puts content
15
+ stdin.close
16
+ }
17
+ else
18
+ puts content
19
+ end
20
+ else
21
+ super
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,4 @@
1
+ module Rbnotes
2
+ VERSION = '0.1.0'
3
+ RELEASE = '2020-10-02'
4
+ end
@@ -0,0 +1,33 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
4
+ $LOAD_PATH.unshift File.expand_path("lib", __dir__)
5
+
6
+ require "rbnotes"
7
+
8
+ DEBUG = true
9
+
10
+ module Rbnotes
11
+ class App
12
+ def initialize
13
+ @conf = default_conf
14
+ end
15
+
16
+ def default_conf
17
+ {
18
+ :repository_type => :file_system,
19
+ :repository_name => DEBUG ? "sandbox/notes_dev" : "notes",
20
+ :repository_base => File.expand_path("~"),
21
+ :pager => "bat",
22
+ }
23
+ end
24
+
25
+ def run(args)
26
+ cmd = args.shift
27
+ Rbnotes::Commands.load(cmd).execute(args, @conf)
28
+ end
29
+ end
30
+ end
31
+
32
+ app = Rbnotes::App.new
33
+ app.run(ARGV)
@@ -0,0 +1,6 @@
1
+ module Textrepo
2
+ require_relative 'textrepo/version'
3
+ require_relative 'textrepo/error'
4
+ require_relative 'textrepo/timestamp'
5
+ require_relative 'textrepo/repository'
6
+ end
@@ -0,0 +1,50 @@
1
+ module Textrepo
2
+ class Error < StandardError; end
3
+
4
+ module ErrMsg
5
+ UNKNOWN_REPO_TYPE = 'unknown type for repository: %s'
6
+ DUPLICATE_TIMESTAMP = 'duplicate timestamp: %s'
7
+ EMPTY_TEXT = 'empty text'
8
+ MISSING_TIMESTAMP = 'missing timestamp: %s'
9
+ end
10
+
11
+ class UnknownRepoTypeError < Error
12
+ def initialize(type)
13
+ super(ErrMsg::UNKNOWN_REPO_TYPE % type)
14
+ end
15
+ end
16
+
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
+ # +--------------------------+---------------------+
31
+
32
+ class DuplicateTimestampError < Error
33
+ def initialize(timestamp)
34
+ super(ErrMsg::DUPLICATE_TIMESTAMP % timestamp)
35
+ end
36
+ end
37
+
38
+ class EmptyTextError < Error
39
+ def initialize
40
+ super(ErrMsg::EMPTY_TEXT)
41
+ end
42
+ end
43
+
44
+ class MissingTimestampError < Error
45
+ def initialize(timestamp)
46
+ super(ErrMsg::MISSING_TIMESTAMP % timestamp)
47
+ end
48
+ end
49
+
50
+ end
@@ -0,0 +1,148 @@
1
+ require 'fileutils'
2
+
3
+ module Textrepo
4
+ # A concrete repository which uses the default file system as a storage.
5
+ class FileSystemRepository < Repository
6
+ attr_reader :path, :extname
7
+
8
+ FAVORITE_REPOSITORY_NAME = 'notes'
9
+ FAVORITE_EXTNAME = 'md'
10
+
11
+ # `conf` must be a Hash object. It must hold the follwoing
12
+ # values:
13
+ #
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
18
+ #
19
+ # The root path of the repository looks like the following:
20
+ # - conf[:repository_base]/conf[:repository_name]
21
+ #
22
+ # Default values are set when `repository_name` and `default_extname`
23
+ # were not defined in `conf`.
24
+ def initialize(conf)
25
+ super
26
+ base = conf[:repository_base]
27
+ @name ||= FAVORITE_REPOSITORY_NAME
28
+ @path = File.expand_path("#{name}", base)
29
+ FileUtils.mkdir_p(@path)
30
+ @extname = conf[:default_extname] || FAVORITE_EXTNAME
31
+ end
32
+
33
+ #
34
+ # repository operations
35
+ #
36
+
37
+ # Creates a file into the repository, which contains the specified
38
+ # text and is associated to the timestamp.
39
+ def create(timestamp, text)
40
+ abs = abspath(timestamp)
41
+ raise DuplicateTimestampError, timestamp if FileTest.exist?(abs)
42
+ raise EmptyTextError if text.nil? || text.empty?
43
+
44
+ write_text(abs, text)
45
+ timestamp
46
+ end
47
+
48
+ # Reads the file content in the repository. Then, returns its
49
+ # content.
50
+ def read(timestamp)
51
+ abs = abspath(timestamp)
52
+ raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
53
+ content = nil
54
+ File.open(abs, 'r') { |f|
55
+ content = f.readlines(chomp: true)
56
+ }
57
+ content
58
+ end
59
+
60
+ # Updates the file content in the repository. A new timestamp
61
+ # will be attached to the text.
62
+ def update(timestamp, text)
63
+ raise EmptyTextError if text.empty?
64
+ org_abs = abspath(timestamp)
65
+ raise MissingTimestampError, timestamp unless FileTest.exist?(org_abs)
66
+
67
+ # the text must be stored with the new timestamp
68
+ new_stamp = Timestamp.new(Time.now)
69
+ new_abs = abspath(new_stamp)
70
+ write_text(new_abs, text)
71
+
72
+ # delete the original file in the repository
73
+ FileUtils.remove_file(org_abs)
74
+
75
+ new_stamp
76
+ end
77
+
78
+ # Deletes the file in the repository.
79
+ def delete(timestamp)
80
+ abs = abspath(timestamp)
81
+ raise MissingTimestampError, timestamp unless FileTest.exist?(abs)
82
+ content = read(timestamp)
83
+
84
+ FileUtils.remove_file(abs)
85
+
86
+ content
87
+ end
88
+
89
+ # Finds entries of text those timestamp matches the specified pattern.
90
+ def entries(stamp_pattern = nil)
91
+ results = []
92
+
93
+ case stamp_pattern.to_s.size
94
+ when "yyyymoddhhmiss_lll".size
95
+ stamp = Timestamp.parse_s(stamp_pattern)
96
+ if exist?(stamp)
97
+ results << stamp.to_s
98
+ end
99
+ when 0, "yyyymoddhhmiss".size, "yyyymodd".size
100
+ results += find_entries(stamp_pattern)
101
+ when 4 # "yyyy" or "modd"
102
+ pat = nil
103
+ # The following distinction is practically correct, but not
104
+ # perfect. It simply assumes that a year is greater than
105
+ # 1231. For, a year before 1232 is too old for us to create
106
+ # any text (I believe...).
107
+ if stamp_pattern.to_i > 1231
108
+ # yyyy
109
+ pat = stamp_pattern
110
+ else
111
+ # modd
112
+ pat = "*#{stamp_pattern}"
113
+ end
114
+ results += find_entries(pat)
115
+ end
116
+
117
+ results
118
+ end
119
+
120
+ private
121
+ def abspath(timestamp)
122
+ filename = timestamp.to_pathname + ".#{@extname}"
123
+ File.expand_path(filename, @path)
124
+ end
125
+
126
+ def write_text(abs, text)
127
+ FileUtils.mkdir_p(File.dirname(abs))
128
+ File.open(abs, 'w') { |f|
129
+ text.each {|line| f.puts(line) }
130
+ }
131
+ end
132
+
133
+ def timestamp_str(text_path)
134
+ File.basename(text_path).delete_suffix(".#{@extname}")
135
+ end
136
+
137
+ def exist?(timestamp)
138
+ FileTest.exist?(abspath(timestamp))
139
+ end
140
+
141
+ def find_entries(stamp_pattern)
142
+ Dir.glob("#{@path}/**/#{stamp_pattern}*.#{@extname}").map { |e|
143
+ timestamp_str(e)
144
+ }
145
+ end
146
+
147
+ end
148
+ end
@@ -0,0 +1,62 @@
1
+ module Textrepo
2
+ class Repository
3
+ attr_reader :type, :name
4
+
5
+ def initialize(conf)
6
+ @type = conf[:repository_type]
7
+ @name = conf[:repository_name]
8
+ end
9
+
10
+ # Stores text data into the repository with the specified timestamp.
11
+ # Returns the timestamp.
12
+ def create(timestamp, text); timestamp; end
13
+
14
+ # Reads text data from the repository, which is associated to the
15
+ # timestamp. Returns an array which contains the text.
16
+ def read(timestamp); []; end
17
+
18
+ # Updates the content with text in the repository, which is
19
+ # associated to the timestamp. Returns the timestamp.
20
+ def update(timestamp, text); timestamp; end
21
+
22
+ # Deletes the content in the repository, which is associated to
23
+ # the timestamp. Returns an array which contains the deleted text.
24
+ def delete(timestamp); []; end
25
+
26
+ # Finds all entries of text those have timestamps which mathes the
27
+ # specified pattern of timestamp. Returns an array which contains
28
+ # timestamps. A pattern must be one of the following:
29
+ #
30
+ # - yyyymoddhhmiss_lll : whole stamp
31
+ # - yyyymoddhhmiss : omit millisecond part
32
+ # - yyyymodd : date part only
33
+ # - yyyymo : month and year
34
+ # - yyyy : year only
35
+ # - modd : month and day
36
+ #
37
+ # If `stamp_pattern` is omitted, the recent entries will be listed.
38
+ # Then, how many entries are listed depends on the implementaiton
39
+ # of the concrete repository class.
40
+ def entries(stamp_pattern = nil); []; end
41
+
42
+ end
43
+
44
+ require_relative 'file_system_repository'
45
+
46
+ # 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`.
51
+ def init(conf)
52
+ type = conf[:repository_type]
53
+ klass_name = type.to_s.split(/_/).map(&:capitalize).join + "Repository"
54
+ if Textrepo.const_defined?(klass_name)
55
+ klass = Textrepo.const_get(klass_name, false)
56
+ else
57
+ raise UnknownRepoTypeError, type.nil? ? "(nil)" : type
58
+ end
59
+ klass.new(conf)
60
+ end
61
+ module_function :init
62
+ end
@@ -0,0 +1,63 @@
1
+ module Textrepo
2
+ class Timestamp
3
+ include Comparable
4
+
5
+ attr_reader :time, :suffix
6
+
7
+ # time: a Time instance
8
+ # suffix: an Integer instance
9
+ def initialize(time, suffix = nil)
10
+ @time = time
11
+ @suffix = suffix
12
+ end
13
+
14
+ def <=>(other)
15
+ result = (self.time <=> other.time)
16
+
17
+ sfx = self.suffix || 0
18
+ osfx = other.suffix || 0
19
+
20
+ result == 0 ? (sfx <=> osfx) : result
21
+ end
22
+
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"
26
+ def to_s
27
+ s = @time.strftime("%Y%m%d%H%M%S")
28
+ s += "_#{"%03u" % @suffix}" unless @suffix.nil? || @suffix == 0
29
+ s
30
+ end
31
+
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
+ 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"
43
+ def split_stamp(stamp_str)
44
+ # yyyy mo dd hh mi ss sfx
45
+ a = [0..3, 4..5, 6..7, 8..9, 10..11, 12..13, 15..17].map {|r| stamp_str[r]}
46
+ a[-1].nil? ? a[0..-2] : a
47
+ end
48
+
49
+ 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)
52
+ end
53
+
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
+ end
62
+ end
63
+ end
@@ -0,0 +1,3 @@
1
+ module Textrepo
2
+ VERSION = '0.4.1'
3
+ end
@@ -0,0 +1,29 @@
1
+ require_relative 'lib/textrepo/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "textrepo"
5
+ spec.version = Textrepo::VERSION
6
+ spec.authors = ["mnbi"]
7
+ spec.email = ["mnbi@users.noreply.github.com"]
8
+
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).}
11
+ spec.homepage = "https://github.com/mnbi/textrepo"
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.7.0")
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+ spec.metadata["source_code_uri"] = "https://github.com/mnbi/textrepo"
17
+ spec.metadata["changelog_uri"] = "https://github.com/mnbi/textrepo/blob/main/CHANGELOG.md"
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
22
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
23
+ end
24
+ spec.bindir = "exe"
25
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
26
+ spec.require_paths = ["lib"]
27
+
28
+ spec.add_development_dependency "bundler", "~> 2.1"
29
+ end
metadata ADDED
@@ -0,0 +1,85 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: textrepo
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.4.1
5
+ platform: ruby
6
+ authors:
7
+ - mnbi
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2020-10-15 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: bundler
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '2.1'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '2.1'
27
+ description: Textrepo is a repository to store text with timestamp. It can manage
28
+ text with the attached timestamp (read/update/delte).
29
+ email:
30
+ - mnbi@users.noreply.github.com
31
+ executables: []
32
+ extensions: []
33
+ extra_rdoc_files: []
34
+ files:
35
+ - ".gitignore"
36
+ - ".travis.yml"
37
+ - CHANGELOG.md
38
+ - Gemfile
39
+ - Gemfile.lock
40
+ - LICENSE.txt
41
+ - README.md
42
+ - Rakefile
43
+ - bin/console
44
+ - bin/setup
45
+ - examples/rbnotes/lib/rbnotes.rb
46
+ - examples/rbnotes/lib/rbnotes/commands.rb
47
+ - examples/rbnotes/lib/rbnotes/commands/import.rb
48
+ - examples/rbnotes/lib/rbnotes/commands/list.rb
49
+ - examples/rbnotes/lib/rbnotes/commands/show.rb
50
+ - examples/rbnotes/lib/rbnotes/version.rb
51
+ - examples/rbnotes/rbnotes
52
+ - lib/textrepo.rb
53
+ - lib/textrepo/error.rb
54
+ - lib/textrepo/file_system_repository.rb
55
+ - lib/textrepo/repository.rb
56
+ - lib/textrepo/timestamp.rb
57
+ - lib/textrepo/version.rb
58
+ - textrepo.gemspec
59
+ homepage: https://github.com/mnbi/textrepo
60
+ licenses:
61
+ - MIT
62
+ metadata:
63
+ homepage_uri: https://github.com/mnbi/textrepo
64
+ source_code_uri: https://github.com/mnbi/textrepo
65
+ changelog_uri: https://github.com/mnbi/textrepo/blob/main/CHANGELOG.md
66
+ post_install_message:
67
+ rdoc_options: []
68
+ require_paths:
69
+ - lib
70
+ required_ruby_version: !ruby/object:Gem::Requirement
71
+ requirements:
72
+ - - ">="
73
+ - !ruby/object:Gem::Version
74
+ version: 2.7.0
75
+ required_rubygems_version: !ruby/object:Gem::Requirement
76
+ requirements:
77
+ - - ">="
78
+ - !ruby/object:Gem::Version
79
+ version: '0'
80
+ requirements: []
81
+ rubygems_version: 3.1.4
82
+ signing_key:
83
+ specification_version: 4
84
+ summary: A repository to store text with timestamp.
85
+ test_files: []