@rip-lang/api 0.7.6 → 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.
Files changed (3) hide show
  1. package/README.md +60 -61
  2. package/api.rip +16 -15
  3. 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 '/', -> '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 }
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
- get '/', -> 'Hello, World!'
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', -> { message: 'It works!', timestamp: Date.now() }
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', -> listUsers!
284
- post '/users', -> createUser!
285
- get '/users/:id', -> getUser!
286
- put '/users/:id', -> updateUser!
287
- patch '/users/:id', -> patchUser!
288
- del '/users/:id', -> deleteUser!
289
- all '/health', -> 'ok' # All methods
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', -> listUsers!
323
- get '/posts', -> listPosts!
324
+ prefix '/api/v1' ->
325
+ get '/users' -> listUsers!
326
+ get '/posts' -> listPosts!
324
327
 
325
- prefix '/api/v2', ->
326
- get '/users', -> listUsersV2!
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
- When you need full control, handlers receive a context object:
511
+ Use `@` to access the context directly no parameter needed:
509
512
 
510
513
  ### Response Helpers
511
514
 
512
515
  ```coffee
513
- get '/demo', (c) ->
516
+ get '/demo' ->
514
517
  # JSON response
515
- c.json { data: 'value' }
516
- c.json { data: 'value' }, 201 # With status
517
- c.json { data: 'value' }, 200, { 'X-Custom': 'header' }
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
- c.text 'Hello'
521
- c.text 'Created', 201
523
+ @text 'Hello'
524
+ @text 'Created', 201
522
525
 
523
526
  # HTML response
524
- c.html '<h1>Hello</h1>'
527
+ @html '<h1>Hello</h1>'
525
528
 
526
529
  # Redirect
527
- c.redirect '/new-location'
528
- c.redirect '/new-location', 301 # Permanent
530
+ @redirect '/new-location'
531
+ @redirect '/new-location', 301 # Permanent
529
532
 
530
533
  # Raw body
531
- c.body data, 200, { 'Content-Type': 'application/octet-stream' }
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', (c) ->
538
- # Path parameters
539
- id = c.req.param 'id'
540
- allParams = c.req.param()
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 = c.req.header 'Authorization'
548
- allHeaders = c.req.header()
546
+ auth = @req.header 'Authorization'
547
+ allHeaders = @req.header()
549
548
 
550
549
  # Body (async)
551
- json = c.req.json!
552
- text = c.req.text!
553
- form = c.req.formData!
554
- parsed = c.req.parseBody!
550
+ json = @req.json!
551
+ text = @req.text!
552
+ form = @req.formData!
553
+ parsed = @req.parseBody!
555
554
 
556
555
  # Raw request
557
- c.req.raw # Native Request object
558
- c.req.method # 'GET', 'POST', etc.
559
- c.req.url # Full URL
560
- c.req.path # Path only
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, ' ').trim()
447
- address: (v) -> v.replace(/\s+/g, ' ').trim()
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) -> (v =~ /^(true|t|1|yes|y|on|false|f|0|no|n|off)$/i) and (v =~ /^(true|t|1|yes|y|on)$/i)
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
- if keyOrTag? and (tagOrMiss? or missOrNil isnt null)
520
- key = keyOrTag
521
- tag = tagOrMiss
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
- return raw unless tag?
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 '')[tag] or null
566
+ v = String(v or '').match(tag)?[0] or null
566
567
 
567
568
  # Object constraints
568
569
  else if typeof tag is 'object'
@@ -592,6 +593,6 @@ export read = (keyOrTag = null, tagOrMiss = null, missOrNil = null) ->
592
593
  else
593
594
  return miss?() if typeof miss is 'function'
594
595
  return null if miss is '' or not miss? # Empty string or null/undefined → null
595
- miss # Return 0, false, etc. as-is
596
+ return miss # Return 0, false, etc. as-is
596
597
 
597
598
  v
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/api",
3
- "version": "0.7.6",
3
+ "version": "0.8.0",
4
4
  "description": "Pure Rip API framework — elegant, fast, zero dependencies",
5
5
  "type": "module",
6
6
  "main": "api.rip",