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/lib/tep/sqlite.rb ADDED
@@ -0,0 +1,215 @@
1
+ # Tep::SQLite -- a thin wrapper around the system libsqlite3 for
2
+ # spinel-AOT'd apps. Uses tep_sqlite.c (compiled to tep_sqlite.o)
3
+ # as a stable C ABI surface, exposed via spinel's `ffi_func` DSL.
4
+ #
5
+ # Why not the `sqlite3` gem? It's a CRuby-MRI native extension
6
+ # (loadable .so/.bundle), which spinel can't link -- spinel
7
+ # produces a single static binary with everything resolved at
8
+ # compile time. The C-shim approach (same pattern as tep's HTTP
9
+ # server in sphttp.c) replaces "load a gem at runtime" with
10
+ # "link a .o at compile time."
11
+ #
12
+ # Usage
13
+ # -----
14
+ #
15
+ # db = Tep::SQLite.new
16
+ # db.open("./app.db")
17
+ # db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, body TEXT)")
18
+ #
19
+ # # Parameterised insert: prepare once, bind, step, finalize.
20
+ # db.prepare("INSERT INTO notes (body) VALUES (?)")
21
+ # db.bind_str(1, "hello")
22
+ # db.step
23
+ # db.finalize
24
+ # id = db.last_rowid
25
+ #
26
+ # # Single-row, single-column read.
27
+ # body = db.first_str("SELECT body FROM notes WHERE id = ?", id.to_s)
28
+ #
29
+ # # Iterating rows.
30
+ # db.prepare("SELECT id, body FROM notes ORDER BY id")
31
+ # while db.step == 1
32
+ # puts db.col_int(0).to_s + ": " + db.col_str(1)
33
+ # end
34
+ # db.finalize
35
+ #
36
+ # Constraints
37
+ # -----------
38
+ # - One in-flight cursor per process (the `prepare`/`step`/`finalize`
39
+ # trio shares a single C-side `sqlite3_stmt *`). Nesting one
40
+ # query inside another's loop will overwrite the parent cursor.
41
+ # The framework runs handlers serially per worker so this is
42
+ # fine for "one DB call per request".
43
+ # - Columns are read as either str or int. Floats / blobs / NULL
44
+ # aren't first-class -- a NULL column returns "" (str) or 0 (int).
45
+ # - The C side caps a single col_str result at 64 KiB. Large blobs
46
+ # would truncate.
47
+ #
48
+ # All FFI plumbing lives at the top level (parallel to `Sock`) so
49
+ # spinel's name resolver finds it from anywhere in the Tep tree.
50
+ module Sqlite
51
+ ffi_cflags "@TEP_SQLITE_O@"
52
+ ffi_lib "sqlite3"
53
+
54
+ ffi_func :tep_sqlite_open, [:str], :int
55
+ ffi_func :tep_sqlite_close, [:int], :int
56
+ ffi_func :tep_sqlite_exec, [:int, :str], :int
57
+ ffi_func :tep_sqlite_prepare, [:int, :str], :int
58
+ ffi_func :tep_sqlite_prepare_cached, [:int, :str], :int
59
+ ffi_func :tep_sqlite_bind_str, [:int, :str], :int
60
+ # bind_int / col_int are 64-bit: the value arg + return use the FFI
61
+ # `:long` (64-bit on LP64) routed through sqlite3_bind_int64 /
62
+ # sqlite3_column_int64, so an integer column > 2^31 round-trips
63
+ # without the 32-bit truncation that wrapped large values negative
64
+ # (issue #171). Spinel's mrb_int is pointer-width, so the Ruby side
65
+ # holds the full range. `:long` still maps to the `int` Spinel token,
66
+ # so callers see an Integer exactly as before.
67
+ ffi_func :tep_sqlite_bind_int, [:int, :long], :int
68
+ ffi_func :tep_sqlite_step, [], :int
69
+ ffi_func :tep_sqlite_col_str, [:int], :str
70
+ ffi_func :tep_sqlite_col_int, [:int], :long
71
+ ffi_func :tep_sqlite_col_count, [], :int
72
+ ffi_func :tep_sqlite_finalize, [], :int
73
+ ffi_func :tep_sqlite_reset, [], :int
74
+ ffi_func :tep_sqlite_last_insert_rowid, [:int], :int
75
+ end
76
+
77
+ module Tep
78
+ class SQLite
79
+ # `:dbh` (rather than the natural `:handle`) -- spinel widens
80
+ # poly dispatch return types when a method name is shared across
81
+ # classes with different signatures. `Tep::Handler#handle(req, res)`
82
+ # is the heart of the framework and returns String; an attr_accessor
83
+ # `handle` on Tep::SQLite would emit a 0-arg / int-return arm,
84
+ # widening the dispatch's return type to poly and cascading
85
+ # through `set_body_if_empty(s)` -> `Response#body` -> the
86
+ # sphttp_write_str(int, const char *) call. (See the gemini-bot
87
+ # commentary in spinel PR #391.)
88
+ attr_accessor :dbh
89
+
90
+ def initialize
91
+ @dbh = -1
92
+ end
93
+
94
+ # Returns true on success, false on failure. Path may be a real
95
+ # file or `:memory:` for an anonymous in-memory db. Multiple
96
+ # opens on the same instance leak the prior handle; close first.
97
+ def open(path)
98
+ h = Sqlite.tep_sqlite_open(path)
99
+ if h < 0
100
+ return false
101
+ end
102
+ @dbh = h
103
+ true
104
+ end
105
+
106
+ def close
107
+ if @dbh >= 0
108
+ Sqlite.tep_sqlite_close(@dbh)
109
+ @dbh = -1
110
+ end
111
+ 0
112
+ end
113
+
114
+ # Run a statement that returns no rows (CREATE / INSERT /
115
+ # UPDATE / DELETE / PRAGMA / BEGIN / COMMIT). Returns true on
116
+ # success. No bind in this form -- inline literal SQL is fine
117
+ # for DDL and constants; for any user-supplied value use
118
+ # prepare + bind + step + finalize.
119
+ def exec(sql)
120
+ if @dbh < 0
121
+ return false
122
+ end
123
+ Sqlite.tep_sqlite_exec(@dbh, sql) == 0
124
+ end
125
+
126
+ # Open a cursor on a parameterised query. Subsequent
127
+ # bind_str / bind_int calls fill in `?` markers (1-indexed).
128
+ # Always pair with `finalize` once iteration is done.
129
+ def prepare(sql)
130
+ if @dbh < 0
131
+ return false
132
+ end
133
+ Sqlite.tep_sqlite_prepare(@dbh, sql) == 0
134
+ end
135
+
136
+ # Cached variant. Same surface as `prepare`, but the underlying
137
+ # `sqlite3_stmt *` is memoised per-(db, sql); subsequent calls
138
+ # with the same SQL string reuse the prepared statement, paying
139
+ # the parse cost only once per process. Pair with `finalize` as
140
+ # usual; on the cached path `finalize` becomes
141
+ # `sqlite3_reset + sqlite3_clear_bindings` (the slot stays
142
+ # alive). The cache is bounded (currently 64 distinct SQL
143
+ # strings per process); apps that exceed the bound fall through
144
+ # to uncached prepare so correctness is preserved.
145
+ #
146
+ # Use for hot-path SQL where the string is known + fixed at
147
+ # codegen / boot time. Apps that build SQL with varying
148
+ # whitespace miss the cache (match is literal); format
149
+ # consistently.
150
+ def prepare_cached(sql)
151
+ if @dbh < 0
152
+ return false
153
+ end
154
+ Sqlite.tep_sqlite_prepare_cached(@dbh, sql) == 0
155
+ end
156
+
157
+ def bind_str(idx, value); Sqlite.tep_sqlite_bind_str(idx, value); end
158
+ def bind_int(idx, value); Sqlite.tep_sqlite_bind_int(idx, value); end
159
+
160
+ # 1 -> row available, 0 -> done (no more rows), -1 -> error.
161
+ def step; Sqlite.tep_sqlite_step; end
162
+ def col_str(i); Sqlite.tep_sqlite_col_str(i); end
163
+ def col_int(i); Sqlite.tep_sqlite_col_int(i); end
164
+ def col_count; Sqlite.tep_sqlite_col_count; end
165
+ def finalize; Sqlite.tep_sqlite_finalize; end
166
+ def reset; Sqlite.tep_sqlite_reset; end
167
+
168
+ def last_rowid
169
+ if @dbh < 0
170
+ return -1
171
+ end
172
+ Sqlite.tep_sqlite_last_insert_rowid(@dbh)
173
+ end
174
+
175
+ # Convenience: prepare a single-row, single-column query, bind
176
+ # one optional string param (pass "" for "no param"), step
177
+ # once, return col[0]. Always finalises the cursor before
178
+ # returning so the caller doesn't have to.
179
+ def first_str(sql, p1)
180
+ if @dbh < 0
181
+ return ""
182
+ end
183
+ if Sqlite.tep_sqlite_prepare(@dbh, sql) != 0
184
+ return ""
185
+ end
186
+ if p1.length > 0
187
+ Sqlite.tep_sqlite_bind_str(1, p1)
188
+ end
189
+ result = ""
190
+ if Sqlite.tep_sqlite_step == 1
191
+ result = Sqlite.tep_sqlite_col_str(0)
192
+ end
193
+ Sqlite.tep_sqlite_finalize
194
+ result
195
+ end
196
+
197
+ def first_int(sql, p1)
198
+ if @dbh < 0
199
+ return 0
200
+ end
201
+ if Sqlite.tep_sqlite_prepare(@dbh, sql) != 0
202
+ return 0
203
+ end
204
+ if p1.length > 0
205
+ Sqlite.tep_sqlite_bind_str(1, p1)
206
+ end
207
+ result = 0
208
+ if Sqlite.tep_sqlite_step == 1
209
+ result = Sqlite.tep_sqlite_col_int(0)
210
+ end
211
+ Sqlite.tep_sqlite_finalize
212
+ result
213
+ end
214
+ end
215
+ end
@@ -0,0 +1,31 @@
1
+ # Tep::Streamer -- subclass and override #pump(out). The framework
2
+ # emits chunked-encoding headers, calls pump with a Stream writer,
3
+ # then emits the end-of-stream marker. Cooperative; pump runs to
4
+ # completion before the connection moves on.
5
+ #
6
+ # spinel can't pass blocks into the framework, so this is the
7
+ # subclass-equivalent of `stream do |out| ... end`. The translator
8
+ # recognises the do/end form and emits a Streamer subclass for you.
9
+ module Tep
10
+ class Streamer
11
+ def pump(out)
12
+ # default no-op; subclasses override
13
+ 0
14
+ end
15
+ end
16
+
17
+ # Per-request handle the user's `pump` writes to. Wraps the client
18
+ # fd so each `out.write(s)` becomes one chunked frame.
19
+ class Stream
20
+ attr_accessor :fd
21
+
22
+ def initialize(fd)
23
+ @fd = fd
24
+ end
25
+
26
+ def write(s)
27
+ Sock.sphttp_write_chunk(@fd, s)
28
+ 0
29
+ end
30
+ end
31
+ end