galbe 0.6.2 → 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.
@@ -108,16 +108,22 @@ const parseOapiSchema = (
108
108
  title: os.title,
109
109
  description: os.description
110
110
  }
111
+ let resp = ''
111
112
  let hasOptions = Object.values(options).some(v => !!v)
112
113
  let optArg = hasOptions ? `, ${JSON.stringify(options)}` : ''
113
114
  let anyOf = os.oneOf || os.anyOf || os.allOf
115
+ let required = os.required
116
+ let nullable = os.nullable
117
+
114
118
  if (anyOf?.length) {
115
- return `$T.union([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}], ${JSON.stringify(
116
- options
117
- )})`
118
- }
119
- if (os.type === 'boolean') return `$T.boolean(${hasOptions ? JSON.stringify(options) : ''})`
120
- if (os.type === 'number') {
119
+ if (anyOf.length === 1) resp = parseOapiSchema(anyOf[0] as OpenAPIV3.SchemaObject, details, extra)
120
+ else {
121
+ resp = `$T.union([${anyOf.map(s => parseOapiSchema(s as OpenAPIV3.SchemaObject)).join(',')}], ${JSON.stringify(
122
+ options
123
+ )})`
124
+ }
125
+ } else if (os.type === 'boolean') resp = `$T.boolean(${hasOptions ? JSON.stringify(options) : ''})`
126
+ else if (os.type === 'number') {
121
127
  let { max, min, exclusiveMax, exclusiveMin } = {
122
128
  max: os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined,
123
129
  min: os.minimum !== undefined && !os.exclusiveMinimum ? os.minimum : undefined,
@@ -126,49 +132,52 @@ const parseOapiSchema = (
126
132
  }
127
133
  options = { ...options, min, max, exclusiveMax, exclusiveMin }
128
134
  hasOptions = Object.values(options).some(v => !!v)
129
- return `$T.number(${hasOptions ? JSON.stringify(options) : ''})`
130
- }
131
- if (os.type === 'integer') {
135
+ resp = `$T.number(${hasOptions ? JSON.stringify(options) : ''})`
136
+ } else if (os.type === 'integer') {
132
137
  let max = os.maximum !== undefined && !os.exclusiveMaximum ? os.maximum : undefined
133
138
  let min = os.minimum !== undefined && !os.exclusiveMinimum ? os.minimum : undefined
134
139
  let exclusiveMax = os.maximum !== undefined && os.exclusiveMaximum ? os.maximum : undefined
135
140
  let exclusiveMin = os.minimum !== undefined && os.exclusiveMinimum ? os.minimum : undefined
136
141
  options = { ...options, min, max, exclusiveMax, exclusiveMin }
137
142
  hasOptions = Object.values(options).some(v => !!v)
138
- return `$T.integer(${hasOptions ? JSON.stringify(options) : ''})`
139
- }
140
- if (os.type === 'string') {
141
- if (os.format === 'binary') return `$T.byteArray(${hasOptions ? JSON.stringify(options) : ''})`
142
- let minLength = os.minLength
143
- let maxLength = os.maxLength
144
- let pattern = os.pattern
145
- options = { ...options, minLength, maxLength, pattern }
146
- hasOptions = Object.values(options).some(v => !!v)
147
- return `$T.string(${hasOptions ? JSON.stringify(options) : ''})`
148
- }
149
- if (os.type === 'array') {
143
+ resp = `$T.integer(${hasOptions ? JSON.stringify(options) : ''})`
144
+ } else if (os.type === 'string') {
145
+ if (os.format === 'binary') resp = `$T.byteArray(${hasOptions ? JSON.stringify(options) : ''})`
146
+ else {
147
+ let minLength = os.minLength
148
+ let maxLength = os.maxLength
149
+ let pattern = os.pattern
150
+ options = { ...options, minLength, maxLength, pattern }
151
+ hasOptions = Object.values(options).some(v => !!v)
152
+ resp = `$T.string(${hasOptions ? JSON.stringify(options) : ''})`
153
+ }
154
+ } else if (os.type === 'array') {
150
155
  let minItems = os.minItems
151
156
  let maxItems = os.maxItems
152
157
  let unique = os.uniqueItems
153
158
  options = { ...options, minItems, maxItems, unique }
154
- return `$T.array(${parseOapiSchema(os?.items)}${optArg})`
155
- }
156
- if (os.type === 'object') {
159
+ resp = `$T.array(${parseOapiSchema(os?.items)}${optArg})`
160
+ } else if (os.type === 'object') {
157
161
  if (extra?.media === 'multipart/form-data') {
158
- return `$T.multipartForm({${Object.entries(os?.properties || {})
162
+ resp = `$T.multipartForm({${Object.entries(os?.properties || {})
159
163
  .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
160
164
  .join(',')}}${optArg})`
161
- }
162
- if (extra?.media === 'application/x-www-form-urlencoded') {
163
- return `$T.urlForm({${Object.entries(os?.properties || {})
165
+ } else if (extra?.media === 'application/x-www-form-urlencoded') {
166
+ resp = `$T.urlForm({${Object.entries(os?.properties || {})
167
+ .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
168
+ .join(',')}}${optArg})`
169
+ } else {
170
+ resp = `$T.object({${Object.entries(os?.properties || {})
164
171
  .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
165
172
  .join(',')}}${optArg})`
166
173
  }
167
- return `$T.object({${Object.entries(os?.properties || {})
168
- .map(([k, v]) => `${k}:${parseOapiSchema(v)}`)
169
- .join(',')}}${optArg})`
170
- }
171
- throw new Error(`Unknown schema type ${os.type}`)
174
+ } else throw new Error(`Unknown schema type ${os.type}`)
175
+
176
+ if (!required && nullable) resp = `$T.nullish(${resp})`
177
+ else if (!required) resp = `$T.optional(${resp})`
178
+ else if (nullable) resp = `$T.nullable(${resp})`
179
+
180
+ return resp
172
181
  }
173
182
 
174
183
  const buildSchemaIndex = (def: OpenAPIV3.Document) => {
@@ -217,7 +226,7 @@ const parseEndpointDef = (method: string, path: string, def?: OpenAPIV3.Operatio
217
226
  let imports = {}
218
227
  let p = path.replaceAll(/\{([^\}]*)\}/g, ':$1')
219
228
  let description = def.summary || def.description
220
- let pathName = path.replaceAll(/\/\{[^\}]*\}/g, 'X').replaceAll(/[^$\w\d-_]([$\w\d-_])/g, (_, $1) => $1.toUpperCase())
229
+ let pathName = path.replaceAll(/\/\{[^\}]*\}/g, 'X').replaceAll(/[^$\w\d_]+([$\w\d_])/g, (_, $1) => $1.toUpperCase())
221
230
  let schemaName = `${method}${pathName}`.replace(/^\w/, c => c.toUpperCase())
222
231
 
223
232
  let meta = '/**\n'
package/docs/context.md CHANGED
@@ -11,7 +11,7 @@ A context has the following properties:
11
11
 
12
12
  **request**
13
13
 
14
- An instance of the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object created by th server.
14
+ An instance of the [Request](https://developer.mozilla.org/en-US/docs/Web/API/Request) object created by the server.
15
15
 
16
16
  **headers**
17
17
 
@@ -112,3 +112,7 @@ galbe.get(
112
112
  $ curl http://localhost:3000/example
113
113
  bar
114
114
  ```
115
+
116
+ **remoteAddress**
117
+
118
+ An instance of the [SocketAdress](https://github.com/oven-sh/bun/blob/fe62a614046948ebba260bed87db96287e67921f/packages/bun-types/bun.d.ts#L2600-L2613) representing the remote address of the client.
package/docs/schemas.md CHANGED
@@ -44,6 +44,14 @@ Schema Type matching integer `number` values.
44
44
  const intSchema = $T.integer(options)
45
45
  ```
46
46
 
47
+ #### Null
48
+
49
+ Schema Type matching `null` values.
50
+
51
+ ```ts
52
+ const nullSchema = $T.null(options)
53
+ ```
54
+
47
55
  #### Any
48
56
 
49
57
  Schema Type matching `any` of the previous Schema Types.
@@ -68,6 +76,22 @@ Makes any type optional. This allows for `undefined` values.
68
76
  const optionalSchema = $T.optional($T.string())
69
77
  ```
70
78
 
79
+ #### Nullable
80
+
81
+ Makes any type nullable. This allows for `null` values.
82
+
83
+ ```ts
84
+ const nullableSchema = $T.nullable($T.string())
85
+ ```
86
+
87
+ #### Nullish
88
+
89
+ Makes any type nullish. This allows for `undefined` and `null` values.
90
+
91
+ ```ts
92
+ const nullishSchema = $T.nullish($T.string())
93
+ ```
94
+
71
95
  #### Union
72
96
 
73
97
  Creates an union of Schema Types.
@@ -155,7 +179,7 @@ const schema = {
155
179
  body: STByteArray | STString | STBoolean | STNumber | STInteger | STLiteral | STObject | STArray | STMulripartForm | STUrlForm | STStream
156
180
  ```
157
181
 
158
- #### Json
182
+ #### Object
159
183
 
160
184
  To define an `application/json` request body, use `STObject` Schema Type. Example:
161
185
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "galbe",
3
- "version": "0.6.2",
3
+ "version": "0.8.0",
4
4
  "description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
5
5
  "author": "Pierre Caillaud M (https://github.com/pierre-cm)",
6
6
  "type": "module",
@@ -51,8 +51,12 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
51
51
  //@ts-ignore
52
52
  return { schema: { $ref: `#/components/schemas/${schema.id}` } }
53
53
  }
54
- // TODO add constraints min, max etc.
55
- if (kind === 'boolean') s = { type: 'boolean' }
54
+
55
+ if (kind === 'null') {
56
+ s = {
57
+ anyOf: ['null']
58
+ }
59
+ } else if (kind === 'boolean') s = { type: 'boolean' }
56
60
  else if (kind === 'byteArray') s = { type: 'string', format: 'byte' }
57
61
  else if (kind === 'number')
58
62
  s = {
@@ -118,10 +122,23 @@ export const OpenAPISerializer = async (g: Galbe, version = '3.0.3'): Promise<Op
118
122
  }
119
123
  } else if (kind === 'union') {
120
124
  let anyOf = (schema as STUnion).anyOf
121
- s = {
122
- anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
125
+ let nullable = anyOf.some(s => s[Kind] === 'null')
126
+ anyOf = anyOf.filter(s => s[Kind] !== 'null')
127
+
128
+ if (anyOf.length === 0) {
129
+ s = {}
130
+ } else if (anyOf.length === 1) {
131
+ s = schemaToOpenapi(anyOf[0]).schema
132
+ } else if (anyOf.length > 1) {
133
+ s = {
134
+ anyOf: anyOf.map(s => schemaToOpenapi(s).schema)
135
+ }
123
136
  }
137
+
138
+ //@ts-ignore
139
+ if (nullable) s.nullable = nullable
124
140
  }
141
+
125
142
  s = { title: schema.title, description: schema.description, ...s }
126
143
  if (components.schemas && schema.id) {
127
144
  components.schemas[schema.id] = s
package/src/parser.ts CHANGED
@@ -56,7 +56,7 @@ export const requestBodyParser = async (
56
56
  }
57
57
  })
58
58
  } else return new Uint8Array()
59
- } else if (schema?.[Optional]) {
59
+ } else if (schema?.[Optional] || kind === 'null') {
60
60
  return null
61
61
  } else {
62
62
  throw new RequestError({ status: 400, payload: { body: `Not a valid ${kind}` } })
@@ -75,7 +75,8 @@ export const requestBodyParser = async (
75
75
  } else return null
76
76
  }
77
77
  } else {
78
- if (kind === 'byteArray') {
78
+ if (kind === 'null') throw new RequestError({ status: 400, payload: { body: `Expected null body` } })
79
+ else if (kind === 'byteArray') {
79
80
  if (isStream) return rsToAsyncIterator(body)
80
81
  const bytes = await readableStreamToArrayBuffer(body)
81
82
  return new Uint8Array(bytes)
@@ -435,7 +436,7 @@ const parseMultipartContent = (
435
436
  } else {
436
437
  throw new RequestError({
437
438
  status: 400,
438
- payload: { body: { [headers.name]: `Expect ${schema?.props[headers.name][Kind]} found json` } }
439
+ payload: { body: { [headers.name]: `Expected ${schema?.props[headers.name][Kind]} found json` } }
439
440
  })
440
441
  }
441
442
  }
package/src/router.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import type { Method, Route, RouteNode } from './types'
2
2
  import { MethodNotAllowedError, NotFoundError } from './types'
3
3
 
4
- const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d][\w-]+[\w\d]))*\/?$/
4
+ const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d.][\w-.]+[\w\d]))*\/?$/
5
5
 
6
6
  const walk = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
7
7
  if (path.length < 1) throw new NotFoundError()
package/src/routes.ts CHANGED
@@ -35,6 +35,9 @@ class GalbeProxy {
35
35
  this.#g = g
36
36
  this.#cb = cb
37
37
  }
38
+ get server() {
39
+ return this.#g.server
40
+ }
38
41
  async get(...args: any[]) {
39
42
  //@ts-ignore
40
43
  const route = this.#g.get(...args) as Route
@@ -231,7 +234,7 @@ export const defineRoutes = async (
231
234
  if (!routes) return
232
235
  const root = process.cwd()
233
236
  if (typeof routes === 'string') {
234
- for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false })) {
237
+ for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false, dot: true })) {
235
238
  const isDir = (await lstat(path)).isDirectory()
236
239
 
237
240
  let files: string[] = []
package/src/schema.ts CHANGED
@@ -31,6 +31,7 @@ export interface ArrayOptions extends Options {
31
31
  }
32
32
  export interface STSchema extends Options {
33
33
  [Kind]:
34
+ | 'null'
34
35
  | 'boolean'
35
36
  | 'byteArray'
36
37
  | 'number'
@@ -61,6 +62,7 @@ export type STPropsValue =
61
62
  | STObject
62
63
  | STUnion
63
64
  | STAny
65
+ | STNull
64
66
  export type STProps = Record<string | number, STPropsValue>
65
67
 
66
68
  type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
@@ -84,6 +86,17 @@ export type STStream<T extends STSchema = STSchema> = T & {
84
86
  [Stream]: true
85
87
  }
86
88
 
89
+ // Null
90
+ export interface STNull extends STSchema, Options {
91
+ [Kind]: 'null'
92
+ static: null
93
+ }
94
+ export function _Null(options: Options = {}): STNull {
95
+ return {
96
+ ...options,
97
+ [Kind]: 'null'
98
+ } as unknown as STNull
99
+ }
87
100
  // ByteArray
88
101
  export interface STByteArray extends STSchema, ByteArrayOptions {
89
102
  [Kind]: 'byteArray'
@@ -310,12 +323,28 @@ export function _Stream<T extends STStreamable>(schema: T): STStream<T> {
310
323
  [Stream]: true
311
324
  } as unknown as STStream<T>
312
325
  }
326
+ // Nullable
327
+ type STNullable<T extends STSchema> = STUnion<[T, STNull]>
328
+ // Nullish
329
+ type STNullish<T extends STSchema> = STOptional<STNullable<T>>
313
330
 
314
331
  export class SchemaType {
315
332
  /** Creates an Optional Schema Type Wrapper*/
316
333
  public optional<T extends STSchema>(schema: T): STOptional<T> {
317
334
  return { ...schema, [Optional]: true }
318
335
  }
336
+ /** Creates an Nullable Schema Type Wrapper*/
337
+ public nullable<T extends STSchema>(schema: T): STNullable<T> {
338
+ return { ..._Union([schema, _Null()], {}) }
339
+ }
340
+ /** Creates an Nullish Schema Type Wrapper*/
341
+ public nullish<T extends STSchema>(schema: T): STNullish<T> {
342
+ return { ..._Union([schema, _Null()], {}), [Optional]: true }
343
+ }
344
+ /** Creates a Null Schema Type */
345
+ public null(options: Options = {}): STNull {
346
+ return _Null(options)
347
+ }
319
348
  /** Creates a ByteArray Schema Type */
320
349
  public byteArray(options: ByteArrayOptions = {}): STByteArray {
321
350
  return _ByteArray(options)
@@ -419,7 +448,8 @@ export const schemaToTypeStr = (schema: STSchema): string => {
419
448
  let type = 'unknown'
420
449
  let kind = schema[Kind]
421
450
 
422
- if (kind === 'boolean') type = 'boolean'
451
+ if (kind === 'null') type = 'null'
452
+ else if (kind === 'boolean') type = 'boolean'
423
453
  else if (kind === 'byteArray') type = 'Uint8Array'
424
454
  else if (kind === 'number') type = 'number'
425
455
  else if (kind === 'integer') type = 'number'
package/src/server.ts CHANGED
@@ -4,7 +4,6 @@ import { InternalError, RequestError } from './types'
4
4
  import { parseEntry, requestBodyParser, requestPathParser, responseParser } from './parser'
5
5
  import { Galbe } from './index'
6
6
  import { validateResponse } from './validator'
7
- import { logger } from 'girok'
8
7
 
9
8
  type MakeOptional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>
10
9
 
@@ -29,7 +28,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
29
28
  galbe.config.basePath = `/${galbe?.config?.basePath}`
30
29
  let pluginsCb = setupPluginCallbacks(galbe)
31
30
 
32
- return Bun.serve({
31
+ const server = Bun.serve({
33
32
  port: port || galbe.config?.port || 3000,
34
33
  hostname: hostname || galbe.config?.hostname || 'localhost',
35
34
  tls: galbe.config?.tls,
@@ -38,6 +37,7 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
38
37
  if (!METHODS.includes(req.method)) return new Response('', { status: 501 })
39
38
  const context = {
40
39
  request: req,
40
+ remoteAddress: server.requestIP(req),
41
41
  set: { headers: {} },
42
42
  state: {}
43
43
  } as MakeOptional<Context, 'headers' | 'params' | 'query' | 'body'>
@@ -129,18 +129,18 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
129
129
  if (nextCalled) console.error('Hook already called - ignored')
130
130
  else {
131
131
  nextCalled = true
132
- await callChain[idx + 1].call()
132
+ return await callChain[idx + 1].call()
133
133
  }
134
134
  }
135
135
  let r = await hook(context as Context, next)
136
136
  if (r) return r
137
- if (!nextCalled && !handlerCalled) await next()
137
+ if (!nextCalled && !handlerCalled) return await next()
138
138
  }
139
139
  }))
140
140
  callChain.push({
141
141
  call: async () => {
142
142
  response = await handlerWrapper(context as Context)
143
- context.set.status = response instanceof Response ? response.status : 200
143
+ context.set.status = response instanceof Response ? response.status : context.set.status || 200
144
144
  }
145
145
  })
146
146
  if (callChain.length > 1) {
@@ -200,4 +200,5 @@ export default async (galbe: Galbe, port?: number, hostname?: string) => {
200
200
  })
201
201
  }
202
202
  })
203
+ return server
203
204
  }
package/src/types.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { ServeOptions, TLSOptions, TLSServeOptions } from 'bun'
1
+ import type { ServeOptions, SocketAddress, TLSOptions, TLSServeOptions } from 'bun'
2
2
  import type {
3
3
  STAny,
4
4
  STArray,
@@ -8,6 +8,7 @@ import type {
8
8
  STJson,
9
9
  STLiteral,
10
10
  STMultipartForm,
11
+ STNull,
11
12
  STNumber,
12
13
  STObject,
13
14
  STOptional,
@@ -48,6 +49,7 @@ export type STResponseValue =
48
49
  | STUnion
49
50
  | STStream
50
51
  | STAny
52
+ | STNull
51
53
  export type STResponse = Record<number, STResponseValue>
52
54
 
53
55
  export type MaybeArray<T> = T | T[]
@@ -162,6 +164,7 @@ export type Context<
162
164
  query: Static<STObject<Exclude<S['query'], undefined>>>
163
165
  body: M extends 'get' | 'options' | 'head' ? null : StaticBody<Exclude<S['body'], undefined>>
164
166
  request: Request
167
+ remoteAddress: SocketAddress | null
165
168
  route?: Route
166
169
  state: Record<string, any>
167
170
  set: {
@@ -171,7 +174,7 @@ export type Context<
171
174
  status?: number
172
175
  }
173
176
  }
174
- export type Next = () => void | Promise<void>
177
+ export type Next = () => void | Promise<any>
175
178
  export type Hook<M extends Method = Method, Path extends string = string, S extends RequestSchema = RequestSchema> = (
176
179
  ctx: Context<M, Path, S>,
177
180
  next: Next
package/src/validator.ts CHANGED
@@ -8,7 +8,9 @@ export const validate = (elt: any, schema: STSchema, parse = false): any => {
8
8
  const errors: ValidationError[] = []
9
9
  const iElt = elt
10
10
 
11
- if (schema[Kind] === 'boolean') {
11
+ if (schema[Kind] === 'null') {
12
+ if (elt !== null) throw `Expected null value got ${iElt}`
13
+ } else if (schema[Kind] === 'boolean') {
12
14
  if (typeof elt === 'string') {
13
15
  if (parse) elt = elt === 'true' ? true : elt === 'false' ? false : null
14
16
  else throw `Expected boolean, got string.`
@@ -324,14 +324,28 @@ describe('parser', () => {
324
324
  bool: false,
325
325
  object: {},
326
326
  array: [],
327
- any: false
327
+ any: false,
328
+ null: null,
329
+ nullable: null,
330
+ nullish: 42
328
331
  }),
329
332
  type,
330
333
  schema,
331
334
  expected: {
332
335
  status: 200,
333
336
  type: 'object',
334
- resp: { ba: '', string: '', number: 0, bool: false, object: {}, array: [], any: false }
337
+ resp: {
338
+ ba: '',
339
+ string: '',
340
+ number: 0,
341
+ bool: false,
342
+ object: {},
343
+ array: [],
344
+ any: false,
345
+ null: null,
346
+ nullable: null,
347
+ nullish: 42
348
+ }
335
349
  }
336
350
  },
337
351
  {
@@ -343,7 +357,10 @@ describe('parser', () => {
343
357
  object: { foo: 'bar' },
344
358
  array: [false, 'one', 2],
345
359
  any: '36',
346
- optional: 'optional'
360
+ optional: 'optional',
361
+ null: null,
362
+ nullable: 'test',
363
+ nullish: null
347
364
  }),
348
365
  type,
349
366
  schema,
@@ -358,7 +375,10 @@ describe('parser', () => {
358
375
  object: { foo: 'bar' },
359
376
  array: [false, 'one', 2],
360
377
  any: '36',
361
- optional: 'optional'
378
+ optional: 'optional',
379
+ null: null,
380
+ nullable: 'test',
381
+ nullish: null
362
382
  }
363
383
  }
364
384
  },
@@ -370,7 +390,8 @@ describe('parser', () => {
370
390
  bool: 'x',
371
391
  object: [],
372
392
  array: {},
373
- any: {}
393
+ any: {},
394
+ null: ''
374
395
  }),
375
396
  type,
376
397
  schema,
@@ -383,7 +404,9 @@ describe('parser', () => {
383
404
  number: 'a is not a valid number',
384
405
  bool: "x is not a valid boolean. Should be 'true' or 'false'",
385
406
  object: 'Expected an object, not an array',
386
- array: 'Not a valid array'
407
+ array: 'Not a valid array',
408
+ null: 'Expected null value got ',
409
+ nullable: 'Required'
387
410
  }
388
411
  }
389
412
  }
@@ -13,18 +13,7 @@ describe('router', () => {
13
13
  test('routes, bad syntax', async () => {
14
14
  const galbe = new Galbe()
15
15
 
16
- const invalidPaths = [
17
- '.',
18
- '/@',
19
- '/-ta',
20
- '/test/-ta',
21
- '/test/my.path',
22
- '/hell@/w0rld',
23
- '/last-',
24
- '../',
25
- './x',
26
- '/hello?'
27
- ]
16
+ const invalidPaths = ['.', '/@', '/-ta', '/test/-ta', '/hell@/w0rld', '/last-', '../', './x', '/hello?']
28
17
 
29
18
  for (const p of invalidPaths) {
30
19
  try {
@@ -32,7 +32,10 @@ export const schema_objectBase = {
32
32
  export const schema_object = {
33
33
  ...schema_objectBase,
34
34
  object: $T.object($T.any()),
35
- array: $T.array()
35
+ array: $T.array(),
36
+ null: $T.null(),
37
+ nullable: $T.nullable($T.string()),
38
+ nullish: $T.nullish($T.number())
36
39
  }
37
40
 
38
41
  export const isAsyncIterator = (obj: any) => {