low_loop 0.7.2 → 0.8.0

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: 46278df08960f6243c32f11afb3ddcb30af1d7711aff896eb6119db918c6405b
4
- data.tar.gz: c37e341b7c0fcdb779c7ec2f0b7a58425235f1ccabe7e408abb30c69cf461690
3
+ metadata.gz: 17df935b57f5de96e6888dd4342ade7c60ec4639f441d0ab7f517c858b269a80
4
+ data.tar.gz: e1934e74675858f24231771c81cf05751c06582b251b80075d4177e8d47154b2
5
5
  SHA512:
6
- metadata.gz: d969993af0a105cddfdc438511b6497bacc366a73809e7aa6b7def338b63f1921deadddcae103d61d2c3d9341fd68a0813be3c8b92c5925abc2b5c0692897b2c
7
- data.tar.gz: 7324e92a8631a473288dbd5e9e87720d8089484c6bbe456efb9a4818fa4a67b4a658a16e538d214ed1bd19364204e7645ed2e621c937ea90b85aace7bded2c2f
6
+ metadata.gz: 48d2c4a1c581f14f0eb5e9255de93354f76e1638e52a1d2157cfd958f752eec9207ce71e91e9eddbf2032990f052deae91b422793cb27ce48f362929a565a145
7
+ data.tar.gz: 6f45681e3d0d17d800ab9c21ad543005016c9b6c779e48f45afe3c6380f98056f151e41a19ee19f71e128d47baea9fb1990f1e48dfeed1008a95e29bb7ad96f4
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'io/wait'
4
+
5
+ require_relative 'request_parser'
6
+ require_relative '../responses/response_builder'
7
+
8
+ module Low
9
+ class ConnectionManager
10
+ DEFAULT_KEEP_ALIVE_TIMEOUT = 30
11
+ DEFAULT_REQUEST_TIMEOUT = 10
12
+
13
+ def initialize(config:)
14
+ @config = config
15
+ end
16
+
17
+ def handle_connection(socket:)
18
+ stream = RequestParser.create_stream(socket:)
19
+ keep_alive = true
20
+ version = nil
21
+
22
+ while keep_alive
23
+ break unless socket.wait_readable(keep_alive_timeout)
24
+
25
+ socket.timeout = request_timeout
26
+ begin
27
+ request = RequestParser.parse(stream:, host: config.host, port: config.port, version:)
28
+ rescue IO::TimeoutError
29
+ break
30
+ ensure
31
+ socket.timeout = nil
32
+ end
33
+ break if request.nil?
34
+
35
+ version ||= request.version
36
+ keep_alive = keep_alive?(request)
37
+
38
+ response_event = Events::RequestEvent.take(request:)
39
+ response = response_event.response
40
+
41
+ ResponseBuilder.respond(config:, socket:, response:, keep_alive:)
42
+ end
43
+ end
44
+
45
+ def keep_alive?(request)
46
+ tokens = (request.headers['connection'] || []).flat_map do |value|
47
+ value.split(',').map { |token| token.strip.downcase }
48
+ end
49
+
50
+ if request.version.to_s.downcase.include?('1.0')
51
+ tokens.include?('keep-alive')
52
+ else
53
+ !tokens.include?('close')
54
+ end
55
+ end
56
+
57
+ def keep_alive_timeout
58
+ config.keep_alive_timeout || DEFAULT_KEEP_ALIVE_TIMEOUT
59
+ end
60
+
61
+ def request_timeout
62
+ config.request_timeout || DEFAULT_REQUEST_TIMEOUT
63
+ end
64
+
65
+ private
66
+
67
+ attr_reader :config
68
+ end
69
+ end
@@ -9,7 +9,7 @@ module Low
9
9
 
10
10
  # TODO: For RouteEvent/FileEvent parse and provide query params as attributes on the event.
11
11
  def initialize(file:, request: nil)
12
- super(key: self.class)
12
+ super(key: self.class, action: :request)
13
13
 
14
14
  @file = file
15
15
  @request = request
data/lib/low_loop.rb CHANGED
@@ -1,29 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'async'
4
- require 'io/wait'
5
4
  require 'paint'
6
5
  require 'socket'
7
6
  require 'low_type'
8
7
  require 'low_event'
9
8
  require 'observers'
10
9
 
11
- require_relative 'factories/response_factory'
12
- require_relative 'requests/request_parser'
13
- require_relative 'responses/response_builder'
10
+ require_relative 'responses/response_factory'
11
+ require_relative 'connections/connection_manager'
14
12
  require_relative 'servers/file_server'
15
13
  require_relative 'support/low_frame'
16
14
 
17
15
  class LowLoop
18
16
  include Observers
19
17
 
20
- DEFAULT_KEEP_ALIVE_TIMEOUT = 30
21
- DEFAULT_REQUEST_TIMEOUT = 10
22
-
23
18
  attr_reader :config
24
19
 
25
20
  def initialize(config:, router: nil, renderer: nil, show_output: true)
26
21
  @config = config
22
+ @connection_manager = Low::ConnectionManager.new(config:)
27
23
  @frame = LowFrame.new(renderer:, fps: 10, show_output:)
28
24
 
29
25
  Low::Events::RequestEvent.define do |observers|
@@ -37,20 +33,13 @@ class LowLoop
37
33
  server = start_server
38
34
 
39
35
  Async do |task|
40
- # Background task.
41
- task.async do
42
- loop do
43
- @frame.render if @frame.renderer
44
- sleep 0.1 # 10fps
45
- end
46
- end
36
+ render_frame(task)
47
37
 
48
- # Request handler.
49
38
  loop do
50
39
  socket = server.accept
51
40
 
52
41
  task.async do
53
- handle_connection(socket)
42
+ @connection_manager.handle_connection(socket:)
54
43
  rescue StandardError => e
55
44
  render_error(e)
56
45
  ensure
@@ -60,6 +49,20 @@ class LowLoop
60
49
  end
61
50
  end
62
51
 
52
+ # Fallback mode for when there's no dependencies and you want to know that the server is still working.
53
+ def mirror(event:)
54
+ request = event.request
55
+ response = tories::ResponseFactory.html(body: "Thank you for visiting #{request.path} with body: '#{request.body}'")
56
+ Low::Events::ResponseEvent.new(response:)
57
+ end
58
+
59
+ # Consider LowLoop a value object in the context of Observers (there can only be one).
60
+ def ==(other) = other.class == self.class
61
+ def eql?(other) = self == other
62
+ def hash = [self.class].hash
63
+
64
+ private
65
+
63
66
  def start_server
64
67
  puts "Starting server @ #{config.host}:#{config.port}" unless config.matrix_mode
65
68
 
@@ -68,28 +71,17 @@ class LowLoop
68
71
  server
69
72
  end
70
73
 
71
- def render
72
- Async do
74
+ def render_frame(task)
75
+ return unless @frame.renderer
76
+
77
+ task.async do
73
78
  loop do
74
79
  @frame.render
80
+ sleep 0.1 # 10fps
75
81
  end
76
82
  end
77
83
  end
78
84
 
79
- # Fallback mode for when there's no dependencies and you want to know that the server is still working.
80
- def mirror(event:)
81
- request = event.request
82
- response = Low::Factories::ResponseFactory.html(body: "Thank you for visiting #{request.path} with body: '#{request.body}'")
83
- Low::Events::ResponseEvent.new(response:)
84
- end
85
-
86
- # Consider LowLoop a value object in the context of Observers (there can only be one).
87
- def ==(other) = other.class == self.class
88
- def eql?(other) = self == other
89
- def hash = [self.class].hash
90
-
91
- private
92
-
93
85
  def render_error(error)
94
86
  puts "\nException:"
95
87
  puts Paint[error.message, :red]
@@ -104,53 +96,4 @@ class LowLoop
104
96
  gets
105
97
  end
106
98
  end
107
-
108
- def handle_connection(socket)
109
- stream = Low::RequestParser.create_stream(socket:)
110
- keep_alive = true
111
- version = nil
112
-
113
- while keep_alive
114
- break unless socket.wait_readable(keep_alive_timeout)
115
-
116
- socket.timeout = request_timeout
117
- begin
118
- request = Low::RequestParser.parse(stream:, host: config.host, port: config.port, version:)
119
- rescue IO::TimeoutError
120
- break
121
- ensure
122
- socket.timeout = nil
123
- end
124
- break if request.nil?
125
-
126
- version ||= request.version
127
- keep_alive = keep_alive?(request)
128
-
129
- # TODO: Handle nil return value; create 500 status code response.
130
- response_event = Low::Events::RequestEvent.take(request:)
131
- response = response_event.response
132
-
133
- Low::ResponseBuilder.respond(config:, socket:, response:, keep_alive:)
134
- end
135
- end
136
-
137
- def keep_alive?(request)
138
- tokens = (request.headers['connection'] || []).flat_map do |value|
139
- value.split(',').map { |token| token.strip.downcase }
140
- end
141
-
142
- if request.version.to_s.downcase.include?('1.0')
143
- tokens.include?('keep-alive')
144
- else
145
- !tokens.include?('close')
146
- end
147
- end
148
-
149
- def keep_alive_timeout
150
- config.keep_alive_timeout || DEFAULT_KEEP_ALIVE_TIMEOUT
151
- end
152
-
153
- def request_timeout
154
- config.request_timeout || DEFAULT_REQUEST_TIMEOUT
155
- end
156
99
  end
@@ -1,15 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative '../factories/response_factory'
3
+ require_relative '../responses/response_factory'
4
4
 
5
5
  module Low
6
6
  class FileResponse
7
7
  class << self
8
- def handle(event:)
8
+ def request(event:)
9
9
  file = event.file
10
10
 
11
11
  if File.exist?(file.path)
12
- response = Factories::ResponseFactory.file(path: file.path, content_type: file.content_type)
12
+ response = ResponseFactory.file(path: file.path, content_type: file.content_type)
13
13
  return Events::ResponseEvent.new(response:).tap(&:branch)
14
14
  end
15
15
 
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'protocol/http'
4
+ require 'protocol/http/body/file'
5
+
6
+ module Low
7
+ class ResponseFactory
8
+ class << self
9
+ def html(body:)
10
+ headers = Protocol::HTTP::Headers.new([['content-type', 'text/html']])
11
+ body = Protocol::HTTP::Body::Buffered.wrap(body.strip)
12
+
13
+ Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
14
+ end
15
+
16
+ def file(path:, content_type:)
17
+ headers = Protocol::HTTP::Headers.new([['content-type', content_type]])
18
+ file = File.open(path, 'rb')
19
+ body = Protocol::HTTP::Body::File.new(file)
20
+
21
+ Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
22
+ end
23
+
24
+ def status(code:)
25
+ headers = Protocol::HTTP::Headers.new([['content-type', 'text/html']])
26
+
27
+ Protocol::HTTP::Response.new('HTTP/1.1', code, headers, nil)
28
+ end
29
+ end
30
+ end
31
+ end
@@ -18,8 +18,8 @@ module Low
18
18
  observers(Events::FileEvent) << FileResponse
19
19
  end
20
20
 
21
- def extension(filepath:)
22
- extension = File.extname(filepath).delete_prefix('.')
21
+ def extension(file_path:)
22
+ extension = File.extname(file_path).delete_prefix('.')
23
23
 
24
24
  return nil if extension == ''
25
25
  return nil unless @content_types.key?(extension)
@@ -27,16 +27,19 @@ module Low
27
27
  extension
28
28
  end
29
29
 
30
- # TODO: Define type: Events::RequestEvent
31
- def handle(event:)
32
- filepath = Protocol::URL[event.request.path].local_path(@web_root)
30
+ def request(event: Events::RequestEvent)
31
+ file_path = event.request.path
33
32
 
34
- extension = extension(filepath:)
33
+ return nil unless file_path.include?('.')
34
+
35
+ url = Protocol::URL[file_path]
36
+ extension = extension(file_path: url.path.to_s)
35
37
  return nil if extension.nil?
36
38
 
37
- file = States::FileState.new(path: filepath, content_type: @content_types[extension])
39
+ file_path = url.local_path(@web_root)
40
+ file = States::FileState.new(path: file_path, content_type: @content_types[extension])
38
41
 
39
- Events::FileEvent.trigger(file:, request: event.request)
42
+ Events::FileEvent.take(file:, request: event.request)
40
43
  end
41
44
  end
42
45
  end
data/lib/version.rb CHANGED
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Low
4
4
  module Loop
5
- VERSION = '0.7.2'
5
+ VERSION = '0.8.0'
6
6
  end
7
7
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: low_loop
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.7.2
4
+ version: 0.8.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -129,12 +129,13 @@ executables: []
129
129
  extensions: []
130
130
  extra_rdoc_files: []
131
131
  files:
132
+ - lib/connections/connection_manager.rb
133
+ - lib/connections/request_parser.rb
132
134
  - lib/events/file_event.rb
133
- - lib/factories/response_factory.rb
134
135
  - lib/low_loop.rb
135
- - lib/requests/request_parser.rb
136
136
  - lib/responses/file_response.rb
137
137
  - lib/responses/response_builder.rb
138
+ - lib/responses/response_factory.rb
138
139
  - lib/servers/file_server.rb
139
140
  - lib/states/file_state.rb
140
141
  - lib/support/config_loader.rb
@@ -1,27 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'protocol/http'
4
- require 'protocol/http/body/file'
5
-
6
- module Low
7
- module Factories
8
- class ResponseFactory
9
- class << self
10
- def html(body:)
11
- headers = Protocol::HTTP::Headers.new([['content-type', 'text/html']])
12
- body = Protocol::HTTP::Body::Buffered.wrap(body.strip)
13
-
14
- Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
15
- end
16
-
17
- def file(path:, content_type:)
18
- headers = Protocol::HTTP::Headers.new([['content-type', content_type]])
19
- file = File.open(path, 'rb')
20
- body = Protocol::HTTP::Body::File.new(file)
21
-
22
- Protocol::HTTP::Response.new('HTTP/1.1', 200, headers, body)
23
- end
24
- end
25
- end
26
- end
27
- end
File without changes