syntropy 0.42.0 → 0.44.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 (51) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +14 -0
  3. data/TODO.md +60 -0
  4. data/examples/blog/app/posts/[id]/edit.rb +1 -1
  5. data/examples/blog/app/posts/[id]/index.rb +3 -3
  6. data/examples/gitshabbos/Dockerfile +25 -0
  7. data/examples/gitshabbos/Gemfile +3 -0
  8. data/examples/gitshabbos/README.md +40 -0
  9. data/examples/gitshabbos/app/[repo]/commits.rb +23 -0
  10. data/examples/gitshabbos/app/[repo]/index.rb +32 -0
  11. data/examples/gitshabbos/app/[repo]/raw+.rb +11 -0
  12. data/examples/gitshabbos/app/[repo]/tree+.rb +37 -0
  13. data/examples/gitshabbos/app/_layout/default.rb +14 -0
  14. data/examples/gitshabbos/app/_layout/repo.rb +27 -0
  15. data/examples/gitshabbos/app/_layout/style.css +466 -0
  16. data/examples/gitshabbos/app/_lib/git_repository.rb +95 -0
  17. data/examples/gitshabbos/app/_repos.rb +10 -0
  18. data/examples/gitshabbos/app/git/[repo]/git-receive-pack.rb +0 -0
  19. data/examples/gitshabbos/app/git/[repo]/git-upload-pack.rb +21 -0
  20. data/examples/gitshabbos/app/git/[repo]/info/refs.rb +19 -0
  21. data/examples/gitshabbos/app/git/[repo]/objects/index+.rb +37 -0
  22. data/examples/gitshabbos/app/git/index+.rb +37 -0
  23. data/examples/gitshabbos/app/index.rb +18 -0
  24. data/examples/gitshabbos/config/Caddyfile +5 -0
  25. data/examples/gitshabbos/config/development.rb +11 -0
  26. data/examples/gitshabbos/config/production.rb +5 -0
  27. data/examples/gitshabbos/config/test.rb +5 -0
  28. data/examples/gitshabbos/docker-compose.yml +71 -0
  29. data/examples/gitshabbos/test/test_app.rb +14 -0
  30. data/examples/mcp-oauth/app/oauth/consent.rb +1 -1
  31. data/examples/mcp-oauth/app/signin.rb +1 -1
  32. data/lib/syntropy/app.rb +4 -4
  33. data/lib/syntropy/applets/builtin/default_error_handler.rb +1 -1
  34. data/lib/syntropy/controller_extensions.rb +1 -1
  35. data/lib/syntropy/errors.rb +2 -0
  36. data/lib/syntropy/http/server_connection.rb +1 -1
  37. data/lib/syntropy/json_api.rb +1 -1
  38. data/lib/syntropy/logger.rb +7 -4
  39. data/lib/syntropy/markdown.rb +1 -1
  40. data/lib/syntropy/request/request_info.rb +11 -2
  41. data/lib/syntropy/request/response.rb +46 -4
  42. data/lib/syntropy/request/validation.rb +2 -1
  43. data/lib/syntropy/routing_tree.rb +16 -7
  44. data/lib/syntropy/version.rb +1 -1
  45. data/test/fixtures/app/mod/bar/index+.rb +5 -1
  46. data/test/fixtures/app/params/[foo].rb +1 -1
  47. data/test/test_app.rb +29 -0
  48. data/test/test_response.rb +115 -0
  49. data/test/test_routing_tree.rb +80 -25
  50. data/test/test_server.rb +3 -3
  51. metadata +25 -1
@@ -0,0 +1,11 @@
1
+ export(
2
+ {
3
+ repositories: [
4
+ {
5
+ name: 'syntropy',
6
+ path: File.expand_path(File.join(__dir__, '../../..')),
7
+ description: 'A web framework for Ruby'
8
+ }
9
+ ]
10
+ }
11
+ )
@@ -0,0 +1,5 @@
1
+ export(
2
+ storage: {
3
+ path: ENV['DATABASE_PATH'] || 'storage/production.db'
4
+ }
5
+ )
@@ -0,0 +1,5 @@
1
+ export({
2
+ storage: {
3
+ path: ENV['DATABASE_PATH'] || tmp_path('test-db')
4
+ }
5
+ })
@@ -0,0 +1,71 @@
1
+ #version: "3.8"
2
+
3
+ services:
4
+ app_server:
5
+ build: .
6
+ security_opt:
7
+ - seccomp:unconfined
8
+ volumes:
9
+ - .:/syntropy
10
+ stop_signal: SIGINT
11
+ stop_grace_period: 10s
12
+ restart: unless-stopped
13
+ healthcheck:
14
+ test: "curl 'http://localhost:1234/'"
15
+ interval: "30s"
16
+ timeout: "3s"
17
+ start_period: "5s"
18
+ retries: 3
19
+ networks:
20
+ - proxy_network
21
+
22
+ console:
23
+ build: .
24
+ command: bundle exec syntropy console
25
+ stdin_open: true # docker run -i
26
+ tty: true
27
+ security_opt:
28
+ - seccomp:unconfined
29
+ profiles:
30
+ - dev
31
+ volumes:
32
+ - .:/syntropy
33
+ stop_signal: SIGINT
34
+ restart: never
35
+
36
+ test:
37
+ build: .
38
+ command: bundle exec syntropy test -w
39
+ stdin_open: true # docker run -i
40
+ tty: true
41
+ security_opt:
42
+ - seccomp:unconfined
43
+ profiles:
44
+ - dev
45
+ volumes:
46
+ - .:/syntropy
47
+ stop_signal: SIGINT
48
+ restart: never
49
+
50
+ proxy:
51
+ depends_on:
52
+ - app_server
53
+ image: caddy:2-alpine
54
+ build:
55
+ context: ./proxy
56
+ dockerfile: Dockerfile
57
+ restart: unless-stopped
58
+ ports:
59
+ - "80:80"
60
+ - "443:443"
61
+ - "443:443/udp"
62
+ volumes:
63
+ - ./config/Caddyfile:/etc/caddy/Caddyfile
64
+ - ./storage/caddy/data:/data
65
+ - ./storage/caddy/config:/config
66
+ networks:
67
+ - proxy_network
68
+
69
+ networks:
70
+ proxy_network:
71
+ name: proxy_network
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ class AppTest < Syntropy::Test
4
+ def test_routing
5
+ req = get('/')
6
+ assert_equal HTTP::OK, req.response_status
7
+
8
+ req = get('/test')
9
+ assert_equal HTTP::OK, req.response_status
10
+
11
+ req = get('/foo')
12
+ assert_equal HTTP::NOT_FOUND, req.response_status
13
+ end
14
+ end
@@ -1,7 +1,7 @@
1
1
  AuthStore = import '../_lib/auth_store'
2
2
 
3
3
  export ->(req) {
4
- case req.method
4
+ case req.http_method
5
5
  when 'get'
6
6
  render_consent_form(req)
7
7
  when 'post'
@@ -1,7 +1,7 @@
1
1
  AuthStore = import '../_lib/auth_store'
2
2
 
3
3
  export ->(req) {
4
- case req.method
4
+ case req.http_method
5
5
  when 'get'
6
6
  render_signin_form(req)
7
7
  when 'post'
data/lib/syntropy/app.rb CHANGED
@@ -120,7 +120,7 @@ module Syntropy
120
120
  if Error.log_error?(e)
121
121
  @logger&.error(
122
122
  message: "Error while serving request: #{e.message}",
123
- method: req.method,
123
+ method: req.http_method,
124
124
  path: path,
125
125
  error: e
126
126
  )
@@ -244,7 +244,7 @@ module Syntropy
244
244
  headers = { 'Content-Type' => MimeTypes[File.extname(fn)] }
245
245
 
246
246
  ->(req) {
247
- case req.method
247
+ case req.http_method
248
248
  when 'head'
249
249
  req.respond(nil, headers)
250
250
  when 'get'
@@ -454,7 +454,7 @@ module Syntropy
454
454
  status = Syntropy::Error.http_status(err)
455
455
 
456
456
  msg = err.message
457
- msg = nil if msg.empty? || (req.method == 'head')
457
+ msg = nil if msg.empty? || (req.http_method == 'head')
458
458
  req.respond(msg, ':status' => status) rescue nil
459
459
  }
460
460
 
@@ -463,7 +463,7 @@ module Syntropy
463
463
  raise if status == HTTP::INTERNAL_SERVER_ERROR
464
464
 
465
465
  msg = err.message
466
- msg = nil if msg.empty? || (req.method == 'head')
466
+ msg = nil if msg.empty? || (req.http_method == 'head')
467
467
  req.respond(msg, ':status' => status) rescue nil
468
468
  }
469
469
 
@@ -62,7 +62,7 @@ def error_response_raw(req, error)
62
62
  message: error.message,
63
63
  backtrace: error.backtrace
64
64
  }
65
- req.json_pretty_response(response, ':status' => status)
65
+ req.respond_json_pretty(response, ':status' => status)
66
66
  end
67
67
 
68
68
  export ->(req, error) {
@@ -109,7 +109,7 @@ module Syntropy
109
109
  # the request's HTTP method. If no method is found, raises a
110
110
  # method_not_allowed error.
111
111
  def route_by_http_method(req)
112
- sym = req.method.to_sym
112
+ sym = req.http_method.to_sym
113
113
  raise Syntropy::Error.method_not_allowed if !respond_to?(sym)
114
114
 
115
115
  send(sym, req)
@@ -43,6 +43,8 @@ module Syntropy
43
43
  # @return [Syntropy::Error]
44
44
  def self.teapot(msg = 'I\'m a teapot') = new(msg, HTTP::TEAPOT)
45
45
 
46
+ def self.bad_request(msg = 'Bad request') = new(msg, HTTP::BAD_REQUEST)
47
+
46
48
  # Initializes a Syntropy error with the given HTTP status and message.
47
49
  #
48
50
  # @param http_status [Integer, String] HTTP status
@@ -11,7 +11,7 @@ module Syntropy
11
11
  # accepted using either fixed length (Content-Length header) or chunked
12
12
  # transfer encoding.
13
13
  class ServerConnection
14
- attr_reader :fd, :response_headers, :logger
14
+ attr_reader :fd, :io, :response_headers, :logger
15
15
 
16
16
  # Initializes a server connection.
17
17
  #
@@ -34,7 +34,7 @@ module Syntropy
34
34
  # @param req [Syntropy::Request]
35
35
  def __invoke__(req)
36
36
  q = req.validate_param(:q, String).to_sym
37
- case req.method
37
+ case req.http_method
38
38
  when 'get'
39
39
  response = __invoke_get__(q, req)
40
40
  when 'post'
@@ -88,8 +88,11 @@ module Syntropy
88
88
  message: o[:message]
89
89
  )
90
90
 
91
- entry.merge!(error_info(error)) if (error = o.delete(:error))
92
- entry.merge!(request_info(request)) if (request = o.delete(:request))
91
+ error = entry.delete(:error)
92
+ entry.merge!(error_info(error)) if error
93
+
94
+ request = entry.delete(:request)
95
+ entry.merge!(request_info(request)) if request
93
96
 
94
97
  entry
95
98
  end
@@ -111,14 +114,14 @@ module Syntropy
111
114
  # @return [Hash] request info
112
115
  def request_info(request)
113
116
  request_headers = request.headers
114
- response_headers = request.response_headers
117
+ # response_headers = request.response_headers
115
118
  elapsed = monotonic_clock - request.start_stamp
116
119
  {
117
120
  client_ip: request.forwarded_for || '?',
118
121
  http_method: request_headers[':method'].upcase,
119
122
  user_agent: request_headers['user-agent'],
120
123
  uri: full_uri(request_headers),
121
- status: response_headers[':status'] || '200',
124
+ # status: response_headers[':status'] || '200',
122
125
  elapsed: elapsed
123
126
  }
124
127
  end
@@ -26,7 +26,7 @@ module Syntropy
26
26
  # @return [Proc] controller proc
27
27
  def to_proc
28
28
  ->(req) {
29
- case req.method
29
+ case req.http_method
30
30
  when 'head'
31
31
  req.respond_html(nil)
32
32
  when 'get'
@@ -44,7 +44,7 @@ module Syntropy
44
44
  # Returns the HTTP method in lower case.
45
45
  #
46
46
  # @return [String]
47
- def method
47
+ def http_method
48
48
  @method ||= @headers[':method'].downcase
49
49
  end
50
50
 
@@ -100,11 +100,20 @@ module Syntropy
100
100
 
101
101
  # Returns the request path.
102
102
  #
103
- # @return [String]
103
+ # @return [String] path
104
104
  def path
105
105
  @path ||= uri.path
106
106
  end
107
107
 
108
+ # Returns the relative path for the route. The relative path will be non-nil
109
+ # only for a sub path of a wildcard route. For example, for a route at
110
+ # /api+, rel_path returns nil for /api, and "foo/bar" for /api/foo/bar.
111
+ #
112
+ # @return [String, nil] relative path or nil
113
+ def rel_path
114
+ @route_params[:__rel_path__]
115
+ end
116
+
108
117
  # Returns the request (unparsed) query string.
109
118
  #
110
119
  # @return [String, nil]
@@ -181,7 +181,7 @@ module Syntropy
181
181
  # @param map [Hash] hash mapping HTTP methods to responses
182
182
  # @return [void]
183
183
  def respond_by_http_method(map)
184
- value = map[self.method]
184
+ value = map[http_method]
185
185
  raise Syntropy::Error.method_not_allowed if !value
186
186
 
187
187
  value = value.() if value.is_a?(Proc)
@@ -196,7 +196,7 @@ module Syntropy
196
196
  # @param headers [Hash] response headers
197
197
  # @return [void]
198
198
  def respond_on_get(body, headers = {})
199
- case self.method
199
+ case http_method
200
200
  when 'head'
201
201
  respond(nil, headers)
202
202
  when 'get'
@@ -213,7 +213,7 @@ module Syntropy
213
213
  # @param headers [Hash] response headers
214
214
  # @return [void]
215
215
  def respond_on_post(body, headers = {})
216
- case self.method
216
+ case http_method
217
217
  when 'head'
218
218
  respond(nil, headers)
219
219
  when 'post'
@@ -254,12 +254,54 @@ module Syntropy
254
254
  # @param obj [any] obj to render into JSON
255
255
  # @param headers [Hash] additional response headers
256
256
  # @return [void]
257
- def json_pretty_response(obj, **headers)
257
+ def respond_json_pretty(obj, **headers)
258
258
  respond(
259
259
  JSON.pretty_generate(obj),
260
260
  'Content-Type' => 'application/json; charset=utf-8',
261
261
  **headers
262
262
  )
263
263
  end
264
+
265
+ # Renders the headers for an SSE (text/event-stream) response. If any events
266
+ # are given, they're rendered into the event stream. If a block is given, it
267
+ # is evaluated.
268
+ #
269
+ # @param events [Array<any>] events to render
270
+ # @return [void]
271
+ def respond_sse(*events)
272
+ send_headers('Content-Type' => 'text/event-stream')
273
+ send_event(*events) if !events.empty?
274
+ yield if block_given?
275
+ rescue SystemCallError => e
276
+ # swallow system call error, but let server connection know we're done
277
+ headers['connection'] = 'close'
278
+ ensure
279
+ finish if !e
280
+ end
281
+
282
+ # Sends one or more SSE events.
283
+ #
284
+ # @param events [Array<Hash, Array, String>] events
285
+ # @return [void]
286
+ def send_event(*events)
287
+ events.each { |e|
288
+ case e
289
+ when Hash
290
+ e.each { |k, v|
291
+ if v.is_a?(Array)
292
+ v = v.map { |i| "data: #{i}\n" }.join
293
+ send_chunk("event: #{k}\n#{v}\n")
294
+ else
295
+ send_chunk("event: #{k}\ndata: #{v}\n\n")
296
+ end
297
+ }
298
+ when Array
299
+ e = e.map { |i| "data: #{i}\n" }.join
300
+ send_chunk("#{e}\n")
301
+ else
302
+ send_chunk("data: #{e}\n\n")
303
+ end
304
+ }
305
+ end
264
306
  end
265
307
  end
@@ -13,7 +13,8 @@ module Syntropy
13
13
  # @param accepted [Array<String>] list of accepted HTTP methods
14
14
  # @return [String] request's HTTP method
15
15
  def validate_http_method(*accepted)
16
- return method if accepted.include?(method)
16
+ m = http_method
17
+ return m if accepted.include?(m)
17
18
 
18
19
  raise Syntropy::Error.method_not_allowed
19
20
  end
@@ -422,6 +422,8 @@ module Syntropy
422
422
  @static_map.freeze
423
423
  @dynamic_map.freeze
424
424
  code = generate_routing_tree_code
425
+ # puts '*' * 40
426
+ # puts code
425
427
  eval(code, binding, '(router)', 1)
426
428
  end
427
429
 
@@ -630,11 +632,15 @@ module Syntropy
630
632
  next if child_entry[:static]
631
633
 
632
634
  emit_code_line(buffer, "#{ws}when #{k.inspect}")
633
- if_clause = child_entry[:handle_subtree] ? '' : " if !segments[#{segment_idx + 1}]"
634
635
 
635
- child_path = child_entry[:path]
636
- route_value = "@dynamic_map[#{child_path.inspect}]"
637
- emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
636
+ route_value = "@dynamic_map[#{child_entry[:path].inspect}]"
637
+ if child_entry[:handle_subtree]
638
+ emit_code_line(buffer, "#{ws} params[:__rel_path__] = (rest = segments[#{segment_idx + 1}..-1]).empty? ? nil : rest.join('/')")
639
+ emit_code_line(buffer, "#{ws} return #{route_value}")
640
+ else
641
+ if_clause = " if !segments[#{segment_idx + 1}]"
642
+ emit_code_line(buffer, "#{ws} return #{route_value}#{if_clause}")
643
+ end
638
644
  when_count += 1
639
645
 
640
646
  elsif has_children
@@ -654,24 +660,26 @@ module Syntropy
654
660
  end
655
661
  end
656
662
 
657
- # parametric route
658
663
  if param_entry
664
+ # parametric route
659
665
  if when_count == 0
660
666
  emit_code_line(buffer, "#{ws}when s")
661
667
  else
662
668
  emit_code_line(buffer, "#{ws}else")
663
669
  end
664
670
 
665
- emit_code_line(buffer, "#{ws} params[#{param_entry[:param].inspect}] = s")
671
+ emit_code_line(buffer, "#{ws} params[#{param_entry[:param].to_sym.inspect}] = s")
666
672
  visit_routing_tree_entry(buffer:, entry: param_entry, indent: indent + 1, segment_idx: segment_idx + 1)
667
- # wildcard route
668
673
  elsif entry[:handle_subtree]
674
+ # wildcard route
669
675
  emit_code_line(buffer, "#{ws}else")
676
+ emit_code_line(buffer, "#{ws} params[:__rel_path__] = segments[#{segment_idx}..-1].join('/')")
670
677
  emit_code_line(buffer, "#{ws} return @dynamic_map[#{entry[:path].inspect}]")
671
678
  end
672
679
  end
673
680
 
674
681
  DEBUG = !!ENV['DEBUG']
682
+ RETURN_NIL_RE = /\sreturn nil\z/
675
683
 
676
684
  # Emits the given code into the given buffer, with a line break at the end.
677
685
  # If the `DEBUG` environment variable is set, adds a source location comment
@@ -681,6 +689,7 @@ module Syntropy
681
689
  # @param code [String] code
682
690
  # @return [void]
683
691
  def emit_code_line(buffer, code)
692
+ return if buffer =~ RETURN_NIL_RE && code =~ RETURN_NIL_RE
684
693
  if DEBUG
685
694
  loc = (m = caller[0].match(/^([^\:]+\:\d+)/)) && m[1]
686
695
  buffer << "#{code} # #{loc}\n"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Syntropy
4
- VERSION = '0.42.0'
4
+ VERSION = '0.44.0'
5
5
  end
@@ -1,5 +1,9 @@
1
+ export self
2
+
1
3
  def env
2
4
  @env
3
5
  end
4
6
 
5
- export self
7
+ def call(req)
8
+ req.respond_json({ rel_path: req.route_params[:__rel_path__]})
9
+ end
@@ -1,3 +1,3 @@
1
1
  export ->(req) {
2
- req.respond_on_get("#{req.route[:path]}-#{req.route_params['foo']}")
2
+ req.respond_on_get("#{req.route[:path]}-#{req.route_params[:foo]}")
3
3
  }
data/test/test_app.rb CHANGED
@@ -410,3 +410,32 @@ class AppDependenciesTest < Minitest::Test
410
410
  assert_equal HTTP::OK, req.response_status
411
411
  end
412
412
  end
413
+
414
+ class AppRouteParamsTest < Syntropy::Test
415
+ self.env = {
416
+ app_root: File.join(__dir__, 'fixtures/app'),
417
+ mount_path: '/test'
418
+ }
419
+
420
+ def test_app_route_params
421
+ req = get('/test/params/abc')
422
+ assert_equal 'abc', req.route_params[:foo]
423
+ end
424
+
425
+ def test_app_route_params_rel_path
426
+ req = get('/test/mod/bar')
427
+ assert_nil req.route_params[:__rel_path__]
428
+ assert_nil req.rel_path
429
+ assert_nil req.response_json[:rel_path]
430
+
431
+ req = get('/test/mod/bar/foo')
432
+ assert_equal 'foo', req.route_params[:__rel_path__]
433
+ assert_equal 'foo', req.rel_path
434
+ assert_equal 'foo', req.response_json[:rel_path]
435
+
436
+ req = get('/test/mod/bar/foo/baz')
437
+ assert_equal 'foo/baz', req.route_params[:__rel_path__]
438
+ assert_equal 'foo/baz', req.rel_path
439
+ assert_equal 'foo/baz', req.response_json[:rel_path]
440
+ end
441
+ end
@@ -121,3 +121,118 @@ class UpgradeTest < Minitest::Test
121
121
  ], r.adapter.calls
122
122
  end
123
123
  end
124
+
125
+ class SSETest < Minitest::Test
126
+ def test_respond_sse_block_empty
127
+ r = Syntropy::MockAdapter.mock
128
+ r.respond_sse {}
129
+
130
+ assert_equal [
131
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
132
+ [:finish, r]
133
+ ], r.adapter.calls
134
+ end
135
+
136
+ def test_respond_sse_block
137
+ r = Syntropy::MockAdapter.mock
138
+ r.respond_sse {
139
+ r.send_event('foo')
140
+ r.send_event('bar', 'baz')
141
+ r.send_event('foo' => 'bar')
142
+ r.send_event(
143
+ { 'bar' => 'baz' },
144
+ { 'baz' => 'foo' },
145
+ )
146
+ }
147
+
148
+ assert_equal [
149
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
150
+ [:send_chunk, r, "data: foo\n\n", { done: false }],
151
+ [:send_chunk, r, "data: bar\n\n", { done: false }],
152
+ [:send_chunk, r, "data: baz\n\n", { done: false }],
153
+ [:send_chunk, r, "event: foo\ndata: bar\n\n", { done: false }],
154
+ [:send_chunk, r, "event: bar\ndata: baz\n\n", { done: false }],
155
+ [:send_chunk, r, "event: baz\ndata: foo\n\n", { done: false }],
156
+ [:finish, r]
157
+ ], r.adapter.calls
158
+ end
159
+
160
+ def test_respond_sse_no_block_no_events
161
+ r = Syntropy::MockAdapter.mock
162
+ r.respond_sse()
163
+
164
+ assert_equal [
165
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
166
+ [:finish, r]
167
+ ], r.adapter.calls
168
+ end
169
+
170
+ def test_respond_sse_no_block_single_arg
171
+ r = Syntropy::MockAdapter.mock
172
+ r.respond_sse('foo')
173
+
174
+ assert_equal [
175
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
176
+ [:send_chunk, r, "data: foo\n\n", { done: false }],
177
+ [:finish, r]
178
+ ], r.adapter.calls
179
+ end
180
+
181
+ def test_respond_sse_no_block_multi_args
182
+ r = Syntropy::MockAdapter.mock
183
+ r.respond_sse('foo', 'bar')
184
+
185
+ assert_equal [
186
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
187
+ [:send_chunk, r, "data: foo\n\n", { done: false }],
188
+ [:send_chunk, r, "data: bar\n\n", { done: false }],
189
+ [:finish, r]
190
+ ], r.adapter.calls
191
+ end
192
+
193
+ def test_respond_sse_no_block_single_hash
194
+ r = Syntropy::MockAdapter.mock
195
+ r.respond_sse('foo' => 'bar')
196
+
197
+ assert_equal [
198
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
199
+ [:send_chunk, r, "event: foo\ndata: bar\n\n", { done: false }],
200
+ [:finish, r]
201
+ ], r.adapter.calls
202
+ end
203
+
204
+ def test_respond_sse_no_block_multi_hashes
205
+ r = Syntropy::MockAdapter.mock
206
+ r.respond_sse({ 'foo' => 'bar' }, { 'bar' => 'baz', 'baz' => 'foo' })
207
+
208
+ assert_equal [
209
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
210
+ [:send_chunk, r, "event: foo\ndata: bar\n\n", { done: false }],
211
+ [:send_chunk, r, "event: bar\ndata: baz\n\n", { done: false }],
212
+ [:send_chunk, r, "event: baz\ndata: foo\n\n", { done: false }],
213
+ [:finish, r]
214
+ ], r.adapter.calls
215
+ end
216
+
217
+ def test_send_event_multi_data
218
+ r = Syntropy::MockAdapter.mock
219
+ r.respond_sse(['foo', 'bar', 'baz'])
220
+
221
+ assert_equal [
222
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
223
+ [:send_chunk, r, "data: foo\ndata: bar\ndata: baz\n\n", { done: false }],
224
+ [:finish, r]
225
+ ], r.adapter.calls
226
+ end
227
+
228
+ def test_send_event_multi_data_hash
229
+ r = Syntropy::MockAdapter.mock
230
+ r.respond_sse('patch' => ['foo', 'bar', 'baz'])
231
+
232
+ assert_equal [
233
+ [:send_headers, r, { 'Content-Type' => 'text/event-stream'}, {empty_response: false}],
234
+ [:send_chunk, r, "event: patch\ndata: foo\ndata: bar\ndata: baz\n\n", { done: false }],
235
+ [:finish, r]
236
+ ], r.adapter.calls
237
+ end
238
+ end