macaw_framework 1.3.1 → 1.3.3

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: faee0746cfa34d7020272e9c6a3d24f12bff1c88ef3a5f8bf1f65be5dc7de8b2
4
- data.tar.gz: f3dfa0a71a12df9d0fdf07fcedcf9fe8cc29db81c20aa003482f5a465a330648
3
+ metadata.gz: f5ce341c22fbc784508cd3678e12b800d9313bb3966ca933d4970798fc5d8d14
4
+ data.tar.gz: 7a4d3d9926365062786b919e7114c16d09e14ae9e1a6828f49322985fb9d8f9e
5
5
  SHA512:
6
- metadata.gz: 92b61e93524104407eba6c9d2621990a7cc6b68e0733a78fedc75cff6fc248601410344aa421dec9e79f77a52b391d7f2e102cb54f639a127ebeaf4b00c5f2fb
7
- data.tar.gz: 3eb62e30474e588dd0200f981aaa847ddf2725d7092d9c4387a0a5c63c2870f2ae87bd7f7998d0149dd00c592d144d0d6188464de6a473334ec94733967aef3d
6
+ metadata.gz: 92d923dfa4e841177186a4b6c35c7a41a543c19b2d0036fc7caa7fd9194f39479d602b8083966ad19930d9992aba88b9f678d78f1cc63b20be715df07e79f848
7
+ data.tar.gz: 65221f77161d6c3903c4d71c7dd2146371f4f846aa49419cedd457377c955805dbbda9de1f8e554966c36e44f974c236a395f6bb8e338b78891a433dc1b2fb56
data/.rubocop.yml CHANGED
@@ -29,3 +29,9 @@ Metrics/ModuleLength:
29
29
 
30
30
  Naming/MemoizedInstanceVariableName:
31
31
  Enabled: false
32
+
33
+ Naming/MethodName:
34
+ Enabled: false
35
+
36
+ Style/ClassAndModuleChildren:
37
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -147,3 +147,13 @@
147
147
  - Fixing bug where missing session configuration on `application.json` break the application
148
148
  - Including a Cache module for manual caching.
149
149
 
150
+ ## [1.3.21]
151
+ - Refactoring shutdown method
152
+ - Fixing a bug where a HTTP call without client data broke the parser
153
+ - Removing logs registering new HTTP connections to reduce log bloat
154
+
155
+ ## [1.3.22]
156
+ - Fixing error with tests on Ruby 3.4.x due to splash operator
157
+
158
+ ## [1.3.3]
159
+ - Add missing dependencies for Ruby 4.x
data/Gemfile CHANGED
@@ -4,11 +4,13 @@ source 'https://rubygems.org'
4
4
 
5
5
  gemspec
6
6
 
7
+ gem 'logger', '~> 1.7'
7
8
  gem 'openssl'
8
9
  gem 'prometheus-client', '~> 4.1'
9
10
 
10
11
  group :test do
11
12
  gem 'minitest', '~> 5.0'
13
+ gem 'ostruct', '~> 0.6.3'
12
14
  gem 'rake', '~> 13.0'
13
15
  gem 'rubocop', '~> 1.21'
14
16
  gem 'simplecov', '~> 0.21.2'
data/README.md CHANGED
@@ -2,8 +2,8 @@
2
2
  # MacawFramework
3
3
 
4
4
  MacawFramework is a lightweight, easy-to-use web framework for Ruby designed to simplify the development of small to
5
- medium-sized web applications. With support for various HTTP methods, caching, and session management, MacawFramework
6
- provides developers with the essential tools to quickly build and deploy their applications.
5
+ medium-sized web applications. Weighting less than 26Kb with support for various HTTP methods, caching, and session management,
6
+ MacawFramework provides developers with the essential tools to quickly build and deploy their applications.
7
7
 
8
8
  - [MacawFramework](#macawframework)
9
9
  * [Features](#features)
@@ -31,7 +31,8 @@ provides developers with the essential tools to quickly build and deploy their a
31
31
  - Session management with server-side in-memory storage
32
32
  - Basic rate limiting and SSL support
33
33
  - Prometheus integration for monitoring and metrics
34
- - Lightweight and easy to learn
34
+ - Less than 26Kb
35
+ - Easy to learn
35
36
 
36
37
  ## Installation
37
38
 
@@ -3,15 +3,15 @@
3
3
  ##
4
4
  # Aspect that provide cache for the endpoints.
5
5
  module CacheAspect
6
- def call_endpoint(cache, *args)
7
- return super(*args) unless !cache[:cache].nil? && cache[:endpoints_to_cache]&.include?(args[0])
6
+ def call_endpoint(cache, *args, **kwargs)
7
+ return super(*args, **kwargs) unless !cache[:cache].nil? && cache[:endpoints_to_cache]&.include?(args[0])
8
8
 
9
9
  cache_filtered_name = cache_name_filter(args[1], cache[:cached_methods][args[0]])
10
10
 
11
11
  cache[:cache].mutex.synchronize do
12
12
  return cache[:cache].cache[cache_filtered_name][0] unless cache[:cache].cache[cache_filtered_name].nil?
13
13
 
14
- response = super(*args)
14
+ response = super(*args, **kwargs)
15
15
  cache[:cache].cache[cache_filtered_name] = [response, Time.now] if should_cache_response?(response[1])
16
16
  response
17
17
  end
@@ -8,11 +8,8 @@ require_relative '../data_filters/log_data_filter'
8
8
  # the input and output of every endpoint called
9
9
  # in the framework.
10
10
  module LoggingAspect
11
- def call_endpoint(logger, *args)
12
- return super(*args) if logger.nil?
13
-
14
- endpoint_name = args[1].split('.')[1..].join('/')
15
- logger.info("Request received for [#{endpoint_name}] from [#{args[-1]}]")
11
+ def call_endpoint(logger, *args, **kwargs)
12
+ return super(*args, **kwargs) if logger.nil?
16
13
 
17
14
  begin
18
15
  response = super(*args)
@@ -3,8 +3,8 @@
3
3
  ##
4
4
  # Aspect that provides application metrics using prometheus.
5
5
  module PrometheusAspect
6
- def call_endpoint(prometheus_middleware, *args)
7
- return super(*args) if prometheus_middleware.nil?
6
+ def call_endpoint(prometheus_middleware, *args, **kwargs)
7
+ return super(*args, **kwargs) if prometheus_middleware.nil?
8
8
 
9
9
  start_time = Time.now
10
10
 
@@ -0,0 +1,91 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Main module for all Macaw classes
5
+ module MacawFramework; end
6
+
7
+ ##
8
+ # This singleton class allows to manually cache
9
+ # parameters and other data.
10
+ class MacawFramework::Cache
11
+ include Singleton
12
+
13
+ attr_accessor :invalidation_frequency
14
+
15
+ ##
16
+ # Write a value to Cache memory.
17
+ # Can be called statically or from an instance.
18
+ # @param {String} tag
19
+ # @param {Object} value
20
+ # @param {Integer} expires_in Defaults to 3600.
21
+ # @return nil
22
+ #
23
+ # @example
24
+ # MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
25
+ def self.write(tag, value, expires_in: 3600)
26
+ MacawFramework::Cache.instance.write(tag, value, expires_in: expires_in)
27
+ end
28
+
29
+ ##
30
+ # Write a value to Cache memory.
31
+ # Can be called statically or from an instance.
32
+ # @param {String} tag
33
+ # @param {Object} value
34
+ # @param {Integer} expires_in Defaults to 3600.
35
+ # @return nil
36
+ #
37
+ # @example
38
+ # MacawFramework::Cache.write("name", "Maria", expires_in: 7200)
39
+ def write(tag, value, expires_in: 3600)
40
+ if read(tag).nil?
41
+ @mutex.synchronize do
42
+ @cache.store(tag, { value: value, expires_in: Time.now + expires_in })
43
+ end
44
+ else
45
+ @cache[tag][:value] = value
46
+ @cache[tag][:expires_in] = Time.now + expires_in
47
+ end
48
+ end
49
+
50
+ ##
51
+ # Read the value with the specified tag.
52
+ # Can be called statically or from an instance.
53
+ # @param {String} tag
54
+ # @return {String|nil}
55
+ #
56
+ # @example
57
+ # MacawFramework::Cache.read("name") # Maria
58
+ def self.read(tag) = MacawFramework::Cache.instance.read(tag)
59
+
60
+ ##
61
+ # Read the value with the specified tag.
62
+ # Can be called statically or from an instance.
63
+ # @param {String} tag
64
+ # @return {String|nil}
65
+ #
66
+ # @example
67
+ # MacawFramework::Cache.read("name") # Maria
68
+ def read(tag) = @cache.dig(tag, :value)
69
+
70
+ private
71
+
72
+ def initialize
73
+ @cache = {}
74
+ @mutex = Mutex.new
75
+ @invalidation_frequency = 60
76
+ invalidate_cache
77
+ end
78
+
79
+ def invalidate_cache
80
+ @invalidator = Thread.new(&method(:invalidation_process))
81
+ end
82
+
83
+ def invalidation_process
84
+ loop do
85
+ sleep @invalidation_frequency
86
+ @mutex.synchronize do
87
+ @cache.delete_if { |_, v| v[:expires_in] < Time.now }
88
+ end
89
+ end
90
+ end
91
+ end
@@ -39,14 +39,13 @@ module ServerBase
39
39
  end
40
40
 
41
41
  def handle_client(client)
42
- path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
42
+ _path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
43
43
  raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
44
44
  raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3])
45
45
 
46
46
  client_data = get_client_data(body, headers, parameters)
47
47
  session_id = declare_client_session(client_data[:headers], @macaw.secure_header) if @macaw.session
48
48
 
49
- @macaw_log&.info("Running #{path.gsub("\n", '').gsub("\r", '')}")
50
49
  message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
51
50
  method_name, client_data, session_id, client.peeraddr[3])
52
51
  response_headers ||= {}
@@ -74,8 +74,17 @@ class ThreadServer
74
74
 
75
75
  ##
76
76
  # Method Responsible for closing the TCP server.
77
- def close
78
- shutdown
77
+ def shutdown
78
+ @is_shutting_down = true
79
+ loop do
80
+ break if @work_queue.empty?
81
+
82
+ sleep 0.1
83
+ end
84
+
85
+ @num_threads.times { @work_queue << :shutdown }
86
+ @workers.each(&:join)
87
+ @server.close
79
88
  end
80
89
 
81
90
  private
@@ -107,17 +116,4 @@ class ThreadServer
107
116
  end
108
117
  end
109
118
  end
110
-
111
- def shutdown
112
- @is_shutting_down = true
113
- loop do
114
- break if @work_queue.empty?
115
-
116
- sleep 0.1
117
- end
118
-
119
- @num_threads.times { @work_queue << :shutdown }
120
- @workers.each(&:join)
121
- @server.close
122
- end
123
119
  end
@@ -11,7 +11,7 @@ module RequestDataFiltering
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)
@@ -26,15 +26,15 @@ module RequestDataFiltering
26
26
 
27
27
  selected_route = nil
28
28
  routes.each do |route|
29
- split_route = route.split('.')
30
- split_name = method_name.split('.')
29
+ split_route = route&.split('.')
30
+ split_name = method_name&.split('.')
31
31
 
32
- next unless split_route.length == split_name.length
32
+ next unless split_route&.length == split_name&.length
33
33
  next unless match_path_with_route(split_name, split_route)
34
34
 
35
35
  selected_route = route
36
- split_route.each_with_index do |var, index|
37
- parameters[var[1..].to_sym] = split_name[index] if var =~ VARIABLE_PATTERN
36
+ split_route&.each_with_index do |var, index|
37
+ parameters[var[1..].to_sym] = split_name&.dig(index) if var =~ VARIABLE_PATTERN
38
38
  end
39
39
  break
40
40
  end
@@ -45,7 +45,7 @@ module RequestDataFiltering
45
45
  end
46
46
 
47
47
  def self.match_path_with_route(split_path, split_route)
48
- split_route.each_with_index do |var, index|
48
+ split_route&.each_with_index do |var, index|
49
49
  return false if var != split_path[index] && !var.match?(VARIABLE_PATTERN)
50
50
  end
51
51
 
@@ -56,26 +56,28 @@ module RequestDataFiltering
56
56
  # Method responsible for sanitizing the method name
57
57
  def self.sanitize_method_name(path)
58
58
  path = extract_path(path)
59
- method_name = path.gsub('/', '.').strip.downcase
60
- method_name.gsub!(' ', '')
59
+ method_name = path&.gsub('/', '.')&.strip&.downcase
60
+ method_name&.gsub!(' ', '')
61
61
  method_name
62
62
  end
63
63
 
64
64
  ##
65
65
  # Method responsible for extracting the path from URI
66
66
  def self.extract_path(path)
67
+ return path if path.nil?
68
+
67
69
  path[0] == '/' ? path[1..].gsub('/', '.') : path.gsub('/', '.')
68
70
  end
69
71
 
70
72
  ##
71
73
  # Method responsible for extracting the headers from request
72
74
  def self.extract_headers(client)
73
- header = client.gets.delete("\n").delete("\r")
75
+ header = client.gets&.delete("\n")&.delete("\r")
74
76
  headers = {}
75
- while header.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
77
+ while header&.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
76
78
  split_header = header.split(':')
77
79
  headers[split_header[0].strip] = split_header[1].strip
78
- header = client.gets.delete("\n").delete("\r")
80
+ header = client.gets&.delete("\n")&.delete("\r")
79
81
  end
80
82
  [header, headers]
81
83
  end
@@ -83,7 +85,7 @@ module RequestDataFiltering
83
85
  ##
84
86
  # Method responsible for extracting the body from request
85
87
  def self.extract_body(client, body_first_line, content_length)
86
- body = client.read(content_length)
88
+ body = client&.read(content_length)
87
89
  body_first_line << body.to_s
88
90
  end
89
91
 
@@ -107,13 +109,13 @@ module RequestDataFiltering
107
109
  ##
108
110
  # Method responsible for sanitizing the parameter name
109
111
  def self.sanitize_parameter_name(name)
110
- name.gsub(/[^\w\s]/, '')
112
+ name&.gsub(/[^\w\s]/, '')
111
113
  end
112
114
 
113
115
  ##
114
116
  # Method responsible for sanitizing the parameter value
115
117
  def self.sanitize_parameter_value(value)
116
- value.gsub(/[^\w\s]/, '')
117
- value.gsub(/\s/, '')
118
+ value&.gsub(/[^\w\s]/, '')
119
+ value&.gsub(/\s/, '')
118
120
  end
119
121
  end
@@ -0,0 +1,264 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'errors/endpoint_not_mapped_error'
4
+ require_relative 'middlewares/prometheus_middleware'
5
+ require_relative 'data_filters/request_data_filtering'
6
+ require_relative 'middlewares/memory_invalidation_middleware'
7
+ require_relative 'core/cron_runner'
8
+ require_relative 'core/thread_server'
9
+ require_relative 'version'
10
+ require 'prometheus/client'
11
+ require 'securerandom'
12
+ require 'singleton'
13
+ require 'pathname'
14
+ require 'logger'
15
+ require 'socket'
16
+ require 'json'
17
+
18
+ ##
19
+ # Main module for all Macaw classes
20
+ module MacawFramework; end
21
+
22
+ ##
23
+ # Class responsible for creating endpoints and
24
+ # starting the web server.
25
+ class MacawFramework::Macaw
26
+ attr_reader :routes, :macaw_log, :config, :jobs, :cached_methods, :secure_header, :session
27
+ attr_accessor :port, :bind, :threads
28
+
29
+ ##
30
+ # Initialize Macaw Class
31
+ # @param {Logger} custom_log
32
+ # @param {ThreadServer} server
33
+ # @param {String?} dir
34
+ def initialize(custom_log: Logger.new($stdout), server: ThreadServer, dir: nil)
35
+ apply_options(custom_log)
36
+ create_endpoint_public_files(dir)
37
+ setup_default_configs
38
+ @server_class = server
39
+ end
40
+
41
+ ##
42
+ # Creates a GET endpoint associated
43
+ # with the respective path.
44
+ # @param {String} path
45
+ # @param {Proc} block
46
+ #
47
+ # @example
48
+ # macaw = MacawFramework::Macaw.new
49
+ # macaw.get("/hello") do |context|
50
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
51
+ # end
52
+ ##
53
+ def get(path, cache: [], &block)
54
+ map_new_endpoint('get', cache, path, &block)
55
+ end
56
+
57
+ ##
58
+ # Creates a POST endpoint associated
59
+ # with the respective path.
60
+ # @param {String} path
61
+ # @param {Boolean} cache
62
+ # @param {Proc} block
63
+ # @example
64
+ #
65
+ # macaw = MacawFramework::Macaw.new
66
+ # macaw.post("/hello") do |context|
67
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
68
+ # end
69
+ ##
70
+ def post(path, cache: [], &block)
71
+ map_new_endpoint('post', cache, path, &block)
72
+ end
73
+
74
+ ##
75
+ # Creates a PUT endpoint associated
76
+ # with the respective path.
77
+ # @param {String} path
78
+ # @param {Proc} block
79
+ # @example
80
+ #
81
+ # macaw = MacawFramework::Macaw.new
82
+ # macaw.put("/hello") do |context|
83
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
84
+ # end
85
+ ##
86
+ def put(path, cache: [], &block)
87
+ map_new_endpoint('put', cache, path, &block)
88
+ end
89
+
90
+ ##
91
+ # Creates a PATCH endpoint associated
92
+ # with the respective path.
93
+ # @param {String} path
94
+ # @param {Proc} block
95
+ # @example
96
+ #
97
+ # macaw = MacawFramework::Macaw.new
98
+ # macaw.patch("/hello") do |context|
99
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
100
+ # end
101
+ ##
102
+ def patch(path, cache: [], &block)
103
+ map_new_endpoint('patch', cache, path, &block)
104
+ end
105
+
106
+ ##
107
+ # Creates a DELETE endpoint associated
108
+ # with the respective path.
109
+ # @param {String} path
110
+ # @param {Proc} block
111
+ # @example
112
+ #
113
+ # macaw = MacawFramework::Macaw.new
114
+ # macaw.delete("/hello") do |context|
115
+ # return "Hello World!", 200, { "Content-Type" => "text/plain" }
116
+ # end
117
+ ##
118
+ def delete(path, cache: [], &block)
119
+ map_new_endpoint('delete', cache, path, &block)
120
+ end
121
+
122
+ ##
123
+ # Spawn and start a thread running the defined periodic job.
124
+ # @param {Integer} interval
125
+ # @param {Integer?} start_delay
126
+ # @param {String} job_name
127
+ # @param {Proc} block
128
+ # @example
129
+ #
130
+ # macaw = MacawFramework::Macaw.new
131
+ # macaw.setup_job(interval: 60, start_delay: 60, job_name: "job 1") do
132
+ # puts "I'm a periodic job that runs every minute"
133
+ # end
134
+ ##
135
+ def setup_job(interval: 60, start_delay: 0, job_name: "job_#{SecureRandom.uuid}", &block)
136
+ @cron_runner ||= CronRunner.new(self)
137
+ @jobs ||= []
138
+ @cron_runner.start_cron_job_thread(interval, start_delay, job_name, &block)
139
+ @jobs << job_name
140
+ end
141
+
142
+ ##
143
+ # Starts the web server
144
+ def start!
145
+ if @macaw_log.nil?
146
+ puts('---------------------------------')
147
+ puts("Starting server at port #{@port}")
148
+ puts("Number of threads: #{@threads}")
149
+ puts('---------------------------------')
150
+ else
151
+ @macaw_log.info('---------------------------------')
152
+ @macaw_log.info("Starting server at port #{@port}")
153
+ @macaw_log.info("Number of threads: #{@threads}")
154
+ @macaw_log.info('---------------------------------')
155
+ end
156
+ @server = @server_class.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
157
+ server_loop(@server)
158
+ rescue Interrupt
159
+ if @macaw_log.nil?
160
+ puts('Stopping server')
161
+ @server.shutdown
162
+ puts('Macaw stop flying for some seeds...')
163
+ else
164
+ @macaw_log.info('Stopping server')
165
+ @server.shutdown
166
+ @macaw_log.info('Macaw stop flying for some seeds...')
167
+ end
168
+ end
169
+
170
+ ##
171
+ # This method is intended to start the framework
172
+ # without an web server. This can be useful when
173
+ # you just want to keep cron jobs running, without
174
+ # mapping any HTTP endpoints.
175
+ def start_without_server!
176
+ @macaw_log.nil? ? puts('Application starting') : @macaw_log.info('Application starting')
177
+ loop { sleep(3600) }
178
+ rescue Interrupt
179
+ @macaw_log.nil? ? puts('Macaw stop flying for some seeds.') : @macaw_log.info('Macaw stop flying for some seeds.')
180
+ end
181
+
182
+ private
183
+
184
+ def setup_default_configs
185
+ @port ||= 8080
186
+ @bind ||= 'localhost'
187
+ @config ||= nil
188
+ @threads ||= 200
189
+ @endpoints_to_cache = []
190
+ @prometheus ||= nil
191
+ @prometheus_middleware ||= nil
192
+ end
193
+
194
+ def apply_options(custom_log)
195
+ setup_basic_config(custom_log)
196
+ setup_session
197
+ setup_cache
198
+ setup_prometheus
199
+ rescue StandardError => e
200
+ @macaw_log&.warn(e.message)
201
+ end
202
+
203
+ def setup_cache
204
+ return if @config['macaw']['cache'].nil?
205
+
206
+ @cache = MemoryInvalidationMiddleware.new(@config['macaw']['cache']['cache_invalidation'].to_i || 3_600)
207
+ end
208
+
209
+ def setup_session
210
+ @session = false
211
+ return if @config['macaw']['session'].nil?
212
+
213
+ @session = true
214
+ @secure_header = @config['macaw']['session']['secure_header'] || 'X-Session-ID'
215
+ end
216
+
217
+ def setup_basic_config(custom_log)
218
+ @routes = []
219
+ @cached_methods = {}
220
+ @macaw_log ||= custom_log
221
+ @config = JSON.parse(File.read('application.json'))
222
+ @port = @config['macaw']['port'] || 8080
223
+ @bind = @config['macaw']['bind'] || 'localhost'
224
+ @threads = @config['macaw']['threads'] || 200
225
+ end
226
+
227
+ def setup_prometheus
228
+ return unless @config['macaw']['prometheus']
229
+
230
+ @prometheus = Prometheus::Client::Registry.new
231
+ @prometheus_middleware = PrometheusMiddleware.new
232
+ @prometheus_middleware&.configure_prometheus(@prometheus, @config, self)
233
+ end
234
+
235
+ def server_loop(server)
236
+ server.run
237
+ end
238
+
239
+ def map_new_endpoint(prefix, cache, path, &block)
240
+ @endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}" unless cache.empty?
241
+ @cached_methods["#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}"] = cache unless cache.empty?
242
+ path_clean = RequestDataFiltering.extract_path(path)
243
+ slash = path[0] == '/' ? '' : '/'
244
+ @macaw_log&.info("Defining #{prefix.upcase} endpoint at #{slash}#{path}")
245
+ define_singleton_method("#{prefix}.#{path_clean}", block || lambda {
246
+ |context = { headers: {}, body: '', params: {} }|
247
+ })
248
+ @routes << "#{prefix}.#{path_clean}"
249
+ end
250
+
251
+ def get_files_public_folder(dir)
252
+ return [] if dir.nil?
253
+
254
+ folder_path = Pathname.new(File.expand_path('public', dir))
255
+ file_paths = folder_path.glob('**/*').select(&:file?)
256
+ file_paths.map { |path| "public/#{path.relative_path_from(folder_path)}" }
257
+ end
258
+
259
+ def create_endpoint_public_files(dir)
260
+ get_files_public_folder(dir).each do |file|
261
+ get(file) { |_context| return File.read(file).to_s, 200, {} }
262
+ end
263
+ end
264
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = '1.3.1'
4
+ VERSION = '1.3.3'
5
5
  end
@@ -1,348 +1,8 @@
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 'singleton'
13
- require 'pathname'
14
- require 'logger'
15
- require 'socket'
16
- require 'json'
3
+ ##
4
+ # Main module for all Macaw classes
5
+ module MacawFramework; end
17
6
 
18
- module MacawFramework
19
- ##
20
- # Class responsible for creating endpoints and
21
- # starting the web server.
22
- class Macaw
23
- attr_reader :routes, :macaw_log, :config, :jobs, :cached_methods, :secure_header, :session
24
- attr_accessor :port, :bind, :threads
25
-
26
- ##
27
- # Initialize Macaw Class
28
- # @param {Logger} custom_log
29
- # @param {ThreadServer} server
30
- # @param {String?} dir
31
- def initialize(custom_log: Logger.new($stdout), server: ThreadServer, dir: nil)
32
- apply_options(custom_log)
33
- create_endpoint_public_files(dir)
34
- setup_default_configs
35
- @server_class = server
36
- end
37
-
38
- ##
39
- # Creates a GET endpoint associated
40
- # with the respective path.
41
- # @param {String} path
42
- # @param {Proc} block
43
- #
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
- ##
50
- def get(path, cache: [], &block)
51
- map_new_endpoint('get', cache, path, &block)
52
- end
53
-
54
- ##
55
- # Creates a POST endpoint associated
56
- # with the respective path.
57
- # @param {String} path
58
- # @param {Boolean} cache
59
- # @param {Proc} block
60
- # @example
61
- #
62
- # macaw = MacawFramework::Macaw.new
63
- # macaw.post("/hello") do |context|
64
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
65
- # end
66
- ##
67
- def post(path, cache: [], &block)
68
- map_new_endpoint('post', cache, path, &block)
69
- end
70
-
71
- ##
72
- # Creates a PUT endpoint associated
73
- # with the respective path.
74
- # @param {String} path
75
- # @param {Proc} block
76
- # @example
77
- #
78
- # macaw = MacawFramework::Macaw.new
79
- # macaw.put("/hello") do |context|
80
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
81
- # end
82
- ##
83
- def put(path, cache: [], &block)
84
- map_new_endpoint('put', cache, path, &block)
85
- end
86
-
87
- ##
88
- # Creates a PATCH endpoint associated
89
- # with the respective path.
90
- # @param {String} path
91
- # @param {Proc} block
92
- # @example
93
- #
94
- # macaw = MacawFramework::Macaw.new
95
- # macaw.patch("/hello") do |context|
96
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
97
- # end
98
- ##
99
- def patch(path, cache: [], &block)
100
- map_new_endpoint('patch', cache, path, &block)
101
- end
102
-
103
- ##
104
- # Creates a DELETE endpoint associated
105
- # with the respective path.
106
- # @param {String} path
107
- # @param {Proc} block
108
- # @example
109
- #
110
- # macaw = MacawFramework::Macaw.new
111
- # macaw.delete("/hello") do |context|
112
- # return "Hello World!", 200, { "Content-Type" => "text/plain" }
113
- # end
114
- ##
115
- def delete(path, cache: [], &block)
116
- map_new_endpoint('delete', cache, path, &block)
117
- end
118
-
119
- ##
120
- # Spawn and start a thread running the defined periodic job.
121
- # @param {Integer} interval
122
- # @param {Integer?} start_delay
123
- # @param {String} job_name
124
- # @param {Proc} block
125
- # @example
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 periodic job that runs every minute"
130
- # end
131
- ##
132
- def setup_job(interval: 60, start_delay: 0, job_name: "job_#{SecureRandom.uuid}", &block)
133
- @cron_runner ||= CronRunner.new(self)
134
- @jobs ||= []
135
- @cron_runner.start_cron_job_thread(interval, start_delay, job_name, &block)
136
- @jobs << job_name
137
- end
138
-
139
- ##
140
- # Starts the web server
141
- def start!
142
- if @macaw_log.nil?
143
- puts('---------------------------------')
144
- puts("Starting server at port #{@port}")
145
- puts("Number of threads: #{@threads}")
146
- puts('---------------------------------')
147
- else
148
- @macaw_log.info('---------------------------------')
149
- @macaw_log.info("Starting server at port #{@port}")
150
- @macaw_log.info("Number of threads: #{@threads}")
151
- @macaw_log.info('---------------------------------')
152
- end
153
- @server = @server_class.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
154
- server_loop(@server)
155
- rescue Interrupt
156
- if @macaw_log.nil?
157
- puts('Stopping server')
158
- @server.close
159
- puts('Macaw stop flying for some seeds...')
160
- else
161
- @macaw_log.info('Stopping server')
162
- @server.close
163
- @macaw_log.info('Macaw stop flying for some seeds...')
164
- end
165
- end
166
-
167
- ##
168
- # This method is intended to start the framework
169
- # without an web server. This can be useful when
170
- # you just want to keep cron jobs running, without
171
- # mapping any HTTP endpoints.
172
- def start_without_server!
173
- @macaw_log.nil? ? puts('Application starting') : @macaw_log.info('Application starting')
174
- loop { sleep(3600) }
175
- rescue Interrupt
176
- @macaw_log.nil? ? puts('Macaw stop flying for some seeds.') : @macaw_log.info('Macaw stop flying for some seeds.')
177
- end
178
-
179
- private
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
-
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)
215
- @routes = []
216
- @cached_methods = {}
217
- @macaw_log ||= custom_log
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)
230
- end
231
-
232
- def server_loop(server)
233
- server.run
234
- end
235
-
236
- def map_new_endpoint(prefix, cache, path, &block)
237
- @endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}" unless cache.empty?
238
- @cached_methods["#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}"] = cache unless cache.empty?
239
- path_clean = RequestDataFiltering.extract_path(path)
240
- slash = path[0] == '/' ? '' : '/'
241
- @macaw_log&.info("Defining #{prefix.upcase} endpoint at #{slash}#{path}")
242
- define_singleton_method("#{prefix}.#{path_clean}", block || lambda {
243
- |context = { headers: {}, body: '', params: {} }|
244
- })
245
- @routes << "#{prefix}.#{path_clean}"
246
- end
247
-
248
- def get_files_public_folder(dir)
249
- return [] if dir.nil?
250
-
251
- folder_path = Pathname.new(File.expand_path('public', dir))
252
- file_paths = folder_path.glob('**/*').select(&:file?)
253
- file_paths.map { |path| "public/#{path.relative_path_from(folder_path)}" }
254
- end
255
-
256
- def create_endpoint_public_files(dir)
257
- get_files_public_folder(dir).each do |file|
258
- get(file) { |_context| return File.read(file).to_s, 200, {} }
259
- end
260
- end
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
348
- end
7
+ require_relative 'macaw_framework/macaw'
8
+ require_relative 'macaw_framework/cache'
metadata CHANGED
@@ -1,14 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macaw_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.1
4
+ version: 1.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2024-09-07 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
13
  name: prometheus-client
@@ -46,6 +45,7 @@ files:
46
45
  - lib/macaw_framework/aspects/cache_aspect.rb
47
46
  - lib/macaw_framework/aspects/logging_aspect.rb
48
47
  - lib/macaw_framework/aspects/prometheus_aspect.rb
48
+ - lib/macaw_framework/cache.rb
49
49
  - lib/macaw_framework/core/common/server_base.rb
50
50
  - lib/macaw_framework/core/cron_runner.rb
51
51
  - lib/macaw_framework/core/thread_server.rb
@@ -54,6 +54,7 @@ files:
54
54
  - lib/macaw_framework/data_filters/response_data_filter.rb
55
55
  - lib/macaw_framework/errors/endpoint_not_mapped_error.rb
56
56
  - lib/macaw_framework/errors/too_many_requests_error.rb
57
+ - lib/macaw_framework/macaw.rb
57
58
  - lib/macaw_framework/middlewares/memory_invalidation_middleware.rb
58
59
  - lib/macaw_framework/middlewares/prometheus_middleware.rb
59
60
  - lib/macaw_framework/middlewares/rate_limiter_middleware.rb
@@ -76,7 +77,6 @@ metadata:
76
77
  documentation_uri: https://rubydoc.info/gems/macaw_framework
77
78
  homepage_uri: https://github.com/ariasdiniz/macaw_framework
78
79
  source_code_uri: https://github.com/ariasdiniz/macaw_framework
79
- post_install_message:
80
80
  rdoc_options: []
81
81
  require_paths:
82
82
  - lib
@@ -91,8 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
91
91
  - !ruby/object:Gem::Version
92
92
  version: '0'
93
93
  requirements: []
94
- rubygems_version: 3.4.10
95
- signing_key:
94
+ rubygems_version: 4.0.3
96
95
  specification_version: 4
97
96
  summary: A lightweight back-end web framework
98
97
  test_files: []