macaw_framework 1.2.6 → 1.3.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.rubocop.yml +7 -9
- data/CHANGELOG.md +11 -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 +30 -28
- 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
@@ -136,3 +136,14 @@
|
|
136
136
|
## [1.2.6]
|
137
137
|
|
138
138
|
- Improving session strategy and fixing vulnerabilities on it.
|
139
|
+
|
140
|
+
## [1.3.0]
|
141
|
+
- Improvements to cache usability
|
142
|
+
- Improving session strategy and fixing vulnerabilities on it
|
143
|
+
- Fixed a bug where errors were being logged with level INFO
|
144
|
+
- Improved error stack trace
|
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 ||= {}
|
@@ -63,7 +63,7 @@ module ServerBase
|
|
63
63
|
client.print "HTTP/1.1 404 Not Found\r\n\r\n"
|
64
64
|
rescue StandardError => e
|
65
65
|
client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
|
66
|
-
@macaw_log&.
|
66
|
+
@macaw_log&.error(e.full_message)
|
67
67
|
ensure
|
68
68
|
begin
|
69
69
|
client.close
|
@@ -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.
|
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
|
- - ">="
|