syntropy 0.37.0 → 0.38.1

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 +12 -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 +25 -3
  19. data/lib/syntropy/controller_extensions.rb +117 -0
  20. data/lib/syntropy/module_loader.rb +46 -48
  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: 1ec61f53807ee4049d4c5e04ae42f66f3b4824733a04cad245d150a0c72f0acc
4
+ data.tar.gz: ee5b563302d15d6a8d58fc7460636b5f402f726f730c42799862ceb92cafa42f
5
5
  SHA512:
6
- metadata.gz: 19d092f17b75b7cc3290cb2802f46bd19efbb30855f3acf78e1cfc2f88084e0c0dca68bbfcd661545bd50def484cc9526d8a2f4057c9a6da8d6639c95b2cd8c4
7
- data.tar.gz: 34d839e355da94f8993ffefa1a48c9bebfecb6da9c6b43cfe7f934d4fe8a804889d2a7a75874f0107b3226bd6a880037466610f4cffd87d48bdab2f19c5e0545
6
+ metadata.gz: 30d9fcc1b9d0db54e4faa8fa009e547d69e8b6b1beeb3684995b9c27c8f22ebffdffc5c3ec0000495aac235d21b6d61b8cb57a2e305941fc3ec00276c12d283c
7
+ data.tar.gz: 8453a4dfc06cba6e3cd741fb1260618af3825366cc937256cb0d2614c6dc520d5da983309c47010e493acbbae66d8a4cc50e898d3cc6723d9189364914c6579a
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ # 0.38.1 2026-06-13
2
+
3
+ - Fix builtin app, controller extensions
4
+
5
+ # 0.38.0 2026-06-13
6
+
7
+ - Reimplement controller extensions: `dispatch_by_host`,
8
+ `dispatch_by_http_method`
9
+ - Fix middleware composition
10
+ - Add CLI command shortcuts
11
+ - Rename `Syntropy::Module` to `Syntropy::ModuleContext`
12
+
1
13
  # 0.37.0 2026-06-07
2
14
 
3
15
  - 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
@@ -24,6 +25,24 @@ module Syntropy
24
25
  site_file_app(env) || default_app(env)
25
26
  end
26
27
 
28
+ BUILTIN_APPLET_app_root = File.expand_path(File.join(__dir__, 'applets/builtin'))
29
+
30
+ # Creates a builtin applet with the given environment hash. By default the
31
+ # builtin applet is mounted at /.syntropy.
32
+ #
33
+ # @param env [Hash] app environment
34
+ # @param mount_path [String] mount path for the builtin applet
35
+ # @return [Syntropy::App] applet
36
+ def builtin_applet(env, mount_path: '/.syntropy')
37
+ new(
38
+ machine: env[:machine],
39
+ app_root: BUILTIN_APPLET_app_root,
40
+ mount_path: mount_path,
41
+ builtin_applet_path: nil,
42
+ watch_files: nil
43
+ )
44
+ end
45
+
27
46
  private
28
47
 
29
48
  # Creates a multi-hostname app if a _site.rb file is detected.
@@ -34,7 +53,7 @@ module Syntropy
34
53
  fn = File.join(env[:app_root], '_site.rb')
35
54
  return nil if !File.file?(fn)
36
55
 
37
- loader = Syntropy::ModuleLoader.new(env)
56
+ loader = Syntropy::ModuleLoader.new(env, extensions: ControllerExtensions)
38
57
  loader.load('_site')
39
58
  end
40
59
 
@@ -61,7 +80,10 @@ module Syntropy
61
80
  @env = env
62
81
  @logger = env[:logger]
63
82
 
64
- @module_loader = Syntropy::ModuleLoader.new(app: self, **env)
83
+ @module_loader = Syntropy::ModuleLoader.new(
84
+ env.merge(app: self),
85
+ extensions: ControllerExtensions
86
+ )
65
87
  setup_routing_tree
66
88
  start
67
89
  end
@@ -167,7 +189,7 @@ module Syntropy
167
189
  # @return [void]
168
190
  def mount_builtin_applet
169
191
  path = @env[:builtin_applet_path]
170
- @builtin_applet ||= Syntropy.builtin_applet(@env, mount_path: path)
192
+ @builtin_applet ||= App.builtin_applet(@env, mount_path: path)
171
193
  @routing_tree.mount_applet(path, @builtin_applet)
172
194
  end
173
195
 
@@ -0,0 +1,117 @@
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 ref [String] directory path
50
+ # @return [Array<Hash>] array of page entries
51
+ def page_list(ref)
52
+ full_path = File.join(@env[:app_root], ref)
53
+ raise 'Not a directory' if !File.directory?(full_path)
54
+
55
+ Dir[File.join(full_path, '*.md')].sort.map {
56
+ atts, markdown = Syntropy::Markdown.parse(it, @env)
57
+ { atts:, markdown: }
58
+ }
59
+ end
60
+
61
+ # Instantiates a Syntropy app for the given environment hash.
62
+ #
63
+ # @return [Syntropy::App]
64
+ def app(**)
65
+ Syntropy::App.new(**)
66
+ end
67
+
68
+ private
69
+
70
+ # Finds sites in the root directory for the given environment hash, adds
71
+ # entries to the given site map.
72
+ #
73
+ # @param dir [String] relative or absolute path
74
+ # @param site_map [Hash] site map
75
+ # @return [void]
76
+ def setup_directory_sites(ref, site_map)
77
+ app_root = @app ? @app.app_root : @env[:app_root]
78
+ ref = normalize_import_ref(ref)
79
+
80
+ Dir[File.join(app_root, ref, '*')]
81
+ .select { File.directory?(it) && File.basename(it) !~ /^_/ }
82
+ .each { |entry| site_map[File.basename(entry)] = make_app(entry) }
83
+ end
84
+
85
+ # converts the given map entries by adding entries to the given site map.
86
+ #
87
+ # @param map [Hash] ref map
88
+ # @param site_map [Hash] site map
89
+ # @return [void]
90
+ def setup_mapped_sites(map, site_map)
91
+ app_root = @app ? @app.app_root : @env[:app_root]
92
+ map.each do |name, ref|
93
+ ref = File.join(File.dirname(@ref), ref) if ref !~ /^\//
94
+ site_root = File.join(app_root, ref)
95
+ site_map[name] = make_app(site_root)
96
+ end
97
+ end
98
+
99
+ # Creates an app loaded from the given root directory, with the present
100
+ # mount path.
101
+ def make_app(site_root)
102
+ mount_path = @ref == '/_site' ? '/' : @ref
103
+ env = @env.merge(app_root: site_root, mount_path:)
104
+ Syntropy::App.new(**env)
105
+ end
106
+
107
+ # Handles the given request by calling the module method corresponding to
108
+ # the request's HTTP method. If no method is found, raises a
109
+ # method_not_allowed error.
110
+ def route_by_http_method(req)
111
+ sym = req.method.to_sym
112
+ raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
113
+
114
+ send(sym, req)
115
+ end
116
+ end
117
+ 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]
@@ -230,14 +243,6 @@ module Syntropy
230
243
 
231
244
  attr_reader :__export_value__, :__dependencies__
232
245
 
233
- # Returns a list of pages found at the given ref.
234
- #
235
- # @param ref [String] directory reference
236
- # @return [Array] array of pages found in directory
237
- def page_list(ref)
238
- Syntropy.page_list(@env, ref)
239
- end
240
-
241
246
  # Returns true if the module is a collection module. See also
242
247
  # #collection_module!
243
248
  #
@@ -329,22 +334,15 @@ module Syntropy
329
334
  Syntropy::App.new(**env)
330
335
  end
331
336
 
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)
337
+ def handle_syntax_error(env, e)
338
+ $stderr.puts("\n#{e.message}") if !Syntropy.test_mode
339
+ m = e.message.match(/^(.+): syntax/)
340
+ raise e if !m
346
341
 
347
- send(sym, req)
342
+ location = m[1]
343
+ e2 = SyntaxError.new("Syntax errors found in module #{env[:ref]}")
344
+ e2.set_backtrace([location] + e.backtrace)
345
+ raise e2
348
346
  end
349
347
  end
350
348
  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.1'
5
5
  end