@rip-lang/api 0.7.7 → 0.8.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 +60 -61
- package/api.rip +15 -14
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -39,12 +39,12 @@ before ->
|
|
|
39
39
|
session.views ?= 0
|
|
40
40
|
session.views += 1
|
|
41
41
|
|
|
42
|
-
get '/'
|
|
43
|
-
get '/json'
|
|
44
|
-
get '/users/:id'
|
|
45
|
-
get '/session'
|
|
46
|
-
get '/login'
|
|
47
|
-
get '/logout'
|
|
42
|
+
get '/' -> 'Hello, World!'
|
|
43
|
+
get '/json' -> { message: 'It works!', timestamp: Date.now() }
|
|
44
|
+
get '/users/:id' -> { user: { id: read('id', 'id!') } }
|
|
45
|
+
get '/session' -> { views: session.views, loggedIn: session.userId? }
|
|
46
|
+
get '/login' -> session.userId = 123; { loggedIn: true, userId: 123 }
|
|
47
|
+
get '/logout' -> delete session.userId; { loggedOut: true }
|
|
48
48
|
|
|
49
49
|
start port: 3000
|
|
50
50
|
```
|
|
@@ -99,17 +99,19 @@ bun add @rip-lang/api
|
|
|
99
99
|
import { get, post, use, read, start } from '@rip-lang/api'
|
|
100
100
|
|
|
101
101
|
# Context-free handlers — just return data!
|
|
102
|
-
|
|
102
|
+
# Note: In Rip, the comma after a string/regex is optional when immediately
|
|
103
|
+
# followed by an arrow function. So `get '/', ->` can be `get '/' ->`
|
|
104
|
+
get '/' -> 'Hello, World!'
|
|
103
105
|
|
|
104
|
-
get '/json'
|
|
106
|
+
get '/json' -> { message: 'It works!', timestamp: Date.now() }
|
|
105
107
|
|
|
106
108
|
# Path parameters
|
|
107
|
-
get '/users/:id'
|
|
109
|
+
get '/users/:id' ->
|
|
108
110
|
id = read 'id', 'id!'
|
|
109
111
|
{ user: { id, name: "User #{id}" } }
|
|
110
112
|
|
|
111
113
|
# Form validation
|
|
112
|
-
post '/signup'
|
|
114
|
+
post '/signup' ->
|
|
113
115
|
email = read 'email', 'email!'
|
|
114
116
|
phone = read 'phone', 'phone'
|
|
115
117
|
age = read 'age', 'int', [18, 120]
|
|
@@ -122,6 +124,7 @@ start port: 3000
|
|
|
122
124
|
|
|
123
125
|
```coffee
|
|
124
126
|
# Access full context for headers, redirects, etc.
|
|
127
|
+
# Note: Comma IS needed when there's a parameter between the path and arrow
|
|
125
128
|
get '/download/:id', (env) ->
|
|
126
129
|
env.header 'Content-Disposition', 'attachment'
|
|
127
130
|
env.body getFile!(read 'id', 'id!')
|
|
@@ -280,31 +283,31 @@ code = read 'postal', 'postalCode!'
|
|
|
280
283
|
```coffee
|
|
281
284
|
import { get, post, put, patch, del, all } from '@rip-lang/api'
|
|
282
285
|
|
|
283
|
-
get '/users'
|
|
284
|
-
post '/users'
|
|
285
|
-
get '/users/:id'
|
|
286
|
-
put '/users/:id'
|
|
287
|
-
patch '/users/:id'
|
|
288
|
-
del '/users/:id'
|
|
289
|
-
all '/health'
|
|
286
|
+
get '/users' -> listUsers!
|
|
287
|
+
post '/users' -> createUser!
|
|
288
|
+
get '/users/:id' -> getUser!
|
|
289
|
+
put '/users/:id' -> updateUser!
|
|
290
|
+
patch '/users/:id' -> patchUser!
|
|
291
|
+
del '/users/:id' -> deleteUser!
|
|
292
|
+
all '/health' -> 'ok' # All methods
|
|
290
293
|
```
|
|
291
294
|
|
|
292
295
|
### Path Parameters
|
|
293
296
|
|
|
294
297
|
```coffee
|
|
295
298
|
# Basic parameters
|
|
296
|
-
get '/users/:id'
|
|
299
|
+
get '/users/:id' ->
|
|
297
300
|
id = read 'id', 'id!'
|
|
298
301
|
{ id }
|
|
299
302
|
|
|
300
303
|
# Multiple parameters
|
|
301
|
-
get '/users/:userId/posts/:postId'
|
|
304
|
+
get '/users/:userId/posts/:postId' ->
|
|
302
305
|
userId = read 'userId', 'id!'
|
|
303
306
|
postId = read 'postId', 'id!'
|
|
304
307
|
{ userId, postId }
|
|
305
308
|
|
|
306
309
|
# Custom patterns
|
|
307
|
-
get '/files/:name{[a-z]+\\.txt}'
|
|
310
|
+
get '/files/:name{[a-z]+\\.txt}' ->
|
|
308
311
|
name = read 'name'
|
|
309
312
|
{ file: name }
|
|
310
313
|
|
|
@@ -318,12 +321,12 @@ get '/static/*', (env) ->
|
|
|
318
321
|
```coffee
|
|
319
322
|
import { prefix } from '@rip-lang/api'
|
|
320
323
|
|
|
321
|
-
prefix '/api/v1'
|
|
322
|
-
get '/users'
|
|
323
|
-
get '/posts'
|
|
324
|
+
prefix '/api/v1' ->
|
|
325
|
+
get '/users' -> listUsers!
|
|
326
|
+
get '/posts' -> listPosts!
|
|
324
327
|
|
|
325
|
-
prefix '/api/v2'
|
|
326
|
-
get '/users'
|
|
328
|
+
prefix '/api/v2' ->
|
|
329
|
+
get '/users' -> listUsersV2!
|
|
327
330
|
```
|
|
328
331
|
|
|
329
332
|
## Middleware
|
|
@@ -386,14 +389,14 @@ before ->
|
|
|
386
389
|
session.views ?= 0
|
|
387
390
|
session.views += 1
|
|
388
391
|
|
|
389
|
-
get '/profile'
|
|
392
|
+
get '/profile' ->
|
|
390
393
|
{ userId: session.userId, views: session.views }
|
|
391
394
|
|
|
392
|
-
get '/login'
|
|
395
|
+
get '/login' ->
|
|
393
396
|
session.userId = 123
|
|
394
397
|
{ loggedIn: true }
|
|
395
398
|
|
|
396
|
-
get '/logout'
|
|
399
|
+
get '/logout' ->
|
|
397
400
|
delete session.userId
|
|
398
401
|
{ loggedOut: true }
|
|
399
402
|
```
|
|
@@ -488,7 +491,7 @@ after ->
|
|
|
488
491
|
```coffee
|
|
489
492
|
import { get, read, session } from '@rip-lang/api'
|
|
490
493
|
|
|
491
|
-
get '/profile'
|
|
494
|
+
get '/profile' ->
|
|
492
495
|
id = read 'id', 'id!' # Works anywhere
|
|
493
496
|
{ id, user: session.userId } # No @ needed
|
|
494
497
|
```
|
|
@@ -496,7 +499,7 @@ get '/profile', ->
|
|
|
496
499
|
**Note:** In nested callbacks, use fat arrow `=>` to preserve `@`:
|
|
497
500
|
|
|
498
501
|
```coffee
|
|
499
|
-
get '/delayed'
|
|
502
|
+
get '/delayed' ->
|
|
500
503
|
@user = 'alice'
|
|
501
504
|
setTimeout => # Fat arrow preserves @
|
|
502
505
|
console.log @user # Works!
|
|
@@ -505,59 +508,55 @@ get '/delayed', ->
|
|
|
505
508
|
|
|
506
509
|
## Context Object
|
|
507
510
|
|
|
508
|
-
|
|
511
|
+
Use `@` to access the context directly — no parameter needed:
|
|
509
512
|
|
|
510
513
|
### Response Helpers
|
|
511
514
|
|
|
512
515
|
```coffee
|
|
513
|
-
get '/demo'
|
|
516
|
+
get '/demo' ->
|
|
514
517
|
# JSON response
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
+
@json { data: 'value' }
|
|
519
|
+
@json { data: 'value' }, 201 # With status
|
|
520
|
+
@json { data: 'value' }, 200, { 'X-Custom': 'header' }
|
|
518
521
|
|
|
519
522
|
# Text response
|
|
520
|
-
|
|
521
|
-
|
|
523
|
+
@text 'Hello'
|
|
524
|
+
@text 'Created', 201
|
|
522
525
|
|
|
523
526
|
# HTML response
|
|
524
|
-
|
|
527
|
+
@html '<h1>Hello</h1>'
|
|
525
528
|
|
|
526
529
|
# Redirect
|
|
527
|
-
|
|
528
|
-
|
|
530
|
+
@redirect '/new-location'
|
|
531
|
+
@redirect '/new-location', 301 # Permanent
|
|
529
532
|
|
|
530
533
|
# Raw body
|
|
531
|
-
|
|
534
|
+
@body data, 200, { 'Content-Type': 'application/octet-stream' }
|
|
532
535
|
```
|
|
533
536
|
|
|
534
537
|
### Request Helpers
|
|
535
538
|
|
|
536
539
|
```coffee
|
|
537
|
-
get '/info'
|
|
538
|
-
# Path parameters
|
|
539
|
-
id =
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
# Query parameters
|
|
543
|
-
q = c.req.query 'q'
|
|
544
|
-
allQuery = c.req.query()
|
|
540
|
+
get '/info' ->
|
|
541
|
+
# Path and query parameters — use read() for validation!
|
|
542
|
+
id = read 'id', 'id!'
|
|
543
|
+
q = read 'q' # Raw value; use 'string' validator to collapse whitespace
|
|
545
544
|
|
|
546
545
|
# Headers
|
|
547
|
-
auth =
|
|
548
|
-
allHeaders =
|
|
546
|
+
auth = @req.header 'Authorization'
|
|
547
|
+
allHeaders = @req.header()
|
|
549
548
|
|
|
550
549
|
# Body (async)
|
|
551
|
-
json =
|
|
552
|
-
text =
|
|
553
|
-
form =
|
|
554
|
-
parsed =
|
|
550
|
+
json = @req.json!
|
|
551
|
+
text = @req.text!
|
|
552
|
+
form = @req.formData!
|
|
553
|
+
parsed = @req.parseBody!
|
|
555
554
|
|
|
556
555
|
# Raw request
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
556
|
+
@req.raw # Native Request object
|
|
557
|
+
@req.method # 'GET', 'POST', etc.
|
|
558
|
+
@req.url # Full URL
|
|
559
|
+
@req.path # Path only
|
|
561
560
|
```
|
|
562
561
|
|
|
563
562
|
### Request-Scoped State
|
|
@@ -571,7 +570,7 @@ use (c, next) ->
|
|
|
571
570
|
@startTime = Date.now()
|
|
572
571
|
await next()
|
|
573
572
|
|
|
574
|
-
get '/profile'
|
|
573
|
+
get '/profile' ->
|
|
575
574
|
@json @user
|
|
576
575
|
```
|
|
577
576
|
|
|
@@ -640,7 +639,7 @@ logRequest = ->
|
|
|
640
639
|
console.log "#{c.req.method} #{c.req.path}" if c
|
|
641
640
|
|
|
642
641
|
# Works in callbacks, helpers, anywhere during request
|
|
643
|
-
get '/demo'
|
|
642
|
+
get '/demo' ->
|
|
644
643
|
logRequest()
|
|
645
644
|
{ ok: true }
|
|
646
645
|
```
|
package/api.rip
CHANGED
|
@@ -294,6 +294,8 @@ export fetch = (req) ->
|
|
|
294
294
|
data = c.req.json!
|
|
295
295
|
else if ct =~ /x-www-form-urlencoded|form-data/i
|
|
296
296
|
data = c.req.parseBody!
|
|
297
|
+
keys = Object.keys(data)
|
|
298
|
+
data = { body: keys[0] } if keys.length is 1 and data[keys[0]] is ''
|
|
297
299
|
else if ct =~ /text\//i
|
|
298
300
|
data = { body: c.req.text! }
|
|
299
301
|
catch
|
|
@@ -315,7 +317,7 @@ runHandler = (c, handler) ->
|
|
|
315
317
|
if _errorHandler?
|
|
316
318
|
_errorHandler.call!(c, err, c)
|
|
317
319
|
else
|
|
318
|
-
new Response err?.message or 'Internal Server Error', { status: 500 }
|
|
320
|
+
new Response err?.message or 'Internal Server Error', { status: err?.status or 500 }
|
|
319
321
|
|
|
320
322
|
# ==============================================================================
|
|
321
323
|
# Server Startup
|
|
@@ -443,8 +445,8 @@ export validators =
|
|
|
443
445
|
text: (v) -> v.replace(/ +/g, ' ')
|
|
444
446
|
|
|
445
447
|
# Name/address
|
|
446
|
-
name: (v) -> v.replace(/\s+/g, ' ')
|
|
447
|
-
address: (v) -> v.replace(/\s+/g, ' ')
|
|
448
|
+
name: (v) -> v.replace(/\s+/g, ' ')
|
|
449
|
+
address: (v) -> v.replace(/\s+/g, ' ')
|
|
448
450
|
|
|
449
451
|
# Time & date
|
|
450
452
|
time: (v) -> v[/^([01]?\d|2[0-3]):[0-5]\d(:[0-5]\d)?$/] and _[0]
|
|
@@ -454,7 +456,7 @@ export validators =
|
|
|
454
456
|
# Booleans
|
|
455
457
|
truthy: (v) -> (v =~ /^(true|t|1|yes|y|on)$/i) and true
|
|
456
458
|
falsy: (v) -> (v =~ /^(false|f|0|no|n|off)$/i) and true
|
|
457
|
-
bool: (v) ->
|
|
459
|
+
bool: (v) -> if v =~ /^(true|t|1|yes|y|on)$/i then true else if v =~ /^(false|f|0|no|n|off)$/i then false else null
|
|
458
460
|
|
|
459
461
|
# Contact, geo, identity
|
|
460
462
|
email: (v) -> v[/^([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})$/] and _[0]
|
|
@@ -516,17 +518,15 @@ export read = (keyOrTag = null, tagOrMiss = null, missOrNil = null) ->
|
|
|
516
518
|
tag = null
|
|
517
519
|
miss = null
|
|
518
520
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
miss = missOrNil
|
|
523
|
-
else
|
|
524
|
-
key = null
|
|
525
|
-
tag = keyOrTag
|
|
526
|
-
miss = tagOrMiss
|
|
521
|
+
[key, tag, miss] = typeof keyOrTag is 'string' \
|
|
522
|
+
? [keyOrTag, tagOrMiss, missOrNil] \
|
|
523
|
+
: [null , keyOrTag , tagOrMiss]
|
|
527
524
|
|
|
528
525
|
raw = if key? then data[key] else data
|
|
529
|
-
|
|
526
|
+
unless tag?
|
|
527
|
+
return raw if raw?
|
|
528
|
+
return miss() if typeof miss is 'function'
|
|
529
|
+
return if miss? and miss isnt '' then miss else null
|
|
530
530
|
|
|
531
531
|
required = false
|
|
532
532
|
if typeof tag is 'string' and tag.endsWith('!')
|
|
@@ -534,6 +534,7 @@ export read = (keyOrTag = null, tagOrMiss = null, missOrNil = null) ->
|
|
|
534
534
|
tag = tag.slice(0, -1)
|
|
535
535
|
|
|
536
536
|
v = raw
|
|
537
|
+
v = v.trim() if typeof v is 'string'
|
|
537
538
|
|
|
538
539
|
# Named validator
|
|
539
540
|
if typeof tag is 'string'
|
|
@@ -562,7 +563,7 @@ export read = (keyOrTag = null, tagOrMiss = null, missOrNil = null) ->
|
|
|
562
563
|
|
|
563
564
|
# Regex
|
|
564
565
|
else if tag instanceof RegExp
|
|
565
|
-
v = String(v or '')[
|
|
566
|
+
v = String(v or '').match(tag)?[0] or null
|
|
566
567
|
|
|
567
568
|
# Object constraints
|
|
568
569
|
else if typeof tag is 'object'
|