syntropy 0.32.0 → 0.34.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 (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/TODO.md +0 -39
  4. data/cmd/console.rb +18 -7
  5. data/cmd/serve.rb +26 -20
  6. data/cmd/test.rb +90 -21
  7. data/examples/blog/.gitignore +1 -0
  8. data/examples/blog/app/_lib/database.rb +13 -0
  9. data/examples/blog/app/_lib/{post_store.rb → posts.rb} +3 -1
  10. data/examples/blog/app/posts/[id]/edit.rb +2 -2
  11. data/examples/blog/app/posts/[id]/index.rb +8 -5
  12. data/examples/blog/app/posts/index.rb +7 -5
  13. data/examples/blog/app/posts/new.rb +1 -1
  14. data/examples/blog/config/development.rb +5 -0
  15. data/examples/blog/config/production.rb +4 -0
  16. data/examples/blog/config/test.rb +5 -0
  17. data/examples/blog/test/test_posts.rb +65 -0
  18. data/examples/mcp-oauth/app/oauth/token.rb +1 -1
  19. data/examples/mcp-oauth/test/test_app.rb +2 -20
  20. data/examples/mcp-oauth/test/test_oauth.rb +93 -217
  21. data/lib/syntropy/app.rb +48 -40
  22. data/lib/syntropy/applets/builtin/auto_refresh/watch.sse.rb +1 -1
  23. data/lib/syntropy/db/schema.rb +1 -1
  24. data/lib/syntropy/db/store.rb +2 -0
  25. data/lib/syntropy/errors.rb +6 -2
  26. data/lib/syntropy/http/client.rb +1 -0
  27. data/lib/syntropy/http/server_connection.rb +15 -13
  28. data/lib/syntropy/json_api.rb +27 -1
  29. data/lib/syntropy/logger.rb +81 -27
  30. data/lib/syntropy/markdown.rb +61 -32
  31. data/lib/syntropy/mime_types.rb +9 -5
  32. data/lib/syntropy/module_loader.rb +25 -13
  33. data/lib/syntropy/papercraft_extensions.rb +2 -2
  34. data/lib/syntropy/request/mock_adapter.rb +10 -8
  35. data/lib/syntropy/request/request_info.rb +91 -0
  36. data/lib/syntropy/request/response.rb +3 -14
  37. data/lib/syntropy/request/validation.rb +1 -0
  38. data/lib/syntropy/request.rb +55 -14
  39. data/lib/syntropy/routing_tree.rb +27 -28
  40. data/lib/syntropy/session.rb +198 -0
  41. data/lib/syntropy/side_run.rb +25 -2
  42. data/lib/syntropy/test.rb +168 -2
  43. data/lib/syntropy/utils.rb +53 -18
  44. data/lib/syntropy/version.rb +1 -1
  45. data/lib/syntropy.rb +44 -10
  46. data/syntropy.gemspec +1 -0
  47. data/test/bm_router_proc.rb +4 -4
  48. data/test/fixtures/app/class_instance.rb +5 -0
  49. data/test/fixtures/app/http.rb +5 -0
  50. data/test/fixtures/app/post_ct.rb +5 -0
  51. data/test/fixtures/app/singleton.rb +3 -0
  52. data/test/test_app.rb +13 -52
  53. data/test/test_caching.rb +2 -2
  54. data/test/test_db_schema.rb +1 -1
  55. data/test/test_http_server_connection.rb +11 -8
  56. data/test/test_module_loader.rb +5 -2
  57. data/test/test_request_session.rb +254 -0
  58. data/test/test_response.rb +0 -19
  59. data/test/test_routing_tree.rb +69 -69
  60. data/test/test_server.rb +5 -9
  61. data/test/test_test.rb +70 -0
  62. metadata +67 -42
  63. data/examples/blog/app/_setup.rb +0 -4
  64. data/examples/mcp-oauth/test/helper.rb +0 -9
  65. /data/test/{app → fixtures/app}/.well-known/foo.rb +0 -0
  66. /data/test/{app → fixtures/app}/_hook.rb +0 -0
  67. /data/test/{app → fixtures/app}/_layout/default.rb +0 -0
  68. /data/test/{app → fixtures/app}/_lib/callable.rb +0 -0
  69. /data/test/{app → fixtures/app}/_lib/dep.rb +0 -0
  70. /data/test/{app → fixtures/app}/_lib/env.rb +0 -0
  71. /data/test/{app → fixtures/app}/_lib/klass.rb +0 -0
  72. /data/test/{app → fixtures/app}/_lib/missing-export.rb +0 -0
  73. /data/test/{app → fixtures/app}/_lib/self.rb +0 -0
  74. /data/test/{app → fixtures/app}/about/_error.rb +0 -0
  75. /data/test/{app → fixtures/app}/about/foo.md +0 -0
  76. /data/test/{app → fixtures/app}/about/index.rb +0 -0
  77. /data/test/{app → fixtures/app}/about/raise.rb +0 -0
  78. /data/test/{app → fixtures/app}/api+.rb +0 -0
  79. /data/test/{app → fixtures/app}/assets/style.css +0 -0
  80. /data/test/{app → fixtures/app}/bad_mod.rb +0 -0
  81. /data/test/{app → fixtures/app}/bar.rb +0 -0
  82. /data/test/{app → fixtures/app}/baz.rb +0 -0
  83. /data/test/{app → fixtures/app}/by_method.rb +0 -0
  84. /data/test/{app → fixtures/app}/deps.rb +0 -0
  85. /data/test/{app → fixtures/app}/index.html +0 -0
  86. /data/test/{app → fixtures/app}/mod/bar/index+.rb +0 -0
  87. /data/test/{app → fixtures/app}/mod/foo/index.rb +0 -0
  88. /data/test/{app → fixtures/app}/mod/path/a.rb +0 -0
  89. /data/test/{app → fixtures/app}/mod/path/b.rb +0 -0
  90. /data/test/{app → fixtures/app}/params/[foo].rb +0 -0
  91. /data/test/{app → fixtures/app}/rss.rb +0 -0
  92. /data/test/{app → fixtures/app}/tmp.rb +0 -0
  93. /data/test/{app_custom → fixtures/app_custom}/_site.rb +0 -0
  94. /data/test/{app_multi_site → fixtures/app_multi_site}/_site.rb +0 -0
  95. /data/test/{app_multi_site → fixtures/app_multi_site}/bar.baz/index.html +0 -0
  96. /data/test/{app_multi_site → fixtures/app_multi_site}/foo.bar/index.html +0 -0
  97. /data/test/{app_setup → fixtures/app_setup}/_setup.rb +0 -0
  98. /data/test/{app_setup → fixtures/app_setup}/index.rb +0 -0
  99. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-01-02-foo.rb +0 -0
  100. /data/test/{app_with_schema → fixtures/app_with_schema}/_schema/2026-05-30-bar.rb +0 -0
  101. /data/test/{schema → fixtures/schema}/2026-01-02-foo.rb +0 -0
  102. /data/test/{schema → fixtures/schema}/2026-05-30-bar.rb +0 -0
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: fbf52e43aad7bcf2dc6259fd40dac6fb6f70d2215513ae89a5e72b120c4b8585
4
- data.tar.gz: 7a7fd31a1e47bf999d64ad3216bb2c77345a4eb3753df6e2a51384870d9f764f
3
+ metadata.gz: 29b7fe87a7350db0c154f0ee25b1c6992c81aead3938cec537e383dc5e3f6a62
4
+ data.tar.gz: 7dcf8de792102d368aefed72ced1ac3c2e5afd1fe0aea3ac106150fa59862a6b
5
5
  SHA512:
6
- metadata.gz: ff3ac2fbb77785db855080dcd0302beb61332c067c30db6330540cda9a60a7ab72ad684b87772951e8498d5c6aa10b2cf57cce7f1a85932f95db889432c704a0
7
- data.tar.gz: 84ee4aa581f46c84999109bb102d9c890f9dc45b2803a52f114357981edf9150b84c187ba3a62670691e3eb8629b6396bd8f1789f882857b1558089c198f2877
6
+ metadata.gz: 904b3740de777c83a631b4b0b37f72ee22b698b2f1618110a906c6ca5adf979f0c46c84af08d1ae228f8b9aa90bcb54093e88de9c18dcf3961d72ea7e0d94d2c
7
+ data.tar.gz: '0081303c531b38423d94f43423efcdcd80852201b0826f0078b276618b03dc7cde09e24dd599e8cee58ddf4ae8ce1f226d4684b4aa8592407728b9f911e30cc5'
data/CHANGELOG.md CHANGED
@@ -1,3 +1,17 @@
1
+ # 0.34.0 2026-06-03
2
+
3
+ - Streamline options for CLI commands
4
+ - Remove `App#setup_db`
5
+ - Add support for config modules (e.g. `config/development.rb`)
6
+ - Add support for running modes: production, development, test
7
+
8
+ # 0.33.0 2026-06-02
9
+
10
+ - Fix `ModuleLoader` to load a module only once
11
+ - Improve testing tools, add `Syntropy::Test` class
12
+ - Implement `Request#session`, `Request#flash`
13
+ - Improve `Request#set_cookie`
14
+
1
15
  # 0.32.0 2026-06-01
2
16
 
3
17
  - Ensure HTTP request body is consumed (skipped) before treating next request
data/TODO.md CHANGED
@@ -1,44 +1,5 @@
1
1
  ## Immediate
2
2
 
3
- - [ ] Session
4
-
5
- https://guides.rubyonrails.org/action_controller_overview.html#session
6
-
7
- We want something that offers the same features as in Ruby on Rails. A storage
8
- space for session metadata which can include a user_id, flash messages etc.
9
-
10
- - The session is attached to the request, and is valid for the browser
11
- session.
12
- - The session is a KV store. It can be used to store any data relevant to the
13
- user's browser session.
14
- - Each session has a unique ID and that ID is passed to the browser as a
15
- non-persistent cookie.
16
- - A session expires if not used (e.g. after 7 days)
17
- - Session storage either in memory or in DB
18
- - The session is generated (and session cookie set) upon first write to
19
- session.
20
- - Session `Set-Cookie` header should be injected into the HTTP response.
21
- - The entire session info can be stored in a cookie, provided it does not
22
- exceed 4KB.
23
-
24
- ```ruby
25
- req.session[:flash] = 'Title cannot be empty!'
26
- req.redirect '/blah'
27
- ```
28
-
29
- - [ ] Flash messages
30
-
31
- - Flash messages are a sub-feature of session storage and are used to relay
32
- messages from one request to the next in the same session.
33
- - Flash messages have more complex semantics:
34
- - There can be more than one.
35
- - They have types - notice, alert, etc.
36
- - They normally disappear on the next request (i.e. the session cookie
37
- should be updated in the response with Set-Cookie).
38
- - But, you can keep them for the next request with `req.session.flash.keep`
39
- - The flash messages could be used for the current request with
40
- `req.session.flash.now`
41
-
42
3
  - [ ] Collection - treat directories and files as collections of data.
43
4
 
44
5
  Kind of similar to the routing tree, but instead of routes it just takes a
data/cmd/console.rb CHANGED
@@ -2,16 +2,28 @@
2
2
 
3
3
  require_relative '../lib/syntropy'
4
4
  require 'optparse'
5
+ require 'fileutils'
5
6
 
6
7
  env = {
8
+ app_root: File.join(FileUtils.pwd, 'app'),
9
+ config_root: File.join(FileUtils.pwd, 'config'),
10
+ mode: ENV['SYNTROPY_MODE'] || 'development',
7
11
  mount_path: '/',
8
- logger: true,
9
12
  builtin_applet_path: '/.syntropy',
13
+ logger: true,
10
14
  watch_files: true
11
15
  }
12
16
 
13
17
  parser = OptionParser.new do |o|
14
- o.banner = 'Usage: syntropy serve [options] DIR'
18
+ o.banner = 'Usage: syntropy serve [options]'
19
+
20
+ o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
21
+ env[:app_root] = path
22
+ end
23
+
24
+ o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
25
+ env[:config_root] = path
26
+ end
15
27
 
16
28
  o.on('-h', '--help', 'Show this help message') do
17
29
  puts o
@@ -19,7 +31,6 @@ parser = OptionParser.new do |o|
19
31
  end
20
32
 
21
33
  o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
22
- p mount: path
23
34
  env[:mount_path] = path
24
35
  env[:builtin_applet_path] = File.join(path, '.syntropy')
25
36
  end
@@ -43,11 +54,11 @@ rescue StandardError => e
43
54
  exit
44
55
  end
45
56
 
46
- $syntropy_dev_mode = env[:dev_mode]
47
- env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
57
+ Syntropy.dev_mode = env[:mode] == 'development'
58
+ Syntropy.load_config(env)
48
59
 
49
- if !File.directory?(env[:root_dir])
50
- puts "#{File.expand_path(env[:root_dir])} Not a directory"
60
+ if !File.directory?(env[:app_root])
61
+ puts "#{File.expand_path(env[:app_root])} Not a directory"
51
62
  exit
52
63
  end
53
64
 
data/cmd/serve.rb CHANGED
@@ -2,19 +2,27 @@
2
2
 
3
3
  require_relative '../lib/syntropy'
4
4
  require 'optparse'
5
+ require 'fileutils'
5
6
 
6
7
  env = {
7
- mount_path: '/',
8
- logger: true,
9
- builtin_applet_path: '/.syntropy',
10
- server_extensions: {
8
+ app_root: File.join(FileUtils.pwd, 'app'),
9
+ config_root: File.join(FileUtils.pwd, 'config'),
10
+ mode: ENV['SYNTROPY_MODE'] || 'development',
11
+ mount_path: '/',
12
+ builtin_applet_path: '/.syntropy',
13
+ logger: true,
14
+ server_extensions: {
11
15
  date: true,
12
16
  name: 'Syntropy'
13
17
  }
14
18
  }
15
19
 
16
20
  parser = OptionParser.new do |o|
17
- o.banner = 'Usage: syntropy serve [options] DIR'
21
+ o.banner = 'Usage: syntropy serve [options]'
22
+
23
+ o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
24
+ env[:app_root] = path
25
+ end
18
26
 
19
27
  o.on('-b', '--bind BIND', String,
20
28
  'Bind address (default: http://0.0.0.0:1234). You can specify this flag multiple times to bind to multiple addresses.') do
@@ -22,14 +30,8 @@ parser = OptionParser.new do |o|
22
30
  env[:bind] << it
23
31
  end
24
32
 
25
- o.on('-s', '--silent', 'Silent mode') do
26
- env[:banner] = nil
27
- env[:logger] = nil
28
- end
29
-
30
- o.on('-d', '--dev', 'Development mode') do
31
- env[:dev_mode] = true
32
- env[:watch_files] = true
33
+ o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
34
+ env[:config_root] = path
33
35
  end
34
36
 
35
37
  o.on('-h', '--help', 'Show this help message') do
@@ -38,7 +40,6 @@ parser = OptionParser.new do |o|
38
40
  end
39
41
 
40
42
  o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
41
- p mount: path
42
43
  env[:mount_path] = path
43
44
  env[:builtin_applet_path] = File.join(path, '.syntropy')
44
45
  end
@@ -51,6 +52,11 @@ parser = OptionParser.new do |o|
51
52
  env[:server_extensions] = nil
52
53
  end
53
54
 
55
+ o.on('-s', '--silent', 'Silent mode') do
56
+ env[:banner] = nil
57
+ env[:logger] = nil
58
+ end
59
+
54
60
  o.on('-v', '--version', 'Show version') do
55
61
  require 'syntropy/version'
56
62
  puts "Syntropy version #{Syntropy::VERSION}"
@@ -72,24 +78,24 @@ rescue StandardError => e
72
78
  exit
73
79
  end
74
80
 
75
- $syntropy_dev_mode = env[:dev_mode]
76
- env[:root_dir] = (ARGV.shift || '.').gsub(/\/$/, '')
81
+ Syntropy.dev_mode = env[:mode] == 'development'
82
+ Syntropy.load_config(env)
83
+ env[:watch_files] = Syntropy.dev_mode
77
84
 
78
- if !File.directory?(env[:root_dir])
79
- puts "#{File.expand_path(env[:root_dir])} Not a directory"
85
+ if !File.directory?(env[:app_root])
86
+ puts "#{File.expand_path(env[:app_root])} Not a directory"
80
87
  exit
81
88
  end
82
89
 
83
90
  puts env[:banner] if env[:banner]
84
91
  env[:banner] = false
85
92
 
86
-
87
93
  # We set Syntropy.machine so we can reference it from anywhere
88
94
  env[:machine] = Syntropy.machine = UM.new
89
95
  env[:logger] = env[:logger] && Syntropy::Logger.new(env[:machine], **env)
90
96
 
91
97
  require 'syntropy/version'
92
- require 'syntropy/dev_mode' if env[:dev_mode]
98
+ require 'syntropy/dev_mode' if Syntropy.dev_mode
93
99
 
94
100
  app = Syntropy::App.load(env)
95
101
  Syntropy.run(env) { app.call(it) }
data/cmd/test.rb CHANGED
@@ -1,40 +1,109 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- env = {}
3
+ require 'fileutils'
4
+ require 'optparse'
5
+
6
+ pwd = FileUtils.pwd
7
+ env = {
8
+ app_root: File.join(FileUtils.pwd, 'app'),
9
+ config_root: File.join(FileUtils.pwd, 'config'),
10
+ test_root: File.join(pwd, 'test'),
11
+ mode: 'test',
12
+ mount_path: '/'
13
+ }
14
+ MINITEST_ARGV = []
15
+
16
+ parser = OptionParser.new do |o|
17
+ o.banner = 'Usage: syntropy test [options]'
18
+
19
+ o.on('-a', '--app PATH', 'Set app directory (default: ./app') do |path|
20
+ env[:app_root] = path
21
+ end
22
+
23
+ o.on('-c', '--config PATH', 'Set config directory (default: ./config') do |path|
24
+ env[:config_root] = path
25
+ end
26
+
27
+ o.on('-h', '--help', 'Show this help message') do
28
+ puts o
29
+ exit
30
+ end
31
+
32
+ o.on('-m', '--mount PATH', 'Set mount path (default: /)') do |path|
33
+ env[:mount_path] = path
34
+ env[:builtin_applet_path] = File.join(path, '.syntropy')
35
+ end
36
+
37
+ o.on('-n', '--name NAME', 'Specify test to run') do |name|
38
+ MINITEST_ARGV << '--name' << name
39
+ end
40
+
41
+ o.on('-s', '--seed SEED', 'Specify random seed') do |seed|
42
+ MINITEST_ARGV << '--seed' << seed
43
+ end
44
+
45
+ o.on('-t', '--test PATH', 'Set test directory (default: ./test)') do |path|
46
+ env[:test_root] = path
47
+ end
48
+
49
+ o.on('-V', '--verbose', 'Verbose test output') do
50
+ MINITEST_ARGV << '--verbose'
51
+ end
52
+
53
+ o.on('-v', '--version', 'Show version') do
54
+ require 'syntropy/version'
55
+ puts "Syntropy version #{Syntropy::VERSION}"
56
+ exit
57
+ end
58
+
59
+ o.on('-w', '--watch', 'Watch for file changes') do
60
+ env[:watch_mode] = true
61
+ end
62
+ end
63
+
4
64
  argv_copy = ARGV.dup
5
- case ARGV[0]
6
- when '-w', '--watch'
7
- env[:watch_mode] = true
8
- ARGV.shift
9
- when '-h', '--help'
10
- puts <<~MSG
11
- Usage: syntropy test [options] [minitest options]
12
- -w, --watch Rerun tests on file system changes
13
- -h, --help Show this help message
14
- MSG
65
+ begin
66
+ parser.parse!
67
+ rescue OptionParser::InvalidOption
68
+ puts parser
69
+ exit
70
+ rescue StandardError => e
71
+ p e
72
+ puts e.message
73
+ puts e.backtrace.join("\n")
15
74
  exit
16
75
  end
17
76
 
18
77
  require_relative '../lib/syntropy'
19
78
  require_relative '../lib/syntropy/test'
20
79
 
80
+ Syntropy.load_config(env)
81
+
21
82
  $stdout.sync = true
22
83
  $stderr.sync = true
23
84
 
24
- Dir.glob("./test/test_*.rb").each { require(it) }
85
+ Dir.glob("#{File.expand_path(env[:test_root])}/test_*.rb").each { require(it) }
86
+
87
+ def restart_on_file_change(machine, dir, restart_argv)
25
88
 
26
- def watch_for_file_changes
27
- m = UM.new
28
- puts "Waiting for file changes in #{FileUtils.pwd}"
29
- m.file_watch(FileUtils.pwd, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
30
- puts "Detected changes to #{it[:fn]}, restarting"
89
+ machine.file_watch(dir, UM::IN_CREATE | UM::IN_DELETE | UM::IN_CLOSE_WRITE) {
90
+ machine.write(UM::STDOUT_FILENO, "File changed: #{it[:fn]}\n")
31
91
  break
32
92
  }
93
+ exec('ruby', __FILE__, *restart_argv)
33
94
  end
34
95
 
35
- Minitest.run ARGV
96
+ Syntropy::Test.env = (env)
97
+ Minitest.run MINITEST_ARGV
98
+
36
99
  if env[:watch_mode]
37
- puts
38
- watch_for_file_changes
39
- exec("ruby", __FILE__, *argv_copy)
100
+ m = UM.new(size: 4)
101
+ m.write(UM::STDOUT_FILENO, "\n")
102
+ trap('SIGINT') { m.write(UM::STDOUT_FILENO, "\n"); exit! }
103
+
104
+ machine.write(UM::STDOUT_FILENO, "Waiting for file changes...\n")
105
+ m.join(
106
+ m.spin { restart_on_file_change(m, env[:app_root], argv_copy) },
107
+ m.spin { restart_on_file_change(m, env[:test_root], argv_copy) }
108
+ )
40
109
  end
@@ -0,0 +1 @@
1
+ storage/*
@@ -0,0 +1,13 @@
1
+ export self
2
+
3
+ def connection_pool
4
+ @connection_pool ||= DB::ConnectionPool.new(@machine, @env[:config][:storage][:path], 4)
5
+ end
6
+
7
+ def schema
8
+ DB::Schema.new(module_loader: @module_loader, schema_root: '_schema')
9
+ end
10
+
11
+ def migrate!
12
+ schema.apply(connection_pool)
13
+ end
@@ -1,3 +1,5 @@
1
+ DB = import '/_lib/database'
2
+
1
3
  class PostStore < Syntropy::DB::Store
2
4
  # @return [Integer] post id
3
5
  def create(title, body)
@@ -44,4 +46,4 @@ class PostStore < Syntropy::DB::Store
44
46
  end
45
47
  end
46
48
 
47
- export PostStore.new(@app.connection_pool)
49
+ export PostStore.new(DB.connection_pool)
@@ -1,11 +1,11 @@
1
- @post_store = import '/_lib/post_store'
1
+ @posts = import '/_lib/posts'
2
2
  @layout = import '/_layout/default'
3
3
 
4
4
  export http_methods
5
5
 
6
6
  def get(req)
7
7
  id = req.route_params['id'].to_i
8
- post = @post_store.get(id)
8
+ post = @posts.get(id)
9
9
  raise Syntropy::Error.not_found if !post
10
10
 
11
11
  req.respond_html(
@@ -1,15 +1,15 @@
1
- @post_store = import '/_lib/post_store'
1
+ @posts = import '/_lib/posts'
2
2
  @layout = import '/_layout/default'
3
3
 
4
4
  export http_methods
5
5
 
6
6
  def get(req)
7
7
  id = req.route_params['id'].to_i
8
- post = @post_store.get(id)
8
+ post = @posts.get(id)
9
9
  raise Syntropy::Error.not_found if !post
10
10
 
11
11
  req.respond_html(
12
- @template.render(post:)
12
+ @template.render(post:, req:)
13
13
  )
14
14
  end
15
15
 
@@ -21,23 +21,26 @@ def post(req)
21
21
  title = req.validate(data['title'], String, /.+/)
22
22
  body = req.validate(data['body'], String, /.+/)
23
23
 
24
- updated = @post_store.update(id, title, body)
24
+ updated = @posts.update(id, title, body)
25
25
  raise BadRequestError, "Failed to update post" if updated != 1
26
26
 
27
+ req.flash[:notice] = 'Post was successfully updated.'
27
28
  req.redirect "/posts/#{id}", Syntropy::HTTP::SEE_OTHER
28
29
  end
29
30
 
30
31
  def delete(req)
31
32
  id = req.route_params['id'].to_i
32
33
 
33
- deleted = @post_store.delete(id)
34
+ deleted = @posts.delete(id)
34
35
  raise BadRequestError, "Failed to delete post" if deleted != 1
35
36
 
37
+ req.flash[:notice] = 'Post was successfully destroyed.'
36
38
  req.redirect "/posts", Syntropy::HTTP::SEE_OTHER
37
39
  end
38
40
 
39
41
  @template = @layout.apply { |post:, **props|
40
42
  h1 "My blog"
43
+ p props[:req]&.flash[:notice], style: 'color: green'
41
44
  div {
42
45
  h2 {
43
46
  a post[:title]
@@ -1,12 +1,12 @@
1
- @post_store = import '_lib/post_store'
1
+ @posts = import '_lib/posts'
2
2
  @layout = import '_layout/default'
3
3
 
4
4
  export http_methods
5
5
 
6
6
  def get(req)
7
- posts = @post_store.get_all
7
+ posts = @posts.get_all
8
8
  req.respond_html(
9
- @template.render(posts:)
9
+ @template.render(posts:, req:)
10
10
  )
11
11
  end
12
12
 
@@ -14,13 +14,15 @@ def post(req)
14
14
  data = req.get_form_data
15
15
  title = req.validate(data['title'], String, /.+/)
16
16
  body = req.validate(data['body'], String, /.+/)
17
- id = @post_store.create(title, body)
17
+ id = @posts.create(title, body)
18
18
 
19
+ req.flash[:notice] = 'Post was successfully created.'
19
20
  req.redirect("posts/#{id}")
20
21
  end
21
22
 
22
23
  @template = @layout.apply { |**props|
23
- h1 "My blog"
24
+ h1 "My awesome blog"
25
+ p props[:req]&.flash[:notice], style: 'color: green'
24
26
  props[:posts].each { |post|
25
27
  div {
26
28
  h2 {
@@ -1,4 +1,4 @@
1
- @post_store = import '/_lib/post_store'
1
+ @posts = import '/_lib/posts'
2
2
  @layout = import '/_layout/default'
3
3
 
4
4
  export http_methods
@@ -0,0 +1,5 @@
1
+ export(
2
+ storage: {
3
+ path: ENV['DATABASE_PATH'] || 'storage/development.db'
4
+ }
5
+ )
@@ -0,0 +1,4 @@
1
+ export(
2
+ storage:
3
+ path: ENV['DATABASE_PATH'] || 'storage/production.db'
4
+ )
@@ -0,0 +1,5 @@
1
+ export({
2
+ storage: {
3
+ path: ENV['DATABASE_PATH'] || Syntropy.tmp_path('test-db')
4
+ }
5
+ })
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ class PostsTest < Syntropy::Test
4
+ def setup
5
+ super
6
+ @posts = load_module('/_lib/posts')
7
+ end
8
+
9
+ def test_get_all
10
+ assert_equal [], @posts.get_all
11
+
12
+ @posts.create('foo', 'bar')
13
+
14
+ assert_equal [
15
+ { id: 1, title: 'foo', body: 'bar' }
16
+ ], @posts.get_all
17
+ end
18
+
19
+ def test_get
20
+ assert_nil @posts.get(1)
21
+ assert_nil @posts.get(2)
22
+
23
+ @posts.create('foo', 'bar')
24
+
25
+ assert_equal(
26
+ { id: 1, title: 'foo', body: 'bar' },
27
+ @posts.get(1)
28
+ )
29
+ assert_nil @posts.get(2)
30
+
31
+ @posts.create('bar', 'baz')
32
+
33
+ assert_equal(
34
+ { id: 1, title: 'foo', body: 'bar' },
35
+ @posts.get(1)
36
+ )
37
+ assert_equal(
38
+ { id: 2, title: 'bar', body: 'baz' },
39
+ @posts.get(2)
40
+ )
41
+ end
42
+
43
+ def test_update
44
+ assert_equal 0, @posts.update(1, 'qqq', 'ttt')
45
+
46
+ @posts.create('foo', 'bar')
47
+ assert_equal 1, @posts.update(1, 'qqq', 'ttt')
48
+
49
+ assert_equal [
50
+ { id: 1, title: 'qqq', body: 'ttt' }
51
+ ], @posts.get_all
52
+ end
53
+
54
+ def test_delete
55
+ assert_equal 0, @posts.delete(1)
56
+
57
+ @posts.create('foo', 'bar')
58
+ @posts.create('bar', 'baz')
59
+
60
+ assert_equal 1, @posts.delete(1)
61
+ assert_equal [
62
+ { id: 2, title: 'bar', body: 'baz' }
63
+ ], @posts.get_all
64
+ end
65
+ end
@@ -65,7 +65,7 @@ rescue ValidationError => e
65
65
  },
66
66
  ':status' => Syntropy::HTTP::BAD_REQUEST
67
67
  )
68
- rescue => e
68
+ rescue StandardError => e
69
69
  status = Syntropy::Error.http_status(e)
70
70
  raise if status == HTTP::INTERNAL_SERVER_ERROR
71
71
 
@@ -1,26 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require_relative 'helper'
4
-
5
- class AppTest < Minitest::Test
6
- APP_ROOT = File.expand_path(File.join(__dir__, '../app'))
7
- HTTP = Syntropy::HTTP
8
-
9
- def setup
10
- @machine = UM.new
11
- @app = Syntropy::App.new(
12
- root_dir: APP_ROOT,
13
- mount_path: '/',
14
- machine: @machine
15
- )
16
- @test_harness = Syntropy::TestHarness.new(@app)
17
- end
18
-
3
+ class AppTest < Syntropy::Test
19
4
  def test_root
20
- req = @test_harness.request(
21
- ':method' => 'GET',
22
- ':path' => '/'
23
- )
5
+ req = get('/')
24
6
  assert_equal HTTP::OK, req.response_status
25
7
  assert_match /Syntropy/, req.response_body
26
8
  end