standup_md 1.0.0 → 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.
@@ -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
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "standup_md/post/message"
4
+ require "standup_md/post/result"
5
+ require "standup_md/post/adapter"
6
+ require "standup_md/post/adapters/slack"
7
+ require "standup_md/parsers/markdown"
8
+
9
+ module StandupMD
10
+ ##
11
+ # Namespace for posting standup entries to chat clients.
12
+ module Post
13
+ ##
14
+ # Base error for posting failures.
15
+ class Error < StandardError; end
16
+
17
+ ##
18
+ # Raised when a configured adapter cannot be found.
19
+ class UnknownAdapter < Error; end
20
+
21
+ module_function
22
+
23
+ ##
24
+ # Renders and posts a standup entry through a configured chat adapter.
25
+ #
26
+ # @param entry [StandupMD::Entry]
27
+ # @param adapter [String, Symbol, nil]
28
+ # @param channel [String, nil]
29
+ # @param text [String, nil]
30
+ # @param renderer [Object, nil]
31
+ # @param config [StandupMD::Config]
32
+ #
33
+ # @return [StandupMD::Post::Result]
34
+ def post(entry, adapter: nil, channel: nil, text: nil, renderer: nil, config: StandupMD.config)
35
+ adapter_name = (adapter || config.post.default_adapter).to_sym
36
+ message = Message.new(
37
+ entry: entry,
38
+ text: text || render_post_text(entry, renderer, config),
39
+ channel: channel,
40
+ adapter: adapter_name
41
+ )
42
+ config.post.build_adapter(adapter_name).post(message)
43
+ end
44
+
45
+ ##
46
+ # Default renderer used when posting an entry.
47
+ #
48
+ # @param config [StandupMD::Config]
49
+ #
50
+ # @return [StandupMD::Parsers::Markdown]
51
+ def default_renderer(config = StandupMD.config)
52
+ StandupMD::Parsers::Markdown.new(config.file)
53
+ end
54
+
55
+ ##
56
+ # Renders text for a posted entry.
57
+ #
58
+ # @param entry [StandupMD::Entry]
59
+ # @param renderer [Object, nil]
60
+ # @param config [StandupMD::Config]
61
+ #
62
+ # @return [String]
63
+ def render_post_text(entry, renderer, config)
64
+ text = (renderer || default_renderer(config)).render_entry(entry)
65
+ apply_post_title(text, entry, config)
66
+ end
67
+
68
+ ##
69
+ # Applies the configured post title format to the first entry header.
70
+ #
71
+ # @param text [String]
72
+ # @param entry [StandupMD::Entry]
73
+ # @param config [StandupMD::Config]
74
+ #
75
+ # @return [String]
76
+ def apply_post_title(text, entry, config)
77
+ return text unless config.post.title
78
+
79
+ title = config.post.title % entry.date.strftime(config.file.header_date_format)
80
+ header = "#" * config.file.header_depth
81
+ text.sub(/\A#{Regexp.escape(header)}\s+.*$/, "#{header} #{title}")
82
+ end
83
+ end
84
+ end
@@ -48,22 +48,11 @@ module StandupMD
48
48
  end
49
49
 
50
50
  ##
51
- # The configured section heading.
51
+ # The semantic section type.
52
52
  #
53
53
  # @return [String]
54
54
  def to_s
55
- StandupMD.config.file.public_send("#{type}_header")
56
- end
57
-
58
- ##
59
- # The section rendered as markdown lines.
60
- #
61
- # @return [Array<String>]
62
- def to_markdown
63
- [
64
- "#" * StandupMD.config.file.sub_header_depth + " " + to_s,
65
- *tasks.map(&:to_markdown)
66
- ]
55
+ type.to_s
67
56
  end
68
57
 
69
58
  private
@@ -39,15 +39,6 @@ module StandupMD
39
39
  text
40
40
  end
41
41
 
42
- ##
43
- # The task rendered as a markdown list item.
44
- #
45
- # @return [String]
46
- def to_markdown
47
- indent = " " * StandupMD.config.file.indent_width * indent_level
48
- "#{indent}#{StandupMD.config.file.bullet_character} #{text}"
49
- end
50
-
51
42
  ##
52
43
  # Compares task contents.
53
44
  def ==(other)
@@ -9,7 +9,7 @@ module StandupMD
9
9
  # Major version.
10
10
  #
11
11
  # @return [Integer]
12
- MAJOR = 1
12
+ MAJOR = 2
13
13
 
14
14
  ##
15
15
  # Minor version.
@@ -48,5 +48,9 @@ module StandupMD
48
48
  end
49
49
  end
50
50
 
51
+ ##
52
+ # Full gem version string.
53
+ #
54
+ # @return [String]
51
55
  VERSION = StandupMD::Version.to_s
52
56
  end
data/lib/standup_md.rb CHANGED
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "standup_md/version"
4
+ require "standup_md/post"
4
5
  require "standup_md/config"
5
6
  require "standup_md/task"
6
- require "standup_md/title"
7
7
  require "standup_md/section"
8
8
  require "standup_md/entry"
9
9
  require "standup_md/entry_list"
@@ -21,7 +21,7 @@ module StandupMD
21
21
  ##
22
22
  # Method for accessing the configuration.
23
23
  #
24
- # @return [StanupMD::Config]
24
+ # @return [StandupMD::Config]
25
25
  def config
26
26
  @config || reset_config
27
27
  end
data/standup_md.gemspec CHANGED
@@ -10,6 +10,7 @@ Gem::Specification.new do |spec|
10
10
  spec.summary = %(The cure for all your standup woes)
11
11
  spec.description = %(Generate and edit standups in markdown format)
12
12
  spec.homepage = "https://evanthegrayt.github.io/standup_md/"
13
+ spec.required_ruby_version = ">= 3.2"
13
14
 
14
15
  unless spec.respond_to?(:metadata)
15
16
  raise "RubyGems 2.0 or newer is required to protect against " \
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: standup_md
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 2.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Evan Gray
@@ -114,15 +114,19 @@ files:
114
114
  - lib/standup_md/config.rb
115
115
  - lib/standup_md/config/cli.rb
116
116
  - lib/standup_md/config/entry.rb
117
- - lib/standup_md/config/entry_list.rb
118
117
  - lib/standup_md/config/file.rb
118
+ - lib/standup_md/config/post.rb
119
119
  - lib/standup_md/entry.rb
120
120
  - lib/standup_md/entry_list.rb
121
121
  - lib/standup_md/file.rb
122
122
  - lib/standup_md/parsers/markdown.rb
123
+ - lib/standup_md/post.rb
124
+ - lib/standup_md/post/adapter.rb
125
+ - lib/standup_md/post/adapters/slack.rb
126
+ - lib/standup_md/post/message.rb
127
+ - lib/standup_md/post/result.rb
123
128
  - lib/standup_md/section.rb
124
129
  - lib/standup_md/task.rb
125
- - lib/standup_md/title.rb
126
130
  - lib/standup_md/version.rb
127
131
  - standup_md.gemspec
128
132
  homepage: https://evanthegrayt.github.io/standup_md/
@@ -140,7 +144,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
140
144
  requirements:
141
145
  - - ">="
142
146
  - !ruby/object:Gem::Version
143
- version: '0'
147
+ version: '3.2'
144
148
  required_rubygems_version: !ruby/object:Gem::Requirement
145
149
  requirements:
146
150
  - - ">="
@@ -1,29 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module StandupMD
4
- class Config
5
- ##
6
- # The configuration class for StandupMD::EntryList
7
- class EntryList
8
- ##
9
- # The default options.
10
- #
11
- # @return [Hash]
12
- DEFAULTS = {}.freeze
13
-
14
- ##
15
- # Initializes the config with default values.
16
- def initalize
17
- reset
18
- end
19
-
20
- ##
21
- # Sets all config values back to their defaults.
22
- #
23
- # @return [Hash]
24
- def reset
25
- DEFAULTS.each { |k, v| instance_variable_set("@#{k}", v) }
26
- end
27
- end
28
- end
29
- end
@@ -1,41 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "date"
4
-
5
- module StandupMD
6
- ##
7
- # The title for a standup entry.
8
- class Title
9
- ##
10
- # The entry date.
11
- #
12
- # @return [Date]
13
- attr_reader :date
14
-
15
- ##
16
- # Constructs an instance of +StandupMD::Title+.
17
- #
18
- # @param [Date] date
19
- def initialize(date)
20
- raise ArgumentError, "Must be a Date object" unless date.is_a?(Date)
21
-
22
- @date = date
23
- end
24
-
25
- ##
26
- # The configured title text.
27
- #
28
- # @return [String]
29
- def to_s
30
- date.strftime(StandupMD.config.file.header_date_format)
31
- end
32
-
33
- ##
34
- # The title rendered as markdown.
35
- #
36
- # @return [String]
37
- def to_markdown
38
- "#" * StandupMD.config.file.header_depth + " " + to_s
39
- end
40
- end
41
- end