syntropy 0.37.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 (58) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +8 -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/version.rb +14 -0
  8. data/examples/blog/app/posts/[id]/edit.rb +1 -1
  9. data/examples/blog/app/posts/[id]/index.rb +1 -1
  10. data/examples/blog/app/posts/index.rb +1 -1
  11. data/examples/blog/app/posts/new.rb +1 -1
  12. data/examples/github/app/[org]/[repo]/index.rb +0 -0
  13. data/examples/github/app/[org]/[repo]/issues/[id].rb +0 -0
  14. data/examples/github/app/[org]/index.rb +0 -0
  15. data/examples/github/app/collections.rb +0 -0
  16. data/examples/github/app/explore.rb +0 -0
  17. data/examples/github/app/index.rb +0 -0
  18. data/lib/syntropy/app.rb +6 -2
  19. data/lib/syntropy/controller_extensions.rb +136 -0
  20. data/lib/syntropy/module_loader.rb +46 -40
  21. data/lib/syntropy/routing_tree.rb +14 -14
  22. data/lib/syntropy/test.rb +28 -10
  23. data/lib/syntropy/version.rb +1 -1
  24. data/lib/syntropy.rb +0 -3
  25. data/test/bm_router_proc.rb +14 -15
  26. data/test/fixtures/app/bad_mod_arity.rb +3 -0
  27. data/test/fixtures/app/by_method.rb +1 -1
  28. data/test/fixtures/app/post_ct.rb +1 -1
  29. data/test/fixtures/app_errors/_error.rb +3 -0
  30. data/test/fixtures/app_errors/foo/_error.rb +3 -0
  31. data/test/fixtures/app_errors/foo/bar/_error.rb +3 -0
  32. data/test/fixtures/app_errors/foo/bar/baz/index.rb +3 -0
  33. data/test/fixtures/app_errors/foo/bar/index.rb +3 -0
  34. data/test/fixtures/app_errors/foo/index.rb +3 -0
  35. data/test/fixtures/app_errors/index.rb +3 -0
  36. data/test/fixtures/app_hooks/_hook.rb +4 -0
  37. data/test/fixtures/app_hooks/foo/_hook.rb +4 -0
  38. data/test/fixtures/app_hooks/foo/bar/_hook.rb +4 -0
  39. data/test/fixtures/app_hooks/foo/bar/baz/_hook.rb +4 -0
  40. data/test/fixtures/app_hooks/foo/bar/baz/index.rb +3 -0
  41. data/test/fixtures/app_hooks/foo/bar/index.rb +3 -0
  42. data/test/fixtures/app_hooks/foo/index.rb +3 -0
  43. data/test/fixtures/app_hooks/index.rb +3 -0
  44. data/test/fixtures/app_multi_site/_site.rb +1 -1
  45. data/test/fixtures/controllers/by_host/bar.com/index.rb +3 -0
  46. data/test/fixtures/controllers/by_host/foo.com/index.rb +3 -0
  47. data/test/fixtures/controllers/by_host_dir.rb +1 -0
  48. data/test/fixtures/controllers/by_host_dir_map.rb +4 -0
  49. data/test/fixtures/controllers/by_host_map.rb +4 -0
  50. data/test/fixtures/controllers/by_http_method.rb +9 -0
  51. data/test/fixtures/controllers/jsonrpc_endpoint.rb +0 -0
  52. data/test/test_app.rb +86 -1
  53. data/test/test_controller.rb +71 -0
  54. data/test/test_module_loader.rb +42 -3
  55. data/test/test_routing_tree.rb +1 -0
  56. data/test/test_test.rb +1 -1
  57. metadata +33 -2
  58. data/lib/syntropy/utils.rb +0 -87
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: eb6c66cfca4b2d7be9060c5571028b5356950fb34f8fba634b0d43b97169cd48
4
- data.tar.gz: 447b2442d22332e0642869e01eee03889d8c85ba31e58909d603382be51f4c7d
3
+ metadata.gz: 85989e43beae58a319d108e1c8cf42f8265fdfaa0675c0fa2b0b8c8df5dbfbd1
4
+ data.tar.gz: cd2dd5db7add2b3e3aa74f2a3fa02da3c1896b808a46be16b088e264875aec10
5
5
  SHA512:
6
- metadata.gz: 19d092f17b75b7cc3290cb2802f46bd19efbb30855f3acf78e1cfc2f88084e0c0dca68bbfcd661545bd50def484cc9526d8a2f4057c9a6da8d6639c95b2cd8c4
7
- data.tar.gz: 34d839e355da94f8993ffefa1a48c9bebfecb6da9c6b43cfe7f934d4fe8a804889d2a7a75874f0107b3226bd6a880037466610f4cffd87d48bdab2f19c5e0545
6
+ metadata.gz: 3cd0fb5d085456bc8d51db049fecaf11f000071cde6b19dd224c7ec422e1fb749be3ba92dfd5b8a43e3e95313f99b1ffed5f926ca37abd5377e2badd249576f3
7
+ data.tar.gz: c3a2b789fe2c9cfbff79b2245381f3ee76bad2e905e3f8408df2666af1495b51f93988d7085c1957330b194237f366e0999b43eb25b8e0e42954f07146aaa5cb
data/CHANGELOG.md CHANGED
@@ -1,3 +1,11 @@
1
+ # 0.38.0 2026-06-13
2
+
3
+ - Reimplement controller extensions: `dispatch_by_host`,
4
+ `dispatch_by_http_method`
5
+ - Fix middleware composition
6
+ - Add CLI command shortcuts
7
+ - Rename `Syntropy::Module` to `Syntropy::ModuleContext`
8
+
1
9
  # 0.37.0 2026-06-07
2
10
 
3
11
  - Call `IO#clear` before closing server connection
data/Gemfile CHANGED
@@ -1,3 +1,7 @@
1
1
  source 'https://gem.coop'
2
2
 
3
3
  gemspec
4
+
5
+ group :development do
6
+ gem 'roda'
7
+ end
data/TODO.md CHANGED
@@ -1,5 +1,9 @@
1
1
  ## Immediate
2
2
 
3
+ - [ ] Controllers
4
+ - [ ] Streamline names of ready-made control methods:
5
+ - [ ] dispatch_json_rpc
6
+
3
7
  - [ ] Collection - treat directories and files as collections of data.
4
8
 
5
9
  Kind of similar to the routing tree, but instead of routes it just takes a
data/bin/syntropy CHANGED
@@ -1,13 +1,23 @@
1
1
  #!/usr/bin/env ruby
2
2
  # frozen_string_literal: true
3
3
 
4
- require_relative '../cmd/_banner'
5
-
6
4
  def cmd_fn(cmd)= File.join(__dir__, "../cmd/#{cmd}.rb")
7
5
 
8
6
  cmd = ARGV.shift || 'help'
9
7
  cmd = 'help' if cmd !~ /^[a-z]+$/
10
8
 
9
+ SHORTCUTS = {
10
+ 'c' => 'console',
11
+ 'n' => 'new',
12
+ 's' => 'serve',
13
+ 't' => 'test',
14
+ 'v' => 'version'
15
+ }
16
+
17
+ if (target_cmd = SHORTCUTS[cmd])
18
+ cmd = target_cmd
19
+ end
20
+
11
21
  fn = cmd_fn(cmd)
12
22
  fn = cmd_fn('help') if !File.file?(fn)
13
23
 
data/cmd/help.rb CHANGED
@@ -5,8 +5,12 @@ HELP = <<~MSG
5
5
 
6
6
  Available commands:
7
7
 
8
+ console Start an IRB session
9
+ help Show this message
10
+ new Create a new Syntropy app
8
11
  serve Start a Syntropy server
9
12
  test Run tests
13
+ version Show version information
10
14
  MSG
11
15
 
12
16
  $stdout << HELP
data/cmd/version.rb ADDED
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'syntropy/version'
4
+ require 'uringmachine/version'
5
+ require_relative './_banner'
6
+
7
+ VERSION = <<~MSG
8
+ Syntropy version #{Syntropy::VERSION}
9
+ UringMachine version #{UringMachine::VERSION}
10
+ Ruby version #{RUBY_VERSION}
11
+ MSG
12
+
13
+ $stdout << SYNTROPY_BANNER
14
+ $stdout << VERSION
@@ -1,7 +1,7 @@
1
1
  @posts = import '/_lib/posts'
2
2
  @layout = import '/_layout/default'
3
3
 
4
- export http_methods
4
+ export dispatch_by_http_method
5
5
 
6
6
  def get(req)
7
7
  id = req.route_params['id'].to_i
@@ -1,7 +1,7 @@
1
1
  @posts = import '/_lib/posts'
2
2
  @layout = import '/_layout/default'
3
3
 
4
- export http_methods
4
+ export dispatch_by_http_method
5
5
 
6
6
  def get(req)
7
7
  id = req.route_params['id'].to_i
@@ -1,7 +1,7 @@
1
1
  @posts = import '_lib/posts'
2
2
  @layout = import '_layout/default'
3
3
 
4
- export http_methods
4
+ export dispatch_by_http_method
5
5
 
6
6
  def get(req)
7
7
  posts = @posts.get_all
@@ -1,7 +1,7 @@
1
1
  @posts = import '/_lib/posts'
2
2
  @layout = import '/_layout/default'
3
3
 
4
- export http_methods
4
+ export dispatch_by_http_method
5
5
 
6
6
  def get(req)
7
7
  req.respond_html(
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
data/lib/syntropy/app.rb CHANGED
@@ -9,6 +9,7 @@ require 'syntropy/errors'
9
9
  require 'syntropy/module_loader'
10
10
  require 'syntropy/routing_tree'
11
11
  require 'syntropy/mime_types'
12
+ require 'syntropy/controller_extensions'
12
13
 
13
14
  module Syntropy
14
15
  # The App implements a Syntropy application. It is responsible for handling
@@ -34,7 +35,7 @@ module Syntropy
34
35
  fn = File.join(env[:app_root], '_site.rb')
35
36
  return nil if !File.file?(fn)
36
37
 
37
- loader = Syntropy::ModuleLoader.new(env)
38
+ loader = Syntropy::ModuleLoader.new(env, extensions: ControllerExtensions)
38
39
  loader.load('_site')
39
40
  end
40
41
 
@@ -61,7 +62,10 @@ module Syntropy
61
62
  @env = env
62
63
  @logger = env[:logger]
63
64
 
64
- @module_loader = Syntropy::ModuleLoader.new(app: self, **env)
65
+ @module_loader = Syntropy::ModuleLoader.new(
66
+ env.merge(app: self),
67
+ extensions: ControllerExtensions
68
+ )
65
69
  setup_routing_tree
66
70
  start
67
71
  end
@@ -0,0 +1,136 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'securerandom'
4
+
5
+ module Syntropy
6
+ # Utilities for use in modules
7
+ module ControllerExtensions
8
+ # Returns a unique temporary path
9
+ #
10
+ # @param prefix [String] temp file prefix
11
+ # @return [String]
12
+ def tmp_path(prefix = 'syntropy')
13
+ "/tmp/#{prefix}-#{SecureRandom.hex(16)}"
14
+ end
15
+
16
+ # Returns a request handler that routes request according to the host
17
+ # header. Looks for site directories (named by host name) in the app's root
18
+ # directory. A map may be given in order to provide additional hostnames to
19
+ # site directories.
20
+ #
21
+ # @param dir [String, nil] relative directory path for host sites
22
+ # @param map [Hash, nil] hash mapping host names to relative site directory
23
+ # @return [Proc] router proc
24
+ def dispatch_by_host(dir = nil, map = nil)
25
+ raise Syntropy::Error, 'Must provide dir and/or map' if !dir && !map
26
+
27
+ site_map = {}
28
+ setup_directory_sites(dir, site_map) if dir
29
+ setup_mapped_sites(map, site_map) if map
30
+
31
+ ->(req) do
32
+ site = site_map[req.host]
33
+ site ? site.call(req) : req.respond(nil, ':status' => HTTP::BAD_REQUEST)
34
+ end
35
+ end
36
+
37
+ # Returns a request handler that handles requests by calling the appropriate
38
+ # module method (e.g. get, post, etc.)
39
+ #
40
+ # @return [Proc]
41
+ def dispatch_by_http_method
42
+ ->(req) do
43
+ route_by_http_method(req)
44
+ end
45
+ end
46
+
47
+ # Returns a list of parsed markdown pages at the given path.
48
+ #
49
+ # @param env [Hash] app environment hash
50
+ # @param ref [String] directory path
51
+ # @return [Array<Hash>] array of page entries
52
+ def page_list(env, ref)
53
+ full_path = File.join(env[:app_root], ref)
54
+ raise 'Not a directory' if !File.directory?(full_path)
55
+
56
+ Dir[File.join(full_path, '*.md')].sort.map {
57
+ atts, markdown = Syntropy::Markdown.parse(it, env)
58
+ { atts:, markdown: }
59
+ }
60
+ end
61
+
62
+ # Instantiates a Syntropy app for the given environment hash.
63
+ #
64
+ # @return [Syntropy::App]
65
+ def app(**)
66
+ Syntropy::App.new(**)
67
+ end
68
+
69
+ BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
70
+
71
+ # Creates a builtin applet with the given environment hash. By default the
72
+ # builtin applet is mounted at /.syntropy.
73
+ #
74
+ # @param env [Hash] app environment
75
+ # @param mount_path [String] mount path for the builtin applet
76
+ # @return [Syntropy::App] applet
77
+ def builtin_applet(env, mount_path: '/.syntropy')
78
+ app(
79
+ machine: env[:machine],
80
+ app_root: BUILTIN_APPLET_app_root,
81
+ mount_path: mount_path,
82
+ builtin_applet_path: nil,
83
+ watch_files: nil
84
+ )
85
+ end
86
+
87
+ private
88
+
89
+ # Finds sites in the root directory for the given environment hash, adds
90
+ # entries to the given site map.
91
+ #
92
+ # @param dir [String] relative or absolute path
93
+ # @param site_map [Hash] site map
94
+ # @return [void]
95
+ def setup_directory_sites(ref, site_map)
96
+ app_root = @app ? @app.app_root : @env[:app_root]
97
+ ref = normalize_import_ref(ref)
98
+
99
+ Dir[File.join(app_root, ref, '*')]
100
+ .select { File.directory?(it) && File.basename(it) !~ /^_/ }
101
+ .each { |entry| site_map[File.basename(entry)] = make_app(entry) }
102
+ end
103
+
104
+ # converts the given map entries by adding entries to the given site map.
105
+ #
106
+ # @param map [Hash] ref map
107
+ # @param site_map [Hash] site map
108
+ # @return [void]
109
+ def setup_mapped_sites(map, site_map)
110
+ app_root = @app ? @app.app_root : @env[:app_root]
111
+ map.each do |name, ref|
112
+ ref = File.join(File.dirname(@ref), ref) if ref !~ /^\//
113
+ site_root = File.join(app_root, ref)
114
+ site_map[name] = make_app(site_root)
115
+ end
116
+ end
117
+
118
+ # Creates an app loaded from the given root directory, with the present
119
+ # mount path.
120
+ def make_app(site_root)
121
+ mount_path = @ref == '/_site' ? '/' : @ref
122
+ env = @env.merge(app_root: site_root, mount_path:)
123
+ Syntropy::App.new(**env)
124
+ end
125
+
126
+ # Handles the given request by calling the module method corresponding to
127
+ # the request's HTTP method. If no method is found, raises a
128
+ # method_not_allowed error.
129
+ def route_by_http_method(req)
130
+ sym = req.method.to_sym
131
+ raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
132
+
133
+ send(sym, req)
134
+ end
135
+ end
136
+ end
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'papercraft'
4
+ require 'syntropy/errors'
4
5
 
5
6
  module Syntropy
6
7
  # The ModuleLoader class implemenets a module loader. It handles loading of
@@ -25,12 +26,14 @@ module Syntropy
25
26
  # Instantiates a module loader
26
27
  #
27
28
  # @param env [Hash] environment hash
29
+ # @param extensions [Module, Array<Module>] extension module(s)
28
30
  # @return [void]
29
- def initialize(env)
31
+ def initialize(env, extensions: nil)
30
32
  @env = env
31
33
  @app_root = env[:app_root]
32
34
  @modules = {} # maps ref to module entry
33
35
  @fn_map = {} # maps filename to ref
36
+ @extensions = extensions
34
37
  end
35
38
 
36
39
  # Loads a module (if not already loaded) and returns its export value.
@@ -131,7 +134,7 @@ module Syntropy
131
134
  @fn_map[fn] = ref
132
135
  code = IO.read(fn)
133
136
  env = @env.merge(module_loader: self, ref: clean_ref(ref))
134
- mod = Syntropy::Module.load(env, code, fn)
137
+ mod = Syntropy::ModuleContext.load(env, code, fn, @extensions)
135
138
  add_dependencies(ref, mod.__dependencies__)
136
139
  export_value = transform_module_export_value(
137
140
  mod.__export_value__, fn, raise_on_missing:
@@ -150,10 +153,10 @@ module Syntropy
150
153
  # @param ref [String] input ref
151
154
  # @return [String] clean ref
152
155
  def clean_ref(ref)
153
- return '/' if ref =~ /^index(\+)?$/
156
+ return '/' if ref =~ /^index[+]?$/
154
157
 
155
- clean = ref.gsub(/\/index(?:\+)?$/, '')
156
- clean == '' ? '/' : clean
158
+ clean = ref.gsub(/\/index[+]?$/, '')
159
+ (clean == '') ? '/' : clean
157
160
  end
158
161
 
159
162
  # Transforms the given export value. If the value is nil, an exception is
@@ -173,9 +176,9 @@ module Syntropy
173
176
  end
174
177
  end
175
178
 
176
- # The Syntropy::Module class implements a reloadable module. A module is a
177
- # `.rb` source file that implements a route endpoint, a template, utility
178
- # methods or any other functionality needed by the web app.
179
+ # The Syntropy::ModuleContext class provides a context for loading a module. A
180
+ # module is a `.rb` source file that implements a route endpoint, a template,
181
+ # utility methods or any other functionality needed by the web app.
179
182
  #
180
183
  # The following instance variables are available to modules:
181
184
  #
@@ -189,35 +192,45 @@ module Syntropy
189
192
  # In addition, the module code also has access to the `MODULE` constant which
190
193
  # is set to `self`, and may be used to refer to various methods defined in the
191
194
  # module.
192
- class Module
195
+ class ModuleContext
193
196
  # Loads a module, returning the module instance
194
- def self.load(env, code, fn)
195
- m = new(**env)
196
- m.instance_eval(code, fn)
197
+ # @param env [Hash] app environment
198
+ # @param code [String] module source code
199
+ # @param fn [String] module file name
200
+ # @param extensions [Module, Array<Module>] extension module(s)
201
+ # @return [Syntropy::ModuleContext] created module context
202
+ def self.load(env, code, fn, extensions)
203
+ mod = new(env)
204
+ apply_extensions(mod, extensions)
205
+ mod.instance_eval(code, fn)
197
206
  env[:logger]&.info(message: "Loaded module at #{fn}")
198
- m
199
- rescue SyntaxError => e
207
+ mod
208
+ rescue StandardError, SyntaxError => e
200
209
  env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
201
- STDERR.puts("\n#{e.message}") if !Syntropy.test_mode
210
+ e.is_a?(SyntaxError) ? handle_syntax_error(env, e) : (raise e)
211
+ end
202
212
 
203
- if (m = e.message.match(/^(.+)\: syntax/))
204
- location = m[1]
205
- e2 = SyntaxError.new("Syntax errors found in module #{env[:ref]}")
206
- e2.set_backtrace([location] + e.backtrace)
207
- raise e2
213
+ # Applies the given extension(s) to the given module context.
214
+ #
215
+ # @param mod [Syntropy::ModuleContext] module context
216
+ # @param extensions [Module, Array<Module>] extension module(s)
217
+ def self.apply_extensions(mod, extensions)
218
+ case extensions
219
+ when Array
220
+ extensions.each { mod.extend(it) }
221
+ when Module
222
+ mod.extend(extensions)
223
+ when nil # return
208
224
  else
209
- raise e
225
+ raise Syntropy::Error, "Invalid module extensions: #{extensions.inspect}"
210
226
  end
211
- rescue => e
212
- env[:logger]&.error(message: "Error while loading module at #{fn}", error: e)
213
- raise e
214
227
  end
215
228
 
216
229
  # Initializes a module with the given environment hash.
217
230
  #
218
231
  # @param env [Hash] environment hash
219
232
  # @return [void]
220
- def initialize(**env)
233
+ def initialize(env)
221
234
  @env = env
222
235
  @machine = env[:machine]
223
236
  @module_loader = env[:module_loader]
@@ -329,22 +342,15 @@ module Syntropy
329
342
  Syntropy::App.new(**env)
330
343
  end
331
344
 
332
- # Returns a request handler that handles requests by calling the appropriate
333
- # module method (e.g. get, post, etc.)
334
- #
335
- # @return [Proc]
336
- def http_methods
337
- ->(req) { route_by_http_method(req) }
338
- end
339
-
340
- # Handles the given request by calling the module method corresponding to
341
- # the request's HTTP method. If no method is found, raises a
342
- # method_not_allowed error.
343
- def route_by_http_method(req)
344
- sym = req.method.to_sym
345
- raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
345
+ def handle_syntax_error(env, e)
346
+ $stderr.puts("\n#{e.message}") if !Syntropy.test_mode
347
+ m = e.message.match(/^(.+): syntax/)
348
+ raise e if !m
346
349
 
347
- send(sym, req)
350
+ location = m[1]
351
+ e2 = SyntaxError.new("Syntax errors found in module #{env[:ref]}")
352
+ e2.set_backtrace([location] + e.backtrace)
353
+ raise e2
348
354
  end
349
355
  end
350
356
  end
@@ -339,8 +339,8 @@ module Syntropy
339
339
  target: { kind:, fn: },
340
340
  # In case we're at the tree root, we need to copy over the hook and
341
341
  # error refs.
342
- hook: !parent[:parent] && parent[:hook],
343
- error: !parent[:parent] && parent[:error]
342
+ hook: parent[:hook],
343
+ error: parent[:error]
344
344
  }
345
345
  end
346
346
  nil
@@ -458,9 +458,9 @@ module Syntropy
458
458
  emit_router_proc_prelude(buffer)
459
459
  segment_idx = 1
460
460
  if @root[:path] != '/'
461
- root_parts = @root[:path].split('/')
462
- segment_idx = root_parts.size
463
- emit_root_validate_guard(buffer:, root_parts:)
461
+ root_segments = @root[:path].split('/')
462
+ segment_idx = root_segments.size
463
+ emit_root_validate_guard(buffer:, root_segments:)
464
464
  end
465
465
 
466
466
  visit_routing_tree_entry(buffer:, entry: @root, segment_idx:)
@@ -492,18 +492,18 @@ module Syntropy
492
492
  def emit_router_proc_prelude(buffer)
493
493
  emit_code_line(buffer, '->(path, params) {')
494
494
  emit_code_line(buffer, ' entry = @static_map[path]; return entry if entry')
495
- emit_code_line(buffer, ' parts = path.split("/")')
495
+ emit_code_line(buffer, ' segments = path.split("/")')
496
496
  end
497
497
 
498
498
  # Emits root path validation guard code.
499
499
  #
500
500
  # @param buffer [String] output buffer
501
- # @param root_parts [Array<String>] root path parts
501
+ # @param root_segments [Array<String>] root path segments
502
502
  # @return [void]
503
- def emit_root_validate_guard(buffer:, root_parts:)
503
+ def emit_root_validate_guard(buffer:, root_segments:)
504
504
  validate_parts = []
505
- (1...root_parts.size).each do |i|
506
- validate_parts << "(parts[#{i}] != #{root_parts[i].inspect})"
505
+ (1...root_segments.size).each do |i|
506
+ validate_parts << "(segments[#{i}] != #{root_segments[i].inspect})"
507
507
  end
508
508
  emit_code_line(buffer, " return nil if #{validate_parts.join(' || ')}")
509
509
  end
@@ -568,7 +568,7 @@ module Syntropy
568
568
 
569
569
  # Get next segment
570
570
  if !case_buffer.empty?
571
- emit_code_line(buffer, "#{ws}case (p = parts[#{segment_idx}])")
571
+ emit_code_line(buffer, "#{ws}case (s = segments[#{segment_idx}])")
572
572
  buffer << case_buffer
573
573
  emit_code_line(buffer, "#{ws}end")
574
574
  end
@@ -630,7 +630,7 @@ module Syntropy
630
630
  next if child_entry[:static]
631
631
 
632
632
  emit_code_line(buffer, "#{ws}when #{k.inspect}")
633
- if_clause = child_entry[:handle_subtree] ? '' : " if !parts[#{segment_idx + 1}]"
633
+ if_clause = child_entry[:handle_subtree] ? '' : " if !segments[#{segment_idx + 1}]"
634
634
 
635
635
  child_path = child_entry[:path]
636
636
  route_value = "@dynamic_map[#{child_path.inspect}]"
@@ -657,12 +657,12 @@ module Syntropy
657
657
  # parametric route
658
658
  if param_entry
659
659
  if when_count == 0
660
- emit_code_line(buffer, "#{ws}when p")
660
+ emit_code_line(buffer, "#{ws}when s")
661
661
  else
662
662
  emit_code_line(buffer, "#{ws}else")
663
663
  end
664
664
 
665
- emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = p")
665
+ emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = s")
666
666
  visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
667
667
  # wildcard route
668
668
  elsif entry[:handle_subtree]
data/lib/syntropy/test.rb CHANGED
@@ -11,12 +11,9 @@ module Syntropy
11
11
  class Test < Minitest::Test
12
12
  HTTP = Syntropy::HTTP
13
13
 
14
- # Sets the app environment for all Syntropy tests.
15
- #
16
- # @param env [Hash] app environment hash
17
- # @return [void]
18
- def self.env=(env)
19
- @@env = env
14
+ class << self
15
+ # Gets/sets app environment for tests
16
+ attr_accessor :env
20
17
  end
21
18
 
22
19
  attr_reader :machine, :app
@@ -25,7 +22,7 @@ module Syntropy
25
22
  #
26
23
  # @return [Hash] test app environment
27
24
  def env
28
- @@env
25
+ self.class.env
29
26
  end
30
27
 
31
28
  # Loads and returns a module with the given reference.
@@ -101,17 +98,38 @@ module Syntropy
101
98
  post(path, 'application/x-www-form-urlencoded', URI.encode_www_form(data), **)
102
99
  end
103
100
 
101
+ # Makes an HTTP PATCH request to the test app.
102
+ #
103
+ # @param path [String] request path
104
+ # @param content_type [String, nil] content MIME type
105
+ # @param body [String] request body
106
+ # @param headers [Hash] request headers
107
+ # @return [Syntropy::Request]
108
+ def patch(path, content_type, body, **headers)
109
+ headers = headers.merge('content-type' => content_type) if content_type
110
+ http_request(
111
+ headers.merge(
112
+ {
113
+ ':method' => 'PATCH',
114
+ ':path' => path
115
+ }
116
+ ),
117
+ body
118
+ )
119
+ end
120
+
104
121
  # Sets up a test instance.
105
122
  #
106
123
  # @return [void]
107
124
  def setup
108
- raise 'Environment not set' if !@@env
125
+ env = self.class.env
126
+ raise 'Environment not set' if !env
109
127
 
110
- Syntropy.load_config(@@env)
128
+ Syntropy.load_config(env)
111
129
 
112
130
  @machine = UM.new
113
131
  @app = Syntropy::App.new(
114
- **@@env.merge(
132
+ **env.merge(
115
133
  machine: @machine,
116
134
  test_mode: true
117
135
  )
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.37.0'
4
+ VERSION = '0.38.0'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -17,15 +17,12 @@ require 'syntropy/papercraft_extensions'
17
17
  require 'syntropy/routing_tree'
18
18
  require 'syntropy/json_api'
19
19
  require 'syntropy/side_run'
20
- require 'syntropy/utils'
21
20
  require 'syntropy/version'
22
21
 
23
22
  # Syntropy is a web framework for building web apps in Ruby. Syntropy uses
24
23
  # UringMachine for I/O and concurrency, and provides a comprehensive and
25
24
  # flexible solution for writing web apps with minimal boilerplate.
26
25
  module Syntropy
27
- extend Utilities
28
-
29
26
  class << self
30
27
  attr_accessor :machine, :dev_mode, :test_mode
31
28