@rip-lang/api 0.7.7 → 0.8.1

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 +73 -82
  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]
@@ -506,92 +508,81 @@ export getValidator = (name) -> validators[name]
506
508
  # read() — Sinatra-style Parameter Reading
507
509
  # ==============================================================================
508
510
 
509
- export read = (keyOrTag = null, tagOrMiss = null, missOrNil = null) ->
510
- store = requestContext.getStore()
511
- unless store?
512
- throw new Error 'read() called outside request context'
513
-
514
- data = store.data or {}
515
- key = null
516
- tag = null
517
- miss = null
518
-
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
511
+ export read = (name = null, type = null, miss = null) ->
512
+ store = requestContext.getStore() or throw new Error 'no context for read()'
513
+
514
+ # missing value helper
515
+ done = (must = false) ->
516
+ return miss() if typeof miss is 'function'
517
+ throw new Error "Missing required field: #{name}" if must
518
+ return miss ?? null
519
+
520
+ # get value from store
521
+ v = store.data or {}
522
+ v = v[name] if name?
523
+ v = v.trim() if typeof v is 'string'
527
524
 
528
- raw = if key? then data[key] else data
529
- return raw unless tag?
525
+ # value only, no validator
526
+ if !type?
527
+ return v if v?
528
+ return done()
530
529
 
531
- required = false
532
- if typeof tag is 'string' and tag.endsWith('!')
533
- required = true
534
- tag = tag.slice(0, -1)
530
+ # String: apply validator function
531
+ else if typeof type is 'string'
535
532
 
536
- v = raw
533
+ # detect required value (trailing '!')
534
+ if type.endsWith '!'
535
+ must = true
536
+ type = type.slice(0, -1)
537
537
 
538
- # Named validator
539
- if typeof tag is 'string'
540
- fn = getValidator(tag)
541
- v = if fn then fn(String(v or '')) else null
538
+ # apply validator function
539
+ f = getValidator(type)
540
+ v = String(v ?? '') unless type in ['array', 'hash', 'json']
541
+ v = if f then f(v) else null
542
+
543
+ # Regex: apply regex pattern
544
+ else if type instanceof RegExp
545
+ s = String(v ?? '')
546
+ v = s.match(type)?[0] or null
542
547
 
543
548
  # Array: [min, max] constraint or enumeration
544
- else if Array.isArray(tag)
545
- if typeof tag[0] is 'number'
546
- minVal = tag[0]
547
- maxVal = tag[1]
548
- if typeof v is 'number' or (String(v) =~ /^[-+]?\d+$/)
549
- n = if typeof v is 'number' then v else parseInt(String(v))
550
- ok = not isNaN(n)
551
- ok = false if minVal? and n < minVal
552
- ok = false if maxVal? and n > maxVal
553
- v = if ok then n else null
549
+ else if Array.isArray(type)
550
+ s = String(v ?? '')
551
+ y = true
552
+ if typeof type[0] is 'number'
553
+ [min, max] = type
554
+ if typeof v is 'number' or (s =~ /^[-+]?\d+$/)
555
+ n = if typeof v is 'number' then v else +s
556
+ y = false if min? and n < min
557
+ y = false if max? and n > max
558
+ v = if y then n else null
554
559
  else
555
- s = String(v or '')
556
- ok = true
557
- ok = false if minVal? and s.length < minVal
558
- ok = false if maxVal? and s.length > maxVal
559
- v = if ok then s else null
560
+ y = false if min? and s.length < min
561
+ y = false if max? and s.length > max
562
+ v = if y then s else null
560
563
  else
561
- v = if tag.includes(String(v)) then String(v) else null
562
-
563
- # Regex
564
- else if tag instanceof RegExp
565
- v = String(v or '')[tag] or null
566
-
567
- # Object constraints
568
- else if typeof tag is 'object'
569
- if tag.start? or tag.end?
570
- n = parseInt(v)
571
- v = if not isNaN(n) and (not tag.start? or n >= tag.start) and (not tag.end? or n <= tag.end) then n else null
572
- else if tag.min? or tag.max?
573
- if typeof v is 'number' or (String(v or '') =~ /^[-+]?\d+$/)
574
- n = if typeof v is 'number' then v else parseInt(String(v))
575
- ok = not isNaN(n)
576
- ok = false if tag.min? and n < tag.min
577
- ok = false if tag.max? and n > tag.max
578
- v = if ok then n else null
564
+ v = if type.includes(s) then s else null
565
+
566
+ # Object: start/end, min/max
567
+ else if type? and typeof type is 'object'
568
+ s = String(v ?? '')
569
+ if type.start? or type.end?
570
+ n = if s =~ /^[-+]?\d+$/ then +s else NaN
571
+ v = if not isNaN(n) and (not type.start? or n >= type.start) and (not type.end? or n <= type.end) then n else null
572
+ else if type.min? or type.max?
573
+ y = true
574
+ if typeof v is 'number' or (s =~ /^[-+]?\d+$/)
575
+ n = if typeof v is 'number' then v else +s
576
+ y = false if type.min? and n < type.min
577
+ y = false if type.max? and n > type.max
578
+ v = if y then n else null
579
579
  else
580
- s = String(v or '')
581
- ok = true
582
- ok = false if tag.min? and s.length < tag.min
583
- ok = false if tag.max? and s.length > tag.max
584
- v = if ok then s else null
585
-
586
- blank = v is null or v is undefined or (typeof v is 'string' and v.trim() is '')
587
-
588
- if blank
589
- if required
590
- return miss?() if typeof miss is 'function'
591
- throw new Error "Missing required field: #{key}"
592
- else
593
- return miss?() if typeof miss is 'function'
594
- return null if miss is '' or not miss? # Empty string or null/undefined → null
595
- return miss # Return 0, false, etc. as-is
580
+ y = false if type.min? and s.length < type.min
581
+ y = false if type.max? and s.length > type.max
582
+ v = if y then s else null
583
+
584
+ # blank / missing value handling
585
+ if not v? or (typeof v is 'string' and v.trim() is '')
586
+ return done(must)
596
587
 
597
588
  v
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rip-lang/api",
3
- "version": "0.7.7",
3
+ "version": "0.8.1",
4
4
  "description": "Pure Rip API framework — elegant, fast, zero dependencies",
5
5
  "type": "module",
6
6
  "main": "api.rip",