syntropy 0.3 → 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/.rubocop.yml +10 -2
- data/CHANGELOG.md +14 -0
- data/README.md +30 -11
- data/TODO.md +135 -0
- data/bin/syntropy +3 -3
- data/cmd/setup/template/site/.gitignore +57 -0
- data/cmd/setup/template/site/Dockerfile +32 -0
- data/cmd/setup/template/site/Gemfile +3 -0
- data/cmd/setup/template/site/README.md +0 -0
- data/cmd/setup/template/site/bin/console +0 -0
- data/cmd/setup/template/site/bin/restart +0 -0
- data/cmd/setup/template/site/bin/server +0 -0
- data/cmd/setup/template/site/bin/start +0 -0
- data/cmd/setup/template/site/bin/stop +0 -0
- data/cmd/setup/template/site/docker-compose.yml +51 -0
- data/cmd/setup/template/site/proxy/Dockerfile +5 -0
- data/cmd/setup/template/site/proxy/etc/Caddyfile +7 -0
- data/cmd/setup/template/site/proxy/etc/tls_auto +2 -0
- data/cmd/setup/template/site/proxy/etc/tls_cloudflare +4 -0
- data/cmd/setup/template/site/proxy/etc/tls_custom +1 -0
- data/cmd/setup/template/site/proxy/etc/tls_selfsigned +1 -0
- data/cmd/setup/template/site/site/_layout/default.rb +11 -0
- data/cmd/setup/template/site/site/about.md +6 -0
- data/cmd/setup/template/site/site/articles/cage.rb +29 -0
- data/cmd/setup/template/site/site/articles/index.rb +3 -0
- data/cmd/setup/template/site/site/assets/css/style.css +40 -0
- data/cmd/setup/template/site/site/assets/img/syntropy.png +0 -0
- data/cmd/setup/template/site/site/index.rb +15 -0
- data/docker-compose.yml +51 -0
- data/lib/syntropy/app.rb +112 -134
- data/lib/syntropy/errors.rb +16 -2
- data/lib/syntropy/file_watch.rb +5 -4
- data/lib/syntropy/module.rb +26 -5
- data/lib/syntropy/request_extensions.rb +96 -0
- data/lib/syntropy/router.rb +208 -0
- data/lib/syntropy/rpc_api.rb +26 -9
- data/lib/syntropy/side_run.rb +46 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +15 -49
- data/syntropy.gemspec +1 -1
- data/test/app/baz.rb +3 -0
- data/test/app_custom/_site.rb +3 -0
- data/test/test_app.rb +96 -51
- data/test/test_file_watch.rb +4 -4
- data/test/test_router.rb +90 -0
- data/test/test_side_run.rb +43 -0
- data/test/test_validation.rb +1 -1
- metadata +34 -3
@@ -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/rpc_api.rb
CHANGED
@@ -11,7 +11,7 @@ module Syntropy
|
|
11
11
|
end
|
12
12
|
|
13
13
|
def call(req)
|
14
|
-
response, status =
|
14
|
+
response, status = __invoke__(req)
|
15
15
|
req.respond(
|
16
16
|
response.to_json,
|
17
17
|
':status' => status,
|
@@ -19,15 +19,17 @@ module Syntropy
|
|
19
19
|
)
|
20
20
|
end
|
21
21
|
|
22
|
-
|
23
|
-
|
22
|
+
private
|
23
|
+
|
24
|
+
def __invoke__(req)
|
25
|
+
q = req.validate_param(:q, String).to_sym
|
24
26
|
response = case req.method
|
25
27
|
when 'get'
|
26
|
-
|
28
|
+
__invoke_get__(q, req)
|
27
29
|
when 'post'
|
28
|
-
|
30
|
+
__invoke_post__(q, req)
|
29
31
|
else
|
30
|
-
raise Syntropy::Error.
|
32
|
+
raise Syntropy::Error.method_not_allowed
|
31
33
|
end
|
32
34
|
[{ status: 'OK', response: response }, Qeweney::Status::OK]
|
33
35
|
rescue => e
|
@@ -35,14 +37,29 @@ module Syntropy
|
|
35
37
|
p e
|
36
38
|
p e.backtrace
|
37
39
|
end
|
38
|
-
|
40
|
+
__error_response__(e)
|
41
|
+
end
|
42
|
+
|
43
|
+
def __invoke_get__(sym, req)
|
44
|
+
return send(sym, req) if respond_to?(sym)
|
45
|
+
|
46
|
+
err = respond_to?(:"#{sym}!") ? Syntropy::Error.method_not_allowed : Syntropy::Error.not_found
|
47
|
+
raise err
|
48
|
+
end
|
49
|
+
|
50
|
+
def __invoke_post__(sym, req)
|
51
|
+
sym_post = :"#{sym}!"
|
52
|
+
return send(sym_post, req) if respond_to?(sym_post)
|
53
|
+
|
54
|
+
err = respond_to?(sym) ? Syntropy::Error.method_not_allowed : Syntropy::Error.not_found
|
55
|
+
raise err
|
39
56
|
end
|
40
57
|
|
41
58
|
INTERNAL_SERVER_ERROR = Qeweney::Status::INTERNAL_SERVER_ERROR
|
42
59
|
|
43
|
-
def
|
60
|
+
def __error_response__(err)
|
44
61
|
http_status = err.respond_to?(:http_status) ? err.http_status : INTERNAL_SERVER_ERROR
|
45
|
-
error_name = err.class.name
|
62
|
+
error_name = err.class.name.split('::').last
|
46
63
|
[{ status: error_name, message: err.message }, http_status]
|
47
64
|
end
|
48
65
|
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'etc'
|
4
|
+
|
5
|
+
module Syntropy
|
6
|
+
module SideRun
|
7
|
+
class << self
|
8
|
+
def call(machine, &block)
|
9
|
+
setup if !@queue
|
10
|
+
|
11
|
+
# TODO: share mailboxes, acquire them with e.g. with_mailbox { |mbox| ... }
|
12
|
+
mailbox = Thread.current[:fiber_mailbox] ||= UM::Queue.new
|
13
|
+
machine.push(@queue, [mailbox, block])
|
14
|
+
result = machine.shift(mailbox)
|
15
|
+
result.is_a?(Exception) ? (raise result) : result
|
16
|
+
end
|
17
|
+
|
18
|
+
def setup
|
19
|
+
@queue = UM::Queue.new
|
20
|
+
count = (Etc.nprocessors - 1).clamp(2..6)
|
21
|
+
@workers = count.times.map {
|
22
|
+
Thread.new { side_run_worker(@queue) }
|
23
|
+
}
|
24
|
+
end
|
25
|
+
|
26
|
+
def side_run_worker(queue)
|
27
|
+
machine = UM.new
|
28
|
+
loop { handle_request(machine, queue) }
|
29
|
+
rescue UM::Terminate
|
30
|
+
# # We can also add a timeout here
|
31
|
+
# t0 = Time.now
|
32
|
+
# while !queue.empty? && (Time.now - t0) < 10
|
33
|
+
# handle_request(machine, queue)
|
34
|
+
# end
|
35
|
+
end
|
36
|
+
|
37
|
+
def handle_request(machine, queue)
|
38
|
+
response_mailbox, closure = machine.shift(queue)
|
39
|
+
result = closure.call
|
40
|
+
machine.push(response_mailbox, result)
|
41
|
+
rescue Exception => e
|
42
|
+
machine.push(response_mailbox, e)
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
@@ -8,64 +8,30 @@ require 'syntropy/errors'
|
|
8
8
|
require 'syntropy/connection_pool'
|
9
9
|
require 'syntropy/module'
|
10
10
|
require 'syntropy/rpc_api'
|
11
|
+
require 'syntropy/side_run'
|
12
|
+
require 'syntropy/router'
|
11
13
|
require 'syntropy/app'
|
14
|
+
require 'syntropy/request_extensions'
|
12
15
|
|
13
|
-
|
14
|
-
|
15
|
-
@ctx ||= {}
|
16
|
-
end
|
17
|
-
|
18
|
-
def validate_param(name, *clauses)
|
19
|
-
value = query[name]
|
20
|
-
clauses.each do |c|
|
21
|
-
valid = param_is_valid?(value, c)
|
22
|
-
raise(Syntropy::ValidationError, 'Validation error') if !valid
|
23
|
-
value = param_convert(value, c)
|
24
|
-
end
|
25
|
-
value
|
26
|
-
end
|
27
|
-
|
28
|
-
private
|
29
|
-
|
30
|
-
BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
|
31
|
-
BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
|
32
|
-
INTEGER_REGEXP = /^[\+\-]?[0-9]+$/
|
33
|
-
FLOAT_REGEXP = /^[\+\-]?[0-9]+(\.[0-9]+)?$/
|
16
|
+
module Syntropy
|
17
|
+
Status = Qeweney::Status
|
34
18
|
|
35
|
-
|
36
|
-
|
37
|
-
return (value && value =~ BOOL_REGEXP)
|
38
|
-
elsif cond == Integer
|
39
|
-
return (value && value =~ INTEGER_REGEXP)
|
40
|
-
elsif cond == Float
|
41
|
-
return (value && value =~ FLOAT_REGEXP)
|
42
|
-
elsif cond.is_a?(Array)
|
43
|
-
return cond.any? { |c| param_is_valid?(value, c) }
|
44
|
-
end
|
19
|
+
class << self
|
20
|
+
attr_accessor :machine
|
45
21
|
|
46
|
-
|
47
|
-
|
22
|
+
def side_run(&block)
|
23
|
+
raise 'Syntropy.machine not set' if !@machine
|
48
24
|
|
49
|
-
|
50
|
-
if klass == :bool
|
51
|
-
value = value =~ BOOL_TRUE_REGEXP ? true : false
|
52
|
-
elsif klass == Integer
|
53
|
-
value = value.to_i
|
54
|
-
elsif klass == Float
|
55
|
-
value = value.to_f
|
56
|
-
else
|
57
|
-
value
|
25
|
+
SideRun.call(@machine, &block)
|
58
26
|
end
|
59
27
|
end
|
60
|
-
end
|
61
28
|
|
62
|
-
module Syntropy
|
63
29
|
def colorize(color_code)
|
64
30
|
"\e[#{color_code}m#{self}\e[0m"
|
65
31
|
end
|
66
32
|
|
67
33
|
GREEN = "\e[32m"
|
68
|
-
|
34
|
+
CLEAR = "\e[0m"
|
69
35
|
YELLOW = "\e[33m"
|
70
36
|
|
71
37
|
BANNER = (
|
@@ -73,10 +39,10 @@ module Syntropy
|
|
73
39
|
" #{GREEN}\n"\
|
74
40
|
" #{GREEN} ooo\n"\
|
75
41
|
" #{GREEN}ooooo\n"\
|
76
|
-
" #{GREEN} ooo vvv #{
|
77
|
-
" #{GREEN} o vvvvv #{
|
78
|
-
" #{GREEN} #{YELLOW}|#{GREEN} vvv o #{
|
42
|
+
" #{GREEN} ooo vvv #{CLEAR}Syntropy - a web framework for Ruby\n"\
|
43
|
+
" #{GREEN} o vvvvv #{CLEAR}--------------------------------------\n"\
|
44
|
+
" #{GREEN} #{YELLOW}|#{GREEN} vvv o #{CLEAR}https://github.com/noteflakes/syntropy\n"\
|
79
45
|
" #{GREEN} :#{YELLOW}|#{GREEN}:::#{YELLOW}|#{GREEN}::#{YELLOW}|#{GREEN}:\n"\
|
80
|
-
"
|
46
|
+
"#{YELLOW}+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\e[0m\n\n"
|
81
47
|
)
|
82
48
|
end
|
data/syntropy.gemspec
CHANGED
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.add_dependency 'json', '2.12.2'
|
26
26
|
s.add_dependency 'papercraft', '1.4'
|
27
27
|
s.add_dependency 'qeweney', '0.21'
|
28
|
-
s.add_dependency 'tp2', '0.
|
28
|
+
s.add_dependency 'tp2', '0.13.2'
|
29
29
|
s.add_dependency 'uringmachine', '0.15'
|
30
30
|
|
31
31
|
s.add_dependency 'listen', '3.9.0'
|
data/test/app/baz.rb
ADDED
data/test/test_app.rb
CHANGED
@@ -2,57 +2,23 @@
|
|
2
2
|
|
3
3
|
require_relative 'helper'
|
4
4
|
|
5
|
-
class
|
5
|
+
class AppTest < Minitest::Test
|
6
|
+
Status = Qeweney::Status
|
7
|
+
|
6
8
|
APP_ROOT = File.join(__dir__, 'app')
|
7
9
|
|
8
10
|
def setup
|
9
11
|
@machine = UM.new
|
10
12
|
|
11
13
|
@tmp_path = '/test/tmp'
|
12
|
-
@tmp_fn = File.join(APP_ROOT,
|
13
|
-
|
14
|
-
@app = Syntropy::App.
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
def test_find_route
|
22
|
-
entry = @app.find_route('/')
|
23
|
-
assert_equal :not_found, entry[:kind]
|
24
|
-
|
25
|
-
entry = @app.find_route('/test')
|
26
|
-
assert_equal :static, entry[:kind]
|
27
|
-
assert_equal full_path('index.html'), entry[:fn]
|
28
|
-
|
29
|
-
entry = @app.find_route('/test/about')
|
30
|
-
assert_equal :module, entry[:kind]
|
31
|
-
assert_equal full_path('about/index.rb'), entry[:fn]
|
32
|
-
|
33
|
-
entry = @app.find_route('/test/../test_app.rb')
|
34
|
-
assert_equal :not_found, entry[:kind]
|
35
|
-
|
36
|
-
entry = @app.find_route('/test/_layout/default')
|
37
|
-
assert_equal :not_found, entry[:kind]
|
38
|
-
|
39
|
-
entry = @app.find_route('/test/api')
|
40
|
-
assert_equal :module, entry[:kind]
|
41
|
-
assert_equal full_path('api+.rb'), entry[:fn]
|
42
|
-
|
43
|
-
entry = @app.find_route('/test/api/foo/bar')
|
44
|
-
assert_equal :module, entry[:kind]
|
45
|
-
assert_equal full_path('api+.rb'), entry[:fn]
|
46
|
-
|
47
|
-
entry = @app.find_route('/test/api/foo/../bar')
|
48
|
-
assert_equal :not_found, entry[:kind]
|
49
|
-
|
50
|
-
entry = @app.find_route('/test/api_1')
|
51
|
-
assert_equal :not_found, entry[:kind]
|
52
|
-
|
53
|
-
entry = @app.find_route('/test/about/foo')
|
54
|
-
assert_equal :markdown, entry[:kind]
|
55
|
-
assert_equal full_path('about/foo.md'), entry[:fn]
|
14
|
+
@tmp_fn = File.join(APP_ROOT, 'tmp.rb')
|
15
|
+
|
16
|
+
@app = Syntropy::App.load(
|
17
|
+
machine: @machine,
|
18
|
+
location: APP_ROOT,
|
19
|
+
mount_path: '/test',
|
20
|
+
watch_files: 0.05
|
21
|
+
)
|
56
22
|
end
|
57
23
|
|
58
24
|
def make_request(*, **)
|
@@ -63,39 +29,91 @@ class AppRoutingTest < Minitest::Test
|
|
63
29
|
|
64
30
|
def test_app_rendering
|
65
31
|
req = make_request(':method' => 'GET', ':path' => '/')
|
66
|
-
assert_equal
|
32
|
+
assert_equal 'Not found', req.response_body
|
33
|
+
assert_equal Status::NOT_FOUND, req.response_status
|
34
|
+
|
35
|
+
req = make_request(':method' => 'HEAD', ':path' => '/')
|
36
|
+
assert_nil req.response_body
|
37
|
+
assert_equal Status::NOT_FOUND, req.response_status
|
38
|
+
|
39
|
+
req = make_request(':method' => 'POST', ':path' => '/')
|
40
|
+
assert_equal 'Not found', req.response_body
|
41
|
+
assert_equal Status::NOT_FOUND, req.response_status
|
67
42
|
|
68
43
|
req = make_request(':method' => 'GET', ':path' => '/test')
|
69
|
-
assert_equal
|
44
|
+
assert_equal Status::OK, req.response_status
|
70
45
|
assert_equal '<h1>Hello, world!</h1>', req.response_body
|
71
46
|
|
47
|
+
req = make_request(':method' => 'HEAD', ':path' => '/test')
|
48
|
+
assert_equal Status::OK, req.response_status
|
49
|
+
assert_nil req.response_body
|
50
|
+
|
51
|
+
req = make_request(':method' => 'POST', ':path' => '/test')
|
52
|
+
assert_equal Status::METHOD_NOT_ALLOWED, req.response_status
|
53
|
+
assert_nil req.response_body
|
54
|
+
|
72
55
|
req = make_request(':method' => 'GET', ':path' => '/test/index')
|
73
56
|
assert_equal '<h1>Hello, world!</h1>', req.response_body
|
74
57
|
|
75
58
|
req = make_request(':method' => 'GET', ':path' => '/test/index.html')
|
76
59
|
assert_equal '<h1>Hello, world!</h1>', req.response_body
|
60
|
+
assert_equal 'text/html', req.response_headers['Content-Type']
|
61
|
+
|
62
|
+
req = make_request(':method' => 'HEAD', ':path' => '/test/index.html')
|
63
|
+
assert_nil req.response_body
|
64
|
+
assert_equal 'text/html', req.response_headers['Content-Type']
|
65
|
+
|
66
|
+
req = make_request(':method' => 'POST', ':path' => '/test/index.html')
|
67
|
+
assert_nil req.response_body
|
68
|
+
assert_equal Status::METHOD_NOT_ALLOWED, req.response_status
|
77
69
|
|
78
70
|
req = make_request(':method' => 'GET', ':path' => '/test/assets/style.css')
|
79
71
|
assert_equal '* { color: beige }', req.response_body
|
72
|
+
assert_equal 'text/css', req.response_headers['Content-Type']
|
80
73
|
|
81
74
|
req = make_request(':method' => 'GET', ':path' => '/assets/style.css')
|
82
|
-
assert_equal
|
75
|
+
assert_equal Status::NOT_FOUND, req.response_status
|
83
76
|
|
84
77
|
req = make_request(':method' => 'GET', ':path' => '/test/api?q=get')
|
85
78
|
assert_equal({ status: 'OK', response: 0 }, req.response_json)
|
86
79
|
|
80
|
+
req = make_request(':method' => 'POST', ':path' => '/test/api?q=get')
|
81
|
+
assert_equal Status::METHOD_NOT_ALLOWED, req.response_status
|
82
|
+
assert_equal({ status: 'Error', message: '' }, req.response_json)
|
83
|
+
|
87
84
|
req = make_request(':method' => 'GET', ':path' => '/test/api/foo?q=get')
|
88
85
|
assert_equal({ status: 'OK', response: 0 }, req.response_json)
|
89
86
|
|
90
87
|
req = make_request(':method' => 'POST', ':path' => '/test/api?q=incr')
|
91
88
|
assert_equal({ status: 'OK', response: 1 }, req.response_json)
|
92
89
|
|
90
|
+
req = make_request(':method' => 'GET', ':path' => '/test/api?q=incr')
|
91
|
+
assert_equal Status::METHOD_NOT_ALLOWED, req.response_status
|
92
|
+
assert_equal({ status: 'Error', message: '' }, req.response_json)
|
93
|
+
|
93
94
|
req = make_request(':method' => 'POST', ':path' => '/test/api/foo?q=incr')
|
94
|
-
assert_equal({ status: '
|
95
|
-
assert_equal
|
95
|
+
assert_equal({ status: 'Error', message: 'Teapot' }, req.response_json)
|
96
|
+
assert_equal Status::TEAPOT, req.response_status
|
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
|
96
101
|
|
97
102
|
req = make_request(':method' => 'GET', ':path' => '/test/bar')
|
98
103
|
assert_equal 'foobar', req.response_body
|
104
|
+
assert_equal Status::OK, req.response_status
|
105
|
+
|
106
|
+
req = make_request(':method' => 'POST', ':path' => '/test/bar')
|
107
|
+
assert_equal 'foobar', req.response_body
|
108
|
+
assert_equal Status::OK, req.response_status
|
109
|
+
|
110
|
+
req = make_request(':method' => 'GET', ':path' => '/test/baz')
|
111
|
+
assert_equal 'foobar', req.response_body
|
112
|
+
assert_equal Status::OK, req.response_status
|
113
|
+
|
114
|
+
req = make_request(':method' => 'POST', ':path' => '/test/baz')
|
115
|
+
assert_nil req.response_body
|
116
|
+
assert_equal Status::METHOD_NOT_ALLOWED, req.response_status
|
99
117
|
|
100
118
|
req = make_request(':method' => 'GET', ':path' => '/test/about')
|
101
119
|
assert_equal 'About', req.response_body.chomp
|
@@ -104,7 +122,7 @@ class AppRoutingTest < Minitest::Test
|
|
104
122
|
assert_equal '<p>Hello from Markdown</p>', req.response_body.chomp
|
105
123
|
|
106
124
|
req = make_request(':method' => 'GET', ':path' => '/test/about/foo/bar')
|
107
|
-
assert_equal
|
125
|
+
assert_equal Status::NOT_FOUND, req.response_status
|
108
126
|
end
|
109
127
|
|
110
128
|
def test_app_file_watching
|
@@ -123,3 +141,30 @@ class AppRoutingTest < Minitest::Test
|
|
123
141
|
IO.write(@tmp_fn, orig_body) if orig_body
|
124
142
|
end
|
125
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)
|