standup_md 0.2.0 → 0.3.3

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,122 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+
5
+ module StandupMD
6
+
7
+ ##
8
+ # Class for handling single entries. Includes the comparable module, and
9
+ # compares by date.
10
+ class Entry
11
+ include Comparable
12
+
13
+ ##
14
+ # Access to the class's configuration.
15
+ #
16
+ # @return [StandupMD::Config::Entry]
17
+ def self.config
18
+ @config ||= StandupMD.config.entry
19
+ end
20
+
21
+ ##
22
+ # The date of the entry.
23
+ #
24
+ # @param [Date] date
25
+ #
26
+ # @return [Date]
27
+ attr_accessor :date
28
+
29
+ ##
30
+ # The tasks for today.
31
+ #
32
+ # @return [Array]
33
+ attr_accessor :current
34
+
35
+ ##
36
+ # The tasks from the previous day.
37
+ #
38
+ # @return [Array]
39
+ attr_accessor :previous
40
+
41
+ ##
42
+ # Iimpediments for this entry.
43
+ #
44
+ # @return [Array]
45
+ attr_accessor :impediments
46
+
47
+ ##
48
+ # Nnotes to add to this entry.
49
+ #
50
+ # @return [Array]
51
+ attr_accessor :notes
52
+
53
+ ##
54
+ # Creates a generic entry. Default values can be set via configuration.
55
+ # Yields the entry if a block is passed so you can change values.
56
+ #
57
+ # @return [StandupMD::Entry]
58
+ def self.create
59
+ entry = new(
60
+ Date.today,
61
+ config.current,
62
+ config.previous,
63
+ config.impediments,
64
+ config.notes
65
+ )
66
+ yield config if block_given?
67
+ entry
68
+ end
69
+
70
+ ##
71
+ # Constructs instance of +StandupMD::Entry+.
72
+ #
73
+ # @param [Date] date
74
+ #
75
+ # @param [Array] current
76
+ #
77
+ # @param [Array] previous
78
+ #
79
+ # @param [Array] impediments
80
+ #
81
+ # @param [Array] notes
82
+ def initialize(date, current, previous, impediments, notes = [])
83
+ raise unless date.is_a?(Date)
84
+ @config = self.class.config
85
+
86
+ @date = date
87
+ @current = current
88
+ @previous = previous
89
+ @impediments = impediments
90
+ @notes = notes
91
+ end
92
+
93
+ ##
94
+ # Sorting method for Comparable. Entries are compared by date.
95
+ def <=>(other)
96
+ date <=> other.date
97
+ end
98
+
99
+ ##
100
+ # Entry as a hash .
101
+ #
102
+ # @return [Hash]
103
+ def to_h
104
+ {
105
+ date => {
106
+ 'current' => current,
107
+ 'previous' => previous,
108
+ 'impediments' => impediments,
109
+ 'notes' => notes
110
+ }
111
+ }
112
+ end
113
+
114
+ ##
115
+ # Entry as a json object.
116
+ #
117
+ # @return [String]
118
+ def to_json
119
+ to_h.to_json
120
+ end
121
+ end
122
+ end
@@ -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
+ # Returns a copy of self sorted by date.
60
+ #
61
+ # @return [StandupMD::EntryList]
62
+ def sort
63
+ self.class.new(*@entries.sort)
64
+ end
65
+
66
+ ##
67
+ # Replace entries with sorted entries by date.
68
+ #
69
+ # @return [StandupMD::EntryList]
70
+ def sort!
71
+ @entries = @entries.sort
72
+ self
73
+ end
74
+
75
+ ##
76
+ # Returns a copy of self sorted by date.
77
+ #
78
+ # @return [StandupMD::EntryList]
79
+ def sort_reverse
80
+ self.class.new(*@entries.sort.reverse)
81
+ end
82
+
83
+ ##
84
+ # Returns entries that are between the start and end date. This method
85
+ # assumes the list has already been sorted.
86
+ #
87
+ # @param [Date] start_date
88
+ #
89
+ # @param [Date] end_date
90
+ #
91
+ # @return [Array]
92
+ def filter(start_date, end_date)
93
+ self.class.new(
94
+ *@entries.select { |e| e.date.between?(start_date, end_date) }
95
+ )
96
+ end
97
+
98
+ ##
99
+ # Replaces entries with results of filter.
100
+ #
101
+ # @param [Date] start_date
102
+ #
103
+ # @param [Date] end_date
104
+ #
105
+ # @return [Array]
106
+ def filter!(start_date, end_date)
107
+ @entries = filter(start_date, end_date)
108
+ self
109
+ end
110
+
111
+ ##
112
+ # The list as a hash, with the dates as keys.
113
+ #
114
+ # @return [Hash]
115
+ def to_h
116
+ Hash[@entries.map { |e| [e.date, {
117
+ 'current' => e.current,
118
+ 'previous' => e.previous,
119
+ 'impediments' => e.impediments,
120
+ 'notes' => e.notes
121
+ }]}]
122
+ end
123
+
124
+ ##
125
+ # The entry list as a json object.
126
+ #
127
+ # @return [String]
128
+ def to_json
129
+ to_h.to_json
130
+ end
131
+
132
+ ##
133
+ # The first entry in the list. This method assumes the list has
134
+ # already been sorted.
135
+ #
136
+ # @return [StandupMD::Entry]
137
+ def first
138
+ to_a.first
139
+ end
140
+
141
+ ##
142
+ # The last entry in the list. This method assumes the list has
143
+ # already been sorted.
144
+ #
145
+ # @return [StandupMD::Entry]
146
+ def last
147
+ to_a.last
148
+ end
149
+
150
+ ##
151
+ # How many entries are in the list.
152
+ #
153
+ # @return [Integer]
154
+ def size
155
+ @entries.size
156
+ end
157
+
158
+ ##
159
+ # Is the list empty?
160
+ #
161
+ # @return [Boolean] true if empty
162
+ def empty?
163
+ @entries.empty?
164
+ end
165
+ end
166
+ end
@@ -0,0 +1,183 @@
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
+ # Convenience method for calling File.find(file_name).load
24
+ #
25
+ # @param [String] file_name
26
+ #
27
+ # @return [StandupMD::File]
28
+ def self.load(file_name)
29
+ new(file_name).load
30
+ end
31
+
32
+ ##
33
+ # Find standup file in directory by file name.
34
+ #
35
+ # @param [String] File_naem
36
+ def self.find(file_name)
37
+ file = Dir.entries(config.directory).bsearch { |f| f == file_name }
38
+ if file.nil? && !config.create
39
+ raise "File #{file_name} not found." unless config.create
40
+ end
41
+ new(file_name)
42
+ end
43
+
44
+ ##
45
+ # Find standup file in directory by Date object.
46
+ #
47
+ # @param [Date] date
48
+ def self.find_by_date(date)
49
+ unless date.is_a?(Date)
50
+ raise ArgumentError, "Argument must be a Date object"
51
+ end
52
+ find(date.strftime(config.name_format))
53
+ end
54
+
55
+ ##
56
+ # The list of entries in the file.
57
+ #
58
+ # @return [StandupMP::EntryList]
59
+ attr_reader :entries
60
+
61
+ ##
62
+ # The name of the file.
63
+ #
64
+ # @return [String]
65
+ attr_reader :name
66
+
67
+ ##
68
+ # Constructs the instance.
69
+ #
70
+ # @param [String] file_name
71
+ #
72
+ # @return [StandupMP::File]
73
+ def initialize(file_name)
74
+ @config = self.class.config
75
+ if file_name.include?(::File::SEPARATOR)
76
+ raise ArgumentError,
77
+ "#{file_name} contains directory. Please use `StandupMD.config.file.directory=`"
78
+ end
79
+
80
+ unless ::File.directory?(@config.directory)
81
+ raise "Dir #{@config.directory} not found." unless @config.create
82
+ FileUtils.mkdir_p(@config.directory)
83
+ end
84
+
85
+ @name = ::File.expand_path(::File.join(@config.directory, file_name))
86
+
87
+ unless ::File.file?(@name)
88
+ raise "File #{@name} not found." unless @config.create
89
+ FileUtils.touch(@name)
90
+ end
91
+
92
+ @new = ::File.zero?(@name)
93
+ @loaded = false
94
+ end
95
+
96
+ ##
97
+ # Was the file just now created?
98
+ #
99
+ # @return [Boolean] true if new
100
+ def new?
101
+ @new
102
+ end
103
+
104
+ ##
105
+ # Has the file been loaded?
106
+ #
107
+ # @return [Boolean] true if loaded
108
+ def loaded?
109
+ @loaded
110
+ end
111
+
112
+ ##
113
+ # Does the file exist?
114
+ #
115
+ # @return [Boolean] true if exists
116
+ def exist?
117
+ ::File.exist?(name)
118
+ end
119
+
120
+ ##
121
+ # Loads the file's contents.
122
+ # TODO clean up this method.
123
+ #
124
+ # @return [StandupMD::FileList]
125
+ def load
126
+ raise "File #{name} does not exist." unless ::File.file?(name)
127
+ entry_list = EntryList.new
128
+ record = {}
129
+ section_type = ''
130
+ ::File.foreach(name) do |line|
131
+ line.chomp!
132
+ next if line.strip.empty?
133
+ if is_header?(line)
134
+ unless record.empty?
135
+ entry_list << new_entry(record)
136
+ record = {}
137
+ end
138
+ record['header'] = line.sub(%r{^\#{#{@config.header_depth}}\s*}, '')
139
+ section_type = @config.notes_header
140
+ record[section_type] = []
141
+ elsif is_sub_header?(line)
142
+ section_type = determine_section_type(line)
143
+ record[section_type] = []
144
+ else
145
+ record[section_type] << line.sub(bullet_character_regex, '')
146
+ end
147
+ end
148
+ entry_list << new_entry(record) unless record.empty?
149
+ @loaded = true
150
+ @entries = entry_list.sort
151
+ self
152
+ rescue => e
153
+ raise "File malformation: #{e}"
154
+ end
155
+
156
+ ##
157
+ # Writes a new entry to the file if the first entry in the file isn't today.
158
+ # This method is destructive; if a file for entries in the date range
159
+ # already exists, it will be clobbered with the entries in the range.
160
+ #
161
+ # @param [Hash] start_and_end_date
162
+ #
163
+ # @return [Boolean] true if successful
164
+ def write(dates = {})
165
+ sorted_entries = entries.sort
166
+ start_date = dates.fetch(:start_date, sorted_entries.first.date)
167
+ end_date = dates.fetch(:end_date, sorted_entries.last.date)
168
+ ::File.open(name, 'w') do |f|
169
+ sorted_entries.filter(start_date, end_date).sort_reverse.each do |entry|
170
+ f.puts header(entry.date)
171
+ @config.sub_header_order.each do |attr|
172
+ tasks = entry.send(attr)
173
+ next if !tasks || tasks.empty?
174
+ f.puts sub_header(@config.send("#{attr}_header").capitalize)
175
+ tasks.each { |task| f.puts @config.bullet_character + ' ' + task }
176
+ end
177
+ f.puts
178
+ end
179
+ end
180
+ true
181
+ end
182
+ end
183
+ end