standup_md 1.0.1 → 2.0.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 +4 -4
- data/.github/workflows/ruby.yml +4 -6
- data/Gemfile.lock +2 -2
- data/README.md +191 -39
- data/completion/zsh/_standup +12 -17
- data/lib/standup_md/cli/helpers.rb +55 -53
- data/lib/standup_md/cli.rb +40 -22
- data/lib/standup_md/config/cli.rb +70 -7
- data/lib/standup_md/config/entry.rb +31 -1
- data/lib/standup_md/config/file.rb +41 -7
- data/lib/standup_md/config/post.rb +152 -0
- data/lib/standup_md/config.rb +18 -5
- data/lib/standup_md/entry.rb +27 -18
- data/lib/standup_md/entry_list.rb +5 -22
- data/lib/standup_md/file.rb +57 -54
- data/lib/standup_md/parsers/markdown.rb +42 -34
- data/lib/standup_md/post/adapter.rb +41 -0
- data/lib/standup_md/post/adapters/slack.rb +108 -0
- data/lib/standup_md/post/message.rb +47 -0
- data/lib/standup_md/post/result.rb +87 -0
- data/lib/standup_md/post.rb +84 -0
- data/lib/standup_md/section.rb +2 -13
- data/lib/standup_md/task.rb +0 -9
- data/lib/standup_md/version.rb +2 -2
- data/lib/standup_md.rb +2 -2
- data/standup_md.gemspec +1 -0
- metadata +8 -4
- data/lib/standup_md/config/entry_list.rb +0 -29
- data/lib/standup_md/title.rb +0 -41
|
@@ -10,15 +10,7 @@ module StandupMD
|
|
|
10
10
|
include Enumerable
|
|
11
11
|
|
|
12
12
|
##
|
|
13
|
-
#
|
|
14
|
-
#
|
|
15
|
-
# @return [StandupMD::Config::EntryList]
|
|
16
|
-
def self.config
|
|
17
|
-
@config ||= StandupMD.config.entry_list
|
|
18
|
-
end
|
|
19
|
-
|
|
20
|
-
##
|
|
21
|
-
# Contruct a list. Can pass any amount of +StandupMD::Entry+ instances.
|
|
13
|
+
# Construct a list. Can pass any amount of +StandupMD::Entry+ instances.
|
|
22
14
|
#
|
|
23
15
|
# @param [Entry] entries
|
|
24
16
|
#
|
|
@@ -26,7 +18,6 @@ module StandupMD
|
|
|
26
18
|
def initialize(*entries)
|
|
27
19
|
entries.each { |entry| validate_entry(entry) }
|
|
28
20
|
|
|
29
|
-
@config = self.class.config
|
|
30
21
|
@entries = entries
|
|
31
22
|
end
|
|
32
23
|
|
|
@@ -35,7 +26,7 @@ module StandupMD
|
|
|
35
26
|
#
|
|
36
27
|
# @param [StandupMD::Entry] entry
|
|
37
28
|
#
|
|
38
|
-
# @return [
|
|
29
|
+
# @return [StandupMD::EntryList]
|
|
39
30
|
def <<(entry)
|
|
40
31
|
validate_entry(entry)
|
|
41
32
|
|
|
@@ -100,9 +91,9 @@ module StandupMD
|
|
|
100
91
|
#
|
|
101
92
|
# @param [Date] end_date
|
|
102
93
|
#
|
|
103
|
-
# @return [
|
|
94
|
+
# @return [StandupMD::EntryList]
|
|
104
95
|
def filter!(start_date, end_date)
|
|
105
|
-
@entries = filter(start_date, end_date)
|
|
96
|
+
@entries = filter(start_date, end_date).to_a
|
|
106
97
|
self
|
|
107
98
|
end
|
|
108
99
|
|
|
@@ -124,18 +115,10 @@ module StandupMD
|
|
|
124
115
|
end.to_h
|
|
125
116
|
end
|
|
126
117
|
|
|
127
|
-
##
|
|
128
|
-
# The entry list as a json object.
|
|
129
|
-
#
|
|
130
|
-
# @return [String]
|
|
131
|
-
def to_json
|
|
132
|
-
to_h.to_json
|
|
133
|
-
end
|
|
134
|
-
|
|
135
118
|
# :section: Delegators
|
|
136
119
|
|
|
137
120
|
##
|
|
138
|
-
# The following are forwarded to @entries, which is the
|
|
121
|
+
# The following are forwarded to @entries, which is the underlying array of
|
|
139
122
|
# entries.
|
|
140
123
|
#
|
|
141
124
|
# +each+:: Iterate over each entry.
|
data/lib/standup_md/file.rb
CHANGED
|
@@ -8,68 +8,59 @@ module StandupMD
|
|
|
8
8
|
##
|
|
9
9
|
# Class for handling reading and writing standup files.
|
|
10
10
|
class File
|
|
11
|
+
##
|
|
12
|
+
# Raised when a standup file or directory is missing and creation is off.
|
|
13
|
+
class NotFoundError < StandardError; end
|
|
14
|
+
|
|
11
15
|
class << self
|
|
12
16
|
##
|
|
13
17
|
# Access to the class's configuration.
|
|
14
18
|
#
|
|
15
|
-
# @return [StandupMD::Config::
|
|
19
|
+
# @return [StandupMD::Config::File]
|
|
16
20
|
def config
|
|
17
|
-
|
|
21
|
+
StandupMD.config.file
|
|
18
22
|
end
|
|
19
23
|
|
|
20
24
|
##
|
|
21
|
-
# Convenience method for calling File.find(file_name).load
|
|
25
|
+
# Convenience method for calling File.find(file_name).load.
|
|
22
26
|
#
|
|
23
27
|
# @param [String] file_name
|
|
28
|
+
# @param [StandupMD::Config::File] config
|
|
24
29
|
#
|
|
25
30
|
# @return [StandupMD::File]
|
|
26
|
-
def load(file_name)
|
|
27
|
-
|
|
28
|
-
raise "Dir #{config.directory} not found." unless config.create
|
|
29
|
-
|
|
30
|
-
FileUtils.mkdir_p(config.directory)
|
|
31
|
-
end
|
|
32
|
-
new(file_name).load
|
|
31
|
+
def load(file_name, config: StandupMD.config.file)
|
|
32
|
+
new(file_name, config: config).load
|
|
33
33
|
end
|
|
34
34
|
|
|
35
35
|
##
|
|
36
36
|
# Find standup file in directory by file name.
|
|
37
37
|
#
|
|
38
|
-
# @param [String]
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
end
|
|
45
|
-
file_path = ::File.join(config.directory, file_name)
|
|
46
|
-
unless ::File.file?(file_path) || config.create
|
|
47
|
-
raise "File #{file_name} not found."
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
new(file_name)
|
|
38
|
+
# @param [String] file_name
|
|
39
|
+
# @param [StandupMD::Config::File] config
|
|
40
|
+
#
|
|
41
|
+
# @return [StandupMD::File]
|
|
42
|
+
def find(file_name, config: StandupMD.config.file)
|
|
43
|
+
new(file_name, config: config)
|
|
51
44
|
end
|
|
52
45
|
|
|
53
46
|
##
|
|
54
47
|
# Find standup file in directory by Date object.
|
|
55
48
|
#
|
|
56
49
|
# @param [Date] date
|
|
57
|
-
|
|
50
|
+
# @param [StandupMD::Config::File] config
|
|
51
|
+
#
|
|
52
|
+
# @return [StandupMD::File]
|
|
53
|
+
def find_by_date(date, config: StandupMD.config.file)
|
|
58
54
|
raise ArgumentError, "Must be a Date object" unless date.is_a?(Date)
|
|
59
55
|
|
|
60
|
-
|
|
61
|
-
raise "Dir #{config.directory} not found." unless config.create
|
|
62
|
-
|
|
63
|
-
FileUtils.mkdir_p(config.directory)
|
|
64
|
-
end
|
|
65
|
-
find(date.strftime(config.name_format))
|
|
56
|
+
find(date.strftime(config.name_format), config: config)
|
|
66
57
|
end
|
|
67
58
|
end
|
|
68
59
|
|
|
69
60
|
##
|
|
70
61
|
# The list of entries in the file.
|
|
71
62
|
#
|
|
72
|
-
# @return [
|
|
63
|
+
# @return [StandupMD::EntryList]
|
|
73
64
|
attr_reader :entries
|
|
74
65
|
|
|
75
66
|
##
|
|
@@ -82,29 +73,20 @@ module StandupMD
|
|
|
82
73
|
# Constructs the instance.
|
|
83
74
|
#
|
|
84
75
|
# @param [String] file_name
|
|
76
|
+
# @param [StandupMD::Config::File] config
|
|
85
77
|
#
|
|
86
|
-
# @return [
|
|
87
|
-
def initialize(file_name)
|
|
88
|
-
@config =
|
|
78
|
+
# @return [StandupMD::File]
|
|
79
|
+
def initialize(file_name, config: StandupMD.config.file)
|
|
80
|
+
@config = config
|
|
89
81
|
@parser = StandupMD::Parsers::Markdown.new(@config)
|
|
90
82
|
if file_name.include?(::File::SEPARATOR)
|
|
91
83
|
raise ArgumentError,
|
|
92
|
-
"#{file_name} contains directory.
|
|
93
|
-
end
|
|
94
|
-
|
|
95
|
-
unless ::File.directory?(@config.directory)
|
|
96
|
-
raise "Dir #{@config.directory} not found." unless @config.create
|
|
97
|
-
|
|
98
|
-
FileUtils.mkdir_p(@config.directory)
|
|
84
|
+
"#{file_name} contains directory. Configure the file directory separately."
|
|
99
85
|
end
|
|
100
86
|
|
|
87
|
+
ensure_directory
|
|
101
88
|
@name = ::File.expand_path(::File.join(@config.directory, file_name))
|
|
102
|
-
|
|
103
|
-
unless ::File.file?(@name)
|
|
104
|
-
raise "File #{@name} not found." unless @config.create
|
|
105
|
-
|
|
106
|
-
FileUtils.touch(@name)
|
|
107
|
-
end
|
|
89
|
+
ensure_file
|
|
108
90
|
|
|
109
91
|
@new = ::File.zero?(@name)
|
|
110
92
|
@loaded = false
|
|
@@ -137,28 +119,49 @@ module StandupMD
|
|
|
137
119
|
##
|
|
138
120
|
# Loads the file's contents.
|
|
139
121
|
#
|
|
140
|
-
# @return [StandupMD::
|
|
122
|
+
# @return [StandupMD::File]
|
|
141
123
|
def load
|
|
142
|
-
raise "File #{name} does not exist." unless ::File.file?(name)
|
|
124
|
+
raise NotFoundError, "File #{name} does not exist." unless ::File.file?(name)
|
|
143
125
|
|
|
144
126
|
@loaded = true
|
|
145
|
-
@entries = @parser.read(name)
|
|
127
|
+
@entries = @parser.parse(::File.read(name))
|
|
146
128
|
self
|
|
147
129
|
end
|
|
148
130
|
|
|
149
131
|
##
|
|
150
|
-
# Writes
|
|
151
|
-
#
|
|
152
|
-
# already exists, it will be clobbered with the entries in the range.
|
|
132
|
+
# Writes entries to disk. This method is destructive; existing file contents
|
|
133
|
+
# are replaced by the rendered entries in the requested date range.
|
|
153
134
|
#
|
|
154
135
|
# @param [Hash] {start_date: Date, end_date: Date}
|
|
155
136
|
#
|
|
156
137
|
# @return [Boolean] true if successful
|
|
157
138
|
def write(**dates)
|
|
139
|
+
raise ArgumentError, "No entries loaded for #{name}" if entries.nil? || entries.empty?
|
|
140
|
+
|
|
158
141
|
sorted_entries = entries.sort
|
|
159
142
|
start_date = dates.fetch(:start_date, sorted_entries.first.date)
|
|
160
143
|
end_date = dates.fetch(:end_date, sorted_entries.last.date)
|
|
161
|
-
|
|
144
|
+
::File.write(
|
|
145
|
+
name,
|
|
146
|
+
@parser.render(sorted_entries, start_date: start_date, end_date: end_date)
|
|
147
|
+
)
|
|
148
|
+
true
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
private
|
|
152
|
+
|
|
153
|
+
def ensure_directory
|
|
154
|
+
return if ::File.directory?(@config.directory)
|
|
155
|
+
raise NotFoundError, "Dir #{@config.directory} not found." unless @config.create
|
|
156
|
+
|
|
157
|
+
FileUtils.mkdir_p(@config.directory)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def ensure_file
|
|
161
|
+
return if ::File.file?(@name)
|
|
162
|
+
raise NotFoundError, "File #{@name} not found." unless @config.create
|
|
163
|
+
|
|
164
|
+
FileUtils.touch(@name)
|
|
162
165
|
end
|
|
163
166
|
end
|
|
164
167
|
end
|
|
@@ -5,7 +5,6 @@ require "standup_md/entry"
|
|
|
5
5
|
require "standup_md/entry_list"
|
|
6
6
|
require "standup_md/section"
|
|
7
7
|
require "standup_md/task"
|
|
8
|
-
require "standup_md/title"
|
|
9
8
|
|
|
10
9
|
module StandupMD
|
|
11
10
|
##
|
|
@@ -14,6 +13,10 @@ module StandupMD
|
|
|
14
13
|
##
|
|
15
14
|
# Parser and renderer for the markdown standup format.
|
|
16
15
|
class Markdown
|
|
16
|
+
##
|
|
17
|
+
# Raised when markdown cannot be parsed into standup entries.
|
|
18
|
+
class Error < StandardError; end
|
|
19
|
+
|
|
17
20
|
##
|
|
18
21
|
# Access to file configuration.
|
|
19
22
|
#
|
|
@@ -29,17 +32,17 @@ module StandupMD
|
|
|
29
32
|
end
|
|
30
33
|
|
|
31
34
|
##
|
|
32
|
-
#
|
|
35
|
+
# Parses entries from markdown text.
|
|
33
36
|
#
|
|
34
|
-
# @param [String]
|
|
37
|
+
# @param [String] text
|
|
35
38
|
#
|
|
36
39
|
# @return [StandupMD::EntryList]
|
|
37
|
-
def
|
|
40
|
+
def parse(text)
|
|
38
41
|
entry_list = EntryList.new
|
|
39
42
|
record = nil
|
|
40
43
|
section = nil
|
|
41
44
|
|
|
42
|
-
|
|
45
|
+
text.to_s.each_line do |line|
|
|
43
46
|
line.chomp!
|
|
44
47
|
next if line.strip.empty?
|
|
45
48
|
|
|
@@ -59,42 +62,40 @@ module StandupMD
|
|
|
59
62
|
entry_list << entry(record) if record
|
|
60
63
|
entry_list.sort
|
|
61
64
|
rescue => e
|
|
62
|
-
raise "
|
|
65
|
+
raise Error, "Markdown malformation: #{e.message}"
|
|
63
66
|
end
|
|
64
67
|
|
|
65
68
|
##
|
|
66
|
-
#
|
|
69
|
+
# Renders entries as markdown text.
|
|
67
70
|
#
|
|
68
|
-
# @param [String] file_name
|
|
69
71
|
# @param [StandupMD::EntryList] entries
|
|
70
72
|
# @param [Date] start_date
|
|
71
73
|
# @param [Date] end_date
|
|
72
74
|
#
|
|
73
|
-
# @return [
|
|
74
|
-
def
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
config.sub_header_order.each do |attr|
|
|
79
|
-
section = Section.new(attr, entry.public_send("#{attr}_tasks"))
|
|
80
|
-
next if section.empty?
|
|
81
|
-
|
|
82
|
-
f.puts section.to_markdown
|
|
83
|
-
end
|
|
84
|
-
f.puts
|
|
85
|
-
end
|
|
86
|
-
end
|
|
87
|
-
true
|
|
75
|
+
# @return [String]
|
|
76
|
+
def render(entries, start_date:, end_date:)
|
|
77
|
+
entries.filter(start_date, end_date).sort_reverse.map do |entry|
|
|
78
|
+
render_entry(entry)
|
|
79
|
+
end.join
|
|
88
80
|
end
|
|
89
81
|
|
|
90
82
|
##
|
|
91
|
-
# Renders a
|
|
83
|
+
# Renders a single entry as markdown text.
|
|
92
84
|
#
|
|
93
|
-
# @param [
|
|
85
|
+
# @param [StandupMD::Entry] entry
|
|
94
86
|
#
|
|
95
87
|
# @return [String]
|
|
96
|
-
def
|
|
97
|
-
|
|
88
|
+
def render_entry(entry)
|
|
89
|
+
lines = [entry_header(entry)]
|
|
90
|
+
config.sub_header_order.each do |type|
|
|
91
|
+
section = Section.new(type, entry.public_send("#{type}_tasks"))
|
|
92
|
+
next if section.empty?
|
|
93
|
+
|
|
94
|
+
lines << section_header(type)
|
|
95
|
+
section.tasks.each { |task| lines << task_line(task) }
|
|
96
|
+
end
|
|
97
|
+
lines << ""
|
|
98
|
+
lines.join("\n") + "\n"
|
|
98
99
|
end
|
|
99
100
|
|
|
100
101
|
private
|
|
@@ -123,11 +124,19 @@ module StandupMD
|
|
|
123
124
|
Section.new(type)
|
|
124
125
|
end
|
|
125
126
|
|
|
127
|
+
def entry_header(entry)
|
|
128
|
+
"#{"#" * config.header_depth} #{entry.date.strftime(config.header_date_format)}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def section_header(type)
|
|
132
|
+
"#{"#" * config.sub_header_depth} #{config.public_send("#{type}_header")}"
|
|
133
|
+
end
|
|
134
|
+
|
|
126
135
|
def section_type(line)
|
|
127
136
|
sub_header = line.sub(/^\#{#{config.sub_header_depth}}\s*/, "")
|
|
128
137
|
Entry::SECTION_TYPES.each do |type|
|
|
129
138
|
header = config.public_send("#{type}_header")
|
|
130
|
-
return type if sub_header
|
|
139
|
+
return type if sub_header == header
|
|
131
140
|
end
|
|
132
141
|
raise "Unrecognized header [#{sub_header}]"
|
|
133
142
|
end
|
|
@@ -146,6 +155,11 @@ module StandupMD
|
|
|
146
155
|
/\A(?<indent>\s*)#{Regexp.escape(config.bullet_character)}\s*(?<text>.*)\z/
|
|
147
156
|
end
|
|
148
157
|
|
|
158
|
+
def task_line(task)
|
|
159
|
+
indent = " " * config.indent_width * task.indent_level
|
|
160
|
+
"#{indent}#{config.bullet_character} #{task.text}"
|
|
161
|
+
end
|
|
162
|
+
|
|
149
163
|
def entry(record)
|
|
150
164
|
Entry.new(
|
|
151
165
|
Date.strptime(record[:title], config.header_date_format),
|
|
@@ -159,12 +173,6 @@ module StandupMD
|
|
|
159
173
|
def tasks(record, type)
|
|
160
174
|
record[:sections].fetch(type, Section.new(type)).tasks
|
|
161
175
|
end
|
|
162
|
-
|
|
163
|
-
def build_task(task)
|
|
164
|
-
return task if task.is_a?(Task)
|
|
165
|
-
|
|
166
|
-
Task.new(task)
|
|
167
|
-
end
|
|
168
176
|
end
|
|
169
177
|
end
|
|
170
178
|
end
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StandupMD
|
|
4
|
+
module Post
|
|
5
|
+
##
|
|
6
|
+
# Base class for chat posting adapters.
|
|
7
|
+
class Adapter
|
|
8
|
+
##
|
|
9
|
+
# Adapter-specific non-secret options.
|
|
10
|
+
#
|
|
11
|
+
# @return [Hash]
|
|
12
|
+
attr_reader :options
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# Creates an adapter.
|
|
16
|
+
#
|
|
17
|
+
# @param options [Hash]
|
|
18
|
+
def initialize(options = {})
|
|
19
|
+
@options = symbolize_keys(options)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
##
|
|
23
|
+
# Sends a message.
|
|
24
|
+
#
|
|
25
|
+
# @param message [StandupMD::Post::Message]
|
|
26
|
+
#
|
|
27
|
+
# @return [StandupMD::Post::Result]
|
|
28
|
+
def post(message)
|
|
29
|
+
raise NotImplementedError, "#{self.class} must implement #post"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
def symbolize_keys(hash)
|
|
35
|
+
hash.each_with_object({}) do |(key, value), result|
|
|
36
|
+
result[key.to_sym] = value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "net/http"
|
|
5
|
+
require "uri"
|
|
6
|
+
require "standup_md/post/adapter"
|
|
7
|
+
require "standup_md/post/result"
|
|
8
|
+
|
|
9
|
+
module StandupMD
|
|
10
|
+
module Post
|
|
11
|
+
##
|
|
12
|
+
# Namespace for built-in posting adapters.
|
|
13
|
+
module Adapters
|
|
14
|
+
##
|
|
15
|
+
# Posts standup entries to Slack using the chat.postMessage Web API.
|
|
16
|
+
class Slack < StandupMD::Post::Adapter
|
|
17
|
+
##
|
|
18
|
+
# Slack chat.postMessage endpoint.
|
|
19
|
+
#
|
|
20
|
+
# @return [String]
|
|
21
|
+
DEFAULT_ENDPOINT = "https://slack.com/api/chat.postMessage"
|
|
22
|
+
|
|
23
|
+
##
|
|
24
|
+
# Environment variable used for the Slack token by default.
|
|
25
|
+
#
|
|
26
|
+
# @return [String]
|
|
27
|
+
DEFAULT_TOKEN_ENV = "STANDUP_MD_SLACK_TOKEN"
|
|
28
|
+
|
|
29
|
+
##
|
|
30
|
+
# Sends a message to Slack.
|
|
31
|
+
#
|
|
32
|
+
# @param message [StandupMD::Post::Message]
|
|
33
|
+
#
|
|
34
|
+
# @return [StandupMD::Post::Result]
|
|
35
|
+
def post(message)
|
|
36
|
+
channel = message.channel || options[:channel]
|
|
37
|
+
token = ENV[token_env]
|
|
38
|
+
return failure(message, channel, "No Slack channel configured") if blank?(channel)
|
|
39
|
+
return failure(message, channel, "Missing Slack token in $#{token_env}") if blank?(token)
|
|
40
|
+
|
|
41
|
+
response = perform_request(channel, message.text, token)
|
|
42
|
+
parsed = parse_response(response.body)
|
|
43
|
+
return success(message, channel, response, parsed) if response.is_a?(Net::HTTPSuccess) && parsed["ok"]
|
|
44
|
+
|
|
45
|
+
failure(message, channel, error_message(response, parsed), parsed)
|
|
46
|
+
rescue => e
|
|
47
|
+
failure(message, channel, e.message)
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
private
|
|
51
|
+
|
|
52
|
+
def endpoint
|
|
53
|
+
options[:endpoint] || DEFAULT_ENDPOINT
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def token_env
|
|
57
|
+
options[:token_env] || DEFAULT_TOKEN_ENV
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def perform_request(channel, text, token)
|
|
61
|
+
uri = URI(endpoint)
|
|
62
|
+
request = Net::HTTP::Post.new(uri)
|
|
63
|
+
request["Authorization"] = "Bearer #{token}"
|
|
64
|
+
request["Content-Type"] = "application/json; charset=utf-8"
|
|
65
|
+
request["Accept"] = "application/json"
|
|
66
|
+
request.body = JSON.generate(channel: channel, text: text)
|
|
67
|
+
|
|
68
|
+
return options[:http].call(request, uri) if options[:http]
|
|
69
|
+
|
|
70
|
+
Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") do |http|
|
|
71
|
+
http.request(request)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def parse_response(body)
|
|
76
|
+
JSON.parse(body.to_s)
|
|
77
|
+
rescue JSON::ParserError
|
|
78
|
+
{}
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def success(message, channel, http_response, parsed)
|
|
82
|
+
StandupMD::Post::Result.success(
|
|
83
|
+
adapter: message.adapter,
|
|
84
|
+
channel: channel,
|
|
85
|
+
response: parsed.merge("code" => http_response.code)
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def failure(message, channel, error, response = {})
|
|
90
|
+
StandupMD::Post::Result.failure(
|
|
91
|
+
adapter: message.adapter,
|
|
92
|
+
channel: channel,
|
|
93
|
+
error: error,
|
|
94
|
+
response: response
|
|
95
|
+
)
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def error_message(http_response, parsed)
|
|
99
|
+
parsed["error"] || "Slack returned HTTP #{http_response.code}"
|
|
100
|
+
end
|
|
101
|
+
|
|
102
|
+
def blank?(value)
|
|
103
|
+
value.nil? || value.to_s.empty?
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
end
|
|
108
|
+
end
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StandupMD
|
|
4
|
+
module Post
|
|
5
|
+
##
|
|
6
|
+
# A platform-neutral message to send through a posting adapter.
|
|
7
|
+
class Message
|
|
8
|
+
##
|
|
9
|
+
# The standup entry being posted.
|
|
10
|
+
#
|
|
11
|
+
# @return [StandupMD::Entry]
|
|
12
|
+
attr_reader :entry
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# The rendered message body.
|
|
16
|
+
#
|
|
17
|
+
# @return [String]
|
|
18
|
+
attr_reader :text
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# The destination channel, room, or conversation identifier.
|
|
22
|
+
#
|
|
23
|
+
# @return [String, nil]
|
|
24
|
+
attr_reader :channel
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# The adapter name requested by the caller.
|
|
28
|
+
#
|
|
29
|
+
# @return [Symbol]
|
|
30
|
+
attr_reader :adapter
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Builds a message for a posting adapter.
|
|
34
|
+
#
|
|
35
|
+
# @param entry [StandupMD::Entry]
|
|
36
|
+
# @param text [String]
|
|
37
|
+
# @param channel [String, nil]
|
|
38
|
+
# @param adapter [String, Symbol]
|
|
39
|
+
def initialize(entry:, text:, channel:, adapter:)
|
|
40
|
+
@entry = entry
|
|
41
|
+
@text = text.to_s
|
|
42
|
+
@channel = channel
|
|
43
|
+
@adapter = adapter.to_sym
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module StandupMD
|
|
4
|
+
module Post
|
|
5
|
+
##
|
|
6
|
+
# Result returned by posting adapters.
|
|
7
|
+
class Result
|
|
8
|
+
##
|
|
9
|
+
# The adapter that handled the post.
|
|
10
|
+
#
|
|
11
|
+
# @return [Symbol]
|
|
12
|
+
attr_reader :adapter
|
|
13
|
+
|
|
14
|
+
##
|
|
15
|
+
# The destination channel, room, or conversation identifier.
|
|
16
|
+
#
|
|
17
|
+
# @return [String, nil]
|
|
18
|
+
attr_reader :channel
|
|
19
|
+
|
|
20
|
+
##
|
|
21
|
+
# Adapter-specific response metadata.
|
|
22
|
+
#
|
|
23
|
+
# @return [Hash]
|
|
24
|
+
attr_reader :response
|
|
25
|
+
|
|
26
|
+
##
|
|
27
|
+
# Human-readable error message for failed posts.
|
|
28
|
+
#
|
|
29
|
+
# @return [String, nil]
|
|
30
|
+
attr_reader :error
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Builds a posting result.
|
|
34
|
+
#
|
|
35
|
+
# @param success [Boolean]
|
|
36
|
+
# @param adapter [String, Symbol]
|
|
37
|
+
# @param channel [String, nil]
|
|
38
|
+
# @param response [Hash]
|
|
39
|
+
# @param error [String, nil]
|
|
40
|
+
def initialize(success:, adapter:, channel:, response: {}, error: nil)
|
|
41
|
+
@success = success
|
|
42
|
+
@adapter = adapter.to_sym
|
|
43
|
+
@channel = channel
|
|
44
|
+
@response = response
|
|
45
|
+
@error = error
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
##
|
|
49
|
+
# Builds a successful result.
|
|
50
|
+
#
|
|
51
|
+
# @return [StandupMD::Post::Result]
|
|
52
|
+
def self.success(adapter:, channel:, response: {})
|
|
53
|
+
new(success: true, adapter: adapter, channel: channel, response: response)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
##
|
|
57
|
+
# Builds a failed result.
|
|
58
|
+
#
|
|
59
|
+
# @return [StandupMD::Post::Result]
|
|
60
|
+
def self.failure(adapter:, channel:, error:, response: {})
|
|
61
|
+
new(
|
|
62
|
+
success: false,
|
|
63
|
+
adapter: adapter,
|
|
64
|
+
channel: channel,
|
|
65
|
+
response: response,
|
|
66
|
+
error: error
|
|
67
|
+
)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
##
|
|
71
|
+
# Was the post successful?
|
|
72
|
+
#
|
|
73
|
+
# @return [Boolean]
|
|
74
|
+
def success?
|
|
75
|
+
@success
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
##
|
|
79
|
+
# Did the post fail?
|
|
80
|
+
#
|
|
81
|
+
# @return [Boolean]
|
|
82
|
+
def failure?
|
|
83
|
+
!success?
|
|
84
|
+
end
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
end
|