syntropy 0.30.0 → 0.31.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/CHANGELOG.md +17 -0
- data/bin/syntropy +8 -86
- data/cmd/_banner.rb +16 -0
- data/cmd/help.rb +12 -0
- data/cmd/serve.rb +95 -0
- data/cmd/test.rb +40 -0
- data/examples/{counter.rb → basic/counter.rb} +1 -1
- data/examples/{templates.rb → basic/templates.rb} +1 -1
- data/examples/mcp-oauth/.ruby-version +1 -0
- data/examples/mcp-oauth/Gemfile +8 -0
- data/examples/mcp-oauth/README.md +128 -0
- data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
- data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
- data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
- data/examples/mcp-oauth/app/index.md +1 -0
- data/examples/mcp-oauth/app/mcp.rb +38 -0
- data/examples/mcp-oauth/app/oauth/authorize.rb +26 -0
- data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
- data/examples/mcp-oauth/app/oauth/register.rb +15 -0
- data/examples/mcp-oauth/app/oauth/token.rb +79 -0
- data/examples/mcp-oauth/app/signin.rb +85 -0
- data/examples/mcp-oauth/test/helper.rb +9 -0
- data/examples/mcp-oauth/test/test_app.rb +27 -0
- data/examples/mcp-oauth/test/test_oauth.rb +628 -0
- data/lib/syntropy/app.rb +15 -4
- data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
- data/lib/syntropy/applets/builtin/req.rb +1 -1
- data/lib/syntropy/dev_mode.rb +1 -1
- data/lib/syntropy/errors.rb +6 -0
- data/lib/syntropy/http/client.rb +43 -0
- data/lib/syntropy/http/client_connection.rb +36 -0
- data/lib/syntropy/http/io_extensions.rb +148 -0
- data/lib/syntropy/http/server.rb +5 -5
- data/lib/syntropy/http/{connection.rb → server_connection.rb} +9 -38
- data/lib/syntropy/http.rb +3 -1
- data/lib/syntropy/logger.rb +5 -1
- data/lib/syntropy/papercraft_extensions.rb +1 -1
- data/lib/syntropy/request/mock_adapter.rb +2 -0
- data/lib/syntropy/request/request_info.rb +20 -1
- data/lib/syntropy/request/response.rb +2 -2
- data/lib/syntropy/request/validation.rb +10 -3
- data/lib/syntropy/routing_tree.rb +2 -1
- data/lib/syntropy/test.rb +65 -0
- data/lib/syntropy/version.rb +1 -1
- data/lib/syntropy.rb +1 -21
- data/syntropy.gemspec +1 -2
- data/test/app/.well-known/foo.rb +3 -0
- data/test/helper.rb +1 -25
- data/test/test_app.rb +53 -68
- data/test/test_caching.rb +1 -1
- data/test/test_http_client.rb +52 -0
- data/test/test_http_client_connection.rb +43 -0
- data/test/{test_connection.rb → test_http_server_connection.rb} +29 -29
- data/test/test_json_api.rb +4 -4
- data/test/{test_request_extensions.rb → test_request.rb} +150 -18
- data/test/test_routing_tree.rb +15 -3
- metadata +41 -29
- data/test/test_request_info.rb +0 -90
- /data/examples/{bad.rb → basic/bad.rb} +0 -0
- /data/examples/{card.rb → basic/card.rb} +0 -0
- /data/examples/{counter.js → basic/counter.js} +0 -0
- /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
- /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
- /data/examples/{index.md → basic/index.md} +0 -0
data/lib/syntropy/dev_mode.rb
CHANGED
data/lib/syntropy/errors.rb
CHANGED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'syntropy/http/client_connection'
|
|
4
|
+
require 'uri'
|
|
5
|
+
|
|
6
|
+
module Syntropy
|
|
7
|
+
module HTTP
|
|
8
|
+
class Client
|
|
9
|
+
def initialize(machine)
|
|
10
|
+
@machine = machine
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def get(url, **headers, &)
|
|
14
|
+
uri = URI.parse(url)
|
|
15
|
+
headers = headers.merge(
|
|
16
|
+
':method' => 'GET',
|
|
17
|
+
':path' => uri.request_uri
|
|
18
|
+
)
|
|
19
|
+
req(uri, **headers, &)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
# @param uri [URI]
|
|
25
|
+
def req(uri, **headers)
|
|
26
|
+
connection = make_connection(uri.scheme, uri.host, uri.port)
|
|
27
|
+
response_headers = connection.req(**headers)
|
|
28
|
+
if block_given?
|
|
29
|
+
yield(response_headers, connection)
|
|
30
|
+
else
|
|
31
|
+
[response_headers, connection.get_response_body(response_headers)]
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def make_connection(_scheme, host, port)
|
|
36
|
+
ip = (host =~ /^\d+\.\d+\.\d+\.\d+$/) ? host : @machine.resolve(host)[0]
|
|
37
|
+
|
|
38
|
+
fd = @machine.tcp_connect(ip, port)
|
|
39
|
+
Syntropy::HTTP::ClientConnection.new(@machine, fd)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'syntropy/errors'
|
|
4
|
+
require 'syntropy/http/io_extensions'
|
|
5
|
+
|
|
6
|
+
module Syntropy
|
|
7
|
+
module HTTP
|
|
8
|
+
class ClientConnection
|
|
9
|
+
attr_reader :fd, :response_headers, :logger
|
|
10
|
+
|
|
11
|
+
def initialize(machine, fd, io_mode: :socket)
|
|
12
|
+
@machine = machine
|
|
13
|
+
@fd = fd
|
|
14
|
+
@io = machine.io(fd, io_mode)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def req(body: nil, **headers)
|
|
18
|
+
if body
|
|
19
|
+
headers = headers.merge(
|
|
20
|
+
'Content-Length' => body.bytesize
|
|
21
|
+
)
|
|
22
|
+
end
|
|
23
|
+
@io.http_write_request_headers(**headers)
|
|
24
|
+
if body
|
|
25
|
+
@io.write(body)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
@io.http_read_response_headers
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def get_response_body(headers)
|
|
32
|
+
@io.http_read_body(headers)
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'syntropy/errors'
|
|
4
|
+
|
|
5
|
+
module Syntropy
|
|
6
|
+
module HTTP
|
|
7
|
+
module ProtocolMethods
|
|
8
|
+
RE_REQUEST_LINE = /^([a-z]+)\s+([^\s]+)\s+HTTP\/([019\.]{1,3})/i
|
|
9
|
+
RE_RESPONSE_LINE = /^HTTP\/1\.1\s+(\d{3})(\s+.+)?$/i
|
|
10
|
+
RE_HEADER_LINE = /^([a-z0-9-]+):\s+(.+)/i
|
|
11
|
+
|
|
12
|
+
MAX_REQUEST_LINE_LEN = 1 << 14 # 16KB
|
|
13
|
+
MAX_RESPONSE_LINE_LEN = 1 << 8 # 256
|
|
14
|
+
MAX_HEADER_LINE_LEN = 1 << 10 # 1KB
|
|
15
|
+
MAX_CHUNK_SIZE_LEN = 16
|
|
16
|
+
|
|
17
|
+
# @return [Hash] headers
|
|
18
|
+
def http_read_request_headers
|
|
19
|
+
line = read_line(MAX_REQUEST_LINE_LEN)
|
|
20
|
+
return nil if !line
|
|
21
|
+
|
|
22
|
+
m = line.match(RE_REQUEST_LINE)
|
|
23
|
+
raise ProtocolError, 'Invalid request line' if !m
|
|
24
|
+
|
|
25
|
+
http_version = m[3]
|
|
26
|
+
raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
|
|
27
|
+
|
|
28
|
+
headers = {
|
|
29
|
+
':method' => m[1].downcase,
|
|
30
|
+
':path' => m[2],
|
|
31
|
+
':protocol' => 'http/1.1'
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
loop do
|
|
35
|
+
line = read_line(MAX_HEADER_LINE_LEN)
|
|
36
|
+
break if line.nil? || line.empty?
|
|
37
|
+
|
|
38
|
+
m = line.match(RE_HEADER_LINE)
|
|
39
|
+
raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
|
|
40
|
+
|
|
41
|
+
headers[m[1].downcase] = m[2]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
headers
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def http_read_response_headers
|
|
48
|
+
line = read_line(MAX_RESPONSE_LINE_LEN)
|
|
49
|
+
return nil if !line
|
|
50
|
+
|
|
51
|
+
m = line.match(RE_RESPONSE_LINE)
|
|
52
|
+
raise ProtocolError, 'Invalid response line' if !m
|
|
53
|
+
|
|
54
|
+
headers = {
|
|
55
|
+
':status' => m[1].to_i
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
loop do
|
|
59
|
+
line = read_line(MAX_HEADER_LINE_LEN)
|
|
60
|
+
break if line.nil? || line.empty?
|
|
61
|
+
|
|
62
|
+
m = line.match(RE_HEADER_LINE)
|
|
63
|
+
raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
|
|
64
|
+
|
|
65
|
+
k = m[1].downcase
|
|
66
|
+
if (h = headers[k])
|
|
67
|
+
(h = headers[k] = [h]) if !h.is_a?(Array)
|
|
68
|
+
h << m[2]
|
|
69
|
+
else
|
|
70
|
+
headers[k] = m[2]
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
headers
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def http_read_body(headers)
|
|
78
|
+
content_length = headers['content-length']
|
|
79
|
+
if content_length
|
|
80
|
+
chunk = read(content_length.to_i)
|
|
81
|
+
return chunk
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
|
85
|
+
if chunked_encoding
|
|
86
|
+
buf = +''
|
|
87
|
+
while (chunk = http_read_cte_chunk(nil))
|
|
88
|
+
buf << chunk
|
|
89
|
+
end
|
|
90
|
+
return buf
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
nil
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def http_read_body_chunk(headers)
|
|
97
|
+
content_length = headers['content-length']
|
|
98
|
+
if content_length
|
|
99
|
+
chunk = read(content_length.to_i)
|
|
100
|
+
return chunk
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
|
104
|
+
return http_read_cte_chunk(nil) if chunked_encoding
|
|
105
|
+
|
|
106
|
+
nil
|
|
107
|
+
end
|
|
108
|
+
|
|
109
|
+
def http_write_request_headers(headers)
|
|
110
|
+
method = headers[':method'] || (raise BadRequestError)
|
|
111
|
+
path = headers[':path'] || (raise BadRequestError)
|
|
112
|
+
|
|
113
|
+
lines = ["#{method} #{path} HTTP/1.1\r\n"]
|
|
114
|
+
headers.each do |k, v|
|
|
115
|
+
next if k =~ /^\:/
|
|
116
|
+
|
|
117
|
+
if v.is_a?(Array)
|
|
118
|
+
v.each { lines << "#{k}: #{it}\r\n" }
|
|
119
|
+
else
|
|
120
|
+
lines << "#{k}: #{v}\r\n"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
lines << "\r\n"
|
|
124
|
+
write(*lines)
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
private
|
|
128
|
+
|
|
129
|
+
def http_read_cte_chunk(buffer)
|
|
130
|
+
chunk_size_str = read_line(MAX_CHUNK_SIZE_LEN)
|
|
131
|
+
return nil if !chunk_size_str
|
|
132
|
+
|
|
133
|
+
chunk_size = chunk_size_str.to_i(16)
|
|
134
|
+
if chunk_size == 0
|
|
135
|
+
read_line(0)
|
|
136
|
+
return nil
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
chunk = read(chunk_size)
|
|
140
|
+
read_line(0)
|
|
141
|
+
|
|
142
|
+
buffer ? (buffer << chunk) : chunk
|
|
143
|
+
end
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
UringMachine::IO.include(Syntropy::HTTP::ProtocolMethods)
|
data/lib/syntropy/http/server.rb
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'syntropy/http/
|
|
3
|
+
require 'syntropy/http/server_connection'
|
|
4
4
|
|
|
5
5
|
module Syntropy
|
|
6
6
|
module HTTP
|
|
@@ -119,13 +119,13 @@ module Syntropy
|
|
|
119
119
|
end
|
|
120
120
|
|
|
121
121
|
def accept_incoming(listen_fd)
|
|
122
|
-
@machine.accept_each(listen_fd) {
|
|
122
|
+
@machine.accept_each(listen_fd) { start_connection(it) }
|
|
123
123
|
rescue UM::Terminate
|
|
124
|
-
|
|
124
|
+
@machine.shutdown(listen_fd, UM::SHUT_RD)
|
|
125
125
|
end
|
|
126
126
|
|
|
127
|
-
def
|
|
128
|
-
conn =
|
|
127
|
+
def start_connection(fd)
|
|
128
|
+
conn = ServerConnection.new(@machine, fd, @env, &@app)
|
|
129
129
|
f = @machine.spin(conn) do
|
|
130
130
|
it.run
|
|
131
131
|
ensure
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
require 'syntropy/errors'
|
|
4
|
+
require 'syntropy/http/io_extensions'
|
|
4
5
|
|
|
5
6
|
module Syntropy
|
|
6
7
|
module HTTP
|
|
@@ -9,16 +10,15 @@ module Syntropy
|
|
|
9
10
|
# body is sent exclusively using chunked transfer encoding. Request bodies are
|
|
10
11
|
# accepted using either fixed length (Content-Length header) or chunked
|
|
11
12
|
# transfer encoding.
|
|
12
|
-
class
|
|
13
|
+
class ServerConnection
|
|
13
14
|
attr_reader :fd, :response_headers, :logger
|
|
14
15
|
|
|
15
|
-
def initialize(
|
|
16
|
-
@server = server
|
|
16
|
+
def initialize(machine, fd, env, io_mode: :socket, &app)
|
|
17
17
|
@machine = machine
|
|
18
18
|
@fd = fd
|
|
19
19
|
@env = env
|
|
20
20
|
@logger = env[:logger]
|
|
21
|
-
@io = machine.io(fd,
|
|
21
|
+
@io = machine.io(fd, io_mode)
|
|
22
22
|
@app = app
|
|
23
23
|
|
|
24
24
|
@done = nil
|
|
@@ -96,46 +96,17 @@ module Syntropy
|
|
|
96
96
|
headers = req.headers
|
|
97
97
|
return nil if headers[':body-done-reading']
|
|
98
98
|
|
|
99
|
-
|
|
100
|
-
if
|
|
101
|
-
|
|
102
|
-
chunk = @io.read(content_length.to_i)
|
|
103
|
-
headers[':body-done-reading'] = true
|
|
104
|
-
return chunk
|
|
105
|
-
end
|
|
106
|
-
|
|
107
|
-
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
|
108
|
-
if chunked_encoding
|
|
109
|
-
buf = +''
|
|
110
|
-
while (chunk = read_chunk(headers, nil))
|
|
111
|
-
buf << chunk
|
|
112
|
-
end
|
|
113
|
-
headers[':body-done-reading'] = true
|
|
114
|
-
return buf
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
nil
|
|
99
|
+
body = @io.http_read_body(headers)
|
|
100
|
+
headers[':body-done-reading'] = true if body
|
|
101
|
+
body
|
|
118
102
|
end
|
|
119
103
|
|
|
120
104
|
def get_body_chunk(req)
|
|
121
105
|
headers = req.headers
|
|
122
|
-
content_length = headers['content-length']
|
|
123
|
-
if content_length
|
|
124
|
-
return nil if headers[':body-done-reading']
|
|
125
|
-
|
|
126
|
-
chunk = @io.read(content_length.to_i)
|
|
127
|
-
headers[':body-done-reading'] = true
|
|
128
|
-
return chunk
|
|
129
|
-
end
|
|
130
|
-
|
|
131
|
-
chunked_encoding = headers['transfer-encoding']&.downcase == 'chunked'
|
|
132
|
-
return read_chunk(headers, nil) if chunked_encoding
|
|
133
|
-
|
|
134
106
|
return nil if headers[':body-done-reading']
|
|
135
107
|
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
headers[':body-done-reading'] = true
|
|
108
|
+
chunk = @io.http_read_body_chunk(headers)
|
|
109
|
+
headers[':body-done-reading'] = true if !chunk
|
|
139
110
|
chunk
|
|
140
111
|
end
|
|
141
112
|
|
data/lib/syntropy/http.rb
CHANGED
data/lib/syntropy/logger.rb
CHANGED
|
@@ -67,7 +67,7 @@ module Syntropy
|
|
|
67
67
|
request = o[:request]
|
|
68
68
|
request_headers = request.headers
|
|
69
69
|
response_headers = o[:response_headers]
|
|
70
|
-
elapsed =
|
|
70
|
+
elapsed = monotonic_clock - request.start_stamp
|
|
71
71
|
{
|
|
72
72
|
level: level.to_s,
|
|
73
73
|
ts: (t = Time.now; t.to_i),
|
|
@@ -82,6 +82,10 @@ module Syntropy
|
|
|
82
82
|
}
|
|
83
83
|
end
|
|
84
84
|
|
|
85
|
+
def monotonic_clock
|
|
86
|
+
::Process.clock_gettime(::Process::CLOCK_MONOTONIC)
|
|
87
|
+
end
|
|
88
|
+
|
|
85
89
|
def make_hash_entry(level, hash)
|
|
86
90
|
{
|
|
87
91
|
level: level.to_s,
|
|
@@ -34,6 +34,16 @@ module Syntropy
|
|
|
34
34
|
@scheme ||= @headers[':scheme']
|
|
35
35
|
end
|
|
36
36
|
|
|
37
|
+
def content_type
|
|
38
|
+
ct = @headers['content-type']
|
|
39
|
+
return nil if !ct
|
|
40
|
+
|
|
41
|
+
m = ct.match(/^([^;]+)/)
|
|
42
|
+
return nil if !m
|
|
43
|
+
|
|
44
|
+
m[1].strip
|
|
45
|
+
end
|
|
46
|
+
|
|
37
47
|
# Rewrites the request path by replacing the given src with the given
|
|
38
48
|
# replacement.
|
|
39
49
|
#
|
|
@@ -77,7 +87,7 @@ module Syntropy
|
|
|
77
87
|
def parse_query(query)
|
|
78
88
|
query.split('&').each_with_object({}) do |kv, h|
|
|
79
89
|
k, v = kv.match(QUERY_KV_REGEXP)[1..2]
|
|
80
|
-
h[k
|
|
90
|
+
h[k] = v ? URI.decode_www_form_component(v) : true
|
|
81
91
|
end
|
|
82
92
|
end
|
|
83
93
|
|
|
@@ -147,6 +157,15 @@ module Syntropy
|
|
|
147
157
|
@accept_parts.include?(mime_type)
|
|
148
158
|
end
|
|
149
159
|
|
|
160
|
+
def auth_bearer_token
|
|
161
|
+
auth = headers['authorization']
|
|
162
|
+
if (m = auth.match(/Bearer\s+([^\w]+)/))
|
|
163
|
+
return m[1]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
150
169
|
private
|
|
151
170
|
|
|
152
171
|
def parse_accept_parts(accept)
|
|
@@ -179,7 +179,7 @@ module Syntropy
|
|
|
179
179
|
end
|
|
180
180
|
end
|
|
181
181
|
|
|
182
|
-
def
|
|
182
|
+
def respond_html(html, **headers)
|
|
183
183
|
respond(
|
|
184
184
|
html,
|
|
185
185
|
'Content-Type' => 'text/html; charset=utf-8',
|
|
@@ -187,7 +187,7 @@ module Syntropy
|
|
|
187
187
|
)
|
|
188
188
|
end
|
|
189
189
|
|
|
190
|
-
def
|
|
190
|
+
def respond_json(obj, **headers)
|
|
191
191
|
respond(
|
|
192
192
|
JSON.dump(obj),
|
|
193
193
|
'Content-Type' => 'application/json; charset=utf-8',
|
|
@@ -18,6 +18,13 @@ module Syntropy
|
|
|
18
18
|
raise Syntropy::Error.method_not_allowed
|
|
19
19
|
end
|
|
20
20
|
|
|
21
|
+
def validate_content_type(*accepted)
|
|
22
|
+
ct = content_type
|
|
23
|
+
return ct if accepted.include?(ct)
|
|
24
|
+
|
|
25
|
+
raise Syntropy::InvalidRequestContentTypeError
|
|
26
|
+
end
|
|
27
|
+
|
|
21
28
|
# Validates and optionally converts request parameter value for the given
|
|
22
29
|
# parameter name against the given clauses. If no clauses are given,
|
|
23
30
|
# verifies the parameter value is not nil. A clause can be a class, such as
|
|
@@ -47,12 +54,12 @@ module Syntropy
|
|
|
47
54
|
# @param value [any] value
|
|
48
55
|
# @clauses [Array] one or more validation clauses
|
|
49
56
|
# @return [any] validated value
|
|
50
|
-
def validate(value, *clauses)
|
|
51
|
-
raise Syntropy::ValidationError,
|
|
57
|
+
def validate(value, *clauses, message: 'Validation error')
|
|
58
|
+
raise Syntropy::ValidationError, message if clauses.empty? && !value
|
|
52
59
|
|
|
53
60
|
clauses.each do |c|
|
|
54
61
|
valid = param_is_valid?(value, c)
|
|
55
|
-
raise(Syntropy::ValidationError,
|
|
62
|
+
raise(Syntropy::ValidationError, message) if !valid
|
|
56
63
|
|
|
57
64
|
value = param_convert(value, c)
|
|
58
65
|
end
|
|
@@ -260,7 +260,8 @@ module Syntropy
|
|
|
260
260
|
# @param dir [String] directory path
|
|
261
261
|
# @return [Array<String>] array of file entries
|
|
262
262
|
def file_search(dir)
|
|
263
|
-
|
|
263
|
+
spec = File.join(dir.gsub(/[\[\]]/) { "\\#{it}"}, '{*,.*}')
|
|
264
|
+
Dir[spec].reject { it =~ /\/\.$/ }
|
|
264
265
|
end
|
|
265
266
|
|
|
266
267
|
# Computes a route entry and/or target for the given file path.
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'syntropy'
|
|
4
|
+
require 'syntropy/request/mock_adapter'
|
|
5
|
+
require 'minitest'
|
|
6
|
+
|
|
7
|
+
module Syntropy
|
|
8
|
+
class TestHarness
|
|
9
|
+
def initialize(app)
|
|
10
|
+
@app = app
|
|
11
|
+
@app.test_mode = true
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def request(headers, body = nil)
|
|
15
|
+
req = mock_req(headers, body)
|
|
16
|
+
@app.call(req)
|
|
17
|
+
req
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
def mock_req(headers, body = nil)
|
|
23
|
+
Syntropy::MockAdapter.mock(headers, body)
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
class Request
|
|
28
|
+
def response_headers
|
|
29
|
+
adapter.response_headers
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def response_status
|
|
33
|
+
adapter.status
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def response_body
|
|
37
|
+
adapter.response_body
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def response_json
|
|
41
|
+
raise if response_content_type != 'application/json'
|
|
42
|
+
JSON.parse(response_body)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def response_content_type
|
|
46
|
+
ct = response_headers['Content-Type']
|
|
47
|
+
return nil if !ct
|
|
48
|
+
|
|
49
|
+
m = ct.match(/^([^;]+)/)
|
|
50
|
+
return nil if !m
|
|
51
|
+
|
|
52
|
+
m[1]
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def response_cookie(name)
|
|
56
|
+
sc = response_headers['Set-Cookie']
|
|
57
|
+
return nil if !sc
|
|
58
|
+
|
|
59
|
+
m = sc.match(/#{name}=([^\s]+)$/)
|
|
60
|
+
return nil if !m
|
|
61
|
+
|
|
62
|
+
m[1]
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
data/lib/syntropy/version.rb
CHANGED
data/lib/syntropy.rb
CHANGED
|
@@ -32,25 +32,6 @@ module Syntropy
|
|
|
32
32
|
end
|
|
33
33
|
end
|
|
34
34
|
|
|
35
|
-
def colorize(color_code)
|
|
36
|
-
"\e[#{color_code}m#{self}\e[0m"
|
|
37
|
-
end
|
|
38
|
-
|
|
39
|
-
GREEN = "\e[32m"
|
|
40
|
-
CLEAR = "\e[0m"
|
|
41
|
-
YELLOW = "\e[33m"
|
|
42
|
-
|
|
43
|
-
BANNER =
|
|
44
|
-
"\n"\
|
|
45
|
-
" #{GREEN}\n"\
|
|
46
|
-
" #{GREEN} ooo\n"\
|
|
47
|
-
" #{GREEN}ooooo\n"\
|
|
48
|
-
" #{GREEN} ooo vvv #{CLEAR}Syntropy - a web framework for Ruby\n"\
|
|
49
|
-
" #{GREEN} o vvvvv #{CLEAR}--------------------------------------\n"\
|
|
50
|
-
" #{GREEN} #{YELLOW}|#{GREEN} vvv o #{CLEAR}https://github.com/digital-fabric/syntropy\n"\
|
|
51
|
-
" #{GREEN} :#{YELLOW}|#{GREEN}:::#{YELLOW}|#{GREEN}::#{YELLOW}|#{GREEN}:\n"\
|
|
52
|
-
"#{YELLOW}+++++++++++++++++++++++++++++++++++++++++++++++++++++++++\e[0m\n\n"
|
|
53
|
-
|
|
54
35
|
class << self
|
|
55
36
|
def run(env = {}, &app)
|
|
56
37
|
if @in_run
|
|
@@ -63,11 +44,10 @@ module Syntropy
|
|
|
63
44
|
begin
|
|
64
45
|
@in_run = true
|
|
65
46
|
machine = env[:machine] || UM.new
|
|
66
|
-
machine.puts(env[:banner]) if env[:banner]
|
|
67
47
|
|
|
68
48
|
env[:logger]&.info(message: "Running Syntropy #{Syntropy::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
|
|
69
49
|
|
|
70
|
-
server = Server.new(machine, env, &app)
|
|
50
|
+
server = HTTP::Server.new(machine, env, &app)
|
|
71
51
|
|
|
72
52
|
setup_signal_handling(machine, Fiber.current)
|
|
73
53
|
server.run
|
data/syntropy.gemspec
CHANGED
|
@@ -23,8 +23,7 @@ Gem::Specification.new do |s|
|
|
|
23
23
|
|
|
24
24
|
s.add_dependency 'extralite', '~>2.14'
|
|
25
25
|
s.add_dependency 'papercraft', '~>3.2.0'
|
|
26
|
-
s.add_dependency 'uringmachine', '~>1.0.
|
|
27
|
-
s.add_dependency 'cgi'
|
|
26
|
+
s.add_dependency 'uringmachine', '~>1.0.2'
|
|
28
27
|
s.add_dependency 'escape_utils', '1.3.0'
|
|
29
28
|
|
|
30
29
|
s.add_dependency 'json'
|
data/test/helper.rb
CHANGED
|
@@ -4,7 +4,7 @@ require 'bundler/setup'
|
|
|
4
4
|
require_relative './coverage' if ENV['COVERAGE']
|
|
5
5
|
require 'uringmachine'
|
|
6
6
|
require 'syntropy'
|
|
7
|
-
require 'syntropy/
|
|
7
|
+
require 'syntropy/test'
|
|
8
8
|
require 'minitest/autorun'
|
|
9
9
|
require 'fileutils'
|
|
10
10
|
|
|
@@ -96,27 +96,3 @@ module Minitest::Assertions
|
|
|
96
96
|
assert_equal exp_content_type, actual
|
|
97
97
|
end
|
|
98
98
|
end
|
|
99
|
-
|
|
100
|
-
# Extensions to be used in conjunction with `Syntropy::TestAdapter`
|
|
101
|
-
class Syntropy::Request
|
|
102
|
-
def response_headers
|
|
103
|
-
adapter.response_headers
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
def response_status
|
|
107
|
-
adapter.status
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def response_body
|
|
111
|
-
adapter.response_body
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
def response_json
|
|
115
|
-
raise if response_content_type != 'application/json'
|
|
116
|
-
JSON.parse(response_body, symbolize_names: true)
|
|
117
|
-
end
|
|
118
|
-
|
|
119
|
-
def response_content_type
|
|
120
|
-
response_headers['Content-Type']
|
|
121
|
-
end
|
|
122
|
-
end
|