syntropy 0.39.0 → 0.41.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.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/TODO.md +44 -142
  4. data/cmd/console.rb +0 -2
  5. data/cmd/new/template/config/production.rb +2 -1
  6. data/cmd/new.rb +1 -1
  7. data/cmd/serve.rb +26 -0
  8. data/examples/agent/Dockerfile +25 -0
  9. data/examples/agent/Gemfile +3 -0
  10. data/examples/agent/README.md +40 -0
  11. data/examples/agent/TODO.md +5 -0
  12. data/examples/agent/app/_layout/default.rb +15 -0
  13. data/examples/agent/app/_lib/storage.rb +13 -0
  14. data/examples/agent/app/_schema/2026-01-01-initial.rb +9 -0
  15. data/examples/agent/app/assets/agent.js +63 -0
  16. data/examples/agent/app/assets/minigfm.js +207 -0
  17. data/examples/agent/app/assets/model.js +45 -0
  18. data/examples/agent/app/assets/style.css +25 -0
  19. data/examples/agent/app/assets/tools.js +74 -0
  20. data/examples/agent/app/assets/ui.js +50 -0
  21. data/examples/agent/app/index.rb +19 -0
  22. data/examples/agent/app/test.rb +7 -0
  23. data/examples/agent/config/Caddyfile +5 -0
  24. data/examples/agent/config/development.rb +5 -0
  25. data/examples/agent/config/production.rb +5 -0
  26. data/examples/agent/config/test.rb +5 -0
  27. data/examples/agent/docker-compose.yml +71 -0
  28. data/examples/agent/response_example.json +108 -0
  29. data/examples/agent/test/test_app.rb +14 -0
  30. data/examples/blog/app/foo.rb +1 -0
  31. data/examples/blog/config/production.rb +2 -1
  32. data/lib/syntropy/app.rb +6 -2
  33. data/lib/syntropy/applets/builtin/auto_refresh/watch.js +1 -1
  34. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +7 -0
  35. data/lib/syntropy/dev_mode.rb +8 -3
  36. data/lib/syntropy/http/client.rb +17 -1
  37. data/lib/syntropy/http/client_connection.rb +16 -0
  38. data/lib/syntropy/http/io_extensions.rb +32 -4
  39. data/lib/syntropy/http/server.rb +55 -24
  40. data/lib/syntropy/http/server_connection.rb +75 -4
  41. data/lib/syntropy/logger.rb +8 -0
  42. data/lib/syntropy/markdown.rb +44 -20
  43. data/lib/syntropy/module_loader.rb +29 -16
  44. data/lib/syntropy/request/request_info.rb +1 -1
  45. data/lib/syntropy/request/response.rb +75 -6
  46. data/lib/syntropy/request/validation.rb +6 -0
  47. data/lib/syntropy/storage/connection_pool.rb +34 -0
  48. data/lib/syntropy/test.rb +4 -4
  49. data/lib/syntropy/version.rb +1 -1
  50. data/lib/syntropy.rb +7 -1
  51. data/syntropy.gemspec +4 -4
  52. data/test/test_markdown.rb +3 -3
  53. data/test/test_module_loader.rb +4 -1
  54. metadata +34 -11
data/lib/syntropy/app.rb CHANGED
@@ -26,7 +26,7 @@ module Syntropy
26
26
  end
27
27
 
28
28
  BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
29
-
29
+
30
30
  # Creates a builtin applet with the given environment hash. By default the
31
31
  # builtin applet is mounted at /.syntropy.
32
32
  #
@@ -102,6 +102,7 @@ module Syntropy
102
102
  # @param req [Syntropy::Request] HTTP request
103
103
  # @return [void]
104
104
  def call(req)
105
+ # before = GC.stat(:total_allocated_objects)
105
106
  path = req.path
106
107
  route = @router_proc.(path, req.route_params)
107
108
  if !route
@@ -126,6 +127,9 @@ module Syntropy
126
127
  end
127
128
  error_handler = get_error_handler(route)
128
129
  error_handler.(req, e)
130
+ # ensure
131
+ # after = GC.stat(:total_allocated_objects)
132
+ # @logger&.debug(message: "Allocated: #{after - before}")
129
133
  end
130
134
 
131
135
  # Returns the route entry for the given path. If compute_proc is true,
@@ -483,7 +487,7 @@ module Syntropy
483
487
  #
484
488
  # @return [void]
485
489
  def start
486
- @module_loader.load('_setup', raise_on_missing: false)
490
+ @module_loader.load('_setup', raise_on_missing_export: false)
487
491
 
488
492
  @machine.spin do
489
493
  # we do startup stuff asynchronously, in order to first let Syntropy do
@@ -7,6 +7,6 @@
7
7
  if (msg.data != '') window.location.reload();
8
8
  })
9
9
  eventSource.addEventListener('error', () => {
10
- console.log(`Failed to connect to auto refresh watcher (${sseURL})`);
10
+ // console.log(`Failed to connect to auto refresh watcher (${sseURL})`);
11
11
  })
12
12
  })()
@@ -9,11 +9,15 @@
9
9
  # residing by default at `/.syntropy/auto_refresh/watch.js`.
10
10
 
11
11
  # Returns a hash holding references to queues for ongoing `watch.sse` requests.
12
+ #
13
+ # @return [Hash] hash of watchers
12
14
  def watchers
13
15
  @watchers ||= {}
14
16
  end
15
17
 
16
18
  # Signals a file change by pushing to all watcher queues.
19
+ #
20
+ # @return [void]
17
21
  def signal!
18
22
  watchers.each_key { @machine.push(it, true) }
19
23
  end
@@ -21,6 +25,9 @@ end
21
25
  # Handles incoming requests to the `watch.sse` route. Adds a queue to the list
22
26
  # of watchers, and waits for the queue to be signalled. In the absence of file
23
27
  # change, a timeout occurs after one minute, and the request is terminated.
28
+ #
29
+ # @param req [Syntropy::Request] request
30
+ # @return [void]
24
31
  def call(req)
25
32
  queue = UM::Queue.new
26
33
  watchers[queue] = true
@@ -1,11 +1,16 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- TAG_DEBUG_PROC = ->(level, fn, line, col) {
3
+ # Injects debug information into Papercraft emitted HTML tags.
4
+ #
5
+ # @param level [Integer] nesting level
6
+ # @param fn [String] source filename
7
+ # @param line [Integer] source line
8
+ # @param col [Integer] source column
9
+ # @return [Hash] HTML data attributes
10
+ Papercraft::Compiler.html_debug_attribute_injector = ->(level, fn, line, col) {
4
11
  {
5
12
  'data-syntropy-level' => level,
6
13
  'data-syntropy-fn' => fn,
7
14
  'data-syntropy-loc' => "zed://file/#{fn}:#{line}:#{col}"
8
15
  }
9
16
  }
10
-
11
- Papercraft::Compiler.html_debug_attribute_injector = TAG_DEBUG_PROC
@@ -7,10 +7,16 @@ module Syntropy
7
7
  module HTTP
8
8
  # HTTP Client class.
9
9
  class Client
10
+ # Initializes an HTTP client.
10
11
  def initialize(machine)
11
12
  @machine = machine
12
13
  end
13
14
 
15
+ # Performs a GET request.
16
+ #
17
+ # @param url [String] URL
18
+ # @param headers [Hash] request headers
19
+ # @return [Array] array containing response headers and body
14
20
  def get(url, **headers, &)
15
21
  uri = URI.parse(url)
16
22
  headers = headers.merge(
@@ -22,7 +28,11 @@ module Syntropy
22
28
 
23
29
  private
24
30
 
25
- # @param uri [URI]
31
+ # Performs an HTTP request, returning the response headers and body.
32
+ #
33
+ # @param uri [URI] request URI
34
+ # @param headers [Hash] request headers
35
+ # @return [Array] array containing response headers and body
26
36
  def req(uri, **headers)
27
37
  connection = make_connection(uri.scheme, uri.host, uri.port)
28
38
  response_headers = connection.req(**headers)
@@ -33,6 +43,12 @@ module Syntropy
33
43
  end
34
44
  end
35
45
 
46
+ # Creates an HTTP connection.
47
+ #
48
+ # @param _scheme [String] connection scheme
49
+ # @param host [String] host
50
+ # @param port [Integer] port
51
+ # @return [Syntropy::HTTP::ClientConnection]
36
52
  def make_connection(_scheme, host, port)
37
53
  ip = (host =~ /^\d+\.\d+\.\d+\.\d+$/) ? host : @machine.resolve(host)[0]
38
54
 
@@ -5,15 +5,27 @@ require 'syntropy/http/io_extensions'
5
5
 
6
6
  module Syntropy
7
7
  module HTTP
8
+ # ClientConnection represents an HTTP client connection.
8
9
  class ClientConnection
9
10
  attr_reader :fd, :response_headers, :logger
10
11
 
12
+ # Initializes a ClientConnection.
13
+ #
14
+ # @param machine [UringMachine] machine instance
15
+ # @param fd [Integer] file descriptor
16
+ # @param io_mode [Symbol] IO mode
17
+ # @return [void]
11
18
  def initialize(machine, fd, io_mode: :socket)
12
19
  @machine = machine
13
20
  @fd = fd
14
21
  @io = machine.io(fd, io_mode)
15
22
  end
16
23
 
24
+ # Performs a request with the given body and headers.
25
+ #
26
+ # @param body [String, nil] request body
27
+ # @param headers [Hash] request headers
28
+ # @return [Hash] response headers
17
29
  def req(body: nil, **headers)
18
30
  if body
19
31
  headers = headers.merge(
@@ -28,6 +40,10 @@ module Syntropy
28
40
  @io.http_read_response_headers
29
41
  end
30
42
 
43
+ # Returns the response body
44
+ #
45
+ # @param headers [Hash] response headers
46
+ # @return [String, nil] response body
31
47
  def get_response_body(headers)
32
48
  @io.http_read_body(headers)
33
49
  end
@@ -4,6 +4,7 @@ require 'syntropy/errors'
4
4
 
5
5
  module Syntropy
6
6
  module HTTP
7
+ # HTTP protocol extensions for UringMachine::IO
7
8
  module ProtocolMethods
8
9
  RE_REQUEST_LINE = /^(get|head|options|trace|put|delete|post|patch|connect)\s+([^\s]+)\s+HTTP\/([019\.]{1,3})/i
9
10
  RE_RESPONSE_LINE = /^HTTP\/1\.1\s+(\d{3})(\s+.+)?$/i
@@ -14,7 +15,9 @@ module Syntropy
14
15
  MAX_HEADER_LINE_LEN = 1 << 13 # 8KB
15
16
  MAX_CHUNK_SIZE_LEN = 16
16
17
 
17
- # @return [Hash] headers
18
+ # Reads HTTP request headers.
19
+ #
20
+ # @return [Hash] request headers
18
21
  def http_read_request_headers
19
22
  line = read_line(MAX_REQUEST_LINE_LEN)
20
23
  return nil if !line
@@ -43,6 +46,9 @@ module Syntropy
43
46
  headers
44
47
  end
45
48
 
49
+ # Reads HTTP response headers.
50
+ #
51
+ # @return [Hash] response headers
46
52
  def http_read_response_headers
47
53
  line = read_line(MAX_RESPONSE_LINE_LEN)
48
54
  return nil if !line
@@ -73,6 +79,10 @@ module Syntropy
73
79
  headers
74
80
  end
75
81
 
82
+ # Reads an HTTP request/response body.
83
+ #
84
+ # @param headers [Hash] request/response headers
85
+ # @return [String] body
76
86
  def http_read_body(headers)
77
87
  content_length = headers['content-length']
78
88
  if content_length
@@ -95,6 +105,10 @@ module Syntropy
95
105
  nil
96
106
  end
97
107
 
108
+ # Skips an HTTP reuqest/response body.
109
+ #
110
+ # @param headers [Hash] request/response headers
111
+ # @return [void]
98
112
  def http_skip_body(headers)
99
113
  content_length = headers['content-length']
100
114
  if content_length
@@ -109,10 +123,12 @@ module Syntropy
109
123
  while http_skip_cte_chunk
110
124
  end
111
125
  end
112
-
113
- nil
114
126
  end
115
127
 
128
+ # Reads an HTTP body chunk. If no chunks remain to be read, returns nil.
129
+ #
130
+ # @param headers [Hash] request/response headers
131
+ # @return [String, nil] body chunk
116
132
  def http_read_body_chunk(headers)
117
133
  content_length = headers['content-length']
118
134
  if content_length
@@ -129,6 +145,10 @@ module Syntropy
129
145
  nil
130
146
  end
131
147
 
148
+ # Writes HTTP request headers.
149
+ #
150
+ # @param headers [Hash] request headers
151
+ # @return [void]
132
152
  def http_write_request_headers(headers)
133
153
  method = headers[':method'] || (raise BadRequestError)
134
154
  path = headers[':path'] || (raise BadRequestError)
@@ -149,6 +169,11 @@ module Syntropy
149
169
 
150
170
  private
151
171
 
172
+ # Read a chunk (in chunked transfer encoding) into the given buffer. If no
173
+ # buffer is given, returns the chunk, otherwise returns the buffer.
174
+ #
175
+ # @param buffer [String, nil] buffer
176
+ # @return [String] buffer or chunk
152
177
  def http_read_cte_chunk(buffer)
153
178
  chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
154
179
  return nil if !chunk_size_str
@@ -165,6 +190,9 @@ module Syntropy
165
190
  buffer ? (buffer << chunk) : chunk
166
191
  end
167
192
 
193
+ # Skips a chunk (in chunked transfer encoding).
194
+ #
195
+ # @return [void]
168
196
  def http_skip_cte_chunk
169
197
  chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
170
198
  return if !chunk_size_str
@@ -172,7 +200,7 @@ module Syntropy
172
200
  chunk_size = chunk_size_str.to_i(16)
173
201
  if chunk_size == 0
174
202
  read_line(0)
175
- return nil
203
+ return
176
204
  end
177
205
 
178
206
  chunk = skip(chunk_size)
@@ -4,41 +4,28 @@ require 'syntropy/http/server_connection'
4
4
 
5
5
  module Syntropy
6
6
  module HTTP
7
+ # HTTP::Server implements an HTTP server.
7
8
  class Server
8
9
  PENDING_REQUESTS_GRACE_PERIOD = 0.1
9
10
  PENDING_REQUESTS_TIMEOUT_PERIOD = 5
10
11
 
11
- def self.syntropy_app(_machine, env)
12
- if env[:app_location]
13
- env[:logger]&.info(message: 'Loading web app', location: env[:app_location])
14
- require env[:app_location]
15
-
16
- env.merge!(Syntropy.config)
17
- end
18
- env[:app]
19
- end
20
-
21
- def self.static_app(env); end
22
-
12
+ # Initializes a server instance.
13
+ #
14
+ # @param machine [UringMachine] machine instance
15
+ # @param env [Hash] app environment
16
+ # @param app [Proc, Syntropy::App] app instance
17
+ # @return [void]
23
18
  def initialize(machine, env, &app)
24
19
  @machine = machine
25
20
  @env = env
26
- @app = app || app_from_env
21
+ @app = app
27
22
  @server_fds = []
28
23
  @accept_fibers = []
29
24
  end
30
25
 
31
- def app_from_env
32
- case @env[:app_type]
33
- when nil, :syntropy
34
- Server.syntropy_app(@machine, @env)
35
- when :static
36
- Server.static_app(@env)
37
- else
38
- raise "Invalid app type #{@env[:app_type].inspect}"
39
- end
40
- end
41
-
26
+ # Runs the server.
27
+ #
28
+ # @return [void]
42
29
  def run
43
30
  setup
44
31
  @machine.await(@accept_fibers)
@@ -46,12 +33,18 @@ module Syntropy
46
33
  graceful_shutdown
47
34
  end
48
35
 
36
+ # Stops the server with graceful shutdown.
37
+ #
38
+ # @return [void]
49
39
  def stop!
50
40
  graceful_shutdown
51
41
  end
52
42
 
53
43
  private
54
44
 
45
+ # Sets up the server.
46
+ #
47
+ # @return [void]
55
48
  def setup
56
49
  bind_info = get_bind_entries
57
50
  bind_info.each do |(host, port)|
@@ -67,6 +60,9 @@ module Syntropy
67
60
  @connection_fibers = Set.new
68
61
  end
69
62
 
63
+ # Returns bind entries from the app environment.
64
+ #
65
+ # @return [Array<Array>] array containing host/port tuples
70
66
  def get_bind_entries
71
67
  bind = @env[:bind]
72
68
  case bind
@@ -80,11 +76,20 @@ module Syntropy
80
76
  end
81
77
  end
82
78
 
79
+ # Parses a bind string into a host/port tuple.
80
+ #
81
+ # @param bind_string [String] bind string
82
+ # @return [Array<String, Integer>] array containing host and port
83
83
  def bind_info(bind_string)
84
84
  parts = bind_string.split(':')
85
85
  [parts[0], parts[1].to_i]
86
86
  end
87
87
 
88
+ # Sets up a TCP socket listening on the given host and port.
89
+ #
90
+ # @param host [String] host
91
+ # @param port [Integer] port
92
+ # @return [Integer] socket fd
88
93
  def setup_server_socket(host, port)
89
94
  fd = @machine.socket(UM::AF_INET, UM::SOCK_STREAM, 0, 0)
90
95
  @machine.setsockopt(fd, UM::SOL_SOCKET, UM::SO_REUSEADDR, true)
@@ -94,6 +99,10 @@ module Syntropy
94
99
  fd
95
100
  end
96
101
 
102
+ # Setup server extensions. This is used for adding additional headers to
103
+ # the response, namely the Server the Date headers.
104
+ #
105
+ # @return [void]
97
106
  def setup_server_extensions
98
107
  extensions = @env[:server_extensions]
99
108
  return if !extensions
@@ -109,6 +118,10 @@ module Syntropy
109
118
  end
110
119
  end
111
120
 
121
+ # Updates server headers.
122
+ #
123
+ # @param server_name [String] server name
124
+ # @return [void]
112
125
  def update_server_headers(server_name)
113
126
  @env[:server_date] = Time.now
114
127
  if server_name
@@ -118,12 +131,20 @@ module Syntropy
118
131
  end
119
132
  end
120
133
 
134
+ # Accepts incoming connections, spinning up a fiber for connection.
135
+ #
136
+ # @param listen_fd [Integer] listening socket fd
137
+ # @return [void]
121
138
  def accept_incoming(listen_fd)
122
139
  @machine.accept_each(listen_fd) { start_connection(it) }
123
140
  rescue UM::Terminate
124
141
  @machine.shutdown(listen_fd, UM::SHUT_RD)
125
142
  end
126
143
 
144
+ # Starts handling a connection on a separate fiber.
145
+ #
146
+ # @param fd [Integer] socket fd
147
+ # @return [void]
127
148
  def start_connection(fd)
128
149
  conn = ServerConnection.new(@machine, fd, @env, &@app)
129
150
  f = @machine.spin(conn) do
@@ -134,17 +155,27 @@ module Syntropy
134
155
  @connection_fibers << f
135
156
  end
136
157
 
158
+ # Closes all listening socket fd's.
159
+ #
160
+ # @return [void]
137
161
  def close_all_server_fds
138
162
  @server_fds.each { @machine.close_async(it) }
139
163
  end
140
164
 
141
165
  STOP = UM::Terminate.new
142
166
 
167
+ # Stops all fibers listening for incoming connections.
168
+ #
169
+ # @return [void]
143
170
  def stop_accept_fibers
144
171
  @accept_fibers.each { @machine.schedule(it, STOP) if !it.done? }
145
172
  @machine.await(@accept_fibers)
146
173
  end
147
174
 
175
+ # Performs a graceful shutdown by stopping listening, then waiting for
176
+ # connection fibers to stop with a timeout of 5 seconds.
177
+ #
178
+ # @return [void]
148
179
  def graceful_shutdown
149
180
  @env[:logger]&.info(message: 'Shutting down gracefully...')
150
181
 
@@ -13,6 +13,14 @@ module Syntropy
13
13
  class ServerConnection
14
14
  attr_reader :fd, :response_headers, :logger
15
15
 
16
+ # Initializes a server connection.
17
+ #
18
+ # @param machine [UringMachine] machine instance
19
+ # @param fd [Integer] file descriptor
20
+ # @param env [Hash] app environment
21
+ # @param io_mode [Symbol] IO mode
22
+ # @param app [Proc, Syntropy::App] server app
23
+ # @return [void]
16
24
  def initialize(machine, fd, env, io_mode: :socket, &app)
17
25
  @machine = machine
18
26
  @fd = fd
@@ -26,6 +34,9 @@ module Syntropy
26
34
  @response_cookies = nil
27
35
  end
28
36
 
37
+ # Runs the connection.
38
+ #
39
+ # @return [void]
29
40
  def run
30
41
  loop do
31
42
  persist = serve_request
@@ -33,6 +44,10 @@ module Syntropy
33
44
  end
34
45
  rescue UM::Terminate
35
46
  # server is terminated, do nothing
47
+ rescue SystemCallError
48
+ @logger&.error(
49
+ message: 'IO Error, closing...'
50
+ )
36
51
  rescue StandardError => e
37
52
  @logger&.error(
38
53
  message: 'Uncaught error while running connection',
@@ -76,9 +91,10 @@ module Syntropy
76
91
  # @return [void]
77
92
  def handle_error(request, err)
78
93
  case err
94
+ when Errno::ECONNRESET
95
+ # ignore
79
96
  when SystemCallError
80
- log_error(err, 'I/O error')
81
- false
97
+ log_error(err, 'Syscall error')
82
98
  when ProtocolError
83
99
  log_error(err, err.message)
84
100
  respond(request, err.message, ':status' => err.http_status)
@@ -99,6 +115,10 @@ module Syntropy
99
115
  @logger&.error(message: "#{message}, closing connection", error: err)
100
116
  end
101
117
 
118
+ # Reads the request body.
119
+ #
120
+ # @param req [Syntropy::Request] request
121
+ # @return [String, nil] request body
102
122
  def get_body(req)
103
123
  headers = req.headers
104
124
  return nil if headers[':body-done-reading']
@@ -108,6 +128,10 @@ module Syntropy
108
128
  body
109
129
  end
110
130
 
131
+ # Reads a request body chunk.
132
+ #
133
+ # @param req [Syntropy::Request] request
134
+ # @return [String, nil] request body chunk
111
135
  def get_body_chunk(req)
112
136
  headers = req.headers
113
137
  return nil if headers[':body-done-reading']
@@ -117,6 +141,9 @@ module Syntropy
117
141
  chunk
118
142
  end
119
143
 
144
+ # Returns true if the request is done
145
+ #
146
+ # @return [bool] whether the request is complete
120
147
  def complete?(req)
121
148
  req.headers[':body-done-reading']
122
149
  end
@@ -135,6 +162,11 @@ module Syntropy
135
162
 
136
163
  DELETE_COOKIE = "; Expires=Thu, 01 Jan 1970 00:00:00 GMT; Path=/; Max-Age=0; HttpOnly"
137
164
 
165
+ # Adds a Set-Cookie header to the response headers.
166
+ #
167
+ # @param key [String] cookie name
168
+ # @param value [String] cookie value
169
+ # @return [void]
138
170
  def set_cookie(key, value)
139
171
  (@response_cookies ||= {})[key] = value || DELETE_COOKIE
140
172
  end
@@ -210,6 +242,13 @@ module Syntropy
210
242
  @done = true
211
243
  end
212
244
 
245
+ # Responds by rendering a static file.
246
+ #
247
+ # @param req [Syntropy::Request] request
248
+ # @param path [String] file path
249
+ # @param env [Hash] app environment
250
+ # @param cache_headers [Hash] cache headers
251
+ # @return [void]
213
252
  def respond_with_static_file(req, path, env, cache_headers)
214
253
  fd = @machine.open(path, UM::O_RDONLY)
215
254
  env ||= {}
@@ -240,6 +279,9 @@ module Syntropy
240
279
  end
241
280
  end
242
281
 
282
+ # Closes the connection.
283
+ #
284
+ # @return [void]
243
285
  def close
244
286
  return if @closed
245
287
 
@@ -248,6 +290,9 @@ module Syntropy
248
290
  @machine.close_async(@fd)
249
291
  end
250
292
 
293
+ # Yields the uhnderlying connection IO and fd to the given block.
294
+ #
295
+ # @return [void]
251
296
  def with_stream
252
297
  yield @io, @fd
253
298
  end
@@ -260,6 +305,9 @@ module Syntropy
260
305
  MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
261
306
  MAX_CHUNK_SIZE_LEN = 16
262
307
 
308
+ # Returns true if the connection should be persisted.
309
+ #
310
+ # @param headers [Hash] request headers
263
311
  def persist_connection?(headers)
264
312
  connection = headers['connection']&.downcase
265
313
  return connection != 'close'
@@ -285,14 +333,23 @@ module Syntropy
285
333
  lines
286
334
  end
287
335
 
336
+ # Formats a response status line.
337
+ #
338
+ # @param body [String] response body
339
+ # @param status [Integer] HTTP status code
340
+ # @return [String] rendered status line
288
341
  def format_status_line(body, status)
289
342
  if !body
290
343
  empty_status_line(status)
291
344
  else
292
- with_body_status_line(status, body)
345
+ with_body_status_line(body, status)
293
346
  end
294
347
  end
295
348
 
349
+ # Returns the status line for an empty response.
350
+ #
351
+ # @param status [Integer] HTTP status code
352
+ # @return [String] rendered status line
296
353
  def empty_status_line(status)
297
354
  if status == 204
298
355
  +"HTTP/1.1 #{status}\r\n"
@@ -301,10 +358,21 @@ module Syntropy
301
358
  end
302
359
  end
303
360
 
304
- def with_body_status_line(status, body)
361
+ # Returns a status line for responses with body.
362
+ #
363
+ # @param body [String] response body
364
+ # @param status [Integer] HTTP status code
365
+ # @return [String] rendered status line
366
+ def with_body_status_line(body, status)
305
367
  +"HTTP/1.1 #{status}\r\nTransfer-Encoding: chunked\r\n"
306
368
  end
307
369
 
370
+ # Renders headers into a lines array.
371
+ #
372
+ # @param lines [Array<String>] array of header lines
373
+ # @param key [String] header name
374
+ # @param value [String, Array<String>] header value(s)
375
+ # @return [void]
308
376
  def collect_header_lines(lines, key, value)
309
377
  if value.is_a?(Array)
310
378
  value.inject(lines) { |_, item| lines << "#{key}: #{item}\r\n" }
@@ -313,6 +381,9 @@ module Syntropy
313
381
  end
314
382
  end
315
383
 
384
+ # Adds Set-Cookie headers to the response headers.
385
+ #
386
+ # @return [void]
316
387
  def add_set_cookie_headers
317
388
  @response_headers ||= {}
318
389
  sc = (@response_headers['Set-Cookie'] ||= [])
@@ -17,6 +17,14 @@ module Syntropy
17
17
  @opts = opts
18
18
  end
19
19
 
20
+ # Logs an DEBUG entry.
21
+ #
22
+ # @param o [Hash] log entry
23
+ # @return [void]
24
+ def debug(o)
25
+ call(:DEBUG, o)
26
+ end
27
+
20
28
  # Logs an INFO entry.
21
29
  #
22
30
  # @param o [Hash] log entry