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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bc595fea6260cf92c377aeb6931cd1f62ce02fd2e05ca2436fb8b49ac57e4b49
4
- data.tar.gz: 9c38f08bf13033aabc5bcf4df9b96e05e238461a43513167b6aeca21fb55c388
3
+ metadata.gz: ef2840797122cef8e0f7f7675c10665c35f60915e091043416ffd0ed70e1cb5c
4
+ data.tar.gz: 0d929bfbe0d629879b05c57b7c01c1e8c0ee1af148dc23570c30233f19e81156
5
5
  SHA512:
6
- metadata.gz: 23d2888d82b16a4f848523193478d7e2813398cf07b32a5ef009c2ce24d76b150579228b32abf6179176075e882e6c68119002819e945984207f611c68726a43
7
- data.tar.gz: 13416d8be55de9b6fc454c7c99e25ad65093be329de19b9e4c561ab28e1436f412d8e42838ec14ad3ab039b79b0d57c3b0ea157c5fbb2795d97d605dc2f34b63
6
+ metadata.gz: 6375f3fb5cbab65d6b710b613c32d0eb9ebb714880f0eb3921fd0b07da9219e0ab94faec2398ae2f33a21fa4f21a8135416faf5b6f2ad0721a99afa067b854f7
7
+ data.tar.gz: 715274f642dc3b0db72b0ec37526f26221b56930e19eb324f804408b18552a5c465d59f313cd8329a24a9caa0aa05abaa6907d523f15e1e7061e3e9e1bf1762c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,7 @@
1
+ # 0.15 2025-08-31
2
+
3
+ - Implement invalidation of reverse dependencies on module file change
4
+
1
5
  # 0.14 2025-08-30
2
6
 
3
7
  - Tweak "boot" sequence
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
- @module_loader.invalidate(fn)
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
@@ -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
- @loaded = {} # maps ref to code
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
- @loaded[ref] ||= load_module(ref)
41
+ entry = (@modules[ref] ||= load_module(ref))
42
+ entry[:export_value]
16
43
  end
17
44
 
18
- def invalidate(fn)
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
- @loaded.delete(ref)
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
- export_value = Syntropy::Module.load(env, code, fn)
36
- transform_module_export_value(export_value)
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
- export_value
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
- attr_reader
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
@@ -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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.14'
4
+ VERSION = '0.15.1'
5
5
  end
@@ -0,0 +1,7 @@
1
+ Foo = import '_lib/self'
2
+
3
+ def bar
4
+ Foo.foo
5
+ end
6
+
7
+ export self
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
@@ -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: '0.14'
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