macaw_framework 1.3.0 → 1.3.1

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: faee0746cfa34d7020272e9c6a3d24f12bff1c88ef3a5f8bf1f65be5dc7de8b2
4
+ data.tar.gz: f3dfa0a71a12df9d0fdf07fcedcf9fe8cc29db81c20aa003482f5a465a330648
5
5
  SHA512:
6
- metadata.gz: e218e790fd300482efcdabb79149819859b5500f4a7e12d511ffecf4d9ca678731045d0defa46cc13893c0fd1367c25104ff10d34ceb73d42ee35ae61c6c32a5
7
- data.tar.gz: 151ffbeecb5789abcabfc52af53228ecae894f45717ad8e257bb8ddbd193e1b61ce1458ce107c5c3e300746a37a63f9715d4d87d856ee61a469ed15d3b99c89d
6
+ metadata.gz: 92b61e93524104407eba6c9d2621990a7cc6b68e0733a78fedc75cff6fc248601410344aa421dec9e79f77a52b391d7f2e102cb54f639a127ebeaf4b00c5f2fb
7
+ data.tar.gz: 3eb62e30474e588dd0200f981aaa847ddf2725d7092d9c4387a0a5c63c2870f2ae87bd7f7998d0149dd00c592d144d0d6188464de6a473334ec94733967aef3d
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,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 "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,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(".")[1..].join("/")
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(".").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
@@ -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", "").gsub("\r", "")}")
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("macaw", "rate_limiting")
83
+ return unless @macaw.config&.dig('macaw', 'rate_limiting')
84
84
 
85
85
  @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
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["macaw"]["ssl"] if @macaw.config&.dig("macaw", "ssl")
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["min"], max: ssl_config["max"] }
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["cert_file_name"]))
99
+ @context.cert = OpenSSL::X509::Certificate.new(File.read(ssl_config['cert_file_name']))
100
100
 
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"]))
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["key_type"]}"
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["macaw"]["ssl"]["cert_file_name"]} and
112
- #{@macaw.config["macaw"]["ssl"]["key_file_name"]}. Please assure the files exist and their names are correct.")
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("macaw", "session", "invalidation_time")
120
- MemoryInvalidationMiddleware.new(@macaw.config["macaw"]["session"]["invalidation_time"])
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 if @macaw.session
132
+ set_session
131
133
  set_ssl
132
134
  end
133
135
  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
@@ -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,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("/", ".").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
+ 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("?", 2)
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 "../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.1'
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
+ puts('Stopping server')
157
158
  @server.close
158
- puts("Macaw stop flying for some seeds...")
159
+ puts('Macaw stop flying for some seeds...')
159
160
  else
160
- @macaw_log.info("Stopping server")
161
+ @macaw_log.info('Stopping server')
161
162
  @server.close
162
- @macaw_log.info("Macaw stop flying for some seeds...")
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.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-05-04 00:00:00.000000000 Z
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: 2.7.0
87
+ version: 3.0.0
88
88
  required_rubygems_version: !ruby/object:Gem::Requirement
89
89
  requirements:
90
90
  - - ">="