syntropy 0.31.0 → 0.32.0
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/.gitignore +2 -0
- data/CHANGELOG.md +13 -0
- data/TODO.md +46 -1
- data/cmd/console.rb +77 -0
- data/cmd/serve.rb +1 -1
- data/examples/blog/app/_layout/default.rb +11 -0
- data/examples/blog/app/_lib/post_store.rb +47 -0
- data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
- data/examples/blog/app/_setup.rb +4 -0
- data/examples/blog/app/index.rb +7 -0
- data/examples/blog/app/posts/[id]/edit.rb +33 -0
- data/examples/blog/app/posts/[id]/index.rb +58 -0
- data/examples/blog/app/posts/index.rb +38 -0
- data/examples/blog/app/posts/new.rb +29 -0
- data/examples/mcp-oauth/README.md +3 -3
- data/examples/mcp-oauth/app/mcp.rb +55 -8
- data/examples/mcp-oauth/app/oauth/authorize.rb +0 -8
- data/examples/mcp-oauth/app/oauth/register.rb +0 -1
- data/lib/syntropy/app.rb +23 -9
- data/lib/syntropy/db/connection_pool.rb +71 -0
- data/lib/syntropy/db/schema.rb +92 -0
- data/lib/syntropy/db/store.rb +31 -0
- data/lib/syntropy/http/io_extensions.rb +33 -5
- data/lib/syntropy/http/server_connection.rb +6 -53
- data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
- data/lib/syntropy/request/request_info.rb +3 -4
- data/lib/syntropy/request/validation.rb +1 -2
- data/lib/syntropy/test.rb +13 -1
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +4 -2
- data/syntropy.gemspec +2 -1
- data/test/app/_hook.rb +1 -1
- data/test/app/by_method.rb +9 -0
- data/test/app_setup/_setup.rb +7 -0
- data/test/app_setup/index.rb +1 -0
- data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
- data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
- data/test/schema/2026-01-02-foo.rb +12 -0
- data/test/schema/2026-05-30-bar.rb +7 -0
- data/test/test_app.rb +58 -3
- data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
- data/test/test_db_schema.rb +96 -0
- data/test/test_db_store.rb +24 -0
- data/test/test_http_protocol.rb +250 -0
- data/test/test_http_server_connection.rb +10 -19
- data/test/test_json_api.rb +1 -1
- data/test/{test_module.rb → test_module_loader.rb} +31 -0
- data/test/test_request.rb +7 -4
- data/test/test_server.rb +9 -13
- metadata +48 -12
- data/lib/syntropy/connection_pool.rb +0 -61
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'extralite'
|
|
4
|
+
|
|
5
|
+
module Syntropy
|
|
6
|
+
module DB
|
|
7
|
+
class ConnectionPool
|
|
8
|
+
attr_reader :count
|
|
9
|
+
|
|
10
|
+
def initialize(machine, fn, max_conn)
|
|
11
|
+
@machine = machine
|
|
12
|
+
@fn = fn
|
|
13
|
+
@count = 0
|
|
14
|
+
@max_conn = max_conn
|
|
15
|
+
@queue = UM::Queue.new
|
|
16
|
+
@key = :"connection_pool_#{object_id}"
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def with_db
|
|
20
|
+
if (db = Thread.current[@key])
|
|
21
|
+
@machine.snooze
|
|
22
|
+
return yield(db)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
db = checkout
|
|
26
|
+
begin
|
|
27
|
+
Thread.current[@key] = db
|
|
28
|
+
yield(db)
|
|
29
|
+
ensure
|
|
30
|
+
Thread.current[@key] = nil
|
|
31
|
+
checkin(db)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def query(sql, *, **, &)
|
|
36
|
+
with_db { it.query(sql, *, **, &) }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def execute(sql, *, **)
|
|
40
|
+
with_db { it.execute(sql, *, **) }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def close
|
|
44
|
+
while @queue.count > 0
|
|
45
|
+
db = @machine.shift(@queue)
|
|
46
|
+
db.close
|
|
47
|
+
@count -= 1
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def checkout
|
|
54
|
+
return make_db_instance if @queue.count == 0 && @count < @max_conn
|
|
55
|
+
|
|
56
|
+
@machine.shift(@queue)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def checkin(db)
|
|
60
|
+
@machine.push(@queue, db)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def make_db_instance
|
|
64
|
+
Extralite::Database.new(@fn, wal: true).tap do
|
|
65
|
+
@count += 1
|
|
66
|
+
it.on_progress(mode: :at_least_once, period: 320, tick: 10) { @machine.snooze }
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
end
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Syntropy
|
|
4
|
+
module DB
|
|
5
|
+
class Schema
|
|
6
|
+
def initialize(module_loader: nil, schema_root: '_schema', &)
|
|
7
|
+
@migrations = {}
|
|
8
|
+
@module_loader = module_loader
|
|
9
|
+
@schema_root = schema_root
|
|
10
|
+
load_schema_from_modules if @module_loader
|
|
11
|
+
run_schema_block(&) if block_given?
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def apply(connection_pool)
|
|
15
|
+
execute_migrations(connection_pool)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def current_version(connection_pool)
|
|
19
|
+
connection_pool.with_db do |db|
|
|
20
|
+
get_schema_version(db)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def load_schema_from_modules
|
|
27
|
+
modules = @module_loader.list(@schema_root)
|
|
28
|
+
modules.each do |name|
|
|
29
|
+
@migrations[File.basename(name)] = @module_loader.load(name)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class SchemaBlockRunner
|
|
34
|
+
def initialize(migrations, &)
|
|
35
|
+
@migrations = migrations
|
|
36
|
+
instance_eval(&)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def initial(&block)
|
|
40
|
+
@migrations['0000'] = block
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def version(key, &block)
|
|
44
|
+
@migrations[key] = block
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def run_schema_block(&)
|
|
49
|
+
SchemaBlockRunner.new(@migrations, &)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def execute_migrations(connection_pool)
|
|
53
|
+
connection_pool.with_db do |db|
|
|
54
|
+
current_version = get_schema_version(db)
|
|
55
|
+
migrations_keys = @migrations.keys.sort
|
|
56
|
+
migrations_keys.select { it > current_version } if current_version
|
|
57
|
+
|
|
58
|
+
migrations_keys.each do |key|
|
|
59
|
+
db.transaction do
|
|
60
|
+
@migrations[key].(db)
|
|
61
|
+
set_schema_version(db, key)
|
|
62
|
+
current_version = key
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
current_version
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def get_schema_version(db)
|
|
71
|
+
db.execute <<~SQL
|
|
72
|
+
create table if not exists __syntropy_schema__(
|
|
73
|
+
k text primary key,
|
|
74
|
+
v text
|
|
75
|
+
);
|
|
76
|
+
SQL
|
|
77
|
+
db.query_single_splat <<~SQL
|
|
78
|
+
select v from __syntropy_schema__
|
|
79
|
+
where k = 'version'
|
|
80
|
+
SQL
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def set_schema_version(db, version)
|
|
84
|
+
db.execute <<~SQL, v: version
|
|
85
|
+
insert into __syntropy_schema__ (k, v)
|
|
86
|
+
values ('version', :v)
|
|
87
|
+
on conflict(k) do update set v = :v
|
|
88
|
+
SQL
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Syntropy
|
|
4
|
+
module DB
|
|
5
|
+
class Store
|
|
6
|
+
def initialize(connection_pool)
|
|
7
|
+
@connection_pool = connection_pool
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def query(sql, *, **)
|
|
11
|
+
@connection_pool.with_db { it.query(sql, *, **) }
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def query_single_row(sql, *, **)
|
|
15
|
+
@connection_pool.with_db { it.query_single(sql, *, **) }
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def query_single_value(sql, *, **)
|
|
19
|
+
@connection_pool.with_db { it.query_single_splat(sql, *, **) }
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def execute(sql, *, **)
|
|
23
|
+
@connection_pool.with_db { it.execute(sql, *, **) }
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def transaction(&)
|
|
27
|
+
@connection_pool.with_db(&)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -5,13 +5,13 @@ require 'syntropy/errors'
|
|
|
5
5
|
module Syntropy
|
|
6
6
|
module HTTP
|
|
7
7
|
module ProtocolMethods
|
|
8
|
-
RE_REQUEST_LINE = /^(
|
|
8
|
+
RE_REQUEST_LINE = /^(get|head|options|trace|put|delete|post|patch|connect)\s+([^\s]+)\s+HTTP\/([019\.]{1,3})/i
|
|
9
9
|
RE_RESPONSE_LINE = /^HTTP\/1\.1\s+(\d{3})(\s+.+)?$/i
|
|
10
|
-
RE_HEADER_LINE = /^([a-z0-9
|
|
10
|
+
RE_HEADER_LINE = /^([a-z0-9\-]+):\s+(.+)/i
|
|
11
11
|
|
|
12
12
|
MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
|
|
13
13
|
MAX_RESPONSE_LINE_LEN = 1 << 8 # 256
|
|
14
|
-
MAX_HEADER_LINE_LEN = 1 <<
|
|
14
|
+
MAX_HEADER_LINE_LEN = 1 << 13 # 8KB
|
|
15
15
|
MAX_CHUNK_SIZE_LEN = 16
|
|
16
16
|
|
|
17
17
|
# @return [Hash] headers
|
|
@@ -27,8 +27,7 @@ module Syntropy
|
|
|
27
27
|
|
|
28
28
|
headers = {
|
|
29
29
|
':method' => m[1].downcase,
|
|
30
|
-
':path' => m[2]
|
|
31
|
-
':protocol' => 'http/1.1'
|
|
30
|
+
':path' => m[2]
|
|
32
31
|
}
|
|
33
32
|
|
|
34
33
|
loop do
|
|
@@ -93,6 +92,21 @@ module Syntropy
|
|
|
93
92
|
nil
|
|
94
93
|
end
|
|
95
94
|
|
|
95
|
+
def http_skip_body(headers)
|
|
96
|
+
content_length = headers['content-length']
|
|
97
|
+
if content_length
|
|
98
|
+
return skip(content_length.to_i)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
|
102
|
+
if chunked_encoding
|
|
103
|
+
while http_skip_cte_chunk
|
|
104
|
+
end
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
nil
|
|
108
|
+
end
|
|
109
|
+
|
|
96
110
|
def http_read_body_chunk(headers)
|
|
97
111
|
content_length = headers['content-length']
|
|
98
112
|
if content_length
|
|
@@ -141,6 +155,20 @@ module Syntropy
|
|
|
141
155
|
|
|
142
156
|
buffer ? (buffer << chunk) : chunk
|
|
143
157
|
end
|
|
158
|
+
|
|
159
|
+
def http_skip_cte_chunk
|
|
160
|
+
chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
|
|
161
|
+
return if !chunk_size_str
|
|
162
|
+
|
|
163
|
+
chunk_size = chunk_size_str.to_i(16)
|
|
164
|
+
if chunk_size == 0
|
|
165
|
+
read_line(0)
|
|
166
|
+
return nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
chunk = skip(chunk_size)
|
|
170
|
+
read_line(0)
|
|
171
|
+
end
|
|
144
172
|
end
|
|
145
173
|
end
|
|
146
174
|
end
|
|
@@ -48,13 +48,17 @@ module Syntropy
|
|
|
48
48
|
# connection should be persisted.
|
|
49
49
|
def serve_request
|
|
50
50
|
@closed = nil
|
|
51
|
-
headers =
|
|
51
|
+
headers = @io.http_read_request_headers
|
|
52
52
|
return false if !headers
|
|
53
53
|
|
|
54
54
|
request = Syntropy::Request.new(headers, self)
|
|
55
55
|
|
|
56
56
|
@app.call(request)
|
|
57
|
-
persist_connection?(headers)
|
|
57
|
+
persist = persist_connection?(headers)
|
|
58
|
+
if persist && !headers[':body-done-reading'] && (headers['content-length'] || headers['transfer-encoding'])
|
|
59
|
+
get_body(request)
|
|
60
|
+
end
|
|
61
|
+
persist
|
|
58
62
|
rescue StandardError => e
|
|
59
63
|
handle_error(request, e)
|
|
60
64
|
false
|
|
@@ -264,57 +268,6 @@ module Syntropy
|
|
|
264
268
|
return connection != 'close'
|
|
265
269
|
end
|
|
266
270
|
|
|
267
|
-
def parse_headers
|
|
268
|
-
headers = get_request_line(MAX_REQUEST_LINE_LEN)
|
|
269
|
-
return nil if !headers
|
|
270
|
-
|
|
271
|
-
loop do
|
|
272
|
-
line = @io.read_line(MAX_HEADER_LINE_LEN)
|
|
273
|
-
break if line.nil? || line.empty?
|
|
274
|
-
|
|
275
|
-
m = line.match(RE_HEADER_LINE)
|
|
276
|
-
raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
|
|
277
|
-
|
|
278
|
-
headers[m[1].downcase] = m[2]
|
|
279
|
-
end
|
|
280
|
-
|
|
281
|
-
headers
|
|
282
|
-
end
|
|
283
|
-
|
|
284
|
-
def get_request_line(buf)
|
|
285
|
-
line = @io.read_line(MAX_REQUEST_LINE_LEN)
|
|
286
|
-
return nil if !line
|
|
287
|
-
|
|
288
|
-
m = line.match(RE_REQUEST_LINE)
|
|
289
|
-
raise ProtocolError, 'Invalid request line' if !m
|
|
290
|
-
|
|
291
|
-
http_version = m[3]
|
|
292
|
-
raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
|
|
293
|
-
|
|
294
|
-
{
|
|
295
|
-
':method' => m[1].downcase,
|
|
296
|
-
':path' => m[2],
|
|
297
|
-
':protocol' => 'http/1.1'
|
|
298
|
-
}
|
|
299
|
-
end
|
|
300
|
-
|
|
301
|
-
def read_chunk(headers, buffer)
|
|
302
|
-
chunk_size_str = @io.read_line(MAX_CHUNK_SIZE_LEN)
|
|
303
|
-
return nil if !chunk_size_str
|
|
304
|
-
|
|
305
|
-
chunk_size = chunk_size_str.to_i(16)
|
|
306
|
-
if chunk_size == 0
|
|
307
|
-
headers[':body-done-reading'] = true
|
|
308
|
-
@io.read_line(0)
|
|
309
|
-
return nil
|
|
310
|
-
end
|
|
311
|
-
|
|
312
|
-
chunk = @io.read(chunk_size)
|
|
313
|
-
@io.read_line(0)
|
|
314
|
-
|
|
315
|
-
buffer ? (buffer << chunk) : chunk
|
|
316
|
-
end
|
|
317
|
-
|
|
318
271
|
INTERNAL_HEADER_REGEXP = /^:/
|
|
319
272
|
|
|
320
273
|
# Formats response headers into an array. If empty_response is true(thy),
|
|
@@ -27,8 +27,8 @@ module Syntropy
|
|
|
27
27
|
# @param env [Hash] environment hash
|
|
28
28
|
# @return [void]
|
|
29
29
|
def initialize(env)
|
|
30
|
-
@root_dir = env[:root_dir]
|
|
31
30
|
@env = env
|
|
31
|
+
@root_dir = env[:root_dir]
|
|
32
32
|
@modules = {} # maps ref to module entry
|
|
33
33
|
@fn_map = {} # maps filename to ref
|
|
34
34
|
end
|
|
@@ -37,13 +37,27 @@ module Syntropy
|
|
|
37
37
|
#
|
|
38
38
|
# @param ref [String] module reference
|
|
39
39
|
# @return [any] export value
|
|
40
|
-
def load(ref)
|
|
40
|
+
def load(ref, raise_on_missing: true)
|
|
41
41
|
ref = "/#{ref}" if ref !~ /^\//
|
|
42
42
|
|
|
43
|
-
entry = (
|
|
43
|
+
entry = load_module(ref, raise_on_missing:)
|
|
44
|
+
return if !entry
|
|
45
|
+
|
|
46
|
+
@modules[ref] ||= entry
|
|
44
47
|
entry[:export_value]
|
|
45
48
|
end
|
|
46
49
|
|
|
50
|
+
# Returns a list of modules found in the given relative path. The module
|
|
51
|
+
# references are returned as absolute paths (relative to the module loader
|
|
52
|
+
# root directory).
|
|
53
|
+
#
|
|
54
|
+
# @param dir [String] relative module directory
|
|
55
|
+
# @return [Array<String>] list of modules
|
|
56
|
+
def list(dir)
|
|
57
|
+
fns = Dir[File.join(@root_dir, dir, '*.rb')]
|
|
58
|
+
fns.map { it.match(/^#{@root_dir}\/(.+)\.rb$/)[1] }.sort
|
|
59
|
+
end
|
|
60
|
+
|
|
47
61
|
# Invalidates a module by its filename, normally following a change to the
|
|
48
62
|
# underlying file (in order to cause reloading of the module). The module
|
|
49
63
|
# will be removed from the modules map, as well as modules dependending on
|
|
@@ -101,17 +115,23 @@ module Syntropy
|
|
|
101
115
|
#
|
|
102
116
|
# @param ref [String] module reference
|
|
103
117
|
# @return [Hash] module entry
|
|
104
|
-
def load_module(ref)
|
|
118
|
+
def load_module(ref, raise_on_missing: true)
|
|
105
119
|
ref = "/#{ref}" if ref !~ /^\//
|
|
106
120
|
fn = File.expand_path(File.join(@root_dir, "#{ref}.rb"))
|
|
107
|
-
|
|
121
|
+
if !File.file?(fn)
|
|
122
|
+
raise Syntropy::Error, "File not found #{fn}" if raise_on_missing
|
|
123
|
+
|
|
124
|
+
return
|
|
125
|
+
end
|
|
108
126
|
|
|
109
127
|
@fn_map[fn] = ref
|
|
110
128
|
code = IO.read(fn)
|
|
111
129
|
env = @env.merge(module_loader: self, ref: clean_ref(ref))
|
|
112
130
|
mod = Syntropy::Module.load(env, code, fn)
|
|
113
131
|
add_dependencies(ref, mod.__dependencies__)
|
|
114
|
-
export_value = transform_module_export_value(
|
|
132
|
+
export_value = transform_module_export_value(
|
|
133
|
+
mod.__export_value__, fn, raise_on_missing:
|
|
134
|
+
)
|
|
115
135
|
|
|
116
136
|
{
|
|
117
137
|
fn: fn,
|
|
@@ -133,10 +153,10 @@ module Syntropy
|
|
|
133
153
|
#
|
|
134
154
|
# @param export_value [any] module's export value
|
|
135
155
|
# @return [any] transformed value
|
|
136
|
-
def transform_module_export_value(export_value)
|
|
156
|
+
def transform_module_export_value(export_value, fn, raise_on_missing:)
|
|
137
157
|
case export_value
|
|
138
158
|
when nil
|
|
139
|
-
raise Syntropy::Error,
|
|
159
|
+
raise Syntropy::Error, "No export found in #{fn}" if raise_on_missing
|
|
140
160
|
when String
|
|
141
161
|
->(req) { req.respond(export_value) }
|
|
142
162
|
when Class
|
|
@@ -278,9 +298,28 @@ module Syntropy
|
|
|
278
298
|
# environment is based on the module's env merged with the given parameters.
|
|
279
299
|
#
|
|
280
300
|
# @param env [Hash] environment
|
|
301
|
+
# @return [Syntropy::App]
|
|
281
302
|
def app(**env)
|
|
282
303
|
env = @env.merge(env)
|
|
283
304
|
Syntropy::App.new(**env)
|
|
284
305
|
end
|
|
306
|
+
|
|
307
|
+
# Returns a request handler that handles requests by calling the appropriate
|
|
308
|
+
# module method (e.g. get, post, etc.)
|
|
309
|
+
#
|
|
310
|
+
# @return [Proc]
|
|
311
|
+
def http_methods
|
|
312
|
+
->(req) { route_by_http_method(req) }
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
# Handles the given request by calling the module method corresponding to
|
|
316
|
+
# the request's HTTP method. If no method is found, raises a
|
|
317
|
+
# method_not_allowed error.
|
|
318
|
+
def route_by_http_method(req)
|
|
319
|
+
sym = req.method.to_sym
|
|
320
|
+
raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
|
|
321
|
+
|
|
322
|
+
send(sym, req)
|
|
323
|
+
end
|
|
285
324
|
end
|
|
286
325
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'uri'
|
|
4
|
-
require 'escape_utils'
|
|
5
4
|
|
|
6
5
|
module Syntropy
|
|
7
6
|
module RequestInfoMethods
|
|
@@ -122,7 +121,7 @@ module Syntropy
|
|
|
122
121
|
raise BadRequestError, 'Invalid cookie format' unless c.strip =~ COOKIE_RE
|
|
123
122
|
|
|
124
123
|
key, value = Regexp.last_match[1..2]
|
|
125
|
-
h[key] =
|
|
124
|
+
h[key] = URI.decode_www_form_component(value)
|
|
126
125
|
end
|
|
127
126
|
end
|
|
128
127
|
|
|
@@ -159,7 +158,7 @@ module Syntropy
|
|
|
159
158
|
|
|
160
159
|
def auth_bearer_token
|
|
161
160
|
auth = headers['authorization']
|
|
162
|
-
if (m = auth.match(/Bearer\s+([^\w]+)/))
|
|
161
|
+
if auth && (m = auth.match(/Bearer\s+([^\w]+)/))
|
|
163
162
|
return m[1]
|
|
164
163
|
end
|
|
165
164
|
|
|
@@ -248,7 +247,7 @@ module Syntropy
|
|
|
248
247
|
v = Regexp.last_match(2)
|
|
249
248
|
raise BadRequestError, 'Invalid parameter size' if v && v.size > MAX_PARAMETER_VALUE_SIZE
|
|
250
249
|
|
|
251
|
-
m[
|
|
250
|
+
m[URI.decode_www_form_component(k)] = v ? URI.decode_www_form_component(v) : true
|
|
252
251
|
end
|
|
253
252
|
end
|
|
254
253
|
end
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'uri'
|
|
4
|
-
require 'escape_utils'
|
|
5
4
|
|
|
6
5
|
module Syntropy
|
|
7
6
|
module RequestValidationMethods
|
|
@@ -40,7 +39,7 @@ module Syntropy
|
|
|
40
39
|
# @clauses [Array] one or more validation clauses
|
|
41
40
|
# @return [any] validated parameter value
|
|
42
41
|
def validate_param(name, *clauses)
|
|
43
|
-
validate(query[name], *clauses)
|
|
42
|
+
validate(query[name.to_s], *clauses)
|
|
44
43
|
end
|
|
45
44
|
|
|
46
45
|
# Validates and optionally converts a value against the given clauses. If no
|
data/lib/syntropy/test.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Syntropy
|
|
|
8
8
|
class TestHarness
|
|
9
9
|
def initialize(app)
|
|
10
10
|
@app = app
|
|
11
|
-
@app.
|
|
11
|
+
@app.raise_internal_server_error = true if @app.respond_to?(:raise_internal_server_error=)
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
def request(headers, body = nil)
|
|
@@ -17,6 +17,18 @@ module Syntropy
|
|
|
17
17
|
req
|
|
18
18
|
end
|
|
19
19
|
|
|
20
|
+
def no_raise_internal_server_error
|
|
21
|
+
return yield if !@app.respond_to?(:raise_internal_server_error=)
|
|
22
|
+
|
|
23
|
+
begin
|
|
24
|
+
@app.raise_internal_server_error = false
|
|
25
|
+
yield
|
|
26
|
+
ensure
|
|
27
|
+
@app.raise_internal_server_error = true
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
|
|
20
32
|
private
|
|
21
33
|
|
|
22
34
|
def mock_req(headers, body = nil)
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -8,10 +8,12 @@ require 'syntropy/logger'
|
|
|
8
8
|
require 'syntropy/http'
|
|
9
9
|
require 'syntropy/mime_types'
|
|
10
10
|
require 'syntropy/app'
|
|
11
|
-
require 'syntropy/connection_pool'
|
|
11
|
+
require 'syntropy/db/connection_pool'
|
|
12
|
+
require 'syntropy/db/schema'
|
|
13
|
+
require 'syntropy/db/store'
|
|
12
14
|
require 'syntropy/errors'
|
|
13
15
|
require 'syntropy/markdown'
|
|
14
|
-
require 'syntropy/
|
|
16
|
+
require 'syntropy/module_loader'
|
|
15
17
|
require 'syntropy/papercraft_extensions'
|
|
16
18
|
require 'syntropy/routing_tree'
|
|
17
19
|
require 'syntropy/json_api'
|
data/syntropy.gemspec
CHANGED
|
@@ -24,11 +24,12 @@ Gem::Specification.new do |s|
|
|
|
24
24
|
s.add_dependency 'extralite', '~>2.14'
|
|
25
25
|
s.add_dependency 'papercraft', '~>3.2.0'
|
|
26
26
|
s.add_dependency 'uringmachine', '~>1.0.2'
|
|
27
|
-
s.add_dependency 'escape_utils', '1.3.0'
|
|
28
27
|
|
|
29
28
|
s.add_dependency 'json'
|
|
30
29
|
s.add_dependency 'logger'
|
|
30
|
+
s.add_dependency 'irb'
|
|
31
31
|
|
|
32
32
|
s.add_development_dependency 'minitest', '~>6.0.1'
|
|
33
33
|
s.add_development_dependency 'rake', '~>13.3.1'
|
|
34
|
+
s.add_development_dependency 'solargraph'
|
|
34
35
|
end
|
data/test/app/_hook.rb
CHANGED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export ->(req) { req.respond('foo') }
|