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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/Makefile +134 -0
- data/README.md +247 -0
- data/SINATRA_COMPAT.md +376 -0
- data/bin/tep +2156 -0
- data/examples/agentic_chat/README.md +103 -0
- data/examples/agentic_chat/app.rb +310 -0
- data/examples/api_gateway/README.md +49 -0
- data/examples/api_gateway/app.rb +66 -0
- data/examples/blog/app.rb +367 -0
- data/examples/blog/views/index.erb +36 -0
- data/examples/blog/views/login.erb +28 -0
- data/examples/blog/views/new_post.erb +25 -0
- data/examples/blog/views/show.erb +16 -0
- data/examples/chat/app.rb +278 -0
- data/examples/chat/assets/logo.svg +13 -0
- data/examples/chat/assets/style.css +209 -0
- data/examples/chat/views/index.erb +142 -0
- data/examples/chatbot/README.md +111 -0
- data/examples/chatbot/app.rb +1024 -0
- data/examples/chatbot/assets/chat.js +249 -0
- data/examples/chatbot/assets/compare.js +93 -0
- data/examples/chatbot/assets/markdown.js +84 -0
- data/examples/chatbot/assets/style.css +215 -0
- data/examples/chatbot/schema.sql +25 -0
- data/examples/chatbot/views/compare.erb +43 -0
- data/examples/chatbot/views/index.erb +42 -0
- data/examples/chatbot/views/login.erb +22 -0
- data/examples/chatbot/views/setup.erb +23 -0
- data/examples/counter/README.md +68 -0
- data/examples/counter/app.rb +85 -0
- data/examples/experiments/AGENTS.md +91 -0
- data/examples/experiments/README.md +99 -0
- data/examples/experiments/app.rb +225 -0
- data/examples/geohash/Gemfile +11 -0
- data/examples/geohash/Gemfile.lock +17 -0
- data/examples/geohash/README.md +58 -0
- data/examples/geohash/app.rb +33 -0
- data/examples/hello.rb +120 -0
- data/examples/llm_gateway/README.md +73 -0
- data/examples/llm_gateway/app.rb +91 -0
- data/examples/maidenhead/Gemfile +7 -0
- data/examples/maidenhead/Gemfile.lock +17 -0
- data/examples/maidenhead/README.md +47 -0
- data/examples/maidenhead/app.rb +46 -0
- data/examples/pg_hello.rb +76 -0
- data/examples/qdrant/Gemfile +11 -0
- data/examples/qdrant/Gemfile.lock +29 -0
- data/examples/qdrant/README.md +54 -0
- data/examples/sinatra_style.rb +32 -0
- data/examples/websocket_echo.rb +37 -0
- data/lib/tep/agent_delegation.rb +35 -0
- data/lib/tep/app.rb +291 -0
- data/lib/tep/assets.rb +52 -0
- data/lib/tep/auth.rb +78 -0
- data/lib/tep/auth_bearer_token.rb +126 -0
- data/lib/tep/auth_oauth2.rb +189 -0
- data/lib/tep/auth_oauth2_client.rb +29 -0
- data/lib/tep/auth_oauth2_code.rb +40 -0
- data/lib/tep/auth_session_cookie.rb +132 -0
- data/lib/tep/broadcast.rb +265 -0
- data/lib/tep/broadcast_subscription.rb +42 -0
- data/lib/tep/cache.rb +49 -0
- data/lib/tep/events.rb +257 -0
- data/lib/tep/filter.rb +21 -0
- data/lib/tep/handler.rb +35 -0
- data/lib/tep/http.rb +599 -0
- data/lib/tep/identity.rb +67 -0
- data/lib/tep/job.rb +186 -0
- data/lib/tep/json.rb +572 -0
- data/lib/tep/jwt.rb +126 -0
- data/lib/tep/live_view.rb +219 -0
- data/lib/tep/llm.rb +505 -0
- data/lib/tep/logger.rb +85 -0
- data/lib/tep/mcp.rb +203 -0
- data/lib/tep/multipart.rb +98 -0
- data/lib/tep/net.rb +155 -0
- data/lib/tep/openai_server.rb +725 -0
- data/lib/tep/parallel.rb +168 -0
- data/lib/tep/parser.rb +81 -0
- data/lib/tep/password.rb +102 -0
- data/lib/tep/pg.rb +1128 -0
- data/lib/tep/presence.rb +589 -0
- data/lib/tep/presence_entry.rb +52 -0
- data/lib/tep/proxy.rb +801 -0
- data/lib/tep/request.rb +194 -0
- data/lib/tep/response.rb +134 -0
- data/lib/tep/router.rb +137 -0
- data/lib/tep/scheduler.rb +342 -0
- data/lib/tep/security.rb +140 -0
- data/lib/tep/server.rb +276 -0
- data/lib/tep/server_scheduled.rb +375 -0
- data/lib/tep/session.rb +98 -0
- data/lib/tep/shell.rb +62 -0
- data/lib/tep/sphttp.c +858 -0
- data/lib/tep/sqlite.rb +215 -0
- data/lib/tep/streamer.rb +31 -0
- data/lib/tep/tep_pg.c +769 -0
- data/lib/tep/tep_sqlite.c +320 -0
- data/lib/tep/url.rb +161 -0
- data/lib/tep/version.rb +3 -0
- data/lib/tep/websocket/connection.rb +171 -0
- data/lib/tep/websocket/driver.rb +169 -0
- data/lib/tep/websocket/frame.rb +238 -0
- data/lib/tep/websocket/handshake.rb +159 -0
- data/lib/tep/websocket.rb +68 -0
- data/lib/tep.rb +981 -0
- data/public/hello.txt +1 -0
- data/public/style.css +4 -0
- data/spinel-ext.json +33 -0
- data/test/helper.rb +248 -0
- data/test/real_world/01_simple.rb +5 -0
- data/test/real_world/02_lifecycle.rb +20 -0
- data/test/real_world/03_chat.rb +75 -0
- data/test/real_world/04_health_api.rb +25 -0
- data/test/real_world/05_todo_api.rb +57 -0
- data/test/real_world/06_basic_auth.rb +25 -0
- data/test/real_world/07_bbc_rest_api.rb +228 -0
- data/test/real_world/07_sklise_things.rb +109 -0
- data/test/real_world/08_jwd83_helloworld.rb +56 -0
- data/test/run_all.rb +7 -0
- data/test/run_parallel.rb +89 -0
- data/test/spinel_scheduled_burst_segv_repro.rb +33 -0
- data/test/test_api_gateway.rb +76 -0
- data/test/test_auth.rb +223 -0
- data/test/test_auth_oauth2.rb +208 -0
- data/test/test_auth_session_cookie.rb +198 -0
- data/test/test_broadcast.rb +197 -0
- data/test/test_broadcast_pg.rb +135 -0
- data/test/test_cache.rb +98 -0
- data/test/test_cache_static.rb +48 -0
- data/test/test_cookies.rb +52 -0
- data/test/test_erb.rb +53 -0
- data/test/test_erb_ivars.rb +58 -0
- data/test/test_events.rb +114 -0
- data/test/test_filters.rb +41 -0
- data/test/test_geohash_example.rb +89 -0
- data/test/test_http.rb +137 -0
- data/test/test_http_pool.rb +122 -0
- data/test/test_http_pool_send.rb +57 -0
- data/test/test_identity.rb +165 -0
- data/test/test_inbound_tls.rb +101 -0
- data/test/test_inbound_tls_scheduled.rb +101 -0
- data/test/test_job.rb +108 -0
- data/test/test_json.rb +168 -0
- data/test/test_jwt.rb +143 -0
- data/test/test_live_view.rb +324 -0
- data/test/test_llm.rb +250 -0
- data/test/test_llm_gateway.rb +95 -0
- data/test/test_logger.rb +101 -0
- data/test/test_maidenhead_example.rb +86 -0
- data/test/test_mcp.rb +264 -0
- data/test/test_misc_v02.rb +54 -0
- data/test/test_modular.rb +43 -0
- data/test/test_multi_filters.rb +40 -0
- data/test/test_mustache.rb +57 -0
- data/test/test_openai_server.rb +598 -0
- data/test/test_optional_segments.rb +45 -0
- data/test/test_parallel.rb +102 -0
- data/test/test_params.rb +99 -0
- data/test/test_pass.rb +42 -0
- data/test/test_password.rb +101 -0
- data/test/test_pg.rb +673 -0
- data/test/test_presence.rb +374 -0
- data/test/test_presence_pg.rb +309 -0
- data/test/test_proxy.rb +556 -0
- data/test/test_proxy_dsl.rb +119 -0
- data/test/test_proxy_streaming.rb +146 -0
- data/test/test_real_world.rb +397 -0
- data/test/test_regex_routes.rb +52 -0
- data/test/test_request_methods.rb +102 -0
- data/test/test_response.rb +123 -0
- data/test/test_routing.rb +109 -0
- data/test/test_scheduler.rb +153 -0
- data/test/test_security.rb +72 -0
- data/test/test_server_scheduled.rb +56 -0
- data/test/test_sessions.rb +59 -0
- data/test/test_shell.rb +54 -0
- data/test/test_sqlite.rb +148 -0
- data/test/test_sqlite_cached.rb +171 -0
- data/test/test_static.rb +57 -0
- data/test/test_streaming.rb +96 -0
- data/test/test_unsupported.rb +32 -0
- data/test/test_websocket.rb +152 -0
- data/test/test_websocket_echo.rb +138 -0
- data/test/views/greet.erb +5 -0
- data/test/views/hello.erb +5 -0
- data/test/views/list.erb +5 -0
- data/test/views/m_ivars.mustache +3 -0
- data/test/views/m_simple.mustache +4 -0
- data/test/views/mixed.erb +3 -0
- metadata +264 -0
data/public/hello.txt
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tep static file serving works.
|
data/public/style.css
ADDED
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,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
|