syntropy 0.30.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.
Files changed (96) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +2 -0
  3. data/CHANGELOG.md +30 -0
  4. data/TODO.md +46 -1
  5. data/bin/syntropy +8 -86
  6. data/cmd/_banner.rb +16 -0
  7. data/cmd/console.rb +77 -0
  8. data/cmd/help.rb +12 -0
  9. data/cmd/serve.rb +95 -0
  10. data/cmd/test.rb +40 -0
  11. data/examples/{counter.rb → basic/counter.rb} +1 -1
  12. data/examples/{templates.rb → basic/templates.rb} +1 -1
  13. data/examples/blog/app/_layout/default.rb +11 -0
  14. data/examples/blog/app/_lib/post_store.rb +47 -0
  15. data/examples/blog/app/_schema/2026-01-01-initial.rb +9 -0
  16. data/examples/blog/app/_setup.rb +4 -0
  17. data/examples/blog/app/index.rb +7 -0
  18. data/examples/blog/app/posts/[id]/edit.rb +33 -0
  19. data/examples/blog/app/posts/[id]/index.rb +58 -0
  20. data/examples/blog/app/posts/index.rb +38 -0
  21. data/examples/blog/app/posts/new.rb +29 -0
  22. data/examples/mcp-oauth/.ruby-version +1 -0
  23. data/examples/mcp-oauth/Gemfile +8 -0
  24. data/examples/mcp-oauth/README.md +128 -0
  25. data/examples/mcp-oauth/app/.well-known/oauth-authorization-server.rb +18 -0
  26. data/examples/mcp-oauth/app/.well-known/oauth-protected-resource.rb +10 -0
  27. data/examples/mcp-oauth/app/_lib/auth_store.rb +23 -0
  28. data/examples/mcp-oauth/app/index.md +1 -0
  29. data/examples/mcp-oauth/app/mcp.rb +85 -0
  30. data/examples/mcp-oauth/app/oauth/authorize.rb +18 -0
  31. data/examples/mcp-oauth/app/oauth/consent.rb +86 -0
  32. data/examples/mcp-oauth/app/oauth/register.rb +14 -0
  33. data/examples/mcp-oauth/app/oauth/token.rb +79 -0
  34. data/examples/mcp-oauth/app/signin.rb +85 -0
  35. data/examples/mcp-oauth/test/helper.rb +9 -0
  36. data/examples/mcp-oauth/test/test_app.rb +27 -0
  37. data/examples/mcp-oauth/test/test_oauth.rb +628 -0
  38. data/lib/syntropy/app.rb +34 -9
  39. data/lib/syntropy/applets/builtin/default_error_handler.rb +3 -3
  40. data/lib/syntropy/applets/builtin/req.rb +1 -1
  41. data/lib/syntropy/db/connection_pool.rb +71 -0
  42. data/lib/syntropy/db/schema.rb +92 -0
  43. data/lib/syntropy/db/store.rb +31 -0
  44. data/lib/syntropy/dev_mode.rb +1 -1
  45. data/lib/syntropy/errors.rb +6 -0
  46. data/lib/syntropy/http/client.rb +43 -0
  47. data/lib/syntropy/http/client_connection.rb +36 -0
  48. data/lib/syntropy/http/io_extensions.rb +176 -0
  49. data/lib/syntropy/http/server.rb +5 -5
  50. data/lib/syntropy/http/{connection.rb → server_connection.rb} +15 -91
  51. data/lib/syntropy/http.rb +3 -1
  52. data/lib/syntropy/logger.rb +5 -1
  53. data/lib/syntropy/{module.rb → module_loader.rb} +47 -8
  54. data/lib/syntropy/papercraft_extensions.rb +1 -1
  55. data/lib/syntropy/request/mock_adapter.rb +2 -0
  56. data/lib/syntropy/request/request_info.rb +22 -4
  57. data/lib/syntropy/request/response.rb +2 -2
  58. data/lib/syntropy/request/validation.rb +11 -5
  59. data/lib/syntropy/routing_tree.rb +2 -1
  60. data/lib/syntropy/test.rb +77 -0
  61. data/lib/syntropy/version.rb +1 -1
  62. data/lib/syntropy.rb +5 -23
  63. data/syntropy.gemspec +3 -3
  64. data/test/app/.well-known/foo.rb +3 -0
  65. data/test/app/_hook.rb +1 -1
  66. data/test/app/by_method.rb +9 -0
  67. data/test/app_setup/_setup.rb +7 -0
  68. data/test/app_setup/index.rb +1 -0
  69. data/test/app_with_schema/_schema/2026-01-02-foo.rb +12 -0
  70. data/test/app_with_schema/_schema/2026-05-30-bar.rb +7 -0
  71. data/test/helper.rb +1 -25
  72. data/test/schema/2026-01-02-foo.rb +12 -0
  73. data/test/schema/2026-05-30-bar.rb +7 -0
  74. data/test/test_app.rb +110 -70
  75. data/test/test_caching.rb +1 -1
  76. data/test/{test_connection_pool.rb → test_db_connection_pool.rb} +7 -2
  77. data/test/test_db_schema.rb +96 -0
  78. data/test/test_db_store.rb +24 -0
  79. data/test/test_http_client.rb +52 -0
  80. data/test/test_http_client_connection.rb +43 -0
  81. data/test/test_http_protocol.rb +250 -0
  82. data/test/{test_connection.rb → test_http_server_connection.rb} +39 -48
  83. data/test/test_json_api.rb +5 -5
  84. data/test/{test_module.rb → test_module_loader.rb} +31 -0
  85. data/test/{test_request_extensions.rb → test_request.rb} +153 -18
  86. data/test/test_routing_tree.rb +15 -3
  87. data/test/test_server.rb +9 -13
  88. metadata +84 -36
  89. data/lib/syntropy/connection_pool.rb +0 -61
  90. data/test/test_request_info.rb +0 -90
  91. /data/examples/{bad.rb → basic/bad.rb} +0 -0
  92. /data/examples/{card.rb → basic/card.rb} +0 -0
  93. /data/examples/{counter.js → basic/counter.js} +0 -0
  94. /data/examples/{counter_api.rb → basic/counter_api.rb} +0 -0
  95. /data/examples/{favicon.ico → basic/favicon.ico} +0 -0
  96. /data/examples/{index.md → basic/index.md} +0 -0
@@ -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 Connection
13
+ class ServerConnection
13
14
  attr_reader :fd, :response_headers, :logger
14
15
 
15
- def initialize(server, machine, fd, env, &app)
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, :socket)
21
+ @io = machine.io(fd, io_mode)
22
22
  @app = app
23
23
 
24
24
  @done = nil
@@ -48,13 +48,17 @@ module Syntropy
48
48
  # connection should be persisted.
49
49
  def serve_request
50
50
  @closed = nil
51
- headers = parse_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
@@ -96,46 +100,17 @@ module Syntropy
96
100
  headers = req.headers
97
101
  return nil if headers[':body-done-reading']
98
102
 
99
- content_length = headers['content-length']
100
- if content_length
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
103
+ body = @io.http_read_body(headers)
104
+ headers[':body-done-reading'] = true if body
105
+ body
118
106
  end
119
107
 
120
108
  def get_body_chunk(req)
121
109
  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
110
  return nil if headers[':body-done-reading']
135
111
 
136
- # if content-length is not specified, we read to EOF, up to max 1MB size
137
- chunk = read(1 << 20, nil, false)
138
- headers[':body-done-reading'] = true
112
+ chunk = @io.http_read_body_chunk(headers)
113
+ headers[':body-done-reading'] = true if !chunk
139
114
  chunk
140
115
  end
141
116
 
@@ -293,57 +268,6 @@ module Syntropy
293
268
  return connection != 'close'
294
269
  end
295
270
 
296
- def parse_headers
297
- headers = get_request_line(MAX_REQUEST_LINE_LEN)
298
- return nil if !headers
299
-
300
- loop do
301
- line = @io.read_line(MAX_HEADER_LINE_LEN)
302
- break if line.nil? || line.empty?
303
-
304
- m = line.match(RE_HEADER_LINE)
305
- raise ProtocolError, "Invalid header: #{line[0..2047].inspect}" if !m
306
-
307
- headers[m[1].downcase] = m[2]
308
- end
309
-
310
- headers
311
- end
312
-
313
- def get_request_line(buf)
314
- line = @io.read_line(MAX_REQUEST_LINE_LEN)
315
- return nil if !line
316
-
317
- m = line.match(RE_REQUEST_LINE)
318
- raise ProtocolError, 'Invalid request line' if !m
319
-
320
- http_version = m[3]
321
- raise UnsupportedHTTPVersionError, 'HTTP version not supported' if http_version != '1.1'
322
-
323
- {
324
- ':method' => m[1].downcase,
325
- ':path' => m[2],
326
- ':protocol' => 'http/1.1'
327
- }
328
- end
329
-
330
- def read_chunk(headers, buffer)
331
- chunk_size_str = @io.read_line(MAX_CHUNK_SIZE_LEN)
332
- return nil if !chunk_size_str
333
-
334
- chunk_size = chunk_size_str.to_i(16)
335
- if chunk_size == 0
336
- headers[':body-done-reading'] = true
337
- @io.read_line(0)
338
- return nil
339
- end
340
-
341
- chunk = @io.read(chunk_size)
342
- @io.read_line(0)
343
-
344
- buffer ? (buffer << chunk) : chunk
345
- end
346
-
347
271
  INTERNAL_HEADER_REGEXP = /^:/
348
272
 
349
273
  # Formats response headers into an array. If empty_response is true(thy),
data/lib/syntropy/http.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'syntropy/http/status'
4
- require 'syntropy/http/connection'
4
+ require 'syntropy/http/client_connection'
5
+ require 'syntropy/http/client'
6
+ require 'syntropy/http/server_connection'
5
7
  require 'syntropy/http/server'
@@ -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 = request.adapter.monotonic_clock - request.start_stamp
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,
@@ -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 = (@modules[ref] ||= load_module(ref))
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
- raise Syntropy::Error, "File not found #{fn}" if !File.file?(fn)
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(mod.__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, 'No export found'
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
@@ -3,7 +3,7 @@
3
3
  require 'papercraft'
4
4
 
5
5
  Papercraft.extension(
6
- 'auto_refresh_watch!': ->(loc = '/.syntropy') {
6
+ 'auto_refresh!': ->(loc = '/.syntropy') {
7
7
  if $syntropy_dev_mode
8
8
  script(src: File.join(loc, 'auto_refresh/watch.js'), type: 'module')
9
9
  end
@@ -42,6 +42,8 @@ module Syntropy
42
42
  end
43
43
 
44
44
  def status
45
+ raise 'No response' if !response_headers
46
+
45
47
  response_headers[':status'] || HTTP::OK
46
48
  end
47
49
 
@@ -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
@@ -34,6 +33,16 @@ module Syntropy
34
33
  @scheme ||= @headers[':scheme']
35
34
  end
36
35
 
36
+ def content_type
37
+ ct = @headers['content-type']
38
+ return nil if !ct
39
+
40
+ m = ct.match(/^([^;]+)/)
41
+ return nil if !m
42
+
43
+ m[1].strip
44
+ end
45
+
37
46
  # Rewrites the request path by replacing the given src with the given
38
47
  # replacement.
39
48
  #
@@ -77,7 +86,7 @@ module Syntropy
77
86
  def parse_query(query)
78
87
  query.split('&').each_with_object({}) do |kv, h|
79
88
  k, v = kv.match(QUERY_KV_REGEXP)[1..2]
80
- h[k.to_sym] = v ? URI.decode_www_form_component(v) : true
89
+ h[k] = v ? URI.decode_www_form_component(v) : true
81
90
  end
82
91
  end
83
92
 
@@ -112,7 +121,7 @@ module Syntropy
112
121
  raise BadRequestError, 'Invalid cookie format' unless c.strip =~ COOKIE_RE
113
122
 
114
123
  key, value = Regexp.last_match[1..2]
115
- h[key] = EscapeUtils.unescape_uri(value)
124
+ h[key] = URI.decode_www_form_component(value)
116
125
  end
117
126
  end
118
127
 
@@ -147,6 +156,15 @@ module Syntropy
147
156
  @accept_parts.include?(mime_type)
148
157
  end
149
158
 
159
+ def auth_bearer_token
160
+ auth = headers['authorization']
161
+ if auth && (m = auth.match(/Bearer\s+([^\w]+)/))
162
+ return m[1]
163
+ end
164
+
165
+ nil
166
+ end
167
+
150
168
  private
151
169
 
152
170
  def parse_accept_parts(accept)
@@ -229,7 +247,7 @@ module Syntropy
229
247
  v = Regexp.last_match(2)
230
248
  raise BadRequestError, 'Invalid parameter size' if v && v.size > MAX_PARAMETER_VALUE_SIZE
231
249
 
232
- m[EscapeUtils.unescape_uri(k)] = v ? EscapeUtils.unescape_uri(v) : true
250
+ m[URI.decode_www_form_component(k)] = v ? URI.decode_www_form_component(v) : true
233
251
  end
234
252
  end
235
253
  end
@@ -179,7 +179,7 @@ module Syntropy
179
179
  end
180
180
  end
181
181
 
182
- def html_response(html, **headers)
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 json_response(obj, **headers)
190
+ def respond_json(obj, **headers)
191
191
  respond(
192
192
  JSON.dump(obj),
193
193
  'Content-Type' => 'application/json; charset=utf-8',
@@ -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
@@ -18,6 +17,13 @@ module Syntropy
18
17
  raise Syntropy::Error.method_not_allowed
19
18
  end
20
19
 
20
+ def validate_content_type(*accepted)
21
+ ct = content_type
22
+ return ct if accepted.include?(ct)
23
+
24
+ raise Syntropy::InvalidRequestContentTypeError
25
+ end
26
+
21
27
  # Validates and optionally converts request parameter value for the given
22
28
  # parameter name against the given clauses. If no clauses are given,
23
29
  # verifies the parameter value is not nil. A clause can be a class, such as
@@ -33,7 +39,7 @@ module Syntropy
33
39
  # @clauses [Array] one or more validation clauses
34
40
  # @return [any] validated parameter value
35
41
  def validate_param(name, *clauses)
36
- validate(query[name], *clauses)
42
+ validate(query[name.to_s], *clauses)
37
43
  end
38
44
 
39
45
  # Validates and optionally converts a value against the given clauses. If no
@@ -47,12 +53,12 @@ module Syntropy
47
53
  # @param value [any] value
48
54
  # @clauses [Array] one or more validation clauses
49
55
  # @return [any] validated value
50
- def validate(value, *clauses)
51
- raise Syntropy::ValidationError, 'Validation error' if clauses.empty? && !value
56
+ def validate(value, *clauses, message: 'Validation error')
57
+ raise Syntropy::ValidationError, message if clauses.empty? && !value
52
58
 
53
59
  clauses.each do |c|
54
60
  valid = param_is_valid?(value, c)
55
- raise(Syntropy::ValidationError, 'Validation error') if !valid
61
+ raise(Syntropy::ValidationError, message) if !valid
56
62
 
57
63
  value = param_convert(value, c)
58
64
  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
- Dir[File.join(dir.gsub(/[\[\]]/) { "\\#{it}"}, '*')]
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,77 @@
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.raise_internal_server_error = true if @app.respond_to?(:raise_internal_server_error=)
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
+ 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
+
32
+ private
33
+
34
+ def mock_req(headers, body = nil)
35
+ Syntropy::MockAdapter.mock(headers, body)
36
+ end
37
+ end
38
+
39
+ class Request
40
+ def response_headers
41
+ adapter.response_headers
42
+ end
43
+
44
+ def response_status
45
+ adapter.status
46
+ end
47
+
48
+ def response_body
49
+ adapter.response_body
50
+ end
51
+
52
+ def response_json
53
+ raise if response_content_type != 'application/json'
54
+ JSON.parse(response_body)
55
+ end
56
+
57
+ def response_content_type
58
+ ct = response_headers['Content-Type']
59
+ return nil if !ct
60
+
61
+ m = ct.match(/^([^;]+)/)
62
+ return nil if !m
63
+
64
+ m[1]
65
+ end
66
+
67
+ def response_cookie(name)
68
+ sc = response_headers['Set-Cookie']
69
+ return nil if !sc
70
+
71
+ m = sc.match(/#{name}=([^\s]+)$/)
72
+ return nil if !m
73
+
74
+ m[1]
75
+ end
76
+ end
77
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.30.0'
4
+ VERSION = '0.32.0'
5
5
  end
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/module'
16
+ require 'syntropy/module_loader'
15
17
  require 'syntropy/papercraft_extensions'
16
18
  require 'syntropy/routing_tree'
17
19
  require 'syntropy/json_api'
@@ -32,25 +34,6 @@ module Syntropy
32
34
  end
33
35
  end
34
36
 
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
37
  class << self
55
38
  def run(env = {}, &app)
56
39
  if @in_run
@@ -63,11 +46,10 @@ module Syntropy
63
46
  begin
64
47
  @in_run = true
65
48
  machine = env[:machine] || UM.new
66
- machine.puts(env[:banner]) if env[:banner]
67
49
 
68
50
  env[:logger]&.info(message: "Running Syntropy #{Syntropy::VERSION}, UringMachine #{UM::VERSION}, Ruby #{RUBY_VERSION}")
69
51
 
70
- server = Server.new(machine, env, &app)
52
+ server = HTTP::Server.new(machine, env, &app)
71
53
 
72
54
  setup_signal_handling(machine, Fiber.current)
73
55
  server.run
data/syntropy.gemspec CHANGED
@@ -23,13 +23,13 @@ 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.0'
27
- s.add_dependency 'cgi'
28
- s.add_dependency 'escape_utils', '1.3.0'
26
+ s.add_dependency 'uringmachine', '~>1.0.2'
29
27
 
30
28
  s.add_dependency 'json'
31
29
  s.add_dependency 'logger'
30
+ s.add_dependency 'irb'
32
31
 
33
32
  s.add_development_dependency 'minitest', '~>6.0.1'
34
33
  s.add_development_dependency 'rake', '~>13.3.1'
34
+ s.add_development_dependency 'solargraph'
35
35
  end
@@ -0,0 +1,3 @@
1
+ export ->(req) {
2
+ req.respond('foo')
3
+ }
data/test/app/_hook.rb CHANGED
@@ -1,4 +1,4 @@
1
1
  export ->(req, proc) {
2
- req.ctx[:foo] = req.query[:foo]
2
+ req.ctx[:foo] = req.query['foo']
3
3
  proc.(req)
4
4
  }
@@ -0,0 +1,9 @@
1
+ export http_methods
2
+
3
+ def get(req)
4
+ req.respond('foo')
5
+ end
6
+
7
+ def post(req)
8
+ req.respond('bar')
9
+ end
@@ -0,0 +1,7 @@
1
+ @app.env[:setup_imported] = true
2
+
3
+ class << @app
4
+ def foobar
5
+ :foobar
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ export ->(req) { req.respond('foo') }
@@ -0,0 +1,12 @@
1
+ export ->(db) {
2
+ db.execute <<~SQL
3
+ create table posts (
4
+ id integer primary key autoincrement,
5
+ title text,
6
+ body text
7
+ );
8
+
9
+ insert into posts (title, body)
10
+ values ('foo', 'bar');
11
+ SQL
12
+ }
@@ -0,0 +1,7 @@
1
+ export ->(db) {
2
+ db.execute <<~SQL
3
+ update posts
4
+ set body = 'baz'
5
+ where title = 'foo';
6
+ SQL
7
+ }