syntropy 0.36.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 (71) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +23 -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/new/template/.gitignore +2 -1
  8. data/cmd/new/template/config/Caddyfile +5 -0
  9. data/cmd/new/template/docker-compose.yml +28 -3
  10. data/cmd/new.rb +7 -1
  11. data/cmd/serve.rb +3 -1
  12. data/cmd/version.rb +14 -0
  13. data/examples/basic/counter_api.rb +1 -1
  14. data/examples/blog/app/posts/[id]/edit.rb +1 -1
  15. data/examples/blog/app/posts/[id]/index.rb +1 -1
  16. data/examples/blog/app/posts/index.rb +1 -1
  17. data/examples/blog/app/posts/new.rb +1 -1
  18. data/examples/github/app/[org]/[repo]/index.rb +0 -0
  19. data/examples/github/app/[org]/[repo]/issues/[id].rb +0 -0
  20. data/examples/github/app/[org]/index.rb +0 -0
  21. data/examples/github/app/collections.rb +0 -0
  22. data/examples/github/app/explore.rb +0 -0
  23. data/examples/github/app/index.rb +0 -0
  24. data/lib/syntropy/app.rb +6 -2
  25. data/lib/syntropy/controller_extensions.rb +136 -0
  26. data/lib/syntropy/http/io_extensions.rb +9 -0
  27. data/lib/syntropy/http/server_connection.rb +1 -0
  28. data/lib/syntropy/json_api.rb +5 -0
  29. data/lib/syntropy/module_loader.rb +46 -42
  30. data/lib/syntropy/routing_tree.rb +14 -14
  31. data/lib/syntropy/storage/schema.rb +3 -3
  32. data/lib/syntropy/test.rb +29 -11
  33. data/lib/syntropy/version.rb +1 -1
  34. data/lib/syntropy.rb +3 -6
  35. data/test/bm_router_proc.rb +14 -15
  36. data/test/fixtures/app/_lib/klass.rb +1 -1
  37. data/test/fixtures/app/api+.rb +1 -1
  38. data/test/fixtures/app/bad_mod_arity.rb +3 -0
  39. data/test/fixtures/app/by_method.rb +1 -1
  40. data/test/fixtures/app/post_ct.rb +1 -1
  41. data/test/fixtures/app_errors/_error.rb +3 -0
  42. data/test/fixtures/app_errors/foo/_error.rb +3 -0
  43. data/test/fixtures/app_errors/foo/bar/_error.rb +3 -0
  44. data/test/fixtures/app_errors/foo/bar/baz/index.rb +3 -0
  45. data/test/fixtures/app_errors/foo/bar/index.rb +3 -0
  46. data/test/fixtures/app_errors/foo/index.rb +3 -0
  47. data/test/fixtures/app_errors/index.rb +3 -0
  48. data/test/fixtures/app_hooks/_hook.rb +4 -0
  49. data/test/fixtures/app_hooks/foo/_hook.rb +4 -0
  50. data/test/fixtures/app_hooks/foo/bar/_hook.rb +4 -0
  51. data/test/fixtures/app_hooks/foo/bar/baz/_hook.rb +4 -0
  52. data/test/fixtures/app_hooks/foo/bar/baz/index.rb +3 -0
  53. data/test/fixtures/app_hooks/foo/bar/index.rb +3 -0
  54. data/test/fixtures/app_hooks/foo/index.rb +3 -0
  55. data/test/fixtures/app_hooks/index.rb +3 -0
  56. data/test/fixtures/app_multi_site/_site.rb +1 -1
  57. data/test/fixtures/controllers/by_host/bar.com/index.rb +3 -0
  58. data/test/fixtures/controllers/by_host/foo.com/index.rb +3 -0
  59. data/test/fixtures/controllers/by_host_dir.rb +1 -0
  60. data/test/fixtures/controllers/by_host_dir_map.rb +4 -0
  61. data/test/fixtures/controllers/by_host_map.rb +4 -0
  62. data/test/fixtures/controllers/by_http_method.rb +9 -0
  63. data/test/fixtures/controllers/jsonrpc_endpoint.rb +0 -0
  64. data/test/test_app.rb +86 -1
  65. data/test/test_controller.rb +71 -0
  66. data/test/test_http_protocol.rb +54 -0
  67. data/test/test_module_loader.rb +43 -5
  68. data/test/test_routing_tree.rb +1 -0
  69. data/test/test_test.rb +1 -1
  70. metadata +34 -2
  71. data/lib/syntropy/utils.rb +0 -87
@@ -19,14 +19,13 @@ 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
 
26
26
  mod = @loader.load('_lib/klass')
27
27
  assert_equal :bar, mod.foo
28
- @env[:baz] += 1
29
- assert_equal 43, mod.bar
28
+ assert_equal 42, mod.bar
30
29
  end
31
30
 
32
31
  def test_import_paths
@@ -36,13 +35,13 @@ class ModuleTest < Minitest::Test
36
35
 
37
36
  assert_equal :foo, mod[:a1]
38
37
  assert_equal :foo, mod[:a2]
39
- assert_kind_of Syntropy::Module, mod[:foo]
38
+ assert_kind_of Syntropy::ModuleContext, mod[:foo]
40
39
  assert_equal 'barbarbar', mod[:callable].(3)
41
40
  end
42
41
 
43
42
  def test_export_self
44
43
  mod = @loader.load('_lib/self')
45
- assert_kind_of Syntropy::Module, mod
44
+ assert_kind_of Syntropy::ModuleContext, mod
46
45
  assert_equal :bar, mod.foo
47
46
  end
48
47
 
@@ -114,3 +113,42 @@ class ModuleTest < Minitest::Test
114
113
  assert_equal [], list
115
114
  end
116
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.36.0
4
+ version: 0.38.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -181,6 +181,7 @@ files:
181
181
  - cmd/new/template/app/assets/style.css
182
182
  - cmd/new/template/app/index.rb
183
183
  - cmd/new/template/app/test.rb
184
+ - cmd/new/template/config/Caddyfile
184
185
  - cmd/new/template/config/development.rb
185
186
  - cmd/new/template/config/production.rb
186
187
  - cmd/new/template/config/test.rb
@@ -188,6 +189,7 @@ files:
188
189
  - cmd/new/template/test/test_app.rb
189
190
  - cmd/serve.rb
190
191
  - cmd/test.rb
192
+ - cmd/version.rb
191
193
  - docker-compose.yml
192
194
  - examples/basic/bad.rb
193
195
  - examples/basic/card.rb
@@ -213,6 +215,12 @@ files:
213
215
  - examples/blog/config/production.rb
214
216
  - examples/blog/config/test.rb
215
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
216
224
  - examples/mcp-oauth/.ruby-version
217
225
  - examples/mcp-oauth/Gemfile
218
226
  - examples/mcp-oauth/README.md
@@ -239,6 +247,7 @@ files:
239
247
  - lib/syntropy/applets/builtin/json_api.js
240
248
  - lib/syntropy/applets/builtin/ping.rb
241
249
  - lib/syntropy/applets/builtin/req.rb
250
+ - lib/syntropy/controller_extensions.rb
242
251
  - lib/syntropy/dev_mode.rb
243
252
  - lib/syntropy/errors.rb
244
253
  - lib/syntropy/http.rb
@@ -269,7 +278,6 @@ files:
269
278
  - lib/syntropy/storage/schema.rb
270
279
  - lib/syntropy/storage/store.rb
271
280
  - lib/syntropy/test.rb
272
- - lib/syntropy/utils.rb
273
281
  - lib/syntropy/version.rb
274
282
  - syntropy.gemspec
275
283
  - test/bm_router_proc.rb
@@ -290,6 +298,7 @@ files:
290
298
  - test/fixtures/app/api+.rb
291
299
  - test/fixtures/app/assets/style.css
292
300
  - test/fixtures/app/bad_mod.rb
301
+ - test/fixtures/app/bad_mod_arity.rb
293
302
  - test/fixtures/app/bar.rb
294
303
  - test/fixtures/app/baz.rb
295
304
  - test/fixtures/app/by_method.rb
@@ -307,6 +316,21 @@ files:
307
316
  - test/fixtures/app/singleton.rb
308
317
  - test/fixtures/app/tmp.rb
309
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
310
334
  - test/fixtures/app_multi_site/_site.rb
311
335
  - test/fixtures/app_multi_site/bar.baz/index.html
312
336
  - test/fixtures/app_multi_site/foo.bar/index.html
@@ -314,6 +338,13 @@ files:
314
338
  - test/fixtures/app_setup/index.rb
315
339
  - test/fixtures/app_with_schema/_schema/2026-01-02-foo.rb
316
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
317
348
  - test/fixtures/schema/2026-01-02-foo.rb
318
349
  - test/fixtures/schema/2026-05-30-bar.rb
319
350
  - test/helper.rb
@@ -321,6 +352,7 @@ files:
321
352
  - test/test_app.rb
322
353
  - test/test_caching.rb
323
354
  - test/test_connection_pool.rb
355
+ - test/test_controller.rb
324
356
  - test/test_errors.rb
325
357
  - test/test_http_client.rb
326
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