standup_md 0.2.1 → 0.3.4

Sign up to get free protection for your applications and to get access to all the features.
@@ -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_date: Date, end_date: 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