galbe 0.2.0 → 0.4.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.
package/src/server.ts CHANGED
@@ -1,8 +1,12 @@
1
- import type { Context, Route } from './types'
1
+ import type { Context, Method, Route } from './types'
2
2
 
3
3
  import { InternalError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
+ import { validateResponse } from './validator'
7
+
8
+ const METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'OPTIONS', 'HEAD']
9
+ const EMPTY_BODY_METHODS = ['GET', 'OPTIONS', 'HEAD']
6
10
 
7
11
  const handleInternalError = (error: any) => {
8
12
  console.error(error)
@@ -10,7 +14,6 @@ const handleInternalError = (error: any) => {
10
14
  }
11
15
 
12
16
  const setupPluginCallbacks = (galbe: Galbe) => ({
13
- init: galbe.plugins.filter(p => p.init),
14
17
  onFetch: galbe.plugins.filter(p => p.onFetch),
15
18
  onRoute: galbe.plugins.filter(p => p.onRoute),
16
19
  beforeHandle: galbe.plugins.filter(p => p.beforeHandle),
@@ -22,12 +25,11 @@ export default async (galbe: Galbe, port?: number) => {
22
25
  if (galbe?.config?.basePath && galbe?.config?.basePath[0] !== '/')
23
26
  galbe.config.basePath = `/${galbe?.config?.basePath}`
24
27
  let pluginsCb = setupPluginCallbacks(galbe)
25
- //@ts-ignore
26
- for (const p of pluginsCb.init) await p.init(galbe?.config?.plugin?.[p.name], galbe)
27
28
 
28
29
  return Bun.serve({
29
30
  port: port || galbe.config?.port || 3000,
30
31
  async fetch(req) {
32
+ if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
31
33
  const context: Context = {
32
34
  request: req,
33
35
  set: { headers: {} },
@@ -48,7 +50,7 @@ export default async (galbe: Galbe, port?: number) => {
48
50
  try {
49
51
  // find route
50
52
  try {
51
- route = router.find(req.method, url.pathname)
53
+ route = router.find(req.method.toLowerCase() as Method, url.pathname)
52
54
  } catch (error) {
53
55
  if (error instanceof RequestError) throw error
54
56
  else throw handleInternalError(error)
@@ -69,37 +71,41 @@ export default async (galbe: Galbe, port?: number) => {
69
71
  for (let [k, v] of url.searchParams) inQuery[k] = v
70
72
  let inParams = requestPathParser(url.pathname, route.path)
71
73
 
72
- context.body = await requestBodyParser(req.body, inHeaders, schema.body)
74
+ context.body = !EMPTY_BODY_METHODS.includes(req.method)
75
+ ? await requestBodyParser(req.body, inHeaders, schema.body)
76
+ : null
73
77
  context.headers = inHeaders
74
78
  context.query = inQuery
75
79
  context.params = inParams
76
80
 
77
81
  // request validation
78
- let errors: RequestError[] = []
79
- try {
80
- if (schema?.headers)
81
- context.headers = {
82
- ...context.headers,
83
- ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
84
- }
85
- } catch (error) {
86
- if (error instanceof RequestError) errors.push(error)
87
- else throw handleInternalError(error)
88
- }
89
- try {
90
- if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
91
- } catch (error) {
92
- if (error instanceof RequestError) errors.push(error)
93
- else throw handleInternalError(error)
94
- }
95
- try {
96
- if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
97
- } catch (error) {
98
- if (error instanceof RequestError) errors.push(error)
99
- else throw handleInternalError(error)
100
- }
101
- if (errors.length) {
102
- throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
82
+ if (galbe.config?.requestValidator?.enabled) {
83
+ let errors: RequestError[] = []
84
+ try {
85
+ if (schema?.headers)
86
+ context.headers = {
87
+ ...context.headers,
88
+ ...parseEntry(context.headers, schema.headers, { name: 'headers', i: true })
89
+ }
90
+ } catch (error) {
91
+ if (error instanceof RequestError) errors.push(error)
92
+ else throw handleInternalError(error)
93
+ }
94
+ try {
95
+ if (schema?.query) context.query = parseEntry(context.query, schema.query, { name: 'query' })
96
+ } catch (error) {
97
+ if (error instanceof RequestError) errors.push(error)
98
+ else throw handleInternalError(error)
99
+ }
100
+ try {
101
+ if (schema?.params) context.params = parseEntry(context.params, schema.params, { name: 'params' })
102
+ } catch (error) {
103
+ if (error instanceof RequestError) errors.push(error)
104
+ else throw handleInternalError(error)
105
+ }
106
+ if (errors.length) {
107
+ throw new RequestError({ status: 400, payload: errors.reduce((acc, c) => ({ ...acc, ...c.payload }), {}) })
108
+ }
103
109
  }
104
110
 
105
111
  for (const p of pluginsCb.beforeHandle) {
@@ -140,7 +146,10 @@ export default async (galbe: Galbe, port?: number) => {
140
146
  if (r) response = r
141
147
  } else response = await handlerWrapper(context)
142
148
 
143
- const parsedResponse = responseParser(response, context)
149
+ const parsedResponse = responseParser(response, context, schema.response)
150
+
151
+ if (galbe.config?.responseValidator?.enabled && schema.response)
152
+ validateResponse(response, schema.response, parsedResponse.status || 200)
144
153
 
145
154
  for (const p of pluginsCb.afterHandle) {
146
155
  //@ts-ignore
@@ -154,12 +163,23 @@ export default async (galbe: Galbe, port?: number) => {
154
163
  let customError
155
164
  if (galbe.errorHandler) customError = responseParser(galbe.errorHandler(error, context), context)
156
165
  if (customError) return customError
157
- if (error instanceof RequestError) {
158
- return new Response(JSON.stringify(error.payload), {
166
+ if (error instanceof InternalError) {
167
+ console.log(`Internal Error`, error?.payload || '')
168
+ return new Response('Internal Server Error', {
159
169
  status: error.status,
160
170
  headers: { 'Content-Type': 'application/json' }
161
171
  })
162
- }
172
+ } else if (error instanceof RequestError) {
173
+ let payload = ''
174
+ if (typeof error.payload === 'string') payload = error.payload
175
+ try {
176
+ payload = JSON.stringify(error.payload)
177
+ } catch (err) {}
178
+ return new Response(payload, {
179
+ status: error.status,
180
+ headers: { 'Content-Type': 'application/json' }
181
+ })
182
+ } else console.log(error)
163
183
  return new Response('"Internal Server Error"', {
164
184
  status: 500,
165
185
  headers: {
package/src/types.ts CHANGED
@@ -1,13 +1,17 @@
1
1
  import type { ServeOptions, TLSServeOptions } from 'bun'
2
2
  import type {
3
+ STAny,
3
4
  STArray,
4
5
  STBoolean,
5
6
  STByteArray,
6
7
  STInteger,
8
+ STJson,
7
9
  STLiteral,
8
10
  STMultipartForm,
9
11
  STNumber,
10
12
  STObject,
13
+ STOptional,
14
+ STSchema,
11
15
  STStream,
12
16
  STString,
13
17
  STUnion,
@@ -22,15 +26,35 @@ export type STBody =
22
26
  | STBoolean
23
27
  | STNumber
24
28
  | STInteger
29
+ | STLiteral
25
30
  | STObject
26
31
  | STArray
27
32
  | STUrlForm
28
33
  | STMultipartForm
29
34
  | STUnion
30
35
  | STStream
36
+ | undefined
37
+
38
+ export type STResponseValue =
39
+ | STByteArray
40
+ | STString
41
+ | STBoolean
42
+ | STNumber
43
+ | STInteger
44
+ | STLiteral
45
+ | STObject
46
+ | STJson
47
+ | STArray
48
+ | STUnion
49
+ | STStream
50
+ | STAny
51
+ export type STResponse = Record<number, STResponseValue>
52
+
31
53
  export type MaybeArray<T> = T | T[]
54
+ export type MaybeSTArray<T extends STSchema> = T | STArray<T>
55
+ export type MaybeSTUnion<T extends STSchema> = T | STUnion<[T, ...T[]]>
32
56
 
33
- export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options'
57
+ export type Method = 'get' | 'post' | 'put' | 'delete' | 'patch' | 'options' | 'head'
34
58
  type MaybePromise<T> = T | Promise<T>
35
59
 
36
60
  export type ExtractParams<T extends string> = T extends `/:${infer P}/${infer Rest}`
@@ -41,13 +65,16 @@ export type ExtractParams<T extends string> = T extends `/:${infer P}/${infer Re
41
65
  ? P
42
66
  : never
43
67
 
44
- type STHeadersValue = STString | STBoolean | STNumber | STInteger | STLiteral | STUnion
68
+ type STHeadersPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
69
+ type STHeadersValue = MaybeSTUnion<STHeadersPrimaryValue>
45
70
  export type STHeaders = Record<string, STHeadersValue>
46
71
 
47
- type STParamsValue = STString | STBoolean | STNumber | STInteger | STLiteral | STUnion
72
+ type STParamsPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
73
+ type STParamsValue = MaybeSTUnion<STParamsPrimaryValue>
48
74
  export type STParams<Path extends string = string> = Record<ExtractParams<Path>, STParamsValue>
49
75
 
50
- type STQueryValue = STString | STBoolean | STNumber | STInteger | STLiteral | STUnion
76
+ type STQueryPrimaryValue = STString | STBoolean | STNumber | STInteger | STLiteral
77
+ type STQueryValue = MaybeSTArray<MaybeSTUnion<STQueryPrimaryValue>>
51
78
  export type STQuery = Record<string, STQueryValue>
52
79
 
53
80
  /**
@@ -74,6 +101,8 @@ export type GalbeConfig = {
74
101
  routes?: boolean | string | string[]
75
102
  router?: { cacheEnabled: boolean }
76
103
  plugin?: Record<string, any>
104
+ requestValidator?: { enabled: boolean }
105
+ responseValidator?: { enabled: boolean }
77
106
  }
78
107
  /**
79
108
  * #### Schema
@@ -95,16 +124,19 @@ export type GalbeConfig = {
95
124
  * ```
96
125
  */
97
126
  export type RequestSchema<
127
+ M extends Method = Method,
98
128
  Path extends string = string,
99
- H extends STHeaders = {},
100
- P extends Partial<STParams<Path>> = {},
101
- Q extends STQuery = {},
102
- B extends STBody = STBody
129
+ H extends STHeaders = STHeaders,
130
+ P extends Partial<STParams<Path>> = Partial<STParams<Path>>,
131
+ Q extends STQuery = STQuery,
132
+ B extends STBody = STBody,
133
+ R extends STResponse = STResponse
103
134
  > = {
104
135
  headers?: H
105
136
  params?: P
106
137
  query?: Q
107
138
  body?: B
139
+ response?: R
108
140
  }
109
141
 
110
142
  type OmitNotDefined<S extends RequestSchema> = {
@@ -115,14 +147,18 @@ type OmitNotDefined<S extends RequestSchema> = {
115
147
  : //@ts-ignore
116
148
  never]: Static<STObject<Exclude<S['params'], undefined>>>[K]
117
149
  }
118
-
119
- export type Context<Path extends string = string, S extends RequestSchema = RequestSchema> = {
150
+ type StaticBody<T extends STSchema> = T extends STOptional<STSchema> ? Static<T> | null : Static<T>
151
+ export type Context<
152
+ M extends Method = Method,
153
+ Path extends string = string,
154
+ S extends RequestSchema = RequestSchema
155
+ > = {
120
156
  headers: Static<STObject<Exclude<S['headers'], undefined>>>
121
157
  params: {
122
158
  [K in ExtractParams<Path>]: K extends keyof OmitNotDefined<S> ? OmitNotDefined<S>[K] : string
123
159
  }
124
160
  query: Static<STObject<Exclude<S['query'], undefined>>>
125
- body: Static<Exclude<S['body'], undefined>>
161
+ body: M extends 'get' | 'options' | 'head' ? null : StaticBody<Exclude<S['body'], undefined>>
126
162
  request: Request
127
163
  route?: Route
128
164
  state: Record<string, any>
@@ -134,57 +170,63 @@ export type Context<Path extends string = string, S extends RequestSchema = Requ
134
170
  }
135
171
  }
136
172
  export type Next = () => void | Promise<void>
137
- export type Hook<Path extends string = string, S extends RequestSchema = RequestSchema> = (
138
- ctx: Context<Path, S>,
173
+ export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
174
+ ctx: Context<M, Path, S>,
139
175
  next: Next
140
176
  ) => any | Promise<any>
141
- export type Handler<Path extends string = string, S extends RequestSchema = RequestSchema> = (
142
- ctx: Context<Path, S>
143
- ) => any
144
- export type Endpoint = {
177
+ export type Handler<
178
+ M extends Method = Method,
179
+ Path extends string = string,
180
+ S extends RequestSchema = RequestSchema
181
+ > = (ctx: Context<M, Path, S>) => any
182
+ export type Endpoint<M extends Method> = {
145
183
  <
146
184
  Path extends string,
147
- H extends STHeaders,
148
185
  P extends Partial<STParams<Path>>,
149
- Q extends STQuery,
150
- B extends STBody = any
186
+ H extends STHeaders = any,
187
+ Q extends STQuery = any,
188
+ B extends STBody = any,
189
+ R extends STResponse = STResponse
151
190
  >(
152
191
  path: Path,
153
- schema: RequestSchema<Path, H, P, Q, B>,
154
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
155
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
192
+ schema: RequestSchema<M, Path, H, P, Q, B, R>,
193
+ hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
194
+ handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
156
195
  ): void
157
196
  <
158
197
  Path extends string,
159
- H extends STHeaders,
160
198
  P extends Partial<STParams<Path>>,
161
- Q extends STQuery,
162
- B extends STBody = any
199
+ H extends STHeaders = any,
200
+ Q extends STQuery = any,
201
+ B extends STBody = any,
202
+ R extends STResponse = STResponse
163
203
  >(
164
204
  path: Path,
165
- schema: RequestSchema<Path, H, P, Q, B>,
166
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
205
+ schema: RequestSchema<M, Path, H, P, Q, B, R>,
206
+ handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
167
207
  ): void
168
208
  <
169
209
  Path extends string,
170
- H extends STHeaders,
171
210
  P extends Partial<STParams<Path>>,
172
- Q extends STQuery,
173
- B extends STBody = any
211
+ H extends STHeaders = any,
212
+ Q extends STQuery = any,
213
+ B extends STBody = any,
214
+ R extends STResponse = STResponse
174
215
  >(
175
216
  path: Path,
176
- hooks: Hook<Path, RequestSchema<Path, H, P, Q, B>>[],
177
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
217
+ hooks: Hook<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>[],
218
+ handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
178
219
  ): void
179
220
  <
180
221
  Path extends string,
181
- H extends STHeaders,
182
222
  P extends Partial<STParams<Path>>,
183
- Q extends STQuery,
184
- B extends STBody = any
223
+ H extends STHeaders = any,
224
+ Q extends STQuery = any,
225
+ B extends STBody = any,
226
+ R extends STResponse = STResponse
185
227
  >(
186
228
  path: Path,
187
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
229
+ handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
188
230
  ): void
189
231
  }
190
232
 
@@ -193,52 +235,56 @@ export class RequestError {
193
235
  payload: any
194
236
  constructor(options: { status?: number; payload?: any }) {
195
237
  this.status = options.status ?? 500
196
- this.payload = options.payload ?? 'Internal server error'
238
+ this.payload = options.payload
197
239
  }
198
240
  }
199
241
 
200
242
  export type ErrorHandler = (error: any, context: Context) => any
201
243
 
202
244
  export type RouteNode = {
203
- route?: Route
245
+ routes: { [K in Method]?: Route }
204
246
  param?: RouteNode
205
247
  children?: Record<string, RouteNode>
206
248
  }
207
249
 
208
250
  export type Route<
251
+ M extends Method = Method,
209
252
  Path extends string = string,
210
- H extends STHeaders = {},
211
253
  P extends Partial<STParams<Path>> = {},
212
- Q extends STQuery = {},
213
- B extends STBody = STBody
254
+ H extends STHeaders = STHeaders,
255
+ Q extends STQuery = STQuery,
256
+ B extends STBody = STBody,
257
+ R extends STResponse = STResponse
214
258
  > = {
215
- method: Method
259
+ method: M
216
260
  path: Path
217
- schema: RequestSchema<Path, H, P, Q, B>
218
- context: Context<Path, RequestSchema<Path, H, P, Q, B>>
261
+ schema: RequestSchema<M, Path, H, P, Q, B, R>
262
+ context: Context<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
219
263
  hooks: Hook[]
220
- handler: Handler<Path, RequestSchema<Path, H, P, Q, B>>
221
- }
222
-
223
- export type RouteTree = {
224
- [key: string]: RouteNode
264
+ handler: Handler<M, Path, RequestSchema<M, Path, H, P, Q, B, R>>
225
265
  }
226
266
 
227
267
  export class NotFoundError extends RequestError {
228
- constructor(message?: string) {
268
+ constructor(message?: any) {
229
269
  super({ status: 404, payload: message ?? 'Not found' })
230
270
  }
231
271
  }
232
272
 
273
+ export class MethodNotAllowedError extends RequestError {
274
+ constructor(message?: any) {
275
+ super({ status: 405, payload: message ?? 'Method not allowed' })
276
+ }
277
+ }
278
+
233
279
  export class InternalError extends RequestError {
234
- constructor(message?: string) {
280
+ constructor(message?: any) {
235
281
  super({ status: 500, payload: message ?? 'Internal Server Error' })
236
282
  }
237
283
  }
238
284
 
239
285
  /**
240
286
  * #### GalbePlugin
241
- * Define a plugin for a Galbe server
287
+ * Define a plugin for a Galbe application
242
288
  *
243
289
  * ---
244
290
  * @example
@@ -264,4 +310,14 @@ export type GalbePlugin = {
264
310
  onRoute?: (context: Context) => MaybePromise<Response | void>
265
311
  beforeHandle?: (context: Context) => MaybePromise<Response | void>
266
312
  afterHandle?: (response: Response, context: Context) => MaybePromise<Response | void>
313
+ cli?: (commands: GalbeCLICommand[]) => MaybePromise<GalbeCLICommand[] | void>
314
+ }
315
+
316
+ export type GalbeCLICommand = {
317
+ name: string
318
+ description?: string
319
+ route: Route
320
+ arguments?: { name: string; type: string; description: string }[]
321
+ options?: { name: string; short: string; type: string; description: string; default: any }[]
322
+ action?: (props: any) => MaybePromise<void>
267
323
  }
package/src/util.ts CHANGED
@@ -1,12 +1,94 @@
1
+ import type { Route, RouteNode } from '.'
2
+ import type { RouteMeta } from './routes'
3
+
1
4
  const METHOD_COLOR: Record<string, string> = {
2
5
  get: '\x1b[32m',
3
6
  post: '\x1b[34m',
4
7
  put: '\x1b[36m',
5
8
  patch: '\x1b[33m',
6
9
  delete: '\x1b[31m',
7
- options: ''
10
+ options: '',
11
+ head: ''
8
12
  }
9
- export const logRoute = (r: { method: string; path: string }) => {
13
+
14
+ export const logRoute = (
15
+ r: { method: string; path: string },
16
+ meta?: RouteMeta,
17
+ format?: { maxPathLength?: number }
18
+ ) => {
10
19
  let color = METHOD_COLOR?.[r.method] || ''
11
- console.log(` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path}`)
20
+ console.log(
21
+ ` [${color}${`${r.method.toUpperCase()}\x1b[0m]`.padEnd(12, ' ')} ${r.path
22
+ .padEnd(format?.maxPathLength ?? r.path.length, ' ')
23
+ .replaceAll(/:([^\/]+)/g, '\x1b[0;33m:$1\x1b[0m')}${meta?.head ? ` ${meta.head}` : ''}`
24
+ )
25
+ }
26
+
27
+ export const walkRoutes = (node: RouteNode, cb: (route: Route) => void) => {
28
+ if (node?.routes) Object.values(node.routes).forEach(r => cb(r))
29
+ for (let c of Object.values(node?.children || {})) walkRoutes(c, cb)
30
+ if (node?.param) walkRoutes(node.param, cb)
31
+ }
32
+
33
+ export const isIterator = (obj: any) => typeof obj?.next === 'function'
34
+
35
+ export const HttpStatus = {
36
+ 100: 'Continue',
37
+ 101: 'Switching Protocols',
38
+ 102: 'Processing',
39
+ 103: 'Early Hints',
40
+ 200: 'OK',
41
+ 201: 'Created',
42
+ 202: 'Accepted',
43
+ 203: 'Non Authoritative Information',
44
+ 204: 'No Content',
45
+ 205: 'Reset Content',
46
+ 206: 'Partial Content',
47
+ 207: 'Multi-Status',
48
+ 300: 'Multiple Choices',
49
+ 301: 'Moved Permanently',
50
+ 302: 'Moved Temporarily',
51
+ 303: 'See Other',
52
+ 304: 'Not Modified',
53
+ 305: 'Use Proxy',
54
+ 307: 'Temporary Redirect',
55
+ 308: 'Permanent Redirect',
56
+ 400: 'Bad Request',
57
+ 401: 'Unauthorized',
58
+ 402: 'Payment Required',
59
+ 403: 'Forbidden',
60
+ 404: 'Not Found',
61
+ 405: 'Method Not Allowed',
62
+ 406: 'Not Acceptable',
63
+ 407: 'Proxy Authentication Required',
64
+ 408: 'Request Timeout',
65
+ 409: 'Conflict',
66
+ 410: 'Gone',
67
+ 411: 'Length Required',
68
+ 412: 'Precondition Failed',
69
+ 413: 'Request Entity Too Large',
70
+ 414: 'Request-URI Too Long',
71
+ 415: 'Unsupported Media Type',
72
+ 416: 'Requested Range Not Satisfiable',
73
+ 417: 'Expectation Failed',
74
+ 418: "I'm a teapot",
75
+ 419: 'Insufficient Space on Resource',
76
+ 420: 'Method Failure',
77
+ 421: 'Misdirected Request',
78
+ 422: 'Unprocessable Entity',
79
+ 423: 'Locked',
80
+ 424: 'Failed Dependency',
81
+ 426: 'Upgrade Required',
82
+ 428: 'Precondition Required',
83
+ 429: 'Too Many Requests',
84
+ 431: 'Request Header Fields Too Large',
85
+ 451: 'Unavailable For Legal Reasons',
86
+ 500: 'Internal Server Error',
87
+ 501: 'Not Implemented',
88
+ 502: 'Bad Gateway',
89
+ 503: 'Service Unavailable',
90
+ 504: 'Gateway Timeout',
91
+ 505: 'HTTP Version Not Supported',
92
+ 507: 'Insufficient Storage',
93
+ 511: 'Network Authentication Required'
12
94
  }
package/src/validator.ts CHANGED
@@ -1,5 +1,7 @@
1
+ import { InternalError, type STResponse } from './index'
1
2
  import type { STSchema, STProps, STUnion } from './schema'
2
- import { Kind, Optional } from './schema'
3
+ import { Kind, Optional, Stream } from './schema'
4
+ import { isIterator } from './util'
3
5
 
4
6
  export const validate = (elt: any, schema: STSchema, parse = false): any => {
5
7
  type ValidationError = string | string[] | { [key: string]: ValidationError }
@@ -7,7 +9,10 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
7
9
  const iElt = elt
8
10
 
9
11
  if (schema[Kind] === 'boolean') {
10
- if (parse && typeof elt === 'string') elt = elt === 'true' ? true : elt === 'false' ? false : null
12
+ if (typeof elt === 'string') {
13
+ if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
14
+ else throw `Expected boolean, got string.`
15
+ }
11
16
  if (elt !== true && elt !== false) throw `${iElt} is not a valid boolean. Should be 'true' or 'false'`
12
17
  } else if (schema[Kind] === 'integer') {
13
18
  if (parse && typeof elt === 'string') elt = Number(elt)
@@ -45,6 +50,8 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
45
50
  }
46
51
  })
47
52
  if (Object.keys(err).length) errors.push(err)
53
+ } else if (schema[Kind] === 'json') {
54
+ elt = validate(elt, { ...schema, [Kind]: schema.type }, parse)
48
55
  } else if (schema[Kind] === 'array') {
49
56
  if (parse && typeof elt === 'string') {
50
57
  try {
@@ -84,6 +91,22 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
84
91
  return elt
85
92
  }
86
93
 
94
+ export const validateResponse = (response: any, schema: STResponse, status: number) => {
95
+ if (!(status in schema)) return
96
+ const s = schema[status]
97
+ if (response instanceof ReadableStream) {
98
+ if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got ReadableStream`)
99
+ } else if (isIterator(response)) {
100
+ if (!s[Stream]) throw new InternalError(`Expected ${s[Kind]} response, but got Iterator`)
101
+ } else {
102
+ try {
103
+ validate(response, s)
104
+ } catch (error) {
105
+ throw new InternalError({ ResponseValidationError: error })
106
+ }
107
+ }
108
+ }
109
+
87
110
  const schemaValidation = (value: any, schema: STSchema) => {
88
111
  const errors = []
89
112
  if (schema[Kind] === 'integer' || schema[Kind] === 'number') {
@@ -92,9 +115,8 @@ const schemaValidation = (value: any, schema: STSchema) => {
92
115
  if (schema.exclusiveMax !== undefined)
93
116
  if ((value as number) >= schema.exclusiveMax)
94
117
  errors.push(`${value} is greater or equal to ${schema.exclusiveMax}`)
95
- if (schema.minimum !== undefined)
96
- if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
97
- if (schema.maximum !== undefined)
118
+ if (schema.min !== undefined) if ((value as number) < schema.min) errors.push(`${value} is less than ${schema.min}`)
119
+ if (schema.max !== undefined)
98
120
  if ((value as number) > schema.max) errors.push(`${value} is greater than ${schema.max}`)
99
121
  } else if (schema[Kind] === 'string') {
100
122
  if (schema.minLength !== undefined && (value as string).length < schema.minLength)
@@ -10,6 +10,7 @@ import {
10
10
  isAsyncIterator
11
11
  } from './test.utils'
12
12
  import { Galbe, $T } from '../src'
13
+ import { schemaToTypeStr } from '../src/schema'
13
14
 
14
15
  const port = 7357
15
16
 
@@ -1195,4 +1196,37 @@ describe('parser', () => {
1195
1196
  expect(body).toEqual(expected.body)
1196
1197
  }
1197
1198
  })
1199
+
1200
+ test('schema to type', async () => {
1201
+ let type = schemaToTypeStr(
1202
+ $T.object({
1203
+ boolean: $T.boolean(),
1204
+ byteArray: $T.byteArray(),
1205
+ number: $T.number(),
1206
+ integer: $T.integer(),
1207
+ string: $T.string(),
1208
+ any: $T.any(),
1209
+ literal: $T.literal('literal'),
1210
+ array: $T.array($T.string()),
1211
+ object: $T.object({
1212
+ foo: $T.string()
1213
+ }),
1214
+ union: $T.union([$T.number(), $T.string()])
1215
+ })
1216
+ )
1217
+ expect(type).toBe(
1218
+ `{` +
1219
+ `'boolean':boolean;` +
1220
+ `'byteArray':Uint8Array;` +
1221
+ `'number':number;` +
1222
+ `'integer':number;` +
1223
+ `'string':string;` +
1224
+ `'any':any;` +
1225
+ `'literal':'literal';` +
1226
+ `'array':Array<string>;` +
1227
+ `'object':{'foo':string};` +
1228
+ `'union':number|string` +
1229
+ `}`
1230
+ )
1231
+ })
1198
1232
  })