@rip-lang/server 1.3.115 → 1.3.117
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.
- package/README.md +435 -622
- package/api.rip +4 -4
- package/control/cli.rip +221 -1
- package/control/control.rip +9 -0
- package/control/lifecycle.rip +6 -1
- package/control/watchers.rip +10 -0
- package/control/workers.rip +9 -5
- package/default.rip +3 -1
- package/docs/READ_VALIDATORS.md +656 -0
- package/docs/edge/CONFIG_LIFECYCLE.md +70 -32
- package/docs/edge/CONTRACTS.md +60 -69
- package/docs/edge/EDGEFILE_CONTRACT.md +258 -29
- package/docs/edge/M0B_REVIEW_NOTES.md +5 -5
- package/edge/config.rip +584 -52
- package/edge/forwarding.rip +6 -2
- package/edge/metrics.rip +19 -1
- package/edge/registry.rip +29 -3
- package/edge/router.rip +138 -0
- package/edge/runtime.rip +98 -0
- package/edge/static.rip +69 -0
- package/edge/tls.rip +23 -0
- package/edge/upstream.rip +272 -0
- package/edge/verify.rip +73 -0
- package/middleware.rip +3 -3
- package/package.json +2 -2
- package/server.rip +775 -393
- package/tests/control.rip +18 -0
- package/tests/edgefile.rip +165 -0
- package/tests/metrics.rip +16 -0
- package/tests/proxy.rip +22 -1
- package/tests/registry.rip +27 -0
- package/tests/router.rip +101 -0
- package/tests/runtime_entrypoints.rip +16 -0
- package/tests/servers.rip +262 -0
- package/tests/static.rip +64 -0
- package/tests/streams_clienthello.rip +108 -0
- package/tests/streams_index.rip +53 -0
- package/tests/streams_pipe.rip +70 -0
- package/tests/streams_router.rip +39 -0
- package/tests/streams_runtime.rip +38 -0
- package/tests/streams_upstream.rip +34 -0
- package/tests/upstream.rip +191 -0
- package/tests/verify.rip +148 -0
- package/tests/watchers.rip +15 -0
|
@@ -0,0 +1,656 @@
|
|
|
1
|
+
# Validation Reference — `read()`
|
|
2
|
+
|
|
3
|
+
> Extracted from the main [@rip-lang/server README](../README.md).
|
|
4
|
+
|
|
5
|
+
The `read()` function is a validation and parsing powerhouse that eliminates
|
|
6
|
+
90% of API boilerplate.
|
|
7
|
+
|
|
8
|
+
### Basic Patterns
|
|
9
|
+
|
|
10
|
+
```coffee
|
|
11
|
+
# Required field (throws if missing)
|
|
12
|
+
email = read 'email', 'email!'
|
|
13
|
+
|
|
14
|
+
# Optional field (returns null if missing)
|
|
15
|
+
phone = read 'phone', 'phone'
|
|
16
|
+
|
|
17
|
+
# With default value
|
|
18
|
+
role = read 'role', ['admin', 'user'], 'user'
|
|
19
|
+
|
|
20
|
+
# Get entire payload
|
|
21
|
+
data = read()
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
### Range Validation
|
|
25
|
+
|
|
26
|
+
The `[min, max]` syntax works for both numbers and string lengths:
|
|
27
|
+
|
|
28
|
+
```coffee
|
|
29
|
+
# Numbers: value range
|
|
30
|
+
age = read 'age', 'int', [18, 120] # Between 18 and 120
|
|
31
|
+
priority = read 'priority', 'int', [1, 10] # 1-10 range
|
|
32
|
+
|
|
33
|
+
# Strings: length range
|
|
34
|
+
username = read 'username', 'string', [3, 20] # 3-20 characters
|
|
35
|
+
bio = read 'bio', 'string', [0, 500] # Up to 500 chars
|
|
36
|
+
|
|
37
|
+
# Named parameters
|
|
38
|
+
views = read 'views', 'int', min: 0 # Non-negative integer
|
|
39
|
+
discount = read 'discount', 'float', max: 100 # Up to 100
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Enumeration Validation
|
|
43
|
+
|
|
44
|
+
```coffee
|
|
45
|
+
# Must be one of these values
|
|
46
|
+
role = read 'role', ['admin', 'user', 'guest']
|
|
47
|
+
status = read 'status', ['pending', 'active', 'closed']
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Regex Validation
|
|
51
|
+
|
|
52
|
+
```coffee
|
|
53
|
+
# Custom pattern matching
|
|
54
|
+
code = read 'code', /^[A-Z]{3,6}$/
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
## Built-in Validators
|
|
58
|
+
|
|
59
|
+
`@rip-lang/server` includes 37 validators for every common API need:
|
|
60
|
+
|
|
61
|
+
### Numbers & Money
|
|
62
|
+
```coffee
|
|
63
|
+
id = read 'user_id', 'id!' # Positive integer (1+)
|
|
64
|
+
count = read 'count', 'whole' # Non-negative integer (0+)
|
|
65
|
+
price = read 'price', 'float' # Decimal number
|
|
66
|
+
cost = read 'cost', 'money' # Banker's rounding to cents
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Text Processing
|
|
70
|
+
```coffee
|
|
71
|
+
title = read 'title', 'string' # Collapses whitespace
|
|
72
|
+
bio = read 'bio', 'text' # Light cleanup
|
|
73
|
+
name = read 'name', 'name' # Trims and normalizes
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Contact Information
|
|
77
|
+
```coffee
|
|
78
|
+
email = read 'email', 'email' # Valid email format
|
|
79
|
+
phone = read 'phone', 'phone' # US phone → (555) 123-4567
|
|
80
|
+
address = read 'address', 'address' # Trimmed address
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Geographic Data
|
|
84
|
+
```coffee
|
|
85
|
+
state = read 'state', 'state' # Two-letter → uppercase
|
|
86
|
+
zip = read 'zip', 'zip' # 5-digit zip
|
|
87
|
+
zipplus4 = read 'zip', 'zipplus4' # 12345-6789 format
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### Identity & Security
|
|
91
|
+
```coffee
|
|
92
|
+
ssn = read 'ssn', 'ssn' # SSN → digits only
|
|
93
|
+
sex = read 'gender', 'sex' # m/f/o
|
|
94
|
+
username = read 'username', 'username' # 3-20 chars, lowercase
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
### Web & Technical
|
|
98
|
+
```coffee
|
|
99
|
+
url = read 'website', 'url' # Valid URL
|
|
100
|
+
ip = read 'ip_address', 'ip' # IPv4 address
|
|
101
|
+
mac = read 'mac', 'mac' # MAC address
|
|
102
|
+
color = read 'color', 'color' # Hex color → #abc123
|
|
103
|
+
uuid = read 'user_id', 'uuid' # UUID format
|
|
104
|
+
semver = read 'version', 'semver' # Semantic version
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### Time & Date
|
|
108
|
+
```coffee
|
|
109
|
+
date = read 'date', 'date' # YYYY-MM-DD
|
|
110
|
+
time = read 'time', 'time' # HH:MM or HH:MM:SS (24-hour)
|
|
111
|
+
time12 = read 'time', 'time12' # 12-hour with am/pm
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
### Boolean & Collections
|
|
115
|
+
```coffee
|
|
116
|
+
active = read 'active', 'truthy' # true/t/1/yes/y/on → true
|
|
117
|
+
inactive = read 'off', 'falsy' # false/f/0/no/n/off → true
|
|
118
|
+
flag = read 'flag', 'bool' # Either → boolean
|
|
119
|
+
tags = read 'tags', 'array' # Must be array
|
|
120
|
+
config = read 'config', 'hash' # Must be object
|
|
121
|
+
settings = read 'data', 'json' # Parse JSON string
|
|
122
|
+
ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
|
|
123
|
+
slug = read 'slug', 'slug' # URL-safe slug
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
### Custom Validators
|
|
127
|
+
|
|
128
|
+
```coffee
|
|
129
|
+
import { registerValidator, read } from '@rip-lang/server'
|
|
130
|
+
|
|
131
|
+
registerValidator 'postalCode', (v) ->
|
|
132
|
+
if v =~ /^[A-Z]\d[A-Z] \d[A-Z]\d$/i
|
|
133
|
+
_[0].toUpperCase()
|
|
134
|
+
else
|
|
135
|
+
null
|
|
136
|
+
|
|
137
|
+
# Now use it
|
|
138
|
+
code = read 'postal', 'postalCode!'
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
## Routing
|
|
142
|
+
|
|
143
|
+
### HTTP Methods
|
|
144
|
+
|
|
145
|
+
```coffee
|
|
146
|
+
import { get, post, put, patch, del, all } from '@rip-lang/server'
|
|
147
|
+
|
|
148
|
+
get '/users' -> listUsers!
|
|
149
|
+
post '/users' -> createUser!
|
|
150
|
+
get '/users/:id' -> getUser!
|
|
151
|
+
put '/users/:id' -> updateUser!
|
|
152
|
+
patch '/users/:id' -> patchUser!
|
|
153
|
+
del '/users/:id' -> deleteUser!
|
|
154
|
+
all '/health' -> 'ok' # All methods
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Path Parameters
|
|
158
|
+
|
|
159
|
+
```coffee
|
|
160
|
+
# Basic parameters
|
|
161
|
+
get '/users/:id' ->
|
|
162
|
+
id = read 'id', 'id!'
|
|
163
|
+
{ id }
|
|
164
|
+
|
|
165
|
+
# Multiple parameters
|
|
166
|
+
get '/users/:userId/posts/:postId' ->
|
|
167
|
+
userId = read 'userId', 'id!'
|
|
168
|
+
postId = read 'postId', 'id!'
|
|
169
|
+
{ userId, postId }
|
|
170
|
+
|
|
171
|
+
# Custom patterns
|
|
172
|
+
get '/files/:name{[a-z]+\\.txt}' ->
|
|
173
|
+
name = read 'name'
|
|
174
|
+
{ file: name }
|
|
175
|
+
|
|
176
|
+
# Wildcards
|
|
177
|
+
get '/static/*', (env) ->
|
|
178
|
+
{ path: env.req.path }
|
|
179
|
+
```
|
|
180
|
+
|
|
181
|
+
### Route Grouping
|
|
182
|
+
|
|
183
|
+
```coffee
|
|
184
|
+
import { prefix } from '@rip-lang/server'
|
|
185
|
+
|
|
186
|
+
prefix '/api/v1' ->
|
|
187
|
+
get '/users' -> listUsers!
|
|
188
|
+
get '/posts' -> listPosts!
|
|
189
|
+
|
|
190
|
+
prefix '/api/v2' ->
|
|
191
|
+
get '/users' -> listUsersV2!
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
## Middleware
|
|
195
|
+
|
|
196
|
+
### Built-in Middleware
|
|
197
|
+
|
|
198
|
+
Import from `@rip-lang/server/middleware`:
|
|
199
|
+
|
|
200
|
+
```coffee
|
|
201
|
+
import { use } from '@rip-lang/server'
|
|
202
|
+
import { cors, logger, compress, sessions, secureHeaders, timeout, bodyLimit } from '@rip-lang/server/middleware'
|
|
203
|
+
|
|
204
|
+
# Logging
|
|
205
|
+
use logger()
|
|
206
|
+
use logger format: 'tiny' # Minimal output
|
|
207
|
+
use logger format: 'dev' # Colorized (default)
|
|
208
|
+
use logger skip: (c) -> c.req.path is '/health'
|
|
209
|
+
|
|
210
|
+
# CORS
|
|
211
|
+
use cors() # Allow all origins
|
|
212
|
+
use cors origin: 'https://myapp.com' # Specific origin
|
|
213
|
+
use cors origin: ['https://a.com', 'https://b.com']
|
|
214
|
+
use cors credentials: true, maxAge: 86400
|
|
215
|
+
|
|
216
|
+
# Compression (gzip/deflate)
|
|
217
|
+
use compress()
|
|
218
|
+
use compress threshold: 1024 # Min bytes to compress
|
|
219
|
+
|
|
220
|
+
# Security headers
|
|
221
|
+
use secureHeaders()
|
|
222
|
+
use secureHeaders hsts: true, contentSecurityPolicy: "default-src 'self'"
|
|
223
|
+
|
|
224
|
+
# Request limits
|
|
225
|
+
use timeout ms: 30000 # 30 second timeout
|
|
226
|
+
use bodyLimit maxSize: 1024 * 1024 # 1MB max body
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
### Middleware Options
|
|
230
|
+
|
|
231
|
+
| Middleware | Options |
|
|
232
|
+
|------------|---------|
|
|
233
|
+
| `logger()` | `format`, `skip`, `stream` |
|
|
234
|
+
| `cors()` | `origin`, `methods`, `headers`, `credentials`, `maxAge`, `exposeHeaders`, `preflight` |
|
|
235
|
+
| `compress()` | `threshold`, `encodings` |
|
|
236
|
+
| `sessions()` | `secret`, `name`, `maxAge`, `secure`, `httpOnly`, `sameSite` |
|
|
237
|
+
| `secureHeaders()` | `hsts`, `hstsMaxAge`, `contentSecurityPolicy`, `frameOptions`, `referrerPolicy` |
|
|
238
|
+
| `timeout()` | `ms`, `message`, `status` |
|
|
239
|
+
| `bodyLimit()` | `maxSize`, `message` |
|
|
240
|
+
|
|
241
|
+
### Session Usage
|
|
242
|
+
|
|
243
|
+
```coffee
|
|
244
|
+
import { get, use, before, session } from '@rip-lang/server'
|
|
245
|
+
import { sessions } from '@rip-lang/server/middleware'
|
|
246
|
+
|
|
247
|
+
# Sessions parses cookies directly from request headers
|
|
248
|
+
use sessions secret: process.env.SESSION_SECRET
|
|
249
|
+
|
|
250
|
+
before ->
|
|
251
|
+
session.views ?= 0
|
|
252
|
+
session.views += 1
|
|
253
|
+
|
|
254
|
+
get '/profile' ->
|
|
255
|
+
{ userId: session.userId, views: session.views }
|
|
256
|
+
|
|
257
|
+
get '/login' ->
|
|
258
|
+
session.userId = 123
|
|
259
|
+
{ loggedIn: true }
|
|
260
|
+
|
|
261
|
+
get '/logout' ->
|
|
262
|
+
delete session.userId
|
|
263
|
+
{ loggedOut: true }
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
|
|
267
|
+
|
|
268
|
+
**Security note:** Without `secret`, sessions use plain base64 (dev only). With `secret`, sessions are HMAC-SHA256 signed (tamper-proof). Always set `secret` in production.
|
|
269
|
+
|
|
270
|
+
### CORS with Preflight
|
|
271
|
+
|
|
272
|
+
```coffee
|
|
273
|
+
import { use } from '@rip-lang/server'
|
|
274
|
+
import { cors } from '@rip-lang/server/middleware'
|
|
275
|
+
|
|
276
|
+
# Handle OPTIONS early (before routes are matched)
|
|
277
|
+
use cors origin: 'https://myapp.com', preflight: true
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
### Custom Middleware
|
|
281
|
+
|
|
282
|
+
```coffee
|
|
283
|
+
# Authentication middleware
|
|
284
|
+
use (c, next) ->
|
|
285
|
+
token = @req.header 'Authorization'
|
|
286
|
+
unless token
|
|
287
|
+
return @json { error: 'Unauthorized' }, 401
|
|
288
|
+
@user = validateToken!(token)
|
|
289
|
+
await next()
|
|
290
|
+
|
|
291
|
+
# Timing middleware
|
|
292
|
+
use (c, next) ->
|
|
293
|
+
start = Date.now()
|
|
294
|
+
await next()
|
|
295
|
+
@header 'X-Response-Time', "#{Date.now() - start}ms"
|
|
296
|
+
```
|
|
297
|
+
|
|
298
|
+
### Request Lifecycle Filters
|
|
299
|
+
|
|
300
|
+
Three filters run at different stages: `raw` → `before` → handler → `after`
|
|
301
|
+
|
|
302
|
+
```coffee
|
|
303
|
+
import { raw, before, after, get } from '@rip-lang/server'
|
|
304
|
+
|
|
305
|
+
# Runs first — modify raw request before body parsing
|
|
306
|
+
raw (req) ->
|
|
307
|
+
if req.headers.get('X-Raw-SQL') is 'true'
|
|
308
|
+
req.headers.set 'content-type', 'text/plain'
|
|
309
|
+
|
|
310
|
+
skipPaths = ['/favicon.ico', '/ping', '/health']
|
|
311
|
+
|
|
312
|
+
# Runs before handler (after body parsing)
|
|
313
|
+
before ->
|
|
314
|
+
@start = Date.now()
|
|
315
|
+
@silent = @req.path in skipPaths
|
|
316
|
+
unless @req.header 'Authorization'
|
|
317
|
+
return @json { error: 'Unauthorized' }, 401
|
|
318
|
+
|
|
319
|
+
# Runs after handler
|
|
320
|
+
after ->
|
|
321
|
+
return if @silent
|
|
322
|
+
console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
**Note:** `raw` receives the native `Request` object (before parsing). `before` and `after` use `@` to access the context.
|
|
326
|
+
|
|
327
|
+
**How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
|
|
328
|
+
- `@req` — Request object
|
|
329
|
+
- `@json()`, `@text()`, `@html()`, `@redirect()`, `@send()` — Response helpers
|
|
330
|
+
- `@header()` — Response header modifier
|
|
331
|
+
- `@anything` — Custom per-request state
|
|
332
|
+
|
|
333
|
+
**Imports that work anywhere** (via AsyncLocalStorage or Proxy):
|
|
334
|
+
- `read` — Validated request parameters
|
|
335
|
+
- `session` — Session data (if middleware enabled)
|
|
336
|
+
- `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
|
|
337
|
+
|
|
338
|
+
## Context Object
|
|
339
|
+
|
|
340
|
+
Use `@` to access the context directly — no parameter needed:
|
|
341
|
+
|
|
342
|
+
### Response Helpers
|
|
343
|
+
|
|
344
|
+
```coffee
|
|
345
|
+
get '/demo' ->
|
|
346
|
+
# JSON response
|
|
347
|
+
@json { data: 'value' }
|
|
348
|
+
@json { data: 'value' }, 201 # With status
|
|
349
|
+
@json { data: 'value' }, 200, { 'X-Custom': 'header' }
|
|
350
|
+
|
|
351
|
+
# Text response
|
|
352
|
+
@text 'Hello'
|
|
353
|
+
@text 'Created', 201
|
|
354
|
+
|
|
355
|
+
# HTML response
|
|
356
|
+
@html '<h1>Hello</h1>'
|
|
357
|
+
|
|
358
|
+
# Redirect
|
|
359
|
+
@redirect '/new-location'
|
|
360
|
+
@redirect '/new-location', 301 # Permanent
|
|
361
|
+
|
|
362
|
+
# Raw body
|
|
363
|
+
@body data, 200, { 'Content-Type': 'application/octet-stream' }
|
|
364
|
+
|
|
365
|
+
# File serving (auto-detected MIME type via Bun.file)
|
|
366
|
+
@send 'public/style.css' # text/css
|
|
367
|
+
@send 'data/export.json', 'application/json' # explicit type
|
|
368
|
+
```
|
|
369
|
+
|
|
370
|
+
### Request Helpers
|
|
371
|
+
|
|
372
|
+
```coffee
|
|
373
|
+
get '/info' ->
|
|
374
|
+
# Path and query parameters — use read() for validation!
|
|
375
|
+
id = read 'id', 'id!'
|
|
376
|
+
q = read 'q'
|
|
377
|
+
|
|
378
|
+
# Headers
|
|
379
|
+
auth = @req.header 'Authorization'
|
|
380
|
+
allHeaders = @req.header()
|
|
381
|
+
|
|
382
|
+
# Body (async)
|
|
383
|
+
json = @req.json!
|
|
384
|
+
text = @req.text!
|
|
385
|
+
form = @req.formData!
|
|
386
|
+
parsed = @req.parseBody!
|
|
387
|
+
|
|
388
|
+
# Raw request
|
|
389
|
+
@req.raw # Native Request object
|
|
390
|
+
@req.method # 'GET', 'POST', etc.
|
|
391
|
+
@req.url # Full URL
|
|
392
|
+
@req.path # Path only
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
### Request-Scoped State
|
|
396
|
+
|
|
397
|
+
```coffee
|
|
398
|
+
# Store data for later middleware/handlers
|
|
399
|
+
use (c, next) ->
|
|
400
|
+
@user = { id: 1, name: 'Alice' }
|
|
401
|
+
@startTime = Date.now()
|
|
402
|
+
await next()
|
|
403
|
+
|
|
404
|
+
get '/profile' ->
|
|
405
|
+
@json @user
|
|
406
|
+
```
|
|
407
|
+
|
|
408
|
+
## File Serving
|
|
409
|
+
|
|
410
|
+
### `@send(path, type?)`
|
|
411
|
+
|
|
412
|
+
Serve a file with auto-detected MIME type. Uses `Bun.file()` internally for
|
|
413
|
+
efficient streaming — the file is never buffered in memory.
|
|
414
|
+
|
|
415
|
+
```coffee
|
|
416
|
+
# Auto-detected content type (30+ extensions supported)
|
|
417
|
+
get '/css/*' -> @send "css/#{@req.path.slice(5)}"
|
|
418
|
+
|
|
419
|
+
# Explicit content type
|
|
420
|
+
get '/files/*' -> @send "uploads/#{@req.path.slice(7)}", 'application/octet-stream'
|
|
421
|
+
|
|
422
|
+
# SPA fallback — serve index.html for all unmatched routes
|
|
423
|
+
notFound -> @send 'index.html', 'text/html; charset=UTF-8'
|
|
424
|
+
```
|
|
425
|
+
|
|
426
|
+
### `mimeType(path)`
|
|
427
|
+
|
|
428
|
+
Exported utility that returns the MIME type for a file path:
|
|
429
|
+
|
|
430
|
+
```coffee
|
|
431
|
+
import { mimeType } from '@rip-lang/server'
|
|
432
|
+
|
|
433
|
+
mimeType 'style.css' # 'text/css; charset=UTF-8'
|
|
434
|
+
mimeType 'app.js' # 'application/javascript'
|
|
435
|
+
mimeType 'photo.png' # 'image/png'
|
|
436
|
+
mimeType 'data.xyz' # 'application/octet-stream'
|
|
437
|
+
```
|
|
438
|
+
|
|
439
|
+
## Error Handling
|
|
440
|
+
|
|
441
|
+
### Custom Error Handler
|
|
442
|
+
|
|
443
|
+
```coffee
|
|
444
|
+
import { onError } from '@rip-lang/server'
|
|
445
|
+
|
|
446
|
+
onError (err, c) ->
|
|
447
|
+
console.error 'Error:', err
|
|
448
|
+
c.json { error: err.message }, err.status or 500
|
|
449
|
+
```
|
|
450
|
+
|
|
451
|
+
### Custom 404 Handler
|
|
452
|
+
|
|
453
|
+
```coffee
|
|
454
|
+
import { notFound } from '@rip-lang/server'
|
|
455
|
+
|
|
456
|
+
notFound (c) ->
|
|
457
|
+
c.json { error: 'Not found', path: c.req.path }, 404
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
## Server Options
|
|
461
|
+
|
|
462
|
+
### Basic Server
|
|
463
|
+
|
|
464
|
+
```coffee
|
|
465
|
+
import { start } from '@rip-lang/server'
|
|
466
|
+
|
|
467
|
+
start port: 3000
|
|
468
|
+
start port: 3000, host: '0.0.0.0'
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
### Handler Only (for custom servers)
|
|
472
|
+
|
|
473
|
+
```coffee
|
|
474
|
+
import { startHandler } from '@rip-lang/server'
|
|
475
|
+
|
|
476
|
+
export default startHandler()
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
### App Pattern
|
|
480
|
+
|
|
481
|
+
```coffee
|
|
482
|
+
import { App, get, post } from '@rip-lang/server'
|
|
483
|
+
|
|
484
|
+
export default App ->
|
|
485
|
+
get '/' -> 'Hello'
|
|
486
|
+
post '/echo' -> read()
|
|
487
|
+
```
|
|
488
|
+
|
|
489
|
+
## Context Utilities
|
|
490
|
+
|
|
491
|
+
### ctx()
|
|
492
|
+
|
|
493
|
+
Get the current request context from anywhere (via AsyncLocalStorage):
|
|
494
|
+
|
|
495
|
+
```coffee
|
|
496
|
+
import { ctx } from '@rip-lang/server'
|
|
497
|
+
|
|
498
|
+
logRequest = ->
|
|
499
|
+
c = ctx()
|
|
500
|
+
console.log "#{c.req.method} #{c.req.path}" if c
|
|
501
|
+
|
|
502
|
+
get '/demo' ->
|
|
503
|
+
logRequest()
|
|
504
|
+
{ ok: true }
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
### resetGlobals()
|
|
508
|
+
|
|
509
|
+
Reset all global state (routes, middleware, filters). Useful for testing:
|
|
510
|
+
|
|
511
|
+
```coffee
|
|
512
|
+
import { resetGlobals, get, start } from '@rip-lang/server'
|
|
513
|
+
|
|
514
|
+
beforeEach ->
|
|
515
|
+
resetGlobals()
|
|
516
|
+
|
|
517
|
+
get '/test' -> { test: true }
|
|
518
|
+
```
|
|
519
|
+
|
|
520
|
+
## Utility Functions
|
|
521
|
+
|
|
522
|
+
### isBlank
|
|
523
|
+
|
|
524
|
+
```coffee
|
|
525
|
+
import { isBlank } from '@rip-lang/server'
|
|
526
|
+
|
|
527
|
+
isBlank null # true
|
|
528
|
+
isBlank undefined # true
|
|
529
|
+
isBlank '' # true
|
|
530
|
+
isBlank ' ' # true
|
|
531
|
+
isBlank [] # true
|
|
532
|
+
isBlank {} # true
|
|
533
|
+
isBlank false # true
|
|
534
|
+
isBlank 'hello' # false
|
|
535
|
+
isBlank [1, 2] # false
|
|
536
|
+
```
|
|
537
|
+
|
|
538
|
+
### toName
|
|
539
|
+
|
|
540
|
+
Advanced name formatting with intelligent capitalization:
|
|
541
|
+
|
|
542
|
+
```coffee
|
|
543
|
+
import { toName } from '@rip-lang/server'
|
|
544
|
+
|
|
545
|
+
toName 'john doe' # 'John Doe'
|
|
546
|
+
toName 'JANE SMITH' # 'Jane Smith'
|
|
547
|
+
toName "o'brien" # "O'Brien"
|
|
548
|
+
toName 'mcdonald' # 'McDonald'
|
|
549
|
+
toName 'P. o. bOX #44', 'address' # 'PO Box #44'
|
|
550
|
+
toName '123 main st ne', 'address' # '123 Main St NE'
|
|
551
|
+
```
|
|
552
|
+
|
|
553
|
+
### toPhone
|
|
554
|
+
|
|
555
|
+
US phone number formatting:
|
|
556
|
+
|
|
557
|
+
```coffee
|
|
558
|
+
import { toPhone } from '@rip-lang/server'
|
|
559
|
+
|
|
560
|
+
toPhone '5551234567' # '(555) 123-4567'
|
|
561
|
+
toPhone '555-123-4567' # '(555) 123-4567'
|
|
562
|
+
toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
|
|
563
|
+
toPhone '+1 555 123 4567' # '(555) 123-4567'
|
|
564
|
+
```
|
|
565
|
+
|
|
566
|
+
## Migration from Hono
|
|
567
|
+
|
|
568
|
+
### Before (Hono)
|
|
569
|
+
|
|
570
|
+
```coffee
|
|
571
|
+
import { Hono } from 'hono'
|
|
572
|
+
|
|
573
|
+
app = new Hono()
|
|
574
|
+
app.get '/users/:id', (c) ->
|
|
575
|
+
id = c.req.param 'id'
|
|
576
|
+
c.json { id }
|
|
577
|
+
|
|
578
|
+
export default app
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
### After (@rip-lang/server)
|
|
582
|
+
|
|
583
|
+
```coffee
|
|
584
|
+
import { get, read, startHandler } from '@rip-lang/server'
|
|
585
|
+
|
|
586
|
+
get '/users/:id' ->
|
|
587
|
+
id = read 'id', 'id!'
|
|
588
|
+
{ id }
|
|
589
|
+
|
|
590
|
+
export default startHandler()
|
|
591
|
+
```
|
|
592
|
+
|
|
593
|
+
### API Compatibility
|
|
594
|
+
|
|
595
|
+
| Hono | @rip-lang/server |
|
|
596
|
+
|------|------------------|
|
|
597
|
+
| `app.get(path, handler)` | `get path, handler` |
|
|
598
|
+
| `app.post(path, handler)` | `post path, handler` |
|
|
599
|
+
| `app.use(middleware)` | `use middleware` |
|
|
600
|
+
| `app.basePath(path)` | `prefix path, -> ...` |
|
|
601
|
+
| `c.json(data)` | `@json(data)` or return `{ data }` |
|
|
602
|
+
| `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
|
|
603
|
+
| `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
|
|
604
|
+
|
|
605
|
+
## Real-World Example
|
|
606
|
+
|
|
607
|
+
```coffee
|
|
608
|
+
import { get, post, put, del, use, read, start, before, after, onError } from '@rip-lang/server'
|
|
609
|
+
import { logger } from '@rip-lang/server/middleware'
|
|
610
|
+
|
|
611
|
+
use logger()
|
|
612
|
+
|
|
613
|
+
before ->
|
|
614
|
+
@start = Date.now()
|
|
615
|
+
|
|
616
|
+
after ->
|
|
617
|
+
console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
|
|
618
|
+
|
|
619
|
+
onError (err) ->
|
|
620
|
+
@json { error: err.message }, err.status or 500
|
|
621
|
+
|
|
622
|
+
get '/' ->
|
|
623
|
+
{ name: 'My API', version: '1.0' }
|
|
624
|
+
|
|
625
|
+
get '/users' ->
|
|
626
|
+
page = read 'page', 'int', [1, 100]
|
|
627
|
+
limit = read 'limit', 'int', [1, 50]
|
|
628
|
+
users = db.listUsers! page or 1, limit or 10
|
|
629
|
+
{ users, page, limit }
|
|
630
|
+
|
|
631
|
+
get '/users/:id' ->
|
|
632
|
+
id = read 'id', 'id!'
|
|
633
|
+
user = db.getUser!(id) or throw { message: 'User not found', status: 404 }
|
|
634
|
+
{ user }
|
|
635
|
+
|
|
636
|
+
post '/users' ->
|
|
637
|
+
email = read 'email', 'email!'
|
|
638
|
+
name = read 'name', 'string', [1, 100]
|
|
639
|
+
phone = read 'phone', 'phone'
|
|
640
|
+
user = db.createUser! { email, name, phone }
|
|
641
|
+
{ user, created: true }
|
|
642
|
+
|
|
643
|
+
put '/users/:id' ->
|
|
644
|
+
id = read 'id', 'id!'
|
|
645
|
+
email = read 'email', 'email'
|
|
646
|
+
name = read 'name', 'string', [1, 100]
|
|
647
|
+
user = db.updateUser! id, { email, name }
|
|
648
|
+
{ user, updated: true }
|
|
649
|
+
|
|
650
|
+
del '/users/:id' ->
|
|
651
|
+
id = read 'id', 'id!'
|
|
652
|
+
db.deleteUser!(id)
|
|
653
|
+
{ deleted: true }
|
|
654
|
+
|
|
655
|
+
start port: 3000
|
|
656
|
+
```
|