syntropy 0.4 → 0.6

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: 553553edc3b0c81902bc3a77470bc6e9789fcd757066dcd5b4e790d38f7a28bf
4
+ data.tar.gz: fdb2cec5f581ba49d5156cce188f31a4811d29767c71833bfcfa37899ee0efc5
5
5
  SHA512:
6
- metadata.gz: f83fa7756c09e12eb87bea546142881d0dba682f214fcaa8ea78b865756232952d8d5a87c308cea0a4d39c4f608a101983c5a11807e99754b3efa19aadd61cad
7
- data.tar.gz: 244d7a878c2e83e2a7ebfc5c0e4a7cd30b429497c7456211a80306ec2b2c3996050111c11dd71e54e9ca63b81e7d14e9475d528127b7d8538255b73e75ad50de
6
+ metadata.gz: 7340ecb01725dcc15a78a66fef5f87a113c2e9759a2305753241b0c1cf738f45b3fd6fea2dcf7bca55196cc3fc309db2ade3e7bb12292fc5aa3abe63362403f9
7
+ data.tar.gz: 48427e653e2c99022177e5475ef53b66c57143288f43e4997c40f1ba464728b310c5e0df343e7f03e3ed79f9fa8637e305b6a22296b19fb15068b7228cf0531f
data/CHANGELOG.md CHANGED
@@ -1,3 +1,13 @@
1
+ ## 0.6 2025-07-05
2
+
3
+ - Add support for middleware and error handlers
4
+
5
+ ## 0.5 2025-07-05
6
+
7
+ - Refactor App class to use Router
8
+ - Refactor routing functionality into separate Router class
9
+ - Add support for _site.rb file
10
+
1
11
  ## 0.4 2025-07-03
2
12
 
3
13
  - Improve errors API
data/TODO.md CHANGED
@@ -1,23 +1,58 @@
1
- - Middleware
1
+ - Some standard middleware:
2
+
3
+ - request rewriter
4
+ - logger
5
+ - auth
6
+ - selector + terminator
2
7
 
3
8
  ```Ruby
4
- # site/_middleware.rb or site/_hook.rb
5
- export ->(req, &app) do
6
- app.call(req)
7
- rescue Syntropy::Error => e
8
- render_error_page(req, e.http_status)
9
+ # For the chainable DSL shown below, we need to create a custom class:
10
+ class Syntropy::Middleware::Selector
11
+ def initialize(select_proc, terminator_proc = nil)
12
+ @select_proc = select_proc
13
+ @terminator_proc = terminator_proc
14
+ end
15
+
16
+ def to_proc
17
+ ->(req, proc) {
18
+ @select_proc.(req) ? @terminator_proc.(req) : proc(req)
19
+ }
20
+ end
21
+
22
+ def terminate(&proc)
23
+ @terminator_proc = proc
24
+ end
9
25
  end
10
26
 
11
- # an alternative, at least for errors is a _error.rb file:
12
- # site/_error.rb
13
- # Just a normal callable:
14
- #
15
- export ->(req, err) do
16
- render_error_page(req, err.http_status)
17
- end
27
+ def Syntropy
28
+ ```
29
+
30
+ ```Ruby
31
+ # a _site.rb file can be used to wrap a whole app
32
+ # site/_site.rb
33
+
34
+ # this means we route according to the host header, with each
35
+ export Syntropy.route_by_host
36
+
37
+ # we can also rewrite requests:
38
+ rewriter = Syntropy
39
+ .select { it.host =~ /^tolkora\.(org|com)$/ }
40
+ .terminate { it.redirect_permanent('https://tolkora.net') }
41
+
42
+ # This is actuall a pretty interesting DSL design:
43
+ # a chain of operations that compose functions. So, we can select a
44
+ export rewriter.wrap(default_app)
45
+
46
+ # composing
47
+ export rewriter.wrap(Syntropy.some_custom_app.wrap(app))
48
+
49
+ # or maybe
50
+ export rewriter << some_other_middleware << app
18
51
  ```
19
52
 
20
53
 
54
+
55
+
21
56
  - CLI tool for setting up a site repo:
22
57
 
23
58
  ```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,121 +64,33 @@ 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
67
+ def render_entry(req, entry)
68
+ kind = entry[:kind]
69
+ return respond_not_found(req) if kind == :not_found
146
70
 
147
- entry = find_route("#{parent}+.rb", cache: false)
148
- entry[:kind] == :module ? entry : NOT_FOUND
71
+ entry[:proc] ||= calculate_route_proc(entry)
72
+ entry[:proc].(req)
149
73
  end
150
74
 
151
- UP_TREE_PATH_RE = %r{^(.+)?/[^/]+$}
152
-
153
- def parent_path(path)
154
- m = path.match(UP_TREE_PATH_RE)
155
- m && m[1]
75
+ def calculate_route_proc(entry)
76
+ render_proc = route_render_proc(entry)
77
+ @router.calc_route_proc_with_hooks(entry, render_proc)
156
78
  end
157
79
 
158
- def render_entry(req, entry)
80
+ def route_render_proc(entry)
159
81
  case entry[:kind]
160
- when :not_found
161
- respond_not_found(req, entry)
162
82
  when :static
163
- respond_static(req, entry)
83
+ ->(req) { respond_static(req, entry) }
164
84
  when :markdown
165
- respond_markdown(req, entry)
85
+ ->(req) { respond_markdown(req, entry) }
166
86
  when :module
167
- respond_module(req, entry)
87
+ load_module(entry)
168
88
  else
169
89
  raise 'Invalid entry kind'
170
90
  end
171
91
  end
172
92
 
173
- def respond_not_found(req, _entry)
93
+ def respond_not_found(req)
174
94
  headers = { ':status' => Qeweney::Status::NOT_FOUND }
175
95
  case req.method
176
96
  when 'head'
@@ -199,13 +119,13 @@ module Syntropy
199
119
  end
200
120
 
201
121
  def respond_module(req, entry)
202
- entry[:code] ||= load_module(entry)
203
- if entry[:code] == :invalid
122
+ entry[:proc] ||= load_module(entry)
123
+ if entry[:proc] == :invalid
204
124
  req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
205
125
  return
206
126
  end
207
127
 
208
- entry[:code].call(req)
128
+ entry[:proc].call(req)
209
129
  rescue Syntropy::Error => e
210
130
  req.respond(nil, ':status' => e.http_status)
211
131
  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,240 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Syntropy
4
+ class Router
5
+ def initialize(opts, module_loader = nil)
6
+ raise 'Invalid location given' if !File.directory?(opts[:location])
7
+
8
+ @opts = opts
9
+ @machine = opts[:machine]
10
+ @root = File.expand_path(opts[:location])
11
+ @mount_path = opts[:mount_path] || '/'
12
+ @rel_path_re ||= /^#{@root}/
13
+ @module_loader = module_loader
14
+
15
+ @cache = {} # maps url path to route entry
16
+ @routes = {} # maps canonical path to route entry (actual routes)
17
+ @files = {} # maps filename to entry
18
+ @deps = {} # maps filenames to array of dependent entries
19
+ @x = {} # maps directories to hook chains
20
+
21
+ scan_routes
22
+ end
23
+
24
+ def [](path)
25
+ get_route_entry(path)
26
+ end
27
+
28
+ def start_file_watcher
29
+ @opts[:logger]&.call('Watching for file changes...', nil)
30
+ @machine.spin { file_watcher_loop }
31
+ end
32
+
33
+ def calc_route_proc_with_hooks(entry, proc)
34
+ compose_up_tree_hooks(entry[:fn], proc)
35
+ end
36
+
37
+ private
38
+
39
+ HIDDEN_RE = /^_/
40
+
41
+ def scan_routes(dir = nil)
42
+ dir ||= @root
43
+
44
+ Dir[File.join(dir, '*')].each do
45
+ basename = File.basename(it)
46
+ next if (basename =~ HIDDEN_RE)
47
+
48
+ File.directory?(it) ? scan_routes(it) : add_route(it)
49
+ end
50
+ end
51
+
52
+ def add_route(fn)
53
+ kind = route_kind(fn)
54
+ rel_path = path_rel(fn)
55
+ canonical_path = path_canonical(rel_path, kind)
56
+ entry = { kind:, fn:, canonical_path: }
57
+ entry[:handle_subtree] = true if (kind == :module) && !!(fn =~ /\+\.rb$/)
58
+
59
+ @routes[canonical_path] = entry
60
+ @files[fn] = entry
61
+ end
62
+
63
+ def route_kind(fn)
64
+ case File.extname(fn)
65
+ when '.md'
66
+ :markdown
67
+ when '.rb'
68
+ :module
69
+ else
70
+ :static
71
+ end
72
+ end
73
+
74
+ def path_rel(path)
75
+ path.gsub(@rel_path_re, '')
76
+ end
77
+
78
+ def path_abs(path, base)
79
+ File.join(base, path)
80
+ end
81
+
82
+ PATH_PARENT_RE = /^(.+)?\/([^\/]+)$/
83
+
84
+ def path_parent(path)
85
+ return nil if path == '/'
86
+
87
+ path.match(PATH_PARENT_RE)[1] || '/'
88
+ end
89
+
90
+ MD_EXT_RE = /\.md$/
91
+ RB_EXT_RE = /[+]?\.rb$/
92
+ INDEX_RE = /^(.*)\/index[+]?\.(?:rb|md|html)$/
93
+
94
+ def path_canonical(rel_path, kind)
95
+ clean = path_clean(rel_path, kind)
96
+ clean.empty? ? @mount_path : File.join(@mount_path, clean)
97
+ end
98
+
99
+ def path_clean(rel_path, kind)
100
+ if (m = rel_path.match(INDEX_RE))
101
+ return m[1]
102
+ end
103
+
104
+ case kind
105
+ when :static
106
+ rel_path
107
+ when :markdown
108
+ rel_path.gsub(MD_EXT_RE, '')
109
+ when :module
110
+ rel_path.gsub(RB_EXT_RE, '')
111
+ end
112
+ end
113
+
114
+ def get_route_entry(path, use_cache: true)
115
+ if use_cache
116
+ cached = @cache[path]
117
+ return cached if cached
118
+ end
119
+
120
+ entry = find_route_entry(path)
121
+ set_cache(path, entry) if use_cache && entry[:kind] != :not_found
122
+ entry
123
+ end
124
+
125
+ def set_cache(path, entry)
126
+ @cache[path] = entry
127
+ (entry[:cache_keys] ||= {})[path] = true
128
+ end
129
+
130
+ # We don't allow access to path with /.., or entries that start with _
131
+ FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
132
+ NOT_FOUND = { kind: :not_found }.freeze
133
+
134
+ def find_route_entry(path)
135
+ return NOT_FOUND if path =~ FORBIDDEN_RE
136
+
137
+ @routes[path] || find_index_route(path) || find_up_tree_module(path) || NOT_FOUND
138
+ end
139
+
140
+ INDEX_OPT_EXT_RE = /^(.*)\/index(?:\.(?:rb|md|html))?$/
141
+
142
+ def find_index_route(path)
143
+ m = path.match(INDEX_OPT_EXT_RE)
144
+ return nil if !m
145
+
146
+ @routes[m[1]]
147
+ end
148
+
149
+ def find_up_tree_module(path)
150
+ parent_path = path_parent(path)
151
+ return nil if !parent_path
152
+
153
+ entry = @routes[parent_path]
154
+ return entry if entry && entry[:handle_subtree]
155
+
156
+ find_up_tree_module(parent_path)
157
+ end
158
+
159
+ def file_watcher_loop
160
+ wf = @opts[:watch_files]
161
+ period = wf.is_a?(Numeric) ? wf : 0.1
162
+ Syntropy.file_watch(@machine, @root, period: period) do |event, fn|
163
+ handle_changed_file(event, fn)
164
+ rescue Exception => e
165
+ p e
166
+ p e.backtrace
167
+ exit!
168
+ end
169
+ end
170
+
171
+ def handle_changed_file(event, fn)
172
+ @opts[:logger]&.call("Detected changed file: #{event} #{fn}")
173
+ @module_loader&.invalidate(fn)
174
+ case event
175
+ when :added
176
+ handle_added_file(fn)
177
+ when :removed
178
+ handle_removed_file(fn)
179
+ when :modified
180
+ handle_modified_file(fn)
181
+ end
182
+ end
183
+
184
+ def handle_added_file(fn)
185
+ add_route(fn)
186
+ @cache.clear # TODO: remove only relevant cache entries
187
+ end
188
+
189
+ def handle_removed_file(fn)
190
+ entry = @files[fn]
191
+ if entry
192
+ remove_entry_cache_keys(entry)
193
+ @routes.delete(entry[:canonical_path])
194
+ @files.delete(fn)
195
+ end
196
+ end
197
+
198
+ def handle_modified_file(fn)
199
+ entry = @files[fn]
200
+ if entry && entry[:kind] == :module
201
+ # invalidate the entry proc, so it will be recalculated
202
+ entry[:proc] = nil
203
+ end
204
+ end
205
+
206
+ def remove_entry_cache_keys(entry)
207
+ entry[:cache_keys]&.each_key { @cache.delete(it) }.clear
208
+ end
209
+
210
+ def compose_up_tree_hooks(path, proc)
211
+ parent = File.dirname(path)
212
+ proc = hook_wrap_if_exists(File.join(parent, '_hook.rb'), proc)
213
+ proc = error_handler_wrap_if_exists(File.join(parent, '_error.rb'), proc)
214
+ return proc if parent == @root
215
+
216
+ compose_up_tree_hooks(parent, proc)
217
+ end
218
+
219
+ def hook_wrap_if_exists(hook_fn, proc)
220
+ return proc if !File.file?(hook_fn)
221
+
222
+ ref = path_rel(hook_fn).gsub(/\.rb$/, '')
223
+ hook_proc = @module_loader.load(ref)
224
+ ->(req) { hook_proc.(req, proc) }
225
+ end
226
+
227
+ def error_handler_wrap_if_exists(error_handler_fn, proc)
228
+ return proc if !File.file?(error_handler_fn)
229
+
230
+ ref = path_rel(error_handler_fn).gsub(/\.rb$/, '')
231
+ error_proc = @module_loader.load(ref)
232
+
233
+ proc do |req|
234
+ proc.(req)
235
+ rescue StandardError => e
236
+ error_proc.(req, e)
237
+ end
238
+ end
239
+ end
240
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.4'
4
+ VERSION = '0.6'
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
data/test/app/_hook.rb ADDED
@@ -0,0 +1,5 @@
1
+ export ->(req, proc) {
2
+ req.ctx[:foo] = req.query[:foo]
3
+ # p proc: proc
4
+ proc.(req)
5
+ }
@@ -0,0 +1,6 @@
1
+ DEFAULT_STATUS = Qeweney::Status::INTERNAL_SERVER_ERROR
2
+
3
+ export ->(req, err) {
4
+ status = err.respond_to?(:http_status) ? err.http_status : DEFAULT_STATUS
5
+ req.respond("<h1>#{err.message}</h1>", ':status' => status, 'Content-Type' => 'text/html')
6
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ raise 'Raised error'
3
+ }
@@ -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
@@ -172,4 +140,43 @@ class AppRoutingTest < Minitest::Test
172
140
  ensure
173
141
  IO.write(@tmp_fn, orig_body) if orig_body
174
142
  end
143
+
144
+ def test_middleware
145
+ req = make_request(':method' => 'HEAD', ':path' => '/test?foo=42')
146
+ assert_equal Status::OK, req.response_status
147
+ assert_nil req.response_body
148
+ assert_equal '42', req.ctx[:foo]
149
+
150
+ req = make_request(':method' => 'HEAD', ':path' => '/test/about/raise?foo=43')
151
+ assert_equal Status::INTERNAL_SERVER_ERROR, req.response_status
152
+ assert_equal '<h1>Raised error</h1>', req.response_body
153
+ assert_equal '43', req.ctx[:foo]
154
+ end
155
+ end
156
+
157
+ class CustomAppTest < Minitest::Test
158
+ Status = Qeweney::Status
159
+
160
+ APP_ROOT = File.join(__dir__, 'app_custom')
161
+
162
+ def setup
163
+ @machine = UM.new
164
+ @app = Syntropy::App.load(
165
+ machine: @machine,
166
+ location: APP_ROOT,
167
+ mount_path: '/'
168
+ )
169
+ end
170
+
171
+ def make_request(*, **)
172
+ req = mock_req(*, **)
173
+ @app.call(req)
174
+ req
175
+ end
176
+
177
+ def test_app_with_site_rb_file
178
+ req = make_request(':method' => 'GET', ':path' => '/foo/bar')
179
+ assert_nil req.response_body
180
+ assert_equal Status::TEAPOT, req.response_status
181
+ end
175
182
  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.6'
5
5
  platform: ruby
6
6
  authors:
7
7
  - Sharon Rosner
@@ -197,28 +197,34 @@ 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
203
204
  - syntropy.gemspec
205
+ - test/app/_hook.rb
204
206
  - test/app/_layout/default.rb
205
207
  - test/app/_lib/callable.rb
206
208
  - test/app/_lib/klass.rb
207
209
  - test/app/_lib/missing-export.rb
210
+ - test/app/about/_error.rb
208
211
  - test/app/about/foo.md
209
212
  - test/app/about/index.rb
213
+ - test/app/about/raise.rb
210
214
  - test/app/api+.rb
211
215
  - test/app/assets/style.css
212
216
  - test/app/bar.rb
213
217
  - test/app/baz.rb
214
218
  - test/app/index.html
215
219
  - test/app/tmp.rb
220
+ - test/app_custom/_site.rb
216
221
  - test/helper.rb
217
222
  - test/run.rb
218
223
  - test/test_app.rb
219
224
  - test/test_connection_pool.rb
220
225
  - test/test_file_watch.rb
221
226
  - test/test_module.rb
227
+ - test/test_router.rb
222
228
  - test/test_rpc_api.rb
223
229
  - test/test_side_run.rb
224
230
  - test/test_validation.rb