macaw_framework 0.1.2 → 0.1.4

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: 512b14576a1be6337300fc758b7be6acf48603fa605c0d60c21d494b2d411a10
4
- data.tar.gz: 29f0115f9e93539c740e56834515d7d66dd768a2b5a8c5ef2668beb82d403fa5
3
+ metadata.gz: 8d6b667252e1efbbe7114b4450c894f15e33721d7686c2455108e99a7211e385
4
+ data.tar.gz: 07f927d324da0be33031108bc85c564f1a62bd42de7b5a017cf3bfca05dee992
5
5
  SHA512:
6
- metadata.gz: aa4dd23ceaa6579ab7f0b77caa42b8745fb48f830aa8f5db9eb713bff5330d1879653209781e888c873b071c45b11afd5744282401d77c9c7609c0ed9f721e4b
7
- data.tar.gz: 2cc30aff12830763c3e9a19c8597fa4cd5f8e8ec7bfc1201f5c367883e93c480cef243d2590725084b62922538e6e27798f53840df0660b8872e09be01336f8a
6
+ metadata.gz: db686066c7e051021c0d088b715a9509bc65f08ea07d53bf7790c786c2da6c27c85f6a47b2bf7f4c39521f78e9439a63152aa077108cc708e6758205dc2931fa
7
+ data.tar.gz: 64e5602bab74ed50d94da1935222c9d86a4c7fce04026061d7ac2585ba146959eb9b108b2db142d15a4f658de8aa67f8cdeca3577096bad1add1192015f7f592
data/.rubocop.yml CHANGED
@@ -1,5 +1,5 @@
1
1
  AllCops:
2
- TargetRubyVersion: 2.6
2
+ TargetRubyVersion: 2.7
3
3
 
4
4
  Style/StringLiterals:
5
5
  Enabled: true
@@ -11,3 +11,9 @@ Style/StringLiteralsInInterpolation:
11
11
 
12
12
  Layout/LineLength:
13
13
  Max: 120
14
+
15
+ Metrics/MethodLength:
16
+ Max: 30
17
+
18
+ Metrics/AbcSize:
19
+ Max: 35
data/CHANGELOG.md CHANGED
@@ -14,3 +14,13 @@
14
14
  - Adding logs to the framework activity
15
15
  - Removing undefined Status Codes from http_status_code hash
16
16
  - Moving methods from Macaw class to RequestDataFiltering module, respecting SOLID
17
+
18
+ ## [0.1.3] - 2022-12-13
19
+
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/README.md CHANGED
@@ -1,7 +1,8 @@
1
1
  # MacawFramework
2
2
 
3
3
  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.
4
+ it is strongly advised to not use it for production purposes for now. Actualy it supports only HTTP. HTTPS and SSL
5
+ support will be implemented soon. Anyone who wishes to contribute is welcome.
5
6
 
6
7
  ## Installation
7
8
 
@@ -24,7 +25,8 @@ in the same directory of the script that will start the application with the fol
24
25
  ```json
25
26
  {
26
27
  "macaw": {
27
- "port": 80
28
+ "port": 8080,
29
+ "bind": "localhost"
28
30
  }
29
31
  }
30
32
  ```
@@ -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
@@ -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
@@ -6,19 +6,27 @@ module RequestDataFiltering
6
6
  ##
7
7
  # Method responsible for extracting information
8
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!(' ', '')
9
+ def self.parse_request_data(client)
10
+ path, parameters = extract_url_parameters(client.gets.gsub("HTTP/1.1", ""))
11
+ method_name = sanitize_method_name(path)
13
12
  body_first_line, headers = extract_headers(client)
14
- body = extract_body(client, body_first_line, headers['Content-Length'].to_i)
13
+ body = extract_body(client, body_first_line, headers["Content-Length"].to_i)
15
14
  [path, method_name, headers, body, parameters]
16
15
  end
17
16
 
17
+ ##
18
+ # Method responsible for sanitizing the method name
19
+ def self.sanitize_method_name(path)
20
+ path = extract_path(path)
21
+ method_name = path.gsub("/", "_").strip.downcase
22
+ method_name.gsub!(" ", "")
23
+ method_name
24
+ end
25
+
18
26
  ##
19
27
  # Method responsible for extracting the path from URI
20
28
  def self.extract_path(path)
21
- path[0] == '/' ? path[1..].gsub('/', '_') : path.gsub('/', '_')
29
+ path[0] == "/" ? path[1..].gsub("/", "_") : path.gsub("/", "_")
22
30
  end
23
31
 
24
32
  ##
@@ -27,8 +35,8 @@ module RequestDataFiltering
27
35
  header = client.gets.delete("\n").delete("\r")
28
36
  headers = {}
29
37
  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..]
38
+ split_header = header.split(":")
39
+ headers[split_header[0].strip] = split_header[1].strip
32
40
  header = client.gets.delete("\n").delete("\r")
33
41
  end
34
42
  [header, headers]
@@ -46,15 +54,28 @@ module RequestDataFiltering
46
54
  def self.extract_url_parameters(http_first_line)
47
55
  return http_first_line, nil unless http_first_line =~ /\?/
48
56
 
49
- path_and_parameters = http_first_line.split('?', 2)
57
+ path_and_parameters = http_first_line.split("?", 2)
50
58
  path = "#{path_and_parameters[0]} "
51
- parameters_array = path_and_parameters[1].split('&')
59
+ parameters_array = path_and_parameters[1].split("&")
52
60
  parameters_array.map! do |item|
53
- split_item = item.split('=')
54
- { split_item[0] => split_item[1].gsub("\n", '').gsub("\r", '').gsub("\s", '') }
61
+ split_item = item.split("=")
62
+ { sanitize_parameter_name(split_item[0]) => sanitize_parameter_value(split_item[1]) }
55
63
  end
56
64
  parameters = {}
57
65
  parameters_array.each { |item| parameters.merge!(item) }
58
66
  [path, parameters]
59
67
  end
68
+
69
+ ##
70
+ # Method responsible for sanitizing the parameter name
71
+ def self.sanitize_parameter_name(name)
72
+ name.gsub(/[^\w\s]/, "")
73
+ end
74
+
75
+ ##
76
+ # Method responsible for sanitizing the parameter value
77
+ def self.sanitize_parameter_value(value)
78
+ value.gsub(/[^\w\s]/, "")
79
+ value.gsub(/[\r\n\s]/, "")
80
+ end
60
81
  end
@@ -0,0 +1,64 @@
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
+ # @return {Server}
20
+ def initialize(macaw, logger, port, bind)
21
+ @port = port
22
+ @bind = bind
23
+ @macaw = macaw
24
+ @macaw_log = logger
25
+ end
26
+
27
+ ##
28
+ # Start running the webserver.
29
+ def run
30
+ @server = TCPServer.new(@bind, @port)
31
+ loop do
32
+ Thread.start(@server.accept) do |client|
33
+ path, method_name, headers, body, parameters = RequestDataFiltering.parse_request_data(client)
34
+ raise EndpointNotMappedError unless @macaw.respond_to?(method_name)
35
+
36
+ @macaw_log.info("Running #{path.gsub("\n", "").gsub("\r", "")}")
37
+ message, status = call_endpoint(@macaw_log, method_name, headers, body, parameters)
38
+ status ||= 200
39
+ message ||= "Ok"
40
+ client.puts "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]} \r\n\r\n#{message}"
41
+ client.close
42
+ rescue EndpointNotMappedError
43
+ client.print "HTTP/1.1 404 Not Found\r\n\r\n"
44
+ client.close
45
+ rescue StandardError => e
46
+ client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
47
+ @macaw_log.info("Error: #{e}")
48
+ client.close
49
+ end
50
+ end
51
+ end
52
+
53
+ ##
54
+ # Method Responsible for closing the TCP server.
55
+ def close
56
+ @server.close
57
+ end
58
+
59
+ private
60
+
61
+ def call_endpoint(name, *arg_array)
62
+ @macaw.send(name.to_sym, *arg_array)
63
+ end
64
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = "0.1.2"
4
+ VERSION = "0.1.4"
5
5
  end
@@ -1,9 +1,10 @@
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
+ require "logger"
7
8
  require "socket"
8
9
  require "json"
9
10
 
@@ -12,18 +13,20 @@ module MacawFramework
12
13
  # Class responsible for creating endpoints and
13
14
  # starting the web server.
14
15
  class Macaw
15
- include(HttpStatusCode)
16
16
  ##
17
17
  # @param {Logger} custom_log
18
- def initialize(custom_log = nil)
18
+ def initialize(custom_log: nil, server: Server)
19
19
  begin
20
- config = JSON.parse(File.read('application.json'))
21
- @port = config['macaw']['port']
22
- rescue StandardError
23
- @port ||= 8080
20
+ @macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
21
+ config = JSON.parse(File.read("application.json"))
22
+ @port = config["macaw"]["port"]
23
+ @bind = config["macaw"]["bind"]
24
+ rescue StandardError => e
25
+ @macaw_log.error(e.message)
24
26
  end
25
27
  @port ||= 8080
26
- @macaw_log ||= custom_log.nil? ? Logger.new($stdout) : custom_log
28
+ @bind ||= "localhost"
29
+ @server = server.new(self, @macaw_log, @port, @bind)
27
30
  end
28
31
 
29
32
  ##
@@ -33,9 +36,7 @@ module MacawFramework
33
36
  # @param {Proc} block
34
37
  # @return {Integer, String}
35
38
  def get(path, &block)
36
- path_clean = RequestDataFiltering.extract_path(path)
37
- @macaw_log.info("Defining GET endpoint at #{path_clean}")
38
- map_new_endpoint('get', path_clean, &block)
39
+ map_new_endpoint("get", path, &block)
39
40
  end
40
41
 
41
42
  ##
@@ -45,9 +46,7 @@ module MacawFramework
45
46
  # @param {Proc} block
46
47
  # @return {String, Integer}
47
48
  def post(path, &block)
48
- path_clean = path[0] == '/' ? path[1..].gsub('/', '_') : path.gsub('/', '_')
49
- @macaw_log.info("Defining POST endpoint at #{path_clean}")
50
- map_new_endpoint('post', path_clean, &block)
49
+ map_new_endpoint("post", path, &block)
51
50
  end
52
51
 
53
52
  ##
@@ -57,9 +56,7 @@ module MacawFramework
57
56
  # @param {Proc} block
58
57
  # @return {String, Integer}
59
58
  def put(path, &block)
60
- path_clean = path[0] == '/' ? path[1..].gsub('/', '_') : path.gsub('/', '_')
61
- @macaw_log.info("Defining PUT endpoint at #{path_clean}")
62
- map_new_endpoint('put', path_clean, &block)
59
+ map_new_endpoint("put", path, &block)
63
60
  end
64
61
 
65
62
  ##
@@ -69,9 +66,7 @@ module MacawFramework
69
66
  # @param {Proc} block
70
67
  # @return {String, Integer}
71
68
  def patch(path, &block)
72
- path_clean = path[0] == '/' ? path[1..].gsub('/', '_') : path.gsub('/', '_')
73
- @macaw_log.info("Defining PATCH endpoint at #{path_clean}")
74
- map_new_endpoint('patch', path_clean, &block)
69
+ map_new_endpoint("patch", path, &block)
75
70
  end
76
71
 
77
72
  ##
@@ -81,9 +76,7 @@ module MacawFramework
81
76
  # @param {Proc} block
82
77
  # @return {String, Integer}
83
78
  def delete(path, &block)
84
- path_clean = path[0] == '/' ? path[1..].gsub('/', '_') : path.gsub('/', '_')
85
- @macaw_log.info("Defining DELETE endpoint at #{path_clean}")
86
- map_new_endpoint('delete', path_clean, &block)
79
+ map_new_endpoint("delete", path, &block)
87
80
  end
88
81
 
89
82
  ##
@@ -91,38 +84,24 @@ module MacawFramework
91
84
  def start!
92
85
  @macaw_log.info("Starting server at port #{@port}")
93
86
  time = Time.now
94
- server = TCPServer.open(@port)
95
87
  @macaw_log.info("Server started in #{Time.now - time} seconds.")
96
- loop do
97
- Thread.start(server.accept) do |client|
98
- path, method_name, headers, body, parameters = RequestDataFiltering.extract_client_info(client)
99
- raise EndpointNotMappedError unless respond_to?(method_name)
100
-
101
- @macaw_log.info("Running #{path.gsub("\n", '').gsub("\r", '')}")
102
- message, status = send(method_name, headers, body, parameters)
103
- status ||= 200
104
- message ||= 'Ok'
105
- client.puts "HTTP/1.1 #{status} #{HTTP_STATUS_CODE_MAP[status]} \r\n\r\n#{message}"
106
- client.close
107
- rescue EndpointNotMappedError
108
- client.print "HTTP/1.1 404 Not Found\r\n\r\n"
109
- client.close
110
- rescue StandardError => e
111
- client.print "HTTP/1.1 500 Internal Server Error\r\n\r\n"
112
- @macaw_log.info("Error: #{e}")
113
- client.close
114
- end
115
- end
88
+ server_loop(@server)
116
89
  rescue Interrupt
117
- @macaw_log.info('Stopping server')
118
- server.close
119
- @macaw_log.info('Macaw stop flying for some seeds...')
90
+ @macaw_log.info("Stopping server")
91
+ @server.close
92
+ @macaw_log.info("Macaw stop flying for some seeds...")
120
93
  end
121
94
 
122
95
  private
123
96
 
97
+ def server_loop(server)
98
+ server.run
99
+ end
100
+
124
101
  def map_new_endpoint(prefix, path, &block)
125
- define_singleton_method("#{prefix}_#{path}", block)
102
+ path_clean = RequestDataFiltering.extract_path(path)
103
+ @macaw_log.info("Defining #{prefix.upcase} endpoint at /#{path}")
104
+ define_singleton_method("#{prefix}_#{path_clean}", block)
126
105
  end
127
106
  end
128
107
  end
@@ -0,0 +1,3 @@
1
+ module LoggingAspect
2
+ def call_endpoint: -> Array[untyped]
3
+ end
@@ -1,7 +1,11 @@
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
+
5
9
  def delete: -> nil
6
10
 
7
11
  def get: -> nil
data/sig/server.rbs ADDED
@@ -0,0 +1,16 @@
1
+ class Server
2
+ @bind: String
3
+ @macaw: MacawFramework::Macaw
4
+ @macaw_log: Logger
5
+ @port: Integer
6
+
7
+ @server: TCPServer
8
+
9
+ def close: -> nil
10
+
11
+ def run: -> nil
12
+
13
+ private
14
+
15
+ def call_endpoint: -> Array[untyped]
16
+ 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.2
4
+ version: 0.1.4
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-11 00:00:00.000000000 Z
11
+ date: 2023-04-09 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,17 @@ 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
32
34
  - sig/http_status_code.rbs
35
+ - sig/logging_aspect.rbs
33
36
  - sig/macaw_framework.rbs
34
37
  - sig/macaw_framework/macaw.rbs
38
+ - sig/server.rbs
35
39
  homepage: https://github.com/ariasdiniz/macaw_framework
36
40
  licenses:
37
41
  - MIT
@@ -54,7 +58,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
54
58
  - !ruby/object:Gem::Version
55
59
  version: '0'
56
60
  requirements: []
57
- rubygems_version: 3.3.26
61
+ rubygems_version: 3.4.10
58
62
  signing_key:
59
63
  specification_version: 4
60
64
  summary: A web framework still in development.