syntropy 0.37.0 → 0.38.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -0
  3. data/Gemfile +4 -0
  4. data/TODO.md +4 -0
  5. data/bin/syntropy +12 -2
  6. data/cmd/help.rb +4 -0
  7. data/cmd/version.rb +14 -0
  8. data/examples/blog/app/posts/[id]/edit.rb +1 -1
  9. data/examples/blog/app/posts/[id]/index.rb +1 -1
  10. data/examples/blog/app/posts/index.rb +1 -1
  11. data/examples/blog/app/posts/new.rb +1 -1
  12. data/examples/github/app/[org]/[repo]/index.rb +0 -0
  13. data/examples/github/app/[org]/[repo]/issues/[id].rb +0 -0
  14. data/examples/github/app/[org]/index.rb +0 -0
  15. data/examples/github/app/collections.rb +0 -0
  16. data/examples/github/app/explore.rb +0 -0
  17. data/examples/github/app/index.rb +0 -0
  18. data/lib/syntropy/app.rb +6 -2
  19. data/lib/syntropy/controller_extensions.rb +136 -0
  20. data/lib/syntropy/module_loader.rb +46 -40
  21. data/lib/syntropy/routing_tree.rb +14 -14
  22. data/lib/syntropy/test.rb +28 -10
  23. data/lib/syntropy/version.rb +1 -1
  24. data/lib/syntropy.rb +0 -3
  25. data/test/bm_router_proc.rb +14 -15
  26. data/test/fixtures/app/bad_mod_arity.rb +3 -0
  27. data/test/fixtures/app/by_method.rb +1 -1
  28. data/test/fixtures/app/post_ct.rb +1 -1
  29. data/test/fixtures/app_errors/_error.rb +3 -0
  30. data/test/fixtures/app_errors/foo/_error.rb +3 -0
  31. data/test/fixtures/app_errors/foo/bar/_error.rb +3 -0
  32. data/test/fixtures/app_errors/foo/bar/baz/index.rb +3 -0
  33. data/test/fixtures/app_errors/foo/bar/index.rb +3 -0
  34. data/test/fixtures/app_errors/foo/index.rb +3 -0
  35. data/test/fixtures/app_errors/index.rb +3 -0
  36. data/test/fixtures/app_hooks/_hook.rb +4 -0
  37. data/test/fixtures/app_hooks/foo/_hook.rb +4 -0
  38. data/test/fixtures/app_hooks/foo/bar/_hook.rb +4 -0
  39. data/test/fixtures/app_hooks/foo/bar/baz/_hook.rb +4 -0
  40. data/test/fixtures/app_hooks/foo/bar/baz/index.rb +3 -0
  41. data/test/fixtures/app_hooks/foo/bar/index.rb +3 -0
  42. data/test/fixtures/app_hooks/foo/index.rb +3 -0
  43. data/test/fixtures/app_hooks/index.rb +3 -0
  44. data/test/fixtures/app_multi_site/_site.rb +1 -1
  45. data/test/fixtures/controllers/by_host/bar.com/index.rb +3 -0
  46. data/test/fixtures/controllers/by_host/foo.com/index.rb +3 -0
  47. data/test/fixtures/controllers/by_host_dir.rb +1 -0
  48. data/test/fixtures/controllers/by_host_dir_map.rb +4 -0
  49. data/test/fixtures/controllers/by_host_map.rb +4 -0
  50. data/test/fixtures/controllers/by_http_method.rb +9 -0
  51. data/test/fixtures/controllers/jsonrpc_endpoint.rb +0 -0
  52. data/test/test_app.rb +86 -1
  53. data/test/test_controller.rb +71 -0
  54. data/test/test_module_loader.rb +42 -3
  55. data/test/test_routing_tree.rb +1 -0
  56. data/test/test_test.rb +1 -1
  57. metadata +33 -2
  58. data/lib/syntropy/utils.rb +0 -87
@@ -138,8 +138,8 @@ def make_tmp_file_tree(dir, spec)
138
138
  dir
139
139
  end
140
140
 
141
- app_root = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
142
- make_tmp_file_tree(app_root, {
141
+ $app_root = "/tmp/#{__FILE__.gsub('/', '-')}-#{SecureRandom.hex}"
142
+ make_tmp_file_tree($app_root, {
143
143
  'index.rb': "export ->(req) { req.redirect('/hello') }",
144
144
  'hello': {
145
145
  'index.rb': "export ->(req) { req.respond('Hello!', 'Content-Type' => 'text/html') }",
@@ -147,13 +147,13 @@ make_tmp_file_tree(app_root, {
147
147
  }
148
148
  })
149
149
 
150
- machine = UM.new
151
- syntropy_app = Syntropy::App.new(
152
- app_root: app_root,
153
- mount_path: '/',
154
- machine: machine
155
- )
156
- proc = ->(req) { syntropy_app.(req) }
150
+ # machine = UM.new
151
+ # syntropy_app = Syntropy::App.new(
152
+ # app_root: app_root,
153
+ # mount_path: '/',
154
+ # machine: machine
155
+ # )
156
+ # proc = ->(req) { syntropy_app.(req) }
157
157
 
158
158
  module ::Kernel
159
159
  def mock_req(headers, body = nil)
@@ -161,11 +161,11 @@ module ::Kernel
161
161
  end
162
162
  end
163
163
 
164
- puts '*' * 40
164
+ # puts '*' * 40
165
165
 
166
- req = mock_req(':method' => 'GET', ':path' => '/hello/world')
167
- proc.(req)
168
- p [req.response_status, req.response_headers, req.response_body]
166
+ # req = mock_req(':method' => 'GET', ':path' => '/hello/world')
167
+ # proc.(req)
168
+ # p [req.response_status, req.response_headers, req.response_body]
169
169
 
170
170
  ################################################################################
171
171
 
@@ -185,9 +185,8 @@ BM.run do
185
185
  def setup
186
186
  machine = UM.new
187
187
  syntropy_app = Syntropy::App.new(
188
- app_root: app_root,
188
+ app_root: $app_root,
189
189
  mount_path: '/',
190
- # watch_files: 0.05,
191
190
  machine: machine
192
191
  )
193
192
  @app = ->(req) { syntropy_app.(req) }
@@ -0,0 +1,3 @@
1
+ export ->(req, blah) {
2
+ req.respond('foobar')
3
+ }
@@ -1,4 +1,4 @@
1
- export http_methods
1
+ export dispatch_by_http_method
2
2
 
3
3
  def get(req)
4
4
  req.respond('foo')
@@ -1,4 +1,4 @@
1
- export http_methods
1
+ export dispatch_by_http_method
2
2
 
3
3
  def post(req)
4
4
  req.respond("#{req.content_type}:#{req.read}")
@@ -0,0 +1,3 @@
1
+ export ->(req, error) {
2
+ req.respond("root: #{error.message}", ':status' => Error.http_status(error))
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req, error) {
2
+ req.respond("foo: #{error.message}", ':status' => Error.http_status(error))
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req, error) {
2
+ req.respond("bar: #{error.message}", ':status' => Error.http_status(error))
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('baz')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('bar')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('foo')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise Error.teapot('root')
3
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :root
3
+ app.(req)
4
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :foo
3
+ app.(req)
4
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :bar
3
+ app.(req)
4
+ }
@@ -0,0 +1,4 @@
1
+ export ->(req, app) {
2
+ (req.ctx[:hooks] ||= []) << :baz
3
+ app.(req)
4
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("baz: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("bar: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("foo: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond("root: #{req.ctx[:hooks].join(' ')}")
3
+ }
@@ -1 +1 @@
1
- export Syntropy.route_by_host(@env)
1
+ export dispatch_by_host('.')
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond('bar')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond('foo')
3
+ }
@@ -0,0 +1 @@
1
+ export dispatch_by_host('by_host')
@@ -0,0 +1,4 @@
1
+ export dispatch_by_host('by_host', {
2
+ 'foofoo' => 'by_host/foo.com',
3
+ 'barbar' => 'by_host/bar.com'
4
+ })
@@ -0,0 +1,4 @@
1
+ export dispatch_by_host(nil, {
2
+ 'foofoo' => 'by_host/foo.com',
3
+ 'barbar' => 'by_host/bar.com'
4
+ })
@@ -0,0 +1,9 @@
1
+ export dispatch_by_http_method
2
+
3
+ def get(req)
4
+ req.respond('get')
5
+ end
6
+
7
+ def post(req)
8
+ req.respond('post')
9
+ end
File without changes
data/test/test_app.rb CHANGED
@@ -119,6 +119,11 @@ class AppTest < Minitest::Test
119
119
  }
120
120
  assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
121
121
 
122
+ req = @test_harness.no_raise_internal_server_error {
123
+ @test_harness.request(':method' => 'GET', ':path' => '/test/bad_mod_arity')
124
+ }
125
+ assert_equal HTTP::INTERNAL_SERVER_ERROR, req.response_status
126
+
122
127
  req = @test_harness.request(':method' => 'GET', ':path' => '/test/.well-known/foo')
123
128
  assert_equal HTTP::OK, req.response_status
124
129
  assert_equal 'foo', req.response_body
@@ -183,6 +188,86 @@ class AppTest < Minitest::Test
183
188
  end
184
189
  end
185
190
 
191
+ class MiddlewareHooksTest < Minitest::Test
192
+ HTTP = Syntropy::HTTP
193
+
194
+ APP_ROOT = File.join(__dir__, 'fixtures/app_hooks')
195
+
196
+ def setup
197
+ @machine = UM.new
198
+
199
+ @tmp_path = '/test/tmp'
200
+ @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
201
+
202
+ @app = Syntropy::App.new(
203
+ app_root: APP_ROOT,
204
+ mount_path: '/',
205
+ watch_files: 0.05,
206
+ machine: @machine
207
+ )
208
+
209
+ @test_harness = Syntropy::TestHarness.new(@app)
210
+ end
211
+
212
+ def test_middleware_composition
213
+ req = @test_harness.request(':method' => 'GET', ':path' => '/')
214
+ assert_equal HTTP::OK, req.response_status
215
+ assert_equal 'root: root', req.response_body
216
+
217
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo')
218
+ assert_equal HTTP::OK, req.response_status
219
+ assert_equal 'foo: root foo', req.response_body
220
+
221
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar')
222
+ assert_equal HTTP::OK, req.response_status
223
+ assert_equal 'bar: root foo bar', req.response_body
224
+
225
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar/baz')
226
+ assert_equal HTTP::OK, req.response_status
227
+ assert_equal 'baz: root foo bar baz', req.response_body
228
+ end
229
+ end
230
+
231
+ class ErrorHandlerTest < Minitest::Test
232
+ HTTP = Syntropy::HTTP
233
+
234
+ APP_ROOT = File.join(__dir__, 'fixtures/app_errors')
235
+
236
+ def setup
237
+ @machine = UM.new
238
+
239
+ @tmp_path = '/test/tmp'
240
+ @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
241
+
242
+ @app = Syntropy::App.new(
243
+ app_root: APP_ROOT,
244
+ mount_path: '/',
245
+ watch_files: 0.05,
246
+ machine: @machine
247
+ )
248
+
249
+ @test_harness = Syntropy::TestHarness.new(@app)
250
+ end
251
+
252
+ def test_error_handlers
253
+ req = @test_harness.request(':method' => 'GET', ':path' => '/')
254
+ assert_equal HTTP::TEAPOT, req.response_status
255
+ assert_equal 'root: root', req.response_body
256
+
257
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo')
258
+ assert_equal HTTP::TEAPOT, req.response_status
259
+ assert_equal 'foo: foo', req.response_body
260
+
261
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar')
262
+ assert_equal HTTP::TEAPOT, req.response_status
263
+ assert_equal 'bar: bar', req.response_body
264
+
265
+ req = @test_harness.request(':method' => 'GET', ':path' => '/foo/bar/baz')
266
+ assert_equal HTTP::TEAPOT, req.response_status
267
+ assert_equal 'bar: baz', req.response_body
268
+ end
269
+ end
270
+
186
271
  class CustomAppTest < Minitest::Test
187
272
  HTTP = Syntropy::HTTP
188
273
 
@@ -220,7 +305,7 @@ class MultiSiteAppTest < Minitest::Test
220
305
  @test_harness = Syntropy::TestHarness.new(@app)
221
306
  end
222
307
 
223
- def test_route_by_host
308
+ def test_dispatch_by_host
224
309
  req = @test_harness.request(':method' => 'GET', ':path' => '/', 'host' => 'blah')
225
310
  assert_nil req.response_body
226
311
  assert_equal HTTP::BAD_REQUEST, req.response_status
@@ -0,0 +1,71 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+ require 'syntropy/test'
5
+
6
+ class DispatchByHostTest < Syntropy::Test
7
+ self.env = {
8
+ app_root: File.join(__dir__, 'fixtures/controllers'),
9
+ mount_path: '/'
10
+ }
11
+
12
+ def test_dispatch_by_host_dir
13
+ req = get('/by_host_dir', 'host' => 'sqdf')
14
+ assert_equal HTTP::BAD_REQUEST, req.response_status
15
+
16
+ req = get('/by_host_dir', 'host' => 'foo.com')
17
+ assert_equal HTTP::OK, req.response_status
18
+ assert_equal 'foo', req.response_body
19
+
20
+ req = get('/by_host_dir', 'host' => 'bar.com')
21
+ assert_equal HTTP::OK, req.response_status
22
+ assert_equal 'bar', req.response_body
23
+ end
24
+
25
+ def test_dispatch_by_host_map
26
+ req = get('/by_host_map', 'host' => 'sqdf')
27
+ assert_equal HTTP::BAD_REQUEST, req.response_status
28
+
29
+ req = get('/by_host_map', 'host' => 'foofoo')
30
+ assert_equal HTTP::OK, req.response_status
31
+ assert_equal 'foo', req.response_body
32
+
33
+ req = get('/by_host_map', 'host' => 'barbar')
34
+ assert_equal HTTP::OK, req.response_status
35
+ assert_equal 'bar', req.response_body
36
+ end
37
+
38
+ def test_dispatch_by_host_dir_map
39
+ req = get('/by_host_dir_map', 'host' => 'sqdf')
40
+ assert_equal HTTP::BAD_REQUEST, req.response_status
41
+
42
+ req = get('/by_host_dir_map', 'host' => 'foo.com')
43
+ assert_equal HTTP::OK, req.response_status
44
+ assert_equal 'foo', req.response_body
45
+
46
+ req = get('/by_host_dir_map', 'host' => 'foofoo')
47
+ assert_equal HTTP::OK, req.response_status
48
+ assert_equal 'foo', req.response_body
49
+
50
+ req = get('/by_host_dir_map', 'host' => 'bar.com')
51
+ assert_equal HTTP::OK, req.response_status
52
+ assert_equal 'bar', req.response_body
53
+
54
+ req = get('/by_host_dir_map', 'host' => 'barbar')
55
+ assert_equal HTTP::OK, req.response_status
56
+ assert_equal 'bar', req.response_body
57
+ end
58
+
59
+ def test_dispatch_by_http_method
60
+ req = get('/by_http_method')
61
+ assert_equal HTTP::OK, req.response_status
62
+ assert_equal 'get', req.response_body
63
+
64
+ req = post('/by_http_method', nil, nil)
65
+ assert_equal HTTP::OK, req.response_status
66
+ assert_equal 'post', req.response_body
67
+
68
+ req = patch('/by_http_method', nil, nil)
69
+ assert_equal HTTP::METHOD_NOT_ALLOWED, req.response_status
70
+ end
71
+ end
@@ -19,7 +19,7 @@ class ModuleTest < Minitest::Test
19
19
  assert_raises(Syntropy::Error) { @loader.load('_lib/missing-export') }
20
20
 
21
21
  mod = @loader.load('_lib/callable')
22
- assert_kind_of Syntropy::Module, mod
22
+ assert_kind_of Syntropy::ModuleContext, mod
23
23
  assert_equal 'barbarbar', mod.call(3)
24
24
  assert_raises(NoMethodError) { mod.foo(2) }
25
25
 
@@ -35,13 +35,13 @@ class ModuleTest < Minitest::Test
35
35
 
36
36
  assert_equal :foo, mod[:a1]
37
37
  assert_equal :foo, mod[:a2]
38
- assert_kind_of Syntropy::Module, mod[:foo]
38
+ assert_kind_of Syntropy::ModuleContext, mod[:foo]
39
39
  assert_equal 'barbarbar', mod[:callable].(3)
40
40
  end
41
41
 
42
42
  def test_export_self
43
43
  mod = @loader.load('_lib/self')
44
- assert_kind_of Syntropy::Module, mod
44
+ assert_kind_of Syntropy::ModuleContext, mod
45
45
  assert_equal :bar, mod.foo
46
46
  end
47
47
 
@@ -113,3 +113,42 @@ class ModuleTest < Minitest::Test
113
113
  assert_equal [], list
114
114
  end
115
115
  end
116
+
117
+
118
+ class ModuleExtensionsTest < Minitest::Test
119
+ module E1
120
+ def e1_foo = :foo
121
+ end
122
+
123
+ module E2
124
+ def e2_bar = :bar
125
+ end
126
+
127
+ module E3
128
+ def e3_baz = :baz
129
+ end
130
+
131
+ def test_module_extension_single
132
+ @machine = UM.new
133
+ @root = File.join(__dir__, 'fixtures/app')
134
+ @env = { app_root: @root, baz: 42, machine: @machine, app: 42 }
135
+ @loader = Syntropy::ModuleLoader.new(@env, extensions: E1)
136
+
137
+ mod = @loader.load('_lib/self')
138
+ assert_equal true, mod.respond_to?(:e1_foo)
139
+ assert_equal :foo, mod.e1_foo
140
+ end
141
+
142
+ def test_module_extension_multi
143
+ @machine = UM.new
144
+ @root = File.join(__dir__, 'fixtures/app')
145
+ @env = { app_root: @root, baz: 42, machine: @machine, app: 42 }
146
+ @loader = Syntropy::ModuleLoader.new(@env, extensions: [E2, E3])
147
+
148
+ mod = @loader.load('_lib/self')
149
+ assert_equal true, mod.respond_to?(:e2_bar)
150
+ assert_equal :bar, mod.e2_bar
151
+ assert_equal true, mod.respond_to?(:e3_baz)
152
+ assert_equal :baz, mod.e3_baz
153
+ end
154
+ end
@@ -105,6 +105,7 @@ class RoutingTreeTest < Minitest::Test
105
105
  issues = repo[:children]['issues']
106
106
  assert_equal repo, issues[:parent]
107
107
  assert_equal '/docs/[org]/[repo]/issues', issues[:path]
108
+ refute_nil issues[:hook]
108
109
  assert_nil issues[:param]
109
110
  assert_equal File.join(@rt.app_root, '[org]/[repo]/issues/index.rb'), issues[:target][:fn]
110
111
  assert_equal ['[]'], issues[:children].keys.sort_by(&:to_s)
data/test/test_test.rb CHANGED
@@ -34,7 +34,7 @@ class TestTest < Syntropy::Test
34
34
 
35
35
  def test_load_module
36
36
  mod = load_module('_lib/env')
37
- assert_kind_of Syntropy::Module, mod
37
+ assert_kind_of Syntropy::ModuleContext, mod
38
38
  assert_equal app, mod.app
39
39
 
40
40
  assert_raises(Syntropy::Error) { load_module('_lib/blah')}
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: syntropy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.37.0
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -189,6 +189,7 @@ files:
189
189
  - cmd/new/template/test/test_app.rb
190
190
  - cmd/serve.rb
191
191
  - cmd/test.rb
192
+ - cmd/version.rb
192
193
  - docker-compose.yml
193
194
  - examples/basic/bad.rb
194
195
  - examples/basic/card.rb
@@ -214,6 +215,12 @@ files:
214
215
  - examples/blog/config/production.rb
215
216
  - examples/blog/config/test.rb
216
217
  - examples/blog/test/test_posts.rb
218
+ - examples/github/app/[org]/[repo]/index.rb
219
+ - examples/github/app/[org]/[repo]/issues/[id].rb
220
+ - examples/github/app/[org]/index.rb
221
+ - examples/github/app/collections.rb
222
+ - examples/github/app/explore.rb
223
+ - examples/github/app/index.rb
217
224
  - examples/mcp-oauth/.ruby-version
218
225
  - examples/mcp-oauth/Gemfile
219
226
  - examples/mcp-oauth/README.md
@@ -240,6 +247,7 @@ files:
240
247
  - lib/syntropy/applets/builtin/json_api.js
241
248
  - lib/syntropy/applets/builtin/ping.rb
242
249
  - lib/syntropy/applets/builtin/req.rb
250
+ - lib/syntropy/controller_extensions.rb
243
251
  - lib/syntropy/dev_mode.rb
244
252
  - lib/syntropy/errors.rb
245
253
  - lib/syntropy/http.rb
@@ -270,7 +278,6 @@ files:
270
278
  - lib/syntropy/storage/schema.rb
271
279
  - lib/syntropy/storage/store.rb
272
280
  - lib/syntropy/test.rb
273
- - lib/syntropy/utils.rb
274
281
  - lib/syntropy/version.rb
275
282
  - syntropy.gemspec
276
283
  - test/bm_router_proc.rb
@@ -291,6 +298,7 @@ files:
291
298
  - test/fixtures/app/api+.rb
292
299
  - test/fixtures/app/assets/style.css
293
300
  - test/fixtures/app/bad_mod.rb
301
+ - test/fixtures/app/bad_mod_arity.rb
294
302
  - test/fixtures/app/bar.rb
295
303
  - test/fixtures/app/baz.rb
296
304
  - test/fixtures/app/by_method.rb
@@ -308,6 +316,21 @@ files:
308
316
  - test/fixtures/app/singleton.rb
309
317
  - test/fixtures/app/tmp.rb
310
318
  - test/fixtures/app_custom/_site.rb
319
+ - test/fixtures/app_errors/_error.rb
320
+ - test/fixtures/app_errors/foo/_error.rb
321
+ - test/fixtures/app_errors/foo/bar/_error.rb
322
+ - test/fixtures/app_errors/foo/bar/baz/index.rb
323
+ - test/fixtures/app_errors/foo/bar/index.rb
324
+ - test/fixtures/app_errors/foo/index.rb
325
+ - test/fixtures/app_errors/index.rb
326
+ - test/fixtures/app_hooks/_hook.rb
327
+ - test/fixtures/app_hooks/foo/_hook.rb
328
+ - test/fixtures/app_hooks/foo/bar/_hook.rb
329
+ - test/fixtures/app_hooks/foo/bar/baz/_hook.rb
330
+ - test/fixtures/app_hooks/foo/bar/baz/index.rb
331
+ - test/fixtures/app_hooks/foo/bar/index.rb
332
+ - test/fixtures/app_hooks/foo/index.rb
333
+ - test/fixtures/app_hooks/index.rb
311
334
  - test/fixtures/app_multi_site/_site.rb
312
335
  - test/fixtures/app_multi_site/bar.baz/index.html
313
336
  - test/fixtures/app_multi_site/foo.bar/index.html
@@ -315,6 +338,13 @@ files:
315
338
  - test/fixtures/app_setup/index.rb
316
339
  - test/fixtures/app_with_schema/_schema/2026-01-02-foo.rb
317
340
  - test/fixtures/app_with_schema/_schema/2026-05-30-bar.rb
341
+ - test/fixtures/controllers/by_host/bar.com/index.rb
342
+ - test/fixtures/controllers/by_host/foo.com/index.rb
343
+ - test/fixtures/controllers/by_host_dir.rb
344
+ - test/fixtures/controllers/by_host_dir_map.rb
345
+ - test/fixtures/controllers/by_host_map.rb
346
+ - test/fixtures/controllers/by_http_method.rb
347
+ - test/fixtures/controllers/jsonrpc_endpoint.rb
318
348
  - test/fixtures/schema/2026-01-02-foo.rb
319
349
  - test/fixtures/schema/2026-05-30-bar.rb
320
350
  - test/helper.rb
@@ -322,6 +352,7 @@ files:
322
352
  - test/test_app.rb
323
353
  - test/test_caching.rb
324
354
  - test/test_connection_pool.rb
355
+ - test/test_controller.rb
325
356
  - test/test_errors.rb
326
357
  - test/test_http_client.rb
327
358
  - test/test_http_client_connection.rb
@@ -1,87 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'securerandom'
4
-
5
- module Syntropy
6
- # Utilities for use in modules
7
- module Utilities
8
- def tmp_path(prefix = 'syntropy')
9
- "/tmp/#{prefix}-#{SecureRandom.hex(16)}"
10
- end
11
-
12
- # Returns a request handler that routes request according to the host
13
- # header. Looks for site directories (named by host name) in the app's root
14
- # directory. A map may be given in order to provide additional hostnames to
15
- # site directories.
16
- #
17
- # @param env [Hash] app environment hash
18
- # @param map [Hash, nil] additional hostname map
19
- # @return [Proc] router proc
20
- def route_by_host(env, map = nil)
21
- sites = find_hostname_sites(env)
22
-
23
- # add map refs
24
- map&.each { |k, v| sites[k] = sites[v] }
25
-
26
- lambda { |req|
27
- site = sites[req.host]
28
- site ? site.call(req) : req.respond(nil, ':status' => HTTP::BAD_REQUEST)
29
- }
30
- end
31
-
32
- # Returns a list of parsed markdown pages at the given path.
33
- #
34
- # @param env [Hash] app environment hash
35
- # @param ref [String] directory path
36
- # @return [Array<Hash>] array of page entries
37
- def page_list(env, ref)
38
- full_path = File.join(env[:app_root], ref)
39
- raise 'Not a directory' if !File.directory?(full_path)
40
-
41
- Dir[File.join(full_path, '*.md')].sort.map {
42
- atts, markdown = Syntropy::Markdown.parse(it, env)
43
- { atts:, markdown: }
44
- }
45
- end
46
-
47
- # Instantiates a Syntropy app for the given environment hash.
48
- #
49
- # @return [Syntropy::App]
50
- def app(**)
51
- Syntropy::App.new(**)
52
- end
53
-
54
- BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
55
-
56
- # Creates a builtin applet with the given environment hash. By default the
57
- # builtin applet is mounted at /.syntropy.
58
- #
59
- # @param env [Hash] app environment
60
- # @param mount_path [String] mount path for the builtin applet
61
- # @return [Syntropy::App] applet
62
- def builtin_applet(env, mount_path: '/.syntropy')
63
- app(
64
- machine: env[:machine],
65
- app_root: BUILTIN_APPLET_app_root,
66
- mount_path: mount_path,
67
- builtin_applet_path: nil,
68
- watch_files: nil
69
- )
70
- end
71
-
72
- private
73
-
74
- # Finds sites in the root directory for the given environment hash.
75
- #
76
- # @param env [Hash] app environment hash
77
- # @return [Hash] hash mapping hostname to app
78
- def find_hostname_sites(env)
79
- Dir[File.join(env[:app_root], '*')]
80
- .select { File.directory?(it) && File.basename(it) !~ /^_/ }
81
- .each_with_object({}) { |fn, h|
82
- name = File.basename(fn)
83
- h[name] = Syntropy::App.new(**env.merge(app_root: fn))
84
- }
85
- end
86
- end
87
- end