macaw_framework 1.3.0 → 1.3.21

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1488af601dcf17bdfabaad9407c4167ba3cfc9bc3913fc6e51a1905e575c905f
4
- data.tar.gz: d2082dadd31530c82ce830b475299c8d4df17b9d600cf4b2e77a9257e3afb045
3
+ metadata.gz: 85d0826c8f583c5485bbd7d9be3868a407a065a8a7968efb1c2e6d60c100d79d
4
+ data.tar.gz: 7b1a1c9ab29d649ccf05e077ac347a7e1cbb9714bb65aea9b417a2139dca5a6a
5
5
  SHA512:
6
- metadata.gz: e218e790fd300482efcdabb79149819859b5500f4a7e12d511ffecf4d9ca678731045d0defa46cc13893c0fd1367c25104ff10d34ceb73d42ee35ae61c6c32a5
7
- data.tar.gz: 151ffbeecb5789abcabfc52af53228ecae894f45717ad8e257bb8ddbd193e1b61ce1458ce107c5c3e300746a37a63f9715d4d87d856ee61a469ed15d3b99c89d
6
+ metadata.gz: 7f04277f705c63685e6886e148f092694b445b4896b383783ea4cadfc1b266c158380af777a75dc791405a2ac2bd462d75a613c361e19da8e67ce284c134ddf9
7
+ data.tar.gz: e0dcbe81cbb9a26b289819e051d21b65d63f64ece3403da0e1c16aefadad1ac0e6a1e744da661d48bba623afe0aecdbe186d2ef9c4fe94131e7aac8bf4537a58
data/.rubocop.yml CHANGED
@@ -1,16 +1,8 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.7
2
+ TargetRubyVersion: 3.0
3
3
  SuggestExtensions: false
4
4
  NewCops: disable
5
5
 
6
- Style/StringLiterals:
7
- Enabled: true
8
- EnforcedStyle: double_quotes
9
-
10
- Style/StringLiteralsInInterpolation:
11
- Enabled: true
12
- EnforcedStyle: double_quotes
13
-
14
6
  Layout/LineLength:
15
7
  Max: 120
16
8
 
@@ -31,3 +23,9 @@ Metrics/PerceivedComplexity:
31
23
 
32
24
  Metrics/ClassLength:
33
25
  Enabled: false
26
+
27
+ Metrics/ModuleLength:
28
+ Enabled: false
29
+
30
+ Naming/MemoizedInstanceVariableName:
31
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -143,3 +143,11 @@
143
143
  - Fixed a bug where errors were being logged with level INFO
144
144
  - Improved error stack trace
145
145
 
146
+ ## [1.3.1]
147
+ - Fixing bug where missing session configuration on `application.json` break the application
148
+ - Including a Cache module for manual caching.
149
+
150
+ ## [1.3.21]
151
+ - Refactoring shutdown method
152
+ - Fixing a bug where a HTTP call without client data broke the parser
153
+ - Removing logs registering new HTTP connections to reduce log bloat
data/Gemfile CHANGED
@@ -1,17 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- source "https://rubygems.org"
3
+ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
- gem "openssl"
8
- gem "prometheus-client", "~> 4.1"
7
+ gem 'openssl'
8
+ gem 'prometheus-client', '~> 4.1'
9
9
 
10
10
  group :test do
11
- gem "minitest", "~> 5.0"
12
- gem "rake", "~> 13.0"
13
- gem "rubocop", "~> 1.21"
14
- gem "simplecov", "~> 0.21.2"
15
- gem "simplecov-json"
16
- gem "simplecov_json_formatter", "~> 0.1.2"
11
+ gem 'minitest', '~> 5.0'
12
+ gem 'rake', '~> 13.0'
13
+ gem 'rubocop', '~> 1.21'
14
+ gem 'simplecov', '~> 0.21.2'
15
+ gem 'simplecov-json'
16
+ gem 'simplecov_json_formatter', '~> 0.1.2'
17
17
  end
data/README.md CHANGED
@@ -18,7 +18,7 @@ provides developers with the essential tools to quickly build and deploy their a
18
18
  + [Configuration: Customize various aspects of the framework through the application.json configuration file, such as rate limiting, SSL support, and Prometheus integration](#configuration-customize-various-aspects-of-the-framework-through-the-applicationjson-configuration-file-such-as-rate-limiting-ssl-support-and-prometheus-integration)
19
19
  + [Monitoring: Easily monitor your application performance and metrics with built-in Prometheus support](#monitoring-easily-monitor-your-application-performance-and-metrics-with-built-in-prometheus-support)
20
20
  + [Routing for "public" Folder: Serve Static Assets](#routing-for-public-folder-serve-static-assets)
21
- + [Cron Jobs](#cron-jobs)
21
+ + [Periodic Jobs](#periodic-jobs)
22
22
  + [Tips](#tips)
23
23
  * [Contributing](#contributing)
24
24
  * [License](#license)
@@ -51,7 +51,7 @@ We evaluated MacawFramework (Version 1.2.0) to assess its ability to handle simu
51
51
 
52
52
  MacawFramework is built to be highly compatible, since it uses only native Ruby code:
53
53
 
54
- - **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 2.7.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
54
+ - **MRI**: MacawFramework is compatible with Matz's Ruby Interpreter (MRI), version 3.0.0 and onwards. If you are using this version or a more recent one, you should not encounter any compatibility issues.
55
55
 
56
56
  - **TruffleRuby**: TruffleRuby is another Ruby interpreter that is fully compatible with MacawFramework. This provides developers with more flexibility in their choice of Ruby interpreter.
57
57
 
@@ -104,6 +104,8 @@ m.start!
104
104
  ### Caching: Improve performance by caching responses and configuring cache invalidation
105
105
 
106
106
  ```ruby
107
+ m = MacawFramework::Macaw.new
108
+
107
109
  m.get('/cached_data', cache: ["header_to_cache", "query_param_to_cache"]) do |context|
108
110
  # Retrieve data
109
111
  end
@@ -111,6 +113,17 @@ end
111
113
 
112
114
  *Observation: To activate caching, you also have to set its properties in the `application.json` file. If you don't, the caching strategy will not work. See the Configuration section below for more details.*
113
115
 
116
+ Another method of cache is the manual cache via the `MacawFramework::Cache` class. You can manually
117
+ call the `read` and `write` methods of this singleton to save and recover values inside your methods.
118
+
119
+ ```ruby
120
+ MacawFramework::Cache.write(:name, 'Maria', expires_in: 1800)
121
+ # Your code
122
+ MacawFramework::Cache.read(:name) # Maria
123
+ ```
124
+
125
+ Manual cache does not need any additional configuration.
126
+
114
127
  ### Session management: Handle user sessions with server-side in-memory storage
115
128
 
116
129
  Session will only be enabled if it's configurations exists in the `application.json` file.
@@ -123,6 +136,8 @@ a session id in the HTTP request. In the case of the client sending an ID of an
123
136
  the framework will return a new session with a new ID.
124
137
 
125
138
  ```ruby
139
+ m = MacawFramework::Macaw.new
140
+
126
141
  m.get('/login') do |context|
127
142
  # Authenticate user
128
143
  context[:client][:user_id] = user_id
@@ -195,17 +210,19 @@ be accessible at http://yourdomain.com/img/logo.png without any additional confi
195
210
 
196
211
  #### Caution: This is incompatible with most non-unix systems, such as Windows. If you are using a non-unix system, you will need to manually configure the "public" folder and use dir as nil to avoid problems.
197
212
 
198
- ### Cron Jobs
213
+ ### Periodic Jobs
199
214
 
200
- Macaw Framework supports the declaration of cron jobs right in your application code. This feature allows developers to
215
+ Macaw Framework supports the declaration of periodic jobs right in your application code. This feature allows developers to
201
216
  define tasks that run at set intervals, starting after an optional delay. Each job runs in a separate thread, meaning
202
- your cron jobs can execute in parallel without blocking the rest of your application.
217
+ your periodic jobs can execute in parallel without blocking the rest of your application.
203
218
 
204
- Here's an example of how to declare a cron job:
219
+ Here's an example of how to declare a periodic job:
205
220
 
206
221
  ```ruby
222
+ m = MacawFramework::Macaw.new
223
+
207
224
  m.setup_job(interval: 5, start_delay: 5, job_name: "cron job 1") do
208
- puts "i'm a cron job that runs every 5 secs!"
225
+ puts "i'm a periodic job that runs every 5 secs!"
209
226
  end
210
227
  ```
211
228
 
data/Rakefile CHANGED
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "bundler/gem_tasks"
4
- require "rake/testtask"
3
+ require 'bundler/gem_tasks'
4
+ require 'rake/testtask'
5
5
 
6
6
  Rake::TestTask.new(:test) do |t|
7
- t.libs << "test"
8
- t.libs << "lib"
9
- t.test_files = FileList["test/**/test_*.rb"]
7
+ t.libs << 'test'
8
+ t.libs << 'lib'
9
+ t.test_files = FileList['test/**/test_*.rb']
10
10
  end
11
11
 
12
- require "rubocop/rake_task"
12
+ require 'rubocop/rake_task'
13
13
 
14
14
  RuboCop::RakeTask.new
15
15
 
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: false
2
2
 
3
- require "logger"
4
- require_relative "../data_filters/log_data_filter"
3
+ require 'logger'
4
+ require_relative '../data_filters/log_data_filter'
5
5
 
6
6
  ##
7
7
  # This Aspect is responsible for logging
@@ -11,9 +11,6 @@ module LoggingAspect
11
11
  def call_endpoint(logger, *args)
12
12
  return super(*args) if logger.nil?
13
13
 
14
- endpoint_name = args[1].split(".")[1..].join("/")
15
- logger.info("Request received for [#{endpoint_name}] from [#{args[-1]}]")
16
-
17
14
  begin
18
15
  response = super(*args)
19
16
  rescue StandardError => e
@@ -13,7 +13,7 @@ module PrometheusAspect
13
13
  ensure
14
14
  duration = (Time.now - start_time) * 1_000
15
15
 
16
- endpoint_name = args[2].split(".").join("/")
16
+ endpoint_name = args[2].split('.').join('/')
17
17
 
18
18
  prometheus_middleware.request_duration_milliseconds.with_labels(endpoint: endpoint_name).observe(duration)
19
19
  prometheus_middleware.request_count.with_labels(endpoint: endpoint_name).increment
@@ -1,14 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../../middlewares/memory_invalidation_middleware"
4
- require_relative "../../middlewares/rate_limiter_middleware"
5
- require_relative "../../data_filters/response_data_filter"
6
- require_relative "../../errors/too_many_requests_error"
7
- require_relative "../../utils/supported_ssl_versions"
8
- require_relative "../../aspects/prometheus_aspect"
9
- require_relative "../../aspects/logging_aspect"
10
- require_relative "../../aspects/cache_aspect"
11
- require "securerandom"
3
+ require_relative '../../middlewares/memory_invalidation_middleware'
4
+ require_relative '../../middlewares/rate_limiter_middleware'
5
+ require_relative '../../data_filters/response_data_filter'
6
+ require_relative '../../errors/too_many_requests_error'
7
+ require_relative '../../utils/supported_ssl_versions'
8
+ require_relative '../../aspects/prometheus_aspect'
9
+ require_relative '../../aspects/logging_aspect'
10
+ require_relative '../../aspects/cache_aspect'
11
+ require 'securerandom'
12
12
 
13
13
  ##
14
14
  # Base module for Server classes. It contains
@@ -29,7 +29,7 @@ module ServerBase
29
29
  headers: client_data[:headers],
30
30
  body: client_data[:body],
31
31
  params: client_data[:params],
32
- client: @session[session_id][0]
32
+ client: @session&.dig(session_id)&.dig(0)
33
33
  }
34
34
  )
35
35
  end
@@ -39,14 +39,13 @@ module ServerBase
39
39
  end
40
40
 
41
41
  def handle_client(client)
42
- path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
42
+ _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
43
43
  raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
44
44
  raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3])
45
45
 
46
46
  client_data = get_client_data(body, headers, parameters)
47
47
  session_id = declare_client_session(client_data[:headers], @macaw.secure_header) if @macaw.session
48
48
 
49
- @macaw_log&.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
50
49
  message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
51
50
  method_name, client_data, session_id, client.peeraddr[3])
52
51
  response_headers ||= {}
@@ -80,44 +79,46 @@ module ServerBase
80
79
  end
81
80
 
82
81
  def set_rate_limiting
83
- return unless @macaw.config&.dig("macaw", "rate_limiting")
82
+ return unless @macaw.config&.dig('macaw', 'rate_limiting')
84
83
 
85
84
  @rate_limit = RateLimiterMiddleware.new(
86
- @macaw.config["macaw"]["rate_limiting"]["window"].to_i || 1,
87
- @macaw.config["macaw"]["rate_limiting"]["max_requests"].to_i || 60
85
+ @macaw.config['macaw']['rate_limiting']['window'].to_i || 1,
86
+ @macaw.config['macaw']['rate_limiting']['max_requests'].to_i || 60
88
87
  )
89
88
  end
90
89
 
91
90
  def set_ssl
92
- ssl_config = @macaw.config["macaw"]["ssl"] if @macaw.config&.dig("macaw", "ssl")
91
+ ssl_config = @macaw.config['macaw']['ssl'] if @macaw.config&.dig('macaw', 'ssl')
93
92
  ssl_config ||= nil
94
93
  unless ssl_config.nil?
95
- version_config = { min: ssl_config["min"], max: ssl_config["max"] }
94
+ version_config = { min: ssl_config['min'], max: ssl_config['max'] }
96
95
  @context = OpenSSL::SSL::SSLContext.new
97
96
  @context.min_version = SupportedSSLVersions::VERSIONS[version_config[:min]] unless version_config[:min].nil?
98
97
  @context.max_version = SupportedSSLVersions::VERSIONS[version_config[:max]] unless version_config[:max].nil?
99
- @context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_config["cert_file_name"]))
98
+ @context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_config['cert_file_name']))
100
99
 
101
- if ssl_config["key_type"] == "RSA" || ssl_config["key_type"].nil?
102
- @context.key = OpenSSL::PKey::RSA.new(File.read(ssl_config["key_file_name"]))
103
- elsif ssl_config["key_type"] == "EC"
104
- @context.key = OpenSSL::PKey::EC.new(File.read(ssl_config["key_file_name"]))
100
+ if ssl_config['key_type'] == 'RSA' || ssl_config['key_type'].nil?
101
+ @context.key = OpenSSL::PKey::RSA.new(File.read(ssl_config['key_file_name']))
102
+ elsif ssl_config['key_type'] == 'EC'
103
+ @context.key = OpenSSL::PKey::EC.new(File.read(ssl_config['key_file_name']))
105
104
  else
106
- raise ArgumentError, "Unsupported SSL/TLS key type: #{ssl_config["key_type"]}"
105
+ raise ArgumentError, "Unsupported SSL/TLS key type: #{ssl_config['key_type']}"
107
106
  end
108
107
  end
109
108
  @context ||= nil
110
109
  rescue IOError => e
111
- @macaw_log&.error("It was not possible to read files #{@macaw.config["macaw"]["ssl"]["cert_file_name"]} and
112
- #{@macaw.config["macaw"]["ssl"]["key_file_name"]}. Please assure the files exist and their names are correct.")
110
+ @macaw_log&.error("It was not possible to read files #{@macaw.config['macaw']['ssl']['cert_file_name']} and
111
+ #{@macaw.config['macaw']['ssl']['key_file_name']}. Please assure the files exist and their names are correct.")
113
112
  @macaw_log&.error(e.backtrace)
114
113
  raise e
115
114
  end
116
115
 
117
116
  def set_session
117
+ return unless @macaw.session
118
+
118
119
  @session ||= {}
119
- inv = if @macaw.config&.dig("macaw", "session", "invalidation_time")
120
- MemoryInvalidationMiddleware.new(@macaw.config["macaw"]["session"]["invalidation_time"])
120
+ inv = if @macaw.config&.dig('macaw', 'session', 'invalidation_time')
121
+ MemoryInvalidationMiddleware.new(@macaw.config['macaw']['session']['invalidation_time'])
121
122
  else
122
123
  MemoryInvalidationMiddleware.new
123
124
  end
@@ -127,7 +128,7 @@ module ServerBase
127
128
  def set_features
128
129
  @is_shutting_down = false
129
130
  set_rate_limiting
130
- set_session if @macaw.session
131
+ set_session
131
132
  set_ssl
132
133
  end
133
134
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "common/server_base"
4
- require "openssl"
3
+ require_relative 'common/server_base'
4
+ require 'openssl'
5
5
 
6
6
  ##
7
7
  # Class responsible for providing a default
@@ -74,8 +74,17 @@ class ThreadServer
74
74
 
75
75
  ##
76
76
  # Method Responsible for closing the TCP server.
77
- def close
78
- shutdown
77
+ def shutdown
78
+ @is_shutting_down = true
79
+ loop do
80
+ break if @work_queue.empty?
81
+
82
+ sleep 0.1
83
+ end
84
+
85
+ @num_threads.times { @work_queue << :shutdown }
86
+ @workers.each(&:join)
87
+ @server.close
79
88
  end
80
89
 
81
90
  private
@@ -107,17 +116,4 @@ class ThreadServer
107
116
  end
108
117
  end
109
118
  end
110
-
111
- def shutdown
112
- @is_shutting_down = true
113
- loop do
114
- break if @work_queue.empty?
115
-
116
- sleep 0.1
117
- end
118
-
119
- @num_threads.times { @work_queue << :shutdown }
120
- @workers.each(&:join)
121
- @server.close
122
- end
123
119
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: false
2
2
 
3
- require "json"
3
+ require 'json'
4
4
 
5
5
  ##
6
6
  # Module responsible for sanitizing log data
@@ -10,7 +10,7 @@ module LogDataFilter
10
10
 
11
11
  def self.config
12
12
  @config ||= begin
13
- file_path = "application.json"
13
+ file_path = 'application.json'
14
14
  config = {
15
15
  max_length: DEFAULT_MAX_LENGTH,
16
16
  sensitive_fields: DEFAULT_SENSITIVE_FIELDS
@@ -19,10 +19,10 @@ module LogDataFilter
19
19
  if File.exist?(file_path)
20
20
  json = JSON.parse(File.read(file_path))
21
21
 
22
- if json["macaw"] && json["macaw"]["log"]
23
- log_config = json["macaw"]["log"]
24
- config[:max_length] = log_config["max_length"] if log_config["max_length"]
25
- config[:sensitive_fields] = log_config["sensitive_fields"] if log_config["sensitive_fields"]
22
+ if json['macaw'] && json['macaw']['log']
23
+ log_config = json['macaw']['log']
24
+ config[:max_length] = log_config['max_length'] if log_config['max_length']
25
+ config[:sensitive_fields] = log_config['sensitive_fields'] if log_config['sensitive_fields']
26
26
  end
27
27
  end
28
28
 
@@ -31,11 +31,11 @@ module LogDataFilter
31
31
  end
32
32
 
33
33
  def self.sanitize_for_logging(data, sensitive_fields: config[:sensitive_fields])
34
- return "" if data.nil?
34
+ return '' if data.nil?
35
35
 
36
- data = data.to_s.force_encoding("UTF-8")
36
+ data = data.to_s.force_encoding('UTF-8')
37
37
  data = data.slice(0, config[:max_length])
38
- data = data.gsub("\\", "")
38
+ data = data.gsub('\\', '')
39
39
 
40
40
  sensitive_fields.each do |field|
41
41
  next unless data.include?(field.to_s)
@@ -1,23 +1,23 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../errors/endpoint_not_mapped_error"
3
+ require_relative '../errors/endpoint_not_mapped_error'
4
4
 
5
5
  ##
6
6
  # Module containing methods to filter Strings
7
7
  module RequestDataFiltering
8
- VARIABLE_PATTERN = %r{:[^/]+}.freeze
8
+ VARIABLE_PATTERN = %r{:[^/]+}
9
9
 
10
10
  ##
11
11
  # Method responsible for extracting information
12
12
  # provided by the client like Headers and Body
13
13
  def self.parse_request_data(client, routes)
14
- path, parameters = extract_url_parameters(client.gets.gsub("HTTP/1.1", ""))
14
+ path, parameters = extract_url_parameters(client.gets&.gsub('HTTP/1.1', ''))
15
15
  parameters = {} if parameters.nil?
16
16
 
17
17
  method_name = sanitize_method_name(path)
18
18
  method_name = select_path(method_name, routes, parameters)
19
19
  body_first_line, headers = extract_headers(client)
20
- body = extract_body(client, body_first_line, headers["Content-Length"].to_i)
20
+ body = extract_body(client, body_first_line, headers['Content-Length'].to_i)
21
21
  [path, method_name, headers, body, parameters]
22
22
  end
23
23
 
@@ -26,15 +26,15 @@ module RequestDataFiltering
26
26
 
27
27
  selected_route = nil
28
28
  routes.each do |route|
29
- split_route = route.split(".")
30
- split_name = method_name.split(".")
29
+ split_route = route&.split('.')
30
+ split_name = method_name&.split('.')
31
31
 
32
- next unless split_route.length == split_name.length
32
+ next unless split_route&.length == split_name&.length
33
33
  next unless match_path_with_route(split_name, split_route)
34
34
 
35
35
  selected_route = route
36
- split_route.each_with_index do |var, index|
37
- parameters[var[1..].to_sym] = split_name[index] if var =~ VARIABLE_PATTERN
36
+ split_route&.each_with_index do |var, index|
37
+ parameters[var[1..].to_sym] = split_name&.dig(index) if var =~ VARIABLE_PATTERN
38
38
  end
39
39
  break
40
40
  end
@@ -45,7 +45,7 @@ module RequestDataFiltering
45
45
  end
46
46
 
47
47
  def self.match_path_with_route(split_path, split_route)
48
- split_route.each_with_index do |var, index|
48
+ split_route&.each_with_index do |var, index|
49
49
  return false if var != split_path[index] && !var.match?(VARIABLE_PATTERN)
50
50
  end
51
51
 
@@ -56,26 +56,28 @@ module RequestDataFiltering
56
56
  # Method responsible for sanitizing the method name
57
57
  def self.sanitize_method_name(path)
58
58
  path = extract_path(path)
59
- method_name = path.gsub("/", ".").strip.downcase
60
- method_name.gsub!(" ", "")
59
+ method_name = path&.gsub('/', '.')&.strip&.downcase
60
+ method_name&.gsub!(' ', '')
61
61
  method_name
62
62
  end
63
63
 
64
64
  ##
65
65
  # Method responsible for extracting the path from URI
66
66
  def self.extract_path(path)
67
- path[0] == "/" ? path[1..].gsub("/", ".") : path.gsub("/", ".")
67
+ return path if path.nil?
68
+
69
+ path[0] == '/' ? path[1..].gsub('/', '.') : path.gsub('/', '.')
68
70
  end
69
71
 
70
72
  ##
71
73
  # Method responsible for extracting the headers from request
72
74
  def self.extract_headers(client)
73
- header = client.gets.delete("\n").delete("\r")
75
+ header = client.gets&.delete("\n")&.delete("\r")
74
76
  headers = {}
75
- while header.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
76
- split_header = header.split(":")
77
+ while header&.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
78
+ split_header = header.split(':')
77
79
  headers[split_header[0].strip] = split_header[1].strip
78
- header = client.gets.delete("\n").delete("\r")
80
+ header = client.gets&.delete("\n")&.delete("\r")
79
81
  end
80
82
  [header, headers]
81
83
  end
@@ -83,7 +85,7 @@ module RequestDataFiltering
83
85
  ##
84
86
  # Method responsible for extracting the body from request
85
87
  def self.extract_body(client, body_first_line, content_length)
86
- body = client.read(content_length)
88
+ body = client&.read(content_length)
87
89
  body_first_line << body.to_s
88
90
  end
89
91
 
@@ -92,11 +94,11 @@ module RequestDataFiltering
92
94
  def self.extract_url_parameters(http_first_line)
93
95
  return http_first_line, nil unless http_first_line =~ /\?/
94
96
 
95
- path_and_parameters = http_first_line.split("?", 2)
97
+ path_and_parameters = http_first_line.split('?', 2)
96
98
  path = "#{path_and_parameters[0]} "
97
- parameters_array = path_and_parameters[1].split("&")
99
+ parameters_array = path_and_parameters[1].split('&')
98
100
  parameters_array.map! do |item|
99
- split_item = item.split("=")
101
+ split_item = item.split('=')
100
102
  { sanitize_parameter_name(split_item[0]) => sanitize_parameter_value(split_item[1]) }
101
103
  end
102
104
  parameters = {}
@@ -107,13 +109,13 @@ module RequestDataFiltering
107
109
  ##
108
110
  # Method responsible for sanitizing the parameter name
109
111
  def self.sanitize_parameter_name(name)
110
- name.gsub(/[^\w\s]/, "")
112
+ name&.gsub(/[^\w\s]/, '')
111
113
  end
112
114
 
113
115
  ##
114
116
  # Method responsible for sanitizing the parameter value
115
117
  def self.sanitize_parameter_value(value)
116
- value.gsub(/[^\w\s]/, "")
117
- value.gsub(/\s/, "")
118
+ value&.gsub(/[^\w\s]/, '')
119
+ value&.gsub(/\s/, '')
118
120
  end
119
121
  end
@@ -1,6 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "../utils/http_status_code"
3
+ require_relative '../utils/http_status_code'
4
4
 
5
5
  ##
6
6
  # Module responsible to filter and mount HTTP responses
@@ -19,9 +19,9 @@ module ResponseDataFilter
19
19
  end
20
20
 
21
21
  def self.mount_response_headers(headers)
22
- return "" if headers.nil?
22
+ return '' if headers.nil?
23
23
 
24
- response = ""
24
+ response = ''
25
25
  headers.each do |key, value|
26
26
  response += "#{key}: #{value}\r\n"
27
27
  end
@@ -4,7 +4,7 @@
4
4
  # Error raised when the client calls
5
5
  # for a path that doesn't exist.
6
6
  class EndpointNotMappedError < StandardError
7
- def initialize(msg = "Undefined endpoint")
7
+ def initialize(msg = 'Undefined endpoint')
8
8
  super
9
9
  end
10
10
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "prometheus/client"
4
- require "prometheus/client/formats/text"
3
+ require 'prometheus/client'
4
+ require 'prometheus/client/formats/text'
5
5
 
6
6
  ##
7
7
  # Middleware responsible to configure prometheus
@@ -14,20 +14,20 @@ class PrometheusMiddleware
14
14
 
15
15
  @request_duration_milliseconds = Prometheus::Client::Histogram.new(
16
16
  :request_duration_milliseconds,
17
- docstring: "The duration of each request in milliseconds",
17
+ docstring: 'The duration of each request in milliseconds',
18
18
  labels: [:endpoint],
19
19
  buckets: (100..1000).step(100).to_a + (2000..10_000).step(1000).to_a
20
20
  )
21
21
 
22
22
  @request_count = Prometheus::Client::Counter.new(
23
23
  :request_count,
24
- docstring: "The total number of requests received",
24
+ docstring: 'The total number of requests received',
25
25
  labels: [:endpoint]
26
26
  )
27
27
 
28
28
  @response_count = Prometheus::Client::Counter.new(
29
29
  :response_count,
30
- docstring: "The total number of responses sent",
30
+ docstring: 'The total number of responses sent',
31
31
  labels: %i[endpoint status]
32
32
  )
33
33
 
@@ -40,9 +40,9 @@ class PrometheusMiddleware
40
40
  private
41
41
 
42
42
  def prometheus_endpoint(prometheus_registry, configurations, macaw)
43
- endpoint = configurations["macaw"]["prometheus"]["endpoint"] || "/metrics"
43
+ endpoint = configurations['macaw']['prometheus']['endpoint'] || '/metrics'
44
44
  macaw.get(endpoint) do |_context|
45
- [Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { "Content-Type" => "plaintext" }]
45
+ [Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { 'Content-Type' => 'plaintext' }]
46
46
  end
47
47
  end
48
48
  end
@@ -7,66 +7,66 @@ module HttpStatusCode
7
7
  ##
8
8
  # Http Status Code Map
9
9
  HTTP_STATUS_CODE_MAP = {
10
- 100 => "Continue",
11
- 101 => "Switching Protocols",
12
- 102 => "Processing",
13
- 103 => "Early Hints",
14
- 200 => "OK",
15
- 201 => "Created",
16
- 202 => "Accepted",
17
- 203 => "Non-Authoritative Information",
18
- 204 => "No Content",
19
- 205 => "Reset Content",
20
- 206 => "Partial Content",
21
- 207 => "Multi-Status",
22
- 208 => "Already Reported",
23
- 226 => "IM Used",
24
- 300 => "Multiple Choices",
25
- 301 => "Moved Permanently",
26
- 302 => "Found",
27
- 303 => "See Other",
28
- 304 => "Not Modified",
29
- 305 => "Use Proxy",
30
- 307 => "Temporary Redirect",
31
- 308 => "Permanent Redirect",
32
- 400 => "Bad Request",
33
- 401 => "Unauthorized",
34
- 402 => "Payment Required",
35
- 403 => "Forbidden",
36
- 404 => "Not Found",
37
- 405 => "Method Not Allowed",
38
- 406 => "Not Acceptable",
39
- 407 => "Proxy Authentication Required",
40
- 408 => "Request Timeout",
41
- 409 => "Conflict",
42
- 410 => "Gone",
43
- 411 => "Length Required",
44
- 412 => "Precondition Failed",
45
- 413 => "Content Too Large",
46
- 414 => "URI Too Long",
47
- 415 => "Unsupported Media Type",
48
- 416 => "Range Not Satisfiable",
49
- 417 => "Expectation Failed",
50
- 421 => "Misdirected Request",
51
- 422 => "Unprocessable Content",
52
- 423 => "Locked",
53
- 424 => "Failed Dependency",
54
- 425 => "Too Early",
55
- 426 => "Upgrade Required",
56
- 428 => "Precondition Required",
57
- 429 => "Too Many Requests",
58
- 431 => "Request Header Fields Too Large",
59
- 451 => "Unavailable For Legal Reasons",
60
- 500 => "Internal Server Error",
61
- 501 => "Not Implemented",
62
- 502 => "Bad Gateway",
63
- 503 => "Service Unavailable",
64
- 504 => "Gateway Timeout",
65
- 505 => "HTTP Version Not Supported",
66
- 506 => "Variant Also Negotiates",
67
- 507 => "Insufficient Storage",
68
- 508 => "Loop Detected",
69
- 510 => "Not Extended (OBSOLETED)",
70
- 511 => "Network Authentication Required"
10
+ 100 => 'Continue',
11
+ 101 => 'Switching Protocols',
12
+ 102 => 'Processing',
13
+ 103 => 'Early Hints',
14
+ 200 => 'OK',
15
+ 201 => 'Created',
16
+ 202 => 'Accepted',
17
+ 203 => 'Non-Authoritative Information',
18
+ 204 => 'No Content',
19
+ 205 => 'Reset Content',
20
+ 206 => 'Partial Content',
21
+ 207 => 'Multi-Status',
22
+ 208 => 'Already Reported',
23
+ 226 => 'IM Used',
24
+ 300 => 'Multiple Choices',
25
+ 301 => 'Moved Permanently',
26
+ 302 => 'Found',
27
+ 303 => 'See Other',
28
+ 304 => 'Not Modified',
29
+ 305 => 'Use Proxy',
30
+ 307 => 'Temporary Redirect',
31
+ 308 => 'Permanent Redirect',
32
+ 400 => 'Bad Request',
33
+ 401 => 'Unauthorized',
34
+ 402 => 'Payment Required',
35
+ 403 => 'Forbidden',
36
+ 404 => 'Not Found',
37
+ 405 => 'Method Not Allowed',
38
+ 406 => 'Not Acceptable',
39
+ 407 => 'Proxy Authentication Required',
40
+ 408 => 'Request Timeout',
41
+ 409 => 'Conflict',
42
+ 410 => 'Gone',
43
+ 411 => 'Length Required',
44
+ 412 => 'Precondition Failed',
45
+ 413 => 'Content Too Large',
46
+ 414 => 'URI Too Long',
47
+ 415 => 'Unsupported Media Type',
48
+ 416 => 'Range Not Satisfiable',
49
+ 417 => 'Expectation Failed',
50
+ 421 => 'Misdirected Request',
51
+ 422 => 'Unprocessable Content',
52
+ 423 => 'Locked',
53
+ 424 => 'Failed Dependency',
54
+ 425 => 'Too Early',
55
+ 426 => 'Upgrade Required',
56
+ 428 => 'Precondition Required',
57
+ 429 => 'Too Many Requests',
58
+ 431 => 'Request Header Fields Too Large',
59
+ 451 => 'Unavailable For Legal Reasons',
60
+ 500 => 'Internal Server Error',
61
+ 501 => 'Not Implemented',
62
+ 502 => 'Bad Gateway',
63
+ 503 => 'Service Unavailable',
64
+ 504 => 'Gateway Timeout',
65
+ 505 => 'HTTP Version Not Supported',
66
+ 506 => 'Variant Also Negotiates',
67
+ 507 => 'Insufficient Storage',
68
+ 508 => 'Loop Detected',
69
+ 510 => 'Not Extended (OBSOLETED)',
70
+ 511 => 'Network Authentication Required'
71
71
  }.freeze
72
72
  end
@@ -1,13 +1,13 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "openssl"
3
+ require 'openssl'
4
4
 
5
5
  module SupportedSSLVersions
6
6
  VERSIONS = {
7
- "SSL2" => OpenSSL::SSL::SSL2_VERSION,
8
- "SSL3" => OpenSSL::SSL::SSL3_VERSION,
9
- "TLS1.1" => OpenSSL::SSL::TLS1_1_VERSION,
10
- "TLS1.2" => OpenSSL::SSL::TLS1_2_VERSION,
11
- "TLS1.3" => OpenSSL::SSL::TLS1_3_VERSION
7
+ 'SSL2' => OpenSSL::SSL::SSL2_VERSION,
8
+ 'SSL3' => OpenSSL::SSL::SSL3_VERSION,
9
+ 'TLS1.1' => OpenSSL::SSL::TLS1_1_VERSION,
10
+ 'TLS1.2' => OpenSSL::SSL::TLS1_2_VERSION,
11
+ 'TLS1.3' => OpenSSL::SSL::TLS1_3_VERSION
12
12
  }.freeze
13
13
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = "1.3.0"
4
+ VERSION = '1.3.21'
5
5
  end
@@ -1,18 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "macaw_framework/errors/endpoint_not_mapped_error"
4
- require_relative "macaw_framework/middlewares/prometheus_middleware"
5
- require_relative "macaw_framework/data_filters/request_data_filtering"
6
- require_relative "macaw_framework/middlewares/memory_invalidation_middleware"
7
- require_relative "macaw_framework/core/cron_runner"
8
- require_relative "macaw_framework/core/thread_server"
9
- require_relative "macaw_framework/version"
10
- require "prometheus/client"
11
- require "securerandom"
12
- require "pathname"
13
- require "logger"
14
- require "socket"
15
- require "json"
3
+ require_relative 'macaw_framework/errors/endpoint_not_mapped_error'
4
+ require_relative 'macaw_framework/middlewares/prometheus_middleware'
5
+ require_relative 'macaw_framework/data_filters/request_data_filtering'
6
+ require_relative 'macaw_framework/middlewares/memory_invalidation_middleware'
7
+ require_relative 'macaw_framework/core/cron_runner'
8
+ require_relative 'macaw_framework/core/thread_server'
9
+ require_relative 'macaw_framework/version'
10
+ require 'prometheus/client'
11
+ require 'securerandom'
12
+ require 'singleton'
13
+ require 'pathname'
14
+ require 'logger'
15
+ require 'socket'
16
+ require 'json'
16
17
 
17
18
  module MacawFramework
18
19
  ##
@@ -30,13 +31,7 @@ module MacawFramework
30
31
  def initialize(custom_log: Logger.new($stdout), server: ThreadServer, dir: nil)
31
32
  apply_options(custom_log)
32
33
  create_endpoint_public_files(dir)
33
- @port ||= 8080
34
- @bind ||= "localhost"
35
- @config ||= nil
36
- @threads ||= 200
37
- @endpoints_to_cache = []
38
- @prometheus ||= nil
39
- @prometheus_middleware ||= nil
34
+ setup_default_configs
40
35
  @server_class = server
41
36
  end
42
37
 
@@ -45,14 +40,15 @@ module MacawFramework
45
40
  # with the respective path.
46
41
  # @param {String} path
47
42
  # @param {Proc} block
48
- # @example
49
43
  #
50
- # macaw = MacawFramework::Macaw.new
51
- # macaw.get("/hello") do |context|
52
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
53
- # end
44
+ # @example
45
+ # macaw = MacawFramework::Macaw.new
46
+ # macaw.get("/hello") do |context|
47
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
48
+ # end
49
+ ##
54
50
  def get(path, cache: [], &block)
55
- map_new_endpoint("get", cache, path, &block)
51
+ map_new_endpoint('get', cache, path, &block)
56
52
  end
57
53
 
58
54
  ##
@@ -63,12 +59,13 @@ module MacawFramework
63
59
  # @param {Proc} block
64
60
  # @example
65
61
  #
66
- # macaw = MacawFramework::Macaw.new
67
- # macaw.post("/hello") do |context|
68
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
69
- # end
62
+ # macaw = MacawFramework::Macaw.new
63
+ # macaw.post("/hello") do |context|
64
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
65
+ # end
66
+ ##
70
67
  def post(path, cache: [], &block)
71
- map_new_endpoint("post", cache, path, &block)
68
+ map_new_endpoint('post', cache, path, &block)
72
69
  end
73
70
 
74
71
  ##
@@ -78,12 +75,13 @@ module MacawFramework
78
75
  # @param {Proc} block
79
76
  # @example
80
77
  #
81
- # macaw = MacawFramework::Macaw.new
82
- # macaw.put("/hello") do |context|
83
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
84
- # end
78
+ # macaw = MacawFramework::Macaw.new
79
+ # macaw.put("/hello") do |context|
80
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
81
+ # end
82
+ ##
85
83
  def put(path, cache: [], &block)
86
- map_new_endpoint("put", cache, path, &block)
84
+ map_new_endpoint('put', cache, path, &block)
87
85
  end
88
86
 
89
87
  ##
@@ -93,12 +91,13 @@ module MacawFramework
93
91
  # @param {Proc} block
94
92
  # @example
95
93
  #
96
- # macaw = MacawFramework::Macaw.new
97
- # macaw.patch("/hello") do |context|
98
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
99
- # end
94
+ # macaw = MacawFramework::Macaw.new
95
+ # macaw.patch("/hello") do |context|
96
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
97
+ # end
98
+ ##
100
99
  def patch(path, cache: [], &block)
101
- map_new_endpoint("patch", cache, path, &block)
100
+ map_new_endpoint('patch', cache, path, &block)
102
101
  end
103
102
 
104
103
  ##
@@ -108,26 +107,28 @@ module MacawFramework
108
107
  # @param {Proc} block
109
108
  # @example
110
109
  #
111
- # macaw = MacawFramework::Macaw.new
112
- # macaw.delete("/hello") do |context|
113
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
114
- # end
110
+ # macaw = MacawFramework::Macaw.new
111
+ # macaw.delete("/hello") do |context|
112
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
113
+ # end
114
+ ##
115
115
  def delete(path, cache: [], &block)
116
- map_new_endpoint("delete", cache, path, &block)
116
+ map_new_endpoint('delete', cache, path, &block)
117
117
  end
118
118
 
119
119
  ##
120
- # Spawn and start a thread running the defined cron job.
120
+ # Spawn and start a thread running the defined periodic job.
121
121
  # @param {Integer} interval
122
122
  # @param {Integer?} start_delay
123
123
  # @param {String} job_name
124
124
  # @param {Proc} block
125
125
  # @example
126
126
  #
127
- # macaw = MacawFramework::Macaw.new
128
- # macaw.setup_job(interval: 60, start_delay: 60, job_name: "job 1") do
129
- # puts "I'm a cron job that runs every minute"
130
- # end
127
+ # macaw = MacawFramework::Macaw.new
128
+ # macaw.setup_job(interval: 60, start_delay: 60, job_name: "job 1") do
129
+ # puts "I'm a periodic job that runs every minute"
130
+ # end
131
+ ##
131
132
  def setup_job(interval: 60, start_delay: 0, job_name: "job_#{SecureRandom.uuid}", &block)
132
133
  @cron_runner ||= CronRunner.new(self)
133
134
  @jobs ||= []
@@ -139,27 +140,27 @@ module MacawFramework
139
140
  # Starts the web server
140
141
  def start!
141
142
  if @macaw_log.nil?
142
- puts("---------------------------------")
143
+ puts('---------------------------------')
143
144
  puts("Starting server at port #{@port}")
144
145
  puts("Number of threads: #{@threads}")
145
- puts("---------------------------------")
146
+ puts('---------------------------------')
146
147
  else
147
- @macaw_log.info("---------------------------------")
148
+ @macaw_log.info('---------------------------------')
148
149
  @macaw_log.info("Starting server at port #{@port}")
149
150
  @macaw_log.info("Number of threads: #{@threads}")
150
- @macaw_log.info("---------------------------------")
151
+ @macaw_log.info('---------------------------------')
151
152
  end
152
153
  @server = @server_class.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
153
154
  server_loop(@server)
154
155
  rescue Interrupt
155
156
  if @macaw_log.nil?
156
- puts("Stopping server")
157
- @server.close
158
- puts("Macaw stop flying for some seeds...")
157
+ puts('Stopping server')
158
+ @server.shutdown
159
+ puts('Macaw stop flying for some seeds...')
159
160
  else
160
- @macaw_log.info("Stopping server")
161
- @server.close
162
- @macaw_log.info("Macaw stop flying for some seeds...")
161
+ @macaw_log.info('Stopping server')
162
+ @server.shutdown
163
+ @macaw_log.info('Macaw stop flying for some seeds...')
163
164
  end
164
165
  end
165
166
 
@@ -169,35 +170,63 @@ module MacawFramework
169
170
  # you just want to keep cron jobs running, without
170
171
  # mapping any HTTP endpoints.
171
172
  def start_without_server!
172
- @macaw_log.nil? ? puts("Application starting") : @macaw_log.info("Application starting")
173
+ @macaw_log.nil? ? puts('Application starting') : @macaw_log.info('Application starting')
173
174
  loop { sleep(3600) }
174
175
  rescue Interrupt
175
- @macaw_log.nil? ? puts("Macaw stop flying for some seeds.") : @macaw_log.info("Macaw stop flying for some seeds.")
176
+ @macaw_log.nil? ? puts('Macaw stop flying for some seeds.') : @macaw_log.info('Macaw stop flying for some seeds.')
176
177
  end
177
178
 
178
179
  private
179
180
 
181
+ def setup_default_configs
182
+ @port ||= 8080
183
+ @bind ||= 'localhost'
184
+ @config ||= nil
185
+ @threads ||= 200
186
+ @endpoints_to_cache = []
187
+ @prometheus ||= nil
188
+ @prometheus_middleware ||= nil
189
+ end
190
+
180
191
  def apply_options(custom_log)
192
+ setup_basic_config(custom_log)
193
+ setup_session
194
+ setup_cache
195
+ setup_prometheus
196
+ rescue StandardError => e
197
+ @macaw_log&.warn(e.message)
198
+ end
199
+
200
+ def setup_cache
201
+ return if @config['macaw']['cache'].nil?
202
+
203
+ @cache = MemoryInvalidationMiddleware.new(@config['macaw']['cache']['cache_invalidation'].to_i || 3_600)
204
+ end
205
+
206
+ def setup_session
207
+ @session = false
208
+ return if @config['macaw']['session'].nil?
209
+
210
+ @session = true
211
+ @secure_header = @config['macaw']['session']['secure_header'] || 'X-Session-ID'
212
+ end
213
+
214
+ def setup_basic_config(custom_log)
181
215
  @routes = []
182
216
  @cached_methods = {}
183
217
  @macaw_log ||= custom_log
184
- @config = JSON.parse(File.read("application.json"))
185
- @port = @config["macaw"]["port"] || 8080
186
- @bind = @config["macaw"]["bind"] || "localhost"
187
- @session = false
188
- unless @config["macaw"]["session"].nil?
189
- @session = true
190
- @secure_header = @config["macaw"]["session"]["secure_header"] || "X-Session-ID"
191
- end
192
- @threads = @config["macaw"]["threads"] || 200
193
- unless @config["macaw"]["cache"].nil?
194
- @cache = MemoryInvalidationMiddleware.new(@config["macaw"]["cache"]["cache_invalidation"].to_i || 3_600)
195
- end
196
- @prometheus = Prometheus::Client::Registry.new if @config["macaw"]["prometheus"]
197
- @prometheus_middleware = PrometheusMiddleware.new if @config["macaw"]["prometheus"]
198
- @prometheus_middleware.configure_prometheus(@prometheus, @config, self) if @config["macaw"]["prometheus"]
199
- rescue StandardError => e
200
- @macaw_log&.warn(e.message)
218
+ @config = JSON.parse(File.read('application.json'))
219
+ @port = @config['macaw']['port'] || 8080
220
+ @bind = @config['macaw']['bind'] || 'localhost'
221
+ @threads = @config['macaw']['threads'] || 200
222
+ end
223
+
224
+ def setup_prometheus
225
+ return unless @config['macaw']['prometheus']
226
+
227
+ @prometheus = Prometheus::Client::Registry.new
228
+ @prometheus_middleware = PrometheusMiddleware.new
229
+ @prometheus_middleware&.configure_prometheus(@prometheus, @config, self)
201
230
  end
202
231
 
203
232
  def server_loop(server)
@@ -208,10 +237,10 @@ module MacawFramework
208
237
  @endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}" unless cache.empty?
209
238
  @cached_methods["#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}"] = cache unless cache.empty?
210
239
  path_clean = RequestDataFiltering.extract_path(path)
211
- slash = path[0] == "/" ? "" : "/"
240
+ slash = path[0] == '/' ? '' : '/'
212
241
  @macaw_log&.info("Defining #{prefix.upcase} endpoint at #{slash}#{path}")
213
242
  define_singleton_method("#{prefix}.#{path_clean}", block || lambda {
214
- |context = { headers: {}, body: "", params: {} }|
243
+ |context = { headers: {}, body: '', params: {} }|
215
244
  })
216
245
  @routes << "#{prefix}.#{path_clean}"
217
246
  end
@@ -219,8 +248,8 @@ module MacawFramework
219
248
  def get_files_public_folder(dir)
220
249
  return [] if dir.nil?
221
250
 
222
- folder_path = Pathname.new(File.expand_path("public", dir))
223
- file_paths = folder_path.glob("**/*").select(&:file?)
251
+ folder_path = Pathname.new(File.expand_path('public', dir))
252
+ file_paths = folder_path.glob('**/*').select(&:file?)
224
253
  file_paths.map { |path| "public/#{path.relative_path_from(folder_path)}" }
225
254
  end
226
255
 
@@ -230,4 +259,90 @@ module MacawFramework
230
259
  end
231
260
  end
232
261
  end
262
+
263
+ ##
264
+ # This singleton class allows to manually cache
265
+ # parameters and other data.
266
+ class Cache
267
+ include Singleton
268
+
269
+ attr_accessor :invalidation_frequency
270
+
271
+ ##
272
+ # Write a value to Cache memory.
273
+ # Can be called statically or from an instance.
274
+ # @param {String} tag
275
+ # @param {Object} value
276
+ # @param {Integer} expires_in Defaults to 3600.
277
+ # @return nil
278
+ #
279
+ # @example
280
+ # MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
281
+ def self.write(tag, value, expires_in: 3600)
282
+ MacawFramework::Cache.instance.write(tag, value, expires_in: expires_in)
283
+ end
284
+
285
+ ##
286
+ # Write a value to Cache memory.
287
+ # Can be called statically or from an instance.
288
+ # @param {String} tag
289
+ # @param {Object} value
290
+ # @param {Integer} expires_in Defaults to 3600.
291
+ # @return nil
292
+ #
293
+ # @example
294
+ # MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
295
+ def write(tag, value, expires_in: 3600)
296
+ if read(tag).nil?
297
+ @mutex.synchronize do
298
+ @cache.store(tag, { value: value, expires_in: Time.now + expires_in })
299
+ end
300
+ else
301
+ @cache[tag][:value] = value
302
+ @cache[tag][:expires_in] = Time.now + expires_in
303
+ end
304
+ end
305
+
306
+ ##
307
+ # Read the value with the specified tag.
308
+ # Can be called statically or from an instance.
309
+ # @param {String} tag
310
+ # @return {String|nil}
311
+ #
312
+ # @example
313
+ # MacawFramework::Cache.read("name") # Maria
314
+ def self.read(tag) = MacawFramework::Cache.instance.read(tag)
315
+
316
+ ##
317
+ # Read the value with the specified tag.
318
+ # Can be called statically or from an instance.
319
+ # @param {String} tag
320
+ # @return {String|nil}
321
+ #
322
+ # @example
323
+ # MacawFramework::Cache.read("name") # Maria
324
+ def read(tag) = @cache.dig(tag, :value)
325
+
326
+ private
327
+
328
+ def initialize
329
+ @cache = {}
330
+ @mutex = Mutex.new
331
+ @invalidation_frequency = 60
332
+ invalidate_cache
333
+ end
334
+
335
+ def invalidate_cache
336
+ @invalidator = Thread.new(&method(:invalidation_process))
337
+ end
338
+
339
+ def invalidation_process
340
+ loop do
341
+ sleep @invalidation_frequency
342
+ @mutex.synchronize do
343
+ @cache.delete_if { |_, v| v[:expires_in] < Time.now }
344
+ end
345
+ end
346
+ end
347
+ end
233
348
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macaw_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.0
4
+ version: 1.3.21
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2024-05-04 00:00:00.000000000 Z
11
+ date: 2025-01-06 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: prometheus-client
@@ -84,7 +84,7 @@ required_ruby_version: !ruby/object:Gem::Requirement
84
84
  requirements:
85
85
  - - ">="
86
86
  - !ruby/object:Gem::Version
87
- version: 2.7.0
87
+ version: 3.0.0
88
88
  required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  requirements:
90
90
  - - ">="