macaw_framework 0.1.3 → 0.1.5

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: 46f8af6ed22b7980942f7ec3def269395d1e65e9c7bfd06a96d161c31666997d
4
- data.tar.gz: 258e71f4b693c410c325e7a7751f8a2702e3b2db814752d0de39566fa1b94576
3
+ metadata.gz: d900a18480da4792dfa03608ca8c85c0d0ba4084a3430c39008f4ca026ae784c
4
+ data.tar.gz: e3bc35f7b78bc0a6e995c0abd906879dda434e4c12306d3d55713c35dc45065c
5
5
  SHA512:
6
- metadata.gz: cc2b8fbd4d25b795df95a47025b7465b3cfb6bd340008bebdfe527232a009bad959e25f9c4528b7eb4ce98f14f6148a47dd44e56b19b4d4c23187a2b4a24b56c
7
- data.tar.gz: 0d0f265ea93a3467718cbccd2ed93d85d0d98f9c84e53a821d3071b20243ca251e7670a788bd6e848f6381cee213c16ee8164dd27a32a2a28b77de5979a23326
6
+ metadata.gz: 6ac4eb646bb2510fbc67ddec9ccfca45bea8dfcb6833f9e55b60f0f18d5edebaf392645c53dae7745eec797c926af1700c273516682bd6f10e66c2b018f1cea6
7
+ data.tar.gz: 0ecb2e16a9f1762833751bb403d824e04571c4f3b79f01355ffb6f1f29eb29c234befc759c7d76d7b939f05d5f9f3c252d8a5828198e9c7674b7854fe2edcdf1
data/.rubocop.yml CHANGED
@@ -17,3 +17,6 @@ Metrics/MethodLength:
17
17
 
18
18
  Metrics/AbcSize:
19
19
  Max: 35
20
+
21
+ Metrics/CyclomaticComplexity:
22
+ Max: 10
data/CHANGELOG.md CHANGED
@@ -18,3 +18,9 @@
18
18
  ## [0.1.3] - 2022-12-13
19
19
 
20
20
  - Adding logger gem to Macaw class to fix a bug on the application start
21
+
22
+ ## [0.1.4] - 2023-04-09
23
+
24
+ - Adding log by aspect on endpoint calls to improve observability
25
+ - Moving the server for a new separate class to respect single responsibility
26
+ - Improved the data filtering middleware to sanitize inputs
data/Gemfile CHANGED
@@ -10,3 +10,5 @@ gem "rake", "~> 13.0"
10
10
  gem "minitest", "~> 5.0"
11
11
 
12
12
  gem "rubocop", "~> 1.21"
13
+
14
+ gem "simplecov", "~> 0.22.0"
data/README.md CHANGED
@@ -1,7 +1,10 @@
1
1
  # MacawFramework
2
2
 
3
+ <img src="macaw_logo.png" alt= “” style="width: 30%;height: 30%;margin-left: 35%">
4
+
3
5
  This is a framework for developing web applications. Please have in mind that this is still a work in progress and
4
- it is strongly advised to not use it for production purposes for now. Anyone who wishes to contribute is welcome.
6
+ it is strongly advised to not use it for production purposes for now. Actualy it supports only HTTP. HTTPS and SSL
7
+ support will be implemented soon. Anyone who wishes to contribute is welcome.
5
8
 
6
9
  ## Installation
7
10
 
@@ -24,7 +27,9 @@ in the same directory of the script that will start the application with the fol
24
27
  ```json
25
28
  {
26
29
  "macaw": {
27
- "port": 80
30
+ "port": 8080,
31
+ "bind": "localhost",
32
+ "threads": 10
28
33
  }
29
34
  }
30
35
  ```
@@ -37,7 +42,10 @@ require 'json'
37
42
 
38
43
  m = MacawFramework::Macaw.new
39
44
 
40
- m.get('/hello_world') do |headers, body, parameters|
45
+ m.get('/hello_world') do |context|
46
+ context[:body] # Returns the request body as string
47
+ context[:params] # Returns query parameters and path variables as a hash
48
+ context[:headers] # Returns headers as a hash
41
49
  return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
42
50
  end
43
51
 
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ ##
6
+ # This Aspect is responsible for logging
7
+ # the input and output of every endpoint called
8
+ # in the framework.
9
+ module LoggingAspect
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}")
14
+ response
15
+ end
16
+ end
@@ -0,0 +1,119 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../errors/endpoint_not_mapped_error"
4
+
5
+ ##
6
+ # Module containing methods to filter Strings
7
+ module RequestDataFiltering
8
+ VARIABLE_PATTERN = %r{:[^/]+}.freeze
9
+
10
+ ##
11
+ # Method responsible for extracting information
12
+ # provided by the client like Headers and Body
13
+ def self.parse_request_data(client, routes)
14
+ path, parameters = extract_url_parameters(client.gets.gsub("HTTP/1.1", ""))
15
+ parameters = {} if parameters.nil?
16
+
17
+ method_name = sanitize_method_name(path)
18
+ method_name = select_path(method_name, routes, parameters)
19
+ body_first_line, headers = extract_headers(client)
20
+ body = extract_body(client, body_first_line, headers["Content-Length"].to_i)
21
+ [path, method_name, headers, body, parameters]
22
+ end
23
+
24
+ def self.select_path(method_name, routes, parameters)
25
+ return method_name if routes.include?(method_name)
26
+
27
+ selected_route = nil
28
+ routes.each do |route|
29
+ split_route = route.split(".")
30
+ split_name = method_name.split(".")
31
+
32
+ next unless split_route.length == split_name.length
33
+ next unless match_path_with_route(split_name, split_route)
34
+
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
38
+ end
39
+ break
40
+ end
41
+
42
+ raise EndpointNotMappedError if selected_route.nil?
43
+
44
+ selected_route
45
+ end
46
+
47
+ def self.match_path_with_route(split_path, split_route)
48
+ split_route.each_with_index do |var, index|
49
+ return false if var != split_path[index] && !var.match?(VARIABLE_PATTERN)
50
+ end
51
+
52
+ true
53
+ end
54
+
55
+ ##
56
+ # Method responsible for sanitizing the method name
57
+ def self.sanitize_method_name(path)
58
+ path = extract_path(path)
59
+ method_name = path.gsub("/", ".").strip.downcase
60
+ method_name.gsub!(" ", "")
61
+ method_name
62
+ end
63
+
64
+ ##
65
+ # Method responsible for extracting the path from URI
66
+ def self.extract_path(path)
67
+ path[0] == "/" ? path[1..].gsub("/", ".") : path.gsub("/", ".")
68
+ end
69
+
70
+ ##
71
+ # Method responsible for extracting the headers from request
72
+ def self.extract_headers(client)
73
+ header = client.gets.delete("\n").delete("\r")
74
+ headers = {}
75
+ while header.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
76
+ split_header = header.split(":")
77
+ headers[split_header[0].strip] = split_header[1].strip
78
+ header = client.gets.delete("\n").delete("\r")
79
+ end
80
+ [header, headers]
81
+ end
82
+
83
+ ##
84
+ # Method responsible for extracting the body from request
85
+ def self.extract_body(client, body_first_line, content_length)
86
+ body = client.read(content_length)
87
+ body_first_line << body.to_s
88
+ end
89
+
90
+ ##
91
+ # Method responsible for extracting the parameters from URI
92
+ def self.extract_url_parameters(http_first_line)
93
+ return http_first_line, nil unless http_first_line =~ /\?/
94
+
95
+ path_and_parameters = http_first_line.split("?", 2)
96
+ path = "#{path_and_parameters[0]} "
97
+ parameters_array = path_and_parameters[1].split("&")
98
+ parameters_array.map! do |item|
99
+ split_item = item.split("=")
100
+ { sanitize_parameter_name(split_item[0]) => sanitize_parameter_value(split_item[1]) }
101
+ end
102
+ parameters = {}
103
+ parameters_array.each { |item| parameters.merge!(item) }
104
+ [path, parameters]
105
+ end
106
+
107
+ ##
108
+ # Method responsible for sanitizing the parameter name
109
+ def self.sanitize_parameter_name(name)
110
+ name.gsub(/[^\w\s]/, "")
111
+ end
112
+
113
+ ##
114
+ # Method responsible for sanitizing the parameter value
115
+ def self.sanitize_parameter_value(value)
116
+ value.gsub(/[^\w\s]/, "")
117
+ value.gsub(/[\r\n\s]/, "")
118
+ end
119
+ end
@@ -0,0 +1,85 @@
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = "0.1.3"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -1,8 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative "macaw_framework/endpoint_not_mapped_error"
4
- require_relative "macaw_framework/request_data_filtering"
5
- require_relative "macaw_framework/http_status_code"
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"
6
6
  require_relative "macaw_framework/version"
7
7
  require "logger"
8
8
  require "socket"
@@ -13,18 +13,27 @@ module MacawFramework
13
13
  # Class responsible for creating endpoints and
14
14
  # starting the web server.
15
15
  class Macaw
16
- include(HttpStatusCode)
16
+ ##
17
+ # Array containing the routes defined in the application
18
+ attr_reader :routes
19
+
17
20
  ##
18
21
  # @param {Logger} custom_log
19
- def initialize(custom_log = nil)
22
+ def initialize(custom_log: nil, server: Server)
20
23
  begin
24
+ @routes = []
25
+ @macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
21
26
  config = JSON.parse(File.read("application.json"))
22
27
  @port = config["macaw"]["port"]
23
- rescue StandardError
24
- @port ||= 8080
28
+ @bind = config["macaw"]["bind"]
29
+ @threads = config["macaw"]["threads"].to_i
30
+ rescue StandardError => e
31
+ @macaw_log.error(e.message)
25
32
  end
26
33
  @port ||= 8080
27
- @macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
34
+ @bind ||= "localhost"
35
+ @threads ||= 5
36
+ @server = server.new(self, @macaw_log, @port, @bind, @threads)
28
37
  end
29
38
 
30
39
  ##
@@ -80,46 +89,30 @@ module MacawFramework
80
89
  ##
81
90
  # Starts the web server
82
91
  def start!
92
+ @macaw_log.info("---------------------------------")
83
93
  @macaw_log.info("Starting server at port #{@port}")
84
- time = Time.now
85
- server = TCPServer.open(@port)
86
- @macaw_log.info("Server started in #{Time.now - time} seconds.")
87
- server_loop(server)
94
+ @macaw_log.info("Number of threads: #{@threads}")
95
+ @macaw_log.info("---------------------------------")
96
+ server_loop(@server)
88
97
  rescue Interrupt
89
98
  @macaw_log.info("Stopping server")
90
- server.close
99
+ @server.close
91
100
  @macaw_log.info("Macaw stop flying for some seeds...")
92
101
  end
93
102
 
94
103
  private
95
104
 
96
105
  def server_loop(server)
97
- loop do
98
- Thread.start(server.accept) do |client|
99
- path, method_name, headers, body, parameters = RequestDataFiltering.extract_client_info(client)
100
- raise EndpointNotMappedError unless respond_to?(method_name)
101
-
102
- @macaw_log.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
103
- message, status = send(method_name, headers, body, parameters)
104
- status ||= 200
105
- message ||= "Ok"
106
- client.puts "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]} \r\n\r\n#{message}"
107
- client.close
108
- rescue EndpointNotMappedError
109
- client.print "HTTP/1.1 404 Not Found\r\n\r\n"
110
- client.close
111
- rescue StandardError => e
112
- client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
113
- @macaw_log.info("Error: #{e}")
114
- client.close
115
- end
116
- end
106
+ server.run
117
107
  end
118
108
 
119
109
  def map_new_endpoint(prefix, path, &block)
120
110
  path_clean = RequestDataFiltering.extract_path(path)
121
111
  @macaw_log.info("Defining #{prefix.upcase} endpoint at /#{path}")
122
- define_singleton_method("#{prefix}_#{path_clean}", block)
112
+ define_singleton_method("#{prefix}.#{path_clean}", block || lambda {
113
+ |context = { headers: {}, body: "", params: {} }|
114
+ })
115
+ @routes << "#{prefix}.#{path_clean}"
123
116
  end
124
117
  end
125
118
  end
data/macaw_logo.png ADDED
Binary file
@@ -0,0 +1,3 @@
1
+ module LoggingAspect
2
+ def call_endpoint: -> Array[untyped]
3
+ end
@@ -1,7 +1,15 @@
1
1
  module MacawFramework
2
2
  class Macaw
3
+ @bind: string
4
+ @macaw_log: Logger
3
5
  @port: int
4
6
 
7
+ @server: Server
8
+
9
+ @threads: Integer
10
+
11
+ attr_reader routes: Array[String]
12
+
5
13
  def delete: -> nil
6
14
 
7
15
  def get: -> nil
@@ -0,0 +1,3 @@
1
+ module RequestDataFiltering
2
+ VARIABLE_PATTERN: Regexp
3
+ end
data/sig/server.rbs ADDED
@@ -0,0 +1,25 @@
1
+ class Server
2
+ @bind: String
3
+ @macaw: MacawFramework::Macaw
4
+ @macaw_log: Logger
5
+ @num_threads: Integer
6
+ @port: Integer
7
+
8
+ @server: TCPServer
9
+
10
+ @threads: Integer
11
+
12
+ @work_queue: Thread::Queue
13
+
14
+ @workers: Array[Thread]
15
+
16
+ def close: -> nil
17
+
18
+ def run: -> nil
19
+
20
+ private
21
+
22
+ def call_endpoint: -> Array[untyped]
23
+
24
+ def handle_client: -> nil
25
+ 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: 0.1.3
4
+ version: 0.1.5
5
5
  platform: ruby
6
6
  authors:
7
7
  - Aria Diniz
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2022-12-14 00:00:00.000000000 Z
11
+ date: 2023-04-16 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: A project started for study purpose that I intend to keep working on.
14
14
  email:
@@ -25,13 +25,19 @@ files:
25
25
  - README.md
26
26
  - Rakefile
27
27
  - lib/macaw_framework.rb
28
- - lib/macaw_framework/endpoint_not_mapped_error.rb
29
- - lib/macaw_framework/http_status_code.rb
30
- - lib/macaw_framework/request_data_filtering.rb
28
+ - lib/macaw_framework/aspects/logging_aspect.rb
29
+ - 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
32
+ - lib/macaw_framework/utils/http_status_code.rb
31
33
  - lib/macaw_framework/version.rb
34
+ - macaw_logo.png
32
35
  - sig/http_status_code.rbs
36
+ - sig/logging_aspect.rbs
33
37
  - sig/macaw_framework.rbs
34
38
  - sig/macaw_framework/macaw.rbs
39
+ - sig/request_data_filtering.rbs
40
+ - sig/server.rbs
35
41
  homepage: https://github.com/ariasdiniz/macaw_framework
36
42
  licenses:
37
43
  - MIT
@@ -54,7 +60,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
60
  - !ruby/object:Gem::Version
55
61
  version: '0'
56
62
  requirements: []
57
- rubygems_version: 3.3.26
63
+ rubygems_version: 3.4.10
58
64
  signing_key:
59
65
  specification_version: 4
60
66
  summary: A web framework still in development.
@@ -1,60 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ##
4
- # Module containing methods to filter Strings
5
- module RequestDataFiltering
6
- ##
7
- # Method responsible for extracting information
8
- # provided by the client like Headers and Body
9
- def self.extract_client_info(client)
10
- path, parameters = extract_url_parameters(client.gets.gsub("HTTP/1.1", ""))
11
- method_name = path.gsub("/", "_").strip!.downcase
12
- method_name.gsub!(" ", "")
13
- body_first_line, headers = extract_headers(client)
14
- body = extract_body(client, body_first_line, headers["Content-Length"].to_i)
15
- [path, method_name, headers, body, parameters]
16
- end
17
-
18
- ##
19
- # Method responsible for extracting the path from URI
20
- def self.extract_path(path)
21
- path[0] == "/" ? path[1..].gsub("/", "_") : path.gsub("/", "_")
22
- end
23
-
24
- ##
25
- # Method responsible for extracting the headers from request
26
- def self.extract_headers(client)
27
- header = client.gets.delete("\n").delete("\r")
28
- headers = {}
29
- while header.match(%r{[a-zA-Z0-9\-/*]*: [a-zA-Z0-9\-/*]})
30
- split_header = header.split(":")
31
- headers[split_header[0]] = split_header[1][1..]
32
- header = client.gets.delete("\n").delete("\r")
33
- end
34
- [header, headers]
35
- end
36
-
37
- ##
38
- # Method responsible for extracting the body from request
39
- def self.extract_body(client, body_first_line, content_length)
40
- body = client.read(content_length)
41
- body_first_line << body.to_s
42
- end
43
-
44
- ##
45
- # Method responsible for extracting the parameters from URI
46
- def self.extract_url_parameters(http_first_line)
47
- return http_first_line, nil unless http_first_line =~ /\?/
48
-
49
- path_and_parameters = http_first_line.split("?", 2)
50
- path = "#{path_and_parameters[0]} "
51
- parameters_array = path_and_parameters[1].split("&")
52
- parameters_array.map! do |item|
53
- split_item = item.split("=")
54
- { split_item[0] => split_item[1].gsub("\n", "").gsub("\r", "").gsub("\s", "") }
55
- end
56
- parameters = {}
57
- parameters_array.each { |item| parameters.merge!(item) }
58
- [path, parameters]
59
- end
60
- end