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.
Files changed (49) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +10 -2
  3. data/CHANGELOG.md +14 -0
  4. data/README.md +30 -11
  5. data/TODO.md +135 -0
  6. data/bin/syntropy +3 -3
  7. data/cmd/setup/template/site/.gitignore +57 -0
  8. data/cmd/setup/template/site/Dockerfile +32 -0
  9. data/cmd/setup/template/site/Gemfile +3 -0
  10. data/cmd/setup/template/site/README.md +0 -0
  11. data/cmd/setup/template/site/bin/console +0 -0
  12. data/cmd/setup/template/site/bin/restart +0 -0
  13. data/cmd/setup/template/site/bin/server +0 -0
  14. data/cmd/setup/template/site/bin/start +0 -0
  15. data/cmd/setup/template/site/bin/stop +0 -0
  16. data/cmd/setup/template/site/docker-compose.yml +51 -0
  17. data/cmd/setup/template/site/proxy/Dockerfile +5 -0
  18. data/cmd/setup/template/site/proxy/etc/Caddyfile +7 -0
  19. data/cmd/setup/template/site/proxy/etc/tls_auto +2 -0
  20. data/cmd/setup/template/site/proxy/etc/tls_cloudflare +4 -0
  21. data/cmd/setup/template/site/proxy/etc/tls_custom +1 -0
  22. data/cmd/setup/template/site/proxy/etc/tls_selfsigned +1 -0
  23. data/cmd/setup/template/site/site/_layout/default.rb +11 -0
  24. data/cmd/setup/template/site/site/about.md +6 -0
  25. data/cmd/setup/template/site/site/articles/cage.rb +29 -0
  26. data/cmd/setup/template/site/site/articles/index.rb +3 -0
  27. data/cmd/setup/template/site/site/assets/css/style.css +40 -0
  28. data/cmd/setup/template/site/site/assets/img/syntropy.png +0 -0
  29. data/cmd/setup/template/site/site/index.rb +15 -0
  30. data/docker-compose.yml +51 -0
  31. data/lib/syntropy/app.rb +112 -134
  32. data/lib/syntropy/errors.rb +16 -2
  33. data/lib/syntropy/file_watch.rb +5 -4
  34. data/lib/syntropy/module.rb +26 -5
  35. data/lib/syntropy/request_extensions.rb +96 -0
  36. data/lib/syntropy/router.rb +208 -0
  37. data/lib/syntropy/rpc_api.rb +26 -9
  38. data/lib/syntropy/side_run.rb +46 -0
  39. data/lib/syntropy/version.rb +1 -1
  40. data/lib/syntropy.rb +15 -49
  41. data/syntropy.gemspec +1 -1
  42. data/test/app/baz.rb +3 -0
  43. data/test/app_custom/_site.rb +3 -0
  44. data/test/test_app.rb +96 -51
  45. data/test/test_file_watch.rb +4 -4
  46. data/test/test_router.rb +90 -0
  47. data/test/test_side_run.rb +43 -0
  48. data/test/test_validation.rb +1 -1
  49. 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
@@ -11,7 +11,7 @@ module Syntropy
11
11
  end
12
12
 
13
13
  def call(req)
14
- response, status = invoke(req)
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
- def invoke(req)
23
- q = req.validate_param(:q, String)
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
- send(q.to_sym, req)
28
+ __invoke_get__(q, req)
27
29
  when 'post'
28
- send(:"#{q}!", req)
30
+ __invoke_post__(q, req)
29
31
  else
30
- raise Syntropy::Error.new(Qeweney::Status::METHOD_NOT_ALLOWED)
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
- error_response(e)
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 error_response(err)
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
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.3'
4
+ VERSION = '0.5'
5
5
  end
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
- class Qeweney::Request
14
- def ctx
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
- def param_is_valid?(value, cond)
36
- if cond == :bool
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
- cond === value
47
- end
22
+ def side_run(&block)
23
+ raise 'Syntropy.machine not set' if !@machine
48
24
 
49
- def param_convert(value, klass)
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
- WHITE = "\e[0m"
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 #{WHITE}Syntropy - a web framework for Ruby\n"\
77
- " #{GREEN} o vvvvv #{WHITE}--------------------------------------\n"\
78
- " #{GREEN} #{YELLOW}|#{GREEN} vvv o #{WHITE}https://github.com/noteflakes/syntropy\n"\
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
- " #{YELLOW}++++++++++++\e[0m\n\n"
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.12.3.1'
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
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond_on_get('foobar')
3
+ }
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond(nil, ':status' => Status::TEAPOT)
3
+ }
data/test/test_app.rb CHANGED
@@ -2,57 +2,23 @@
2
2
 
3
3
  require_relative 'helper'
4
4
 
5
- class AppRoutingTest < Minitest::Test
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, "tmp.rb")
13
-
14
- @app = Syntropy::App.new(@machine, APP_ROOT, '/test', watch_files: 0.05)
15
- end
16
-
17
- def full_path(fn)
18
- File.join(APP_ROOT, fn)
19
- end
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 Qeweney::Status::NOT_FOUND, req.response_status
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 Qeweney::Status::OK, req.response_status
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 Qeweney::Status::NOT_FOUND, req.response_status
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: 'Syntropy::Error', message: 'Teapot' }, req.response_json)
95
- assert_equal Qeweney::Status::TEAPOT, req.response_status
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 Qeweney::Status::NOT_FOUND, req.response_status
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
@@ -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)