syntropy 0.42.0 → 0.44.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/TODO.md +60 -0
  4. data/examples/blog/app/posts/[id]/edit.rb +1 -1
  5. data/examples/blog/app/posts/[id]/index.rb +3 -3
  6. data/examples/gitshabbos/Dockerfile +25 -0
  7. data/examples/gitshabbos/Gemfile +3 -0
  8. data/examples/gitshabbos/README.md +40 -0
  9. data/examples/gitshabbos/app/[repo]/commits.rb +23 -0
  10. data/examples/gitshabbos/app/[repo]/index.rb +32 -0
  11. data/examples/gitshabbos/app/[repo]/raw+.rb +11 -0
  12. data/examples/gitshabbos/app/[repo]/tree+.rb +37 -0
  13. data/examples/gitshabbos/app/_layout/default.rb +14 -0
  14. data/examples/gitshabbos/app/_layout/repo.rb +27 -0
  15. data/examples/gitshabbos/app/_layout/style.css +466 -0
  16. data/examples/gitshabbos/app/_lib/git_repository.rb +95 -0
  17. data/examples/gitshabbos/app/_repos.rb +10 -0
  18. data/examples/gitshabbos/app/git/[repo]/git-receive-pack.rb +0 -0
  19. data/examples/gitshabbos/app/git/[repo]/git-upload-pack.rb +21 -0
  20. data/examples/gitshabbos/app/git/[repo]/info/refs.rb +19 -0
  21. data/examples/gitshabbos/app/git/[repo]/objects/index+.rb +37 -0
  22. data/examples/gitshabbos/app/git/index+.rb +37 -0
  23. data/examples/gitshabbos/app/index.rb +18 -0
  24. data/examples/gitshabbos/config/Caddyfile +5 -0
  25. data/examples/gitshabbos/config/development.rb +11 -0
  26. data/examples/gitshabbos/config/production.rb +5 -0
  27. data/examples/gitshabbos/config/test.rb +5 -0
  28. data/examples/gitshabbos/docker-compose.yml +71 -0
  29. data/examples/gitshabbos/test/test_app.rb +14 -0
  30. data/examples/mcp-oauth/app/oauth/consent.rb +1 -1
  31. data/examples/mcp-oauth/app/signin.rb +1 -1
  32. data/lib/syntropy/app.rb +4 -4
  33. data/lib/syntropy/applets/builtin/default_error_handler.rb +1 -1
  34. data/lib/syntropy/controller_extensions.rb +1 -1
  35. data/lib/syntropy/errors.rb +2 -0
  36. data/lib/syntropy/http/server_connection.rb +1 -1
  37. data/lib/syntropy/json_api.rb +1 -1
  38. data/lib/syntropy/logger.rb +7 -4
  39. data/lib/syntropy/markdown.rb +1 -1
  40. data/lib/syntropy/request/request_info.rb +11 -2
  41. data/lib/syntropy/request/response.rb +46 -4
  42. data/lib/syntropy/request/validation.rb +2 -1
  43. data/lib/syntropy/routing_tree.rb +16 -7
  44. data/lib/syntropy/version.rb +1 -1
  45. data/test/fixtures/app/mod/bar/index+.rb +5 -1
  46. data/test/fixtures/app/params/[foo].rb +1 -1
  47. data/test/test_app.rb +29 -0
  48. data/test/test_response.rb +115 -0
  49. data/test/test_routing_tree.rb +80 -25
  50. data/test/test_server.rb +3 -3
  51. metadata +25 -1
@@ -0,0 +1,466 @@
1
+ * {
2
+ border: 0;
3
+ font: inherit;
4
+ font-size: 100%;
5
+ vertical-align: baseline;
6
+ line-height: 1.5em;
7
+ margin: 0;
8
+ padding: 0;
9
+ }
10
+
11
+ body {
12
+ font-size: 1.5em;
13
+ font-family: sans-serif;
14
+ background-color: #f8f8f8;
15
+ color: #222;
16
+ }
17
+
18
+ ul {
19
+ list-style-position: inside;
20
+ }
21
+
22
+ @media (max-width: 768px) {
23
+ body {
24
+ margin-inline: 1em;
25
+ }
26
+ }
27
+
28
+ nav {
29
+ padding: 0.5em 1em;
30
+ }
31
+
32
+ header {
33
+ padding: 0.5em 1em;
34
+ background-color: #48A;
35
+ color: #eee;
36
+
37
+ h3 {
38
+ font-weight: bold;
39
+ }
40
+
41
+ code {
42
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
43
+ }
44
+ }
45
+
46
+ content {
47
+ display: block;
48
+ padding: 0.5em 1em;
49
+
50
+ &:has(main) {
51
+ display: grid;
52
+ grid-template-columns: 1fr 12em;
53
+ }
54
+ }
55
+
56
+ main {
57
+ display: block;
58
+ margin-right: 1em;
59
+
60
+ h1,
61
+ h2,
62
+ h3 {
63
+ font-weight: bold;
64
+ margin-bottom: 0.75em;
65
+ }
66
+
67
+ h1 {
68
+ font-size: 1.5em;
69
+ }
70
+
71
+ h2 {
72
+ margin-top: 1em;
73
+ font-size: 1.25em;
74
+ }
75
+
76
+ h3 {
77
+ font-size: 1.1em;
78
+ }
79
+
80
+ small {
81
+ font-size: 0.8em;
82
+ }
83
+
84
+ small.date {
85
+ padding-left: 2em;
86
+ color: #888;
87
+ }
88
+
89
+ a {
90
+ color: #248;
91
+ font-weight: 600;
92
+ }
93
+
94
+ a:visited {
95
+ color: #824;
96
+ }
97
+
98
+ li {
99
+ margin-left: 1em;
100
+ margin-bottom: 0.25em;
101
+ }
102
+
103
+ ol li {
104
+ margin-left: 2em;
105
+ }
106
+
107
+ em {
108
+ font-style: italic;
109
+ }
110
+
111
+ strong {
112
+ font-weight: bold;
113
+ }
114
+
115
+ p,
116
+ ul,
117
+ ol {
118
+ margin: 0 0 0.75em 0;
119
+ padding: 0;
120
+ }
121
+
122
+ p code,
123
+ li code {
124
+ color: rgb(66, 146, 41);
125
+ }
126
+
127
+ pre,
128
+ code {
129
+ font-family: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
130
+ font-size: 0.9em;
131
+ line-height: 1.25em;
132
+ }
133
+
134
+ pre {
135
+ padding: 0;
136
+ overflow-x: scroll;
137
+ scrollbar-width: none;
138
+
139
+ /* border-left: 3px solid #a8b8c8;
140
+ padding-left: 12px; */
141
+ }
142
+
143
+ pre * {
144
+ line-height: 1em;
145
+ }
146
+
147
+ pre + p {
148
+ margin-top: 1em;
149
+ }
150
+
151
+ pre code {
152
+ /* margin: 10px; */
153
+ word-wrap: no;
154
+ }
155
+ }
156
+
157
+ sidebar {
158
+ font-size: 0.9em;
159
+ display: block;
160
+ margin-left: 1em;
161
+
162
+ ul {
163
+ list-style: none;
164
+ }
165
+ }
166
+
167
+ commits {
168
+ ul {
169
+ list-style: none;
170
+ }
171
+ }
172
+
173
+ file-content {
174
+ display: block;
175
+ pre {
176
+ background-color: #f8f8f8;
177
+ color: #d4d4d4;
178
+ padding: 1rem;
179
+ border-radius: 6px;
180
+ font-family: 'Fira Code', Consolas, monospace;
181
+ overflow-x: auto;
182
+ }
183
+
184
+ code {
185
+ counter-reset: line-number;
186
+ display: block;
187
+ }
188
+
189
+ .line {
190
+ display: block;
191
+ counter-increment: line-number;
192
+ }
193
+
194
+ .line::before {
195
+ content: counter(line-number);
196
+ display: inline-block;
197
+ width: 2rem;
198
+ margin-right: 1rem;
199
+ color: #6e7681;
200
+ text-align: right;
201
+ user-select: none; /* Prevents selection when copying */
202
+ -webkit-user-select: none;
203
+ }
204
+ }
205
+
206
+ footer {
207
+ padding: 0.5em 1em;
208
+ background-color: #eca;
209
+ }
210
+
211
+ /* Syntax highlighting */
212
+ div.highlight {
213
+ margin-bottom: 1em;
214
+ padding: 1em;
215
+
216
+ pre {
217
+ padding: 0;
218
+ overflow-x: scroll;
219
+ scrollbar-width: none;
220
+ text-align: left;
221
+ }
222
+
223
+ pre * {
224
+ line-height: 1em;
225
+ }
226
+
227
+ pre + p {
228
+ margin-top: 1em;
229
+ }
230
+
231
+ pre code {
232
+ /* margin: 10px; */
233
+ word-wrap: no;
234
+ }
235
+
236
+ &, * {
237
+ background: #f0f0f0;
238
+ }
239
+ .hll {
240
+ background-color: #ffffcc;
241
+ }
242
+ .c {
243
+ color: #408080;
244
+ font-style: italic;
245
+ } /* Comment */
246
+ .err {
247
+ border: 1px solid #ff0000;
248
+ } /* Error */
249
+ .k {
250
+ color: #008000;
251
+ font-weight: bold;
252
+ } /* Keyword */
253
+ .o {
254
+ color: #666666;
255
+ } /* Operator */
256
+ .ch {
257
+ color: #408080;
258
+ font-style: italic;
259
+ } /* Comment.Hashbang */
260
+ .cm {
261
+ color: #408080;
262
+ font-style: italic;
263
+ } /* Comment.Multiline */
264
+ .cp {
265
+ color: #bc7a00;
266
+ } /* Comment.Preproc */
267
+ .cpf {
268
+ color: #408080;
269
+ font-style: italic;
270
+ } /* Comment.PreprocFile */
271
+ .c1 {
272
+ color: #408080;
273
+ font-style: italic;
274
+ } /* Comment.Single */
275
+ .cs {
276
+ color: #408080;
277
+ font-style: italic;
278
+ } /* Comment.Special */
279
+ .gd {
280
+ color: #a00000;
281
+ } /* Generic.Deleted */
282
+ .ge {
283
+ font-style: italic;
284
+ } /* Generic.Emph */
285
+ .gr {
286
+ color: #ff0000;
287
+ } /* Generic.Error */
288
+ .gh {
289
+ color: #000080;
290
+ font-weight: bold;
291
+ } /* Generic.Heading */
292
+ .gi {
293
+ color: #00a000;
294
+ } /* Generic.Inserted */
295
+ .go {
296
+ color: #888888;
297
+ } /* Generic.Output */
298
+ .gp {
299
+ color: #000080;
300
+ font-weight: bold;
301
+ } /* Generic.Prompt */
302
+ .gs {
303
+ font-weight: bold;
304
+ } /* Generic.Strong */
305
+ .gu {
306
+ color: #800080;
307
+ font-weight: bold;
308
+ } /* Generic.Subheading */
309
+ .gt {
310
+ color: #0044dd;
311
+ } /* Generic.Traceback */
312
+ .kc {
313
+ color: #008000;
314
+ font-weight: bold;
315
+ } /* Keyword.Constant */
316
+ .kd {
317
+ color: #008000;
318
+ font-weight: bold;
319
+ } /* Keyword.Declaration */
320
+ .kn {
321
+ color: #008000;
322
+ font-weight: bold;
323
+ } /* Keyword.Namespace */
324
+ .kp {
325
+ color: #008000;
326
+ } /* Keyword.Pseudo */
327
+ .kr {
328
+ color: #008000;
329
+ font-weight: bold;
330
+ } /* Keyword.Reserved */
331
+ .kt {
332
+ color: #b00040;
333
+ } /* Keyword.Type */
334
+ .m {
335
+ color: #666666;
336
+ } /* Literal.Number */
337
+ .s {
338
+ color: #ba2121;
339
+ } /* Literal.String */
340
+ .na {
341
+ color: #7d9029;
342
+ } /* Name.Attribute */
343
+ .nb {
344
+ color: #008000;
345
+ } /* Name.Builtin */
346
+ .nc {
347
+ color: #0000ff;
348
+ font-weight: bold;
349
+ } /* Name.Class */
350
+ .no {
351
+ color: #880000;
352
+ } /* Name.Constant */
353
+ .nd {
354
+ color: #aa22ff;
355
+ } /* Name.Decorator */
356
+ .ni {
357
+ color: #999999;
358
+ font-weight: bold;
359
+ } /* Name.Entity */
360
+ .ne {
361
+ color: #d2413a;
362
+ font-weight: bold;
363
+ } /* Name.Exception */
364
+ .nf {
365
+ color: #0000ff;
366
+ } /* Name.Function */
367
+ .nl {
368
+ color: #a0a000;
369
+ } /* Name.Label */
370
+ .nn {
371
+ color: #0000ff;
372
+ font-weight: bold;
373
+ } /* Name.Namespace */
374
+ .nt {
375
+ color: #008000;
376
+ font-weight: bold;
377
+ } /* Name.Tag */
378
+ .nv {
379
+ color: #19177c;
380
+ } /* Name.Variable */
381
+ .ow {
382
+ color: #aa22ff;
383
+ font-weight: bold;
384
+ } /* Operator.Word */
385
+ .w {
386
+ color: #bbbbbb;
387
+ } /* Text.Whitespace */
388
+ .mb {
389
+ color: #666666;
390
+ } /* Literal.Number.Bin */
391
+ .mf {
392
+ color: #666666;
393
+ } /* Literal.Number.Float */
394
+ .mh {
395
+ color: #666666;
396
+ } /* Literal.Number.Hex */
397
+ .mi {
398
+ color: #666666;
399
+ } /* Literal.Number.Integer */
400
+ .mo {
401
+ color: #666666;
402
+ } /* Literal.Number.Oct */
403
+ .sa {
404
+ color: #ba2121;
405
+ } /* Literal.String.Affix */
406
+ .sb {
407
+ color: #ba2121;
408
+ } /* Literal.String.Backtick */
409
+ .sc {
410
+ color: #ba2121;
411
+ } /* Literal.String.Char */
412
+ .dl {
413
+ color: #ba2121;
414
+ } /* Literal.String.Delimiter */
415
+ .sd {
416
+ color: #ba2121;
417
+ font-style: italic;
418
+ } /* Literal.String.Doc */
419
+ .s2 {
420
+ color: #ba2121;
421
+ } /* Literal.String.Double */
422
+ .se {
423
+ color: #bb6622;
424
+ font-weight: bold;
425
+ } /* Literal.String.Escape */
426
+ .sh {
427
+ color: #ba2121;
428
+ } /* Literal.String.Heredoc */
429
+ .si {
430
+ color: #bb6688;
431
+ font-weight: bold;
432
+ } /* Literal.String.Interpol */
433
+ .sx {
434
+ color: #008000;
435
+ } /* Literal.String.Other */
436
+ .sr {
437
+ color: #bb6688;
438
+ } /* Literal.String.Regex */
439
+ .s1 {
440
+ color: #ba2121;
441
+ } /* Literal.String.Single */
442
+ .ss {
443
+ color: #19177c;
444
+ } /* Literal.String.Symbol */
445
+ .bp {
446
+ color: #008000;
447
+ } /* Name.Builtin.Pseudo */
448
+ .fm {
449
+ color: #0000ff;
450
+ } /* Name.Function.Magic */
451
+ .vc {
452
+ color: #19177c;
453
+ } /* Name.Variable.Class */
454
+ .vg {
455
+ color: #19177c;
456
+ } /* Name.Variable.Global */
457
+ .vi {
458
+ color: #19177c;
459
+ } /* Name.Variable.Instance */
460
+ .vm {
461
+ color: #19177c;
462
+ } /* Name.Variable.Magic */
463
+ .il {
464
+ color: #666666;
465
+ } /* Literal.Number.Integer.Long */
466
+ }
@@ -0,0 +1,95 @@
1
+ require 'syntropy/mime_types'
2
+ require 'open3'
3
+
4
+ class GitRepository
5
+ attr_reader :path, :name, :description, :commits
6
+
7
+ def initialize(path, description)
8
+ @path = path
9
+ @rel_re = /^#{path}\//.freeze
10
+ @files = {}
11
+ @description = description
12
+ setup
13
+ end
14
+
15
+ def get(rel_path)
16
+ item = @files[rel_path]
17
+ return nil if !item
18
+
19
+ item[:mime_type] ||= Syntropy::MimeTypes[
20
+ (m = item[:ref].match(/([^\.]+)$/)) && m[1]
21
+ ]
22
+ item[:binary] = binary_file?(item[:fn]) if item[:binary].nil?
23
+
24
+ item
25
+ end
26
+
27
+ def list
28
+ @files.values
29
+ end
30
+
31
+ def advertise_refs
32
+ `git upload-pack --stateless-rpc --advertise-refs #{@path}`
33
+ end
34
+
35
+ def rpc(machine, service, input, &block)
36
+ Open3.popen3('git', service, '--stateless-rpc', @path) do |i, o, e, w|
37
+ puts '*' * 40
38
+ p 1
39
+ machine.write(i.fileno, input)
40
+ p 2
41
+ machine.close(i.fileno)
42
+ p 3
43
+ # io_out = machine.io(o.fileno, :file)
44
+ p 4
45
+ buf = +''
46
+ loop {
47
+ len = machine.read(o.fileno, buf, 1 << 30)
48
+ break if len == 0
49
+ block.(buf)
50
+ }
51
+ # io.read_each(&block)
52
+ p 5
53
+ w.join
54
+ p 6
55
+ end
56
+ end
57
+
58
+ private
59
+
60
+ def setup
61
+ @name = File.basename(@path)
62
+
63
+ fn = File.join(@path, '.git/description')
64
+ @description ||= File.exist?(fn) ? File.read(fn).strip : ""
65
+
66
+ r = `git -C #{@path} ls-tree -r --name-only HEAD`
67
+ files = r.lines.map(&:chomp).map { File.join(@path, it) }. select { File.file?(it) }
68
+ calc_tree(files)
69
+
70
+ @commits = get_commits
71
+ end
72
+
73
+ def calc_tree(files)
74
+ @readme_fn = files.find { |file_path| file_path =~ /\AREADME(\.md)?\z/ }
75
+ files.each do |fn|
76
+ ref = rel_path(fn)
77
+ item = { fn:, ref: }
78
+ @files[ref] = item
79
+ end
80
+ end
81
+
82
+ def rel_path(path)
83
+ path.gsub(@rel_re, '')
84
+ end
85
+
86
+ def binary_file?(fn)
87
+ File.binread(fn, 8000).to_s.include?("\0")
88
+ end
89
+
90
+ def get_commits
91
+ `git log -C #{@path} --date=short --pretty=format:'%h %ad %an %s'`.lines
92
+ end
93
+ end
94
+
95
+ export GitRepository
@@ -0,0 +1,10 @@
1
+ GitRepository = import '_lib/git_repository'
2
+
3
+ repos = {}
4
+ @env[:config][:repositories].each do |d|
5
+ path = File.expand_path(d[:path])
6
+ repo = GitRepository.new(path, d[:description])
7
+ repos[d[:name]] = repo
8
+ end
9
+
10
+ export repos
@@ -0,0 +1,21 @@
1
+ require 'zlib'
2
+ require 'stringio'
3
+
4
+ repos = import '/_repos'
5
+
6
+ export ->(req) {
7
+ req.validate_http_method('post')
8
+ repo = repos[req.route_params[:repo]]
9
+ raise Error.not_found if !repo
10
+
11
+ input = req.read
12
+ input = Zlib::GzipReader.new(StringIO.new(input)).read if req.headers['content-encoding'] == 'gzip'
13
+
14
+ req.send_headers(':status' => 200, 'Content-Type' => 'application/x-upload-pack-result')
15
+ repo.rpc(@machine, 'upload-pack', input) {
16
+ p len: it.bytesize
17
+ req.adapter.io.write(it)
18
+ }
19
+
20
+ # req.respond(out, 'Content-Type' => "application/x-upload-pack-result")
21
+ }
@@ -0,0 +1,19 @@
1
+ repos = import '/_repos'
2
+
3
+ export ->(req) {
4
+ req.validate_http_method('get')
5
+ repo = repos[req.route_params[:repo]]
6
+ raise Error.not_found if !repo
7
+
8
+ advertise(req, repo)
9
+ }
10
+
11
+ def advertise(req, repo)
12
+ refs = repo.advertise_refs
13
+ body = pkt("# service=git-upload-pack\n") + '0000' + refs
14
+ req.respond(body, 'Content-Type' => 'application/x-git-upload-pack-advertisement')
15
+ end
16
+
17
+ def pkt(line)
18
+ format('%04x%s', line.bytesize + 4, line)
19
+ end
@@ -0,0 +1,37 @@
1
+ require 'open3'
2
+
3
+ repos = import '../../_repos'
4
+
5
+ export ->(req) {
6
+ req.validate_http_method('get')
7
+
8
+ repo = repos[req.route_params[:repo]]
9
+ raise Error.not_found if !repo
10
+
11
+ puts '*' * 40
12
+ p query: req.query
13
+ p rel_path: req.rel_path
14
+
15
+ case req.rel_path
16
+ when 'info/refs'
17
+ if req.query['service'] != 'git-upload-pack'
18
+ raise Error.new('Bad request', HTTP::BAD_REQUEST)
19
+ end
20
+ body = generate_info_refs_body(repo.path)
21
+ req.respond(body, 'Content-Type' => 'Content-Type: application/x-git-upload-pack-advertisement')
22
+ else
23
+ raise Error.bad_request
24
+ end
25
+ }
26
+
27
+ def generate_info_refs_body(repo_path)
28
+ # Execute the git CLI command in stateless RPC advertisement mode
29
+ stdout_str, status = Open3.capture2('git', 'upload-pack', '--stateless-rpc', '--advertise-refs', repo_path)
30
+
31
+ return nil unless status.success?
32
+
33
+ # Smart HTTP header prefix: length (001e) + payload + flush packet (0000)
34
+ header = "001e# service=git-upload-pack\n0000"
35
+
36
+ header + stdout_str
37
+ end
@@ -0,0 +1,37 @@
1
+ require 'open3'
2
+
3
+ repos = import '../../_repos'
4
+
5
+ export ->(req) {
6
+ req.validate_http_method('get')
7
+
8
+ repo = repos[req.route_params[:repo]]
9
+ raise Error.not_found if !repo
10
+
11
+ puts '*' * 40
12
+ p query: req.query
13
+ p rel_path: req.rel_path
14
+
15
+ case req.rel_path
16
+ when 'info/refs'
17
+ if req.query['service'] != 'git-upload-pack'
18
+ raise Error.new('Bad request', HTTP::BAD_REQUEST)
19
+ end
20
+ body = generate_info_refs_body(repo.path)
21
+ req.respond(body, 'Content-Type' => 'Content-Type: application/x-git-upload-pack-advertisement')
22
+ else
23
+ raise Error.bad_request
24
+ end
25
+ }
26
+
27
+ def generate_info_refs_body(repo_path)
28
+ # Execute the git CLI command in stateless RPC advertisement mode
29
+ stdout_str, status = Open3.capture2('git', 'upload-pack', '--stateless-rpc', '--advertise-refs', repo_path)
30
+
31
+ return nil unless status.success?
32
+
33
+ # Smart HTTP header prefix: length (001e) + payload + flush packet (0000)
34
+ header = "001e# service=git-upload-pack\n0000"
35
+
36
+ header + stdout_str
37
+ end
@@ -0,0 +1,18 @@
1
+ layout = import '_layout/default'
2
+ repos = import '../../_repos'
3
+
4
+ template = layout.apply { |repo_list:, **props|
5
+ content {
6
+ h2 'Repositories'
7
+ ul {
8
+ repo_list.each { |n, u|
9
+ li { a n, href: u }
10
+ }
11
+ }
12
+ }
13
+ }
14
+
15
+ export ->(req) {
16
+ repo_list = repos.transform_values { |r| File.join(@ref, r.name) }
17
+ req.respond_html(template.render(repo_list:))
18
+ }
@@ -0,0 +1,5 @@
1
+ localhost {
2
+ reverse_proxy app_server:1234
3
+ tls internal
4
+ encode
5
+ }