macaw_framework 0.1.5 → 1.0.0

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: d900a18480da4792dfa03608ca8c85c0d0ba4084a3430c39008f4ca026ae784c
4
- data.tar.gz: e3bc35f7b78bc0a6e995c0abd906879dda434e4c12306d3d55713c35dc45065c
3
+ metadata.gz: 3b35cc5b2fba0adc8ded0f76db41699323cc3957d54382d19e554d0e3dc77f51
4
+ data.tar.gz: bcd983f6afe8b8864b5d2c012c120ead8290b521ed66f49477f8defb7eed2ede
5
5
  SHA512:
6
- metadata.gz: 6ac4eb646bb2510fbc67ddec9ccfca45bea8dfcb6833f9e55b60f0f18d5edebaf392645c53dae7745eec797c926af1700c273516682bd6f10e66c2b018f1cea6
7
- data.tar.gz: 0ecb2e16a9f1762833751bb403d824e04571c4f3b79f01355ffb6f1f29eb29c234befc759c7d76d7b939f05d5f9f3c252d8a5828198e9c7674b7854fe2edcdf1
6
+ metadata.gz: 9a7f7ef95d76e906b706a036d8cb299ba1b5e3be607ba8cdc13da216b11665b019635bca062ac83fc3067a18ab36148df12d3af2bdbc0fbe42fa3a8e0cb2c018
7
+ data.tar.gz: 56a97dff9ddac5f4142b82d441de3264d4044ace0f8f9496a2e0f49a8e384c6cf57ed705bcf68dd051c536b0dc4e4b9a643161f6563a7bcfe095f2b6f691e5ff
data/.rubocop.yml CHANGED
@@ -16,7 +16,13 @@ Metrics/MethodLength:
16
16
  Max: 30
17
17
 
18
18
  Metrics/AbcSize:
19
- Max: 35
19
+ Enabled: false
20
20
 
21
21
  Metrics/CyclomaticComplexity:
22
- Max: 10
22
+ Enabled: false
23
+
24
+ Metrics/ParameterLists:
25
+ Max: 15
26
+
27
+ Metrics/PerceivedComplexity:
28
+ Enabled: false
data/CHANGELOG.md CHANGED
@@ -24,3 +24,20 @@
24
24
  - Adding log by aspect on endpoint calls to improve observability
25
25
  - Moving the server for a new separate class to respect single responsibility
26
26
  - Improved the data filtering middleware to sanitize inputs
27
+
28
+ ## [0.1.5] - 2023-04-16
29
+
30
+ - Adding support to path variables
31
+
32
+ ## [0.2.0] - 2023-04-22
33
+
34
+ - Adding middleware for integration with Prometheus to collect metrics
35
+ - Adding a simple caching mechanism that can be enabled separately for each endpoint
36
+ - Performance and functional optimizations
37
+
38
+ ## [1.0.0] - 2023-04-28
39
+
40
+ - Adding support to HTTPS/SSL using security certificates
41
+ - Implemented a middleware for rate limiting to prevent DoS attacks
42
+ - Improvement of caching strategy to ignore optional headers
43
+ - First production-ready version
data/Gemfile CHANGED
@@ -11,4 +11,10 @@ gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "rubocop", "~> 1.21"
13
13
 
14
- gem "simplecov", "~> 0.22.0"
14
+ gem "prometheus-client", "~> 4.1"
15
+
16
+ group :test do
17
+ gem "simplecov", "~> 0.21.2"
18
+ gem "simplecov-json"
19
+ gem "simplecov_json_formatter", "~> 0.1.2"
20
+ end
data/README.md CHANGED
@@ -1,9 +1,7 @@
1
1
  # MacawFramework
2
2
 
3
- <img src="macaw_logo.png" alt= “” style="width: 30%;height: 30%;margin-left: 35%">
4
-
5
3
  This is a framework for developing web applications. Please have in mind that this is still a work in progress and
6
- it is strongly advised to not use it for production purposes for now. Actualy it supports only HTTP. HTTPS and SSL
4
+ it is strongly advised to not use it for production purposes for now. Actually it supports only HTTP. and HTTPS/SSL
7
5
  support will be implemented soon. Anyone who wishes to contribute is welcome.
8
6
 
9
7
  ## Installation
@@ -19,7 +17,6 @@ If bundler is not being used to manage dependencies, install the gem by executin
19
17
  ## Usage
20
18
 
21
19
  The usage of the framework still very simple. Actually it support 5 HTTP verbs: GET, POST, PUT, PATCH and DELETE.
22
- For now, the framework can't resolve client request body and headers. The support for this will be included soon.
23
20
 
24
21
  The default server port is 8080. To choose a different port, create a file with the name `application.json`
25
22
  in the same directory of the script that will start the application with the following content:
@@ -29,11 +26,43 @@ in the same directory of the script that will start the application with the fol
29
26
  "macaw": {
30
27
  "port": 8080,
31
28
  "bind": "localhost",
32
- "threads": 10
29
+ "threads": 10,
30
+ "cache": {
31
+ "cache_invalidation": 3600
32
+ },
33
+ "prometheus": {
34
+ "endpoint": "/metrics"
35
+ },
36
+ "rate_limiting": {
37
+ "window": 10,
38
+ "max_requests": 3,
39
+ "ignore_headers": [
40
+ "header-to-be-ignored-from-caching-strategy",
41
+ "another-header-to-be-ignored-from-caching-strategy"
42
+ ]
43
+ },
44
+ "ssl": {
45
+ "ssl": {
46
+ "cert_file_name": "path/to/cert/file/file.crt",
47
+ "key_file_name": "path/to/cert/key/file.key"
48
+ }
49
+ }
33
50
  }
34
51
  }
35
52
  ```
36
53
 
54
+ Cache invalidation time should be specified in seconds. In order to enable caching, The application.json file
55
+ should exist in the app main directory and it need the `cache_invalidation` config set. It is possible to
56
+ provide a list of strings in the property `ignore_headers`. All the client headers with the same name of any
57
+ of the strings provided will be ignored from caching strategy. This is useful to exclude headers like
58
+ correlation IDs from the caching strategy.
59
+
60
+ Rate Limit window should also be specified in seconds. Rate limit will be activated only if the `rate_limiting` config
61
+ exists inside `application.json`.
62
+
63
+ If the SSL configuration is provided in the `application.json` file with valid certificate and key files, the TCP server
64
+ will be wrapped with HTTPS security using the provided certificate.
65
+
37
66
  Example of usage:
38
67
 
39
68
  ```ruby
@@ -42,20 +71,32 @@ require 'json'
42
71
 
43
72
  m = MacawFramework::Macaw.new
44
73
 
45
- m.get('/hello_world') do |context|
74
+ m.get('/hello_world', cache: true) do |context|
75
+ context[:body] # Returns the request body as string
76
+ context[:params] # Returns query parameters and path variables as a hash
77
+ context[:headers] # Returns headers as a hash
78
+ return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200, {"Content-Type" => "application/json"}
79
+ end
80
+
81
+ m.post('/hello_world/:path_variable') do |context|
46
82
  context[:body] # Returns the request body as string
47
83
  context[:params] # Returns query parameters and path variables as a hash
48
84
  context[:headers] # Returns headers as a hash
85
+ context[:params][:path_variable] # The defined path variable can be found in :params
49
86
  return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
50
87
  end
51
88
 
52
89
  m.start!
53
90
  ```
54
91
 
55
- The above example will start a server and will create a GET endpoint at localhost/hello_world.
92
+ The example above starts a server and creates a GET endpoint at localhost/hello_world.
93
+
94
+ If prometheus is enabled, a get endpoint will be defined at path `/metrics` to collect prometheus metrics. This path
95
+ is configurable via the `application.json` file.
56
96
 
57
- The verb methods must always return a String or nil (Used as response) and a number corresponding the
58
- HTTP Status Code to be returned to the client.
97
+ The verb methods must always return a string or nil (used as the response), a number corresponding to the HTTP status
98
+ code to be returned to the client and the response headers as a Hash or nil. If an endpoint doesn't return a value or
99
+ returns nil for body, status code and headers, a default 200 OK status will be sent as the response.
59
100
 
60
101
  ## Contributing
61
102
 
@@ -0,0 +1,26 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Aspect that provide cache for the endpoints.
5
+ module CacheAspect
6
+ def call_endpoint(cache, *args)
7
+ return super(*args) unless !cache[:cache].nil? && cache[:endpoints_to_cache].include?(args[0])
8
+
9
+ cache_filtered_name = cache_name_filter(args[1], cache[:ignored_headers])
10
+
11
+ cache[:cache].mutex.synchronize do
12
+ return cache[:cache].cache[cache_filtered_name][0] unless cache[:cache].cache[cache_filtered_name].nil?
13
+
14
+ response = super(*args)
15
+ cache[:cache].cache[cache_filtered_name] = [response, Time.now]
16
+ response
17
+ end
18
+ end
19
+
20
+ private
21
+
22
+ def cache_name_filter(client_data, ignored_headers)
23
+ filtered_headers = client_data[:headers].filter { |key, _value| !ignored_headers.include?(key) }
24
+ [{ body: client_data[:body], params: client_data[:params], headers: filtered_headers }].to_s.to_sym
25
+ end
26
+ end
@@ -8,9 +8,17 @@ require "logger"
8
8
  # in the framework.
9
9
  module LoggingAspect
10
10
  def call_endpoint(logger, *args)
11
- logger.info("Input of #{args[0]}: #{args}")
12
- response = super(*args)
13
- logger.info("Output of #{args[0]} #{response}")
11
+ endpoint_name = args[1].split(".")[1..].join("/")
12
+ logger.info("Request received for #{endpoint_name} with arguments: #{args[2..]}")
13
+
14
+ begin
15
+ response = super(*args)
16
+ logger.info("Response for #{endpoint_name}: #{response}")
17
+ rescue StandardError => e
18
+ logger.error("Error processing #{endpoint_name}: #{e.message}\n#{e.backtrace.join("\n")}")
19
+ raise e
20
+ end
21
+
14
22
  response
15
23
  end
16
24
  end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Aspect that provides application metrics using prometheus.
5
+ module PrometheusAspect
6
+ def call_endpoint(prometheus_middleware, *args)
7
+ return super(*args) if prometheus_middleware.nil?
8
+
9
+ start_time = Time.now
10
+
11
+ begin
12
+ response = super(*args)
13
+ ensure
14
+ duration = (Time.now - start_time) * 1_000
15
+
16
+ endpoint_name = args[2].split(".").join("/")
17
+
18
+ prometheus_middleware.request_duration_milliseconds.with_labels(endpoint: endpoint_name).observe(duration)
19
+ prometheus_middleware.request_count.with_labels(endpoint: endpoint_name).increment
20
+ if response
21
+ prometheus_middleware.response_count.with_labels(endpoint: endpoint_name,
22
+ status: response[1]).increment
23
+ end
24
+ end
25
+
26
+ response
27
+ end
28
+ end
@@ -0,0 +1,144 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../middlewares/rate_limiter_middleware"
4
+ require_relative "../data_filters/response_data_filter"
5
+ require_relative "../errors/too_many_requests_error"
6
+ require_relative "../aspects/prometheus_aspect"
7
+ require_relative "../aspects/logging_aspect"
8
+ require_relative "../aspects/cache_aspect"
9
+ require "openssl"
10
+
11
+ ##
12
+ # Class responsible for providing a default
13
+ # webserver.
14
+ class Server
15
+ prepend CacheAspect
16
+ prepend LoggingAspect
17
+ prepend PrometheusAspect
18
+ # rubocop:disable Metrics/ParameterLists
19
+
20
+ ##
21
+ # Create a new instance of Server.
22
+ # @param {Macaw} macaw
23
+ # @param {Logger} logger
24
+ # @param {Integer} port
25
+ # @param {String} bind
26
+ # @param {Integer} num_threads
27
+ # @param {CachingMiddleware} cache
28
+ # @param {Prometheus::Client:Registry} prometheus
29
+ # @return {Server}
30
+ def initialize(macaw, endpoints_to_cache = nil, cache = nil, prometheus = nil, prometheus_mw = nil)
31
+ @port = macaw.port
32
+ @bind = macaw.bind
33
+ @macaw = macaw
34
+ @macaw_log = macaw.macaw_log
35
+ @num_threads = macaw.threads
36
+ @work_queue = Queue.new
37
+ ignored_headers = set_rate_limiting
38
+ set_ssl
39
+ @rate_limit ||= nil
40
+ ignored_headers ||= nil
41
+ @cache = { cache: cache, endpoints_to_cache: endpoints_to_cache || [], ignored_headers: ignored_headers }
42
+ @prometheus = prometheus
43
+ @prometheus_middleware = prometheus_mw
44
+ @workers = []
45
+ end
46
+
47
+ # rubocop:enable Metrics/ParameterLists
48
+
49
+ ##
50
+ # Start running the webserver.
51
+ def run
52
+ @server = TCPServer.new(@bind, @port)
53
+ @server = OpenSSL::SSL::SSLServer.new(@server, @context) if @context
54
+ @num_threads.times do
55
+ @workers << Thread.new do
56
+ loop do
57
+ client = @work_queue.pop
58
+ break if client == :shutdown
59
+
60
+ handle_client(client)
61
+ end
62
+ end
63
+ end
64
+
65
+ loop do
66
+ @work_queue << @server.accept
67
+ rescue OpenSSL::SSL::SSLError => e
68
+ @macaw_log.error("SSL error: #{e.message}")
69
+ rescue IOError, Errno::EBADF
70
+ break
71
+ end
72
+ end
73
+
74
+ ##
75
+ # Method Responsible for closing the TCP server.
76
+ def close
77
+ @server.close
78
+ @num_threads.times { @work_queue << :shutdown }
79
+ @workers.each(&:join)
80
+ end
81
+
82
+ private
83
+
84
+ def handle_client(client)
85
+ path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
86
+ raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
87
+ raise TooManyRequestsError unless @rate_limit.nil? || @rate_limit.allow?(client.peeraddr[3])
88
+
89
+ client_data = get_client_data(body, headers, parameters)
90
+
91
+ @macaw_log.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
92
+ message, status, response_headers = call_endpoint(@prometheus_middleware, @macaw_log, @cache,
93
+ method_name, client_data)
94
+ status ||= 200
95
+ message ||= nil
96
+ response_headers ||= nil
97
+ client.puts ResponseDataFilter.mount_response(status, response_headers, message)
98
+ rescue TooManyRequestsError
99
+ client.print "HTTP/1.1 429 Too Many Requests\r\n\r\n"
100
+ rescue EndpointNotMappedError
101
+ client.print "HTTP/1.1 404 Not Found\r\n\r\n"
102
+ rescue StandardError => e
103
+ client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
104
+ @macaw_log.info("Error: #{e}")
105
+ ensure
106
+ client.close
107
+ end
108
+
109
+ def set_rate_limiting
110
+ if @macaw.config&.dig("macaw", "rate_limiting")
111
+ ignored_headers = @macaw.config["macaw"]["rate_limiting"]["ignore_headers"] || []
112
+ @rate_limit = RateLimiterMiddleware.new(
113
+ @macaw.config["macaw"]["rate_limiting"]["window"].to_i || 1,
114
+ @macaw.config["macaw"]["rate_limiting"]["max_requests"].to_i || 60
115
+ )
116
+ end
117
+ ignored_headers
118
+ end
119
+
120
+ def set_ssl
121
+ if @macaw.config&.dig("macaw", "ssl")
122
+ @context = OpenSSL::SSL::SSLContext.new
123
+ @context.cert = OpenSSL::X509::Certificate.new(File.read(@macaw.config["macaw"]["ssl"]["cert_file_name"]))
124
+ @context.key = OpenSSL::PKey::RSA.new(File.read(@macaw.config["macaw"]["ssl"]["key_file_name"]))
125
+ end
126
+ @context ||= nil
127
+ rescue IOError => e
128
+ @macaw_log.error("It was not possible to read files #{@macaw.config["macaw"]["ssl"]["cert_file_name"]} and
129
+ #{@macaw.config["macaw"]["ssl"]["key_file_name"]}. Please assure the files exists and their names are correct.")
130
+ @macaw_log.error(e.backtrace)
131
+ raise e
132
+ end
133
+
134
+ def call_endpoint(name, client_data)
135
+ @macaw.send(
136
+ name.to_sym,
137
+ { headers: client_data[:headers], body: client_data[:body], params: client_data[:parameters] }
138
+ )
139
+ end
140
+
141
+ def get_client_data(body, headers, parameters)
142
+ { body: body, headers: headers, parameters: parameters }
143
+ end
144
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../utils/http_status_code"
4
+
5
+ ##
6
+ # Module responsible to filter and mount HTTP responses
7
+ module ResponseDataFilter
8
+ include HttpStatusCode
9
+
10
+ def self.mount_response(status, headers, body)
11
+ "#{mount_first_response_line(status, headers)}#{mount_response_headers(headers)}#{body}"
12
+ end
13
+
14
+ def self.mount_first_response_line(status, headers)
15
+ separator = " \r\n\r\n"
16
+ separator = " \r\n" unless headers.nil?
17
+
18
+ "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]}#{separator}"
19
+ end
20
+
21
+ def self.mount_response_headers(headers)
22
+ return nil if headers.nil?
23
+
24
+ response = ""
25
+ headers.each do |key, value|
26
+ response += "#{key}: #{value}\r\n"
27
+ end
28
+ response += "\r\n\r\n"
29
+ response
30
+ end
31
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class TooManyRequestsError < StandardError
4
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Middleware responsible for storing and
5
+ # invalidating cache.
6
+ class CachingMiddleware
7
+ attr_accessor :cache, :mutex
8
+
9
+ def initialize(inv_time_seconds = 3_600)
10
+ @cache = {}
11
+ @mutex = Mutex.new
12
+ Thread.new do
13
+ loop do
14
+ sleep(1)
15
+ @mutex.synchronize do
16
+ @cache.each_pair do |key, value|
17
+ @cache.delete(key) if Time.now - value[1] >= inv_time_seconds
18
+ end
19
+ end
20
+ end
21
+ end
22
+ sleep(2)
23
+ end
24
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "prometheus/client"
4
+ require "prometheus/client/formats/text"
5
+
6
+ ##
7
+ # Middleware responsible to configure prometheus
8
+ # defining metrics and an endpoint to access them.
9
+ class PrometheusMiddleware
10
+ attr_accessor :request_duration_milliseconds, :request_count, :response_count
11
+
12
+ def configure_prometheus(prometheus_registry, configurations, macaw)
13
+ return nil unless prometheus_registry
14
+
15
+ @request_duration_milliseconds = Prometheus::Client::Histogram.new(
16
+ :request_duration_milliseconds,
17
+ docstring: "The duration of each request in milliseconds",
18
+ labels: [:endpoint],
19
+ buckets: (100..1000).step(100).to_a + (2000..10_000).step(1000).to_a
20
+ )
21
+
22
+ @request_count = Prometheus::Client::Counter.new(
23
+ :request_count,
24
+ docstring: "The total number of requests received",
25
+ labels: [:endpoint]
26
+ )
27
+
28
+ @response_count = Prometheus::Client::Counter.new(
29
+ :response_count,
30
+ docstring: "The total number of responses sent",
31
+ labels: %i[endpoint status]
32
+ )
33
+
34
+ prometheus_registry.register(@request_duration_milliseconds)
35
+ prometheus_registry.register(@request_count)
36
+ prometheus_registry.register(@response_count)
37
+ prometheus_endpoint(prometheus_registry, configurations, macaw)
38
+ end
39
+
40
+ private
41
+
42
+ def prometheus_endpoint(prometheus_registry, configurations, macaw)
43
+ endpoint = configurations["macaw"]["prometheus"]["endpoint"] || "/metrics"
44
+ macaw.get(endpoint) do |_context|
45
+ [Prometheus::Client::Formats::Text.marshal(prometheus_registry), 200, { "Content-Type" => "plaintext" }]
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ ##
4
+ # Middleware responsible for implementing
5
+ # rate limiting
6
+ class RateLimiterMiddleware
7
+ attr_reader :window_size, :max_requests
8
+
9
+ def initialize(window_size, max_requests)
10
+ @window_size = window_size
11
+ @max_requests = max_requests
12
+ @client_timestamps = Hash.new { |key, value| key[value] = [] }
13
+ @mutex = Mutex.new
14
+ end
15
+
16
+ def allow?(client_id)
17
+ @mutex.synchronize do
18
+ now = Time.now.to_i
19
+ timestamps = @client_timestamps[client_id]
20
+
21
+ timestamps.reject! { |timestamp| timestamp <= now - window_size }
22
+
23
+ if timestamps.length < max_requests
24
+ timestamps << now
25
+ true
26
+ else
27
+ false
28
+ end
29
+ end
30
+ end
31
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = "0.1.5"
4
+ VERSION = "1.0.0"
5
5
  end
@@ -1,9 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "macaw_framework/errors/endpoint_not_mapped_error"
4
- require_relative "macaw_framework/middlewares/request_data_filtering"
5
- require_relative "macaw_framework/middlewares/server"
4
+ require_relative "macaw_framework/middlewares/prometheus_middleware"
5
+ require_relative "macaw_framework/data_filters/request_data_filtering"
6
+ require_relative "macaw_framework/middlewares/caching_middleware"
7
+ require_relative "macaw_framework/core/server"
6
8
  require_relative "macaw_framework/version"
9
+ require "prometheus/client"
7
10
  require "logger"
8
11
  require "socket"
9
12
  require "json"
@@ -15,7 +18,7 @@ module MacawFramework
15
18
  class Macaw
16
19
  ##
17
20
  # Array containing the routes defined in the application
18
- attr_reader :routes
21
+ attr_reader :routes, :port, :bind, :threads, :macaw_log, :config
19
22
 
20
23
  ##
21
24
  # @param {Logger} custom_log
@@ -23,17 +26,27 @@ module MacawFramework
23
26
  begin
24
27
  @routes = []
25
28
  @macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
26
- config = JSON.parse(File.read("application.json"))
27
- @port = config["macaw"]["port"]
28
- @bind = config["macaw"]["bind"]
29
- @threads = config["macaw"]["threads"].to_i
29
+ @config = JSON.parse(File.read("application.json"))
30
+ @port = @config["macaw"]["port"] || 8080
31
+ @bind = @config["macaw"]["bind"] || "localhost"
32
+ @threads = @config["macaw"]["threads"] || 5
33
+ unless @config["macaw"]["cache"].nil?
34
+ @cache = CachingMiddleware.new(@config["macaw"]["cache"]["cache_invalidation"].to_i || 3_600)
35
+ end
36
+ @prometheus = Prometheus::Client::Registry.new if @config["macaw"]["prometheus"]
37
+ @prometheus_middleware = PrometheusMiddleware.new if @config["macaw"]["prometheus"]
38
+ @prometheus_middleware.configure_prometheus(@prometheus, @config, self) if @config["macaw"]["prometheus"]
30
39
  rescue StandardError => e
31
40
  @macaw_log.error(e.message)
32
41
  end
33
42
  @port ||= 8080
34
43
  @bind ||= "localhost"
44
+ @config ||= nil
35
45
  @threads ||= 5
36
- @server = server.new(self, @macaw_log, @port, @bind, @threads)
46
+ @endpoints_to_cache = []
47
+ @prometheus ||= nil
48
+ @prometheus_middleware ||= nil
49
+ @server = server.new(self, @endpoints_to_cache, @cache, @prometheus, @prometheus_middleware)
37
50
  end
38
51
 
39
52
  ##
@@ -42,18 +55,19 @@ module MacawFramework
42
55
  # @param {String} path
43
56
  # @param {Proc} block
44
57
  # @return {Integer, String}
45
- def get(path, &block)
46
- map_new_endpoint("get", path, &block)
58
+ def get(path, cache: false, &block)
59
+ map_new_endpoint("get", cache, path, &block)
47
60
  end
48
61
 
49
62
  ##
50
63
  # Creates a POST endpoint associated
51
64
  # with the respective path.
52
65
  # @param {String} path
66
+ # @param {Boolean} cache
53
67
  # @param {Proc} block
54
68
  # @return {String, Integer}
55
- def post(path, &block)
56
- map_new_endpoint("post", path, &block)
69
+ def post(path, cache: false, &block)
70
+ map_new_endpoint("post", cache, path, &block)
57
71
  end
58
72
 
59
73
  ##
@@ -62,8 +76,8 @@ module MacawFramework
62
76
  # @param {String} path
63
77
  # @param {Proc} block
64
78
  # @return {String, Integer}
65
- def put(path, &block)
66
- map_new_endpoint("put", path, &block)
79
+ def put(path, cache: false, &block)
80
+ map_new_endpoint("put", cache, path, &block)
67
81
  end
68
82
 
69
83
  ##
@@ -72,8 +86,8 @@ module MacawFramework
72
86
  # @param {String} path
73
87
  # @param {Proc} block
74
88
  # @return {String, Integer}
75
- def patch(path, &block)
76
- map_new_endpoint("patch", path, &block)
89
+ def patch(path, cache: false, &block)
90
+ map_new_endpoint("patch", cache, path, &block)
77
91
  end
78
92
 
79
93
  ##
@@ -82,8 +96,8 @@ module MacawFramework
82
96
  # @param {String} path
83
97
  # @param {Proc} block
84
98
  # @return {String, Integer}
85
- def delete(path, &block)
86
- map_new_endpoint("delete", path, &block)
99
+ def delete(path, cache: false, &block)
100
+ map_new_endpoint("delete", cache, path, &block)
87
101
  end
88
102
 
89
103
  ##
@@ -106,7 +120,8 @@ module MacawFramework
106
120
  server.run
107
121
  end
108
122
 
109
- def map_new_endpoint(prefix, path, &block)
123
+ def map_new_endpoint(prefix, cache, path, &block)
124
+ @endpoints_to_cache << "#{prefix}.#{RequestDataFiltering.sanitize_method_name(path)}" if cache
110
125
  path_clean = RequestDataFiltering.extract_path(path)
111
126
  @macaw_log.info("Defining #{prefix.upcase} endpoint at /#{path}")
112
127
  define_singleton_method("#{prefix}.#{path_clean}", block || lambda {
@@ -0,0 +1,5 @@
1
+ class CachingMiddleware
2
+ @cache: Hash[String, Array[string]]
3
+
4
+ attr_accessor cache: Hash[String, Array[string]]
5
+ end
@@ -1,15 +1,25 @@
1
1
  module MacawFramework
2
2
  class Macaw
3
- @bind: string
3
+ @bind: String
4
+ @cache: untyped
5
+ @config: Hash[String, untyped]
6
+ @endpoints_to_cache: Array[String]
4
7
  @macaw_log: Logger
5
- @port: int
6
8
 
9
+ @prometheus: untyped
10
+ @prometheus_middleware: untyped
7
11
  @server: Server
8
12
 
9
13
  @threads: Integer
10
14
 
15
+ attr_reader bind: String
16
+ attr_reader config: Hash[String, untyped]
17
+ attr_reader macaw_log: Logger
18
+ attr_reader port: Integer
11
19
  attr_reader routes: Array[String]
12
20
 
21
+ attr_reader threads: Integer
22
+
13
23
  def delete: -> nil
14
24
 
15
25
  def get: -> nil
data/sig/server.rbs CHANGED
@@ -1,11 +1,16 @@
1
1
  class Server
2
2
  @bind: String
3
+ @cache: Hash[Symbol, Array]
4
+ @context: OpenSSL::SSL::SSLContext
5
+ @endpoints_to_cache: Array[String]
3
6
  @macaw: MacawFramework::Macaw
4
7
  @macaw_log: Logger
5
8
  @num_threads: Integer
6
9
  @port: Integer
7
10
 
8
- @server: TCPServer
11
+ @prometheus: untyped
12
+ @prometheus_middleware: untyped
13
+ @server: TCPServer|OpenSSL::SSL::SSLServer
9
14
 
10
15
  @threads: Integer
11
16
 
metadata CHANGED
@@ -1,15 +1,29 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: macaw_framework
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.5
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2023-04-16 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-04-28 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: prometheus-client
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '4.1'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '4.1'
13
27
  description: A project started for study purpose that I intend to keep working on.
14
28
  email:
15
29
  - aria.diniz.dev@gmail.com
@@ -25,13 +39,21 @@ files:
25
39
  - README.md
26
40
  - Rakefile
27
41
  - lib/macaw_framework.rb
42
+ - lib/macaw_framework/aspects/cache_aspect.rb
28
43
  - lib/macaw_framework/aspects/logging_aspect.rb
44
+ - lib/macaw_framework/aspects/prometheus_aspect.rb
45
+ - lib/macaw_framework/core/server.rb
46
+ - lib/macaw_framework/data_filters/request_data_filtering.rb
47
+ - lib/macaw_framework/data_filters/response_data_filter.rb
29
48
  - lib/macaw_framework/errors/endpoint_not_mapped_error.rb
30
- - lib/macaw_framework/middlewares/request_data_filtering.rb
31
- - lib/macaw_framework/middlewares/server.rb
49
+ - lib/macaw_framework/errors/too_many_requests_error.rb
50
+ - lib/macaw_framework/middlewares/caching_middleware.rb
51
+ - lib/macaw_framework/middlewares/prometheus_middleware.rb
52
+ - lib/macaw_framework/middlewares/rate_limiter_middleware.rb
32
53
  - lib/macaw_framework/utils/http_status_code.rb
33
54
  - lib/macaw_framework/version.rb
34
55
  - macaw_logo.png
56
+ - sig/caching_middleware.rbs
35
57
  - sig/http_status_code.rbs
36
58
  - sig/logging_aspect.rbs
37
59
  - sig/macaw_framework.rbs
@@ -60,7 +82,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
60
82
  - !ruby/object:Gem::Version
61
83
  version: '0'
62
84
  requirements: []
63
- rubygems_version: 3.4.10
85
+ rubygems_version: 3.4.12
64
86
  signing_key:
65
87
  specification_version: 4
66
88
  summary: A web framework still in development.
@@ -1,85 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require_relative "../aspects/logging_aspect"
4
- require_relative "../utils/http_status_code"
5
-
6
- ##
7
- # Class responsible for providing a default
8
- # webserver.
9
- class Server
10
- prepend LoggingAspect
11
- include HttpStatusCode
12
-
13
- ##
14
- # Create a new instance of Server.
15
- # @param {Macaw} macaw
16
- # @param {Logger} logger
17
- # @param {Integer} port
18
- # @param {String} bind
19
- # @param {Integer} num_threads
20
- # @return {Server}
21
- def initialize(macaw, logger, port, bind, num_threads)
22
- @port = port
23
- @bind = bind
24
- @macaw = macaw
25
- @macaw_log = logger
26
- @num_threads = num_threads
27
- @work_queue = Queue.new
28
- @workers = []
29
- end
30
-
31
- ##
32
- # Start running the webserver.
33
- def run
34
- @server = TCPServer.new(@bind, @port)
35
- @num_threads.times do
36
- @workers << Thread.new do
37
- loop do
38
- client = @work_queue.pop
39
- break if client == :shutdown
40
-
41
- handle_client(client)
42
- end
43
- end
44
- end
45
-
46
- loop do
47
- @work_queue << @server.accept
48
- rescue IOError, Errno::EBADF
49
- break
50
- end
51
- end
52
-
53
- ##
54
- # Method Responsible for closing the TCP server.
55
- def close
56
- @server.close
57
- @num_threads.times { @work_queue << :shutdown }
58
- @workers.each(&:join)
59
- end
60
-
61
- private
62
-
63
- def handle_client(client)
64
- path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client, @macaw.routes)
65
- raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
66
-
67
- @macaw_log.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
68
- message, status = call_endpoint(@macaw_log, method_name, headers, body, parameters)
69
- status ||= 200
70
- message ||= "Ok"
71
- client.puts "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]} \r\n\r\n#{message}"
72
- client.close
73
- rescue EndpointNotMappedError
74
- client.print "HTTP/1.1 404 Not Found\r\n\r\n"
75
- client.close
76
- rescue StandardError => e
77
- client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
78
- @macaw_log.info("Error: #{e}")
79
- client.close
80
- end
81
-
82
- def call_endpoint(name, headers, body, parameters)
83
- @macaw.send(name.to_sym, { headers: headers, body: body, params: parameters })
84
- end
85
- end