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/parser.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MaybeArray, STBody, Context } from './index'
1
+ import type { MaybeArray, STBody, Context, STResponse } from './index'
2
2
  import type {
3
3
  STStream,
4
4
  STUrlForm,
@@ -17,6 +17,7 @@ import { readableStreamToArrayBuffer } from 'bun'
17
17
  import { Kind, Optional, Stream } from './schema'
18
18
  import { validate } from './validator'
19
19
  import { InternalError, RequestError } from './index'
20
+ import { isIterator } from './util'
20
21
 
21
22
  const textDecoder = new TextDecoder()
22
23
  const textEncoder = new TextEncoder()
@@ -282,6 +283,7 @@ const streamToUrlForm = async (body: ReadableStream<Uint8Array>, schema?: STUrlF
282
283
  }
283
284
  async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundary: string, schema?: STMultipartForm) {
284
285
  const bound = textEncoder.encode(boundary)
286
+ const delimiter = textEncoder.encode('\r\n\r\n')
285
287
  let rest = new Uint8Array()
286
288
  let bK: Uint8Array = new Uint8Array()
287
289
  let bV: Uint8Array = new Uint8Array()
@@ -293,6 +295,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
293
295
  start = 0
294
296
  for (let i = 0; i < chunk.length; i++) {
295
297
  let matchBound = true
298
+ let matchDelimiter = true
296
299
  for (let b = 0; b < bound.length; b++) {
297
300
  if (chunk[i + b] === bound[b]) continue
298
301
  else {
@@ -300,6 +303,15 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
300
303
  break
301
304
  }
302
305
  }
306
+ if (!matchBound) {
307
+ for (let b = 0; b < delimiter.length; b++) {
308
+ if (chunk[i + b] === delimiter[b]) continue
309
+ else {
310
+ matchDelimiter = false
311
+ break
312
+ }
313
+ }
314
+ }
303
315
  if (matchBound) {
304
316
  bV = new Uint8Array(rest.length + i - start)
305
317
  bV.set(rest)
@@ -323,7 +335,7 @@ async function* $streamToMultipartForm(data: ReadableStream<Uint8Array>, boundar
323
335
  start = i + bound.length
324
336
  rest = new Uint8Array()
325
337
  i = start
326
- } else if (chunk[i] === 0x0d && chunk[i + 1] === 0x0a && chunk[i + 2] === 0x0d) {
338
+ } else if (matchDelimiter) {
327
339
  bK = new Uint8Array(rest.length + i - start)
328
340
  bK.set(rest)
329
341
  bK.set(chunk.slice(start, i), rest.length)
@@ -594,7 +606,7 @@ export const requestPathParser = (input: string, path: string) => {
594
606
  }
595
607
 
596
608
  export const parseEntry = <T extends STProps>(
597
- params: { [key: string]: string | string[] },
609
+ params: { [key: string]: any },
598
610
  schema: T,
599
611
  options?: { name?: string; i?: boolean }
600
612
  ): Static<STObject<T>> => {
@@ -629,16 +641,24 @@ export const parseEntry = <T extends STProps>(
629
641
  return parsedParams as Static<STObject<T>>
630
642
  }
631
643
 
632
- const isIterator = (obj: any) => typeof obj?.next === 'function'
633
-
634
- export const responseParser = (response: any, ctx: Context) => {
644
+ export const responseParser = (response: any, ctx: Context, schema?: STResponse) => {
635
645
  const details = {
636
646
  status: ctx.set.status || 200,
637
647
  headers: new Headers(ctx.set.headers)
638
648
  }
639
649
  if (response instanceof Response) return response
640
650
  else if (typeof response === 'string') {
641
- if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'text/plain')
651
+ if (!details?.headers?.has('content-type')) {
652
+ if (schema?.[details.status][Kind] === 'json') {
653
+ details?.headers?.set('content-type', 'application/json')
654
+ response = `"${response}"`
655
+ } else details?.headers?.set('content-type', 'text/plain')
656
+ }
657
+ return new Response(response, details)
658
+ } else if (response instanceof Uint8Array) {
659
+ if (!details?.headers?.has('content-type')) {
660
+ details?.headers?.set('content-type', 'application/octet-stream')
661
+ }
642
662
  return new Response(response, details)
643
663
  }
644
664
  if (response instanceof ReadableStream) {
@@ -667,7 +687,8 @@ export const responseParser = (response: any, ctx: Context) => {
667
687
  } else {
668
688
  try {
669
689
  if (!details?.headers?.has('content-type')) details?.headers?.set('content-type', 'application/json')
670
- return new Response(JSON.stringify(response), details)
690
+ if (details.headers.get('content-type') === 'application/json') response = JSON.stringify(response)
691
+ return new Response(response, details)
671
692
  } catch (error) {
672
693
  console.error(error)
673
694
  throw new InternalError()
package/src/router.ts CHANGED
@@ -1,39 +1,34 @@
1
- import type { Route, RouteNode, RouteTree } from './types'
2
- import { NotFoundError } from './types'
1
+ import type { Method, Route, RouteNode } from './types'
2
+ import { MethodNotAllowedError, NotFoundError } from './types'
3
3
 
4
4
  const ROUTE_REGEX = /^(\/(\*|:?\d+|:?\w+|:?[\w\d][\w-]+[\w\d]))*\/?$/
5
5
 
6
- const walkRoutes = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
6
+ const walk = (path: string[], node: RouteNode, alts: RouteNode[] = []): RouteNode => {
7
7
  if (path.length < 1) throw new NotFoundError()
8
- if (path.length === 1 && node.route) return node
8
+ if (path.length === 1 && node.routes && !!Object.keys(node.routes).length) return node
9
9
 
10
10
  if (node.children?.['*']) alts.push(node.children['*'])
11
11
  if (node.param) alts.push(node.param)
12
12
 
13
13
  if (node.children && path[1] in node.children) {
14
14
  path.shift()
15
- return walkRoutes(path, node.children[path[0]], alts)
15
+ return walk(path, node.children[path[0]], alts)
16
16
  }
17
17
  if (node.param) {
18
18
  path.shift()
19
19
  alts.pop()
20
- try {
21
- return walkRoutes(path, node.param, alts)
22
- } catch (error) {
23
- if (error instanceof NotFoundError) console.log(error)
24
- else throw error
25
- }
20
+ return walk(path, node.param, alts)
26
21
  }
27
22
  if (alts.length > 1) {
28
- return walkRoutes(path, alts.pop() as RouteNode, alts)
23
+ return walk(path, alts.pop() as RouteNode, alts)
29
24
  }
30
25
  if (alts.length === 1) {
31
26
  let lastAlt = alts.pop() as RouteNode
32
27
  try {
33
- return walkRoutes(path, lastAlt, alts)
28
+ return walk(path, lastAlt, alts)
34
29
  } catch (error) {
35
30
  if (error instanceof NotFoundError) {
36
- if (lastAlt?.route) return lastAlt
31
+ if (lastAlt?.routes) return lastAlt
37
32
  } else throw error
38
33
  }
39
34
  }
@@ -42,12 +37,12 @@ const walkRoutes = (path: string[], node: RouteNode, alts: RouteNode[] = []): Ro
42
37
  }
43
38
 
44
39
  export class GalbeRouter {
45
- routes: RouteTree
40
+ routes: RouteNode
46
41
  prefix: string
47
42
  cacheEnabled: boolean
48
43
  cachedRoutes: Map<string, Route | null>
49
44
  constructor(options?: { prefix?: string; cacheEnabled?: boolean }) {
50
- this.routes = { GET: {}, POST: {}, PUT: {}, PATCH: {}, DELETE: {}, OPTIONS: {} }
45
+ this.routes = { routes: {} }
51
46
  let prefix = options?.prefix || ''
52
47
  if (prefix && !prefix.match(/^\//)) prefix = `/${prefix}`
53
48
  this.prefix = prefix
@@ -62,42 +57,46 @@ export class GalbeRouter {
62
57
  route.path = `${this.prefix || ''}${route.path}`
63
58
  let path = route.path.replace(/^\/$(.*)\/?$/, '$1').split('/')
64
59
  path.shift()
65
- let r = this.routes[route.method.toUpperCase()]
60
+ let r = this.routes
66
61
  if (!path.length) {
67
- r.route = route
62
+ r.routes[route.method] = route
68
63
  } else {
69
64
  while (path.length) {
70
65
  let p = path.shift()
71
66
  if (p === undefined) break
72
67
  if (!path.length) {
73
- if (p.match(/^:/)) r.param = { route }
74
- else {
68
+ if (p.match(/^:/)) {
69
+ if (!r.param) r.param = { routes: {} }
70
+ r.param.routes[route.method] = route
71
+ } else {
75
72
  if (!r.children) r.children = {}
76
- r.children[p] = { ...r.children[p], route }
73
+ if (!(p in r.children)) r.children[p] = { routes: {} }
74
+ r.children[p].routes[route.method] = route
77
75
  }
78
76
  } else {
79
77
  if (p.match(/^:/)) {
80
- if (!r.param) r.param = {}
78
+ if (!r.param) r.param = { routes: {} }
81
79
  r = r.param
82
80
  } else {
83
81
  if (!r.children) r.children = {}
84
- if (!(p in r.children)) r.children[p] = {}
82
+ if (!(p in r.children)) r.children[p] = { routes: {} }
85
83
  r = r.children[p]
86
84
  }
87
85
  }
88
86
  }
89
87
  }
90
88
  }
91
- find(method: string, path: string) {
89
+ find(method: Method, path: string): Route {
92
90
  const staticRoute = this.cachedRoutes.get(`[${method}]${path}`)
93
91
  if (staticRoute === null) throw new NotFoundError()
94
92
  if (staticRoute !== undefined) return staticRoute
95
- let parts = path.split('/')
96
- const route = walkRoutes(parts, this.routes[method]).route
97
- if (!route) {
93
+ let parts = path === '/' ? [''] : path.split('/')
94
+ let r = walk(parts, this.routes)
95
+ if (!r || !Object.keys(r.routes).length) {
98
96
  if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, null)
99
97
  throw new NotFoundError()
100
- }
98
+ } else if (!(method in r.routes)) throw new MethodNotAllowedError()
99
+ const route = r.routes[method] as Route
101
100
  if (this.cacheEnabled) this.cachedRoutes.set(`[${method}]${path}`, route)
102
101
  return route
103
102
  }
package/src/routes.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { GalbeConfig } from './types'
1
+ import type { GalbeConfig, Route } from './types'
2
2
 
3
3
  import { readdir, lstat } from 'fs/promises'
4
4
  import { extname } from 'path'
@@ -10,14 +10,116 @@ import { Glob } from 'bun'
10
10
 
11
11
  export const DEFAULT_ROUTE_PATTERN = 'src/**/*.route.{js,ts}'
12
12
 
13
- export type RouteMeta = {
13
+ export type RouteMeta = { head?: string } & Record<string, boolean | string | string[]>
14
+ export type RoutesMeta = {
14
15
  header: Record<string, boolean | string | string[]>
15
- routes: Record<string, Record<string, Record<string, boolean | string | string[]>>>
16
+ routes: Record<string, Record<string, RouteMeta>>
16
17
  }
17
-
18
+ export type RouteInstanciationCallback = <T extends 'add' | 'error'>(event: {
19
+ type: T
20
+ error?: T extends 'error' ? any : undefined
21
+ route: T extends 'add' ? Route : undefined
22
+ filepath?: string
23
+ meta: T extends 'add' ? RouteMeta : undefined
24
+ }) => any | Promise<any>
18
25
  export type RouteFileMeta = {
19
26
  file: string
20
- } & RouteMeta
27
+ } & RoutesMeta
28
+
29
+ class GalbeProxy {
30
+ #g: Galbe
31
+ #cb?: RouteInstanciationCallback
32
+ filepath?: string
33
+ meta?: RoutesMeta
34
+ constructor(g: Galbe, cb?: RouteInstanciationCallback) {
35
+ this.#g = g
36
+ this.#cb = cb
37
+ }
38
+ async get(...args: any[]) {
39
+ //@ts-ignore
40
+ const route = this.#g.get(...args) as Route
41
+ if (this.#cb)
42
+ await this.#cb({
43
+ type: 'add',
44
+ route,
45
+ filepath: this.filepath || '',
46
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
47
+ })
48
+ return route
49
+ }
50
+ async post(...args: any[]) {
51
+ //@ts-ignore
52
+ const route = this.#g.post(...args) as Route
53
+ if (this.#cb)
54
+ await this.#cb({
55
+ type: 'add',
56
+ route,
57
+ filepath: this.filepath,
58
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
59
+ })
60
+ return route
61
+ }
62
+ async put(...args: any[]) {
63
+ //@ts-ignore
64
+ const route = this.#g.put(...args) as Route
65
+ if (this.#cb)
66
+ await this.#cb({
67
+ type: 'add',
68
+ route,
69
+ filepath: this.filepath,
70
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
71
+ })
72
+ return route
73
+ }
74
+ async patch(...args: any[]) {
75
+ //@ts-ignore
76
+ const route = this.#g.patch(...args) as Route
77
+ if (this.#cb)
78
+ await this.#cb({
79
+ type: 'add',
80
+ route,
81
+ filepath: this.filepath,
82
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
83
+ })
84
+ return route
85
+ }
86
+ async delete(...args: any[]) {
87
+ //@ts-ignore
88
+ const route = this.#g.delete(...args) as Route
89
+ if (this.#cb)
90
+ await this.#cb({
91
+ type: 'add',
92
+ route,
93
+ filepath: this.filepath,
94
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
95
+ })
96
+ return route
97
+ }
98
+ async options(...args: any[]) {
99
+ //@ts-ignore
100
+ const route = this.#g.options(...args) as Route
101
+ if (this.#cb)
102
+ await this.#cb({
103
+ type: 'add',
104
+ route,
105
+ filepath: this.filepath,
106
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
107
+ })
108
+ return route
109
+ }
110
+ async head(...args: any[]) {
111
+ //@ts-ignore
112
+ const route = this.#g.head(...args) as Route
113
+ if (this.#cb)
114
+ await this.#cb({
115
+ type: 'add',
116
+ route,
117
+ filepath: this.filepath,
118
+ meta: this.meta?.routes?.[route.path]?.[route.method] || {}
119
+ })
120
+ return route
121
+ }
122
+ }
21
123
 
22
124
  const parseComment = (comment: string): Record<string, string | string[]> => {
23
125
  const head =
@@ -40,11 +142,11 @@ const parseComment = (comment: string): Record<string, string | string[]> => {
40
142
  }
41
143
  return refs
42
144
  }
43
- export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
145
+ export const metaAnalysis = async (filePath: string): Promise<RoutesMeta> => {
44
146
  const file = Bun.file(filePath)
45
147
  const fileExt = extname(filePath)
46
148
  let content = await file.text()
47
- let meta: RouteMeta = { header: {}, routes: {} }
149
+ let meta: RoutesMeta = { header: {}, routes: {} }
48
150
 
49
151
  if (fileExt === '.ts') {
50
152
  //// Much faster but doesn't include comments. See https://github.com/oven-sh/bun/pull/7055
@@ -69,25 +171,31 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
69
171
  }).code
70
172
  }
71
173
 
72
- const comments: Record<number, string> = {}
174
+ const comments: Record<number, Record<number, string>> = {}
73
175
 
74
176
  const ast = parse(content, {
75
177
  ecmaVersion: 'latest',
76
178
  sourceType: 'module',
77
179
  locations: true,
78
180
  onComment: (isBlock, text, _start, _end, _locStart, locEnd) => {
79
- if (isBlock && locEnd?.line) comments[locEnd?.line] = text
181
+ if (isBlock && locEnd?.line !== undefined && locEnd?.column !== undefined) {
182
+ if (!comments?.[locEnd.line]) comments[locEnd.line] = []
183
+ comments[locEnd.line][locEnd.column] = text
184
+ }
80
185
  }
81
186
  })
82
187
  simple(ast, {
83
188
  ExportDefaultDeclaration(node) {
84
- // @ts-ignore
85
- let galbeIdentifier = node.declaration.params[0].name
86
-
87
- const headerLine = node.loc?.start.line
88
- const headerCom = headerLine !== undefined && comments?.[headerLine] ? comments[headerLine] : ''
189
+ const headerLine = node.loc?.start.line || -1
190
+ const headerCol = node.loc?.start.column || -1
191
+ const headerCom = comments?.[headerLine]?.[headerCol - 1] ? comments[headerLine][headerCol - 1] : ''
89
192
  const headerRef = parseComment(headerCom)
90
193
  meta.header = headerRef
194
+
195
+ // @ts-ignore
196
+ let galbeIdentifier = node.declaration?.params?.[0]?.name
197
+ if (!galbeIdentifier) return meta
198
+
91
199
  // @ts-ignore
92
200
  simple(node.declaration.body, {
93
201
  CallExpression(node) {
@@ -97,8 +205,9 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
97
205
  const path = node.arguments[0].value
98
206
  // @ts-ignore
99
207
  const method = node.callee.property.name
100
- const line = node.loc?.start.line
101
- const com = line !== undefined && comments?.[line] ? comments[line] : ''
208
+ const line = node.loc?.start.line || -1
209
+ const col = node.loc?.start.column || -1
210
+ const com = comments?.[line]?.[col - 1] ? comments[line][col - 1] : ''
102
211
 
103
212
  const routeRefs = parseComment(com)
104
213
 
@@ -112,48 +221,41 @@ export const metaAnalysis = async (filePath: string): Promise<RouteMeta> => {
112
221
  return meta
113
222
  }
114
223
 
115
- const importRoutes = async (filePath: string, galbe: Galbe) => {
116
- const routes = (await import(filePath)).default
117
- routes(galbe)
118
- }
119
-
120
- export const defineRoutes = async (options: GalbeConfig, galbe: Galbe) => {
224
+ export const defineRoutes = async (
225
+ options: Pick<GalbeConfig, 'routes'>,
226
+ galbe: Galbe,
227
+ cb?: RouteInstanciationCallback
228
+ ) => {
121
229
  const routes = options?.routes === true ? DEFAULT_ROUTE_PATTERN : options?.routes
122
- if (!routes) {
123
- console.log(`\x1b\[38;5;245m No route file defined\x1b[0m`)
124
- return
125
- }
230
+ const proxy = new GalbeProxy(galbe, cb)
231
+ if (!routes) return
126
232
  const root = process.cwd()
127
233
  if (typeof routes === 'string') {
128
- let noRouteFound = true
129
234
  for await (const path of new Glob(routes).scan({ cwd: root, absolute: true, onlyFiles: false })) {
130
- noRouteFound = false
131
235
  const isDir = (await lstat(path)).isDirectory()
132
236
 
133
237
  let files: string[] = []
134
238
  if (!isDir) files.push(path)
135
239
  else files = files.concat((await readdir(path)).map(f => `${path}/${f}`))
136
- if (files.length === 0) console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
137
240
  for (const f of files) {
138
241
  try {
139
242
  const metadata = await metaAnalysis(f)
243
+ proxy.filepath = f
244
+ proxy.meta = metadata
140
245
  galbe.meta?.push({ file: path, ...metadata })
141
- console.log(`\n\x1b\[0;36m ${f}\x1b[0m`)
142
- await importRoutes(f, galbe)
143
- } catch (err) {
144
- // console.log(`\x1b\[0;31m ${f}\x1b[0m`)
145
- throw err
246
+ const imported = await import(f)
247
+ if (!imported?.default) throw new Error('No default export function')
248
+ if (typeof imported.default !== 'function') throw new Error('Default export must be a function')
249
+ const routes = imported.default
250
+ routes(proxy)
251
+ } catch (err: any) {
252
+ if (cb) await cb({ type: 'error', error: err, filepath: f, route: undefined, meta: undefined })
146
253
  }
147
254
  }
148
255
  }
149
- if (noRouteFound) {
150
- process.stdout.write('\r\x1b[K')
151
- console.log(`\x1b\[38;5;245m No route found\x1b[0m`)
152
- return
153
- }
154
256
  } else if (Array.isArray(routes)) {
155
257
  for (const r of routes) {
156
- await defineRoutes({ routes: r }, galbe)
258
+ await defineRoutes({ routes: r }, galbe, cb)
157
259
  }
158
260
  }
159
261
  }
package/src/schema.ts CHANGED
@@ -3,6 +3,7 @@ export const Optional = Symbol.for('Galbe.SchemaType.Optional')
3
3
  export const Stream = Symbol.for('Galbe.SchemaType.Stream')
4
4
 
5
5
  export interface Options {
6
+ id?: string
6
7
  title?: string
7
8
  description?: string
8
9
  default?: any
@@ -38,11 +39,11 @@ export interface STSchema extends Options {
38
39
  | 'literal'
39
40
  | 'array'
40
41
  | 'object'
42
+ | 'json'
41
43
  | 'urlForm'
42
44
  | 'multipartForm'
43
45
  | 'any'
44
46
  | 'union'
45
- | 'stream'
46
47
  [Optional]?: boolean
47
48
  [Stream]?: boolean
48
49
  params: unknown[]
@@ -63,6 +64,16 @@ export type STPropsValue =
63
64
  export type STProps = Record<string | number, STPropsValue>
64
65
 
65
66
  type Evaluate<T> = T extends infer O ? { [K in keyof O]: O[K] } : never
67
+
68
+ /**
69
+ * Infer the static TypeScript type from a {@link https://galbe.dev/documentation/schemas#schema-types Schema Type}
70
+ * @example
71
+ * ```ts
72
+ * const schema = $T.object({ foo: $T.string() })
73
+ * type T = Static<typeof schema>
74
+ * // ^? type T = { foo: string }
75
+ * ```
76
+ */
66
77
  export type Static<T extends STSchema, P extends unknown[] = unknown[]> = (T & { params: P })['static']
67
78
 
68
79
  // Utils
@@ -162,6 +173,11 @@ export interface STObject<T extends STProps = STProps> extends STSchema {
162
173
  static: ObjectStatic<T, this['params']>
163
174
  props: T
164
175
  }
176
+ export interface STJson<T extends STBoolean | STNumber | STString | STObject = any> extends STSchema {
177
+ [Kind]: 'json'
178
+ type: 'boolean' | 'number' | 'string' | 'object' | 'unknown'
179
+ static: Static<T>
180
+ }
165
181
  type ObjectStatic<T extends STProps, P extends unknown[]> = ObjectStaticProps<T, { [K in keyof T]: Static<T[K], P> }>
166
182
  type OptionalPropertyKeys<T extends STProps> = {
167
183
  [K in keyof T]: T[K] extends STOptional<STSchema> ? K : never
@@ -180,6 +196,13 @@ function _Object<T extends STProps>(properties?: T, options: Options = {}): STOb
180
196
  ? { ...options, [Kind]: 'object', props: clonedProperties, required: requiredKeys }
181
197
  : { ...options, [Kind]: 'object', props: clonedProperties }) as unknown as STObject<T>
182
198
  }
199
+ function _Json<T extends STBoolean | STNumber | STString | STObject>(value?: T, options: Options = {}): STJson<T> {
200
+ if (value?.[Kind] === 'boolean') return { ..._Bool(options), [Kind]: 'json', type: 'boolean' }
201
+ if (value?.[Kind] === 'number') return { ..._Number(options), [Kind]: 'json', type: 'number' }
202
+ if (value?.[Kind] === 'string') return { ..._String(options), [Kind]: 'json', type: 'string' }
203
+ if (value?.[Kind] === 'object') return { ..._Object(value.props, options), [Kind]: 'json', type: 'object' }
204
+ throw Error('Invalid Json type definition')
205
+ }
183
206
 
184
207
  // UrlForm
185
208
  export type STUrlFormValues =
@@ -221,7 +244,19 @@ export interface MultipartFormData<K extends string = string, V extends Static<S
221
244
  }
222
245
  export interface STMultipartForm<T extends STProps = STProps> extends STSchema {
223
246
  [Kind]: 'multipartForm'
224
- static: T extends undefined ? Record<string, any> : ObjectStatic<T, this['params']>
247
+ static: T extends undefined
248
+ ? {
249
+ [k: string]: {
250
+ headers: { type?: string; name: string; filename?: string }
251
+ content: Static<STMultipartFormValues>
252
+ }
253
+ }
254
+ : {
255
+ [K in keyof T]: {
256
+ headers: { type?: string; name: K; filename?: string }
257
+ content: Static<T[K]>
258
+ }
259
+ }
225
260
  props: T
226
261
  }
227
262
  function _MultipartForm<T extends STProps>(properties?: T, options: Options = {}): STMultipartForm<T> {
@@ -241,12 +276,12 @@ export interface STArray<T extends STSchema = STSchema> extends STSchema {
241
276
  static: Static<T>[]
242
277
  items: T
243
278
  }
244
- export function _Array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T | STAny> {
279
+ export function _Array<T extends STSchema>(schema?: T, options: ArrayOptions = {}): STArray<T> {
245
280
  return {
246
281
  ...options,
247
282
  [Kind]: 'array',
248
283
  items: schema ?? _Any()
249
- } as unknown as T extends undefined ? STArray<STAny> : STArray<T>
284
+ } as unknown as STArray<T>
250
285
  }
251
286
 
252
287
  // Union
@@ -313,9 +348,12 @@ export class SchemaType {
313
348
  public object<T extends STProps>(properties?: T, options: Options = {}): STObject<T> {
314
349
  return _Object(properties, options)
315
350
  }
316
- /** Alias for Object Schema Type */
317
- public json<T extends STProps>(properties?: T, options: Options = {}): STObject<T> {
318
- return _Object(properties, options)
351
+ /** Creates a JSON Schema Type */
352
+ public json<T extends STString | STBoolean | STNumber | STObject<STProps>>(
353
+ value: T,
354
+ options: Options = {}
355
+ ): STJson<T> {
356
+ return _Json(value, options)
319
357
  }
320
358
  /** Creates an UrlForm Schema Type */
321
359
  public urlForm<T extends STUrlFormProps>(properties?: T, options: Options = {}): STUrlForm<T> {
@@ -326,7 +364,7 @@ export class SchemaType {
326
364
  return _MultipartForm(properties, options)
327
365
  }
328
366
  /** Crates an Array Schema Type */
329
- public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T | STAny> {
367
+ public array<T extends STSchema>(schema?: T, options: Options = {}): STArray<T> {
330
368
  return _Array(schema, options)
331
369
  }
332
370
  /** Crates an Union Schema Type */
@@ -376,3 +414,36 @@ export class SchemaType {
376
414
  return _Stream(schema)
377
415
  }
378
416
  }
417
+
418
+ export const schemaToTypeStr = (schema: STSchema): string => {
419
+ let type = 'unknown'
420
+ let kind = schema[Kind]
421
+
422
+ if (kind === 'boolean') type = 'boolean'
423
+ else if (kind === 'byteArray') type = 'Uint8Array'
424
+ else if (kind === 'number') type = 'number'
425
+ else if (kind === 'integer') type = 'number'
426
+ else if (kind === 'string') type = 'string'
427
+ else if (kind === 'any') type = 'any'
428
+ else if (kind === 'literal') {
429
+ let value = (schema as STLiteral).value
430
+ if (typeof value === 'string') type = `'${value}'`
431
+ else type = String(value)
432
+ } else if (kind === 'array') {
433
+ type = `Array<${schemaToTypeStr((schema as STArray).items)}>`
434
+ } else if (kind === 'object') {
435
+ let props = (schema as STObject).props
436
+ type = `{${Object.entries(props)
437
+ .map(([k, v]) => `${typeof k === 'string' ? `'${k}'` : k}:${schemaToTypeStr(v)}`)
438
+ .join(';')}}`
439
+ } else if (kind === 'json') {
440
+ type = `Json<${schemaToTypeStr({ ...schema, [Kind]: schema.type })}>`
441
+ } else if (kind === 'union') {
442
+ let anyOf = (schema as STUnion).anyOf
443
+ type = anyOf.map(s => schemaToTypeStr(s)).join('|')
444
+ }
445
+
446
+ if (schema[Optional]) type = `${type}|undefined`
447
+
448
+ return type
449
+ }