bns 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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