syntropy 0.2 → 0.4
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 +12 -4
- data/CHANGELOG.md +12 -0
- data/README.md +30 -11
- data/TODO.md +89 -97
- data/bin/syntropy +7 -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 +134 -50
- data/lib/syntropy/errors.rb +16 -2
- data/lib/syntropy/module.rb +26 -5
- data/lib/syntropy/request_extensions.rb +96 -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 +14 -49
- data/syntropy.gemspec +1 -1
- data/test/app/baz.rb +3 -0
- data/test/test_app.rb +57 -7
- data/test/test_side_run.rb +43 -0
- data/test/test_validation.rb +1 -1
- metadata +31 -3
data/docker-compose.yml
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
services:
|
2
|
+
backend:
|
3
|
+
build: .
|
4
|
+
privileged: true
|
5
|
+
restart: always
|
6
|
+
ports:
|
7
|
+
- 127.0.0.1:1234:1234
|
8
|
+
# expose:
|
9
|
+
# - 1234
|
10
|
+
volumes:
|
11
|
+
- .:/home/app
|
12
|
+
deploy:
|
13
|
+
# replicas: 1
|
14
|
+
resources:
|
15
|
+
limits:
|
16
|
+
memory: 500M
|
17
|
+
# restart: unless-stopped
|
18
|
+
logging:
|
19
|
+
driver: "json-file"
|
20
|
+
options:
|
21
|
+
max-size: "1M"
|
22
|
+
max-file: "10"
|
23
|
+
|
24
|
+
# healthcheck:
|
25
|
+
# test: "curl 'http://localhost:1234/?q=ping'"
|
26
|
+
# interval: "30s"
|
27
|
+
# timeout: "3s"
|
28
|
+
# start_period: "5s"
|
29
|
+
# retries: 3
|
30
|
+
|
31
|
+
proxy:
|
32
|
+
depends_on:
|
33
|
+
- backend
|
34
|
+
build:
|
35
|
+
context: ./proxy
|
36
|
+
dockerfile: Dockerfile
|
37
|
+
restart: always
|
38
|
+
volumes:
|
39
|
+
- ./proxy/etc/Caddyfile:/etc/caddy/Caddyfile
|
40
|
+
ports:
|
41
|
+
- "80:80"
|
42
|
+
- "443:443"
|
43
|
+
- "443:443/udp"
|
44
|
+
# env_file:
|
45
|
+
# - ./conf/caddy.env
|
46
|
+
# - ./conf/caddy_sensitive.env
|
47
|
+
logging:
|
48
|
+
driver: "json-file"
|
49
|
+
options:
|
50
|
+
max-size: "1M"
|
51
|
+
max-file: "10"
|
data/lib/syntropy/app.rb
CHANGED
@@ -1,7 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
require 'qeweney'
|
4
3
|
require 'json'
|
4
|
+
require 'yaml'
|
5
|
+
|
6
|
+
require 'qeweney'
|
5
7
|
require 'papercraft'
|
6
8
|
|
7
9
|
require 'syntropy/errors'
|
@@ -12,23 +14,23 @@ module Syntropy
|
|
12
14
|
class App
|
13
15
|
attr_reader :route_cache
|
14
16
|
|
15
|
-
def initialize(machine, src_path, mount_path,
|
17
|
+
def initialize(machine, src_path, mount_path, opts = {})
|
16
18
|
@machine = machine
|
17
|
-
@src_path = src_path
|
19
|
+
@src_path = File.expand_path(src_path)
|
18
20
|
@mount_path = mount_path
|
19
21
|
@route_cache = {}
|
20
|
-
@
|
22
|
+
@opts = opts
|
21
23
|
|
22
24
|
@relative_path_re = calculate_relative_path_re(mount_path)
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
p e.backtrace
|
30
|
-
end
|
25
|
+
@machine.spin do
|
26
|
+
# we do startup stuff asynchronously, in order to first let TP2 do its
|
27
|
+
# setup tasks
|
28
|
+
@machine.sleep 0.15
|
29
|
+
@opts[:logger]&.call("Serving from #{File.expand_path(@src_path)}")
|
30
|
+
start_file_watcher if opts[:watch_files]
|
31
31
|
end
|
32
|
+
|
33
|
+
@module_loader ||= Syntropy::ModuleLoader.new(@src_path, @opts)
|
32
34
|
end
|
33
35
|
|
34
36
|
def find_route(path, cache: true)
|
@@ -36,24 +38,16 @@ module Syntropy
|
|
36
38
|
return cached if cached
|
37
39
|
|
38
40
|
entry = calculate_route(path)
|
39
|
-
if entry[:kind] != :not_found
|
40
|
-
@route_cache[path] = entry if cache
|
41
|
-
end
|
41
|
+
@route_cache[path] = entry if entry[:kind] != :not_found && cache
|
42
42
|
entry
|
43
43
|
end
|
44
44
|
|
45
|
-
def invalidate_cache(fn)
|
46
|
-
invalidated_keys = []
|
47
|
-
@route_cache.each do |k, v|
|
48
|
-
invalidated_keys << k if v[:fn] == fn
|
49
|
-
end
|
50
|
-
|
51
|
-
invalidated_keys.each { @route_cache.delete(it) }
|
52
|
-
end
|
53
|
-
|
54
45
|
def call(req)
|
55
46
|
entry = find_route(req.path)
|
56
47
|
render_entry(req, entry)
|
48
|
+
rescue Syntropy::Error => e
|
49
|
+
msg = e.message
|
50
|
+
req.respond(msg.empty? ? nil : msg, ':status' => e.http_status)
|
57
51
|
rescue StandardError => e
|
58
52
|
p e
|
59
53
|
p e.backtrace
|
@@ -62,9 +56,37 @@ module Syntropy
|
|
62
56
|
|
63
57
|
private
|
64
58
|
|
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
|
+
|
65
87
|
def calculate_relative_path_re(mount_path)
|
66
88
|
mount_path = '' if mount_path == '/'
|
67
|
-
|
89
|
+
%r{^#{mount_path}(?:/(.*))?$}
|
68
90
|
end
|
69
91
|
|
70
92
|
FILE_KINDS = {
|
@@ -74,7 +96,7 @@ module Syntropy
|
|
74
96
|
NOT_FOUND = { kind: :not_found }
|
75
97
|
|
76
98
|
# We don't allow access to path with /.., or entries that start with _
|
77
|
-
FORBIDDEN_RE = /
|
99
|
+
FORBIDDEN_RE = %r{(/_)|((/\.\.)/?)}
|
78
100
|
|
79
101
|
def calculate_route(path)
|
80
102
|
return NOT_FOUND if path =~ FORBIDDEN_RE
|
@@ -95,7 +117,7 @@ module Syntropy
|
|
95
117
|
end
|
96
118
|
|
97
119
|
def file_entry(fn)
|
98
|
-
{ fn: fn, kind: FILE_KINDS[File.extname(fn)] || :static }
|
120
|
+
{ fn: File.expand_path(fn), kind: FILE_KINDS[File.extname(fn)] || :static }
|
99
121
|
end
|
100
122
|
|
101
123
|
def find_index_entry(dir)
|
@@ -126,7 +148,7 @@ module Syntropy
|
|
126
148
|
entry[:kind] == :module ? entry : NOT_FOUND
|
127
149
|
end
|
128
150
|
|
129
|
-
UP_TREE_PATH_RE =
|
151
|
+
UP_TREE_PATH_RE = %r{^(.+)?/[^/]+$}
|
130
152
|
|
131
153
|
def parent_path(path)
|
132
154
|
m = path.match(UP_TREE_PATH_RE)
|
@@ -136,21 +158,47 @@ module Syntropy
|
|
136
158
|
def render_entry(req, entry)
|
137
159
|
case entry[:kind]
|
138
160
|
when :not_found
|
139
|
-
req
|
161
|
+
respond_not_found(req, entry)
|
140
162
|
when :static
|
141
|
-
|
142
|
-
req.respond(IO.read(entry[:fn]), 'Content-Type' => entry[:mime_type])
|
163
|
+
respond_static(req, entry)
|
143
164
|
when :markdown
|
144
|
-
|
145
|
-
req.respond(body, 'Content-Type' => 'text/html')
|
165
|
+
respond_markdown(req, entry)
|
146
166
|
when :module
|
147
|
-
|
167
|
+
respond_module(req, entry)
|
148
168
|
else
|
149
|
-
raise
|
169
|
+
raise 'Invalid entry kind'
|
150
170
|
end
|
151
171
|
end
|
152
172
|
|
153
|
-
def
|
173
|
+
def respond_not_found(req, _entry)
|
174
|
+
headers = { ':status' => Qeweney::Status::NOT_FOUND }
|
175
|
+
case req.method
|
176
|
+
when 'head'
|
177
|
+
req.respond(nil, headers)
|
178
|
+
else
|
179
|
+
req.respond('Not found', headers)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
def respond_static(req, entry)
|
184
|
+
entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
|
185
|
+
headers = { 'Content-Type' => entry[:mime_type] }
|
186
|
+
req.respond_by_http_method(
|
187
|
+
'head' => [nil, headers],
|
188
|
+
'get' => -> { [IO.read(entry[:fn]), headers] }
|
189
|
+
)
|
190
|
+
end
|
191
|
+
|
192
|
+
def respond_markdown(req, entry)
|
193
|
+
entry[:mime_type] ||= Qeweney::MimeTypes[File.extname(entry[:fn])]
|
194
|
+
headers = { 'Content-Type' => entry[:mime_type] }
|
195
|
+
req.respond_by_http_method(
|
196
|
+
'head' => [nil, headers],
|
197
|
+
'get' => -> { [render_markdown(entry[:fn]), headers] }
|
198
|
+
)
|
199
|
+
end
|
200
|
+
|
201
|
+
def respond_module(req, entry)
|
154
202
|
entry[:code] ||= load_module(entry)
|
155
203
|
if entry[:code] == :invalid
|
156
204
|
req.respond(nil, ':status' => Qeweney::Status::INTERNAL_SERVER_ERROR)
|
@@ -158,6 +206,8 @@ module Syntropy
|
|
158
206
|
end
|
159
207
|
|
160
208
|
entry[:code].call(req)
|
209
|
+
rescue Syntropy::Error => e
|
210
|
+
req.respond(nil, ':status' => e.http_status)
|
161
211
|
rescue StandardError => e
|
162
212
|
p e
|
163
213
|
p e.backtrace
|
@@ -165,28 +215,62 @@ module Syntropy
|
|
165
215
|
end
|
166
216
|
|
167
217
|
def load_module(entry)
|
168
|
-
|
169
|
-
|
170
|
-
o
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
if o.is_a?(Papercraft::HTML)
|
175
|
-
return wrap_template(o)
|
176
|
-
else
|
177
|
-
return o
|
178
|
-
end
|
218
|
+
ref = entry[:fn].gsub(%r{^#{@src_path}/}, '').gsub(/\.rb$/, '')
|
219
|
+
o = @module_loader.load(ref)
|
220
|
+
o.is_a?(Papercraft::Template) ? wrap_template(o) : o
|
221
|
+
rescue Exception => e
|
222
|
+
@opts[:logger]&.call("Error while loading module #{ref}: #{e.message}")
|
223
|
+
:invalid
|
179
224
|
end
|
180
225
|
|
181
226
|
def wrap_template(templ)
|
182
|
-
|
227
|
+
lambda { |req|
|
183
228
|
body = templ.render
|
184
229
|
req.respond(body, 'Content-Type' => 'text/html')
|
185
230
|
}
|
186
231
|
end
|
187
232
|
|
188
|
-
def render_markdown(
|
189
|
-
|
233
|
+
def render_markdown(fn)
|
234
|
+
atts, md = parse_markdown_file(fn)
|
235
|
+
|
236
|
+
if atts[:layout]
|
237
|
+
layout = @module_loader.load("_layout/#{atts[:layout]}")
|
238
|
+
html = layout.apply { emit_markdown(md) }.render
|
239
|
+
else
|
240
|
+
html = Papercraft.markdown(md)
|
241
|
+
end
|
242
|
+
html
|
243
|
+
end
|
244
|
+
|
245
|
+
DATE_REGEXP = /(\d{4}\-\d{2}\-\d{2})/
|
246
|
+
FRONT_MATTER_REGEXP = /\A(---\s*\n.*?\n?)^((---|\.\.\.)\s*$\n?)/m
|
247
|
+
YAML_OPTS = {
|
248
|
+
permitted_classes: [Date],
|
249
|
+
symbolize_names: true
|
250
|
+
}
|
251
|
+
|
252
|
+
# Parses the markdown file at the given path.
|
253
|
+
#
|
254
|
+
# @param path [String] file path
|
255
|
+
# @return [Array] an tuple containing properties<Hash>, contents<String>
|
256
|
+
def parse_markdown_file(path)
|
257
|
+
content = IO.read(path) || ''
|
258
|
+
atts = {}
|
259
|
+
|
260
|
+
# Parse date from file name
|
261
|
+
if (m = path.match(DATE_REGEXP))
|
262
|
+
atts[:date] ||= Date.parse(m[1])
|
263
|
+
end
|
264
|
+
|
265
|
+
if (m = content.match(FRONT_MATTER_REGEXP))
|
266
|
+
front_matter = m[1]
|
267
|
+
content = m.post_match
|
268
|
+
|
269
|
+
yaml = YAML.safe_load(front_matter, **YAML_OPTS)
|
270
|
+
atts = atts.merge(yaml)
|
271
|
+
end
|
272
|
+
|
273
|
+
[atts, content]
|
190
274
|
end
|
191
275
|
end
|
192
276
|
end
|
data/lib/syntropy/errors.rb
CHANGED
@@ -4,17 +4,31 @@ require 'qeweney'
|
|
4
4
|
|
5
5
|
module Syntropy
|
6
6
|
class Error < StandardError
|
7
|
+
Status = Qeweney::Status
|
8
|
+
|
7
9
|
attr_reader :http_status
|
8
10
|
|
9
11
|
def initialize(status, msg = '')
|
10
|
-
@http_status = status || Qeweney::Status::INTERNAL_SERVER_ERROR
|
11
12
|
super(msg)
|
13
|
+
@http_status = status || Qeweney::Status::INTERNAL_SERVER_ERROR
|
14
|
+
end
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# Create class methods for common errors
|
18
|
+
{
|
19
|
+
not_found: Status::NOT_FOUND,
|
20
|
+
method_not_allowed: Status::METHOD_NOT_ALLOWED,
|
21
|
+
teapot: Status::TEAPOT
|
22
|
+
}
|
23
|
+
.each { |k, v|
|
24
|
+
define_method(k) { |msg = ''| new(v, msg) }
|
25
|
+
}
|
12
26
|
end
|
13
27
|
end
|
14
28
|
|
15
29
|
class ValidationError < Error
|
16
30
|
def initialize(msg)
|
17
|
-
|
31
|
+
super(Qeweney::Status::BAD_REQUEST, msg)
|
18
32
|
end
|
19
33
|
end
|
20
34
|
end
|
data/lib/syntropy/module.rb
CHANGED
@@ -1,37 +1,54 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
require 'papercraft'
|
4
|
+
|
3
5
|
module Syntropy
|
4
6
|
class ModuleLoader
|
5
7
|
def initialize(root, env)
|
6
8
|
@root = root
|
7
9
|
@env = env
|
8
|
-
@loaded = {}
|
10
|
+
@loaded = {} # maps ref to code
|
11
|
+
@fn_map = {} # maps filename to ref
|
9
12
|
end
|
10
13
|
|
11
14
|
def load(ref)
|
12
15
|
@loaded[ref] ||= load_module(ref)
|
13
16
|
end
|
14
17
|
|
18
|
+
def unload(fn)
|
19
|
+
ref = @fn_map[fn]
|
20
|
+
return if !ref
|
21
|
+
|
22
|
+
@loaded.delete(ref)
|
23
|
+
@fn_map.delete(fn)
|
24
|
+
end
|
25
|
+
|
15
26
|
private
|
16
27
|
|
17
28
|
def load_module(ref)
|
18
|
-
fn = File.join(@root, "#{ref}.rb")
|
19
|
-
|
29
|
+
fn = File.expand_path(File.join(@root, "#{ref}.rb"))
|
30
|
+
@fn_map[fn] = ref
|
31
|
+
raise "File not found #{fn}" if !File.file?(fn)
|
20
32
|
|
21
33
|
mod_body = IO.read(fn)
|
22
34
|
mod_ctx = Class.new(Syntropy::Module)
|
23
35
|
mod_ctx.loader = self
|
24
|
-
# mod_ctx = .new(self, @env)
|
25
36
|
mod_ctx.module_eval(mod_body, fn, 1)
|
26
37
|
|
27
38
|
export_value = mod_ctx.__export_value__
|
28
39
|
|
40
|
+
wrap_module(mod_ctx, export_value)
|
41
|
+
end
|
42
|
+
|
43
|
+
def wrap_module(mod_ctx, export_value)
|
29
44
|
case export_value
|
30
45
|
when nil
|
31
|
-
raise
|
46
|
+
raise 'No export found'
|
32
47
|
when Symbol
|
33
48
|
# TODO: verify export_value denotes a valid method
|
34
49
|
mod_ctx.new(@env)
|
50
|
+
when String
|
51
|
+
->(req) { req.respond(export_value) }
|
35
52
|
when Proc
|
36
53
|
export_value
|
37
54
|
else
|
@@ -57,6 +74,10 @@ module Syntropy
|
|
57
74
|
@__export_value__ = ref
|
58
75
|
end
|
59
76
|
|
77
|
+
def self.templ(&block)
|
78
|
+
Papercraft.html(&block)
|
79
|
+
end
|
80
|
+
|
60
81
|
def self.__export_value__
|
61
82
|
@__export_value__
|
62
83
|
end
|
@@ -0,0 +1,96 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'qeweney'
|
4
|
+
|
5
|
+
module Syntropy
|
6
|
+
module RequestExtensions
|
7
|
+
def ctx
|
8
|
+
@ctx ||= {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def validate_http_method(*accepted)
|
12
|
+
raise Syntropy::Error.method_not_allowed if !accepted.include?(method)
|
13
|
+
end
|
14
|
+
|
15
|
+
def respond_by_http_method(map)
|
16
|
+
value = map[self.method]
|
17
|
+
raise Syntropy::Error.method_not_allowed if !value
|
18
|
+
|
19
|
+
value = value.() if value.is_a?(Proc)
|
20
|
+
(body, headers) = value
|
21
|
+
respond(body, headers)
|
22
|
+
end
|
23
|
+
|
24
|
+
def respond_on_get(body, headers = {})
|
25
|
+
case self.method
|
26
|
+
when 'head'
|
27
|
+
respond(nil, headers)
|
28
|
+
when 'get'
|
29
|
+
respond(body, headers)
|
30
|
+
else
|
31
|
+
raise Syntropy::Error.method_not_allowed
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
def respond_on_post(body, headers = {})
|
36
|
+
case self.method
|
37
|
+
when 'head'
|
38
|
+
respond(nil, headers)
|
39
|
+
when 'post'
|
40
|
+
respond(body, headers)
|
41
|
+
else
|
42
|
+
raise Syntropy::Error.method_not_allowed
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def validate_param(name, *clauses)
|
47
|
+
value = query[name]
|
48
|
+
clauses.each do |c|
|
49
|
+
valid = param_is_valid?(value, c)
|
50
|
+
raise(Syntropy::ValidationError, 'Validation error') if !valid
|
51
|
+
|
52
|
+
value = param_convert(value, c)
|
53
|
+
end
|
54
|
+
value
|
55
|
+
end
|
56
|
+
|
57
|
+
private
|
58
|
+
|
59
|
+
BOOL_REGEXP = /^(t|f|true|false|on|off|1|0|yes|no)$/
|
60
|
+
BOOL_TRUE_REGEXP = /^(t|true|on|1|yes)$/
|
61
|
+
INTEGER_REGEXP = /^[+-]?[0-9]+$/
|
62
|
+
FLOAT_REGEXP = /^[+-]?[0-9]+(\.[0-9]+)?$/
|
63
|
+
|
64
|
+
def param_is_valid?(value, cond)
|
65
|
+
return cond.any? { |c| param_is_valid?(value, c) } if cond.is_a?(Array)
|
66
|
+
|
67
|
+
if value
|
68
|
+
if cond == :bool
|
69
|
+
return value =~ BOOL_REGEXP
|
70
|
+
elsif cond == Integer
|
71
|
+
return value =~ INTEGER_REGEXP
|
72
|
+
elsif cond == Float
|
73
|
+
return value =~ FLOAT_REGEXP
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
cond === value
|
78
|
+
end
|
79
|
+
|
80
|
+
def param_convert(value, klass)
|
81
|
+
if klass == :bool
|
82
|
+
value =~ BOOL_TRUE_REGEXP ? true : false
|
83
|
+
elsif klass == Integer
|
84
|
+
value.to_i
|
85
|
+
elsif klass == Float
|
86
|
+
value.to_f
|
87
|
+
elsif klass == Symbol
|
88
|
+
value.to_sym
|
89
|
+
else
|
90
|
+
value
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
Qeweney::Request.include(Syntropy::RequestExtensions)
|
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