macaw_framework 1.3.0 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -9
- data/CHANGELOG.md +4 -0
- data/Gemfile +9 -9
- data/README.md +24 -7
- data/Rakefile +6 -6
- data/lib/macaw_framework/aspects/logging_aspect.rb +3 -3
- data/lib/macaw_framework/aspects/prometheus_aspect.rb +1 -1
- data/lib/macaw_framework/core/common/server_base.rb +29 -27
- data/lib/macaw_framework/core/thread_server.rb +2 -2
- data/lib/macaw_framework/data_filters/log_data_filter.rb +9 -9
- data/lib/macaw_framework/data_filters/request_data_filtering.rb +16 -16
- data/lib/macaw_framework/data_filters/response_data_filter.rb +3 -3
- data/lib/macaw_framework/errors/endpoint_not_mapped_error.rb +1 -1
- data/lib/macaw_framework/middlewares/prometheus_middleware.rb +7 -7
- data/lib/macaw_framework/utils/http_status_code.rb +61 -61
- data/lib/macaw_framework/utils/supported_ssl_versions.rb +6 -6
- data/lib/macaw_framework/version.rb +1 -1
- data/lib/macaw_framework.rb +197 -82
- metadata +3 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: faee0746cfa34d7020272e9c6a3d24f12bff1c88ef3a5f8bf1f65be5dc7de8b2
|
4
|
+
data.tar.gz: f3dfa0a71a12df9d0fdf07fcedcf9fe8cc29db81c20aa003482f5a465a330648
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 92b61e93524104407eba6c9d2621990a7cc6b68e0733a78fedc75cff6fc248601410344aa421dec9e79f77a52b391d7f2e102cb54f639a127ebeaf4b00c5f2fb
|
7
|
+
data.tar.gz: 3eb62e30474e588dd0200f981aaa847ddf2725d7092d9c4387a0a5c63c2870f2ae87bd7f7998d0149dd00c592d144d0d6188464de6a473334ec94733967aef3d
|
data/.rubocop.yml
CHANGED
@@ -1,16 +1,8 @@
|
|
1
1
|
AllCops:
|
2
|
-
TargetRubyVersion:
|
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,7 @@
|
|
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
|
+
|
data/Gemfile
CHANGED
@@ -1,17 +1,17 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
source
|
3
|
+
source 'https://rubygems.org'
|
4
4
|
|
5
5
|
gemspec
|
6
6
|
|
7
|
-
gem
|
8
|
-
gem
|
7
|
+
gem 'openssl'
|
8
|
+
gem 'prometheus-client', '~> 4.1'
|
9
9
|
|
10
10
|
group :test do
|
11
|
-
gem
|
12
|
-
gem
|
13
|
-
gem
|
14
|
-
gem
|
15
|
-
gem
|
16
|
-
gem
|
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
|
-
+ [
|
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
|
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
|
-
###
|
213
|
+
### Periodic Jobs
|
199
214
|
|
200
|
-
Macaw Framework supports the declaration of
|
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
|
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
|
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
|
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
|
4
|
-
require
|
3
|
+
require 'bundler/gem_tasks'
|
4
|
+
require 'rake/testtask'
|
5
5
|
|
6
6
|
Rake::TestTask.new(:test) do |t|
|
7
|
-
t.libs <<
|
8
|
-
t.libs <<
|
9
|
-
t.test_files = FileList[
|
7
|
+
t.libs << 'test'
|
8
|
+
t.libs << 'lib'
|
9
|
+
t.test_files = FileList['test/**/test_*.rb']
|
10
10
|
end
|
11
11
|
|
12
|
-
require
|
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
|
4
|
-
require_relative
|
3
|
+
require 'logger'
|
4
|
+
require_relative '../data_filters/log_data_filter'
|
5
5
|
|
6
6
|
##
|
7
7
|
# This Aspect is responsible for logging
|
@@ -11,7 +11,7 @@ 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(
|
14
|
+
endpoint_name = args[1].split('.')[1..].join('/')
|
15
15
|
logger.info("Request received for [#{endpoint_name}] from [#{args[-1]}]")
|
16
16
|
|
17
17
|
begin
|
@@ -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(
|
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
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require_relative
|
11
|
-
require
|
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
|
32
|
+
client: @session&.dig(session_id)&.dig(0)
|
33
33
|
}
|
34
34
|
)
|
35
35
|
end
|
@@ -46,7 +46,7 @@ module ServerBase
|
|
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",
|
49
|
+
@macaw_log&.info("Running #{path.gsub("\n", '').gsub("\r", '')}")
|
50
50
|
message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
|
51
51
|
method_name, client_data, session_id, client.peeraddr[3])
|
52
52
|
response_headers ||= {}
|
@@ -80,44 +80,46 @@ module ServerBase
|
|
80
80
|
end
|
81
81
|
|
82
82
|
def set_rate_limiting
|
83
|
-
return unless @macaw.config&.dig(
|
83
|
+
return unless @macaw.config&.dig('macaw', 'rate_limiting')
|
84
84
|
|
85
85
|
@rate_limit = RateLimiterMiddleware.new(
|
86
|
-
@macaw.config[
|
87
|
-
@macaw.config[
|
86
|
+
@macaw.config['macaw']['rate_limiting']['window'].to_i || 1,
|
87
|
+
@macaw.config['macaw']['rate_limiting']['max_requests'].to_i || 60
|
88
88
|
)
|
89
89
|
end
|
90
90
|
|
91
91
|
def set_ssl
|
92
|
-
ssl_config = @macaw.config[
|
92
|
+
ssl_config = @macaw.config['macaw']['ssl'] if @macaw.config&.dig('macaw', 'ssl')
|
93
93
|
ssl_config ||= nil
|
94
94
|
unless ssl_config.nil?
|
95
|
-
version_config = { min: ssl_config[
|
95
|
+
version_config = { min: ssl_config['min'], max: ssl_config['max'] }
|
96
96
|
@context = OpenSSL::SSL::SSLContext.new
|
97
97
|
@context.min_version = SupportedSSLVersions::VERSIONS[version_config[:min]] unless version_config[:min].nil?
|
98
98
|
@context.max_version = SupportedSSLVersions::VERSIONS[version_config[:max]] unless version_config[:max].nil?
|
99
|
-
@context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_config[
|
99
|
+
@context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_config['cert_file_name']))
|
100
100
|
|
101
|
-
if ssl_config[
|
102
|
-
@context.key = OpenSSL::PKey::RSA.new(File.read(ssl_config[
|
103
|
-
elsif ssl_config[
|
104
|
-
@context.key = OpenSSL::PKey::EC.new(File.read(ssl_config[
|
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']))
|
105
105
|
else
|
106
|
-
raise ArgumentError, "Unsupported SSL/TLS key type: #{ssl_config[
|
106
|
+
raise ArgumentError, "Unsupported SSL/TLS key type: #{ssl_config['key_type']}"
|
107
107
|
end
|
108
108
|
end
|
109
109
|
@context ||= nil
|
110
110
|
rescue IOError => e
|
111
|
-
@macaw_log&.error("It was not possible to read files #{@macaw.config[
|
112
|
-
#{@macaw.config[
|
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.")
|
113
113
|
@macaw_log&.error(e.backtrace)
|
114
114
|
raise e
|
115
115
|
end
|
116
116
|
|
117
117
|
def set_session
|
118
|
+
return unless @macaw.session
|
119
|
+
|
118
120
|
@session ||= {}
|
119
|
-
inv = if @macaw.config&.dig(
|
120
|
-
MemoryInvalidationMiddleware.new(@macaw.config[
|
121
|
+
inv = if @macaw.config&.dig('macaw', 'session', 'invalidation_time')
|
122
|
+
MemoryInvalidationMiddleware.new(@macaw.config['macaw']['session']['invalidation_time'])
|
121
123
|
else
|
122
124
|
MemoryInvalidationMiddleware.new
|
123
125
|
end
|
@@ -127,7 +129,7 @@ module ServerBase
|
|
127
129
|
def set_features
|
128
130
|
@is_shutting_down = false
|
129
131
|
set_rate_limiting
|
130
|
-
set_session
|
132
|
+
set_session
|
131
133
|
set_ssl
|
132
134
|
end
|
133
135
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: false
|
2
2
|
|
3
|
-
require
|
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 =
|
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[
|
23
|
-
log_config = json[
|
24
|
-
config[:max_length] = log_config[
|
25
|
-
config[:sensitive_fields] = log_config[
|
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
|
34
|
+
return '' if data.nil?
|
35
35
|
|
36
|
-
data = data.to_s.force_encoding(
|
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
|
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{:[^/]+}
|
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(
|
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[
|
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,8 +26,8 @@ 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
32
|
next unless split_route.length == split_name.length
|
33
33
|
next unless match_path_with_route(split_name, split_route)
|
@@ -56,15 +56,15 @@ 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(
|
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] ==
|
67
|
+
path[0] == '/' ? path[1..].gsub('/', '.') : path.gsub('/', '.')
|
68
68
|
end
|
69
69
|
|
70
70
|
##
|
@@ -73,7 +73,7 @@ module RequestDataFiltering
|
|
73
73
|
header = client.gets.delete("\n").delete("\r")
|
74
74
|
headers = {}
|
75
75
|
while header.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
|
76
|
-
split_header = header.split(
|
76
|
+
split_header = header.split(':')
|
77
77
|
headers[split_header[0].strip] = split_header[1].strip
|
78
78
|
header = client.gets.delete("\n").delete("\r")
|
79
79
|
end
|
@@ -92,11 +92,11 @@ module RequestDataFiltering
|
|
92
92
|
def self.extract_url_parameters(http_first_line)
|
93
93
|
return http_first_line, nil unless http_first_line =~ /\?/
|
94
94
|
|
95
|
-
path_and_parameters = http_first_line.split(
|
95
|
+
path_and_parameters = http_first_line.split('?', 2)
|
96
96
|
path = "#{path_and_parameters[0]} "
|
97
|
-
parameters_array = path_and_parameters[1].split(
|
97
|
+
parameters_array = path_and_parameters[1].split('&')
|
98
98
|
parameters_array.map! do |item|
|
99
|
-
split_item = item.split(
|
99
|
+
split_item = item.split('=')
|
100
100
|
{ sanitize_parameter_name(split_item[0]) => sanitize_parameter_value(split_item[1]) }
|
101
101
|
end
|
102
102
|
parameters = {}
|
@@ -107,13 +107,13 @@ module RequestDataFiltering
|
|
107
107
|
##
|
108
108
|
# Method responsible for sanitizing the parameter name
|
109
109
|
def self.sanitize_parameter_name(name)
|
110
|
-
name.gsub(/[^\w\s]/,
|
110
|
+
name.gsub(/[^\w\s]/, '')
|
111
111
|
end
|
112
112
|
|
113
113
|
##
|
114
114
|
# Method responsible for sanitizing the parameter value
|
115
115
|
def self.sanitize_parameter_value(value)
|
116
|
-
value.gsub(/[^\w\s]/,
|
117
|
-
value.gsub(/\s/,
|
116
|
+
value.gsub(/[^\w\s]/, '')
|
117
|
+
value.gsub(/\s/, '')
|
118
118
|
end
|
119
119
|
end
|
@@ -1,6 +1,6 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
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
|
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
|
@@ -1,7 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require
|
4
|
-
require
|
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:
|
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:
|
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:
|
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[
|
43
|
+
endpoint = configurations['macaw']['prometheus']['endpoint'] || '/metrics'
|
44
44
|
macaw.get(endpoint) do |_context|
|
45
|
-
[Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, {
|
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 =>
|
11
|
-
101 =>
|
12
|
-
102 =>
|
13
|
-
103 =>
|
14
|
-
200 =>
|
15
|
-
201 =>
|
16
|
-
202 =>
|
17
|
-
203 =>
|
18
|
-
204 =>
|
19
|
-
205 =>
|
20
|
-
206 =>
|
21
|
-
207 =>
|
22
|
-
208 =>
|
23
|
-
226 =>
|
24
|
-
300 =>
|
25
|
-
301 =>
|
26
|
-
302 =>
|
27
|
-
303 =>
|
28
|
-
304 =>
|
29
|
-
305 =>
|
30
|
-
307 =>
|
31
|
-
308 =>
|
32
|
-
400 =>
|
33
|
-
401 =>
|
34
|
-
402 =>
|
35
|
-
403 =>
|
36
|
-
404 =>
|
37
|
-
405 =>
|
38
|
-
406 =>
|
39
|
-
407 =>
|
40
|
-
408 =>
|
41
|
-
409 =>
|
42
|
-
410 =>
|
43
|
-
411 =>
|
44
|
-
412 =>
|
45
|
-
413 =>
|
46
|
-
414 =>
|
47
|
-
415 =>
|
48
|
-
416 =>
|
49
|
-
417 =>
|
50
|
-
421 =>
|
51
|
-
422 =>
|
52
|
-
423 =>
|
53
|
-
424 =>
|
54
|
-
425 =>
|
55
|
-
426 =>
|
56
|
-
428 =>
|
57
|
-
429 =>
|
58
|
-
431 =>
|
59
|
-
451 =>
|
60
|
-
500 =>
|
61
|
-
501 =>
|
62
|
-
502 =>
|
63
|
-
503 =>
|
64
|
-
504 =>
|
65
|
-
505 =>
|
66
|
-
506 =>
|
67
|
-
507 =>
|
68
|
-
508 =>
|
69
|
-
510 =>
|
70
|
-
511 =>
|
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
|
3
|
+
require 'openssl'
|
4
4
|
|
5
5
|
module SupportedSSLVersions
|
6
6
|
VERSIONS = {
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
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
|
data/lib/macaw_framework.rb
CHANGED
@@ -1,18 +1,19 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require_relative
|
4
|
-
require_relative
|
5
|
-
require_relative
|
6
|
-
require_relative
|
7
|
-
require_relative
|
8
|
-
require_relative
|
9
|
-
require_relative
|
10
|
-
require
|
11
|
-
require
|
12
|
-
require
|
13
|
-
require
|
14
|
-
require
|
15
|
-
require
|
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
|
-
|
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
|
-
#
|
51
|
-
#
|
52
|
-
#
|
53
|
-
#
|
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(
|
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
|
-
#
|
67
|
-
#
|
68
|
-
#
|
69
|
-
#
|
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(
|
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
|
-
#
|
82
|
-
#
|
83
|
-
#
|
84
|
-
#
|
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(
|
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
|
-
#
|
97
|
-
#
|
98
|
-
#
|
99
|
-
#
|
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(
|
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
|
-
#
|
112
|
-
#
|
113
|
-
#
|
114
|
-
#
|
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(
|
116
|
+
map_new_endpoint('delete', cache, path, &block)
|
117
117
|
end
|
118
118
|
|
119
119
|
##
|
120
|
-
# Spawn and start a thread running the defined
|
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
|
-
#
|
128
|
-
#
|
129
|
-
#
|
130
|
-
#
|
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(
|
157
|
+
puts('Stopping server')
|
157
158
|
@server.close
|
158
|
-
puts(
|
159
|
+
puts('Macaw stop flying for some seeds...')
|
159
160
|
else
|
160
|
-
@macaw_log.info(
|
161
|
+
@macaw_log.info('Stopping server')
|
161
162
|
@server.close
|
162
|
-
@macaw_log.info(
|
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(
|
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(
|
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(
|
185
|
-
@port = @config[
|
186
|
-
@bind = @config[
|
187
|
-
@
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
193
|
-
|
194
|
-
|
195
|
-
|
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:
|
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(
|
223
|
-
file_paths = folder_path.glob(
|
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.
|
4
|
+
version: 1.3.1
|
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-
|
11
|
+
date: 2024-09-07 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:
|
87
|
+
version: 3.0.0
|
88
88
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
89
89
|
requirements:
|
90
90
|
- - ">="
|