low_loop 0.2.0 → 0.3.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: 1ca13498531cc4abf7f577bee5f78387d0f42fcbb414e5b20be39db848a154e5
4
- data.tar.gz: '075782581f7fd32f7b7d077bbcf03101561560823f39a6f8ecd38ac312c3a363'
3
+ metadata.gz: 61a2d187bf64d4db14f12ab1dbdbe215cb3f608e3d529e17b5af8694c4bf453d
4
+ data.tar.gz: cea153ef1443bd98f22cecb8370e77136e7bfddb0143171bf94cbf18882b9e26
5
5
  SHA512:
6
- metadata.gz: ddaf50e11f1eb8a5af18580932fd4d84117ac0754ea84a468e7e78772ab3894df04774428b10ea8d45ebdaaa2e240cbc95ff2ae8fbd6af6848b31b1e2b299ca0
7
- data.tar.gz: a3bb6a044dda05bda5bf3f0b73cb1747c3b3c10275541651073b592a2cba3541b5a1d23d8e489d0a7828611e4fc4643c3dc1289c0a94a6d81a181ffe6ad03263
6
+ metadata.gz: 0dae5020c04416c92d23463f1def7f5dafd14bfe324d059e3242e2e1ecc65e583df12064c4981b8c34621dba10fab2162aa04bdb534cc3db81a53d1d9bfe7809
7
+ data.tar.gz: ec738cd515d3fee61f5ee98055c4fdeab4ae510c9ee50cc7591a5922874030c876da9d224b6e4f1060d17b2b098c11a1b0cb64ab73a99393ea815a76c6df7de9
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'low_event'
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()
13
+
14
+ @file = file
15
+ @request = request
16
+ end
17
+ end
18
+ end
19
+ end
@@ -1,16 +1,25 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'protocol/http'
4
+ require 'protocol/http/body/file'
4
5
 
5
6
  module Low
6
7
  class ResponseFactory
7
8
  class << self
8
- def response(body:)
9
+ def html(body:)
9
10
  headers = Protocol::HTTP::Headers.new(['content-type', 'text/html'])
10
11
  body = Protocol::HTTP::Body::Buffered.wrap(body)
11
12
 
12
13
  Protocol::HTTP::Response.new('http/1.1', 200, headers, body)
13
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
14
23
  end
15
24
  end
16
25
  end
data/lib/low_loop.rb CHANGED
@@ -7,17 +7,27 @@ require 'low_event'
7
7
  require 'observers'
8
8
 
9
9
  require_relative 'factories/response_factory'
10
- require_relative 'request_parser'
11
- require_relative 'response_builder'
10
+ require_relative 'requests/request_parser'
11
+ require_relative 'responses/response_builder'
12
12
 
13
13
  module Low
14
14
  class Loop
15
15
  include Observers
16
16
 
17
- def start(config:)
18
- server = TCPServer.new(config.host, config.port)
19
- server.listen(10)
20
- puts "Server@#{config.host}:#{config.port}" if config.matrix_mode
17
+ attr_reader :config
18
+
19
+ def initialize(config:, dependencies: [])
20
+ @config = config
21
+
22
+ dependencies.each do |dependency|
23
+ observers << dependency
24
+ end
25
+
26
+ observers.push(self, action: :mirror) if config.mirror_mode
27
+ end
28
+
29
+ def start
30
+ server = start_server
21
31
 
22
32
  Fiber.set_scheduler(Async::Scheduler.new)
23
33
 
@@ -27,13 +37,8 @@ module Low
27
37
 
28
38
  Fiber.schedule do
29
39
  request = RequestParser.parse(socket:, host: config.host, port: config.port)
30
-
31
- if config.mirror_mode
32
- response = ResponseFactory.response(body: "Thank you for visiting #{request.path} with '#{request.body}'")
33
- else
34
- response_event = take(Events::RequestEvent.new(request:))
35
- response = response_event.response
36
- end
40
+ response_event = take(event: Events::RequestEvent.new(request:))
41
+ response = response_event.response
37
42
 
38
43
  ResponseBuilder.respond(config:, socket:, response:)
39
44
  rescue StandardError => e
@@ -44,6 +49,26 @@ module Low
44
49
  end
45
50
  end
46
51
  end
52
+
53
+ def start_server
54
+ puts "Starting server @ #{config.host}:#{config.port}" unless config.matrix_mode
55
+
56
+ server = TCPServer.new(config.host, config.port)
57
+ server.listen(10)
58
+ server
59
+ end
60
+
61
+ # Fallback mode for when there's no dependencies and you want to know that the server is still working.
62
+ def mirror(event:)
63
+ request = event.request
64
+ response = ResponseFactory.html(body: "Thank you for visiting #{request.path} with body: '#{request.body}'")
65
+ Events::ResponseEvent.new(response:)
66
+ end
67
+
68
+ # Consider LowLoop a value object in the context of Observers (there can only be one).
69
+ def ==(other) = other.class == self.class
70
+ def eql?(other) = self == other
71
+ def hash = [self.class].hash
47
72
  end
48
73
  end
49
74
 
@@ -31,7 +31,8 @@ module Low
31
31
  # \r\n
32
32
  # :body
33
33
  #
34
- def parse_request(stream:) # TODO: Handle type for namespaced "IO:Stream".
34
+ # TODO: Handle type for namespaced "IO:Stream".
35
+ def parse_request(stream:)
35
36
  request_line = stream.gets || raise(StandardError, 'EOF')
36
37
 
37
38
  method, full_path, _http_version = request_line.strip.split(' ', 3)
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../factories/response_factory'
4
+
5
+ module Low
6
+ class FileResponse
7
+ class << self
8
+ def handle(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 Low::Events::ResponseEvent.new(response:)
14
+ end
15
+
16
+ nil
17
+ end
18
+ end
19
+ end
20
+ end
@@ -7,8 +7,19 @@ module Low
7
7
  def respond(config:, socket:, response:)
8
8
  socket.puts "#{response.version} #{response.status}\r\n"
9
9
  socket.puts config.port.nil? ? "Host: #{config.host}\r\n" : "Host: #{config.host}:#{config.port}\r\n"
10
+
11
+ response.headers.fields.each_slice(2) do |key, value|
12
+ socket.puts "#{key}: #{value}\r\n"
13
+ end
14
+
10
15
  socket.puts "\r\n"
11
- socket.puts(response.body.read)
16
+
17
+ if response.body.respond_to?(:file)
18
+ IO.copy_stream(response.body.file, socket)
19
+ else
20
+ socket.puts(response.body.read)
21
+ end
22
+
12
23
  socket.close
13
24
  end
14
25
  end
@@ -0,0 +1,49 @@
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(File) << FileResponse
19
+ end
20
+
21
+ def extension(filepath:)
22
+ extension = File.extname(filepath).delete_prefix('.')
23
+
24
+ return nil if extension == ''
25
+ return nil unless @content_types.key?(extension)
26
+
27
+ extension
28
+ end
29
+
30
+ # TODO: Define type: Events::RequestEvent
31
+ def handle(event:)
32
+ filepath = Protocol::URL[event.request.path].path
33
+
34
+ extension = extension(filepath:)
35
+ return nil if extension.nil?
36
+
37
+ file = FileState.new(path: safe_path(filepath), content_type: @content_types[extension])
38
+
39
+ trigger File, event: Events::FileEvent.new(file:, request: event.request)
40
+ end
41
+
42
+ private
43
+
44
+ def safe_path(path)
45
+ safe_path = path.split('/').reject { |segment| ['.', '..'].include?(segment) }.join('/')
46
+ File.join(@web_root, safe_path)
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Low
4
+ class FileState
5
+ attr_reader :path, :content_type
6
+
7
+ def initialize(path:, content_type:)
8
+ @path = path
9
+ @content_type = content_type
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ConfigSupport
4
+ class << self
5
+ def parse_boolean(value)
6
+ return true if value == '1'
7
+ return false if value == '0'
8
+
9
+ nil
10
+ end
11
+ end
12
+ end
data/lib/version.rb CHANGED
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Low
4
- LOOP_VERSION = '0.2.0'
4
+ LOOP_VERSION = '0.3.0'
5
5
  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.2.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - maedi
@@ -65,6 +65,20 @@ dependencies:
65
65
  - - ">="
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
+ - !ruby/object:Gem::Dependency
69
+ name: protocol-url
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'
68
82
  - !ruby/object:Gem::Dependency
69
83
  name: low_event
70
84
  requirement: !ruby/object:Gem::Requirement
@@ -101,10 +115,15 @@ executables: []
101
115
  extensions: []
102
116
  extra_rdoc_files: []
103
117
  files:
118
+ - lib/events/file_event.rb
104
119
  - lib/factories/response_factory.rb
105
120
  - lib/low_loop.rb
106
- - lib/request_parser.rb
107
- - lib/response_builder.rb
121
+ - lib/requests/request_parser.rb
122
+ - lib/responses/file_response.rb
123
+ - lib/responses/response_builder.rb
124
+ - lib/servers/file_server.rb
125
+ - lib/states/file_state.rb
126
+ - lib/support/config_support.rb
108
127
  - lib/version.rb
109
128
  homepage: https://github.com/low-rb/low_loop
110
129
  licenses: []