syntropy 0.43.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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +9 -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/markdown.rb +1 -1
  39. data/lib/syntropy/request/request_info.rb +11 -2
  40. data/lib/syntropy/request/response.rb +46 -4
  41. data/lib/syntropy/request/validation.rb +2 -1
  42. data/lib/syntropy/routing_tree.rb +16 -7
  43. data/lib/syntropy/version.rb +1 -1
  44. data/test/fixtures/app/mod/bar/index+.rb +5 -1
  45. data/test/fixtures/app/params/[foo].rb +1 -1
  46. data/test/test_app.rb +29 -0
  47. data/test/test_response.rb +115 -0
  48. data/test/test_routing_tree.rb +80 -25
  49. data/test/test_server.rb +3 -3
  50. metadata +25 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6109ea0e6322adfd9467ba0aed2b85f04182556dc24efa07eaa6982a19a05480
4
- data.tar.gz: 24520d125bf9f1c2bf54af4def8e2b7f2f0027da3e2508f58f754e46d48e6610
3
+ metadata.gz: 277eab69875c267230dd9c936c17d0bfcb4ee53a3b8baa5a3bea632a8a1517d4
4
+ data.tar.gz: 753ca178d4d5be205ee2d71ee08509b1f8745db80bba1b23785e08a0c7608265
5
5
  SHA512:
6
- metadata.gz: 1af2662580cac682ca0162c2d7088ad2756d0738b8f16cbf41102b3ee06d9283118bafcac36fe24b388f46801481d279833e9bd3b44b6394b2af1ecca7a4856a
7
- data.tar.gz: 26a62a199095a62dcdda41a811991a0818934959f2fd4851d224e83790b26beb305a0e258cbae63e08d018742727d9b789444754e3d99cfb3c5c9291b6804d2e
6
+ metadata.gz: f80177d7e93d797a0ef12644e62265b198275ceac4c0621dc10ab692806bb0f98eb99c4082c84fcada831deaee27d86b1fe9323228fa745b609e4ad18ba94987
7
+ data.tar.gz: 296a00998843af0e1a900ff51419613c6fdb6b4e68c0cd6e14d557ed04c9598e5826c50925954b65d56e8353efdfeb3fb7cea87406d4fe76f829ea092ef47f1b
data/CHANGELOG.md CHANGED
@@ -1,3 +1,12 @@
1
+ # 0.44.0 2026-11-05
2
+
3
+ - Rename `Request#json_pretty_response` to `Request#respond_json_pretty`
4
+ - Rename `Request#method` to `Request#http_method`
5
+ - Add `Request#respond_sse` and `Request#send_event` methods
6
+ - Add `Error.bad_request` helper method
7
+ - Prevent multiple redundant `return nil` statements at end of router proc code
8
+ - Implement `Request#rel_path` for wildcard routes
9
+
1
10
  # 0.43.0 2026-10-07
2
11
 
3
12
  - Fix logging
data/TODO.md CHANGED
@@ -1,3 +1,30 @@
1
+ ## Immediate
2
+
3
+ - Datastar-specific APIs:
4
+
5
+ ```ruby
6
+ req.respond_sse {
7
+ req.send_datastar_event(patch_signals: { _states: { '/time' => Time.now.to_i } })
8
+ }
9
+
10
+ # and also without a block (for non-streaming datastar events)
11
+ req.respond_sse(
12
+ 'datastar-patch-signals' => 'signals { _states: { '/time': #{Time.now.to_i} } }'
13
+ )
14
+
15
+ # more sugar:
16
+ req.respond_datastar(
17
+ patch_signals: { _states: { '/time' => Time.now.to_i } }
18
+ )
19
+ ```
20
+
21
+ - gitshabbos
22
+ - main (README) page `/[repo]`
23
+ - commits page `/[repo]/commits`
24
+ - file page `/[repo]/tree/**`
25
+ - raw page `/[repo]/raw/**`
26
+ - git protocol `/[repo]/git/**`
27
+
1
28
  ## Extralite doc site
2
29
 
3
30
  - Similar to Papercraft site
@@ -98,6 +125,39 @@ Verbose output (`-v`):
98
125
  - CLI command `syntropy background`
99
126
  - Run automatically when running `syntropy serve` in dev mode
100
127
 
128
+ ```sql
129
+ create table jobs (
130
+ id blob primary key not null,
131
+ due_stamp integer,
132
+ interval integer,
133
+ retry_count integer not null,
134
+ state integer not null, -- pending/running/done
135
+ payload jsonb not null
136
+ ) strict;
137
+
138
+ create index idx_jobs_pending on jobs (state) where state = 1;
139
+
140
+ create table job_receipts (
141
+ id integer primary key autoincrement,
142
+ stamp integer not null,
143
+ job_id blob not null,
144
+ retry_count integer not null,
145
+ payload jsonb not null,
146
+ result jsonb not null
147
+ ) strict;
148
+
149
+ create index idx_job_receipts_stamp on job_receipts (stamp);
150
+ create index idx_job_receipts_job_id on job_receipts (job_id);
151
+ ```
152
+
153
+ Create a UUID (not really conforming to UUID v4 RFC 4122, just 16 random bytes):
154
+
155
+ ```ruby
156
+ require 'securerandom'
157
+
158
+ uuid = SecureRandom.random_bytes(16)
159
+ ```
160
+
101
161
  ## Admin Dashboard
102
162
 
103
163
  - Server state / stats
@@ -4,7 +4,7 @@
4
4
  export dispatch_by_http_method
5
5
 
6
6
  def get(req)
7
- id = req.route_params['id'].to_i
7
+ id = req.route_params[:id].to_i
8
8
  post = @posts.get(id)
9
9
  raise Syntropy::Error.not_found if !post
10
10
 
@@ -4,7 +4,7 @@
4
4
  export dispatch_by_http_method
5
5
 
6
6
  def get(req)
7
- id = req.route_params['id'].to_i
7
+ id = req.route_params[:id].to_i
8
8
  post = @posts.get(id)
9
9
  raise Syntropy::Error.not_found if !post
10
10
 
@@ -15,7 +15,7 @@ def post(req)
15
15
  data = req.get_form_data
16
16
  return delete(req) if data['method'] == 'delete'
17
17
 
18
- id = req.route_params['id'].to_i
18
+ id = req.route_params[:id].to_i
19
19
  title = req.validate(data['title'], String, /.+/)
20
20
  body = req.validate(data['body'], String, /.+/)
21
21
 
@@ -27,7 +27,7 @@ def post(req)
27
27
  end
28
28
 
29
29
  def delete(req)
30
- id = req.route_params['id'].to_i
30
+ id = req.route_params[:id].to_i
31
31
 
32
32
  deleted = @posts.delete(id)
33
33
  raise BadRequestError, 'Failed to delete post' if deleted != 1
@@ -0,0 +1,25 @@
1
+ FROM ruby:4.0-slim AS base
2
+ WORKDIR /syntropy
3
+ RUN apt-get update -qq && apt-get install -y \
4
+ curl \
5
+ build-essential \
6
+ libpq-dev \
7
+ libsqlite3-dev \
8
+ libssl-dev \
9
+ libyaml-dev \
10
+ pkg-config && \
11
+ rm -rf /var/lib/apt/lists /var/cache/apt/archives
12
+
13
+ ENV BUNDLE_PATH="/usr/local/bundle"
14
+ COPY Gemfile Gemfile.lock ./
15
+ RUN bundle install
16
+
17
+ RUN groupadd --system --gid 1000 syntropy && \
18
+ useradd syntropy --uid 1000 --gid 1000 --create-home --shell /bin/bash
19
+ USER 1000:1000
20
+
21
+ EXPOSE 1234
22
+
23
+ # Start the main process
24
+ # CMD ["bash"]
25
+ CMD ["bundle", "exec", "syntropy", "serve"]
@@ -0,0 +1,3 @@
1
+ source 'https://gem.coop'
2
+
3
+ gem 'syntropy', path: '../..'
@@ -0,0 +1,40 @@
1
+ # My awesome Syntropy app
2
+
3
+ ## Installation
4
+
5
+ ```bash
6
+ $ bundle install
7
+ ```
8
+
9
+ ## Running the web server
10
+
11
+ ```bash
12
+ $ bundle exec syntropy serve
13
+ ```
14
+
15
+ ## Starting the app console
16
+
17
+ ```bash
18
+ $ bundle exec syntropy console
19
+ ```
20
+
21
+ ### Running tests
22
+
23
+ ```bash
24
+ $ bundle exec syntropy tests
25
+ ```
26
+
27
+ ### Usage with Docker Compose
28
+
29
+ You can also run the app on Docker Compose:
30
+
31
+ ```bash
32
+ # run the app server
33
+ $ docker compose up
34
+
35
+ # run the console
36
+ $ docker compose run --remove-orphans console
37
+
38
+ # run tests
39
+ $ docker compose run --remove-orphans test
40
+ ```
@@ -0,0 +1,23 @@
1
+ layout = import '../_layout/repo'
2
+ repos = import '../../_repos'
3
+
4
+ template = layout.apply { |commits:, **props|
5
+ commits {
6
+ ul {
7
+ commits.each {
8
+ li it
9
+ }
10
+ }
11
+ }
12
+ }
13
+
14
+ export ->(req) {
15
+ repo = repos[req.route_params[:repo]]
16
+ raise Error.not_found if !repo
17
+
18
+ commits = repo.commits[0..15]
19
+ title = repo.name
20
+ repo_url = File.join('/', repo.name)
21
+
22
+ req.respond_html(template.render(title:, repo:, repo_url:, commits:))
23
+ }
@@ -0,0 +1,32 @@
1
+ layout = import '../_layout/repo'
2
+ repos = import '../../_repos'
3
+
4
+ template = layout.apply { |md:, files:, **props|
5
+ main {
6
+ markdown(md)
7
+ }
8
+ sidebar {
9
+ ul {
10
+ files.each { |r, u|
11
+ li { a r, href: u }
12
+ }
13
+ }
14
+ }
15
+ }
16
+
17
+ export ->(req) {
18
+ repo = repos[req.route_params[:repo]]
19
+ raise Error.not_found if !repo
20
+
21
+ item = repo.get('README.md')
22
+ md = item ? @machine.file_read(item[:fn]).force_encoding('UTF-8') : ''
23
+
24
+ files = repo.list.each_with_object({}) { |r, h|
25
+ h[r[:ref]] = File.join(req.path, 'tree', r[:ref])
26
+ }.sort_by { |k, v| k }
27
+
28
+ title = repo.name
29
+ repo_url = req.path
30
+
31
+ req.respond_html(template.render(title:, repo:, repo_url:, md:, files:))
32
+ }
@@ -0,0 +1,11 @@
1
+ repos = import '../../_repos'
2
+
3
+ export ->(req) {
4
+ repo = repos[req.route_params[:repo]]
5
+ raise Error.not_found if !repo
6
+
7
+ item = repo.get(req.rel_path)
8
+ raise Error.not_found if !item
9
+
10
+ req.respond(@machine.file_read(item[:fn]), 'Content-Type' => item[:mime_type])
11
+ }
@@ -0,0 +1,37 @@
1
+ layout = import '../_layout/repo'
2
+ repos = import '../../_repos'
3
+
4
+ template = layout.apply { |title:, item:, content:, url_raw:, **props|
5
+ p(class: 'single-file') { strong title }
6
+ p { a 'Download raw file', href: url_raw }
7
+
8
+ file_content {
9
+ if item[:fn] =~ /\.(png|jpe?g|gif|svg)\z/i
10
+ img(src: url_raw, alt: '')
11
+ elsif item[:binary]
12
+ p 'Binary file, not shown.'
13
+ else
14
+ pre {
15
+ code {
16
+ content.lines.each.each { span it, class: 'line' }
17
+ }
18
+ }
19
+ end
20
+ }
21
+ }
22
+
23
+ export ->(req) {
24
+ repo = repos[req.route_params[:repo]]
25
+ raise Error.not_found if !repo
26
+
27
+ item = repo.get(req.rel_path)
28
+ raise Error.not_found if !item
29
+ content = @machine.file_read(item[:fn]).force_encoding('UTF-8')
30
+
31
+ title = "#{repo.name} - #{req.rel_path}"
32
+ url_raw = File.join('/', req.route_params[:repo], 'raw', req.rel_path)
33
+
34
+ repo_url = File.join('/', repo.name)
35
+
36
+ req.respond_html(template.render(title:, repo:, repo_url:, item:, content:, url_raw:))
37
+ }
@@ -0,0 +1,14 @@
1
+ export template { |**props|
2
+ html {
3
+ head {
4
+ title "Gitshabbos - #{props[:title]}"
5
+ meta charset: 'utf-8'
6
+ meta name: 'viewport', content: 'width=device-width, initial-scale=1.0'
7
+ link rel: "stylesheet", type: "text/css", href: "/assets/style.css"
8
+ }
9
+ body {
10
+ render_children(**props)
11
+ auto_refresh!
12
+ }
13
+ }
14
+ }
@@ -0,0 +1,27 @@
1
+ default = import './default'
2
+
3
+ export default.apply { |repo:, **props|
4
+ nav {
5
+ a '← index', href: '/'
6
+ text ' | '
7
+ a 'about', href: props[:repo_url]
8
+ text ' | '
9
+ a 'commits', href: File.join(props[:repo_url], 'commits')
10
+ }
11
+ if props[:header] != false
12
+ header {
13
+ h3 repo.name
14
+ p repo.description
15
+ p { code "git clone https://git.noteflakes.com/#{repo.name}/.git" }
16
+ }
17
+ end
18
+ content {
19
+ render_children(**props)
20
+ }
21
+ footer {
22
+ p {
23
+ text 'Powered by '
24
+ a 'Syntropy', href: 'https://github.com/digital-fabric/syntropy'
25
+ }
26
+ }
27
+ }