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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +9 -0
- data/Rakefile +1 -1
- data/TODO.md +180 -135
- data/bin/syntropy +8 -3
- data/lib/syntropy/app.rb +227 -111
- data/lib/syntropy/errors.rb +40 -12
- data/lib/syntropy/markdown.rb +4 -2
- data/lib/syntropy/module.rb +9 -10
- data/lib/syntropy/request_extensions.rb +112 -2
- data/lib/syntropy/routing_tree.rb +553 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +1 -1
- data/syntropy.gemspec +1 -1
- data/test/app/params/[foo].rb +3 -0
- data/test/helper.rb +18 -2
- data/test/test_app.rb +17 -25
- data/test/test_errors.rb +38 -0
- data/test/test_request_extensions.rb +163 -0
- data/test/test_routing_tree.rb +427 -0
- metadata +8 -6
- data/lib/syntropy/router.rb +0 -245
- data/test/test_router.rb +0 -116
- data/test/test_validation.rb +0 -35
data/lib/syntropy/app.rb
CHANGED
@@ -8,7 +8,9 @@ require 'p2'
|
|
8
8
|
|
9
9
|
require 'syntropy/errors'
|
10
10
|
require 'syntropy/file_watch'
|
11
|
+
|
11
12
|
require 'syntropy/module'
|
13
|
+
require 'syntropy/routing_tree'
|
12
14
|
|
13
15
|
module Syntropy
|
14
16
|
class App
|
@@ -19,150 +21,162 @@ module Syntropy
|
|
19
21
|
|
20
22
|
private
|
21
23
|
|
24
|
+
# for apps with a _site.rb file
|
22
25
|
def site_file_app(opts)
|
23
|
-
site_fn = File.join(opts[:
|
26
|
+
site_fn = File.join(opts[:root_dir], '_site.rb')
|
24
27
|
return nil if !File.file?(site_fn)
|
25
28
|
|
26
|
-
loader = Syntropy::ModuleLoader.new(opts[:
|
29
|
+
loader = Syntropy::ModuleLoader.new(opts[:root_dir], opts)
|
27
30
|
loader.load('_site')
|
28
31
|
end
|
29
32
|
|
33
|
+
# default app
|
30
34
|
def default_app(opts)
|
31
|
-
new(opts
|
35
|
+
new(**opts)
|
32
36
|
end
|
33
37
|
end
|
34
38
|
|
35
|
-
|
36
|
-
|
37
|
-
@
|
39
|
+
attr_reader :module_loader, :routing_tree, :root_dir, :mount_path, :opts
|
40
|
+
def initialize(root_dir:, mount_path:, **opts)
|
41
|
+
@machine = opts[:machine]
|
42
|
+
@root_dir = root_dir
|
38
43
|
@mount_path = mount_path
|
39
44
|
@opts = opts
|
40
45
|
|
41
|
-
@module_loader = Syntropy::ModuleLoader.new(@
|
42
|
-
|
43
|
-
|
44
|
-
@machine.spin do
|
45
|
-
# we do startup stuff asynchronously, in order to first let TP2 do its
|
46
|
-
# setup tasks
|
47
|
-
@machine.sleep 0.15
|
48
|
-
@opts[:logger]&.info(
|
49
|
-
message: "Serving from #{File.expand_path(@location)}"
|
50
|
-
)
|
51
|
-
@router.start_file_watcher if opts[:watch_files]
|
52
|
-
end
|
46
|
+
@module_loader = Syntropy::ModuleLoader.new(@root_dir, opts)
|
47
|
+
setup_routing_tree
|
48
|
+
start_app
|
53
49
|
end
|
54
50
|
|
51
|
+
# Processes an incoming HTTP request. Requests are processed by first
|
52
|
+
# looking up the route for the request path, then calling the route proc. If
|
53
|
+
# the route proc is not set, it is computed according to the route target,
|
54
|
+
# and composed recursively into hooks encountered up the routing tree.
|
55
|
+
#
|
56
|
+
# Normal exceptions (StandardError and descendants) are trapped and passed
|
57
|
+
# to route's error handler. If no such handler is found, the default error
|
58
|
+
# handler is used, which simply generates a textual response containing the
|
59
|
+
# error message, and with the appropriate HTTP status code, according to the
|
60
|
+
# type of error.
|
61
|
+
#
|
62
|
+
# @param req [Qeweney::Request] HTTP request
|
63
|
+
# @return [void]
|
55
64
|
def call(req)
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
65
|
+
route = @router_proc.(req.path, req.route_params)
|
66
|
+
raise Syntropy::Error.not_found('Not found') if !route
|
67
|
+
|
68
|
+
req.route = route
|
69
|
+
proc = route[:proc] ||= compute_route_proc(route)
|
70
|
+
proc.(req)
|
61
71
|
rescue StandardError => e
|
62
|
-
|
63
|
-
|
64
|
-
req.respond(e.message, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
72
|
+
error_handler = get_error_handler(route)
|
73
|
+
error_handler.(req, e)
|
65
74
|
end
|
66
75
|
|
67
76
|
private
|
68
77
|
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
78
|
+
# Instantiates a routing tree with the app settings, and generates a router
|
79
|
+
# proc.
|
80
|
+
#
|
81
|
+
# @return [void]
|
82
|
+
def setup_routing_tree
|
83
|
+
@routing_tree = Syntropy::RoutingTree.new(
|
84
|
+
root_dir: @root_dir, mount_path: @mount_path, **@opts
|
85
|
+
)
|
86
|
+
@router_proc = @routing_tree.router_proc
|
75
87
|
end
|
76
88
|
|
77
|
-
|
78
|
-
|
79
|
-
|
89
|
+
# Computes the route proc for the given route, wrapping it in hooks found up
|
90
|
+
# the routing tree.
|
91
|
+
#
|
92
|
+
# @param route [Hash] route entry
|
93
|
+
# @return [Proc] route proc
|
94
|
+
def compute_route_proc(route)
|
95
|
+
pure = pure_route_proc(route)
|
96
|
+
compose_up_tree_hooks(route, pure)
|
80
97
|
end
|
81
98
|
|
82
|
-
def
|
83
|
-
case
|
99
|
+
def pure_route_proc(route)
|
100
|
+
case (kind = route[:target][:kind])
|
84
101
|
when :static
|
85
|
-
|
102
|
+
static_route_proc(route)
|
86
103
|
when :markdown
|
87
|
-
|
104
|
+
markdown_route_proc(route)
|
88
105
|
when :module
|
89
|
-
|
106
|
+
module_route_proc(route)
|
90
107
|
else
|
91
|
-
raise
|
108
|
+
raise Syntropy::Error, "Invalid route kind: #{kind.inspect}"
|
92
109
|
end
|
93
110
|
end
|
94
111
|
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
req.respond(nil, headers)
|
100
|
-
else
|
101
|
-
req.respond('Not found', headers)
|
102
|
-
end
|
103
|
-
end
|
112
|
+
# Returns a proc rendering the given static route
|
113
|
+
def static_route_proc(route)
|
114
|
+
fn = route[:target][:fn]
|
115
|
+
headers = { 'Content-Type' => Qeweney::MimeTypes[File.extname(fn)] }
|
104
116
|
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
)
|
117
|
+
->(req) {
|
118
|
+
req.respond_by_http_method(
|
119
|
+
'head' => [nil, headers],
|
120
|
+
'get' => -> { [IO.read(fn), headers] }
|
121
|
+
)
|
122
|
+
}
|
112
123
|
end
|
113
124
|
|
114
|
-
|
115
|
-
|
116
|
-
headers = { 'Content-Type' =>
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
125
|
+
# Returns a proc rendering the given markdown route
|
126
|
+
def markdown_route_proc(route)
|
127
|
+
headers = { 'Content-Type' => 'text/html' }
|
128
|
+
|
129
|
+
->(req) {
|
130
|
+
req.respond_by_http_method(
|
131
|
+
'head' => [nil, headers],
|
132
|
+
'get' => -> { [render_markdown(route), headers] }
|
133
|
+
)
|
134
|
+
}
|
121
135
|
end
|
122
136
|
|
123
|
-
def
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
137
|
+
def render_markdown(route)
|
138
|
+
atts, md = Syntropy.parse_markdown_file(route[:target][:fn], @opts)
|
139
|
+
|
140
|
+
if (layout = atts[:layout])
|
141
|
+
route[:applied_layouts] ||= {}
|
142
|
+
proc = route[:applied_layouts][layout] ||= markdown_layout_proc(layout)
|
143
|
+
html = proc.render(md: md, **atts)
|
144
|
+
else
|
145
|
+
html = P2.markdown(md)
|
128
146
|
end
|
147
|
+
html
|
148
|
+
end
|
129
149
|
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
p e.backtrace
|
136
|
-
req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
150
|
+
# returns a markdown template based on the given layout
|
151
|
+
def markdown_layout_proc(layout)
|
152
|
+
@layouts ||= {}
|
153
|
+
template = @module_loader.load("_layout/#{layout}")
|
154
|
+
@layouts[layout] = template.apply { |md:, **| markdown(md) }
|
137
155
|
end
|
138
156
|
|
139
|
-
def
|
140
|
-
ref =
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
@opts[:logger]&.error(
|
145
|
-
message: "Error while loading module #{ref}",
|
146
|
-
error: e
|
147
|
-
)
|
148
|
-
:invalid
|
157
|
+
def module_route_proc(route)
|
158
|
+
ref = @routing_tree.fn_to_rel_path(route[:target][:fn])
|
159
|
+
# ref = route[:target][:fn].sub(@mount_path, '')
|
160
|
+
mod = @module_loader.load(ref)
|
161
|
+
compute_module_proc(mod)
|
149
162
|
end
|
150
163
|
|
151
|
-
def
|
164
|
+
def compute_module_proc(mod)
|
152
165
|
case mod
|
153
166
|
when P2::Template
|
154
|
-
|
167
|
+
p2_template_proc(mod)
|
155
168
|
when Papercraft::Template
|
156
|
-
|
169
|
+
papercraft_template_proc(mod)
|
157
170
|
else
|
158
171
|
mod
|
159
172
|
end
|
160
173
|
end
|
161
174
|
|
162
|
-
def
|
163
|
-
template =
|
164
|
-
|
165
|
-
|
175
|
+
def p2_template_proc(template)
|
176
|
+
template = template.proc
|
177
|
+
headers = { 'Content-Type' => 'text/html' }
|
178
|
+
|
179
|
+
->(req) {
|
166
180
|
req.respond_by_http_method(
|
167
181
|
'head' => [nil, headers],
|
168
182
|
'get' => -> { [template.render, headers] }
|
@@ -170,36 +184,138 @@ module Syntropy
|
|
170
184
|
}
|
171
185
|
end
|
172
186
|
|
173
|
-
def
|
174
|
-
lambda { |req|
|
187
|
+
def papercraft_template_proc(template)
|
175
188
|
headers = { 'Content-Type' => template.mime_type }
|
189
|
+
->(req) {
|
176
190
|
req.respond_by_http_method(
|
177
191
|
'head' => [nil, headers],
|
178
192
|
'get' => -> { [template.render, headers] }
|
179
193
|
)
|
180
194
|
}
|
181
|
-
|
182
195
|
end
|
183
196
|
|
184
|
-
|
185
|
-
|
197
|
+
# Composes the given proc into up tree hooks, recursively. Hooks have the
|
198
|
+
# signature `->(req, proc) { ... }` where proc is the pure route proc. Each
|
199
|
+
# hook therefore can decide whether to_ respond itself to the request, pass
|
200
|
+
# in additional parameters, perform any other kind of modification on the
|
201
|
+
# incoming reuqest, or capture the response from the route proc and modify
|
202
|
+
# it.
|
203
|
+
#
|
204
|
+
# Nested hooks will be invoked from the routing tree root down. For example
|
205
|
+
# `/site/_hook.rb` will wrap `/site/admin/_hook.rb` which wraps the route at
|
206
|
+
# `/site/admin/users.rb`.
|
207
|
+
#
|
208
|
+
# @param route [Hash] route entry
|
209
|
+
# @param proc [Proc] route proc
|
210
|
+
def compose_up_tree_hooks(route, proc)
|
211
|
+
hook_spec = route[:hook]
|
212
|
+
if hook_spec
|
213
|
+
orig_proc = proc
|
214
|
+
hook_proc = hook_spec[:proc] ||= load_aux_module(hook_spec)
|
215
|
+
proc = ->(req) { hook_proc.(req, orig_proc) }
|
216
|
+
end
|
186
217
|
|
187
|
-
|
188
|
-
|
189
|
-
|
190
|
-
|
191
|
-
|
192
|
-
|
218
|
+
(parent = route[:parent]) ? compose_up_tree_hooks(parent, proc) : proc
|
219
|
+
end
|
220
|
+
|
221
|
+
def load_aux_module(hook_spec)
|
222
|
+
ref = @routing_tree.fn_to_rel_path(hook_spec[:fn])
|
223
|
+
@module_loader.load(ref)
|
224
|
+
end
|
225
|
+
|
226
|
+
DEFAULT_ERROR_HANDLER = ->(req, err) {
|
227
|
+
msg = err.message
|
228
|
+
msg = nil if msg.empty? || (req.method == 'head')
|
229
|
+
req.respond(msg, ':status' => Syntropy::Error.http_status(err))
|
230
|
+
}
|
231
|
+
|
232
|
+
# Returns an error handler for the given route. If route is nil, looks up
|
233
|
+
# the error handler for the routing tree root. If no handler is found,
|
234
|
+
# returns the default error handler.
|
235
|
+
#
|
236
|
+
# @param route [Hash] route entry
|
237
|
+
# @return [Proc] error handler proc
|
238
|
+
def get_error_handler(route)
|
239
|
+
route_error_handler(route || @routing_tree.root) || DEFAULT_ERROR_HANDLER
|
240
|
+
end
|
241
|
+
|
242
|
+
# Returns the given route's error handler, caching the result.
|
243
|
+
#
|
244
|
+
# @param route [Hash] route entry
|
245
|
+
# @return [Proc] error handler proc
|
246
|
+
def route_error_handler(route)
|
247
|
+
route[:error_handler] ||= compute_error_handler(route)
|
248
|
+
end
|
249
|
+
|
250
|
+
# Finds and loads the error handler for the given route.
|
251
|
+
#
|
252
|
+
# @param route [Hash] route entry
|
253
|
+
# @return [Proc, nil] error handler proc or nil
|
254
|
+
def compute_error_handler(route)
|
255
|
+
error_target = find_error_handler(route)
|
256
|
+
return nil if !error_target
|
257
|
+
|
258
|
+
load_aux_module(error_target)
|
259
|
+
end
|
260
|
+
|
261
|
+
# Finds the closest error handler for the given route. If no error handler
|
262
|
+
# is defined for the route, searches for an error handler up the routing
|
263
|
+
# tree.
|
264
|
+
#
|
265
|
+
# @param route [Hash] route entry
|
266
|
+
# @return [Hash, nil] error handler target or nil
|
267
|
+
def find_error_handler(route)
|
268
|
+
return route[:error] if route[:error]
|
269
|
+
|
270
|
+
route[:parent] && find_error_handler(route[:parent])
|
271
|
+
end
|
272
|
+
|
273
|
+
# Performs app start up, creating a log message and starting the file
|
274
|
+
# watcher according to app options.
|
275
|
+
#
|
276
|
+
# @return [void]
|
277
|
+
def start_app
|
278
|
+
@machine.spin do
|
279
|
+
# we do startup stuff asynchronously, in order to first let TP2 do its
|
280
|
+
# setup tasks
|
281
|
+
@machine.sleep 0.2
|
282
|
+
@opts[:logger]&.info(
|
283
|
+
message: "Serving from #{File.expand_path(@location)}"
|
284
|
+
)
|
285
|
+
file_watcher_loop if opts[:watch_files]
|
193
286
|
end
|
194
|
-
html
|
195
287
|
end
|
196
288
|
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
289
|
+
# Runs the file watcher loop. When a file change is encountered, invalidates
|
290
|
+
# the corresponding module, and triggers recomputation of the routing tree.
|
291
|
+
#
|
292
|
+
# @return [void]
|
293
|
+
def file_watcher_loop
|
294
|
+
wf = @opts[:watch_files]
|
295
|
+
period = wf.is_a?(Numeric) ? wf : 0.1
|
296
|
+
Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
|
297
|
+
@module_loader.invalidate(fn)
|
298
|
+
debounce_file_change
|
299
|
+
end
|
300
|
+
rescue Exception => e
|
301
|
+
p e
|
302
|
+
p e.backtrace
|
303
|
+
exit!
|
304
|
+
end
|
305
|
+
|
306
|
+
# Delays responding to a file change, then reloads the routing tree.
|
307
|
+
#
|
308
|
+
# @return [void]
|
309
|
+
def debounce_file_change
|
310
|
+
if @routing_tree_reloader
|
311
|
+
@machine.schedule(@routing_tree_reloader, UM::Terminate.new)
|
312
|
+
end
|
313
|
+
|
314
|
+
@routing_tree_reloader = @machine.spin do
|
315
|
+
@machine.sleep(0.1)
|
316
|
+
setup_routing_tree
|
317
|
+
@routing_tree_reloader = nil
|
318
|
+
end
|
203
319
|
end
|
204
320
|
end
|
205
321
|
end
|
data/lib/syntropy/errors.rb
CHANGED
@@ -3,29 +3,57 @@
|
|
3
3
|
require 'qeweney'
|
4
4
|
|
5
5
|
module Syntropy
|
6
|
+
# The base Syntropy error class
|
6
7
|
class Error < StandardError
|
7
8
|
Status = Qeweney::Status
|
8
9
|
|
10
|
+
# By default, the HTTP status for errors is 500 Internal Server Error
|
11
|
+
DEFAULT_STATUS = Qeweney::Status::INTERNAL_SERVER_ERROR
|
12
|
+
|
13
|
+
# Returns the HTTP status for the given exception
|
14
|
+
#
|
15
|
+
# @param err [Exception] exception
|
16
|
+
# @return [Integer, String] HTTP status
|
17
|
+
def self.http_status(err)
|
18
|
+
err.respond_to?(:http_status) ? err.http_status : DEFAULT_STATUS
|
19
|
+
end
|
20
|
+
|
21
|
+
# Creates an error with status 404 Not Found
|
22
|
+
#
|
23
|
+
# @return [Syntropy::Error]
|
24
|
+
def self.not_found(msg = '') = new(Status::NOT_FOUND, msg)
|
25
|
+
|
26
|
+
# Creates an error with status 405 Method Not Allowed
|
27
|
+
#
|
28
|
+
# @return [Syntropy::Error]
|
29
|
+
def self.method_not_allowed(msg = '') = new(Status::METHOD_NOT_ALLOWED, msg)
|
30
|
+
|
31
|
+
# Creates an error with status 418 I'm a teapot
|
32
|
+
#
|
33
|
+
# @return [Syntropy::Error]
|
34
|
+
def self.teapot(msg = '') = new(Status::TEAPOT, msg)
|
35
|
+
|
9
36
|
attr_reader :http_status
|
10
37
|
|
11
|
-
|
38
|
+
# Initializes a Syntropy error with the given HTTP status and message.
|
39
|
+
#
|
40
|
+
# @param http_status [Integer, String] HTTP status
|
41
|
+
# @param msg [String] error message
|
42
|
+
# @return [void]
|
43
|
+
def initialize(http_status = DEFAULT_STATUS, msg = '')
|
12
44
|
super(msg)
|
13
|
-
@http_status =
|
45
|
+
@http_status = http_status
|
14
46
|
end
|
15
47
|
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
teapot: Status::TEAPOT
|
22
|
-
}
|
23
|
-
.each { |k, v|
|
24
|
-
define_method(k) { |msg = ''| new(v, msg) }
|
25
|
-
}
|
48
|
+
# Returns the HTTP status for the error.
|
49
|
+
#
|
50
|
+
# @return [Integer, String] HTTP status
|
51
|
+
def http_status
|
52
|
+
@http_status || Qeweney::Status::INTERNAL_SERVER_ERROR
|
26
53
|
end
|
27
54
|
end
|
28
55
|
|
56
|
+
# ValidationError is raised when a validation has failed.
|
29
57
|
class ValidationError < Error
|
30
58
|
def initialize(msg)
|
31
59
|
super(Qeweney::Status::BAD_REQUEST, msg)
|
data/lib/syntropy/markdown.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'yaml'
|
4
|
+
|
3
5
|
module Syntropy
|
4
6
|
DATE_REGEXP = /(\d{4}-\d{2}-\d{2})/
|
5
7
|
FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
|
@@ -28,9 +30,9 @@ module Syntropy
|
|
28
30
|
atts = atts.merge(yaml)
|
29
31
|
end
|
30
32
|
|
31
|
-
if opts[:
|
33
|
+
if opts[:root_dir]
|
32
34
|
atts[:url] = path
|
33
|
-
.gsub(/#{opts[:
|
35
|
+
.gsub(/#{opts[:root_dir]}/, '')
|
34
36
|
.gsub(/\.md$/, '')
|
35
37
|
end
|
36
38
|
|
data/lib/syntropy/module.rb
CHANGED
@@ -36,7 +36,6 @@ module Syntropy
|
|
36
36
|
mod_ctx.module_eval(mod_body, fn, 1)
|
37
37
|
|
38
38
|
export_value = mod_ctx.__export_value__
|
39
|
-
|
40
39
|
wrap_module(mod_ctx, export_value)
|
41
40
|
end
|
42
41
|
|
@@ -85,20 +84,20 @@ module Syntropy
|
|
85
84
|
def template(proc = nil, &block)
|
86
85
|
proc ||= block
|
87
86
|
raise "No template block/proc given" if !proc
|
88
|
-
|
87
|
+
|
89
88
|
P2::Template.new(proc)
|
90
89
|
end
|
91
90
|
alias_method :html, :template
|
92
91
|
|
93
92
|
def route_by_host(map = nil)
|
94
|
-
root = @env[:
|
93
|
+
root = @env[:root_dir]
|
95
94
|
sites = Dir[File.join(root, '*')]
|
96
95
|
.reject { File.basename(it) =~ /^_/ }
|
97
96
|
.select { File.directory?(it) }
|
98
97
|
.each_with_object({}) { |fn, h|
|
99
98
|
name = File.basename(fn)
|
100
|
-
opts = @env.merge(
|
101
|
-
h[name] = Syntropy::App.new(opts
|
99
|
+
opts = @env.merge(root_dir: fn)
|
100
|
+
h[name] = Syntropy::App.new(**opts)
|
102
101
|
}
|
103
102
|
|
104
103
|
map&.each do |k, v|
|
@@ -112,7 +111,7 @@ module Syntropy
|
|
112
111
|
end
|
113
112
|
|
114
113
|
def page_list(ref)
|
115
|
-
full_path = File.join(@env[:
|
114
|
+
full_path = File.join(@env[:root_dir], ref)
|
116
115
|
raise 'Not a directory' if !File.directory?(full_path)
|
117
116
|
|
118
117
|
Dir[File.join(full_path, '*.md')].sort.map {
|
@@ -121,11 +120,11 @@ module Syntropy
|
|
121
120
|
}
|
122
121
|
end
|
123
122
|
|
124
|
-
def app(
|
125
|
-
|
123
|
+
def app(root_dir = nil, mount_path = nil)
|
124
|
+
root_dir ||= @env[:root_dir]
|
126
125
|
mount_path ||= @env[:mount_path]
|
127
|
-
opts = @env.merge(
|
128
|
-
Syntropy::App.new(opts
|
126
|
+
opts = @env.merge(root_dir:, mount_path:)
|
127
|
+
Syntropy::App.new(**opts)
|
129
128
|
end
|
130
129
|
end
|
131
130
|
end
|