syntropy 0.11 → 0.12

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.
@@ -0,0 +1,553 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ # The RoutingTree class implements a file-based routing tree with support for
5
+ # static files, markdown files, ruby modules, parametric routes, subtree routes,
6
+ # nested middleware and error handlers.
7
+ #
8
+ # A RoutingTree instance takes the given directory (root_dir) and constructs a
9
+ # tree of route entries corresponding to the directory's contents. Finally, it
10
+ # generates an optimized router proc, which is used by the application to return
11
+ # a route entry for each incoming HTTP request.
12
+ #
13
+ # Once initialized, the routing tree is immutable. When running Syntropy in
14
+ # watch mode, whenever a file or directory is changed, added or deleted, a new
15
+ # routing tree will be constructed, and the old one will be discarded.
16
+ #
17
+ # File-based routing in Syntropy follows some simple rules:
18
+ #
19
+ # - Static files (anything other than markdown files or dynamic Ruby modules)
20
+ # are routed to according to their location in the file tree.
21
+ # - Index files with `.md` or `.rb` extension handle requests to their
22
+ # immediate containing directory. For example, `/users/index.rb` will handle
23
+ # requests to `/users`.
24
+ # - Index files with a `+` suffix will also handle requests to anywhere in their
25
+ # subtree. For example, `/users/index+.rb` will also handle requests to
26
+ # `/users/foo/bar`.
27
+ # - Other markdown and module files will handle requests to their bare name
28
+ # (that is, without the extension.) Thus, `/users/foo.rb` will handle requests
29
+ # to `/users/foo`. A route with a `+` suffix will also handle requests to the
30
+ # route's subtree. Thus, `/users/foo+.rb` will also handle requests to
31
+ # `/users/foo/bar`.
32
+ # - Parametric routes are implemented by enclosing the route name in square
33
+ # brackets. For example, `/processes/[proc_id]/index.rb` will handle requests
34
+ # to `/posts/14` etc. Parametric route parts can also be expressed as files,
35
+ # e.g. `/processes/[id]/sources/[src_id].rb` will handle requests to
36
+ # `/posts/14/sources/42` etc. The values for placeholders are added to the
37
+ # incoming request. Here too, a `+` suffix causes the route to also handle
38
+ # requests to its subtree.
39
+ # - Directories and files whose names start with an underscore, e.g. `/_foo` or
40
+ # `/docs/_bar.rb` are skipped and will not be added to the routing tree. This
41
+ # allows you to prevent access through the HTTP server to protected or
42
+ # internal modules or files.
43
+ class RoutingTree
44
+ attr_reader :root_dir, :mount_path, :static_map, :dynamic_map, :root
45
+
46
+ # Initializes a new RoutingTree instance and computes the routing tree
47
+ #
48
+ # @param root_dir [String] root directory of file tree
49
+ # @param mount_path [String] base URL path
50
+ # @return [void]
51
+ def initialize(root_dir:, mount_path:, **env)
52
+ @root_dir = root_dir
53
+ @mount_path = mount_path
54
+ @static_map = {}
55
+ @dynamic_map = {}
56
+ @env = env
57
+ @root = compute_tree
58
+ @static_map.freeze
59
+ @dynamic_map.freeze
60
+ end
61
+
62
+ # Returns the generated router proc for the routing tree
63
+ #
64
+ # @return [Proc] router proc
65
+ def router_proc
66
+ @router_proc ||= compile_router_proc
67
+ end
68
+
69
+ # Returns the first error handler found for the entry. If no error handler
70
+ # exists for the entry itself, the search continues up the tree through the
71
+ # entry's ancestors.
72
+ #
73
+ # @param entry [Hash] route entry
74
+ # @return [String, nil] filename of error handler, or nil if not found
75
+ def route_error_handler(entry)
76
+ return entry[:error] if entry[:error]
77
+
78
+ entry[:parent] && route_error_handler(entry[:parent])
79
+ end
80
+
81
+ # Returns a list of all hooks found up the tree from the given entry. Hooks
82
+ # are returned ordered from the tree root down to the given entry.
83
+ #
84
+ # @param entry [Hash] route entry
85
+ # @return [Array<String>] list of hook entries
86
+ def route_hooks(entry)
87
+ hooks = []
88
+ while entry
89
+ hooks.unshift(entry[:hook]) if entry[:hook]
90
+ entry = entry[:parent]
91
+ end
92
+ hooks
93
+ end
94
+
95
+ # Computes a "clean" URL path for the given path. Modules and markdown are
96
+ # stripped of their extensions, and index file paths are also converted to the
97
+ # containing directory path. For example, the clean URL path for `/foo/bar.rb`
98
+ # is `/foo/bar`. The Clean URL path for `/bar/baz/index.rb` is `/bar/baz`.
99
+ #
100
+ # @param fn [String] file path
101
+ # @return [String] clean path
102
+ def compute_clean_url_path(fn)
103
+ rel_path = fn.sub(@root_dir, '')
104
+ case rel_path
105
+ when /^(.*)\/index\.(md|rb|html)$/
106
+ Regexp.last_match(1).then { it == '' ? '/' : it }
107
+ when /^(.*)\.(md|rb|html)$/
108
+ Regexp.last_match(1)
109
+ else
110
+ rel_path
111
+ end
112
+ end
113
+
114
+ # Converts filename to relative path.
115
+ #
116
+ # @param fn [String] filename
117
+ # @return [String] relative path
118
+ def fn_to_rel_path(fn)
119
+ fn.sub(/^#{Regexp.escape(@root_dir)}\//, '').sub(/\.[^\.]+$/, '')
120
+ end
121
+
122
+ private
123
+
124
+ # Maps extensions to route kind.
125
+ FILE_TYPE = {
126
+ '.rb' => :module,
127
+ '.md' => :markdown
128
+ }
129
+
130
+ # Computes the routing tree, returning the root entry. Route entries are
131
+ # represented as hashes with the following keys:
132
+ #
133
+ # - `:parent` - reference to the parent entry.
134
+ # - `:path` - the URL path for the entry.
135
+ # - `:target` - a hash containing route target information.
136
+ # - `:param` - the parameter name for parametric routes.
137
+ # - `:hook` - a reference to the hook module (`_hook.rb`) for the directory,
138
+ # if exists.
139
+ # - `:error` - a reference to the error handler module (`_error.rb`) for the
140
+ # directory, if exists.
141
+ # - `children` - a hash mapping segment names to the corresponding child
142
+ # entries.
143
+ #
144
+ # Route entries are created for any directory, and for any *dynamic* files
145
+ # (i.e. markdown or Ruby module files). Files starting with `_` are not
146
+ # considered as routes and will not be included in the routing tree. Static
147
+ # files will also not be included in the routing tree, but instead will be
148
+ # mapped in the static file map (see below).
149
+ #
150
+ # The routing tree is complemented with two maps:
151
+ #
152
+ # - `static_map` - maps URL paths to the corresponding static route entries.
153
+ # - `dynamic_map` - maps URL paths to the corresponding dynamic route entries.
154
+ #
155
+ # @return [Hash] root entry
156
+ def compute_tree
157
+ compute_route_directory(dir: @root_dir, rel_path: '/', parent: nil)
158
+ end
159
+
160
+ # Computes a route entry for a directory.
161
+ #
162
+ # @param dir [String] directory path
163
+ # @param rel_path [String] relative directory path
164
+ # @param parent [Hash, nil] parent entry
165
+ def compute_route_directory(dir:, rel_path:, parent: nil)
166
+ param = (m = File.basename(dir).match(/^\[(.+)\]$/)) ? m[1] : nil
167
+ entry = {
168
+ parent: parent,
169
+ path: rel_path_to_abs_path(rel_path),
170
+ param: param,
171
+ hook: find_aux_module_entry(dir, '_hook.rb'),
172
+ error: find_aux_module_entry(dir, '_error.rb')
173
+ }
174
+ entry[:children] = compute_child_routes(
175
+ dir:, rel_path:, parent: entry
176
+ )
177
+ entry
178
+ end
179
+
180
+ # Searches for a file of the given name in the given directory. If found,
181
+ # returns the file path.
182
+ #
183
+ # @param dir [String] directory path
184
+ # @param name [String] filename
185
+ # @return [String, nil] file path if found
186
+ def find_aux_module_entry(dir, name)
187
+ fn = File.join(dir, name)
188
+ File.file?(fn) ? ({ kind: :module, fn: fn }) : nil
189
+ end
190
+
191
+ # Returns a hash mapping file/dir names to route entries.
192
+ #
193
+ # @param dir [String] directory path to scan for files
194
+ # @param rel_path [String] directory path relative to root directory
195
+ # @param parent [Hash] directory's corresponding route entry
196
+ def compute_child_routes(dir:, rel_path:, parent:)
197
+ file_search(dir).each_with_object({}) { |fn, map|
198
+ next if File.basename(fn) =~ /^_/
199
+
200
+ rel_path = compute_clean_url_path(fn)
201
+ child = if File.file?(fn)
202
+ compute_route_file(fn:, rel_path:, parent:)
203
+ elsif File.directory?(fn)
204
+ compute_route_directory(dir: fn, rel_path:, parent:)
205
+ end
206
+ map[child_key(child)] = child if child
207
+ }
208
+ end
209
+
210
+ # Returns all entries in the given dir.
211
+ #
212
+ # @param dir [String] directory path
213
+ # @return [Array<String>] array of file entries
214
+ def file_search(dir)
215
+ Dir[File.join(dir.gsub(/[\[\]]/) { "\\#{it}"}, '*')]
216
+ end
217
+
218
+ # Computes a route entry and/or target for the given file path.
219
+ #
220
+ # @param fn [String] file path
221
+ # @param rel_path [String] relative path
222
+ # @param parent [Hash, nil] parent entry
223
+ # @return [void]
224
+ def compute_route_file(fn:, rel_path:, parent:)
225
+ abs_path = rel_path_to_abs_path(rel_path)
226
+ case
227
+ # index.rb, index+.rb, index.md
228
+ when (m = fn.match(/\/index(\+)?(\.(?:rb|md))$/))
229
+ # Index files (modules or markdown) files) are applied as targets to the
230
+ # immediate containing directory. A + suffix indicates this route
231
+ # handles requests to its subtree
232
+ plus, ext = m[1..2]
233
+ kind = FILE_TYPE[ext]
234
+ handle_subtree = (plus == '+') && (kind == :module)
235
+ set_index_route_target(parent:, path: abs_path, kind:, fn:, handle_subtree:)
236
+
237
+ # index.html
238
+ when fn.match(/\/index\.html$/)
239
+ # HTML index files are applied as targets to the immediate containing
240
+ # directory. It is considered static and therefore not added to the
241
+ # routing tree.
242
+ set_index_route_target(parent:, path: abs_path, kind: :static, fn:)
243
+
244
+ # foo.rb, foo+.rb, foo.md, [foo].rb, [foo]+.rb
245
+ when (m = fn.match(/\/(\[)?([^\]\/\+]+)(\])?(\+)?(\.(?:rb|md))$/))
246
+ # Module and markdown route targets. A + suffix indicates the module
247
+ # also handles requests to the subtree. For example, `/foo/bar.rb` will
248
+ # handle requests to `/foo/bar`, but `/foo/bar+.rb` will also handle
249
+ # requests to `/foo/bar/baz/bug`.
250
+ #
251
+ # parametric, or wildcard, routes convert segments of the URL path into
252
+ # parameters that are added to the HTTP request. Parametric routes are
253
+ # denoted using square brackets around the file/directory name. For
254
+ # example, `/api/posts/[id].rb`` will handle requests to
255
+ # `/api/posts/42`, and will extract the parameter `posts => 42` to add
256
+ # to the incoming request.
257
+ #
258
+ # A + suffix indicates the module also handles the subtree, so e.g.
259
+ # `/api/posts/[id]+.rb` will also handle requests to
260
+ # `/api/posts/42/fans` etc.
261
+ ob, param, cb, plus, ext = m[1..5]
262
+ kind = FILE_TYPE[ext]
263
+ make_route_entry(
264
+ parent:,
265
+ path: abs_path,
266
+ param: ob && cb ? param : nil,
267
+ target: make_route_target(kind:, fn:),
268
+ handle_subtree: (plus == '+') && (kind == :module)
269
+ )
270
+
271
+ # foo.html
272
+ when (m = fn.match(/\/[^\/]+\.html$/))
273
+ # HTML files are routed by their clean URL, i.e. without the `.html`
274
+ # extension. Those are considered as static routes, and do not have
275
+ # entries in the routing tree. Instead they are stored in the static
276
+ # map.
277
+ make_route_entry(
278
+ parent: parent,
279
+ path: abs_path,
280
+ target: make_route_target(kind: :static, fn:)
281
+ )
282
+
283
+ # everything else
284
+ else
285
+ # static files resolved using the static map, and are not added to the
286
+ # routing tree, which is used for resolving dynamic routes.
287
+ make_route_entry(
288
+ parent: parent,
289
+ path: abs_path,
290
+ target: { kind: :static, fn: fn }
291
+ )
292
+ end
293
+ end
294
+
295
+ # Sets an index route target for the given parent entry.
296
+ #
297
+ # @param parent [Hash] parent route entry
298
+ # @param path [String] route path
299
+ # @param kind [Symbol] route target kind
300
+ # @param fn [String] route target filename
301
+ # @param handle_subtree [bool] whether the target handles the subtree
302
+ # @return [nil] (prevents addition of an index route)
303
+ def set_index_route_target(parent:, path:, kind:, fn:, handle_subtree: nil)
304
+ if is_parametric_route?(parent) || handle_subtree
305
+ @dynamic_map[path] = parent
306
+ parent[:target] = make_route_target(kind:, fn:)
307
+ parent[:handle_subtree] = handle_subtree
308
+ else
309
+ @static_map[path] = {
310
+ parent: parent[:parent],
311
+ path: path,
312
+ target: { kind:, fn: },
313
+ # In case we're at the tree root, we need to copy over the hook and
314
+ # error refs.
315
+ hook: !parent[:parent] && parent[:hook],
316
+ error: !parent[:parent] && parent[:error]
317
+ }
318
+ end
319
+ nil
320
+ end
321
+
322
+ # Creates a new route entry, registering it in the static or dynamic map,
323
+ # according to its type.
324
+ #
325
+ # @param entry [Hash] route entry
326
+ def make_route_entry(entry)
327
+ path = entry[:path]
328
+ if is_parametric_route?(entry) || entry[:handle_subtree]
329
+ @dynamic_map[path] = entry
330
+ else
331
+ entry[:static] = true
332
+ @static_map[path] = entry
333
+ end
334
+ end
335
+
336
+ # returns true if the route or any of its ancestors are parametric.
337
+ #
338
+ # @param entry [Hash] route entry
339
+ def is_parametric_route?(entry)
340
+ entry[:param] || (entry[:parent] && is_parametric_route?(entry[:parent]))
341
+ end
342
+
343
+ # Converts a relative URL path to absolute URL path.
344
+ #
345
+ # @param rel_path [String] relative path
346
+ # @return [String] absolute path
347
+ def rel_path_to_abs_path(rel_path)
348
+ rel_path == '/' ? @mount_path : File.join(@mount_path, rel_path)
349
+ end
350
+
351
+ # Returns the key for the given route entry to be used in its parent's
352
+ # children map.
353
+ #
354
+ # @param entry [Hash] route entry
355
+ # @return [String] child key
356
+ def child_key(entry)
357
+ entry[:param] ? '[]' : File.basename(entry[:path]).gsub(/\+$/, '')
358
+ end
359
+
360
+ # Returns a hash representing a route target for the given route target kind
361
+ # and file name.
362
+ #
363
+ # @param kind [Symbol] route target kind
364
+ # @param fn [String] filename
365
+ # @return [Hash] route target hash
366
+ def make_route_target(kind:, fn:)
367
+ {
368
+ kind: kind,
369
+ fn: fn
370
+ }
371
+ end
372
+
373
+ # Generates and returns a router proc based on the routing tree.
374
+ #
375
+ # @return [Proc] router proc
376
+ def compile_router_proc
377
+ code = generate_routing_tree_code
378
+ eval(code, binding, '(router)', 1)
379
+ end
380
+
381
+ # Generates the router proc source code. The router proc code is dynamically
382
+ # generated from the routing tree, converting the routing tree structure into
383
+ # Ruby proc of the following signature:
384
+ #
385
+ # ```ruby
386
+ # # @param path [String] URL path
387
+ # # @param params [Hash] Hash receiving parametric route values
388
+ # # @return [Hash, nil] route entry
389
+ # ->(path, params) { ... }
390
+ # ```
391
+ #
392
+ # The generated code performs the following tasks:
393
+ #
394
+ # - Test if the given path corresponds to a static file (using `@static_map`)
395
+ # - Otherwise, split the given path into path segments
396
+ # - Walk through the path segments according to the routing tree structure
397
+ # - Emit parametric route values to the `params` hash
398
+ # - Return the found route entry
399
+ #
400
+ # @return [String] router proc code to be `eval`ed
401
+ def generate_routing_tree_code
402
+ buffer = +''
403
+ buffer << "# frozen_string_literal: true\n"
404
+
405
+ emit_code_line(buffer, '->(path, params) {')
406
+ emit_code_line(buffer, ' entry = @static_map[path]; return entry if entry')
407
+ emit_code_line(buffer, ' parts = path.split("/")')
408
+
409
+ if @root[:path] != '/'
410
+ root_parts = @root[:path].split('/')
411
+ segment_idx = root_parts.size
412
+ validate_parts = []
413
+ (1..(segment_idx - 1)).each do |i|
414
+ validate_parts << "(parts[#{i}] != #{root_parts[i].inspect})"
415
+ end
416
+ emit_code_line(buffer, " return nil if #{validate_parts.join(' || ')}")
417
+ else
418
+ segment_idx = 1
419
+ end
420
+
421
+ visit_routing_tree_entry(buffer:, entry: @root, segment_idx:)
422
+
423
+ emit_code_line(buffer, " return nil")
424
+ emit_code_line(buffer, "}")
425
+ buffer#.tap { puts '*' * 40; puts it; puts }
426
+ end
427
+
428
+ # Generates routing logic code for the given route entry.
429
+ #
430
+ # @param buffer [String] buffer receiving code
431
+ # @param entry [Hash] route entry
432
+ # @param indent [Integer] indent level
433
+ # @param segment_idx [Integer] path segment index
434
+ # @return [void]
435
+ def visit_routing_tree_entry(buffer:, entry:, indent: 1, segment_idx:)
436
+ ws = ' ' * (indent * 2)
437
+
438
+ # If no targets exist in the entry's subtree, we can return nil
439
+ # immediately.
440
+ if !entry[:target] && !find_target_in_subtree(entry)
441
+ emit_code_line(buffer, "#{ws}return nil")
442
+ return
443
+ end
444
+
445
+ if is_void_route?(entry)
446
+ parent = entry[:parent]
447
+ parametric_sibling = parent && parent[:children] && parent[:children]['[]']
448
+ if parametric_sibling
449
+ emit_code_line(buffer, "#{ws}return nil")
450
+ return
451
+ end
452
+ end
453
+
454
+ # Get next segment
455
+ emit_code_line(buffer, "#{ws}case (p = parts[#{segment_idx}])")
456
+
457
+ # In case of no next segment
458
+ emit_code_line(buffer, "#{ws}when nil")
459
+ if entry[:target]
460
+ map = entry[:static] ? '@static_map' : '@dynamic_map'
461
+ emit_code_line(buffer, "#{ws} return #{map}[#{entry[:path].inspect}]")
462
+ else
463
+ emit_code_line(buffer, "#{ws} return nil")
464
+ end
465
+
466
+ if entry[:children]
467
+ param_entry = entry[:children]['[]']
468
+ entry[:children].each do |k, child_entry|
469
+ # skip if wildcard entry (treated in else clause below)
470
+ next if k == '[]'
471
+
472
+ # skip if entry is void (no target, no children)
473
+ has_target = child_entry[:target]
474
+ has_children = child_entry[:children] && !child_entry[:children].empty?
475
+ next if !has_target && !has_children
476
+
477
+ if has_target && !has_children
478
+ # use the target
479
+ next if child_entry[:static]
480
+
481
+ emit_code_line(buffer, "#{ws}when #{k.inspect}")
482
+ if_clause = child_entry[:handle_subtree] ? '' : " if !parts[#{segment_idx + 1}]"
483
+ route_value = "@dynamic_map[#{child_entry[:path].inspect}]"
484
+ emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
485
+
486
+ elsif has_children
487
+ # otherwise look at the next segment
488
+ next if is_void_route?(child_entry) && !param_entry
489
+
490
+ emit_code_line(buffer, "#{ws}when #{k.inspect}")
491
+ visit_routing_tree_entry(buffer:, entry: child_entry, indent: indent + 1, segment_idx: segment_idx + 1)
492
+ end
493
+ end
494
+
495
+ # parametric route
496
+ if param_entry
497
+ emit_code_line(buffer, "#{ws}else")
498
+ emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = p")
499
+ visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
500
+ end
501
+ end
502
+ emit_code_line(buffer, "#{ws}end")
503
+ end
504
+
505
+ # Returns the first target found in the given entry's subtree.
506
+ #
507
+ # @param entry [Hash] route entry
508
+ # @return [Hash, nil] route target if exists
509
+ def find_target_in_subtree(entry)
510
+ entry[:children]&.values&.each { |e|
511
+ target = e[:target] || find_target_in_subtree(e)
512
+ return target if target
513
+ }
514
+
515
+ nil
516
+ end
517
+
518
+ # Returns true if the given route is not parametric, has no children and is
519
+ # static, or has children and all are void.
520
+ #
521
+ # @param entry [Hash] route entry
522
+ # @return [bool]
523
+ def is_void_route?(entry)
524
+ return false if entry[:param]
525
+
526
+ if entry[:children]
527
+ return true if !entry[:children]['[]'] && entry[:children]&.values&.all? { is_void_route?(it) }
528
+ else
529
+ return true if entry[:static]
530
+ end
531
+
532
+ false
533
+ end
534
+
535
+ DEBUG = !!ENV['DEBUG']
536
+
537
+ # Emits the given code into the given buffer, with a line break at the end.
538
+ # If the `DEBUG` environment variable is set, adds a source location comment
539
+ # at the end of the line, referencing the callsite.
540
+ #
541
+ # @param buffer [String] code buffer
542
+ # @param code [String] code
543
+ # @return [void]
544
+ def emit_code_line(buffer, code)
545
+ if DEBUG
546
+ loc = (m = caller[0].match(/^([^\:]+\:\d+)/)) && m[1]
547
+ buffer << "#{code} # #{loc}\n"
548
+ else
549
+ buffer << "#{code}\n"
550
+ end
551
+ end
552
+ end
553
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.11'
4
+ VERSION = '0.12'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -12,7 +12,7 @@ require 'syntropy/errors'
12
12
  require 'syntropy/markdown'
13
13
  require 'syntropy/module'
14
14
  require 'syntropy/request_extensions'
15
- require 'syntropy/router'
15
+ require 'syntropy/routing_tree'
16
16
  require 'syntropy/rpc_api'
17
17
  require 'syntropy/side_run'
18
18
 
data/syntropy.gemspec CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
25
25
  s.add_dependency 'json', '2.13.2'
26
26
  s.add_dependency 'p2', '2.8'
27
27
  s.add_dependency 'papercraft', '1.4'
28
- s.add_dependency 'qeweney', '0.21'
28
+ s.add_dependency 'qeweney', '0.22'
29
29
  s.add_dependency 'tp2', '0.14.1'
30
30
  s.add_dependency 'uringmachine', '0.16'
31
31
 
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond_on_get("#{req.route[:path]}-#{req.route_params['foo']}")
3
+ }
data/test/helper.rb CHANGED
@@ -6,13 +6,14 @@ require 'uringmachine'
6
6
  require 'syntropy'
7
7
  require 'qeweney/mock_adapter'
8
8
  require 'minitest/autorun'
9
+ require 'fileutils'
9
10
 
10
11
  STDOUT.sync = true
11
12
  STDERR.sync = true
12
13
 
13
14
  module ::Kernel
14
- def mock_req(**args)
15
- Qeweney::MockAdapter.mock(**args)
15
+ def mock_req(headers, body = nil)
16
+ Qeweney::MockAdapter.mock(headers, body).tap { it.setup_mock_request }
16
17
  end
17
18
 
18
19
  def capture_exception
@@ -48,6 +49,21 @@ module ::Kernel
48
49
  def monotonic_clock
49
50
  ::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
50
51
  end
52
+
53
+ def make_tmp_file_tree(dir, spec)
54
+ FileUtils.mkdir(dir) rescue nil
55
+ spec.each do |k, v|
56
+ fn = File.join(dir, k.to_s)
57
+ case v
58
+ when String
59
+ IO.write(fn, v)
60
+ when Hash
61
+ FileUtils.mkdir(fn) rescue nil
62
+ make_tmp_file_tree(fn, v)
63
+ end
64
+ end
65
+ dir
66
+ end
51
67
  end
52
68
 
53
69
  module Minitest::Assertions