macaw_framework 0.1.2 → 0.1.4

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: 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.