standup_md 0.2.1 → 0.3.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.
@@ -0,0 +1,166 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandupMD
4
+
5
+ ##
6
+ # Enumerable list of entries.
7
+ class EntryList
8
+ include Enumerable
9
+
10
+ ##
11
+ # Access to the class's configuration.
12
+ #
13
+ # @return [StandupMD::Config::EntryList]
14
+ def self.config
15
+ @config ||= StandupMD.config.entry_list
16
+ end
17
+
18
+ ##
19
+ # Contruct a list. Can pass any amount of +StandupMD::Entry+ instances.
20
+ #
21
+ # @param [Entry] entries
22
+ #
23
+ # @return [StandupMD::EntryList]
24
+ def initialize(*entries)
25
+ @config = self.class.config
26
+ unless entries.all? { |e| e.is_a?(StandupMD::Entry) }
27
+ raise ArgumentError, 'Entry must instance of StandupMD::Entry'
28
+ end
29
+ @entries = entries
30
+ end
31
+
32
+ ##
33
+ # Iterate over the list and yield each entry.
34
+ def each(&block)
35
+ @entries.each(&block)
36
+ end
37
+
38
+ ##
39
+ # Appends entries to list.
40
+ #
41
+ # @param [StandupMD::Entry] entry
42
+ #
43
+ # @return [Array]
44
+ def <<(entry)
45
+ unless entry.is_a?(StandupMD::Entry)
46
+ raise ArgumentError, 'Entry must instance of StandupMD::Entry'
47
+ end
48
+ @entries << entry
49
+ end
50
+
51
+ ##
52
+ # Finds an entry based on date. This method assumes the list has already
53
+ # been sorted.
54
+ def find(key)
55
+ to_a.bsearch { |e| e.date == key }
56
+ end
57
+
58
+ ##
59
+ # How many entries are in the list.
60
+ #
61
+ # @return [Integer]
62
+ def size
63
+ @entries.size
64
+ end
65
+
66
+ ##
67
+ # Is the list empty?
68
+ #
69
+ # @return [Boolean] true if empty
70
+ def empty?
71
+ @entries.empty?
72
+ end
73
+
74
+ ##
75
+ # Returns a copy of self sorted by date.
76
+ #
77
+ # @return [StandupMD::EntryList]
78
+ def sort
79
+ self.class.new(*@entries.sort)
80
+ end
81
+
82
+ ##
83
+ # Replace entries with sorted entries by date.
84
+ #
85
+ # @return [StandupMD::EntryList]
86
+ def sort!
87
+ @entries = @entries.sort
88
+ self
89
+ end
90
+
91
+ ##
92
+ # Returns a copy of self sorted by date.
93
+ #
94
+ # @return [StandupMD::EntryList]
95
+ def sort_reverse
96
+ self.class.new(*@entries.sort.reverse)
97
+ end
98
+
99
+ ##
100
+ # Returns entries that are between the start and end date. This method
101
+ # assumes the list has already been sorted.
102
+ #
103
+ # @param [Date] start_date
104
+ #
105
+ # @param [Date] end_date
106
+ #
107
+ # @return [Array]
108
+ def filter(start_date, end_date)
109
+ self.class.new(
110
+ *@entries.select { |e| e.date.between?(start_date, end_date) }
111
+ )
112
+ end
113
+
114
+ ##
115
+ # Replaces entries with results of filter.
116
+ #
117
+ # @param [Date] start_date
118
+ #
119
+ # @param [Date] end_date
120
+ #
121
+ # @return [Array]
122
+ def filter!(start_date, end_date)
123
+ @entries = filter(start_date, end_date)
124
+ self
125
+ end
126
+
127
+ ##
128
+ # The first entry in the list. This method assumes the list has
129
+ # already been sorted.
130
+ #
131
+ # @return [StandupMD::Entry]
132
+ def first
133
+ to_a.first
134
+ end
135
+
136
+ ##
137
+ # The last entry in the list. This method assumes the list has
138
+ # already been sorted.
139
+ #
140
+ # @return [StandupMD::Entry]
141
+ def last
142
+ to_a.last
143
+ end
144
+
145
+ ##
146
+ # The list as a hash, with the dates as keys.
147
+ #
148
+ # @return [Hash]
149
+ def to_h
150
+ Hash[@entries.map { |e| [e.date, {
151
+ 'current' => e.current,
152
+ 'previous' => e.previous,
153
+ 'impediments' => e.impediments,
154
+ 'notes' => e.notes
155
+ }]}]
156
+ end
157
+
158
+ ##
159
+ # The entry list as a json object.
160
+ #
161
+ # @return [String]
162
+ def to_json
163
+ to_h.to_json
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,172 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'date'
4
+ require 'fileutils'
5
+ require_relative 'file/helpers'
6
+
7
+ module StandupMD
8
+
9
+ ##
10
+ # Class for handling reading and writing standup files.
11
+ class File
12
+ include StandupMD::File::Helpers
13
+
14
+ ##
15
+ # Access to the class's configuration.
16
+ #
17
+ # @return [StandupMD::Config::EntryList]
18
+ def self.config
19
+ @config ||= StandupMD.config.file
20
+ end
21
+
22
+ ##
23
+ # Find standup file in directory by file name.
24
+ #
25
+ # @param [String] File_naem
26
+ def self.find(file_name)
27
+ file = Dir.entries(config.directory).bsearch { |f| f == file_name }
28
+ if file.nil? && !config.create
29
+ raise "File #{file_name} not found." unless config.create
30
+ end
31
+ new(file_name)
32
+ end
33
+
34
+ ##
35
+ # Find standup file in directory by Date object.
36
+ #
37
+ # @param [Date] date
38
+ def self.find_by_date(date)
39
+ unless date.is_a?(Date)
40
+ raise ArgumentError, "Argument must be a Date object"
41
+ end
42
+ find(date.strftime(config.name_format))
43
+ end
44
+
45
+ ##
46
+ # The list of entries in the file.
47
+ #
48
+ # @return [StandupMP::EntryList]
49
+ attr_reader :entries
50
+
51
+ ##
52
+ # The name of the file.
53
+ #
54
+ # @return [String]
55
+ attr_reader :name
56
+
57
+ ##
58
+ # Constructs the instance.
59
+ #
60
+ # @param [String] file_name
61
+ #
62
+ # @return [StandupMP::File]
63
+ def initialize(file_name)
64
+ @config = self.class.config
65
+ if file_name.include?(::File::SEPARATOR)
66
+ raise ArgumentError,
67
+ "#{file_name} contains directory. Please use `StandupMD.config.file.directory=`"
68
+ end
69
+
70
+ unless ::File.directory?(@config.directory)
71
+ raise "Dir #{@config.directory} not found." unless @config.create
72
+ FileUtils.mkdir_p(@config.directory)
73
+ end
74
+
75
+ @name = ::File.expand_path(::File.join(@config.directory, file_name))
76
+
77
+ unless ::File.file?(@name)
78
+ raise "File #{@name} not found." unless @config.create
79
+ FileUtils.touch(@name)
80
+ end
81
+
82
+ @new = ::File.zero?(@name)
83
+ @loaded = false
84
+ end
85
+
86
+ ##
87
+ # Was the file just now created?
88
+ #
89
+ # @return [Boolean] true if new
90
+ def new?
91
+ @new
92
+ end
93
+
94
+ ##
95
+ # Has the file been loaded?
96
+ #
97
+ # @return [Boolean] true if loaded
98
+ def loaded?
99
+ @loaded
100
+ end
101
+
102
+ ##
103
+ # Does the file exist?
104
+ #
105
+ # @return [Boolean] true if exists
106
+ def exist?
107
+ ::File.exist?(name)
108
+ end
109
+
110
+ ##
111
+ # Loads the file's contents.
112
+ # TODO clean up this method.
113
+ #
114
+ # @return [StandupMD::FileList]
115
+ def load
116
+ raise "File #{name} does not exist." unless ::File.file?(name)
117
+ entry_list = EntryList.new
118
+ record = {}
119
+ section_type = ''
120
+ ::File.foreach(name) do |line|
121
+ line.chomp!
122
+ next if line.strip.empty?
123
+ if is_header?(line)
124
+ unless record.empty?
125
+ entry_list << new_entry(record)
126
+ record = {}
127
+ end
128
+ record['header'] = line.sub(%r{^\#{#{@config.header_depth}}\s*}, '')
129
+ section_type = @config.notes_header
130
+ record[section_type] = []
131
+ elsif is_sub_header?(line)
132
+ section_type = determine_section_type(line)
133
+ record[section_type] = []
134
+ else
135
+ record[section_type] << line.sub(bullet_character_regex, '')
136
+ end
137
+ end
138
+ entry_list << new_entry(record) unless record.empty?
139
+ @loaded = true
140
+ @entries = entry_list.sort
141
+ rescue => e
142
+ raise "File malformation: #{e}"
143
+ end
144
+
145
+ ##
146
+ # Writes a new entry to the file if the first entry in the file isn't today.
147
+ # This method is destructive; if a file for entries in the date range
148
+ # already exists, it will be clobbered with the entries in the range.
149
+ #
150
+ # @param [Hash] start_and_end_date
151
+ #
152
+ # @return [Boolean] true if successful
153
+ def write(dates = {})
154
+ sorted_entries = entries.sort
155
+ start_date = dates.fetch(:start_date, sorted_entries.first.date)
156
+ end_date = dates.fetch(:end_date, sorted_entries.last.date)
157
+ ::File.open(name, 'w') do |f|
158
+ sorted_entries.filter(start_date, end_date).sort_reverse.each do |entry|
159
+ f.puts header(entry.date)
160
+ @config.sub_header_order.each do |attr|
161
+ tasks = entry.send(attr)
162
+ next if !tasks || tasks.empty?
163
+ f.puts sub_header(@config.send("#{attr}_header").capitalize)
164
+ tasks.each { |task| f.puts @config.bullet_character + ' ' + task }
165
+ end
166
+ f.puts
167
+ end
168
+ end
169
+ true
170
+ end
171
+ end
172
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module StandupMD
4
+ class File
5
+
6
+ ##
7
+ # Module responsible for reading and writing standup files.
8
+ module Helpers # :nodoc:
9
+
10
+ private
11
+
12
+ def is_header?(line) # :nodoc:
13
+ line.match(header_regex)
14
+ end
15
+
16
+ def is_sub_header?(line) # :nodoc:
17
+ line.match(sub_header_regex)
18
+ end
19
+
20
+ def header_regex # :nodoc:
21
+ %r{^#{'#' * StandupMD.config.file.header_depth}\s+}
22
+ end
23
+
24
+ def sub_header_regex # :nodoc:
25
+ %r{^#{'#' * StandupMD.config.file.sub_header_depth}\s+}
26
+ end
27
+
28
+ def bullet_character_regex # :nodoc:
29
+ %r{\s*#{StandupMD.config.file.bullet_character}\s*}
30
+ end
31
+
32
+ def determine_section_type(line) # :nodoc:
33
+ line = line.sub(%r{^\#{#{StandupMD.config.file.sub_header_depth}}\s*}, '')
34
+ [
35
+ StandupMD.config.file.current_header,
36
+ StandupMD.config.file.previous_header,
37
+ StandupMD.config.file.impediments_header,
38
+ StandupMD.config.file.notes_header
39
+ ].each { |header| return header if line.include?(header) }
40
+ raise "Unrecognized header [#{line}]"
41
+ end
42
+
43
+ def new_entry(record) # :nodoc:
44
+ Entry.new(
45
+ Date.strptime(record['header'], StandupMD.config.file.header_date_format),
46
+ record[StandupMD.config.file.current_header],
47
+ record[StandupMD.config.file.previous_header],
48
+ record[StandupMD.config.file.impediments_header],
49
+ record[StandupMD.config.file.notes_header]
50
+ )
51
+ end
52
+
53
+ def header(date)
54
+ '#' * StandupMD.config.file.header_depth + ' ' + date.strftime(StandupMD.config.file.header_date_format)
55
+ end
56
+
57
+ def sub_header(sh)
58
+ '#' * StandupMD.config.file.sub_header_depth + ' ' + sh
59
+ end
60
+ end
61
+ end
62
+ end
@@ -1,9 +1,11 @@
1
- class StandupMD
1
+ # frozen_string_literal: true
2
+
3
+ module StandupMD
2
4
  ##
3
5
  # The gem verision
4
6
  #
5
7
  # @example
6
8
  # StandupMD::VERSION
7
- # # => '0.2.1'
8
- VERSION = '0.2.1'
9
+ # # => '0.3.0'
10
+ VERSION = '0.3.0'
9
11
  end
@@ -36,4 +36,5 @@ Gem::Specification.new do |spec|
36
36
  spec.require_paths = ['lib']
37
37
  spec.add_development_dependency 'rake', '~> 13.0', '>= 13.0.1'
38
38
  spec.add_development_dependency 'test-unit', '~> 3.3', '>= 3.3.5'
39
+ spec.add_development_dependency 'simplecov'
39
40
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standup_md
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.1
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Gray
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-05-03 00:00:00.000000000 Z
11
+ date: 2020-06-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rake
@@ -50,6 +50,20 @@ dependencies:
50
50
  - - ">="
51
51
  - !ruby/object:Gem::Version
52
52
  version: 3.3.5
53
+ - !ruby/object:Gem::Dependency
54
+ name: simplecov
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ">="
58
+ - !ruby/object:Gem::Version
59
+ version: '0'
60
+ type: :development
61
+ prerelease: false
62
+ version_requirements: !ruby/object:Gem::Requirement
63
+ requirements:
64
+ - - ">="
65
+ - !ruby/object:Gem::Version
66
+ version: '0'
53
67
  description: Generate and edit standups in markdown format
54
68
  email: evanthegrayt@vivaldi.net
55
69
  executables:
@@ -69,6 +83,15 @@ files:
69
83
  - doc/README_md.html
70
84
  - doc/StandupMD.html
71
85
  - doc/StandupMD/Cli.html
86
+ - doc/StandupMD/Cli/Helpers.html
87
+ - doc/StandupMD/Config.html
88
+ - doc/StandupMD/Config/Cli.html
89
+ - doc/StandupMD/Config/Entry.html
90
+ - doc/StandupMD/Config/EntryList.html
91
+ - doc/StandupMD/Config/File.html
92
+ - doc/StandupMD/Entry.html
93
+ - doc/StandupMD/EntryList.html
94
+ - doc/StandupMD/File.html
72
95
  - doc/created.rid
73
96
  - doc/css/fonts.css
74
97
  - doc/css/rdoc.css
@@ -115,6 +138,16 @@ files:
115
138
  - doc/table_of_contents.html
116
139
  - lib/standup_md.rb
117
140
  - lib/standup_md/cli.rb
141
+ - lib/standup_md/cli/helpers.rb
142
+ - lib/standup_md/config.rb
143
+ - lib/standup_md/config/cli.rb
144
+ - lib/standup_md/config/entry.rb
145
+ - lib/standup_md/config/entry_list.rb
146
+ - lib/standup_md/config/file.rb
147
+ - lib/standup_md/entry.rb
148
+ - lib/standup_md/entry_list.rb
149
+ - lib/standup_md/file.rb
150
+ - lib/standup_md/file/helpers.rb
118
151
  - lib/standup_md/version.rb
119
152
  - standup_md.gemspec
120
153
  homepage: https://evanthegrayt.github.io/standup_md/