bns 0.1.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.
Files changed (39) hide show
  1. checksums.yaml +7 -0
  2. data/.rspec +3 -0
  3. data/.rubocop.yml +14 -0
  4. data/CHANGELOG.md +11 -0
  5. data/CODE_OF_CONDUCT.md +132 -0
  6. data/CONTRIBUTING.md +66 -0
  7. data/Gemfile +18 -0
  8. data/Gemfile.lock +91 -0
  9. data/LICENSE +21 -0
  10. data/README.md +198 -0
  11. data/Rakefile +12 -0
  12. data/lib/bns/dispatcher/base.rb +31 -0
  13. data/lib/bns/dispatcher/discord/exceptions/invalid_webhook_token.rb +16 -0
  14. data/lib/bns/dispatcher/discord/implementation.rb +51 -0
  15. data/lib/bns/dispatcher/discord/types/response.rb +22 -0
  16. data/lib/bns/domain/birthday.rb +23 -0
  17. data/lib/bns/domain/exceptions/function_not_implemented.rb +18 -0
  18. data/lib/bns/domain/pto.rb +26 -0
  19. data/lib/bns/fetcher/base.rb +30 -0
  20. data/lib/bns/fetcher/notion/birthday.rb +53 -0
  21. data/lib/bns/fetcher/notion/exceptions/invalid_api_key.rb +15 -0
  22. data/lib/bns/fetcher/notion/exceptions/invalid_database_id.rb +15 -0
  23. data/lib/bns/fetcher/notion/helper.rb +21 -0
  24. data/lib/bns/fetcher/notion/pto.rb +48 -0
  25. data/lib/bns/fetcher/notion/types/response.rb +26 -0
  26. data/lib/bns/formatter/base.rb +29 -0
  27. data/lib/bns/formatter/discord/birthday.rb +43 -0
  28. data/lib/bns/formatter/discord/exceptions/invalid_data.rb +17 -0
  29. data/lib/bns/formatter/discord/pto.rb +52 -0
  30. data/lib/bns/mapper/base.rb +30 -0
  31. data/lib/bns/mapper/notion/birthday.rb +76 -0
  32. data/lib/bns/mapper/notion/pto.rb +96 -0
  33. data/lib/bns/use_cases/types/config.rb +19 -0
  34. data/lib/bns/use_cases/use_case.rb +39 -0
  35. data/lib/bns/use_cases/use_cases.rb +150 -0
  36. data/lib/bns/version.rb +6 -0
  37. data/lib/bns.rb +9 -0
  38. data/sig/business_notification_system.rbs +4 -0
  39. metadata +84 -0
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../base"
4
+ require_relative "./exceptions/invalid_webhook_token"
5
+ require_relative "./types/response"
6
+
7
+ module Dispatcher
8
+ module Discord
9
+ ##
10
+ # This class is an implementation of the Dispatcher::Base interface, specifically designed
11
+ # for dispatching messages to Discord.
12
+ #
13
+ class Implementation < Base
14
+ # Implements the dispatching logic for the Discord use case. It sends a POST request to
15
+ # the Discord webhook with the specified payload.
16
+ #
17
+ # <br>
18
+ # <b>Params:</b>
19
+ # * <tt>String</tt> payload: Payload to be dispatched to discord.
20
+ # <br>
21
+ # <b>raises</b> <tt>Exceptions::Discord::InvalidWebookToken</tt> if the provided webhook token is invalid.
22
+ #
23
+ # <br>
24
+ # <b>returns</b> <tt>Dispatcher::Discord::Types::Response</tt>
25
+ #
26
+ def dispatch(payload)
27
+ body = {
28
+ username: name,
29
+ avatar_url: "",
30
+ content: payload
31
+ }.to_json
32
+ response = HTTParty.post(webhook, { body: body, headers: { "Content-Type" => "application/json" } })
33
+
34
+ discord_response = Dispatcher::Discord::Types::Response.new(response)
35
+
36
+ validate_response(discord_response)
37
+ end
38
+
39
+ private
40
+
41
+ def validate_response(response)
42
+ case response.code
43
+ when 50_027
44
+ raise Exceptions::Discord::InvalidWebookToken, response.message
45
+ else
46
+ response
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Dispatcher
4
+ module Discord
5
+ module Types
6
+ ##
7
+ # Represents a response received from Discord. It encapsulates essential information about the response,
8
+ # providing a structured way to handle and analyze Discord server responses.
9
+ #
10
+ class Response
11
+ attr_reader :code, :http_code, :message, :response
12
+
13
+ def initialize(response)
14
+ @http_code = response.code
15
+ @code = response["code"]
16
+ @message = response.message
17
+ @response = response.response
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domain
4
+ ##
5
+ # The Domain::Birthday class provides a domain-specific representation of a Birthday object.
6
+ # It encapsulates the individual's name and their birthdate, offering a structured way to
7
+ # handle and manipulate birthday information.
8
+ class Birthday
9
+ attr_reader :individual_name, :birth_date
10
+
11
+ # Initializes a Domain::Birthday instance with the specified individual name, and date of birth.
12
+ #
13
+ # <br>
14
+ # <b>Params:</b>
15
+ # * <tt>String</tt> individual_name Name of the individual
16
+ # * <tt>Date</tt> birth_date Birthdate from the individual
17
+ #
18
+ def initialize(individual_name, date)
19
+ @individual_name = individual_name
20
+ @birth_date = date
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domain
4
+ module Exceptions
5
+ ##
6
+ # Provides a domain-specific representation for errors that occur when a function has not been implemented yet.
7
+ # It inherits from StandardError # and allows developers to raise a specific exception when a required function
8
+ # remains unimplemented in a subclass.
9
+ #
10
+ class FunctionNotImplemented < StandardError
11
+ # Initializes the exception with an optional custom error message.
12
+ #
13
+ def initialize(message = "The function haven't been implemented yet.")
14
+ super(message)
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Domain
4
+ ##
5
+ # The Domain::Pto class provides a domain-specific representation of a Paid Time Off (PTO) object.
6
+ # It encapsulates information about an individual's time off, including the individual's name,
7
+ # the start date, and the end date of the time off period.
8
+ #
9
+ class Pto
10
+ attr_reader :individual_name, :start_date, :end_date
11
+
12
+ # Initializes a Domain::Pto instance with the specified individual name, start date, and end date.
13
+ #
14
+ # <br>
15
+ # <b>Params:</b>
16
+ # * <tt>String</tt> individual_name Name of the individual.
17
+ # * <tt>DateTime</tt> start_date Start day of the PTO.
18
+ # * <tt>String</tt> end_date End date of the PTO.
19
+ #
20
+ def initialize(individual_name, start_date, end_date)
21
+ @individual_name = individual_name
22
+ @start_date = start_date
23
+ @end_date = end_date
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/exceptions/function_not_implemented"
4
+
5
+ module Fetcher
6
+ ##
7
+ # The Fetcher::Base class serves as the foundation for implementing specific data fetchers within the Fetcher module.
8
+ # Operating as an interface, this class defines essential attributes and methods, providing a blueprint for creating
9
+ # custom fetchers tailored to different data sources.
10
+ #
11
+ class Base
12
+ attr_reader :config
13
+
14
+ # Initializes the fetcher with essential configuration parameters.
15
+ #
16
+ def initialize(config)
17
+ @config = config
18
+ end
19
+
20
+ # A method meant to fetch data from an specific source depending on the implementation.
21
+ # Must be overridden by subclasses, with specific logic based on the use case.
22
+ #
23
+ # <br>
24
+ # <b>raises</b> <tt>Domain::Exceptions::FunctionNotImplemented</tt> when missing implementation.
25
+ #
26
+ def fetch
27
+ raise Domain::Exceptions::FunctionNotImplemented
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "date"
5
+
6
+ require_relative "../base"
7
+ require_relative "./exceptions/invalid_api_key"
8
+ require_relative "./exceptions/invalid_database_id"
9
+ require_relative "./types/response"
10
+ require_relative "./helper"
11
+
12
+ module Fetcher
13
+ module Notion
14
+ ##
15
+ # This class is an implementation of the Fetcher::Base interface, specifically designed
16
+ # for fetching birthday data from Notion.
17
+ #
18
+ class Birthday < Base
19
+ # Implements the data fetching logic for Birthdays data from Notion. It sends a POST
20
+ # request to the Notion API to query the specified database and returns a validated response.
21
+ #
22
+ # <br>
23
+ # <b>raises</b> <tt>Exceptions::Notion::InvalidApiKey</tt> if the API key provided is incorrect or invalid.
24
+ #
25
+ # <b>raises</b> <tt>Exceptions::Notion::InvalidDatabaseId</tt> if the Database id provided is incorrect
26
+ # or invalid.
27
+ #
28
+ def fetch
29
+ url = build_url(config[:base_url], config[:database_id])
30
+
31
+ httparty_response = HTTParty.post(url, { body: config[:filter].to_json, headers: headers })
32
+
33
+ notion_response = Fetcher::Notion::Types::Response.new(httparty_response)
34
+
35
+ Fetcher::Notion::Helper.validate_response(notion_response)
36
+ end
37
+
38
+ private
39
+
40
+ def headers
41
+ {
42
+ "Authorization" => "Bearer #{config[:secret]}",
43
+ "Content-Type" => "application/json",
44
+ "Notion-Version" => "2022-06-28"
45
+ }
46
+ end
47
+
48
+ def build_url(base, database_id)
49
+ "#{base}/v1/databases/#{database_id}/query"
50
+ end
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exceptions
4
+ module Notion
5
+ ##
6
+ # Provides a domain-specific representation for errors that occurs when an invalid API key is provided
7
+ # for a Notion-related operation.
8
+ #
9
+ class InvalidApiKey < StandardError
10
+ def initialize(message = "The provided API token is invalid.")
11
+ super(message)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Exceptions
4
+ module Notion
5
+ ##
6
+ # Provides a domain-specific representation for errors that occurs when an invalid database id is provided
7
+ # for a Notion-related operation.
8
+ #
9
+ class InvalidDatabaseId < StandardError
10
+ def initialize(message = "The provided id doesn't match any database.")
11
+ super(message)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fetcher
4
+ module Notion
5
+ ##
6
+ # Provides common fuctionalities along the Notion domain.
7
+ #
8
+ module Helper
9
+ def self.validate_response(response)
10
+ case response.status_code
11
+ when 401
12
+ raise Exceptions::Notion::InvalidApiKey, response.message
13
+ when 404
14
+ raise Exceptions::Notion::InvalidDatabaseId, response.message
15
+ else
16
+ response
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "httparty"
4
+ require "date"
5
+
6
+ require_relative "../base"
7
+ require_relative "./exceptions/invalid_api_key"
8
+ require_relative "./exceptions/invalid_database_id"
9
+ require_relative "./types/response"
10
+
11
+ module Fetcher
12
+ module Notion
13
+ ##
14
+ # This class is an implementation of the Fetcher::Base interface, specifically designed
15
+ # for fetching Paid Time Off (PTO) data from Notion.
16
+ #
17
+ class Pto < Base
18
+ # Implements the data fetching logic for PTO's data from Notion. It sends a POST
19
+ # request to the Notion API to query the specified database and returns a validated response.
20
+ #
21
+ # <br>
22
+ # <b>raises</b> <tt>Exceptions::Notion::InvalidApiKey</tt> if the API key provided is incorrect or invalid.
23
+ #
24
+ # <b>raises</b> <tt>Exceptions::Notion::InvalidDatabaseId</tt> if the Database id provided is incorrect
25
+ # or invalid.
26
+ #
27
+ def fetch
28
+ url = "#{config[:base_url]}/v1/databases/#{config[:database_id]}/query"
29
+
30
+ httparty_response = HTTParty.post(url, { body: config[:filter].to_json, headers: headers })
31
+
32
+ notion_response = Fetcher::Notion::Types::Response.new(httparty_response)
33
+
34
+ Fetcher::Notion::Helper.validate_response(notion_response)
35
+ end
36
+
37
+ private
38
+
39
+ def headers
40
+ {
41
+ "Authorization" => "Bearer #{config[:secret]}",
42
+ "Content-Type" => "application/json",
43
+ "Notion-Version" => "2022-06-28"
44
+ }
45
+ end
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Fetcher
4
+ module Notion
5
+ module Types
6
+ ##
7
+ # Represents a response received from the Notion API. It encapsulates essential information about the response,
8
+ # providing a structured way to handle and analyze it's responses.
9
+ class Response
10
+ attr_reader :status_code, :message, :results
11
+
12
+ def initialize(response)
13
+ if response["results"].nil?
14
+ @status_code = response["status"]
15
+ @message = response["message"]
16
+ @results = nil
17
+ else
18
+ @status_code = 200
19
+ @message = "success"
20
+ @results = response["results"]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/exceptions/function_not_implemented"
4
+
5
+ module Formatter
6
+ ##
7
+ # The Formatter::Base module serves as the foundation for implementing specific data presentation logic
8
+ # within the Formatter module. Defines essential methods, that provide a blueprint for creating custom
9
+ # formatters tailored to different use cases.
10
+ #
11
+ module Base
12
+ # A method meant to give an specified format depending on the implementation to the data coming from an
13
+ # implementation of the Mapper::Base interface.
14
+ # Must be overridden by subclasses, with specific logic based on the use case.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>List<Domain::></tt> domain_data: List of specific domain objects depending on the use case.
19
+ #
20
+ # <br>
21
+ # <b>raises</b> <tt>Domain::Exceptions::FunctionNotImplemented</tt> when missing implementation.
22
+ #
23
+ # <b>returns</b> <tt>String</tt> Formatted payload suitable for a Dispatcher::Base implementation.
24
+ #
25
+ def format(_domain_data)
26
+ raise Domain::Exceptions::FunctionNotImplemented
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,43 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/birthday"
4
+ require_relative "../base"
5
+ require_relative "./exceptions/invalid_data"
6
+
7
+ module Formatter
8
+ module Discord
9
+ ##
10
+ # This class implementats the methods of the Formatter::Base module, specifically designed for formatting birthday
11
+ # data in a way suitable for Discord messages.
12
+ class Birthday
13
+ include Base
14
+
15
+ # Implements the logic for building a formatted payload with the given template for birthdays.
16
+ #
17
+ # <br>
18
+ # <b>Params:</b>
19
+ # * <tt>List<Domain::Birthday></tt> birthdays_list: list of mapped birthdays.
20
+ #
21
+ # <br>
22
+ # <b>raises</b> <tt>Formatter::Discord::Exceptions::InvalidData</tt> when invalid data is provided.
23
+ #
24
+ # <br>
25
+ # <b>returns</b> <tt>String</tt> payload: formatted payload suitable for a Discord message.
26
+ #
27
+ def format(birthdays_list)
28
+ raise Formatter::Discord::Exceptions::InvalidData unless birthdays_list.all? do |brithday|
29
+ brithday.is_a?(Domain::Birthday)
30
+ end
31
+
32
+ template = "NAME, Wishing you a very happy birthday! Enjoy your special day! :birthday: :gift:"
33
+ payload = ""
34
+
35
+ birthdays_list.each do |birthday|
36
+ payload += "#{template.gsub("NAME", birthday.individual_name)}\n"
37
+ end
38
+
39
+ payload
40
+ end
41
+ end
42
+ end
43
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Formatter
4
+ module Discord
5
+ module Exceptions
6
+ ##
7
+ # Provides a domain-specific representation for errors that occurs when trying to process invalid
8
+ # data on a Fetcher::Base implementation
9
+ #
10
+ class InvalidData < StandardError
11
+ def initialize(message = "")
12
+ super(message)
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,52 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/pto"
4
+ require_relative "../base"
5
+
6
+ module Formatter
7
+ module Discord
8
+ ##
9
+ # This class is an implementation of the Formatter::Base interface, specifically designed for formatting PTO
10
+ # data in a way suitable for Discord messages.
11
+ class Pto
12
+ include Base
13
+
14
+ # Implements the logic for building a formatted payload with the given template for PTO's.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>List<Domain::Pto></tt> pto_list: List of mapped PTO's.
19
+ #
20
+ # <br>
21
+ # <b>raises</b> <tt>Formatter::Discord::Exceptions::InvalidData</tt> when invalid data is provided.
22
+ #
23
+ # <br>
24
+ # <b>returns</b> <tt>String</tt> payload, formatted payload suitable for a Discord message.
25
+ #
26
+ def format(ptos_list)
27
+ raise Formatter::Discord::Exceptions::InvalidData unless ptos_list.all? { |pto| pto.is_a?(Domain::Pto) }
28
+
29
+ template = ":beach: NAME is on PTO"
30
+ payload = ""
31
+
32
+ ptos_list.each do |pto|
33
+ payload += "#{template.gsub("NAME", pto.individual_name)} #{build_pto_message(pto)}\n"
34
+ end
35
+
36
+ payload
37
+ end
38
+
39
+ private
40
+
41
+ def build_pto_message(pto)
42
+ if pto.start_date.include?("|")
43
+ start_time = pto.start_date.split("|")
44
+ end_time = pto.end_date.split("|")
45
+ "#{start_time[1]} - #{end_time[1]}"
46
+ else
47
+ "all day"
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../domain/exceptions/function_not_implemented"
4
+
5
+ module Mapper
6
+ ##
7
+ # The Mapper::Base module serves as the foundation for implementing specific data shaping logic within the
8
+ # Mapper module. Defines essential methods, that provide a blueprint for organizing or shaping data in a manner
9
+ # suitable for downstream formatting processes.
10
+ #
11
+ module Base
12
+ # An method meant to prepare or organize the data coming from an implementation of the Fetcher::Base interface.
13
+ # Must be overridden by subclasses, with specific logic based on the use case.
14
+ #
15
+ # <br>
16
+ # <b>Params:</b>
17
+ # * <tt>Fetcher::Notion::Types::Response</tt> response: Response produced by a fetcher.
18
+ #
19
+ # <br>
20
+ #
21
+ # <b>raises</b> <tt>Domain::Exceptions::FunctionNotImplemented</tt> when missing implementation.
22
+ # <br>
23
+ #
24
+ # <b>returns</b> <tt>List<Domain::></tt> Mapped list of data, ready to be formatted.
25
+ #
26
+ def map(_response)
27
+ raise Domain::Exceptions::FunctionNotImplemented
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../../domain/birthday"
4
+ require_relative "../base"
5
+
6
+ module Mapper
7
+ module Notion
8
+ ##
9
+ # This class implementats the methods of the Mapper::Base module, specifically designed for preparing or
10
+ # shaping birthdays data coming from a Fetcher::Base implementation.
11
+ class Birthday
12
+ include Base
13
+
14
+ # Implements the logic for shaping the results from a fetcher response.
15
+ #
16
+ # <br>
17
+ # <b>Params:</b>
18
+ # * <tt>Fetcher::Notion::Types::Response</tt> notion_response: Notion response object.
19
+ #
20
+ # <br>
21
+ # <b>return</b> <tt>List<Domain::Birthday></tt> birthdays_list, mapped birthdays to be used by a
22
+ # Formatter::Base implementation.
23
+ #
24
+ def map(notion_response)
25
+ return [] if notion_response.results.empty?
26
+
27
+ normalized_notion_data = normalize_response(notion_response.results)
28
+
29
+ normalized_notion_data.map do |birthday|
30
+ Domain::Birthday.new(birthday["name"], birthday["birth_date"])
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ def normalize_response(results)
37
+ return [] if results.nil?
38
+
39
+ normalized_results = []
40
+
41
+ results.map do |value|
42
+ properties = value["properties"]
43
+ properties.delete("Name")
44
+
45
+ normalized_value = normalize(properties)
46
+
47
+ normalized_results.append(normalized_value)
48
+ end
49
+
50
+ normalized_results
51
+ end
52
+
53
+ def normalize(properties)
54
+ normalized_value = {}
55
+
56
+ properties.each do |k, v|
57
+ if k == "Complete Name"
58
+ normalized_value["name"] = extract_rich_text_field_value(v)
59
+ elsif k == "BD_this_year"
60
+ normalized_value["birth_date"] = extract_date_field_value(v)
61
+ end
62
+ end
63
+
64
+ normalized_value
65
+ end
66
+
67
+ def extract_rich_text_field_value(data)
68
+ data["rich_text"][0]["plain_text"]
69
+ end
70
+
71
+ def extract_date_field_value(data)
72
+ data["formula"]["date"]["start"]
73
+ end
74
+ end
75
+ end
76
+ end