tep 0.11.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 (193) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +21 -0
  3. data/Makefile +134 -0
  4. data/README.md +247 -0
  5. data/SINATRA_COMPAT.md +376 -0
  6. data/bin/tep +2156 -0
  7. data/examples/agentic_chat/README.md +103 -0
  8. data/examples/agentic_chat/app.rb +310 -0
  9. data/examples/api_gateway/README.md +49 -0
  10. data/examples/api_gateway/app.rb +66 -0
  11. data/examples/blog/app.rb +367 -0
  12. data/examples/blog/views/index.erb +36 -0
  13. data/examples/blog/views/login.erb +28 -0
  14. data/examples/blog/views/new_post.erb +25 -0
  15. data/examples/blog/views/show.erb +16 -0
  16. data/examples/chat/app.rb +278 -0
  17. data/examples/chat/assets/logo.svg +13 -0
  18. data/examples/chat/assets/style.css +209 -0
  19. data/examples/chat/views/index.erb +142 -0
  20. data/examples/chatbot/README.md +111 -0
  21. data/examples/chatbot/app.rb +1024 -0
  22. data/examples/chatbot/assets/chat.js +249 -0
  23. data/examples/chatbot/assets/compare.js +93 -0
  24. data/examples/chatbot/assets/markdown.js +84 -0
  25. data/examples/chatbot/assets/style.css +215 -0
  26. data/examples/chatbot/schema.sql +25 -0
  27. data/examples/chatbot/views/compare.erb +43 -0
  28. data/examples/chatbot/views/index.erb +42 -0
  29. data/examples/chatbot/views/login.erb +22 -0
  30. data/examples/chatbot/views/setup.erb +23 -0
  31. data/examples/counter/README.md +68 -0
  32. data/examples/counter/app.rb +85 -0
  33. data/examples/experiments/AGENTS.md +91 -0
  34. data/examples/experiments/README.md +99 -0
  35. data/examples/experiments/app.rb +225 -0
  36. data/examples/geohash/Gemfile +11 -0
  37. data/examples/geohash/Gemfile.lock +17 -0
  38. data/examples/geohash/README.md +58 -0
  39. data/examples/geohash/app.rb +33 -0
  40. data/examples/hello.rb +120 -0
  41. data/examples/llm_gateway/README.md +73 -0
  42. data/examples/llm_gateway/app.rb +91 -0
  43. data/examples/maidenhead/Gemfile +7 -0
  44. data/examples/maidenhead/Gemfile.lock +17 -0
  45. data/examples/maidenhead/README.md +47 -0
  46. data/examples/maidenhead/app.rb +46 -0
  47. data/examples/pg_hello.rb +76 -0
  48. data/examples/qdrant/Gemfile +11 -0
  49. data/examples/qdrant/Gemfile.lock +29 -0
  50. data/examples/qdrant/README.md +54 -0
  51. data/examples/sinatra_style.rb +32 -0
  52. data/examples/websocket_echo.rb +37 -0
  53. data/lib/tep/agent_delegation.rb +35 -0
  54. data/lib/tep/app.rb +291 -0
  55. data/lib/tep/assets.rb +52 -0
  56. data/lib/tep/auth.rb +78 -0
  57. data/lib/tep/auth_bearer_token.rb +126 -0
  58. data/lib/tep/auth_oauth2.rb +189 -0
  59. data/lib/tep/auth_oauth2_client.rb +29 -0
  60. data/lib/tep/auth_oauth2_code.rb +40 -0
  61. data/lib/tep/auth_session_cookie.rb +132 -0
  62. data/lib/tep/broadcast.rb +265 -0
  63. data/lib/tep/broadcast_subscription.rb +42 -0
  64. data/lib/tep/cache.rb +49 -0
  65. data/lib/tep/events.rb +257 -0
  66. data/lib/tep/filter.rb +21 -0
  67. data/lib/tep/handler.rb +35 -0
  68. data/lib/tep/http.rb +599 -0
  69. data/lib/tep/identity.rb +67 -0
  70. data/lib/tep/job.rb +186 -0
  71. data/lib/tep/json.rb +572 -0
  72. data/lib/tep/jwt.rb +126 -0
  73. data/lib/tep/live_view.rb +219 -0
  74. data/lib/tep/llm.rb +505 -0
  75. data/lib/tep/logger.rb +85 -0
  76. data/lib/tep/mcp.rb +203 -0
  77. data/lib/tep/multipart.rb +98 -0
  78. data/lib/tep/net.rb +155 -0
  79. data/lib/tep/openai_server.rb +725 -0
  80. data/lib/tep/parallel.rb +168 -0
  81. data/lib/tep/parser.rb +81 -0
  82. data/lib/tep/password.rb +102 -0
  83. data/lib/tep/pg.rb +1128 -0
  84. data/lib/tep/presence.rb +589 -0
  85. data/lib/tep/presence_entry.rb +52 -0
  86. data/lib/tep/proxy.rb +801 -0
  87. data/lib/tep/request.rb +194 -0
  88. data/lib/tep/response.rb +134 -0
  89. data/lib/tep/router.rb +137 -0
  90. data/lib/tep/scheduler.rb +342 -0
  91. data/lib/tep/security.rb +140 -0
  92. data/lib/tep/server.rb +276 -0
  93. data/lib/tep/server_scheduled.rb +375 -0
  94. data/lib/tep/session.rb +98 -0
  95. data/lib/tep/shell.rb +62 -0
  96. data/lib/tep/sphttp.c +858 -0
  97. data/lib/tep/sqlite.rb +215 -0
  98. data/lib/tep/streamer.rb +31 -0
  99. data/lib/tep/tep_pg.c +769 -0
  100. data/lib/tep/tep_sqlite.c +320 -0
  101. data/lib/tep/url.rb +161 -0
  102. data/lib/tep/version.rb +3 -0
  103. data/lib/tep/websocket/connection.rb +171 -0
  104. data/lib/tep/websocket/driver.rb +169 -0
  105. data/lib/tep/websocket/frame.rb +238 -0
  106. data/lib/tep/websocket/handshake.rb +159 -0
  107. data/lib/tep/websocket.rb +68 -0
  108. data/lib/tep.rb +981 -0
  109. data/public/hello.txt +1 -0
  110. data/public/style.css +4 -0
  111. data/spinel-ext.json +33 -0
  112. data/test/helper.rb +248 -0
  113. data/test/real_world/01_simple.rb +5 -0
  114. data/test/real_world/02_lifecycle.rb +20 -0
  115. data/test/real_world/03_chat.rb +75 -0
  116. data/test/real_world/04_health_api.rb +25 -0
  117. data/test/real_world/05_todo_api.rb +57 -0
  118. data/test/real_world/06_basic_auth.rb +25 -0
  119. data/test/real_world/07_bbc_rest_api.rb +228 -0
  120. data/test/real_world/07_sklise_things.rb +109 -0
  121. data/test/real_world/08_jwd83_helloworld.rb +56 -0
  122. data/test/run_all.rb +7 -0
  123. data/test/run_parallel.rb +89 -0
  124. data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
  125. data/test/test_api_gateway.rb +76 -0
  126. data/test/test_auth.rb +223 -0
  127. data/test/test_auth_oauth2.rb +208 -0
  128. data/test/test_auth_session_cookie.rb +198 -0
  129. data/test/test_broadcast.rb +197 -0
  130. data/test/test_broadcast_pg.rb +135 -0
  131. data/test/test_cache.rb +98 -0
  132. data/test/test_cache_static.rb +48 -0
  133. data/test/test_cookies.rb +52 -0
  134. data/test/test_erb.rb +53 -0
  135. data/test/test_erb_ivars.rb +58 -0
  136. data/test/test_events.rb +114 -0
  137. data/test/test_filters.rb +41 -0
  138. data/test/test_geohash_example.rb +89 -0
  139. data/test/test_http.rb +137 -0
  140. data/test/test_http_pool.rb +122 -0
  141. data/test/test_http_pool_send.rb +57 -0
  142. data/test/test_identity.rb +165 -0
  143. data/test/test_inbound_tls.rb +101 -0
  144. data/test/test_inbound_tls_scheduled.rb +101 -0
  145. data/test/test_job.rb +108 -0
  146. data/test/test_json.rb +168 -0
  147. data/test/test_jwt.rb +143 -0
  148. data/test/test_live_view.rb +324 -0
  149. data/test/test_llm.rb +250 -0
  150. data/test/test_llm_gateway.rb +95 -0
  151. data/test/test_logger.rb +101 -0
  152. data/test/test_maidenhead_example.rb +86 -0
  153. data/test/test_mcp.rb +264 -0
  154. data/test/test_misc_v02.rb +54 -0
  155. data/test/test_modular.rb +43 -0
  156. data/test/test_multi_filters.rb +40 -0
  157. data/test/test_mustache.rb +57 -0
  158. data/test/test_openai_server.rb +598 -0
  159. data/test/test_optional_segments.rb +45 -0
  160. data/test/test_parallel.rb +102 -0
  161. data/test/test_params.rb +99 -0
  162. data/test/test_pass.rb +42 -0
  163. data/test/test_password.rb +101 -0
  164. data/test/test_pg.rb +673 -0
  165. data/test/test_presence.rb +374 -0
  166. data/test/test_presence_pg.rb +309 -0
  167. data/test/test_proxy.rb +556 -0
  168. data/test/test_proxy_dsl.rb +119 -0
  169. data/test/test_proxy_streaming.rb +146 -0
  170. data/test/test_real_world.rb +397 -0
  171. data/test/test_regex_routes.rb +52 -0
  172. data/test/test_request_methods.rb +102 -0
  173. data/test/test_response.rb +123 -0
  174. data/test/test_routing.rb +109 -0
  175. data/test/test_scheduler.rb +153 -0
  176. data/test/test_security.rb +72 -0
  177. data/test/test_server_scheduled.rb +56 -0
  178. data/test/test_sessions.rb +59 -0
  179. data/test/test_shell.rb +54 -0
  180. data/test/test_sqlite.rb +148 -0
  181. data/test/test_sqlite_cached.rb +171 -0
  182. data/test/test_static.rb +57 -0
  183. data/test/test_streaming.rb +96 -0
  184. data/test/test_unsupported.rb +32 -0
  185. data/test/test_websocket.rb +152 -0
  186. data/test/test_websocket_echo.rb +138 -0
  187. data/test/views/greet.erb +5 -0
  188. data/test/views/hello.erb +5 -0
  189. data/test/views/list.erb +5 -0
  190. data/test/views/m_ivars.mustache +3 -0
  191. data/test/views/m_simple.mustache +4 -0
  192. data/test/views/mixed.erb +3 -0
  193. metadata +264 -0
data/public/hello.txt ADDED
@@ -0,0 +1 @@
1
+ tep static file serving works.
data/public/style.css ADDED
@@ -0,0 +1,4 @@
1
+ body { font-family: system-ui, sans-serif; margin: 2em; max-width: 50em; }
2
+ h1 { color: #b00; border-bottom: 1px solid #ccc; }
3
+ ul { line-height: 1.6; }
4
+ code { background: #f0f0f0; padding: 0.1em 0.4em; border-radius: 3px; }
data/spinel-ext.json ADDED
@@ -0,0 +1,33 @@
1
+ [
2
+ {
3
+ "name": "sphttp",
4
+ "placeholder": "@TEP_SPHTTP_O@",
5
+ "source": "lib/tep/sphttp.c",
6
+ "cflags": ["-O2"]
7
+ },
8
+ {
9
+ "name": "sqlite",
10
+ "placeholder": "@TEP_SQLITE_O@",
11
+ "source": "lib/tep/tep_sqlite.c",
12
+ "cflags": ["-O2"],
13
+ "pkg_config": "sqlite3",
14
+ "pkg_config_fallback": "-lsqlite3",
15
+ "optional": true,
16
+ "disabled_cflags": "-DNO_SQLITE"
17
+ },
18
+ {
19
+ "name": "pg",
20
+ "placeholder": "@TEP_PG_O@",
21
+ "source": "lib/tep/tep_pg.c",
22
+ "cflags": ["-O2"],
23
+ "optional": true,
24
+ "disabled_cflags": "-DNO_PG"
25
+ },
26
+ {
27
+ "name": "pg",
28
+ "placeholder": "@TEP_PG_CFLAGS@",
29
+ "pkg_config": "libpq",
30
+ "optional": true,
31
+ "disabled_cflags": "-lc"
32
+ }
33
+ ]
data/test/helper.rb ADDED
@@ -0,0 +1,248 @@
1
+ # Test harness for the Sinatra compatibility checklist.
2
+ #
3
+ # Each test class declares one app source via `app_source <<~RB ... RB`,
4
+ # the harness compiles it once with bin/tep (Sinatra-classic style) or
5
+ # spinel directly (subclass style), starts the binary on a fresh port,
6
+ # and runs N tests against the live server. Cleanup happens at_exit.
7
+ #
8
+ # Per-class boot keeps total wall time reasonable -- one ~1s spinel
9
+ # compile per file, not per test.
10
+
11
+ ENV["MT_NO_PLUGINS"] ||= "1" # avoid Rails minitest plugin autoload pulling in railties
12
+ require "minitest/autorun"
13
+ # Force serial execution -- per-class boot expects only one thread
14
+ # touching the class state at a time.
15
+ Minitest.parallel_executor = Minitest::Parallel::Executor.new(1) if defined?(Minitest::Parallel::Executor)
16
+ require "net/http"
17
+ require "uri"
18
+ require "socket"
19
+ require "fileutils"
20
+ require "tmpdir"
21
+
22
+ module TepHarness
23
+ TEP_BIN = File.expand_path("../bin/tep", __dir__)
24
+ SPINEL = ENV.fetch("SPINEL", "spinel")
25
+
26
+ @running = []
27
+ @port = (ENV["TEP_TEST_PORT_BASE"] || "4900").to_i
28
+
29
+ class << self
30
+ attr_reader :port
31
+ end
32
+
33
+ def self.next_port
34
+ p = @port
35
+ @port += 1
36
+ p
37
+ end
38
+
39
+ # Compile `source` (Sinatra-classic by default) and return the bound
40
+ # port. Pass `workers: N` to launch the binary in prefork mode --
41
+ # used by tests that need to exercise cross-worker behaviour (e.g.
42
+ # the #128 parent-only run_end emission).
43
+ def self.spawn_app(source, mode: :sinatra, workers: 1)
44
+ tmp = Dir.mktmpdir("tep-test")
45
+ src = File.join(tmp, "app.rb")
46
+ File.write(src, source)
47
+ bin = File.join(tmp, "app")
48
+ case mode
49
+ when :sinatra
50
+ out = `#{TEP_BIN} build #{src} -o #{bin} 2>&1`
51
+ raise "tep build failed:\n#{out}" unless $?.success?
52
+ when :direct
53
+ out = `#{SPINEL} #{src} -o #{bin} 2>&1`
54
+ raise "spinel failed:\n#{out}" unless $?.success?
55
+ else
56
+ raise "unknown mode: #{mode}"
57
+ end
58
+ port = next_port
59
+ log = File.join(tmp, "app.log")
60
+ args = [bin, "-p", port.to_s]
61
+ args += ["-w", workers.to_s] if workers > 1
62
+ # Spawn the app as its own process-group leader (pgroup: true ->
63
+ # pgid == this pid). The server's shutdown contract is SIGTERM-to-
64
+ # the-pgroup (lib/tep/server.rb): in prefork mode the parent forks
65
+ # workers that block in accept(), and only a GROUP-wide signal
66
+ # reaches them. Teardown signals the group (see #reap); the group
67
+ # being distinct from the test runner's keeps that signal off us.
68
+ pid = Process.spawn(*args, out: log, err: [:child, :out], pgroup: true)
69
+ wait_for_port(port, tmp: tmp, pid: pid)
70
+ @running << { pid: pid, tmp: tmp, log: log, port: port }
71
+ port
72
+ end
73
+
74
+ # Find the spawned record for a given bound port (used by tests
75
+ # that need to send signals to the process they booted, e.g.
76
+ # shutdown-hook tests asserting on run_end emission).
77
+ def self.find_by_port(port)
78
+ @running.find { |s| s[:port] == port }
79
+ end
80
+
81
+ # SIGTERM the process bound to `port` + wait for exit. Removes the
82
+ # entry from @running so kill_all's at_exit doesn't double-reap.
83
+ def self.terminate(port, timeout: 5.0)
84
+ s = find_by_port(port)
85
+ return unless s
86
+ reap(s[:pid], timeout: timeout)
87
+ @running.delete(s)
88
+ end
89
+
90
+ def self.kill_all
91
+ @running.each do |s|
92
+ reap(s[:pid])
93
+ FileUtils.rm_rf(s[:tmp])
94
+ end
95
+ @running.clear
96
+ end
97
+
98
+ # Gracefully stop a spawned app and wait for it to exit, BOUNDED.
99
+ # Signals the whole process group (negative pid) so prefork workers
100
+ # blocked in accept() actually receive the signal -- a bare
101
+ # `Process.kill("TERM", pid)` hits only the parent, leaving workers
102
+ # wedged and the parent stuck in wait_any, which used to hang the
103
+ # at_exit reap forever (test process in do_wait). Escalates to
104
+ # SIGKILL if the graceful stop doesn't land in time, so no
105
+ # misbehaving server can stall the suite.
106
+ def self.reap(pid, timeout: 5.0)
107
+ signal_group(pid, "TERM")
108
+ return if wait_exit(pid, timeout)
109
+ signal_group(pid, "KILL")
110
+ wait_exit(pid, 2.0)
111
+ end
112
+
113
+ # Signal `pid`'s process group; fall back to the bare pid if the
114
+ # group is already gone. Swallows ESRCH (already dead).
115
+ def self.signal_group(pid, sig)
116
+ begin
117
+ Process.kill(sig, -pid)
118
+ rescue Errno::ESRCH
119
+ begin
120
+ Process.kill(sig, pid)
121
+ rescue Errno::ESRCH
122
+ end
123
+ end
124
+ end
125
+
126
+ # Poll-wait for `pid` to exit, up to `timeout` seconds. Returns true
127
+ # if it was reaped (or is already gone), false on timeout.
128
+ def self.wait_exit(pid, timeout)
129
+ deadline = Time.now + timeout
130
+ loop do
131
+ begin
132
+ got, _ = Process.waitpid2(pid, Process::WNOHANG)
133
+ return true if got
134
+ rescue Errno::ECHILD
135
+ return true
136
+ end
137
+ return false if Time.now > deadline
138
+ sleep 0.02
139
+ end
140
+ end
141
+
142
+ def self.wait_for_port(port, timeout: 5.0, tmp: nil, pid: nil)
143
+ deadline = Time.now + timeout
144
+ loop do
145
+ begin
146
+ TCPSocket.new("127.0.0.1", port).close
147
+ return
148
+ rescue Errno::ECONNREFUSED
149
+ sleep 0.05
150
+ if Time.now > deadline
151
+ msg = "tep server failed to bind :#{port}"
152
+ if tmp
153
+ log = File.join(tmp, "app.log")
154
+ if File.exist?(log)
155
+ msg += "\n--- app log (#{log}) ---\n" + File.read(log)
156
+ end
157
+ end
158
+ alive = pid && (Process.kill(0, pid) rescue false)
159
+ msg += "\n--- pid #{pid} alive=#{!!alive}" if pid
160
+ raise msg
161
+ end
162
+ end
163
+ end
164
+ end
165
+ end
166
+
167
+ # Tear down spawned apps AFTER Minitest finishes. A bare `at_exit`
168
+ # would fire BEFORE the test runner (Ruby at_exit is LIFO and
169
+ # require "minitest/autorun" registers its at_exit first), so
170
+ # @running would be empty and apps would leak as orphans to PID 1
171
+ # when ruby exits. Minitest.after_run fires post-suite, with the
172
+ # @running list populated. See #117 for the original investigation.
173
+ Minitest.after_run { TepHarness.kill_all }
174
+
175
+ # Kill any zombie tep test processes leaking from previous runs.
176
+ # Skip on hosts that don't ship `pgrep` (some slim containers don't).
177
+ if system("which pgrep >/dev/null 2>&1")
178
+ `pgrep -f tep-test 2>/dev/null`.split.each do |pid|
179
+ Process.kill("TERM", pid.to_i) rescue nil
180
+ end
181
+ end
182
+
183
+ class TepTest < Minitest::Test
184
+ # Class-level: capture `app_source` and `app_mode`. Boot lazily on
185
+ # first `setup`. Per-class @port memoises the bound port.
186
+ def self.app_source(src = nil, mode: :sinatra)
187
+ if src
188
+ @app_source = src
189
+ @app_mode = mode
190
+ else
191
+ [@app_source, @app_mode]
192
+ end
193
+ end
194
+
195
+ # Number of prefork workers to launch the test binary with.
196
+ # Defaults to 1 (the existing single-process shape). Tests that
197
+ # need to exercise multi-worker behaviour (e.g. #128 run_end
198
+ # aggregation across workers) call `workers 2` at class scope.
199
+ def self.workers(n = nil)
200
+ if n
201
+ @workers = n
202
+ else
203
+ @workers || 1
204
+ end
205
+ end
206
+
207
+ @@boot_lock = Mutex.new
208
+
209
+ def self.boot!
210
+ @@boot_lock.synchronize do
211
+ return @port if @port
212
+ src, mode = app_source
213
+ raise "#{name}: app_source not set" unless src
214
+ @port = TepHarness.spawn_app(src, mode: mode, workers: workers)
215
+ end
216
+ end
217
+
218
+ def setup
219
+ @port = self.class.boot!
220
+ end
221
+
222
+ # ---- HTTP helpers ----
223
+ def get(path, headers = {}) req(:get, path, nil, headers); end
224
+ def post(path, body = "", headers = {}) req(:post, path, body, headers); end
225
+ def put(path, body = "", headers = {}) req(:put, path, body, headers); end
226
+ def patch(path, body = "", headers = {}) req(:patch, path, body, headers); end
227
+ def delete(path, headers = {}) req(:delete, path, nil, headers); end
228
+ def head(path, headers = {}) req(:head, path, nil, headers); end
229
+
230
+ def req(method, path, body, headers)
231
+ uri = URI("http://127.0.0.1:#{@port}#{path}")
232
+ klass = {
233
+ get: Net::HTTP::Get,
234
+ post: Net::HTTP::Post,
235
+ put: Net::HTTP::Put,
236
+ patch: Net::HTTP::Patch,
237
+ delete: Net::HTTP::Delete,
238
+ head: Net::HTTP::Head,
239
+ options: Net::HTTP::Options,
240
+ }.fetch(method)
241
+ Net::HTTP.start(uri.host, uri.port, read_timeout: 3) do |http|
242
+ r = klass.new(uri)
243
+ r.body = body if body && %i[post put patch].include?(method)
244
+ headers.each { |k, v| r[k] = v }
245
+ http.request(r)
246
+ end
247
+ end
248
+ end
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby -I ../lib -I lib
2
+ # frozen_string_literal: true
3
+
4
+ require 'sinatra'
5
+ get('/') { 'this is a simple app' }
@@ -0,0 +1,20 @@
1
+ #!/usr/bin/env ruby -I ../lib -I lib
2
+ # frozen_string_literal: true
3
+
4
+ require 'sinatra'
5
+
6
+ get('/') do
7
+ 'This shows how lifecycle events work'
8
+ end
9
+
10
+ on_start do
11
+ puts "=============="
12
+ puts " Booting up"
13
+ puts "=============="
14
+ end
15
+
16
+ on_stop do
17
+ puts "================="
18
+ puts " Shutting down"
19
+ puts "================="
20
+ end
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env ruby -I ../lib -I lib
2
+ # frozen_string_literal: true
3
+
4
+ # This example does *not* work properly with WEBrick or other
5
+ # servers that buffer output. To shut down the server, close any
6
+ # open browser tabs that are connected to the chat server.
7
+
8
+ require 'sinatra'
9
+ set :server, :puma
10
+ connections = Set.new
11
+
12
+ get '/' do
13
+ halt erb(:login) unless params[:user]
14
+ erb :chat, locals: { user: params[:user].gsub(/\W/, '') }
15
+ end
16
+
17
+ get '/stream', provides: 'text/event-stream' do
18
+ stream :keep_open do |out|
19
+ if connections.add?(out)
20
+ out.callback { connections.delete(out) }
21
+ end
22
+ out << "heartbeat:\n"
23
+ sleep 1
24
+ rescue
25
+ out.close
26
+ end
27
+ end
28
+
29
+ post '/' do
30
+ connections.each do |out|
31
+ out << "data: #{params[:msg]}\n\n"
32
+ rescue
33
+ out.close
34
+ end
35
+ 204 # response without entity body
36
+ end
37
+
38
+ __END__
39
+
40
+ @@ layout
41
+ <html>
42
+ <head>
43
+ <title>Super Simple Chat with Sinatra</title>
44
+ <meta charset="utf-8" />
45
+ <script src="http://ajax.googleapis.com/ajax/libs/jquery/1/jquery.min.js"></script>
46
+ </head>
47
+ <body><%= yield %></body>
48
+ </html>
49
+
50
+ @@ login
51
+ <form action="/">
52
+ <label for='user'>User Name:</label>
53
+ <input name="user" value="" />
54
+ <input type="submit" value="GO!" />
55
+ </form>
56
+
57
+ @@ chat
58
+ <pre id='chat'></pre>
59
+ <form>
60
+ <input id='msg' placeholder='type message here...' />
61
+ </form>
62
+
63
+ <script>
64
+ // reading
65
+ var es = new EventSource('/stream');
66
+ es.onmessage = function(e) { $('#chat').append(e.data + "\n") };
67
+
68
+ // writing
69
+ $("form").on('submit',function(e) {
70
+ $.post('/', {msg: "<%= user %>: " + $('#msg').val()});
71
+ $('#msg').val(''); $('#msg').focus();
72
+ e.preventDefault();
73
+ });
74
+ </script>
75
+
@@ -0,0 +1,25 @@
1
+ # Common pattern: tiny health/version JSON API. Exercises content_type,
2
+ # multiple routes, no params, no body parsing.
3
+ require 'sinatra'
4
+
5
+ VERSION = '1.4.2'
6
+
7
+ before do
8
+ content_type 'application/json'
9
+ end
10
+
11
+ get '/healthz' do
12
+ '{"status":"ok"}'
13
+ end
14
+
15
+ get '/version' do
16
+ '{"version":"' + VERSION + '"}'
17
+ end
18
+
19
+ get '/' do
20
+ '{"endpoints":["/healthz","/version"]}'
21
+ end
22
+
23
+ not_found do
24
+ '{"error":"not found","path":"' + request.path + '"}'
25
+ end
@@ -0,0 +1,57 @@
1
+ # In-memory todo list. Exercises GET / POST / DELETE, path params,
2
+ # top-level mutable state, and JSON responses.
3
+ require 'sinatra'
4
+
5
+ # Two parallel arrays as the store. Spinel won't let us push procs
6
+ # into a hash, but plain typed arrays are first-class. Seed-and-clear
7
+ # so spinel infers the element type (`[]` defaults to int_array).
8
+ $todos_id = [0]
9
+ $todos_id.delete_at(0)
10
+ $todos_text = [""]
11
+ $todos_text.delete_at(0)
12
+ $next_id = 1
13
+
14
+ before do
15
+ content_type 'application/json'
16
+ end
17
+
18
+ get '/todos' do
19
+ out = '['
20
+ i = 0
21
+ while i < $todos_id.length
22
+ out += '{"id":' + $todos_id[i].to_s + ',"text":"' + $todos_text[i] + '"}'
23
+ out += ',' if i + 1 < $todos_id.length
24
+ i += 1
25
+ end
26
+ out + ']'
27
+ end
28
+
29
+ post '/todos' do
30
+ text = params[:text]
31
+ $todos_id.push($next_id)
32
+ $todos_text.push(text)
33
+ $next_id += 1
34
+ '{"id":' + ($next_id - 1).to_s + ',"text":"' + text + '"}'
35
+ end
36
+
37
+ delete '/todos/:id' do
38
+ target = params[:id].to_i
39
+ i = 0
40
+ found = false
41
+ while i < $todos_id.length
42
+ if $todos_id[i] == target
43
+ $todos_id.delete_at(i)
44
+ $todos_text.delete_at(i)
45
+ found = true
46
+ i = $todos_id.length
47
+ else
48
+ i += 1
49
+ end
50
+ end
51
+ if found
52
+ '{"deleted":' + target.to_s + '}'
53
+ else
54
+ status 404
55
+ '{"error":"not found"}'
56
+ end
57
+ end
@@ -0,0 +1,25 @@
1
+ # Before-filter auth: every /admin/* request must carry a token. Tests
2
+ # the halt-from-before-filter pattern.
3
+ require 'sinatra'
4
+
5
+ TOKEN = 'sekret-42'
6
+
7
+ before do
8
+ if request.path.start_with?('/admin')
9
+ if request.headers['x-token'] != TOKEN
10
+ halt 401, 'forbidden'
11
+ end
12
+ end
13
+ end
14
+
15
+ get '/' do
16
+ 'public homepage'
17
+ end
18
+
19
+ get '/admin/dashboard' do
20
+ 'admin: ok'
21
+ end
22
+
23
+ get '/admin/users' do
24
+ 'admin: users'
25
+ end