syntropy 0.32.0 → 0.34.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/TODO.md +0 -39
  4. data/cmd/console.rb +18 -7
  5. data/cmd/serve.rb +26 -20
  6. data/cmd/test.rb +90 -21
  7. data/examples/blog/.gitignore +1 -0
  8. data/examples/blog/app/_lib/database.rb +13 -0
  9. data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
  10. data/examples/blog/app/posts/[id]/edit.rb +2 -2
  11. data/examples/blog/app/posts/[id]/index.rb +8 -5
  12. data/examples/blog/app/posts/index.rb +7 -5
  13. data/examples/blog/app/posts/new.rb +1 -1
  14. data/examples/blog/config/development.rb +5 -0
  15. data/examples/blog/config/production.rb +4 -0
  16. data/examples/blog/config/test.rb +5 -0
  17. data/examples/blog/test/test_posts.rb +65 -0
  18. data/examples/mcp-oauth/app/oauth/token.rb +1 -1
  19. data/examples/mcp-oauth/test/test_app.rb +2 -20
  20. data/examples/mcp-oauth/test/test_oauth.rb +93 -217
  21. data/lib/syntropy/app.rb +48 -40
  22. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
  23. data/lib/syntropy/db/schema.rb +1 -1
  24. data/lib/syntropy/db/store.rb +2 -0
  25. data/lib/syntropy/errors.rb +6 -2
  26. data/lib/syntropy/http/client.rb +1 -0
  27. data/lib/syntropy/http/server_connection.rb +15 -13
  28. data/lib/syntropy/json_api.rb +27 -1
  29. data/lib/syntropy/logger.rb +81 -27
  30. data/lib/syntropy/markdown.rb +61 -32
  31. data/lib/syntropy/mime_types.rb +9 -5
  32. data/lib/syntropy/module_loader.rb +25 -13
  33. data/lib/syntropy/papercraft_extensions.rb +2 -2
  34. data/lib/syntropy/request/mock_adapter.rb +10 -8
  35. data/lib/syntropy/request/request_info.rb +91 -0
  36. data/lib/syntropy/request/response.rb +3 -14
  37. data/lib/syntropy/request/validation.rb +1 -0
  38. data/lib/syntropy/request.rb +55 -14
  39. data/lib/syntropy/routing_tree.rb +27 -28
  40. data/lib/syntropy/session.rb +198 -0
  41. data/lib/syntropy/side_run.rb +25 -2
  42. data/lib/syntropy/test.rb +168 -2
  43. data/lib/syntropy/utils.rb +53 -18
  44. data/lib/syntropy/version.rb +1 -1
  45. data/lib/syntropy.rb +44 -10
  46. data/syntropy.gemspec +1 -0
  47. data/test/bm_router_proc.rb +4 -4
  48. data/test/fixtures/app/class_instance.rb +5 -0
  49. data/test/fixtures/app/http.rb +5 -0
  50. data/test/fixtures/app/post_ct.rb +5 -0
  51. data/test/fixtures/app/singleton.rb +3 -0
  52. data/test/test_app.rb +13 -52
  53. data/test/test_caching.rb +2 -2
  54. data/test/test_db_schema.rb +1 -1
  55. data/test/test_http_server_connection.rb +11 -8
  56. data/test/test_module_loader.rb +5 -2
  57. data/test/test_request_session.rb +254 -0
  58. data/test/test_response.rb +0 -19
  59. data/test/test_routing_tree.rb +69 -69
  60. data/test/test_server.rb +5 -9
  61. data/test/test_test.rb +70 -0
  62. metadata +67 -42
  63. data/examples/blog/app/_setup.rb +0 -4
  64. data/examples/mcp-oauth/test/helper.rb +0 -9
  65. /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
  66. /data/test/{app → fixtures/app}/_hook.rb +0 -0
  67. /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
  68. /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
  69. /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
  70. /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
  71. /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
  72. /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
  73. /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
  74. /data/test/{app → fixtures/app}/about/_error.rb +0 -0
  75. /data/test/{app → fixtures/app}/about/foo.md +0 -0
  76. /data/test/{app → fixtures/app}/about/index.rb +0 -0
  77. /data/test/{app → fixtures/app}/about/raise.rb +0 -0
  78. /data/test/{app → fixtures/app}/api+.rb +0 -0
  79. /data/test/{app → fixtures/app}/assets/style.css +0 -0
  80. /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
  81. /data/test/{app → fixtures/app}/bar.rb +0 -0
  82. /data/test/{app → fixtures/app}/baz.rb +0 -0
  83. /data/test/{app → fixtures/app}/by_method.rb +0 -0
  84. /data/test/{app → fixtures/app}/deps.rb +0 -0
  85. /data/test/{app → fixtures/app}/index.html +0 -0
  86. /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
  87. /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
  88. /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
  89. /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
  90. /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
  91. /data/test/{app → fixtures/app}/rss.rb +0 -0
  92. /data/test/{app → fixtures/app}/tmp.rb +0 -0
  93. /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
  94. /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
  95. /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
  96. /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
  97. /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
  98. /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
  99. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
  100. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
  101. /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
  102. /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
+ # Implements a mock adapter for testing
4
5
  class MockAdapter
5
6
  attr_reader :response_body, :response_headers, :calls
6
7
 
@@ -19,14 +20,13 @@ module Syntropy
19
20
  end
20
21
 
21
22
  def initialize(request_body)
22
- case request_body
23
- when Array
24
- @request_body_chunks = request_body
25
- when nil
26
- @request_body_chunks = []
27
- else
28
- @request_body_chunks = [request_body]
29
- end
23
+ @request_body_chunks =
24
+ case request_body
25
+ when Array then request_body
26
+ when nil then []
27
+ else [request_body]
28
+ end
29
+
30
30
  @calls = []
31
31
  end
32
32
 
@@ -47,6 +47,8 @@ module Syntropy
47
47
  response_headers[':status'] || HTTP::OK
48
48
  end
49
49
 
50
+ def respond_to_missing?(sym) = true
51
+
50
52
  def method_missing(sym, *args)
51
53
  calls << [sym, *args]
52
54
  end
@@ -3,36 +3,61 @@
3
3
  require 'uri'
4
4
 
5
5
  module Syntropy
6
+ # Request information extension methods.
6
7
  module RequestInfoMethods
8
+ # Returns the request host.
9
+ #
10
+ # @return [String, nil]
7
11
  def host
8
12
  @headers['host'] || @headers[':authority']
9
13
  end
10
14
  alias_method :authority, :host
11
15
 
16
+ # Returns the connection header value.
17
+ #
18
+ # @return [String, nil]
12
19
  def connection
13
20
  @headers['connection']
14
21
  end
15
22
 
23
+ # Returns the upgrade protocol.
24
+ #
25
+ # @return [String, nil]
16
26
  def upgrade_protocol
17
27
  connection == 'upgrade' && @headers['upgrade']&.downcase
18
28
  end
19
29
 
30
+ # Returns the websocket version.
31
+ #
32
+ # @return [String, nil]
20
33
  def websocket_version
21
34
  headers['sec-websocket-version'].to_i
22
35
  end
23
36
 
37
+ # Returns the protocol.
38
+ #
39
+ # @return [String, nil]
24
40
  def protocol
25
41
  @protocol ||= @adapter.protocol
26
42
  end
27
43
 
44
+ # Returns the HTTP method in lower case.
45
+ #
46
+ # @return [String]
28
47
  def method
29
48
  @method ||= @headers[':method'].downcase
30
49
  end
31
50
 
51
+ # Returns the request scheme.
52
+ #
53
+ # @return [String, nil]
32
54
  def scheme
33
55
  @scheme ||= @headers[':scheme']
34
56
  end
35
57
 
58
+ # Returns the request content type.
59
+ #
60
+ # @return [String, nil]
36
61
  def content_type
37
62
  ct = @headers['content-type']
38
63
  return nil if !ct
@@ -59,22 +84,37 @@ module Syntropy
59
84
  self
60
85
  end
61
86
 
87
+ # Returns the parsed request URI.
88
+ #
89
+ # @return [URI::Generic]
62
90
  def uri
63
91
  @uri ||= URI.parse(@headers[':path'] || '')
64
92
  end
65
93
 
94
+ # Returns the parsed full request URI.
95
+ #
96
+ # @return [URI::HTTP]
66
97
  def full_uri
67
98
  @full_uri = "#{scheme}://#{host}#{uri}"
68
99
  end
69
100
 
101
+ # Returns the request path.
102
+ #
103
+ # @return [String]
70
104
  def path
71
105
  @path ||= uri.path
72
106
  end
73
107
 
108
+ # Returns the request (unparsed) query string.
109
+ #
110
+ # @return [String, nil]
74
111
  def query_string
75
112
  @query_string ||= uri.query
76
113
  end
77
114
 
115
+ # Returns the parsed query hash.
116
+ #
117
+ # @return [Hash]
78
118
  def query
79
119
  return @query if @query
80
120
 
@@ -83,6 +123,10 @@ module Syntropy
83
123
 
84
124
  QUERY_KV_REGEXP = /([^=]+)(?:=(.*))?/
85
125
 
126
+ # Converts a query string into a query hash
127
+ #
128
+ # @param query [String]
129
+ # @return [Hash]
86
130
  def parse_query(query)
87
131
  query.split('&').each_with_object({}) do |kv, h|
88
132
  k, v = kv.match(QUERY_KV_REGEXP)[1..2]
@@ -90,10 +134,16 @@ module Syntropy
90
134
  end
91
135
  end
92
136
 
137
+ # Returns the request ID.
138
+ #
139
+ # @return [String, nil]
93
140
  def request_id
94
141
  @headers['x-request-id']
95
142
  end
96
143
 
144
+ # Returns the forwarded for value.
145
+ #
146
+ # @return [String, nil]
97
147
  def forwarded_for
98
148
  @headers['x-forwarded-for']
99
149
  end
@@ -107,6 +157,9 @@ module Syntropy
107
157
  encoding.split(',').map { |i| i.strip }
108
158
  end
109
159
 
160
+ # Returns the parsed cookie values.
161
+ #
162
+ # @return [String, nil]
110
163
  def cookies
111
164
  @cookies ||= parse_cookies(headers['cookie'])
112
165
  end
@@ -114,6 +167,10 @@ module Syntropy
114
167
  COOKIE_RE = /^([^=]+)=(.*)$/.freeze
115
168
  SEMICOLON = ';'
116
169
 
170
+ # Parses the cookie string.
171
+ #
172
+ # @param cookies [String]
173
+ # @return [Hash]
117
174
  def parse_cookies(cookies)
118
175
  return {} unless cookies
119
176
 
@@ -139,6 +196,9 @@ module Syntropy
139
196
  raise Syntropy::Error.new('Invalid form data', HTTP::BAD_REQUEST)
140
197
  end
141
198
 
199
+ # Returns true if the user-agent is a browser.
200
+ #
201
+ # @return [bool]
142
202
  def browser?
143
203
  user_agent = headers['user-agent']
144
204
  user_agent && user_agent =~ /^Mozilla\//
@@ -156,6 +216,9 @@ module Syntropy
156
216
  @accept_parts.include?(mime_type)
157
217
  end
158
218
 
219
+ # Returns the bearer token.
220
+ #
221
+ # @return [String, nil]
159
222
  def auth_bearer_token
160
223
  auth = headers['authorization']
161
224
  if auth && (m = auth.match(/Bearer\s+([^\w]+)/))
@@ -167,12 +230,22 @@ module Syntropy
167
230
 
168
231
  private
169
232
 
233
+ # Parses an accept string into an array of accepted MIME types.
234
+ #
235
+ # @param accept [string]
236
+ # @return [Array<String>]
170
237
  def parse_accept_parts(accept)
171
238
  accept.split(',').map { it.match(/^\s*([^\s;]+)/)[1] }
172
239
  end
173
240
  end
174
241
 
242
+ # Request info class methods
175
243
  module RequestInfoClassMethods
244
+ # Parses form data into a hash
245
+ #
246
+ # @param body [String]
247
+ # @param headers [Hash]
248
+ # @return [Hash]
176
249
  def parse_form_data(body, headers)
177
250
  case (content_type = headers['content-type'])
178
251
  when /^multipart\/form\-data; boundary=([^\s]+)/
@@ -185,6 +258,11 @@ module Syntropy
185
258
  end
186
259
  end
187
260
 
261
+ # Parses a multipart form body.
262
+ #
263
+ # @param body [String]
264
+ # @param boundary [String]
265
+ # @return [Hash]
188
266
  def parse_multipart_form_data(body, boundary)
189
267
  parts = body.split(boundary)
190
268
  raise BadRequestError, 'Invalid form data' if parts.size < 2
@@ -197,6 +275,11 @@ module Syntropy
197
275
  end
198
276
  end
199
277
 
278
+ # Parses a multipart form data part.
279
+ #
280
+ # @param body [String]
281
+ # @param hash [Hash] output hash
282
+ # @return [void]
200
283
  def parse_multipart_form_data_part(part, hash)
201
284
  body, headers = parse_multipart_form_data_part_headers(part)
202
285
  disposition = headers['content-disposition'] || ''
@@ -211,6 +294,10 @@ module Syntropy
211
294
  end
212
295
  end
213
296
 
297
+ # Parses a multipart form data part headers.
298
+ #
299
+ # @param part [String]
300
+ # @return [Hash]
214
301
  def parse_multipart_form_data_part_headers(part)
215
302
  headers = {}
216
303
  while true
@@ -234,6 +321,10 @@ module Syntropy
234
321
  MAX_PARAMETER_NAME_SIZE = 256
235
322
  MAX_PARAMETER_VALUE_SIZE = 2**20 # 1MB
236
323
 
324
+ # Parses a URL-encoded form.
325
+ #
326
+ # @param body [String]
327
+ # @return [Hash]
237
328
  def parse_urlencoded_form_data(body)
238
329
  return {} unless body
239
330
 
@@ -7,18 +7,7 @@ require_relative '../http/status'
7
7
  require_relative '../mime_types'
8
8
 
9
9
  module Syntropy
10
- module StaticFileCaching
11
- class << self
12
- def file_stat_to_etag(stat)
13
- "#{stat.mtime.to_i.to_s(36)}#{stat.size.to_s(36)}"
14
- end
15
-
16
- def file_stat_to_last_modified(stat)
17
- stat.mtime.httpdate
18
- end
19
- end
20
- end
21
-
10
+ # Response methods.
22
11
  module ResponseMethods
23
12
  WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
24
13
 
@@ -105,8 +94,8 @@ module Syntropy
105
94
  adapter.set_response_headers(headers)
106
95
  end
107
96
 
108
- def set_cookie(*)
109
- adapter.set_cookie(*)
97
+ def set_cookie(k, v)
98
+ adapter.set_cookie(k, v)
110
99
  end
111
100
 
112
101
  def upgrade(protocol, custom_headers = nil, &block)
@@ -3,6 +3,7 @@
3
3
  require 'uri'
4
4
 
5
5
  module Syntropy
6
+ # Request validation methods.
6
7
  module RequestValidationMethods
7
8
 
8
9
  # Checks the request's HTTP method against the given accepted values. If not
@@ -3,19 +3,26 @@
3
3
  require_relative './request/request_info'
4
4
  require_relative './request/validation'
5
5
  require_relative './request/response'
6
+ require_relative './session'
6
7
  require_relative './http/status'
7
8
 
8
9
  module Syntropy
10
+ # Syntropy::Request represents an HTTP request. By interacting with the
11
+ # request, the app can extract request information and respond to the request.
9
12
  class Request
10
13
  include RequestInfoMethods
11
14
  include RequestValidationMethods
12
15
  include ResponseMethods
13
-
14
16
  extend RequestInfoClassMethods
15
17
 
16
18
  attr_reader :headers, :adapter, :start_stamp, :route_params
17
19
  attr_accessor :route
18
20
 
21
+ # Initializes the request.
22
+ #
23
+ # @param headers [Hash] request headers
24
+ # @param adapter [Object] connection adapter
25
+ # @return [void]
19
26
  def initialize(headers, adapter)
20
27
  @headers = headers
21
28
  @adapter = adapter
@@ -25,37 +32,62 @@ module Syntropy
25
32
  @ctx = nil
26
33
  end
27
34
 
28
- # Returns the request context
35
+ # Returns the request context, used to store auxiliary information.
36
+ #
37
+ # @return [Hash] request context hash
29
38
  def ctx
30
39
  @ctx ||= {}
31
40
  end
32
41
 
42
+ # Returns the next request body chunk.
43
+ #
44
+ # @return [String, nil]
33
45
  def next_chunk
34
46
  @adapter.get_body_chunk(self)
35
47
  end
36
48
 
49
+ # Reads request body chunks until the entire body is consumed, yielding each
50
+ # chunk to the given block.
51
+ #
52
+ # @return [void]
37
53
  def each_chunk
38
54
  while (chunk = @adapter.get_body_chunk(self))
39
55
  yield chunk
40
56
  end
41
57
  end
42
58
 
59
+ # Reads the request body.
60
+ #
61
+ # @return [String, nil] request body
43
62
  def read
44
63
  @adapter.get_body(self)
45
64
  end
46
65
  alias_method :body, :read
47
66
 
67
+ # Returns true if the request body has been consumed.
68
+ #
69
+ # @return [bool]
48
70
  def complete?
49
71
  @adapter.complete?(self)
50
72
  end
51
73
 
52
74
  EMPTY_HEADERS = {}.freeze
53
75
 
76
+ # Sends a response.
77
+ #
78
+ # @param body [String, nil] response body
79
+ # @param headers [Hash] response headers
80
+ # @return [void]
54
81
  def respond(body, headers = EMPTY_HEADERS)
55
82
  @adapter.respond(self, body, headers)
56
83
  @headers_sent = true
57
84
  end
58
85
 
86
+ # Sends response headers.
87
+ #
88
+ # @param headers [Hash] response headers
89
+ # @param empty_response [bool] body should be sent
90
+ # @return [void]
59
91
  def send_headers(headers = EMPTY_HEADERS, empty_response = false)
60
92
  return if @headers_sent
61
93
 
@@ -63,6 +95,11 @@ module Syntropy
63
95
  @adapter.send_headers(self, headers, empty_response: empty_response)
64
96
  end
65
97
 
98
+ # Sends a response body chunk.
99
+ #
100
+ # @param body [String] response body chunk
101
+ # @param done [bool] body is complete
102
+ # @return [void]
66
103
  def send_chunk(body, done: false)
67
104
  send_headers({}) unless @headers_sent
68
105
 
@@ -70,30 +107,34 @@ module Syntropy
70
107
  end
71
108
  alias_method :<<, :send_chunk
72
109
 
110
+ # Finish response.
111
+ #
112
+ # @return [void]
73
113
  def finish
74
114
  send_headers({}) unless @headers_sent
75
115
 
76
116
  @adapter.finish(self)
77
117
  end
78
118
 
119
+ # Returns true if response headers were sent.
120
+ #
121
+ # @return [bool]
79
122
  def headers_sent?
80
123
  @headers_sent
81
124
  end
82
125
 
83
- def rx_incr(count)
84
- headers[':rx'] ? headers[':rx'] += count : headers[':rx'] = count
85
- end
86
-
87
- def tx_incr(count)
88
- headers[':tx'] ? headers[':tx'] += count : headers[':tx'] = count
89
- end
90
-
91
- def transfer_counts
92
- [headers[':rx'], headers[':tx']]
126
+ # Returns the request session.
127
+ #
128
+ # @return [Syntropy::Session]
129
+ def session
130
+ @session ||= Session.new(self)
93
131
  end
94
132
 
95
- def total_transfer
96
- (headers[':rx'] || 0) + (headers[':tx'] || 0)
133
+ # Returns the request flash session storage.
134
+ #
135
+ # @return [Syntropy::Session::Flash]
136
+ def flash
137
+ session.flash
97
138
  end
98
139
  end
99
140
  end
@@ -5,7 +5,7 @@ module Syntropy
5
5
  # static files, markdown files, ruby modules, parametric routes, subtree routes,
6
6
  # nested middleware and error handlers.
7
7
  #
8
- # A RoutingTree instance takes the given directory (root_dir) and constructs a
8
+ # A RoutingTree instance takes the given directory (app_root) and constructs a
9
9
  # tree of route entries corresponding to the directory's contents. Finally, it
10
10
  # generates an optimized router proc, which is used by the application to return
11
11
  # a route entry for each incoming HTTP request.
@@ -41,15 +41,15 @@ module Syntropy
41
41
  # allows you to prevent access through the HTTP server to protected or
42
42
  # internal modules or files.
43
43
  class RoutingTree
44
- attr_reader :root_dir, :mount_path, :static_map, :dynamic_map, :root
44
+ attr_reader :app_root, :mount_path, :static_map, :dynamic_map, :root
45
45
 
46
46
  # Initializes a new RoutingTree instance and computes the routing tree
47
47
  #
48
- # @param root_dir [String] root directory of file tree
48
+ # @param app_root [String] root directory of file tree
49
49
  # @param mount_path [String] base URL path
50
50
  # @return [void]
51
- def initialize(root_dir:, mount_path:, **env)
52
- @root_dir = root_dir
51
+ def initialize(app_root:, mount_path:, **env)
52
+ @app_root = app_root
53
53
  @mount_path = mount_path
54
54
  @static_map = {}
55
55
  @dynamic_map = {}
@@ -72,7 +72,7 @@ module Syntropy
72
72
  # @param fn [String] file path
73
73
  # @return [String] clean path
74
74
  def compute_clean_url_path(fn)
75
- rel_path = fn.sub(@root_dir, '')
75
+ rel_path = fn.sub(@app_root, '')
76
76
  case rel_path
77
77
  when /^(.*)\/index\.(md|rb|html)$/
78
78
  Regexp.last_match(1).then { it == '' ? '/' : it }
@@ -88,7 +88,7 @@ module Syntropy
88
88
  # @param fn [String] filename
89
89
  # @return [String] relative path
90
90
  def fn_to_rel_path(fn)
91
- fn.sub(/^#{Regexp.escape(@root_dir)}\//, '').sub(/\.[^\.]+$/, '')
91
+ fn.sub(/^#{Regexp.escape(@app_root)}\//, '').sub(/\.[^\.]+$/, '')
92
92
  end
93
93
 
94
94
  # Mounts the given applet on the routng tree at the given (absolute) mount
@@ -142,7 +142,7 @@ module Syntropy
142
142
  #
143
143
  # @return [Hash] root entry
144
144
  def compute_tree
145
- compute_route_directory(dir: @root_dir, rel_path: '/', parent: nil)
145
+ compute_route_directory(dir: @app_root, rel_path: '/', parent: nil)
146
146
  end
147
147
 
148
148
  # Converts the given absolute path to a relative one (relative to the
@@ -233,7 +233,7 @@ module Syntropy
233
233
  # @return [String, nil] file path if found
234
234
  def find_aux_module_entry(dir, name)
235
235
  fn = File.join(dir, name)
236
- File.file?(fn) ? ({ kind: :module, fn: }) : nil
236
+ File.file?(fn) ? { kind: :module, fn: } : nil
237
237
  end
238
238
 
239
239
  # Returns a hash mapping file/dir names to route entries.
@@ -247,10 +247,10 @@ module Syntropy
247
247
 
248
248
  rel_path = compute_clean_url_path(fn)
249
249
  child = if File.file?(fn)
250
- compute_route_file(fn:, rel_path:, parent:)
251
- elsif File.directory?(fn)
252
- compute_route_directory(dir: fn, rel_path:, parent:)
253
- end
250
+ compute_route_file(fn:, rel_path:, parent:)
251
+ elsif File.directory?(fn)
252
+ compute_route_directory(dir: fn, rel_path:, parent:)
253
+ end
254
254
  map[child_key(child)] = child if child
255
255
  }
256
256
  end
@@ -317,7 +317,6 @@ module Syntropy
317
317
  set_index_route_target(parent:, path:, kind:, fn:, handle_subtree:)
318
318
  end
319
319
 
320
-
321
320
  # Sets an index route target for the given parent entry. Index files are
322
321
  # applied as targets to the immediate containing directory. HTML index files
323
322
  # are considered static and therefore not added to the routing tree.
@@ -329,7 +328,7 @@ module Syntropy
329
328
  # @param handle_subtree [bool] whether the target handles the subtree
330
329
  # @return [nil] (prevents addition of an index route)
331
330
  def set_index_route_target(parent:, path:, kind:, fn:, handle_subtree: nil)
332
- if is_parametric_route?(parent) || handle_subtree
331
+ if parametric_route?(parent) || handle_subtree
333
332
  @dynamic_map[path] = parent
334
333
  parent[:target] = { kind:, fn: }
335
334
  parent[:handle_subtree] = handle_subtree
@@ -383,7 +382,7 @@ module Syntropy
383
382
  # @param entry [Hash] route entry
384
383
  def make_route_entry(entry)
385
384
  path = entry[:path]
386
- if is_parametric_route?(entry) || entry[:handle_subtree]
385
+ if parametric_route?(entry) || entry[:handle_subtree]
387
386
  @dynamic_map[path] = entry
388
387
  else
389
388
  entry[:static] = true
@@ -394,8 +393,8 @@ module Syntropy
394
393
  # returns true if the route or any of its ancestors are parametric.
395
394
  #
396
395
  # @param entry [Hash] route entry
397
- def is_parametric_route?(entry)
398
- entry[:param] || (entry[:parent] && is_parametric_route?(entry[:parent]))
396
+ def parametric_route?(entry)
397
+ entry[:param] || (entry[:parent] && parametric_route?(entry[:parent]))
399
398
  end
400
399
 
401
400
  # Converts a relative URL path to absolute URL path.
@@ -468,7 +467,7 @@ module Syntropy
468
467
  emit_router_proc_postlude(buffer, default_route_path: wildcard_root && @root[:path])
469
468
  end
470
469
 
471
- buffer#.tap { puts '*' * 40; puts it; puts }
470
+ buffer # .tap { puts '*' * 40; puts it; puts }
472
471
  end
473
472
 
474
473
  # Emits optimized code for a childless wildcard router.
@@ -540,7 +539,7 @@ module Syntropy
540
539
  return
541
540
  end
542
541
 
543
- if is_void_route?(entry)
542
+ if void_route?(entry)
544
543
  parent = entry[:parent]
545
544
  parametric_sibling = parent && parent[:children] && parent[:children]['[]']
546
545
  if parametric_sibling
@@ -580,7 +579,7 @@ module Syntropy
580
579
  # @param entry [Hash] route entry
581
580
  # @return [Hash, nil] route target if exists
582
581
  def find_target_in_subtree(entry)
583
- entry[:children]&.values&.each { |e|
582
+ entry[:children]&.each_value { |e|
584
583
  target = e[:target] || find_target_in_subtree(e)
585
584
  return target if target
586
585
  }
@@ -593,14 +592,14 @@ module Syntropy
593
592
  #
594
593
  # @param entry [Hash] route entry
595
594
  # @return [bool]
596
- def is_void_route?(entry)
595
+ def void_route?(entry)
597
596
  return false if entry[:param] || entry[:target]
597
+ return true if entry[:static]
598
598
 
599
- if entry[:children]
600
- return true if !entry[:children]['[]'] && entry[:children]&.values&.all? { is_void_route?(it) }
601
- else
602
- return true if entry[:static]
603
- end
599
+ children = entry[:children]
600
+ return false if !children
601
+
602
+ return true if !children['[]'] && children.values.all? { void_route?(it) }
604
603
 
605
604
  false
606
605
  end
@@ -640,7 +639,7 @@ module Syntropy
640
639
 
641
640
  elsif has_children
642
641
  # otherwise look at the next segment
643
- next if is_void_route?(child_entry) && !param_entry
642
+ next if void_route?(child_entry) && !param_entry
644
643
 
645
644
  when_buffer = +''
646
645
  visit_routing_tree_entry(buffer: when_buffer, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)