lowloop 0.8.1
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 +7 -0
- data/lib/connections/connection_manager.rb +69 -0
- data/lib/connections/request_parser.rb +76 -0
- data/lib/events/file_event.rb +19 -0
- data/lib/lowloop.rb +99 -0
- data/lib/responses/file_response.rb +20 -0
- data/lib/responses/response_builder.rb +54 -0
- data/lib/responses/response_factory.rb +31 -0
- data/lib/servers/file_server.rb +45 -0
- data/lib/states/file_state.rb +7 -0
- data/lib/support/config_loader.rb +28 -0
- data/lib/support/low_frame.rb +53 -0
- data/lib/version.rb +7 -0
- metadata +166 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 0cb4879d732cfa8bcceccce38ec1712d3ad5abd65fd5d06243301c3c7bc7fbfc
|
|
4
|
+
data.tar.gz: c40fa4f9fa6c7bb068b690ae91c66d3cc3b994bf9329bad57e88c738f356e667
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 6a9ac09e3922269acf23efb9f257d69ae1835dd6775a41a0962e287524a9b1025a20da6836c7cc92fd64dbce6cd1dc98cd826b395a4477cc78ba3ee47636d7a1
|
|
7
|
+
data.tar.gz: d0b84d595b03f666a3b60dc7e14e011dc611a4c45dc49869fae240b6711b6af5b19a94c732f32cafbd67ed36f9dab789580c3d16c92c1166c46ea617b39c05da
|
|
@@ -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
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/stream'
|
|
4
|
+
require 'async/http'
|
|
5
|
+
require 'protocol/http'
|
|
6
|
+
|
|
7
|
+
module Low
|
|
8
|
+
class RequestParser
|
|
9
|
+
include LowType
|
|
10
|
+
|
|
11
|
+
class << self
|
|
12
|
+
def create_stream(socket: TCPSocket) -> { IO::Stream::Generic }
|
|
13
|
+
IO::Stream(socket)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def parse(stream: IO::Stream::Generic, host: String, port: Integer, version: nil) -> { ::Protocol::HTTP::Request | nil }
|
|
17
|
+
version ||= Async::HTTP::Protocol::HTTP.default.protocol_for(stream)::VERSION
|
|
18
|
+
|
|
19
|
+
result = parse_request(stream:)
|
|
20
|
+
return nil unless result
|
|
21
|
+
|
|
22
|
+
method, full_path, = result
|
|
23
|
+
headers = parse_headers(stream:)
|
|
24
|
+
body = parse_body(stream:, method:, headers:)
|
|
25
|
+
|
|
26
|
+
::Protocol::HTTP::Request.new('http', "#{host}:#{port}", method, full_path, version, headers, body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
# HTTP Request format:
|
|
32
|
+
#
|
|
33
|
+
# :verb :path HTTP/1.1\r\n
|
|
34
|
+
# :header_1\r\n
|
|
35
|
+
# :header_2\r\n
|
|
36
|
+
# :header_3\r\n
|
|
37
|
+
# \r\n
|
|
38
|
+
# :body
|
|
39
|
+
def parse_request(stream: IO::Stream::Generic)
|
|
40
|
+
request_line = stream.gets || return
|
|
41
|
+
|
|
42
|
+
request_line = request_line.strip
|
|
43
|
+
return nil if request_line.empty?
|
|
44
|
+
|
|
45
|
+
method, full_path, _http_version = request_line.split(' ', 3)
|
|
46
|
+
path, query = full_path.split('?', 2)
|
|
47
|
+
|
|
48
|
+
[method, full_path, path, query]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def parse_headers(stream: IO::Stream::Generic) -> { ::Protocol::HTTP::Headers }
|
|
52
|
+
fields = []
|
|
53
|
+
|
|
54
|
+
# Sometimes returns nil which represents a client disconnect mid-request.
|
|
55
|
+
while (line = stream.gets)
|
|
56
|
+
line = line.strip
|
|
57
|
+
break if line.empty?
|
|
58
|
+
|
|
59
|
+
key, value = line.split(/:\s/, 2)
|
|
60
|
+
fields << [key, value]
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
::Protocol::HTTP::Headers.new(fields)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def parse_body(stream: IO::Stream::Generic, method: String, headers: ::Protocol::HTTP::Headers)
|
|
67
|
+
return nil unless %w[POST PUT PATCH].include?(method)
|
|
68
|
+
|
|
69
|
+
content_length = headers['content-length']&.first&.to_i
|
|
70
|
+
return nil unless content_length&.positive?
|
|
71
|
+
|
|
72
|
+
stream.read(content_length)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'lowevent'
|
|
4
|
+
|
|
5
|
+
module Low
|
|
6
|
+
module Events
|
|
7
|
+
class FileEvent < LowEvent
|
|
8
|
+
attr_reader :file, :request
|
|
9
|
+
|
|
10
|
+
# TODO: For RouteEvent/FileEvent parse and provide query params as attributes on the event.
|
|
11
|
+
def initialize(file:, request: nil)
|
|
12
|
+
super(key: self.class, action: :request)
|
|
13
|
+
|
|
14
|
+
@file = file
|
|
15
|
+
@request = request
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
data/lib/lowloop.rb
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'async'
|
|
4
|
+
require 'paint'
|
|
5
|
+
require 'socket'
|
|
6
|
+
require 'lowtype'
|
|
7
|
+
require 'lowevent'
|
|
8
|
+
require 'observers'
|
|
9
|
+
|
|
10
|
+
require_relative 'responses/response_factory'
|
|
11
|
+
require_relative 'connections/connection_manager'
|
|
12
|
+
require_relative 'servers/file_server'
|
|
13
|
+
require_relative 'support/low_frame'
|
|
14
|
+
|
|
15
|
+
class LowLoop
|
|
16
|
+
include Observers
|
|
17
|
+
|
|
18
|
+
attr_reader :config
|
|
19
|
+
|
|
20
|
+
def initialize(config:, router: nil, renderer: nil, show_output: true)
|
|
21
|
+
@config = config
|
|
22
|
+
@connection_manager = Low::ConnectionManager.new(config:)
|
|
23
|
+
@frame = LowFrame.new(renderer:, fps: 10, show_output:)
|
|
24
|
+
|
|
25
|
+
Low::Events::RequestEvent.define do |observers|
|
|
26
|
+
observers << Low::FileServer.new(web_root: config.web_root, content_types: config.content_types)
|
|
27
|
+
observers << router if router
|
|
28
|
+
observers.push(self, action: :mirror) if config.mirror_mode
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def start
|
|
33
|
+
server = start_server
|
|
34
|
+
|
|
35
|
+
Async do |task|
|
|
36
|
+
render_frame(task)
|
|
37
|
+
|
|
38
|
+
loop do
|
|
39
|
+
socket = server.accept
|
|
40
|
+
|
|
41
|
+
task.async do
|
|
42
|
+
@connection_manager.handle_connection(socket:)
|
|
43
|
+
rescue StandardError => e
|
|
44
|
+
render_error(e)
|
|
45
|
+
ensure
|
|
46
|
+
socket&.close
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
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
|
+
|
|
66
|
+
def start_server
|
|
67
|
+
puts "Starting server @ #{config.host}:#{config.port}" unless config.matrix_mode
|
|
68
|
+
|
|
69
|
+
server = TCPServer.new(config.host, config.port)
|
|
70
|
+
server.listen(10)
|
|
71
|
+
server
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def render_frame(task)
|
|
75
|
+
return unless @frame.renderer
|
|
76
|
+
|
|
77
|
+
task.async do
|
|
78
|
+
loop do
|
|
79
|
+
@frame.render
|
|
80
|
+
sleep 0.1 # 10fps
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render_error(error)
|
|
86
|
+
puts "\nException:"
|
|
87
|
+
puts Paint[error.message, :red]
|
|
88
|
+
puts ''
|
|
89
|
+
|
|
90
|
+
return unless @config.debug_mode
|
|
91
|
+
|
|
92
|
+
Fiber.blocking do
|
|
93
|
+
puts Paint[error.backtrace.join("\n"), :blue]
|
|
94
|
+
puts ''
|
|
95
|
+
puts 'Press ENTER to continue...'
|
|
96
|
+
gets
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative '../responses/response_factory'
|
|
4
|
+
|
|
5
|
+
module Low
|
|
6
|
+
class FileResponse
|
|
7
|
+
class << self
|
|
8
|
+
def request(event:)
|
|
9
|
+
file = event.file
|
|
10
|
+
|
|
11
|
+
if File.exist?(file.path)
|
|
12
|
+
response = ResponseFactory.file(path: file.path, content_type: file.content_type)
|
|
13
|
+
return Events::ResponseEvent.new(response:).tap(&:branch)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
nil
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Low
|
|
4
|
+
class ResponseBuilder
|
|
5
|
+
class << self
|
|
6
|
+
# TODO: Use Async wherever we can where it doesn't have "Task" requirement.
|
|
7
|
+
def respond(config:, socket:, response:, keep_alive: true)
|
|
8
|
+
file_body = response.body.respond_to?(:file)
|
|
9
|
+
|
|
10
|
+
if file_body
|
|
11
|
+
content_length = response.body.file.size
|
|
12
|
+
else
|
|
13
|
+
body_data = response.body.read || ''
|
|
14
|
+
content_length = body_data.bytesize
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
write_status_line(socket, response)
|
|
18
|
+
write_host_header(socket, config)
|
|
19
|
+
write_response_headers(socket, response)
|
|
20
|
+
write_final_headers(socket, content_length, keep_alive)
|
|
21
|
+
|
|
22
|
+
if file_body
|
|
23
|
+
IO.copy_stream(response.body.file, socket)
|
|
24
|
+
else
|
|
25
|
+
socket.write(body_data)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
def write_status_line(socket, response)
|
|
32
|
+
socket.puts "#{response.version} #{response.status}\r\n"
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def write_host_header(socket, config)
|
|
36
|
+
socket.puts config.port.nil? ? "Host: #{config.host}\r\n" : "Host: #{config.host}:#{config.port}\r\n"
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def write_response_headers(socket, response)
|
|
40
|
+
response.headers.fields.each do |key, value|
|
|
41
|
+
next if %w[content-length connection].include?(key.to_s.downcase)
|
|
42
|
+
|
|
43
|
+
socket.puts "#{key}: #{value}\r\n"
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def write_final_headers(socket, content_length, keep_alive)
|
|
48
|
+
socket.puts "Content-Length: #{content_length}\r\n"
|
|
49
|
+
socket.puts "Connection: #{keep_alive ? 'keep-alive' : 'close'}\r\n"
|
|
50
|
+
socket.puts "\r\n"
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -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
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'protocol/url'
|
|
4
|
+
|
|
5
|
+
require_relative '../events/file_event'
|
|
6
|
+
require_relative '../responses/file_response'
|
|
7
|
+
require_relative '../states/file_state'
|
|
8
|
+
|
|
9
|
+
module Low
|
|
10
|
+
class FileServer
|
|
11
|
+
include LowType
|
|
12
|
+
include Observers
|
|
13
|
+
|
|
14
|
+
def initialize(web_root:, content_types:)
|
|
15
|
+
@web_root = web_root
|
|
16
|
+
@content_types = content_types.transform_keys(&:to_s)
|
|
17
|
+
|
|
18
|
+
observers(Events::FileEvent) << FileResponse
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def extension(file_path:)
|
|
22
|
+
extension = File.extname(file_path).delete_prefix('.')
|
|
23
|
+
|
|
24
|
+
return nil if extension == ''
|
|
25
|
+
return nil unless @content_types.key?(extension)
|
|
26
|
+
|
|
27
|
+
extension
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def request(event: Events::RequestEvent)
|
|
31
|
+
file_path = event.request.path
|
|
32
|
+
|
|
33
|
+
return nil unless file_path.include?('.')
|
|
34
|
+
|
|
35
|
+
url = Protocol::URL[file_path]
|
|
36
|
+
extension = extension(file_path: url.path.to_s)
|
|
37
|
+
return nil if extension.nil?
|
|
38
|
+
|
|
39
|
+
file_path = url.local_path(@web_root)
|
|
40
|
+
file = States::FileState.new(path: file_path, content_type: @content_types[extension])
|
|
41
|
+
|
|
42
|
+
Events::FileEvent.take(file:, request: event.request)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'ostruct'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
|
|
6
|
+
module Low
|
|
7
|
+
class ConfigLoader
|
|
8
|
+
class << self
|
|
9
|
+
def load(filepath, env = {})
|
|
10
|
+
config_file = YAML.safe_load_file(filepath, symbolize_names: true)
|
|
11
|
+
|
|
12
|
+
# Environment variables override config file.
|
|
13
|
+
config_data = config_file.merge(env) do |_key, old_value, new_value|
|
|
14
|
+
new_value.nil? ? old_value : new_value
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
OpenStruct.new(config_data)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def parse_boolean(value)
|
|
21
|
+
return true if value == '1'
|
|
22
|
+
return false if value == '0'
|
|
23
|
+
|
|
24
|
+
nil
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'io/console'
|
|
4
|
+
|
|
5
|
+
class LowFrame
|
|
6
|
+
attr_reader :screen_size, :renderer
|
|
7
|
+
|
|
8
|
+
def initialize(renderer:, fps: 10, show_output: true)
|
|
9
|
+
@renderer = renderer
|
|
10
|
+
@show_output = show_output
|
|
11
|
+
|
|
12
|
+
# Millisecond duration of each frame. We lose a small amount of precision dropping the decimal.
|
|
13
|
+
@frame_time = ((1.0 / fps) * 1000).to_i
|
|
14
|
+
@last_frame = nil
|
|
15
|
+
|
|
16
|
+
setup if renderer && show_output
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def render
|
|
20
|
+
return unless @last_frame.nil? || (current_timestamp - @last_frame) >= @frame_time
|
|
21
|
+
|
|
22
|
+
@last_frame = current_timestamp
|
|
23
|
+
@renderer.render(screen_size: @screen_size)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def setup
|
|
27
|
+
print "\e[?25l" # Hide cursor.
|
|
28
|
+
system 'clear'
|
|
29
|
+
|
|
30
|
+
resize
|
|
31
|
+
|
|
32
|
+
Signal.trap('WINCH') do
|
|
33
|
+
resize
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
Signal.trap('INT') do
|
|
37
|
+
print "\e[?25h\e[0m" # Show cursor and reset colors.
|
|
38
|
+
system 'clear'
|
|
39
|
+
exit
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def resize
|
|
44
|
+
row_count, column_count = IO.console.winsize
|
|
45
|
+
@screen_size = { row_count:, column_count: }
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def current_timestamp
|
|
51
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
|
|
52
|
+
end
|
|
53
|
+
end
|
data/lib/version.rb
ADDED
metadata
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: lowloop
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.1
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- maedi
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: async
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '0'
|
|
26
|
+
- !ruby/object:Gem::Dependency
|
|
27
|
+
name: async-http
|
|
28
|
+
requirement: !ruby/object:Gem::Requirement
|
|
29
|
+
requirements:
|
|
30
|
+
- - ">="
|
|
31
|
+
- !ruby/object:Gem::Version
|
|
32
|
+
version: '0'
|
|
33
|
+
type: :runtime
|
|
34
|
+
prerelease: false
|
|
35
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
36
|
+
requirements:
|
|
37
|
+
- - ">="
|
|
38
|
+
- !ruby/object:Gem::Version
|
|
39
|
+
version: '0'
|
|
40
|
+
- !ruby/object:Gem::Dependency
|
|
41
|
+
name: io-stream
|
|
42
|
+
requirement: !ruby/object:Gem::Requirement
|
|
43
|
+
requirements:
|
|
44
|
+
- - ">="
|
|
45
|
+
- !ruby/object:Gem::Version
|
|
46
|
+
version: '0'
|
|
47
|
+
type: :runtime
|
|
48
|
+
prerelease: false
|
|
49
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
50
|
+
requirements:
|
|
51
|
+
- - ">="
|
|
52
|
+
- !ruby/object:Gem::Version
|
|
53
|
+
version: '0'
|
|
54
|
+
- !ruby/object:Gem::Dependency
|
|
55
|
+
name: paint
|
|
56
|
+
requirement: !ruby/object:Gem::Requirement
|
|
57
|
+
requirements:
|
|
58
|
+
- - ">="
|
|
59
|
+
- !ruby/object:Gem::Version
|
|
60
|
+
version: '0'
|
|
61
|
+
type: :runtime
|
|
62
|
+
prerelease: false
|
|
63
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
64
|
+
requirements:
|
|
65
|
+
- - ">="
|
|
66
|
+
- !ruby/object:Gem::Version
|
|
67
|
+
version: '0'
|
|
68
|
+
- !ruby/object:Gem::Dependency
|
|
69
|
+
name: protocol-http
|
|
70
|
+
requirement: !ruby/object:Gem::Requirement
|
|
71
|
+
requirements:
|
|
72
|
+
- - ">="
|
|
73
|
+
- !ruby/object:Gem::Version
|
|
74
|
+
version: '0'
|
|
75
|
+
type: :runtime
|
|
76
|
+
prerelease: false
|
|
77
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
78
|
+
requirements:
|
|
79
|
+
- - ">="
|
|
80
|
+
- !ruby/object:Gem::Version
|
|
81
|
+
version: '0'
|
|
82
|
+
- !ruby/object:Gem::Dependency
|
|
83
|
+
name: protocol-url
|
|
84
|
+
requirement: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">="
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: '0'
|
|
89
|
+
type: :runtime
|
|
90
|
+
prerelease: false
|
|
91
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
92
|
+
requirements:
|
|
93
|
+
- - ">="
|
|
94
|
+
- !ruby/object:Gem::Version
|
|
95
|
+
version: '0'
|
|
96
|
+
- !ruby/object:Gem::Dependency
|
|
97
|
+
name: lowevent
|
|
98
|
+
requirement: !ruby/object:Gem::Requirement
|
|
99
|
+
requirements:
|
|
100
|
+
- - ">="
|
|
101
|
+
- !ruby/object:Gem::Version
|
|
102
|
+
version: '0'
|
|
103
|
+
type: :runtime
|
|
104
|
+
prerelease: false
|
|
105
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
106
|
+
requirements:
|
|
107
|
+
- - ">="
|
|
108
|
+
- !ruby/object:Gem::Version
|
|
109
|
+
version: '0'
|
|
110
|
+
- !ruby/object:Gem::Dependency
|
|
111
|
+
name: lowtype
|
|
112
|
+
requirement: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - "~>"
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '1.0'
|
|
117
|
+
type: :runtime
|
|
118
|
+
prerelease: false
|
|
119
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
120
|
+
requirements:
|
|
121
|
+
- - "~>"
|
|
122
|
+
- !ruby/object:Gem::Version
|
|
123
|
+
version: '1.0'
|
|
124
|
+
description: An asynchronous server that creates events for your event-driven application
|
|
125
|
+
to use
|
|
126
|
+
email:
|
|
127
|
+
- maediprichard@gmail.com
|
|
128
|
+
executables: []
|
|
129
|
+
extensions: []
|
|
130
|
+
extra_rdoc_files: []
|
|
131
|
+
files:
|
|
132
|
+
- lib/connections/connection_manager.rb
|
|
133
|
+
- lib/connections/request_parser.rb
|
|
134
|
+
- lib/events/file_event.rb
|
|
135
|
+
- lib/lowloop.rb
|
|
136
|
+
- lib/responses/file_response.rb
|
|
137
|
+
- lib/responses/response_builder.rb
|
|
138
|
+
- lib/responses/response_factory.rb
|
|
139
|
+
- lib/servers/file_server.rb
|
|
140
|
+
- lib/states/file_state.rb
|
|
141
|
+
- lib/support/config_loader.rb
|
|
142
|
+
- lib/support/low_frame.rb
|
|
143
|
+
- lib/version.rb
|
|
144
|
+
homepage: https://github.com/low-rb/lowloop
|
|
145
|
+
licenses: []
|
|
146
|
+
metadata:
|
|
147
|
+
homepage_uri: https://github.com/low-rb/lowloop
|
|
148
|
+
source_code_uri: https://github.com/low-rb/lowloop/src/branch/main
|
|
149
|
+
rdoc_options: []
|
|
150
|
+
require_paths:
|
|
151
|
+
- lib
|
|
152
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
153
|
+
requirements:
|
|
154
|
+
- - ">="
|
|
155
|
+
- !ruby/object:Gem::Version
|
|
156
|
+
version: 3.4.0
|
|
157
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
158
|
+
requirements:
|
|
159
|
+
- - ">="
|
|
160
|
+
- !ruby/object:Gem::Version
|
|
161
|
+
version: '0'
|
|
162
|
+
requirements: []
|
|
163
|
+
rubygems_version: 4.0.6
|
|
164
|
+
specification_version: 4
|
|
165
|
+
summary: An event-driven event loop
|
|
166
|
+
test_files: []
|