syntropy 0.14 → 0.15.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +4 -0
- data/TODO.md +5 -3
- data/bin/syntropy +3 -0
- data/lib/syntropy/app.rb +3 -2
- data/lib/syntropy/module.rb +131 -13
- data/lib/syntropy/routing_tree.rb +2 -28
- data/lib/syntropy/utils.rb +4 -4
- data/lib/syntropy/version.rb +1 -1
- data/test/app/_lib/dep.rb +7 -0
- data/test/test_module.rb +10 -0
- data/test/test_routing_tree.rb +0 -52
- metadata +2 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ef2840797122cef8e0f7f7675c10665c35f60915e091043416ffd0ed70e1cb5c
|
4
|
+
data.tar.gz: 0d929bfbe0d629879b05c57b7c01c1e8c0ee1af148dc23570c30233f19e81156
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 6375f3fb5cbab65d6b710b613c32d0eb9ebb714880f0eb3921fd0b07da9219e0ab94faec2398ae2f33a21fa4f21a8135416faf5b6f2ad0721a99afa067b854f7
|
7
|
+
data.tar.gz: 715274f642dc3b0db72b0ec37526f26221b56930e19eb324f804408b18552a5c465d59f313cd8329a24a9caa0aa05abaa6907d523f15e1e7061e3e9e1bf1762c
|
data/CHANGELOG.md
CHANGED
data/TODO.md
CHANGED
@@ -1,5 +1,7 @@
|
|
1
|
+
## Immediate
|
2
|
+
|
1
3
|
- [ ] Collection - treat directories and files as collections of data.
|
2
|
-
|
4
|
+
|
3
5
|
Kind of similar to the routing tree, but instead of routes it just takes a
|
4
6
|
bunch of files and turns it into a dataset. Each directory is a "table" and is
|
5
7
|
composed of zero or more files that form rows in the table. Supported file
|
@@ -15,9 +17,9 @@
|
|
15
17
|
Articles = @app.collection('_articles/*.md')
|
16
18
|
article = Articles.last_by(&:date)
|
17
19
|
|
18
|
-
article.title #=>
|
20
|
+
article.title #=>
|
19
21
|
article.date #=>
|
20
|
-
article.layout #=>
|
22
|
+
article.layout #=>
|
21
23
|
article.render_proc #=> (load layout, apply article)
|
22
24
|
article.render #=> (render to HTML)
|
23
25
|
...
|
data/bin/syntropy
CHANGED
@@ -69,5 +69,8 @@ env[:banner] = false
|
|
69
69
|
env[:machine] = Syntropy.machine = UM.new
|
70
70
|
env[:logger] = env[:logger] && TP2::Logger.new(env[:machine], **env)
|
71
71
|
|
72
|
+
require 'syntropy/version'
|
73
|
+
|
74
|
+
env[:logger]&.info(message: "Running Syntropy version #{Syntropy::VERSION}")
|
72
75
|
app = Syntropy::App.load(env)
|
73
76
|
TP2.run(env) { app.call(it) }
|
data/lib/syntropy/app.rb
CHANGED
@@ -37,7 +37,7 @@ module Syntropy
|
|
37
37
|
end
|
38
38
|
|
39
39
|
attr_reader :module_loader, :routing_tree, :root_dir, :mount_path, :env
|
40
|
-
|
40
|
+
|
41
41
|
def initialize(**env)
|
42
42
|
@machine = env[:machine]
|
43
43
|
@root_dir = File.expand_path(env[:root_dir])
|
@@ -302,7 +302,8 @@ module Syntropy
|
|
302
302
|
wf = @env[:watch_files]
|
303
303
|
period = wf.is_a?(Numeric) ? wf : 0.1
|
304
304
|
Syntropy.file_watch(@machine, @root_dir, period: period) do |event, fn|
|
305
|
-
@
|
305
|
+
@env[:logger]&.info(message: "File change detected", fn: fn)
|
306
|
+
@module_loader.invalidate_fn(fn)
|
306
307
|
debounce_file_change
|
307
308
|
end
|
308
309
|
rescue Exception => e
|
data/lib/syntropy/module.rb
CHANGED
@@ -3,39 +3,118 @@
|
|
3
3
|
require 'p2'
|
4
4
|
|
5
5
|
module Syntropy
|
6
|
+
# The ModuleLoader class implemenets a module loader. It handles loading of
|
7
|
+
# modules, tracking of dependencies between modules, and invalidation of
|
8
|
+
# loaded modules (following a change to the module file).
|
9
|
+
#
|
10
|
+
# A module may implement a route endpoint, a layout template, utility methods,
|
11
|
+
# classes, or any other functionality needed by the web app.
|
12
|
+
#
|
13
|
+
# Modules are Ruby files that can import other modules as dependencies. A
|
14
|
+
# module must export a single value, which can be a class, a template, a proc,
|
15
|
+
# or any other Ruby object. A module can also export itself by calling `export
|
16
|
+
# self`.
|
17
|
+
#
|
18
|
+
# Modules are referenced relative to the web app's root directory, without the
|
19
|
+
# `.rb` extension. For example, for a site residing in `/my_site`, the
|
20
|
+
# reference `_lib/foo` will point to a module residing in
|
21
|
+
# `/my_site/_lib/foo.rb`.
|
6
22
|
class ModuleLoader
|
23
|
+
attr_reader :modules
|
24
|
+
|
25
|
+
# Instantiates a module loader
|
26
|
+
#
|
27
|
+
# @param env [Hash] environment hash
|
28
|
+
# @return [void]
|
7
29
|
def initialize(env)
|
8
30
|
@root_dir = env[:root_dir]
|
9
31
|
@env = env
|
10
|
-
@
|
32
|
+
@modules = {} # maps ref to module entry
|
11
33
|
@fn_map = {} # maps filename to ref
|
12
34
|
end
|
13
35
|
|
36
|
+
# Loads a module (if not already loaded) and returns its export value.
|
37
|
+
#
|
38
|
+
# @param ref [String] module reference
|
39
|
+
# @return [any] export value
|
14
40
|
def load(ref)
|
15
|
-
@
|
41
|
+
entry = (@modules[ref] ||= load_module(ref))
|
42
|
+
entry[:export_value]
|
16
43
|
end
|
17
44
|
|
18
|
-
|
45
|
+
# Invalidates a module by its filename, normally following a change to the
|
46
|
+
# underlying file (in order to cause reloading of the module). The module
|
47
|
+
# will be removed from the modules map, as well as modules dependending on
|
48
|
+
# it.
|
49
|
+
#
|
50
|
+
# @param fn [String] module filename
|
51
|
+
# @return [void]
|
52
|
+
def invalidate_fn(fn)
|
19
53
|
ref = @fn_map[fn]
|
20
54
|
return if !ref
|
21
55
|
|
22
|
-
|
23
|
-
@fn_map.delete(fn)
|
56
|
+
invalidate_ref(ref)
|
24
57
|
end
|
25
58
|
|
26
59
|
private
|
27
60
|
|
61
|
+
# Invalidates a module by its reference, normally following a change to the
|
62
|
+
# underlying file (in order to cause reloading of the module). The module
|
63
|
+
# will be removed from the modules map, as well as modules dependending on
|
64
|
+
# it.
|
65
|
+
#
|
66
|
+
# @param ref [String] module reference
|
67
|
+
# @return [void]
|
68
|
+
def invalidate_ref(ref)
|
69
|
+
entry = @modules.delete(ref)
|
70
|
+
return if !entry
|
71
|
+
|
72
|
+
@fn_map.delete(entry[:fn])
|
73
|
+
entry[:reverse_deps].each { invalidate_ref(it) }
|
74
|
+
end
|
75
|
+
|
76
|
+
# Registers reverse dependencies for the given module reference.
|
77
|
+
#
|
78
|
+
# @param ref [String] module reference
|
79
|
+
# @param deps [Array<String>] array of dependencies for the given module
|
80
|
+
# @return [void]
|
81
|
+
def add_dependencies(ref, deps)
|
82
|
+
deps.each do
|
83
|
+
entry = @modules[it]
|
84
|
+
next if !entry
|
85
|
+
|
86
|
+
entry[:reverse_deps] << ref
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
# Loads a module and returns a module entry. Any dependencies (using
|
91
|
+
# `import`) are loaded as well.
|
92
|
+
#
|
93
|
+
# @param ref [String] module reference
|
94
|
+
# @return [Hash] module entry
|
28
95
|
def load_module(ref)
|
29
96
|
fn = File.expand_path(File.join(@root_dir, "#{ref}.rb"))
|
30
|
-
@fn_map[fn] = ref
|
31
97
|
raise Syntropy::Error, "File not found #{fn}" if !File.file?(fn)
|
32
98
|
|
99
|
+
@fn_map[fn] = ref
|
33
100
|
code = IO.read(fn)
|
34
101
|
env = @env.merge(module_loader: self, ref: ref)
|
35
|
-
|
36
|
-
|
102
|
+
m = Syntropy::Module.load(env, code, fn)
|
103
|
+
add_dependencies(ref, m.__dependencies__)
|
104
|
+
export_value = transform_module_export_value(m.__export_value__)
|
105
|
+
|
106
|
+
{
|
107
|
+
fn: fn,
|
108
|
+
export_value: export_value,
|
109
|
+
reverse_deps: []
|
110
|
+
}
|
37
111
|
end
|
38
112
|
|
113
|
+
# Transforms the given export value. If the value is nil, an exception is
|
114
|
+
# raised.
|
115
|
+
#
|
116
|
+
# @param export_value [any] module's export value
|
117
|
+
# @return [any] transformed value
|
39
118
|
def transform_module_export_value(export_value)
|
40
119
|
case export_value
|
41
120
|
when nil
|
@@ -50,13 +129,16 @@ module Syntropy
|
|
50
129
|
end
|
51
130
|
end
|
52
131
|
|
132
|
+
# The Syntropy::Module class implements a reloadable module. A module is a
|
133
|
+
# `.rb` source file that implements a route endpoint, a template, utility
|
134
|
+
# methods or any other functionality needed by the web app.
|
53
135
|
class Module
|
136
|
+
# Loads a module, returning the module instance
|
54
137
|
def self.load(env, code, fn)
|
55
138
|
m = new(**env)
|
56
139
|
m.instance_eval(code, fn)
|
57
|
-
export_value = m.__export_value__
|
58
140
|
env[:logger]&.info(message: "Loaded module at #{fn}")
|
59
|
-
|
141
|
+
m
|
60
142
|
rescue StandardError => e
|
61
143
|
env[:logger]&.error(
|
62
144
|
message: "Error while loading module #{fn}",
|
@@ -65,7 +147,10 @@ module Syntropy
|
|
65
147
|
raise
|
66
148
|
end
|
67
149
|
|
68
|
-
|
150
|
+
# Initializes a module with the given environment hash.
|
151
|
+
#
|
152
|
+
# @param env [Hash] environment hash
|
153
|
+
# @return [void]
|
69
154
|
def initialize(**env)
|
70
155
|
@env = env
|
71
156
|
@machine = env[:machine]
|
@@ -74,16 +159,42 @@ module Syntropy
|
|
74
159
|
@ref = env[:ref]
|
75
160
|
singleton_class.const_set(:MODULE, self)
|
76
161
|
end
|
77
|
-
|
162
|
+
|
78
163
|
attr_reader :__export_value__
|
164
|
+
|
165
|
+
# Exports the given value. This value will be used as the module's
|
166
|
+
# entrypoint. It can be any Ruby value, but for a route module would
|
167
|
+
# normally be a proc.
|
168
|
+
#
|
169
|
+
# @param v [any] export value
|
170
|
+
# @return [void]
|
79
171
|
def export(v)
|
80
172
|
@__export_value__ = v
|
81
173
|
end
|
82
174
|
|
175
|
+
# Returns the list of module references imported by the module.
|
176
|
+
#
|
177
|
+
# @return [Array] array of module references
|
178
|
+
def __dependencies__
|
179
|
+
@__dependencies__ ||= []
|
180
|
+
end
|
181
|
+
|
182
|
+
# Imports the module corresponding to the given reference. The return value
|
183
|
+
# is the module's export value.
|
184
|
+
#
|
185
|
+
# @param ref [String] module reference
|
186
|
+
# @return [any] loaded dependency's export value
|
83
187
|
def import(ref)
|
84
|
-
@module_loader.load(ref)
|
188
|
+
@module_loader.load(ref).tap {
|
189
|
+
__dependencies__ << ref
|
190
|
+
}
|
85
191
|
end
|
86
192
|
|
193
|
+
# Creates and returns a P2 template created with the given block.
|
194
|
+
#
|
195
|
+
# @param proc [Proc, nil] template proc or nil
|
196
|
+
# @param block [Proc] template block
|
197
|
+
# @return [P2::Template] template
|
87
198
|
def template(proc = nil, &block)
|
88
199
|
proc ||= block
|
89
200
|
raise "No template block/proc given" if !proc
|
@@ -91,10 +202,17 @@ module Syntropy
|
|
91
202
|
P2::Template.new(proc)
|
92
203
|
end
|
93
204
|
|
205
|
+
# Returns a list of pages found at the given ref.
|
206
|
+
#
|
207
|
+
# @param ref [String] directory reference
|
208
|
+
# @return [Array] array of pages found in directory
|
94
209
|
def page_list(ref)
|
95
210
|
Syntropy.page_list(@env, ref)
|
96
211
|
end
|
97
212
|
|
213
|
+
# Creates and returns a Syntropy app for the given environment.
|
214
|
+
#
|
215
|
+
# @param env [Hash] environment
|
98
216
|
def app(**env)
|
99
217
|
Syntropy::App.new(**(@env.merge(env)))
|
100
218
|
end
|
@@ -66,32 +66,6 @@ module Syntropy
|
|
66
66
|
@router_proc ||= compile_router_proc
|
67
67
|
end
|
68
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
69
|
# Computes a "clean" URL path for the given path. Modules and markdown are
|
96
70
|
# stripped of their extensions, and index file paths are also converted to the
|
97
71
|
# containing directory path. For example, the clean URL path for `/foo/bar.rb`
|
@@ -233,7 +207,7 @@ module Syntropy
|
|
233
207
|
case
|
234
208
|
when (m = fn.match(/\/index(\+)?(\.(?:rb|md))$/))
|
235
209
|
make_index_module_route(m:, parent:, path: abs_path, fn:)
|
236
|
-
|
210
|
+
|
237
211
|
# index.html
|
238
212
|
when fn.match(/\/index\.html$/)
|
239
213
|
set_index_route_target(parent:, path: abs_path, kind: :static, fn:)
|
@@ -256,7 +230,7 @@ module Syntropy
|
|
256
230
|
# (modules or markdown) files) are applied as targets to the immediate
|
257
231
|
# containing directory. A + suffix indicates this route handles requests to
|
258
232
|
# its subtree
|
259
|
-
#
|
233
|
+
#
|
260
234
|
# @param m [MatchData] path match data
|
261
235
|
# @param parent [Hash] parent route entry
|
262
236
|
# @param path [String] route path
|
data/lib/syntropy/utils.rb
CHANGED
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
|
-
|
2
|
+
|
3
3
|
module Syntropy
|
4
4
|
# Utilities for use in modules
|
5
5
|
module Utilities
|
6
|
-
# Returns a request handler that routes request according to
|
6
|
+
# Returns a request handler that routes request according to
|
7
7
|
def route_by_host(env, map = nil)
|
8
8
|
root = env[:root_dir]
|
9
9
|
sites = Dir[File.join(root, '*')]
|
@@ -17,7 +17,7 @@ module Syntropy
|
|
17
17
|
# copy over map refs
|
18
18
|
map&.each { |k, v| sites[k] = sites[v] }
|
19
19
|
|
20
|
-
#
|
20
|
+
#
|
21
21
|
lambda { |req|
|
22
22
|
site = sites[req.host]
|
23
23
|
site ? site.call(req) : req.respond(nil, ':status' => Status::BAD_REQUEST)
|
@@ -36,6 +36,6 @@ module Syntropy
|
|
36
36
|
|
37
37
|
def app(**env)
|
38
38
|
Syntropy::App.new(**env)
|
39
|
-
end
|
39
|
+
end
|
40
40
|
end
|
41
41
|
end
|
data/lib/syntropy/version.rb
CHANGED
data/test/test_module.rb
CHANGED
@@ -44,4 +44,14 @@ class ModuleTest < Minitest::Test
|
|
44
44
|
assert_equal @loader, mod.module_loader
|
45
45
|
assert_equal 42, mod.app
|
46
46
|
end
|
47
|
+
|
48
|
+
def test_dependency_invalidation
|
49
|
+
mod = @loader.load('_lib/dep')
|
50
|
+
assert_equal ['_lib/self', '_lib/dep'], @loader.modules.keys
|
51
|
+
|
52
|
+
self_fn = @loader.modules['_lib/self'][:fn]
|
53
|
+
@loader.invalidate_fn(self_fn)
|
54
|
+
|
55
|
+
assert_equal [], @loader.modules.keys
|
56
|
+
end
|
47
57
|
end
|
data/test/test_routing_tree.rb
CHANGED
@@ -288,58 +288,6 @@ class RoutingTreeTest < Minitest::Test
|
|
288
288
|
assert_equal 'foo', params['id']
|
289
289
|
end
|
290
290
|
|
291
|
-
def test_route_error_handler
|
292
|
-
e = @rt.dynamic_map['/docs/[org]']
|
293
|
-
target = @rt.route_error_handler(e)
|
294
|
-
assert_kind_of Hash, target
|
295
|
-
assert_equal :module, target[:kind]
|
296
|
-
assert_equal File.join(@rt.root_dir, '_error.rb'), target[:fn]
|
297
|
-
|
298
|
-
e = @rt.dynamic_map['/docs/api+']
|
299
|
-
target = @rt.route_error_handler(e)
|
300
|
-
assert_equal :module, target[:kind]
|
301
|
-
assert_equal File.join(@rt.root_dir, '_error.rb'), target[:fn]
|
302
|
-
|
303
|
-
e = @rt.dynamic_map['/docs/[org]/[repo]']
|
304
|
-
target = @rt.route_error_handler(e)
|
305
|
-
assert_equal :module, target[:kind]
|
306
|
-
assert_equal File.join(@rt.root_dir, '[org]/[repo]/_error.rb'), target[:fn]
|
307
|
-
end
|
308
|
-
|
309
|
-
def test_route_hooks
|
310
|
-
e = @rt.dynamic_map['/docs/[org]']
|
311
|
-
hooks = @rt.route_hooks(e)
|
312
|
-
assert_equal [
|
313
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '_hook.rb') }
|
314
|
-
], hooks
|
315
|
-
|
316
|
-
e = @rt.dynamic_map['/docs/api+']
|
317
|
-
hooks = @rt.route_hooks(e)
|
318
|
-
assert_equal [
|
319
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '_hook.rb') }
|
320
|
-
], hooks
|
321
|
-
|
322
|
-
e = @rt.dynamic_map['/docs/[org]/[repo]']
|
323
|
-
hooks = @rt.route_hooks(e)
|
324
|
-
assert_equal [
|
325
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '_hook.rb') }
|
326
|
-
], hooks
|
327
|
-
|
328
|
-
e = @rt.dynamic_map['/docs/[org]/[repo]/issues']
|
329
|
-
hooks = @rt.route_hooks(e)
|
330
|
-
assert_equal [
|
331
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '_hook.rb') },
|
332
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '[org]/[repo]/issues/_hook.rb') }
|
333
|
-
], hooks
|
334
|
-
|
335
|
-
e = @rt.dynamic_map['/docs/[org]/[repo]/issues/[id]']
|
336
|
-
hooks = @rt.route_hooks(e)
|
337
|
-
assert_equal [
|
338
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '_hook.rb') },
|
339
|
-
{ kind: :module, fn: File.join(@rt.root_dir, '[org]/[repo]/issues/_hook.rb') }
|
340
|
-
], hooks
|
341
|
-
end
|
342
|
-
|
343
291
|
def test_routing_root_mounted
|
344
292
|
rt = Syntropy::RoutingTree.new(root_dir: File.join(@root_dir, 'site'), mount_path: '/')
|
345
293
|
router = rt.router_proc
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: syntropy
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 0.15.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Sharon Rosner
|
@@ -221,6 +221,7 @@ files:
|
|
221
221
|
- test/app/_hook.rb
|
222
222
|
- test/app/_layout/default.rb
|
223
223
|
- test/app/_lib/callable.rb
|
224
|
+
- test/app/_lib/dep.rb
|
224
225
|
- test/app/_lib/env.rb
|
225
226
|
- test/app/_lib/klass.rb
|
226
227
|
- test/app/_lib/missing-export.rb
|