syntropy 0.4 → 0.5

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: 50c0c34b9bdad39e7116f404c5d4efa7a8a79aa5911e95432efc62c8c90e6592
4
- data.tar.gz: 72e0cb6947b187628e86be0af3f8fd5440c79f5c862b614e7074b45ae4dabf60
3
+ metadata.gz: 7f125135bc3ed83c1050467241f48caf366600fead1d75db79455038e42e311a
4
+ data.tar.gz: ee9bd2bbf906eea9c90481ecdf643126285f65f63fe66a3f2b4f3050893e7c28
5
5
  SHA512:
6
- metadata.gz: f83fa7756c09e12eb87bea546142881d0dba682f214fcaa8ea78b865756232952d8d5a87c308cea0a4d39c4f608a101983c5a11807e99754b3efa19aadd61cad
7
- data.tar.gz: 244d7a878c2e83e2a7ebfc5c0e4a7cd30b429497c7456211a80306ec2b2c3996050111c11dd71e54e9ca63b81e7d14e9475d528127b7d8538255b73e75ad50de
6
+ metadata.gz: 0bdce31c490701521d73829c02e1193fcd0da399b1e6186066ad5d6ecfa067dc143941300bf670fa4b447d5d5c177e903fa273d9d6d5bbfa2319ec0c74fd2678
7
+ data.tar.gz: 7b5eddf105f8654f379bf1d7f52948b713ee342dbd97eb1896e28b618b442ccf33c716d27a7f67c21cfd2c65f610b5c9677cd5e4faa0e777b8f727552628865c
data/CHANGELOG.md CHANGED
@@ -1,3 +1,9 @@
1
+ ## 0.5 2025-07-05
2
+
3
+ - Refactor App class to use Router
4
+ - Refactor routing functionality into separate Router class
5
+ - Add support for _site.rb file
6
+
1
7
  ## 0.4 2025-07-03
2
8
 
3
9
  - Improve errors API
data/TODO.md CHANGED
@@ -1,7 +1,30 @@
1
+ - Refactor routing code into a separate Router class.
2
+ - The Router class is in charge of:
3
+ - caching routes
4
+ - loading modules
5
+ - unloading modules on file change
6
+ - calculating middleware for routes
7
+ - middleware is defined in `_hook.rb` modules
8
+ - interface: ->(req, next)
9
+ - a special case for handling errors is `_error.rb`
10
+ - interface: ->(req, err)
11
+ - dispatching routes
12
+ - error handling:
13
+ - on uncaught error, if an `_error.rb` file exists in the same directory
14
+ or up the file tree
15
+ - middleware:
16
+ - a closure is created from the composition of the different hooks
17
+ defined, from the route's directory and up the file
18
+ - error handlers and middleware closures are cached as part of the route's
19
+ entry
20
+ - on file change for any _hook.rb or _error.rb files, all route entries in
21
+ the corresponding subtree are invalidated
22
+
23
+
1
24
  - Middleware
2
25
 
3
26
  ```Ruby
4
- # site/_middleware.rb or site/_hook.rb
27
+ # site/_hook.rb
5
28
  export ->(req, &app) do
6
29
  app.call(req)
7
30
  rescue Syntropy::Error => e
@@ -15,9 +38,32 @@
15
38
  export ->(req, err) do
16
39
  render_error_page(req, err.http_status)
17
40
  end
41
+
42
+ # a _site.rb file can be used to wrap a whole app
43
+ # site/_site.rb
44
+
45
+ # this means we route according to the host header, with each
46
+ export Syntropy.route_by_host
47
+
48
+ # we can also rewrite requests:
49
+ rewriter = Syntropy
50
+ .select { it.host =~ /^tolkora\.(org|com)$/ }
51
+ .terminate { it.redirect_permanent('https://tolkora.net') }
52
+
53
+ # This is actuall a pretty interesting DSL design:
54
+ # a chain of operations that compose functions. So, we can select a
55
+ export rewriter.wrap(default_app)
56
+
57
+ # composing
58
+ export rewriter.wrap(Syntropy.some_custom_app.wrap(app))
59
+
60
+ # or maybe
61
+ export rewriter << some_other_middleware << app
18
62
  ```
19
63
 
20
64
 
65
+
66
+
21
67
  - CLI tool for setting up a site repo:
22
68
 
23
69
  ```bash
data/bin/syntropy CHANGED
@@ -61,5 +61,5 @@ end
61
61
  opts[:machine] = Syntropy.machine = UM.new
62
62
  opts[:logger] = opts[:logger] && TP2::Logger.new(opts[:machine], **opts)
63
63
 
64
- app = Syntropy::App.new(opts[:machine], opts[:location], '/', opts)
64
+ app = Syntropy::App.load(opts)
65
65
  TP2.run(opts) { app.call(it) }
data/lib/syntropy/app.rb CHANGED
@@ -12,38 +12,46 @@ require 'syntropy/module'
12
12
 
13
13
  module Syntropy
14
14
  class App
15
- attr_reader :route_cache
15
+ class << self
16
+ def load(opts)
17
+ site_file_app(opts) || default_app(opts)
18
+ end
19
+
20
+ private
21
+
22
+ def site_file_app(opts)
23
+ site_fn = File.join(opts[:location], '_site.rb')
24
+ return nil if !File.file?(site_fn)
25
+
26
+ loader = Syntropy::ModuleLoader.new(opts[:location], opts)
27
+ loader.load('_site')
28
+ end
29
+
30
+ def default_app(opts)
31
+ new(opts[:machine], opts[:location], opts[:mount_path] || '/', opts)
32
+ end
33
+ end
16
34
 
17
35
  def initialize(machine, src_path, mount_path, opts = {})
18
36
  @machine = machine
19
37
  @src_path = File.expand_path(src_path)
20
38
  @mount_path = mount_path
21
- @route_cache = {}
22
39
  @opts = opts
23
40
 
24
- @relative_path_re = calculate_relative_path_re(mount_path)
41
+ @module_loader = Syntropy::ModuleLoader.new(@src_path, @opts)
42
+ @router = Syntropy::Router.new(@opts, @module_loader)
43
+
25
44
  @machine.spin do
26
45
  # we do startup stuff asynchronously, in order to first let TP2 do its
27
46
  # setup tasks
28
47
  @machine.sleep 0.15
29
48
  @opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
30
- start_file_watcher if opts[:watch_files]
49
+ @router.start_file_watcher if opts[:watch_files]
31
50
  end
32
-
33
- @module_loader ||= Syntropy::ModuleLoader.new(@src_path, @opts)
34
- end
35
-
36
- def find_route(path, cache: true)
37
- cached = @route_cache[path]
38
- return cached if cached
39
-
40
- entry = calculate_route(path)
41
- @route_cache[path] = entry if entry[:kind] != :not_found && cache
42
- entry
43
51
  end
44
52
 
45
53
  def call(req)
46
- entry = find_route(req.path)
54
+ entry = @router[req.path]
47
55
  render_entry(req, entry)
48
56
  rescue Syntropy::Error => e
49
57
  msg = e.message
@@ -56,105 +64,6 @@ module Syntropy
56
64
 
57
65
  private
58
66
 
59
- def start_file_watcher
60
- @opts[:logger]&.call('Watching for module file changes...', nil)
61
- wf = @opts[:watch_files]
62
- period = wf.is_a?(Numeric) ? wf : 0.1
63
- @machine.spin do
64
- Syntropy.file_watch(@machine, @src_path, period: period) do
65
- @opts[:logger]&.call("Detected changed file: #{it}")
66
- invalidate_cache(it)
67
- rescue Exception => e
68
- p e
69
- p e.backtrace
70
- exit!
71
- end
72
- end
73
- end
74
-
75
- def invalidate_cache(fn)
76
- @module_loader.unload(fn)
77
-
78
- invalidated_keys = []
79
- @route_cache.each do |k, v|
80
- @opts[:logger]&.call("Invalidate cache for #{k}", nil)
81
- invalidated_keys << k if v[:fn] == fn
82
- end
83
-
84
- invalidated_keys.each { @route_cache.delete(it) }
85
- end
86
-
87
- def calculate_relative_path_re(mount_path)
88
- mount_path = '' if mount_path == '/'
89
- %r{^#{mount_path}(?:/(.*))?$}
90
- end
91
-
92
- FILE_KINDS = {
93
- '.rb' => :module,
94
- '.md' => :markdown
95
- }
96
- NOT_FOUND = { kind: :not_found }
97
-
98
- # We don't allow access to path with /.., or entries that start with _
99
- FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
100
-
101
- def calculate_route(path)
102
- return NOT_FOUND if path =~ FORBIDDEN_RE
103
-
104
- m = path.match(@relative_path_re)
105
- return NOT_FOUND if !m
106
-
107
- relative_path = m[1] || ''
108
- fs_path = File.join(@src_path, relative_path)
109
-
110
- return file_entry(fs_path) if File.file?(fs_path)
111
- return find_index_entry(fs_path) if File.directory?(fs_path)
112
-
113
- entry = find_file_entry_with_extension(fs_path)
114
- return entry if entry[:kind] != :not_found
115
-
116
- find_up_tree_module(path)
117
- end
118
-
119
- def file_entry(fn)
120
- { fn: File.expand_path(fn), kind: FILE_KINDS[File.extname(fn)] || :static }
121
- end
122
-
123
- def find_index_entry(dir)
124
- find_file_entry_with_extension(File.join(dir, 'index'))
125
- end
126
-
127
- def find_file_entry_with_extension(path)
128
- fn = "#{path}.html"
129
- return file_entry(fn) if File.file?(fn)
130
-
131
- fn = "#{path}.md"
132
- return file_entry(fn) if File.file?(fn)
133
-
134
- fn = "#{path}.rb"
135
- return file_entry(fn) if File.file?(fn)
136
-
137
- fn = "#{path}+.rb"
138
- return file_entry(fn) if File.file?(fn)
139
-
140
- NOT_FOUND
141
- end
142
-
143
- def find_up_tree_module(path)
144
- parent = parent_path(path)
145
- return NOT_FOUND if !parent
146
-
147
- entry = find_route("#{parent}+.rb", cache: false)
148
- entry[:kind] == :module ? entry : NOT_FOUND
149
- end
150
-
151
- UP_TREE_PATH_RE = %r{^(.+)?/[^/]+$}
152
-
153
- def parent_path(path)
154
- m = path.match(UP_TREE_PATH_RE)
155
- m && m[1]
156
- end
157
-
158
67
  def render_entry(req, entry)
159
68
  case entry[:kind]
160
69
  when :not_found
@@ -199,13 +108,13 @@ module Syntropy
199
108
  end
200
109
 
201
110
  def respond_module(req, entry)
202
- entry[:code] ||= load_module(entry)
203
- if entry[:code] == :invalid
111
+ entry[:proc] ||= load_module(entry)
112
+ if entry[:proc] == :invalid
204
113
  req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
205
114
  return
206
115
  end
207
116
 
208
- entry[:code].call(req)
117
+ entry[:proc].call(req)
209
118
  rescue Syntropy::Error => e
210
119
  req.respond(nil, ':status' => e.http_status)
211
120
  rescue StandardError => e
@@ -8,15 +8,16 @@ module Syntropy
8
8
 
9
9
  queue = Thread::Queue.new
10
10
  listener = Listen.to(*roots) do |modified, added, removed|
11
- fns = (modified + added + removed).uniq
12
- fns.each { queue.push(it) }
11
+ modified.each { queue.push([:modified, it]) }
12
+ added.each { queue.push([:added, it]) }
13
+ removed.each { queue.push([:removed, it]) }
13
14
  end
14
15
  listener.start
15
16
 
16
17
  loop do
17
18
  machine.sleep(period) while queue.empty?
18
- fn = queue.shift
19
- block.call(fn)
19
+ event, fn = queue.shift
20
+ block.call(event, fn)
20
21
  end
21
22
  rescue StandardError => e
22
23
  p e
@@ -15,7 +15,7 @@ module Syntropy
15
15
  @loaded[ref] ||= load_module(ref)
16
16
  end
17
17
 
18
- def unload(fn)
18
+ def invalidate(fn)
19
19
  ref = @fn_map[fn]
20
20
  return if !ref
21
21
 
@@ -0,0 +1,208 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ class Router
5
+ attr_reader :cache
6
+
7
+ def initialize(opts, module_loader = nil)
8
+ raise 'Invalid location given' if !File.directory?(opts[:location])
9
+
10
+ @opts = opts
11
+ @machine = opts[:machine]
12
+ @root = File.expand_path(opts[:location])
13
+ @mount_path = opts[:mount_path] || '/'
14
+ @rel_path_re ||= /^#{@root}/
15
+ @module_loader = module_loader
16
+
17
+ @cache = {} # maps url path to route entry
18
+ @routes = {} # maps canonical path to route entry (actual routes)
19
+ @files = {} # maps filename to entry
20
+ @deps = {} # maps filenames to array of dependent entries
21
+ @x = {} # maps directories to hook chains
22
+
23
+ scan_routes
24
+ end
25
+
26
+ def [](path)
27
+ get_route_entry(path)
28
+ end
29
+
30
+ def start_file_watcher
31
+ @opts[:logger]&.call('Watching for file changes...', nil)
32
+ @machine.spin { file_watcher_loop }
33
+ end
34
+
35
+ private
36
+
37
+ HIDDEN_RE = /^_/
38
+
39
+ def scan_routes(dir = nil)
40
+ dir ||= @root
41
+
42
+ Dir[File.join(dir, '*')].each do
43
+ basename = File.basename(it)
44
+ next if (basename =~ HIDDEN_RE)
45
+
46
+ File.directory?(it) ? scan_routes(it) : add_route(it)
47
+ end
48
+ end
49
+
50
+ def add_route(fn)
51
+ kind = route_kind(fn)
52
+ rel_path = path_rel(fn)
53
+ canonical_path = path_canonical(rel_path, kind)
54
+ entry = { kind:, fn:, canonical_path: }
55
+ entry[:handle_subtree] = true if (kind == :module) && !!(fn =~ /\+\.rb$/)
56
+
57
+ @routes[canonical_path] = entry
58
+ @files[fn] = entry
59
+ end
60
+
61
+ def route_kind(fn)
62
+ case File.extname(fn)
63
+ when '.md'
64
+ :markdown
65
+ when '.rb'
66
+ :module
67
+ else
68
+ :static
69
+ end
70
+ end
71
+
72
+ def path_rel(path)
73
+ path.gsub(@rel_path_re, '')
74
+ end
75
+
76
+ def path_abs(path, base)
77
+ File.join(base, path)
78
+ end
79
+
80
+ PATH_PARENT_RE = /^(.+)?\/([^\/]+)$/
81
+
82
+ def path_parent(path)
83
+ return nil if path == '/'
84
+
85
+ path.match(PATH_PARENT_RE)[1] || '/'
86
+ end
87
+
88
+ MD_EXT_RE = /\.md$/
89
+ RB_EXT_RE = /[+]?\.rb$/
90
+ INDEX_RE = /^(.*)\/index[+]?\.(?:rb|md|html)$/
91
+
92
+ def path_canonical(rel_path, kind)
93
+ clean = path_clean(rel_path, kind)
94
+ clean.empty? ? @mount_path : File.join(@mount_path, clean)
95
+ end
96
+
97
+ def path_clean(rel_path, kind)
98
+ if (m = rel_path.match(INDEX_RE))
99
+ return m[1]
100
+ end
101
+
102
+ case kind
103
+ when :static
104
+ rel_path
105
+ when :markdown
106
+ rel_path.gsub(MD_EXT_RE, '')
107
+ when :module
108
+ rel_path.gsub(RB_EXT_RE, '')
109
+ end
110
+ end
111
+
112
+ def get_route_entry(path, use_cache: true)
113
+ if use_cache
114
+ cached = @cache[path]
115
+ return cached if cached
116
+ end
117
+
118
+ entry = find_route_entry(path)
119
+ set_cache(path, entry) if use_cache && entry[:kind] != :not_found
120
+ entry
121
+ end
122
+
123
+ def set_cache(path, entry)
124
+ @cache[path] = entry
125
+ (entry[:cache_keys] ||= {})[path] = true
126
+ end
127
+
128
+ # We don't allow access to path with /.., or entries that start with _
129
+ FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
130
+ NOT_FOUND = { kind: :not_found }.freeze
131
+
132
+ def find_route_entry(path)
133
+ return NOT_FOUND if path =~ FORBIDDEN_RE
134
+
135
+ @routes[path] || find_index_route(path) || find_up_tree_module(path) || NOT_FOUND
136
+ end
137
+
138
+ INDEX_OPT_EXT_RE = /^(.*)\/index(?:\.(?:rb|md|html))?$/
139
+
140
+ def find_index_route(path)
141
+ m = path.match(INDEX_OPT_EXT_RE)
142
+ return nil if !m
143
+
144
+ @routes[m[1]]
145
+ end
146
+
147
+ def find_up_tree_module(path)
148
+ parent_path = path_parent(path)
149
+ return nil if !parent_path
150
+
151
+ entry = @routes[parent_path]
152
+ return entry if entry && entry[:handle_subtree]
153
+
154
+ find_up_tree_module(parent_path)
155
+ end
156
+
157
+ def file_watcher_loop
158
+ wf = @opts[:watch_files]
159
+ period = wf.is_a?(Numeric) ? wf : 0.1
160
+ Syntropy.file_watch(@machine, @root, period: period) do |event, fn|
161
+ handle_changed_file(event, fn)
162
+ rescue Exception => e
163
+ p e
164
+ p e.backtrace
165
+ exit!
166
+ end
167
+ end
168
+
169
+ def handle_changed_file(event, fn)
170
+ @opts[:logger]&.call("Detected changed file: #{event} #{fn}")
171
+ @module_loader&.invalidate(fn)
172
+ case event
173
+ when :added
174
+ handle_added_file(fn)
175
+ when :removed
176
+ handle_removed_file(fn)
177
+ when :modified
178
+ handle_modified_file(fn)
179
+ end
180
+ end
181
+
182
+ def handle_added_file(fn)
183
+ add_route(fn)
184
+ @cache.clear # TODO: remove only relevant cache entries
185
+ end
186
+
187
+ def handle_removed_file(fn)
188
+ entry = @files[fn]
189
+ if entry
190
+ remove_entry_cache_keys(entry)
191
+ @routes.delete(entry[:canonical_path])
192
+ @files.delete(fn)
193
+ end
194
+ end
195
+
196
+ def handle_modified_file(fn)
197
+ entry = @files[fn]
198
+ if entry && entry[:kind] == :module
199
+ # invalidate the entry proc, so it will be recalculated
200
+ entry[:proc] = nil
201
+ end
202
+ end
203
+
204
+ def remove_entry_cache_keys(entry)
205
+ entry[:cache_keys]&.each_key { @cache.delete(it) }.clear
206
+ end
207
+ end
208
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.4'
4
+ VERSION = '0.5'
5
5
  end
data/lib/syntropy.rb CHANGED
@@ -9,11 +9,12 @@ require 'syntropy/connection_pool'
9
9
  require 'syntropy/module'
10
10
  require 'syntropy/rpc_api'
11
11
  require 'syntropy/side_run'
12
+ require 'syntropy/router'
12
13
  require 'syntropy/app'
13
14
  require 'syntropy/request_extensions'
14
15
 
15
16
  module Syntropy
16
- Status = Qeweney::Status
17
+ Status = Qeweney::Status
17
18
 
18
19
  class << self
19
20
  attr_accessor :machine
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond(nil, ':status' => Status::TEAPOT)
3
+ }
data/test/test_app.rb CHANGED
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class AppRoutingTest < Minitest::Test
5
+ class AppTest < Minitest::Test
6
6
  Status = Qeweney::Status
7
7
 
8
8
  APP_ROOT = File.join(__dir__, 'app')
@@ -13,48 +13,12 @@ class AppRoutingTest < Minitest::Test
13
13
  @tmp_path = '/test/tmp'
14
14
  @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
15
15
 
16
- @app = Syntropy::App.new(@machine, APP_ROOT, '/test', watch_files: 0.05)
17
- end
18
-
19
- def full_path(fn)
20
- File.join(APP_ROOT, fn)
21
- end
22
-
23
- def test_find_route
24
- entry = @app.find_route('/')
25
- assert_equal :not_found, entry[:kind]
26
-
27
- entry = @app.find_route('/test')
28
- assert_equal :static, entry[:kind]
29
- assert_equal full_path('index.html'), entry[:fn]
30
-
31
- entry = @app.find_route('/test/about')
32
- assert_equal :module, entry[:kind]
33
- assert_equal full_path('about/index.rb'), entry[:fn]
34
-
35
- entry = @app.find_route('/test/../test_app.rb')
36
- assert_equal :not_found, entry[:kind]
37
-
38
- entry = @app.find_route('/test/_layout/default')
39
- assert_equal :not_found, entry[:kind]
40
-
41
- entry = @app.find_route('/test/api')
42
- assert_equal :module, entry[:kind]
43
- assert_equal full_path('api+.rb'), entry[:fn]
44
-
45
- entry = @app.find_route('/test/api/foo/bar')
46
- assert_equal :module, entry[:kind]
47
- assert_equal full_path('api+.rb'), entry[:fn]
48
-
49
- entry = @app.find_route('/test/api/foo/../bar')
50
- assert_equal :not_found, entry[:kind]
51
-
52
- entry = @app.find_route('/test/api_1')
53
- assert_equal :not_found, entry[:kind]
54
-
55
- entry = @app.find_route('/test/about/foo')
56
- assert_equal :markdown, entry[:kind]
57
- assert_equal full_path('about/foo.md'), entry[:fn]
16
+ @app = Syntropy::App.load(
17
+ machine: @machine,
18
+ location: APP_ROOT,
19
+ mount_path: '/test',
20
+ watch_files: 0.05
21
+ )
58
22
  end
59
23
 
60
24
  def make_request(*, **)
@@ -131,6 +95,10 @@ class AppRoutingTest < Minitest::Test
131
95
  assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
132
96
  assert_equal Status::TEAPOT, req.response_status
133
97
 
98
+ req = make_request(':method' => 'POST', ':path' => '/test/api/foo/bar?q=incr')
99
+ assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
100
+ assert_equal Status::TEAPOT, req.response_status
101
+
134
102
  req = make_request(':method' => 'GET', ':path' => '/test/bar')
135
103
  assert_equal 'foobar', req.response_body
136
104
  assert_equal Status::OK, req.response_status
@@ -173,3 +141,30 @@ class AppRoutingTest < Minitest::Test
173
141
  IO.write(@tmp_fn, orig_body) if orig_body
174
142
  end
175
143
  end
144
+
145
+ class CustomAppTest < Minitest::Test
146
+ Status = Qeweney::Status
147
+
148
+ APP_ROOT = File.join(__dir__, 'app_custom')
149
+
150
+ def setup
151
+ @machine = UM.new
152
+ @app = Syntropy::App.load(
153
+ machine: @machine,
154
+ location: APP_ROOT,
155
+ mount_path: '/'
156
+ )
157
+ end
158
+
159
+ def make_request(*, **)
160
+ req = mock_req(*, **)
161
+ @app.call(req)
162
+ req
163
+ end
164
+
165
+ def test_app_with_site_rb_file
166
+ req = make_request(':method' => 'GET', ':path' => '/foo/bar')
167
+ assert_nil req.response_body
168
+ assert_equal Status::TEAPOT, req.response_status
169
+ end
170
+ end
@@ -14,21 +14,21 @@ class FileWatchTest < Minitest::Test
14
14
  queue = UM::Queue.new
15
15
 
16
16
  f = @machine.spin do
17
- Syntropy.file_watch(@machine, @root, period: 0.01) { @machine.push(queue, it) }
17
+ Syntropy.file_watch(@machine, @root, period: 0.01) { |event, fn| @machine.push(queue, [event, fn]) }
18
18
  end
19
19
  @machine.sleep(0.05)
20
20
  assert_equal 0, queue.count
21
21
 
22
22
  fn = File.join(@root, 'foo.bar')
23
23
  IO.write(fn, 'abc')
24
- assert_equal fn, @machine.shift(queue)
24
+ assert_equal [:added, fn], @machine.shift(queue)
25
25
 
26
26
  fn = File.join(@root, 'foo.bar')
27
27
  IO.write(fn, 'def')
28
- assert_equal fn, @machine.shift(queue)
28
+ assert_equal [:modified, fn], @machine.shift(queue)
29
29
 
30
30
  FileUtils.rm(fn)
31
- assert_equal fn, @machine.shift(queue)
31
+ assert_equal [:removed, fn], @machine.shift(queue)
32
32
  ensure
33
33
  @machine.schedule(f, UM::Terminate)
34
34
  # @machine.join(f)
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'helper'
4
+
5
+ class RouterTest < Minitest::Test
6
+ APP_ROOT = File.join(__dir__, 'app')
7
+
8
+ def setup
9
+ @machine = UM.new
10
+
11
+ @tmp_path = '/test/tmp'
12
+ @tmp_fn = File.join(APP_ROOT, 'tmp.rb')
13
+
14
+ @router = Syntropy::Router.new(
15
+ machine: @machine,
16
+ location: APP_ROOT,
17
+ mount_path: '/test',
18
+ watch_files: 0.05
19
+ )
20
+ @router.start_file_watcher
21
+ end
22
+
23
+ def full_path(fn)
24
+ File.join(APP_ROOT, fn)
25
+ end
26
+
27
+ def test_find_route
28
+ # entry = @router['/']
29
+ # assert_equal :not_found, entry[:kind]
30
+
31
+ entry = @router['/test']
32
+ assert_equal :static, entry[:kind]
33
+ assert_equal full_path('index.html'), entry[:fn]
34
+
35
+ entry = @router['/test/about']
36
+ assert_equal :module, entry[:kind]
37
+ assert_equal full_path('about/index.rb'), entry[:fn]
38
+
39
+ entry = @router['/test/../test_app.rb']
40
+ assert_equal :not_found, entry[:kind]
41
+
42
+ entry = @router['/test/_layout/default']
43
+ assert_equal :not_found, entry[:kind]
44
+
45
+ entry = @router['/test/api']
46
+ assert_equal :module, entry[:kind]
47
+ assert_equal full_path('api+.rb'), entry[:fn]
48
+
49
+ entry = @router['/test/api/foo/bar']
50
+ assert_equal :module, entry[:kind]
51
+ assert_equal full_path('api+.rb'), entry[:fn]
52
+
53
+ entry = @router['/test/api/foo/../bar']
54
+ assert_equal :not_found, entry[:kind]
55
+
56
+ entry = @router['/test/api_1']
57
+ assert_equal :not_found, entry[:kind]
58
+
59
+ entry = @router['/test/about/foo']
60
+ assert_equal :markdown, entry[:kind]
61
+ assert_equal full_path('about/foo.md'), entry[:fn]
62
+ end
63
+
64
+ def test_router_file_watching
65
+ @machine.sleep 0.2
66
+
67
+ entry = @router[@tmp_path]
68
+ assert_equal :module, entry[:kind]
69
+
70
+ # remove file
71
+ orig_body = IO.read(@tmp_fn)
72
+ FileUtils.rm(@tmp_fn)
73
+ @machine.sleep(0.3)
74
+
75
+ entry = @router[@tmp_path]
76
+ assert_equal :not_found, entry[:kind]
77
+
78
+ IO.write(@tmp_fn, 'foobar')
79
+ @machine.sleep(0.3)
80
+ entry = @router[@tmp_path]
81
+ assert_equal :module, entry[:kind]
82
+
83
+ entry[:proc] = ->(x) { x }
84
+ IO.write(@tmp_fn, 'barbaz')
85
+ @machine.sleep(0.3)
86
+ assert_nil entry[:proc]
87
+ ensure
88
+ IO.write(@tmp_fn, orig_body) if orig_body
89
+ end
90
+ end
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.4'
4
+ version: '0.5'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -197,6 +197,7 @@ files:
197
197
  - lib/syntropy/file_watch.rb
198
198
  - lib/syntropy/module.rb
199
199
  - lib/syntropy/request_extensions.rb
200
+ - lib/syntropy/router.rb
200
201
  - lib/syntropy/rpc_api.rb
201
202
  - lib/syntropy/side_run.rb
202
203
  - lib/syntropy/version.rb
@@ -213,12 +214,14 @@ files:
213
214
  - test/app/baz.rb
214
215
  - test/app/index.html
215
216
  - test/app/tmp.rb
217
+ - test/app_custom/_site.rb
216
218
  - test/helper.rb
217
219
  - test/run.rb
218
220
  - test/test_app.rb
219
221
  - test/test_connection_pool.rb
220
222
  - test/test_file_watch.rb
221
223
  - test/test_module.rb
224
+ - test/test_router.rb
222
225
  - test/test_rpc_api.rb
223
226
  - test/test_side_run.rb
224
227
  - test/test_validation.rb