@rip-lang/api 0.5.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.
- package/README.md +776 -0
- package/api.rip +564 -0
- package/middleware.rip +394 -0
- package/package.json +43 -0
package/README.md
ADDED
|
@@ -0,0 +1,776 @@
|
|
|
1
|
+
<img src="https://raw.githubusercontent.com/shreeve/rip-lang/main/docs/rip.svg" style="width:50px" /> <br>
|
|
2
|
+
|
|
3
|
+
# Rip API - @rip-lang/api
|
|
4
|
+
|
|
5
|
+
> **Pure Rip API framework — elegant, fast, zero dependencies**
|
|
6
|
+
|
|
7
|
+
## Overview
|
|
8
|
+
|
|
9
|
+
`@rip-lang/api` is a complete API framework written entirely in Rip. It provides Hono-compatible routing, middleware composition, and powerful validation — all with no external dependencies.
|
|
10
|
+
|
|
11
|
+
- **`api.rip`** (~565 lines) — Core framework: routing, validation, `read()`, `session`, server
|
|
12
|
+
- **`middleware.rip`** (~395 lines) — Optional middleware: cors, logger, compress, cookies, sessions, etc.
|
|
13
|
+
|
|
14
|
+
**Core Philosophy**: API development should be intuitive, safe, and beautiful. Every function eliminates boilerplate, prevents common errors, and makes your intent crystal clear.
|
|
15
|
+
|
|
16
|
+
### Key Features
|
|
17
|
+
|
|
18
|
+
- **Zero Dependencies** — Pure Rip implementation, no Hono or other frameworks
|
|
19
|
+
- **Sinatra-Style Handlers** — Return data directly, no ceremony
|
|
20
|
+
- **Magic `@` Access** — Use `@req`, `@json()`, `@session`, `@silent` like Sinatra
|
|
21
|
+
- **Powerful Validation** — 37 built-in validators with elegant `read()` function
|
|
22
|
+
- **Before/After Filters** — Sinatra-style request lifecycle hooks
|
|
23
|
+
- **AsyncLocalStorage** — Safe, race-condition-free request context
|
|
24
|
+
- **Hono-Compatible API** — Easy migration from existing Hono apps
|
|
25
|
+
|
|
26
|
+
> **See Also**: For complete Rip language documentation, see the [main rip-lang repository](https://github.com/shreeve/rip-lang) and [docs/GUIDE.md](https://github.com/shreeve/rip-lang/blob/main/docs/GUIDE.md).
|
|
27
|
+
|
|
28
|
+
## Try it Now
|
|
29
|
+
|
|
30
|
+
Run the included example to see everything working:
|
|
31
|
+
|
|
32
|
+
```bash
|
|
33
|
+
# Clone the repo (if you haven't)
|
|
34
|
+
git clone https://github.com/shreeve/rip-lang.git
|
|
35
|
+
cd rip-lang
|
|
36
|
+
|
|
37
|
+
# Run the example server
|
|
38
|
+
bun packages/api/test/example.rip
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
**Test the endpoints:**
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
# Basic routes
|
|
45
|
+
curl http://localhost:3000/
|
|
46
|
+
curl http://localhost:3000/json
|
|
47
|
+
curl http://localhost:3000/users/42
|
|
48
|
+
|
|
49
|
+
# Session demo (use -c/-b for cookies)
|
|
50
|
+
curl -c cookies.txt -b cookies.txt http://localhost:3000/session
|
|
51
|
+
# {"views":1,"loggedIn":false}
|
|
52
|
+
|
|
53
|
+
curl -c cookies.txt -b cookies.txt http://localhost:3000/session
|
|
54
|
+
# {"views":2,"loggedIn":false}
|
|
55
|
+
|
|
56
|
+
curl -c cookies.txt -b cookies.txt http://localhost:3000/login
|
|
57
|
+
# {"loggedIn":true,"userId":123}
|
|
58
|
+
|
|
59
|
+
curl -c cookies.txt -b cookies.txt http://localhost:3000/session
|
|
60
|
+
# {"views":4,"userId":123,"loggedIn":true}
|
|
61
|
+
|
|
62
|
+
curl -c cookies.txt -b cookies.txt http://localhost:3000/logout
|
|
63
|
+
# {"loggedOut":true}
|
|
64
|
+
|
|
65
|
+
# POST with validation
|
|
66
|
+
curl -X POST http://localhost:3000/signup \
|
|
67
|
+
-H "Content-Type: application/json" \
|
|
68
|
+
-d '{"email":"test@example.com","age":25}'
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
## Quick Start
|
|
72
|
+
|
|
73
|
+
### Installation
|
|
74
|
+
|
|
75
|
+
```bash
|
|
76
|
+
bun add @rip-lang/api
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Basic Usage
|
|
80
|
+
|
|
81
|
+
```coffee
|
|
82
|
+
import { get, post, use, read, start } from '@rip-lang/api'
|
|
83
|
+
|
|
84
|
+
# Context-free handlers — just return data!
|
|
85
|
+
get '/', -> 'Hello, World!'
|
|
86
|
+
|
|
87
|
+
get '/json', -> { message: 'It works!', timestamp: Date.now() }
|
|
88
|
+
|
|
89
|
+
# Path parameters
|
|
90
|
+
get '/users/:id', ->
|
|
91
|
+
id = read 'id', 'id!'
|
|
92
|
+
{ user: { id, name: "User #{id}" } }
|
|
93
|
+
|
|
94
|
+
# Form validation
|
|
95
|
+
post '/signup', ->
|
|
96
|
+
email = read 'email', 'email!'
|
|
97
|
+
phone = read 'phone', 'phone'
|
|
98
|
+
age = read 'age', 'int', [18, 120]
|
|
99
|
+
{ success: true, email, phone, age }
|
|
100
|
+
|
|
101
|
+
start port: 3000
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
### With Context (when needed)
|
|
105
|
+
|
|
106
|
+
```coffee
|
|
107
|
+
# Access full context for headers, redirects, etc.
|
|
108
|
+
get '/download/:id', (env) ->
|
|
109
|
+
env.header 'Content-Disposition', 'attachment'
|
|
110
|
+
env.body getFile!(read 'id', 'id!')
|
|
111
|
+
|
|
112
|
+
get '/redirect', (env) ->
|
|
113
|
+
env.redirect 'https://example.com'
|
|
114
|
+
|
|
115
|
+
get '/custom', (env) ->
|
|
116
|
+
env.json { created: true }, 201
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
## The `read()` Function
|
|
120
|
+
|
|
121
|
+
The crown jewel of `@rip-lang/api` — a validation and parsing powerhouse that eliminates 90% of API boilerplate.
|
|
122
|
+
|
|
123
|
+
### Basic Patterns
|
|
124
|
+
|
|
125
|
+
```coffee
|
|
126
|
+
# Required field (throws if missing)
|
|
127
|
+
email = read 'email', 'email!'
|
|
128
|
+
|
|
129
|
+
# Optional field (returns null if missing)
|
|
130
|
+
phone = read 'phone', 'phone'
|
|
131
|
+
|
|
132
|
+
# With default value
|
|
133
|
+
role = read 'role', ['admin', 'user'], 'user'
|
|
134
|
+
|
|
135
|
+
# Get entire payload
|
|
136
|
+
data = read()
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
### Range Validation
|
|
140
|
+
|
|
141
|
+
The `[min, max]` syntax works for both numbers and string lengths:
|
|
142
|
+
|
|
143
|
+
```coffee
|
|
144
|
+
# Numbers: value range
|
|
145
|
+
age = read 'age', 'int', [18, 120] # Between 18 and 120
|
|
146
|
+
priority = read 'priority', 'int', [1, 10] # 1-10 range
|
|
147
|
+
|
|
148
|
+
# Strings: length range
|
|
149
|
+
username = read 'username', 'string', [3, 20] # 3-20 characters
|
|
150
|
+
bio = read 'bio', 'string', [0, 500] # Up to 500 chars
|
|
151
|
+
|
|
152
|
+
# Numbers: value range
|
|
153
|
+
views = read 'views', 'int', min: 0 # Non-negative integer
|
|
154
|
+
discount = read 'discount', 'number', max: 100 # Up to 100
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
### Enumeration Validation
|
|
158
|
+
|
|
159
|
+
```coffee
|
|
160
|
+
# Must be one of these values
|
|
161
|
+
role = read 'role', ['admin', 'user', 'guest']
|
|
162
|
+
status = read 'status', ['pending', 'active', 'closed']
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Regex Validation
|
|
166
|
+
|
|
167
|
+
```coffee
|
|
168
|
+
# Custom pattern matching
|
|
169
|
+
code = read 'code', /^[A-Z]{3,6}$/
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Built-in Validators
|
|
173
|
+
|
|
174
|
+
`@rip-lang/api` includes 37 validators for every common API need:
|
|
175
|
+
|
|
176
|
+
### Numbers & Money
|
|
177
|
+
```coffee
|
|
178
|
+
id = read 'user_id', 'id!' # Positive integer (1+)
|
|
179
|
+
count = read 'count', 'whole' # Non-negative integer (0+)
|
|
180
|
+
price = read 'price', 'decimal' # Decimal number
|
|
181
|
+
cost = read 'cost', 'money' # Cents (multiplies by 100)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
### Text Processing
|
|
185
|
+
```coffee
|
|
186
|
+
title = read 'title', 'string' # Collapses whitespace
|
|
187
|
+
bio = read 'bio', 'text' # Light cleanup
|
|
188
|
+
name = read 'name', 'name' # Trims and normalizes
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### Contact Information
|
|
192
|
+
```coffee
|
|
193
|
+
email = read 'email', 'email' # Valid email format
|
|
194
|
+
phone = read 'phone', 'phone' # US phone → (555) 123-4567
|
|
195
|
+
address = read 'address', 'address' # Trimmed address
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Geographic Data
|
|
199
|
+
```coffee
|
|
200
|
+
state = read 'state', 'state' # Two-letter → uppercase
|
|
201
|
+
zip = read 'zip', 'zip' # 5-digit zip
|
|
202
|
+
zipplus4 = read 'zip', 'zipplus4' # 12345-6789 format
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
### Identity & Security
|
|
206
|
+
```coffee
|
|
207
|
+
ssn = read 'ssn', 'ssn' # SSN → digits only
|
|
208
|
+
sex = read 'gender', 'sex' # m/f/o
|
|
209
|
+
username = read 'username', 'username' # 3-20 chars, lowercase
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Web & Technical
|
|
213
|
+
```coffee
|
|
214
|
+
url = read 'website', 'url' # Valid URL
|
|
215
|
+
ip = read 'ip_address', 'ip' # IPv4 address
|
|
216
|
+
mac = read 'mac', 'mac' # MAC address
|
|
217
|
+
color = read 'color', 'color' # Hex color → #abc123
|
|
218
|
+
uuid = read 'user_id', 'uuid' # UUID format
|
|
219
|
+
semver = read 'version', 'semver' # Semantic version
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
### Time & Date
|
|
223
|
+
```coffee
|
|
224
|
+
time = read 'time', 'time' # HH:MM or HH:MM:SS
|
|
225
|
+
date = read 'date', 'date' # YYYY-MM-DD
|
|
226
|
+
time24 = read 'time', 'time24' # 24-hour format
|
|
227
|
+
time12 = read 'time', 'time12' # 12-hour with am/pm
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
### Boolean & Collections
|
|
231
|
+
```coffee
|
|
232
|
+
active = read 'active', 'truthy' # true/t/1/yes/y/on → true
|
|
233
|
+
inactive = read 'off', 'falsy' # false/f/0/no/n/off → true
|
|
234
|
+
flag = read 'flag', 'bool' # Either → boolean
|
|
235
|
+
tags = read 'tags', 'array' # Must be array
|
|
236
|
+
config = read 'config', 'hash' # Must be object
|
|
237
|
+
settings = read 'data', 'json' # Parse JSON string
|
|
238
|
+
ids = read 'ids', 'ids' # "1,2,3" → [1, 2, 3]
|
|
239
|
+
slug = read 'slug', 'slug' # URL-safe slug
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
### Custom Validators
|
|
243
|
+
|
|
244
|
+
Register your own validators:
|
|
245
|
+
|
|
246
|
+
```coffee
|
|
247
|
+
import { registerValidator, read } from '@rip-lang/api'
|
|
248
|
+
|
|
249
|
+
registerValidator 'postalCode', (v) ->
|
|
250
|
+
if v =~ /^[A-Z]\d[A-Z] \d[A-Z]\d$/i
|
|
251
|
+
_[0].toUpperCase()
|
|
252
|
+
else
|
|
253
|
+
null
|
|
254
|
+
|
|
255
|
+
# Now use it
|
|
256
|
+
code = read 'postal', 'postalCode!'
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
## Routing
|
|
260
|
+
|
|
261
|
+
### HTTP Methods
|
|
262
|
+
|
|
263
|
+
```coffee
|
|
264
|
+
import { get, post, put, patch, del, all } from '@rip-lang/api'
|
|
265
|
+
|
|
266
|
+
get '/users', -> listUsers!
|
|
267
|
+
post '/users', -> createUser!
|
|
268
|
+
get '/users/:id', -> getUser!
|
|
269
|
+
put '/users/:id', -> updateUser!
|
|
270
|
+
patch '/users/:id', -> patchUser!
|
|
271
|
+
del '/users/:id', -> deleteUser!
|
|
272
|
+
all '/health', -> 'ok' # All methods
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Path Parameters
|
|
276
|
+
|
|
277
|
+
```coffee
|
|
278
|
+
# Basic parameters
|
|
279
|
+
get '/users/:id', ->
|
|
280
|
+
id = read 'id', 'id!'
|
|
281
|
+
{ id }
|
|
282
|
+
|
|
283
|
+
# Multiple parameters
|
|
284
|
+
get '/users/:userId/posts/:postId', ->
|
|
285
|
+
userId = read 'userId', 'id!'
|
|
286
|
+
postId = read 'postId', 'id!'
|
|
287
|
+
{ userId, postId }
|
|
288
|
+
|
|
289
|
+
# Custom patterns
|
|
290
|
+
get '/files/:name{[a-z]+\\.txt}', ->
|
|
291
|
+
name = read 'name'
|
|
292
|
+
{ file: name }
|
|
293
|
+
|
|
294
|
+
# Wildcards
|
|
295
|
+
get '/static/*', (env) ->
|
|
296
|
+
{ path: env.req.path }
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
### Route Grouping
|
|
300
|
+
|
|
301
|
+
```coffee
|
|
302
|
+
import { prefix } from '@rip-lang/api'
|
|
303
|
+
|
|
304
|
+
prefix '/api/v1', ->
|
|
305
|
+
get '/users', -> listUsers!
|
|
306
|
+
get '/posts', -> listPosts!
|
|
307
|
+
|
|
308
|
+
prefix '/api/v2', ->
|
|
309
|
+
get '/users', -> listUsersV2!
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
## Middleware
|
|
313
|
+
|
|
314
|
+
### Built-in Middleware
|
|
315
|
+
|
|
316
|
+
Import from `@rip-lang/api/middleware`:
|
|
317
|
+
|
|
318
|
+
```coffee
|
|
319
|
+
import { use } from '@rip-lang/api'
|
|
320
|
+
import { cors, logger, compress, cookies, sessions, secureHeaders, timeout, bodyLimit } from '@rip-lang/api/middleware'
|
|
321
|
+
|
|
322
|
+
# Logging
|
|
323
|
+
use logger()
|
|
324
|
+
use logger format: 'tiny' # Minimal output
|
|
325
|
+
use logger format: 'dev' # Colorized (default)
|
|
326
|
+
use logger skip: (c) -> c.req.path is '/health'
|
|
327
|
+
|
|
328
|
+
# CORS
|
|
329
|
+
use cors() # Allow all origins
|
|
330
|
+
use cors origin: 'https://myapp.com' # Specific origin
|
|
331
|
+
use cors origin: ['https://a.com', 'https://b.com']
|
|
332
|
+
use cors credentials: true, maxAge: 86400
|
|
333
|
+
|
|
334
|
+
# Compression (gzip/deflate)
|
|
335
|
+
use compress()
|
|
336
|
+
use compress threshold: 1024 # Min bytes to compress
|
|
337
|
+
|
|
338
|
+
# Cookies
|
|
339
|
+
use cookies()
|
|
340
|
+
use cookies secret: 'my-secret', secure: true, httpOnly: true
|
|
341
|
+
|
|
342
|
+
# Security headers
|
|
343
|
+
use secureHeaders()
|
|
344
|
+
use secureHeaders hsts: true, contentSecurityPolicy: "default-src 'self'"
|
|
345
|
+
|
|
346
|
+
# Request limits
|
|
347
|
+
use timeout ms: 30000 # 30 second timeout
|
|
348
|
+
use bodyLimit maxSize: 1024 * 1024 # 1MB max body
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
### Middleware Options
|
|
352
|
+
|
|
353
|
+
| Middleware | Options |
|
|
354
|
+
|------------|---------|
|
|
355
|
+
| `logger()` | `format`, `skip`, `stream` |
|
|
356
|
+
| `cors()` | `origin`, `methods`, `headers`, `credentials`, `maxAge` |
|
|
357
|
+
| `compress()` | `threshold`, `encodings` |
|
|
358
|
+
| `cookies()` | `secure`, `httpOnly`, `sameSite`, `maxAge`, `path` |
|
|
359
|
+
| `sessions()` | `name`, `maxAge`, `secure`, `httpOnly`, `sameSite` |
|
|
360
|
+
| `secureHeaders()` | `hsts`, `hstsMaxAge`, `contentSecurityPolicy`, `frameOptions` |
|
|
361
|
+
| `timeout()` | `ms`, `message`, `status` |
|
|
362
|
+
| `bodyLimit()` | `maxSize`, `message` |
|
|
363
|
+
|
|
364
|
+
### Session Usage
|
|
365
|
+
|
|
366
|
+
```coffee
|
|
367
|
+
import { get, use, before, session } from '@rip-lang/api'
|
|
368
|
+
import { cookies, sessions } from '@rip-lang/api/middleware'
|
|
369
|
+
|
|
370
|
+
use cookies()
|
|
371
|
+
use sessions()
|
|
372
|
+
|
|
373
|
+
before ->
|
|
374
|
+
session.views ?= 0
|
|
375
|
+
session.views += 1
|
|
376
|
+
|
|
377
|
+
get '/profile', ->
|
|
378
|
+
{ userId: session.userId, views: session.views }
|
|
379
|
+
|
|
380
|
+
get '/login', ->
|
|
381
|
+
session.userId = 123
|
|
382
|
+
{ loggedIn: true }
|
|
383
|
+
|
|
384
|
+
get '/logout', ->
|
|
385
|
+
delete session.userId
|
|
386
|
+
{ loggedOut: true }
|
|
387
|
+
```
|
|
388
|
+
|
|
389
|
+
The `session` import works anywhere via AsyncLocalStorage — no `@` needed, works in helpers and nested callbacks.
|
|
390
|
+
|
|
391
|
+
### Cookie Usage (low-level)
|
|
392
|
+
|
|
393
|
+
```coffee
|
|
394
|
+
import { get, use } from '@rip-lang/api'
|
|
395
|
+
import { cookies } from '@rip-lang/api/middleware'
|
|
396
|
+
|
|
397
|
+
use cookies()
|
|
398
|
+
|
|
399
|
+
get '/set-cookie', ->
|
|
400
|
+
@cookie 'theme', 'dark', maxAge: 3600
|
|
401
|
+
{ success: true }
|
|
402
|
+
|
|
403
|
+
get '/get-cookie', ->
|
|
404
|
+
theme = @cookie 'theme'
|
|
405
|
+
{ theme }
|
|
406
|
+
|
|
407
|
+
get '/logout', ->
|
|
408
|
+
@clearCookie 'theme'
|
|
409
|
+
{ cleared: true }
|
|
410
|
+
```
|
|
411
|
+
|
|
412
|
+
### Custom Middleware
|
|
413
|
+
|
|
414
|
+
```coffee
|
|
415
|
+
# Authentication middleware
|
|
416
|
+
use (c, next) ->
|
|
417
|
+
token = @req.header 'Authorization'
|
|
418
|
+
unless token
|
|
419
|
+
return @json { error: 'Unauthorized' }, 401
|
|
420
|
+
@user = validateToken!(token)
|
|
421
|
+
await next()
|
|
422
|
+
|
|
423
|
+
# Timing middleware
|
|
424
|
+
use (c, next) ->
|
|
425
|
+
start = Date.now()
|
|
426
|
+
await next()
|
|
427
|
+
@header 'X-Response-Time', "#{Date.now() - start}ms"
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
### Middleware Chain
|
|
431
|
+
|
|
432
|
+
Middleware runs in registration order with Koa-style `next()`:
|
|
433
|
+
|
|
434
|
+
```coffee
|
|
435
|
+
use (c, next) ->
|
|
436
|
+
console.log 'Before handler'
|
|
437
|
+
await next()
|
|
438
|
+
console.log 'After handler'
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
### Before/After Filters (Sinatra-style)
|
|
442
|
+
|
|
443
|
+
For simpler cases, use `before` and `after` filters. Use `@` to access context:
|
|
444
|
+
|
|
445
|
+
```coffee
|
|
446
|
+
import { before, after, get } from '@rip-lang/api'
|
|
447
|
+
|
|
448
|
+
skipPaths = ['/favicon.ico', '/ping', '/health']
|
|
449
|
+
|
|
450
|
+
# Runs before every request
|
|
451
|
+
before ->
|
|
452
|
+
@start = Date.now()
|
|
453
|
+
@silent = @req.path in skipPaths
|
|
454
|
+
# Return a Response to short-circuit (e.g., for auth)
|
|
455
|
+
unless @req.header 'Authorization'
|
|
456
|
+
return @json { error: 'Unauthorized' }, 401
|
|
457
|
+
|
|
458
|
+
# Runs after every request
|
|
459
|
+
after ->
|
|
460
|
+
return if @silent
|
|
461
|
+
console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
|
|
462
|
+
```
|
|
463
|
+
|
|
464
|
+
**How `@` works:** Handlers are called with `this` bound to the context, so `@foo` is `this.foo`. This gives you Sinatra-like magic access to:
|
|
465
|
+
- `@req` — Request object
|
|
466
|
+
- `@json()`, `@text()`, `@html()`, `@redirect()` — Response helpers
|
|
467
|
+
- `@header()` — Response header modifier
|
|
468
|
+
- `@anything` — Custom per-request state
|
|
469
|
+
|
|
470
|
+
**Imports that work anywhere** (via AsyncLocalStorage or Proxy):
|
|
471
|
+
- `read` — Validated request parameters
|
|
472
|
+
- `session` — Session data (if middleware enabled)
|
|
473
|
+
- `env` — `process.env` shortcut (e.g., `env.DATABASE_URL`)
|
|
474
|
+
|
|
475
|
+
```coffee
|
|
476
|
+
import { get, read, session } from '@rip-lang/api'
|
|
477
|
+
|
|
478
|
+
get '/profile', ->
|
|
479
|
+
id = read 'id', 'id!' # Works anywhere
|
|
480
|
+
{ id, user: session.userId } # No @ needed
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
**Note:** In nested callbacks, use fat arrow `=>` to preserve `@`:
|
|
484
|
+
|
|
485
|
+
```coffee
|
|
486
|
+
get '/delayed', ->
|
|
487
|
+
@user = 'alice'
|
|
488
|
+
setTimeout => # Fat arrow preserves @
|
|
489
|
+
console.log @user # Works!
|
|
490
|
+
, 100
|
|
491
|
+
```
|
|
492
|
+
|
|
493
|
+
## Context Object
|
|
494
|
+
|
|
495
|
+
When you need full control, handlers receive a context object:
|
|
496
|
+
|
|
497
|
+
### Response Helpers
|
|
498
|
+
|
|
499
|
+
```coffee
|
|
500
|
+
get '/demo', (c) ->
|
|
501
|
+
# JSON response
|
|
502
|
+
c.json { data: 'value' }
|
|
503
|
+
c.json { data: 'value' }, 201 # With status
|
|
504
|
+
c.json { data: 'value' }, 200, { 'X-Custom': 'header' }
|
|
505
|
+
|
|
506
|
+
# Text response
|
|
507
|
+
c.text 'Hello'
|
|
508
|
+
c.text 'Created', 201
|
|
509
|
+
|
|
510
|
+
# HTML response
|
|
511
|
+
c.html '<h1>Hello</h1>'
|
|
512
|
+
|
|
513
|
+
# Redirect
|
|
514
|
+
c.redirect '/new-location'
|
|
515
|
+
c.redirect '/new-location', 301 # Permanent
|
|
516
|
+
|
|
517
|
+
# Raw body
|
|
518
|
+
c.body data, 200, { 'Content-Type': 'application/octet-stream' }
|
|
519
|
+
```
|
|
520
|
+
|
|
521
|
+
### Request Helpers
|
|
522
|
+
|
|
523
|
+
```coffee
|
|
524
|
+
get '/info', (c) ->
|
|
525
|
+
# Path parameters
|
|
526
|
+
id = c.req.param 'id'
|
|
527
|
+
allParams = c.req.param()
|
|
528
|
+
|
|
529
|
+
# Query parameters
|
|
530
|
+
q = c.req.query 'q'
|
|
531
|
+
allQuery = c.req.query()
|
|
532
|
+
|
|
533
|
+
# Headers
|
|
534
|
+
auth = c.req.header 'Authorization'
|
|
535
|
+
allHeaders = c.req.header()
|
|
536
|
+
|
|
537
|
+
# Body (async)
|
|
538
|
+
json = c.req.json!
|
|
539
|
+
text = c.req.text!
|
|
540
|
+
form = c.req.formData!
|
|
541
|
+
parsed = c.req.parseBody!
|
|
542
|
+
|
|
543
|
+
# Raw request
|
|
544
|
+
c.req.raw # Native Request object
|
|
545
|
+
c.req.method # 'GET', 'POST', etc.
|
|
546
|
+
c.req.url # Full URL
|
|
547
|
+
c.req.path # Path only
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
### Request-Scoped State
|
|
551
|
+
|
|
552
|
+
Use `@` to store per-request state (each request gets a fresh context):
|
|
553
|
+
|
|
554
|
+
```coffee
|
|
555
|
+
# Store data for later middleware/handlers
|
|
556
|
+
use (c, next) ->
|
|
557
|
+
@user = { id: 1, name: 'Alice' }
|
|
558
|
+
@startTime = Date.now()
|
|
559
|
+
await next()
|
|
560
|
+
|
|
561
|
+
get '/profile', ->
|
|
562
|
+
@json @user
|
|
563
|
+
```
|
|
564
|
+
|
|
565
|
+
## Error Handling
|
|
566
|
+
|
|
567
|
+
### Custom Error Handler
|
|
568
|
+
|
|
569
|
+
```coffee
|
|
570
|
+
import { onError } from '@rip-lang/api'
|
|
571
|
+
|
|
572
|
+
onError (err, c) ->
|
|
573
|
+
console.error 'Error:', err
|
|
574
|
+
c.json { error: err.message }, err.status or 500
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
### Custom 404 Handler
|
|
578
|
+
|
|
579
|
+
```coffee
|
|
580
|
+
import { notFound } from '@rip-lang/api'
|
|
581
|
+
|
|
582
|
+
notFound (c) ->
|
|
583
|
+
c.json { error: 'Not found', path: c.req.path }, 404
|
|
584
|
+
```
|
|
585
|
+
|
|
586
|
+
## Server Options
|
|
587
|
+
|
|
588
|
+
### Basic Server
|
|
589
|
+
|
|
590
|
+
```coffee
|
|
591
|
+
import { start } from '@rip-lang/api'
|
|
592
|
+
|
|
593
|
+
start port: 3000
|
|
594
|
+
start port: 3000, host: '0.0.0.0'
|
|
595
|
+
```
|
|
596
|
+
|
|
597
|
+
### Handler Only (for @rip-lang/server)
|
|
598
|
+
|
|
599
|
+
```coffee
|
|
600
|
+
import { startHandler } from '@rip-lang/api'
|
|
601
|
+
|
|
602
|
+
export default startHandler()
|
|
603
|
+
```
|
|
604
|
+
|
|
605
|
+
### App Pattern
|
|
606
|
+
|
|
607
|
+
```coffee
|
|
608
|
+
import { App, get, post } from '@rip-lang/api'
|
|
609
|
+
|
|
610
|
+
export default App ->
|
|
611
|
+
get '/', -> 'Hello'
|
|
612
|
+
post '/echo', -> read()
|
|
613
|
+
```
|
|
614
|
+
|
|
615
|
+
## Utility Functions
|
|
616
|
+
|
|
617
|
+
### isBlank
|
|
618
|
+
|
|
619
|
+
```coffee
|
|
620
|
+
import { isBlank } from '@rip-lang/api'
|
|
621
|
+
|
|
622
|
+
isBlank null # true
|
|
623
|
+
isBlank undefined # true
|
|
624
|
+
isBlank '' # true
|
|
625
|
+
isBlank ' ' # true
|
|
626
|
+
isBlank [] # true
|
|
627
|
+
isBlank {} # true
|
|
628
|
+
isBlank false # true
|
|
629
|
+
isBlank 'hello' # false
|
|
630
|
+
isBlank [1, 2] # false
|
|
631
|
+
```
|
|
632
|
+
|
|
633
|
+
### toName
|
|
634
|
+
|
|
635
|
+
Advanced name formatting with intelligent capitalization:
|
|
636
|
+
|
|
637
|
+
```coffee
|
|
638
|
+
import { toName } from '@rip-lang/api'
|
|
639
|
+
|
|
640
|
+
toName 'john doe' # 'John Doe'
|
|
641
|
+
toName 'JANE SMITH' # 'Jane Smith'
|
|
642
|
+
toName "o'brien" # "O'Brien"
|
|
643
|
+
toName 'mcdonald' # 'McDonald'
|
|
644
|
+
toName 'los angeles', 'address' # 'Los Angeles'
|
|
645
|
+
```
|
|
646
|
+
|
|
647
|
+
### toPhone
|
|
648
|
+
|
|
649
|
+
US phone number formatting:
|
|
650
|
+
|
|
651
|
+
```coffee
|
|
652
|
+
import { toPhone } from '@rip-lang/api'
|
|
653
|
+
|
|
654
|
+
toPhone '5551234567' # '(555) 123-4567'
|
|
655
|
+
toPhone '555-123-4567' # '(555) 123-4567'
|
|
656
|
+
toPhone '555.123.4567 x99' # '(555) 123-4567, ext. 99'
|
|
657
|
+
toPhone '+1 555 123 4567' # '(555) 123-4567'
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
## Migration from Hono
|
|
661
|
+
|
|
662
|
+
`@rip-lang/api` provides a Hono-compatible API surface:
|
|
663
|
+
|
|
664
|
+
### Before (Hono)
|
|
665
|
+
|
|
666
|
+
```coffee
|
|
667
|
+
import { Hono } from 'hono'
|
|
668
|
+
|
|
669
|
+
app = new Hono()
|
|
670
|
+
app.get '/users/:id', (c) ->
|
|
671
|
+
id = c.req.param 'id'
|
|
672
|
+
c.json { id }
|
|
673
|
+
|
|
674
|
+
export default app
|
|
675
|
+
```
|
|
676
|
+
|
|
677
|
+
### After (@rip-lang/api)
|
|
678
|
+
|
|
679
|
+
```coffee
|
|
680
|
+
import { get, read, startHandler } from '@rip-lang/api'
|
|
681
|
+
|
|
682
|
+
get '/users/:id', ->
|
|
683
|
+
id = read 'id', 'id!'
|
|
684
|
+
{ id }
|
|
685
|
+
|
|
686
|
+
export default startHandler()
|
|
687
|
+
```
|
|
688
|
+
|
|
689
|
+
### API Compatibility
|
|
690
|
+
|
|
691
|
+
| Hono | @rip-lang/api |
|
|
692
|
+
|------|---------------|
|
|
693
|
+
| `app.get(path, handler)` | `get path, handler` |
|
|
694
|
+
| `app.post(path, handler)` | `post path, handler` |
|
|
695
|
+
| `app.use(middleware)` | `use middleware` |
|
|
696
|
+
| `app.basePath(path)` | `prefix path, -> ...` |
|
|
697
|
+
| `c.json(data)` | `@json(data)` or return `{ data }` |
|
|
698
|
+
| `c.req.param('id')` | `@req.param('id')` or `read 'id'` |
|
|
699
|
+
| `c.req.query('q')` | `@req.query('q')` or `read 'q'` |
|
|
700
|
+
|
|
701
|
+
## Real-World Example
|
|
702
|
+
|
|
703
|
+
```coffee
|
|
704
|
+
import { get, post, put, del, use, read, start, before, after, onError } from '@rip-lang/api'
|
|
705
|
+
import { logger } from '@rip-lang/api/middleware'
|
|
706
|
+
|
|
707
|
+
# Middleware
|
|
708
|
+
use logger()
|
|
709
|
+
|
|
710
|
+
# Filters
|
|
711
|
+
before ->
|
|
712
|
+
@start = Date.now()
|
|
713
|
+
|
|
714
|
+
after ->
|
|
715
|
+
console.log "#{@req.method} #{@req.path} - #{Date.now() - @start}ms"
|
|
716
|
+
|
|
717
|
+
# Error handling
|
|
718
|
+
onError (err) ->
|
|
719
|
+
@json { error: err.message }, err.status or 500
|
|
720
|
+
|
|
721
|
+
# Routes
|
|
722
|
+
get '/', ->
|
|
723
|
+
{ name: 'My API', version: '1.0' }
|
|
724
|
+
|
|
725
|
+
get '/users', ->
|
|
726
|
+
page = read 'page', 'int', [1, 100]
|
|
727
|
+
limit = read 'limit', 'int', [1, 50]
|
|
728
|
+
users = db.listUsers! page or 1, limit or 10
|
|
729
|
+
{ users, page, limit }
|
|
730
|
+
|
|
731
|
+
get '/users/:id', ->
|
|
732
|
+
id = read 'id', 'id!'
|
|
733
|
+
user = db.getUser!(id)
|
|
734
|
+
unless user
|
|
735
|
+
throw { message: 'User not found', status: 404 }
|
|
736
|
+
{ user }
|
|
737
|
+
|
|
738
|
+
post '/users', ->
|
|
739
|
+
email = read 'email', 'email!'
|
|
740
|
+
name = read 'name', 'string', [1, 100]
|
|
741
|
+
phone = read 'phone', 'phone'
|
|
742
|
+
user = db.createUser! { email, name, phone }
|
|
743
|
+
{ user, created: true }
|
|
744
|
+
|
|
745
|
+
put '/users/:id', ->
|
|
746
|
+
id = read 'id', 'id!'
|
|
747
|
+
email = read 'email', 'email'
|
|
748
|
+
name = read 'name', 'string', [1, 100]
|
|
749
|
+
user = db.updateUser! id, { email, name }
|
|
750
|
+
{ user, updated: true }
|
|
751
|
+
|
|
752
|
+
del '/users/:id', ->
|
|
753
|
+
id = read 'id', 'id!'
|
|
754
|
+
db.deleteUser!(id)
|
|
755
|
+
{ deleted: true }
|
|
756
|
+
|
|
757
|
+
start port: 3000
|
|
758
|
+
```
|
|
759
|
+
|
|
760
|
+
## Performance
|
|
761
|
+
|
|
762
|
+
- **Minimal footprint** — Core is ~565 lines, optional middleware ~395 lines
|
|
763
|
+
- **Zero dependencies** — No external packages to load
|
|
764
|
+
- **Compiled patterns** — Route regexes compiled once at startup
|
|
765
|
+
- **Smart response wrapping** — Minimal overhead for return-value handlers
|
|
766
|
+
- **AsyncLocalStorage** — Industry-standard, zero-copy context propagation
|
|
767
|
+
|
|
768
|
+
## Contributing
|
|
769
|
+
|
|
770
|
+
Contributions that enhance developer productivity and code clarity are welcome. See the [main rip-lang repository](https://github.com/shreeve/rip-lang) for contribution guidelines.
|
|
771
|
+
|
|
772
|
+
---
|
|
773
|
+
|
|
774
|
+
**Transform your API development from verbose boilerplate to clear, elegant code.**
|
|
775
|
+
|
|
776
|
+
*"90% less code, 100% more clarity"*
|