syntropy 0.30.0 → 0.32.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 (96) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +30 -0
  4. data/TODO.md +46 -1
  5. data/bin/syntropy +8 -86
  6. data/cmd/_banner.rb +16 -0
  7. data/cmd/console.rb +77 -0
  8. data/cmd/help.rb +12 -0
  9. data/cmd/serve.rb +95 -0
  10. data/cmd/test.rb +40 -0
  11. data/examples/{counter.rb → basic/counter.rb} +1 -1
  12. data/examples/{templates.rb → basic/templates.rb} +1 -1
  13. data/examples/blog/app/_layout/default.rb +11 -0
  14. data/examples/blog/app/_lib/post_store.rb +47 -0
  15. data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
  16. data/examples/blog/app/_setup.rb +4 -0
  17. data/examples/blog/app/index.rb +7 -0
  18. data/examples/blog/app/posts/[id]/edit.rb +33 -0
  19. data/examples/blog/app/posts/[id]/index.rb +58 -0
  20. data/examples/blog/app/posts/index.rb +38 -0
  21. data/examples/blog/app/posts/new.rb +29 -0
  22. data/examples/mcp-oauth/.ruby-version +1 -0
  23. data/examples/mcp-oauth/Gemfile +8 -0
  24. data/examples/mcp-oauth/README.md +128 -0
  25. data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
  26. data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
  27. data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
  28. data/examples/mcp-oauth/app/index.md +1 -0
  29. data/examples/mcp-oauth/app/mcp.rb +85 -0
  30. data/examples/mcp-oauth/app/oauth/authorize.rb +18 -0
  31. data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
  32. data/examples/mcp-oauth/app/oauth/register.rb +14 -0
  33. data/examples/mcp-oauth/app/oauth/token.rb +79 -0
  34. data/examples/mcp-oauth/app/signin.rb +85 -0
  35. data/examples/mcp-oauth/test/helper.rb +9 -0
  36. data/examples/mcp-oauth/test/test_app.rb +27 -0
  37. data/examples/mcp-oauth/test/test_oauth.rb +628 -0
  38. data/lib/syntropy/app.rb +34 -9
  39. data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
  40. data/lib/syntropy/applets/builtin/req.rb +1 -1
  41. data/lib/syntropy/db/connection_pool.rb +71 -0
  42. data/lib/syntropy/db/schema.rb +92 -0
  43. data/lib/syntropy/db/store.rb +31 -0
  44. data/lib/syntropy/dev_mode.rb +1 -1
  45. data/lib/syntropy/errors.rb +6 -0
  46. data/lib/syntropy/http/client.rb +43 -0
  47. data/lib/syntropy/http/client_connection.rb +36 -0
  48. data/lib/syntropy/http/io_extensions.rb +176 -0
  49. data/lib/syntropy/http/server.rb +5 -5
  50. data/lib/syntropy/http/{connection.rb → server_connection.rb} +15 -91
  51. data/lib/syntropy/http.rb +3 -1
  52. data/lib/syntropy/logger.rb +5 -1
  53. data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
  54. data/lib/syntropy/papercraft_extensions.rb +1 -1
  55. data/lib/syntropy/request/mock_adapter.rb +2 -0
  56. data/lib/syntropy/request/request_info.rb +22 -4
  57. data/lib/syntropy/request/response.rb +2 -2
  58. data/lib/syntropy/request/validation.rb +11 -5
  59. data/lib/syntropy/routing_tree.rb +2 -1
  60. data/lib/syntropy/test.rb +77 -0
  61. data/lib/syntropy/version.rb +1 -1
  62. data/lib/syntropy.rb +5 -23
  63. data/syntropy.gemspec +3 -3
  64. data/test/app/.well-known/foo.rb +3 -0
  65. data/test/app/_hook.rb +1 -1
  66. data/test/app/by_method.rb +9 -0
  67. data/test/app_setup/_setup.rb +7 -0
  68. data/test/app_setup/index.rb +1 -0
  69. data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
  70. data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
  71. data/test/helper.rb +1 -25
  72. data/test/schema/2026-01-02-foo.rb +12 -0
  73. data/test/schema/2026-05-30-bar.rb +7 -0
  74. data/test/test_app.rb +110 -70
  75. data/test/test_caching.rb +1 -1
  76. data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
  77. data/test/test_db_schema.rb +96 -0
  78. data/test/test_db_store.rb +24 -0
  79. data/test/test_http_client.rb +52 -0
  80. data/test/test_http_client_connection.rb +43 -0
  81. data/test/test_http_protocol.rb +250 -0
  82. data/test/{test_connection.rb → test_http_server_connection.rb} +39 -48
  83. data/test/test_json_api.rb +5 -5
  84. data/test/{test_module.rb → test_module_loader.rb} +31 -0
  85. data/test/{test_request_extensions.rb → test_request.rb} +153 -18
  86. data/test/test_routing_tree.rb +15 -3
  87. data/test/test_server.rb +9 -13
  88. metadata +84 -36
  89. data/lib/syntropy/connection_pool.rb +0 -61
  90. data/test/test_request_info.rb +0 -90
  91. /data/examples/{bad.rb → basic/bad.rb} +0 -0
  92. /data/examples/{card.rb → basic/card.rb} +0 -0
  93. /data/examples/{counter.js → basic/counter.js} +0 -0
  94. /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
  95. /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
  96. /data/examples/{index.md → basic/index.md} +0 -0
@@ -2,31 +2,116 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class MethodValidationTest < Minitest::Test
6
- VE = Syntropy::ValidationError
5
+ class RequestInfoTest < Minitest::Test
6
+ def test_uri
7
+ r = Syntropy::MockAdapter.mock(':path' => '/test/path')
8
+ assert_equal '/test/path', r.path
9
+ assert_equal({}, r.query)
7
10
 
8
- def test_validate_http_method
9
- @req = mock_req(':method' => 'GET', ':path' => '/foo')
11
+ r = Syntropy::MockAdapter.mock(':path' => '/test/path?a=1&b=2&c=3%2f4')
12
+ assert_equal '/test/path', r.path
13
+ assert_equal({ 'a' => '1', 'b' => '2', 'c' => '3/4' }, r.query)
14
+ end
10
15
 
11
- assert_equal 'get', @req.validate_http_method('get', 'head')
12
- assert_raises(Syntropy::Error) { @req.validate_http_method('post', 'put') }
13
- assert_raises(Syntropy::Error) { @req.validate_http_method() }
16
+ def test_query
17
+ r = Syntropy::MockAdapter.mock(':path' => '/GponForm/diag_Form?images/')
18
+ assert_equal '/GponForm/diag_Form', r.path
19
+ assert_equal({ 'images/' => true }, r.query)
20
+
21
+ r = Syntropy::MockAdapter.mock(':path' => '/?a=1&b=2')
22
+ assert_equal '/', r.path
23
+ assert_equal({ 'a' => '1', 'b' => '2'}, r.query)
24
+
25
+ r = Syntropy::MockAdapter.mock(':path' => '/?l=a&t=&x=42')
26
+ assert_equal({ 'l' => 'a', 't' => '', 'x' => '42'}, r.query)
14
27
  end
15
28
 
16
- def test_respond_by_http_method
17
- @req = mock_req(':method' => 'GET', ':path' => '/foo')
29
+ def test_host
30
+ r = Syntropy::MockAdapter.mock(':path' => '/')
31
+ assert_nil r.host
32
+ assert_nil r.authority
18
33
 
19
- @req.respond_by_http_method(
20
- 'get' => ['GET foo', {}],
21
- 'post' => ['POST foo', {}]
34
+ r = Syntropy::MockAdapter.mock('host' => 'my.example.com')
35
+ assert_equal 'my.example.com', r.host
36
+ assert_equal 'my.example.com', r.authority
37
+
38
+ r = Syntropy::MockAdapter.mock(':authority' => 'my.foo.com')
39
+ assert_equal 'my.foo.com', r.host
40
+ assert_equal 'my.foo.com', r.authority
41
+ end
42
+
43
+ def test_full_uri
44
+ r = Syntropy::MockAdapter.mock(
45
+ ':scheme' => 'https',
46
+ 'host' => 'foo.bar',
47
+ ':path' => '/hey?a=b&c=d'
22
48
  )
23
- assert_equal 'GET foo', @req.response_body
24
49
 
25
- assert_raises(Syntropy::Error) {
26
- @req.respond_by_http_method(
27
- 'post' => ['POST foo', {}]
28
- )
29
- }
50
+ assert_equal 'https://foo.bar/hey?a=b&c=d', r.full_uri
51
+ end
52
+
53
+ def test_cookies
54
+ r = Syntropy::MockAdapter.mock
55
+
56
+ assert_equal({}, r.cookies)
57
+
58
+ r = Syntropy::MockAdapter.mock(
59
+ 'cookie' => 'uaid=a%2Fb; lastLocus=settings; signin_ref=/'
60
+ )
61
+
62
+ assert_equal({
63
+ 'uaid' => 'a/b',
64
+ 'lastLocus' => 'settings',
65
+ 'signin_ref' => '/'
66
+ }, r.cookies)
67
+ end
68
+
69
+ def test_content_type
70
+ r = Syntropy::MockAdapter.mock(
71
+ ':scheme' => 'https',
72
+ 'host' => 'foo.bar',
73
+ ':path' => '/hey?a=b&c=d',
74
+ 'content-type' => 'text/plain'
75
+ )
76
+ assert_equal 'text/plain', r.content_type
77
+
78
+ r = Syntropy::MockAdapter.mock(
79
+ ':scheme' => 'https',
80
+ 'host' => 'foo.bar',
81
+ ':path' => '/hey?a=b&c=d',
82
+ 'content-type' => 'text/plain; charset=utf-8'
83
+ )
84
+ assert_equal 'text/plain', r.content_type
85
+
86
+ r = Syntropy::MockAdapter.mock(
87
+ ':scheme' => 'https',
88
+ 'host' => 'foo.bar',
89
+ ':path' => '/hey?a=b&c=d',
90
+ 'content-type' => 'text/plain ; charset=utf-8'
91
+ )
92
+ assert_equal 'text/plain', r.content_type
93
+ end
94
+
95
+ def test_rewrite!
96
+ r = Syntropy::MockAdapter.mock(
97
+ ':scheme' => 'https',
98
+ 'host' => 'foo.bar',
99
+ ':path' => '/hey/ho?a=b&c=d'
100
+ )
101
+
102
+ assert_equal '/hey/ho', r.path
103
+ assert_equal URI.parse('/hey/ho?a=b&c=d'), r.uri
104
+ assert_equal 'https://foo.bar/hey/ho?a=b&c=d', r.full_uri
105
+
106
+ r.rewrite!('/hhh', '/')
107
+ assert_equal '/hey/ho', r.path
108
+ assert_equal URI.parse('/hey/ho?a=b&c=d'), r.uri
109
+ assert_equal 'https://foo.bar/hey/ho?a=b&c=d', r.full_uri
110
+
111
+ r.rewrite!('/hey', '/')
112
+ assert_equal '/ho', r.path
113
+ assert_equal URI.parse('/ho?a=b&c=d'), r.uri
114
+ assert_equal 'https://foo.bar/ho?a=b&c=d', r.full_uri
30
115
  end
31
116
  end
32
117
 
@@ -72,6 +157,9 @@ class ValidationTest < Minitest::Test
72
157
  assert_equal 'foo', @req.validate('foo', [String, nil])
73
158
  assert_nil @req.validate(nil, [String, nil])
74
159
 
160
+ assert_equal 'foo', @req.validate('foo', String, /.+/)
161
+ assert_raises(VE) { @req.validate('', String, /.+/) }
162
+
75
163
  assert_equal 123, @req.validate('123', Integer)
76
164
  assert_raises(VE) { @req.validate('a123', Integer) }
77
165
  assert_equal 123, @req.validate('123', Integer, 120..125)
@@ -90,6 +178,53 @@ class ValidationTest < Minitest::Test
90
178
  assert_raises(VE) { @req.validate('foo', :bool) }
91
179
  assert_raises(VE) { @req.validate(nil, :bool) }
92
180
  end
181
+
182
+ def test_validate_http_method
183
+ @req = mock_req(':method' => 'GET', ':path' => '/foo')
184
+
185
+ assert_equal 'get', @req.validate_http_method('get', 'head')
186
+ assert_raises(Syntropy::Error) { @req.validate_http_method('post', 'put') }
187
+ assert_raises(Syntropy::Error) { @req.validate_http_method() }
188
+ end
189
+
190
+ def test_respond_by_http_method
191
+ @req = mock_req(':method' => 'GET', ':path' => '/foo')
192
+
193
+ @req.respond_by_http_method(
194
+ 'get' => ['GET foo', {}],
195
+ 'post' => ['POST foo', {}]
196
+ )
197
+ assert_equal 'GET foo', @req.response_body
198
+
199
+ assert_raises(Syntropy::Error) {
200
+ @req.respond_by_http_method(
201
+ 'post' => ['POST foo', {}]
202
+ )
203
+ }
204
+ end
205
+
206
+ def test_validate_content_type
207
+ @req = mock_req(':method' => 'POST', ':path' => '/foo')
208
+
209
+ assert_raises(Syntropy::InvalidRequestContentTypeError) {
210
+ @req.validate_content_type('application/json')
211
+ }
212
+
213
+ @req = mock_req(':method' => 'POST', ':path' => '/foo', 'content-type' => 'application/json')
214
+ assert_equal 'application/json', @req.validate_content_type(
215
+ 'application/json', 'text/plain'
216
+ )
217
+
218
+ @req = mock_req(':method' => 'POST', ':path' => '/foo', 'content-type' => 'text/html; charset=utf-8')
219
+ assert_equal 'text/html', @req.validate_content_type(
220
+ 'text/plain', 'text/html'
221
+ )
222
+
223
+ @req = mock_req(':method' => 'POST', ':path' => '/foo', 'content-type' => 'text/html ; charset=utf-8')
224
+ assert_equal 'text/html', @req.validate_content_type(
225
+ 'text/plain', 'text/html'
226
+ )
227
+ end
93
228
  end
94
229
 
95
230
  class FormDataTest < Minitest::Test
@@ -46,6 +46,9 @@ class RoutingTreeTest < Minitest::Test
46
46
  'old': {
47
47
  'index.html': '',
48
48
  'baz.html': ''
49
+ },
50
+ '.well-known': {
51
+ 'foo.rb': ''
49
52
  }
50
53
  }
51
54
  }
@@ -72,7 +75,9 @@ class RoutingTreeTest < Minitest::Test
72
75
  assert_nil root[:parent]
73
76
  assert_nil root[:param]
74
77
  assert_nil root[:target]
75
- assert_equal ['[]', 'about', 'api', 'assets', 'old', 'posts'], root[:children].keys.sort_by(&:to_s)
78
+ assert_equal [
79
+ '.well-known', '[]', 'about', 'api', 'assets', 'old', 'posts'
80
+ ], root[:children].keys.sort_by(&:to_s)
76
81
 
77
82
  entry = @rt.static_map['/docs']
78
83
  assert_equal File.join(@rt.root_dir, 'index.rb'), entry[:target][:fn]
@@ -132,18 +137,25 @@ class RoutingTreeTest < Minitest::Test
132
137
  assets = root[:children]['assets']
133
138
  refute_nil assets
134
139
  assert_nil assets[:target]
140
+
141
+ well_known = root[:children]['.well-known']
142
+ refute_nil well_known
143
+ assert_nil well_known[:target]
135
144
  end
136
145
 
137
146
  def test_static_map
138
147
  map = @rt.static_map
139
- assert_equal 7, map.size
148
+ assert_equal 8, map.size
140
149
 
141
150
  keys = map.keys.sort
142
151
  assert_equal [
143
- '/docs', '/docs/about', '/docs/assets/css/style.css',
152
+ '/docs', '/docs/.well-known/foo', '/docs/about', '/docs/assets/css/style.css',
144
153
  '/docs/assets/img/foo.jpg', '/docs/old', '/docs/old/baz', '/docs/posts'
145
154
  ], keys
146
155
 
156
+ o = map['/docs/.well-known/foo']
157
+ assert_equal File.join(@rt.root_dir, '.well-known/foo.rb'), o[:target][:fn]
158
+
147
159
  o = map['/docs/assets/css/style.css']
148
160
  assert_equal File.join(@rt.root_dir, 'assets/css/style.css'), o[:target][:fn]
149
161
 
data/test/test_server.rb CHANGED
@@ -89,12 +89,12 @@ class ServerTest < Minitest::Test
89
89
  req.respond("method: #{req.method}")
90
90
  }
91
91
 
92
- write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHmet /bar HTTP/1.1\r\n\r\n"
92
+ write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nPUT /bar HTTP/1.1\r\n\r\n"
93
93
 
94
94
  @machine.sleep(0.1)
95
95
  response = read_client_side
96
96
  expected = "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: get\r\n0\r\n\r\n" +
97
- "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\ne\r\nmethod: schmet\r\n0\r\n\r\n"
97
+ "HTTP/1.1 200\r\nTransfer-Encoding: chunked\r\n\r\nb\r\nmethod: put\r\n0\r\n\r\n"
98
98
  assert_equal(expected, response)
99
99
  end
100
100
 
@@ -107,7 +107,7 @@ class ServerTest < Minitest::Test
107
107
  raise
108
108
  end
109
109
 
110
- write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nSCHMET /bar HTTP/1.1\r\n\r\n"
110
+ write_http_request "GET /foo HTTP/1.1\r\nServer: foo.com\r\n\r\nPUT /bar HTTP/1.1\r\n\r\n"
111
111
 
112
112
  @machine.sleep(0.01)
113
113
  @server.stop!
@@ -132,7 +132,7 @@ class ServerTest < Minitest::Test
132
132
  Server: foo.com
133
133
  Content-Length: 3
134
134
 
135
- abcSCHMOST /bar HTTP/1.1
135
+ abcPOST /bar HTTP/1.1
136
136
  Server: bar.com
137
137
  Content-Length: 6
138
138
 
@@ -148,7 +148,6 @@ class ServerTest < Minitest::Test
148
148
  assert_equal({
149
149
  ':method' => 'post',
150
150
  ':path' => '/foo',
151
- ':protocol' => 'http/1.1',
152
151
  'server' => 'foo.com',
153
152
  'content-length' => '3',
154
153
  ':body-done-reading' => true,
@@ -160,13 +159,12 @@ class ServerTest < Minitest::Test
160
159
  req1 = @reqs.shift
161
160
  headers = req1.headers
162
161
  assert_equal({
163
- ':method' => 'schmost',
162
+ ':method' => 'post',
164
163
  ':path' => '/bar',
165
- ':protocol' => 'http/1.1',
166
164
  'server' => 'bar.com',
167
165
  'content-length' => '6',
168
166
  ':body-done-reading' => true,
169
- ':tx' => 59,
167
+ ':tx' => 56,
170
168
  }, headers)
171
169
  body = @bodies.shift
172
170
  assert_equal 'defghi', body
@@ -196,7 +194,7 @@ class ServerTest < Minitest::Test
196
194
  de
197
195
  0
198
196
 
199
- SCHMOST /bar HTTP/1.1
197
+ POST /bar HTTP/1.1
200
198
  Server: bar.com
201
199
  Transfer-Encoding: chunked
202
200
 
@@ -218,7 +216,6 @@ class ServerTest < Minitest::Test
218
216
  assert_equal({
219
217
  ':method' => 'post',
220
218
  ':path' => '/foo',
221
- ':protocol' => 'http/1.1',
222
219
  'server' => 'foo.com',
223
220
  'transfer-encoding' => 'chunked',
224
221
  ':body-done-reading' => true,
@@ -230,13 +227,12 @@ class ServerTest < Minitest::Test
230
227
  req1 = @reqs.shift
231
228
  headers = req1.headers
232
229
  assert_equal({
233
- ':method' => 'schmost',
230
+ ':method' => 'post',
234
231
  ':path' => '/bar',
235
- ':protocol' => 'http/1.1',
236
232
  'server' => 'bar.com',
237
233
  'transfer-encoding' => 'chunked',
238
234
  ':body-done-reading' => true,
239
- ':tx' => 59
235
+ ':tx' => 56
240
236
  }, headers)
241
237
  body = @bodies.shift
242
238
  assert_equal '123456789abcdefghijklmnopqrstuv', body
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.30.0
4
+ version: 0.32.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -43,16 +43,16 @@ dependencies:
43
43
  requirements:
44
44
  - - "~>"
45
45
  - !ruby/object:Gem::Version
46
- version: 1.0.0
46
+ version: 1.0.2
47
47
  type: :runtime
48
48
  prerelease: false
49
49
  version_requirements: !ruby/object:Gem::Requirement
50
50
  requirements:
51
51
  - - "~>"
52
52
  - !ruby/object:Gem::Version
53
- version: 1.0.0
53
+ version: 1.0.2
54
54
  - !ruby/object:Gem::Dependency
55
- name: cgi
55
+ name: json
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
@@ -66,21 +66,7 @@ dependencies:
66
66
  - !ruby/object:Gem::Version
67
67
  version: '0'
68
68
  - !ruby/object:Gem::Dependency
69
- name: escape_utils
70
- requirement: !ruby/object:Gem::Requirement
71
- requirements:
72
- - - '='
73
- - !ruby/object:Gem::Version
74
- version: 1.3.0
75
- type: :runtime
76
- prerelease: false
77
- version_requirements: !ruby/object:Gem::Requirement
78
- requirements:
79
- - - '='
80
- - !ruby/object:Gem::Version
81
- version: 1.3.0
82
- - !ruby/object:Gem::Dependency
83
- name: json
69
+ name: logger
84
70
  requirement: !ruby/object:Gem::Requirement
85
71
  requirements:
86
72
  - - ">="
@@ -94,7 +80,7 @@ dependencies:
94
80
  - !ruby/object:Gem::Version
95
81
  version: '0'
96
82
  - !ruby/object:Gem::Dependency
97
- name: logger
83
+ name: irb
98
84
  requirement: !ruby/object:Gem::Requirement
99
85
  requirements:
100
86
  - - ">="
@@ -135,6 +121,20 @@ dependencies:
135
121
  - - "~>"
136
122
  - !ruby/object:Gem::Version
137
123
  version: 13.3.1
124
+ - !ruby/object:Gem::Dependency
125
+ name: solargraph
126
+ requirement: !ruby/object:Gem::Requirement
127
+ requirements:
128
+ - - ">="
129
+ - !ruby/object:Gem::Version
130
+ version: '0'
131
+ type: :development
132
+ prerelease: false
133
+ version_requirements: !ruby/object:Gem::Requirement
134
+ requirements:
135
+ - - ">="
136
+ - !ruby/object:Gem::Version
137
+ version: '0'
138
138
  email: sharon@noteflakes.com
139
139
  executables:
140
140
  - syntropy
@@ -153,6 +153,10 @@ files:
153
153
  - Rakefile
154
154
  - TODO.md
155
155
  - bin/syntropy
156
+ - cmd/_banner.rb
157
+ - cmd/console.rb
158
+ - cmd/help.rb
159
+ - cmd/serve.rb
156
160
  - cmd/setup/template/site/.gitignore
157
161
  - cmd/setup/template/site/Dockerfile
158
162
  - cmd/setup/template/site/Gemfile
@@ -176,15 +180,41 @@ files:
176
180
  - cmd/setup/template/site/site/assets/css/style.css
177
181
  - cmd/setup/template/site/site/assets/img/syntropy.png
178
182
  - cmd/setup/template/site/site/index.rb
183
+ - cmd/test.rb
179
184
  - docker-compose.yml
180
- - examples/bad.rb
181
- - examples/card.rb
182
- - examples/counter.js
183
- - examples/counter.rb
184
- - examples/counter_api.rb
185
- - examples/favicon.ico
186
- - examples/index.md
187
- - examples/templates.rb
185
+ - examples/basic/bad.rb
186
+ - examples/basic/card.rb
187
+ - examples/basic/counter.js
188
+ - examples/basic/counter.rb
189
+ - examples/basic/counter_api.rb
190
+ - examples/basic/favicon.ico
191
+ - examples/basic/index.md
192
+ - examples/basic/templates.rb
193
+ - examples/blog/app/_layout/default.rb
194
+ - examples/blog/app/_lib/post_store.rb
195
+ - examples/blog/app/_schema/2026-01-01-initial.rb
196
+ - examples/blog/app/_setup.rb
197
+ - examples/blog/app/index.rb
198
+ - examples/blog/app/posts/[id]/edit.rb
199
+ - examples/blog/app/posts/[id]/index.rb
200
+ - examples/blog/app/posts/index.rb
201
+ - examples/blog/app/posts/new.rb
202
+ - examples/mcp-oauth/.ruby-version
203
+ - examples/mcp-oauth/Gemfile
204
+ - examples/mcp-oauth/README.md
205
+ - examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb
206
+ - examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb
207
+ - examples/mcp-oauth/app/_lib/auth_store.rb
208
+ - examples/mcp-oauth/app/index.md
209
+ - examples/mcp-oauth/app/mcp.rb
210
+ - examples/mcp-oauth/app/oauth/authorize.rb
211
+ - examples/mcp-oauth/app/oauth/consent.rb
212
+ - examples/mcp-oauth/app/oauth/register.rb
213
+ - examples/mcp-oauth/app/oauth/token.rb
214
+ - examples/mcp-oauth/app/signin.rb
215
+ - examples/mcp-oauth/test/helper.rb
216
+ - examples/mcp-oauth/test/test_app.rb
217
+ - examples/mcp-oauth/test/test_oauth.rb
188
218
  - lib/syntropy.rb
189
219
  - lib/syntropy/app.rb
190
220
  - lib/syntropy/applets/builtin/auto_refresh/watch.js
@@ -196,18 +226,23 @@ files:
196
226
  - lib/syntropy/applets/builtin/json_api.js
197
227
  - lib/syntropy/applets/builtin/ping.rb
198
228
  - lib/syntropy/applets/builtin/req.rb
199
- - lib/syntropy/connection_pool.rb
229
+ - lib/syntropy/db/connection_pool.rb
230
+ - lib/syntropy/db/schema.rb
231
+ - lib/syntropy/db/store.rb
200
232
  - lib/syntropy/dev_mode.rb
201
233
  - lib/syntropy/errors.rb
202
234
  - lib/syntropy/http.rb
203
- - lib/syntropy/http/connection.rb
235
+ - lib/syntropy/http/client.rb
236
+ - lib/syntropy/http/client_connection.rb
237
+ - lib/syntropy/http/io_extensions.rb
204
238
  - lib/syntropy/http/server.rb
239
+ - lib/syntropy/http/server_connection.rb
205
240
  - lib/syntropy/http/status.rb
206
241
  - lib/syntropy/json_api.rb
207
242
  - lib/syntropy/logger.rb
208
243
  - lib/syntropy/markdown.rb
209
244
  - lib/syntropy/mime_types.rb
210
- - lib/syntropy/module.rb
245
+ - lib/syntropy/module_loader.rb
211
246
  - lib/syntropy/papercraft_extensions.rb
212
247
  - lib/syntropy/request.rb
213
248
  - lib/syntropy/request/mock_adapter.rb
@@ -216,9 +251,11 @@ files:
216
251
  - lib/syntropy/request/validation.rb
217
252
  - lib/syntropy/routing_tree.rb
218
253
  - lib/syntropy/side_run.rb
254
+ - lib/syntropy/test.rb
219
255
  - lib/syntropy/utils.rb
220
256
  - lib/syntropy/version.rb
221
257
  - syntropy.gemspec
258
+ - test/app/.well-known/foo.rb
222
259
  - test/app/_hook.rb
223
260
  - test/app/_layout/default.rb
224
261
  - test/app/_lib/callable.rb
@@ -236,6 +273,7 @@ files:
236
273
  - test/app/bad_mod.rb
237
274
  - test/app/bar.rb
238
275
  - test/app/baz.rb
276
+ - test/app/by_method.rb
239
277
  - test/app/deps.rb
240
278
  - test/app/index.html
241
279
  - test/app/mod/bar/index+.rb
@@ -249,20 +287,30 @@ files:
249
287
  - test/app_multi_site/_site.rb
250
288
  - test/app_multi_site/bar.baz/index.html
251
289
  - test/app_multi_site/foo.bar/index.html
290
+ - test/app_setup/_setup.rb
291
+ - test/app_setup/index.rb
292
+ - test/app_with_schema/_schema/2026-01-02-foo.rb
293
+ - test/app_with_schema/_schema/2026-05-30-bar.rb
252
294
  - test/bm_router_proc.rb
253
295
  - test/bm_server.rb
254
296
  - test/helper.rb
255
297
  - test/run.rb
298
+ - test/schema/2026-01-02-foo.rb
299
+ - test/schema/2026-05-30-bar.rb
256
300
  - test/test_app.rb
257
301
  - test/test_caching.rb
258
- - test/test_connection.rb
259
- - test/test_connection_pool.rb
302
+ - test/test_db_connection_pool.rb
303
+ - test/test_db_schema.rb
304
+ - test/test_db_store.rb
260
305
  - test/test_errors.rb
306
+ - test/test_http_client.rb
307
+ - test/test_http_client_connection.rb
308
+ - test/test_http_protocol.rb
309
+ - test/test_http_server_connection.rb
261
310
  - test/test_json_api.rb
262
311
  - test/test_mock_adapter.rb
263
- - test/test_module.rb
264
- - test/test_request_extensions.rb
265
- - test/test_request_info.rb
312
+ - test/test_module_loader.rb
313
+ - test/test_request.rb
266
314
  - test/test_response.rb
267
315
  - test/test_routing_tree.rb
268
316
  - test/test_server.rb
@@ -1,61 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'extralite'
4
-
5
- module Syntropy
6
- class ConnectionPool
7
- attr_reader :count
8
-
9
- def initialize(machine, fn, max_conn)
10
- @machine = machine
11
- @fn = fn
12
- @count = 0
13
- @max_conn = max_conn
14
- @queue = UM::Queue.new
15
- @key = :"db_#{fn}"
16
- end
17
-
18
- def with_db
19
- if (db = Thread.current[@key])
20
- @machine.snooze
21
- return yield(db)
22
- end
23
-
24
- db = checkout
25
- begin
26
- Thread.current[@key] = db
27
- yield(db)
28
- ensure
29
- Thread.current[@key] = nil
30
- checkin(db)
31
- end
32
- end
33
-
34
- def query(sql, *, **, &)
35
- with_db { it.query(sql, *, **, &) }
36
- end
37
-
38
- def execute(sql, *, **)
39
- with_db { it.execute(sql, *, **) }
40
- end
41
-
42
- private
43
-
44
- def checkout
45
- return make_db_instance if @queue.count == 0 && @count < @max_conn
46
-
47
- @machine.shift(@queue)
48
- end
49
-
50
- def checkin(db)
51
- @machine.push(@queue, db)
52
- end
53
-
54
- def make_db_instance
55
- Extralite::Database.new(@fn, wal: true).tap do
56
- @count += 1
57
- it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
58
- end
59
- end
60
- end
61
- end