macaw_framework 0.1.4 → 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: 8d6b667252e1efbbe7114b4450c894f15e33721d7686c2455108e99a7211e385
4
- data.tar.gz: 07f927d324da0be33031108bc85c564f1a62bd42de7b5a017cf3bfca05dee992
3
+ metadata.gz: d900a18480da4792dfa03608ca8c85c0d0ba4084a3430c39008f4ca026ae784c
4
+ data.tar.gz: e3bc35f7b78bc0a6e995c0abd906879dda434e4c12306d3d55713c35dc45065c
5
5
  SHA512:
6
- metadata.gz: db686066c7e051021c0d088b715a9509bc65f08ea07d53bf7790c786c2da6c27c85f6a47b2bf7f4c39521f78e9439a63152aa077108cc708e6758205dc2931fa
7
- data.tar.gz: 64e5602bab74ed50d94da1935222c9d86a4c7fce04026061d7ac2585ba146959eb9b108b2db142d15a4f658de8aa67f8cdeca3577096bad1add1192015f7f592
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/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,5 +1,7 @@
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
6
  it is strongly advised to not use it for production purposes for now. Actualy it supports only HTTP. HTTPS and SSL
5
7
  support will be implemented soon. Anyone who wishes to contribute is welcome.
@@ -26,7 +28,8 @@ in the same directory of the script that will start the application with the fol
26
28
  {
27
29
  "macaw": {
28
30
  "port": 8080,
29
- "bind": "localhost"
31
+ "bind": "localhost",
32
+ "threads": 10
30
33
  }
31
34
  }
32
35
  ```
@@ -39,7 +42,10 @@ require 'json'
39
42
 
40
43
  m = MacawFramework::Macaw.new
41
44
 
42
- 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
43
49
  return JSON.pretty_generate({ hello_message: 'Hello World!' }), 200
44
50
  end
45
51
 
@@ -1,24 +1,62 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../errors/endpoint_not_mapped_error"
4
+
3
5
  ##
4
6
  # Module containing methods to filter Strings
5
7
  module RequestDataFiltering
8
+ VARIABLE_PATTERN = %r{:[^/]+}.freeze
9
+
6
10
  ##
7
11
  # Method responsible for extracting information
8
12
  # provided by the client like Headers and Body
9
- def self.parse_request_data(client)
13
+ def self.parse_request_data(client, routes)
10
14
  path, parameters = extract_url_parameters(client.gets.gsub("HTTP/1.1", ""))
15
+ parameters = {} if parameters.nil?
16
+
11
17
  method_name = sanitize_method_name(path)
18
+ method_name = select_path(method_name, routes, parameters)
12
19
  body_first_line, headers = extract_headers(client)
13
20
  body = extract_body(client, body_first_line, headers["Content-Length"].to_i)
14
21
  [path, method_name, headers, body, parameters]
15
22
  end
16
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
+
17
55
  ##
18
56
  # Method responsible for sanitizing the method name
19
57
  def self.sanitize_method_name(path)
20
58
  path = extract_path(path)
21
- method_name = path.gsub("/", "_").strip.downcase
59
+ method_name = path.gsub("/", ".").strip.downcase
22
60
  method_name.gsub!(" ", "")
23
61
  method_name
24
62
  end
@@ -26,7 +64,7 @@ module RequestDataFiltering
26
64
  ##
27
65
  # Method responsible for extracting the path from URI
28
66
  def self.extract_path(path)
29
- path[0] == "/" ? path[1..].gsub("/", "_") : path.gsub("/", "_")
67
+ path[0] == "/" ? path[1..].gsub("/", ".") : path.gsub("/", ".")
30
68
  end
31
69
 
32
70
  ##
@@ -16,49 +16,70 @@ class Server
16
16
  # @param {Logger} logger
17
17
  # @param {Integer} port
18
18
  # @param {String} bind
19
+ # @param {Integer} num_threads
19
20
  # @return {Server}
20
- def initialize(macaw, logger, port, bind)
21
+ def initialize(macaw, logger, port, bind, num_threads)
21
22
  @port = port
22
23
  @bind = bind
23
24
  @macaw = macaw
24
25
  @macaw_log = logger
26
+ @num_threads = num_threads
27
+ @work_queue = Queue.new
28
+ @workers = []
25
29
  end
26
30
 
27
31
  ##
28
32
  # Start running the webserver.
29
33
  def run
30
34
  @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
+ @num_threads.times do
36
+ @workers << Thread.new do
37
+ loop do
38
+ client = @work_queue.pop
39
+ break if client == :shutdown
35
40
 
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
41
+ handle_client(client)
42
+ end
49
43
  end
50
44
  end
45
+
46
+ loop do
47
+ @work_queue << @server.accept
48
+ rescue IOError, Errno::EBADF
49
+ break
50
+ end
51
51
  end
52
52
 
53
53
  ##
54
54
  # Method Responsible for closing the TCP server.
55
55
  def close
56
56
  @server.close
57
+ @num_threads.times { @work_queue << :shutdown }
58
+ @workers.each(&:join)
57
59
  end
58
60
 
59
61
  private
60
62
 
61
- def call_endpoint(name, *arg_array)
62
- @macaw.send(name.to_sym, *arg_array)
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 })
63
84
  end
64
85
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module MacawFramework
4
- VERSION = "0.1.4"
4
+ VERSION = "0.1.5"
5
5
  end
@@ -13,20 +13,27 @@ module MacawFramework
13
13
  # Class responsible for creating endpoints and
14
14
  # starting the web server.
15
15
  class Macaw
16
+ ##
17
+ # Array containing the routes defined in the application
18
+ attr_reader :routes
19
+
16
20
  ##
17
21
  # @param {Logger} custom_log
18
22
  def initialize(custom_log: nil, server: Server)
19
23
  begin
24
+ @routes = []
20
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
28
  @bind = config["macaw"]["bind"]
29
+ @threads = config["macaw"]["threads"].to_i
24
30
  rescue StandardError => e
25
31
  @macaw_log.error(e.message)
26
32
  end
27
33
  @port ||= 8080
28
34
  @bind ||= "localhost"
29
- @server = server.new(self, @macaw_log, @port, @bind)
35
+ @threads ||= 5
36
+ @server = server.new(self, @macaw_log, @port, @bind, @threads)
30
37
  end
31
38
 
32
39
  ##
@@ -82,9 +89,10 @@ module MacawFramework
82
89
  ##
83
90
  # Starts the web server
84
91
  def start!
92
+ @macaw_log.info("---------------------------------")
85
93
  @macaw_log.info("Starting server at port #{@port}")
86
- time = Time.now
87
- @macaw_log.info("Server started in #{Time.now - time} seconds.")
94
+ @macaw_log.info("Number of threads: #{@threads}")
95
+ @macaw_log.info("---------------------------------")
88
96
  server_loop(@server)
89
97
  rescue Interrupt
90
98
  @macaw_log.info("Stopping server")
@@ -101,7 +109,10 @@ module MacawFramework
101
109
  def map_new_endpoint(prefix, path, &block)
102
110
  path_clean = RequestDataFiltering.extract_path(path)
103
111
  @macaw_log.info("Defining #{prefix.upcase} endpoint at /#{path}")
104
- 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}"
105
116
  end
106
117
  end
107
118
  end
data/macaw_logo.png ADDED
Binary file
@@ -6,6 +6,10 @@ module MacawFramework
6
6
 
7
7
  @server: Server
8
8
 
9
+ @threads: Integer
10
+
11
+ attr_reader routes: Array[String]
12
+
9
13
  def delete: -> nil
10
14
 
11
15
  def get: -> nil
@@ -0,0 +1,3 @@
1
+ module RequestDataFiltering
2
+ VARIABLE_PATTERN: Regexp
3
+ end
data/sig/server.rbs CHANGED
@@ -2,10 +2,17 @@ class Server
2
2
  @bind: String
3
3
  @macaw: MacawFramework::Macaw
4
4
  @macaw_log: Logger
5
+ @num_threads: Integer
5
6
  @port: Integer
6
7
 
7
8
  @server: TCPServer
8
9
 
10
+ @threads: Integer
11
+
12
+ @work_queue: Thread::Queue
13
+
14
+ @workers: Array[Thread]
15
+
9
16
  def close: -> nil
10
17
 
11
18
  def run: -> nil
@@ -13,4 +20,6 @@ class Server
13
20
  private
14
21
 
15
22
  def call_endpoint: -> Array[untyped]
23
+
24
+ def handle_client: -> nil
16
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.4
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: 2023-04-09 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:
@@ -31,10 +31,12 @@ files:
31
31
  - lib/macaw_framework/middlewares/server.rb
32
32
  - lib/macaw_framework/utils/http_status_code.rb
33
33
  - lib/macaw_framework/version.rb
34
+ - macaw_logo.png
34
35
  - sig/http_status_code.rbs
35
36
  - sig/logging_aspect.rbs
36
37
  - sig/macaw_framework.rbs
37
38
  - sig/macaw_framework/macaw.rbs
39
+ - sig/request_data_filtering.rbs
38
40
  - sig/server.rbs
39
41
  homepage: https://github.com/ariasdiniz/macaw_framework
40
42
  licenses: