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 +4 -4
- data/CHANGELOG.md +6 -0
- data/TODO.md +47 -1
- data/bin/syntropy +1 -1
- data/lib/syntropy/app.rb +27 -118
- data/lib/syntropy/file_watch.rb +5 -4
- data/lib/syntropy/module.rb +1 -1
- data/lib/syntropy/router.rb +208 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +2 -1
- data/test/app_custom/_site.rb +3 -0
- data/test/test_app.rb +38 -43
- data/test/test_file_watch.rb +4 -4
- data/test/test_router.rb +90 -0
- metadata +4 -1
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 7f125135bc3ed83c1050467241f48caf366600fead1d75db79455038e42e311a
|
4
|
+
data.tar.gz: ee9bd2bbf906eea9c90481ecdf643126285f65f63fe66a3f2b4f3050893e7c28
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 0bdce31c490701521d73829c02e1193fcd0da399b1e6186066ad5d6ecfa067dc143941300bf670fa4b447d5d5c177e903fa273d9d6d5bbfa2319ec0c74fd2678
|
7
|
+
data.tar.gz: 7b5eddf105f8654f379bf1d7f52948b713ee342dbd97eb1896e28b618b442ccf33c716d27a7f67c21cfd2c65f610b5c9677cd5e4faa0e777b8f727552628865c
|
data/CHANGELOG.md
CHANGED
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/
|
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.
|
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
|
-
|
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
|
-
@
|
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 =
|
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[:
|
203
|
-
if entry[:
|
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[:
|
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
|
data/lib/syntropy/file_watch.rb
CHANGED
@@ -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
|
-
|
12
|
-
|
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
|
data/lib/syntropy/module.rb
CHANGED
@@ -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
|
data/lib/syntropy/version.rb
CHANGED
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
|
-
|
17
|
+
Status = Qeweney::Status
|
17
18
|
|
18
19
|
class << self
|
19
20
|
attr_accessor :machine
|
data/test/test_app.rb
CHANGED
@@ -2,7 +2,7 @@
|
|
2
2
|
|
3
3
|
require_relative 'helper'
|
4
4
|
|
5
|
-
class
|
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.
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
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
|
data/test/test_file_watch.rb
CHANGED
@@ -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,
|
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)
|
data/test/test_router.rb
ADDED
@@ -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
|
+
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
|