@lovrozagar/honey 0.5.1 → 0.5.3

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/tree.ts CHANGED
@@ -12,8 +12,8 @@ export type RouteHandler = {
12
12
  _skip?: boolean
13
13
  /** boundary error key — wraps undeclared/unexpected errors (null = use default or internal_server_error) */
14
14
  bek: string | null
15
- /** pre-computed error factory subset (null = use global factory) */
16
- ef: Record<string, (...args: never[]) => unknown> | null
15
+ /** pre-computed error factory subset (omit/`null` = use global factory) */
16
+ ef?: Record<string, (...args: never[]) => unknown> | null
17
17
  /** declared error keys for this route — used for runtime enforcement */
18
18
  ek: Set<string>
19
19
  /** handler function */
@@ -32,9 +32,9 @@ export type RouteHandler = {
32
32
  /** output schemas by content-type (null = no output validation) */
33
33
  os: OutputSchemaDef | null
34
34
  /** output validator function — validates response body against schema */
35
- ov: OutputValidator | null
36
- /** registered path pattern (e.g. "/users/:id") */
37
- rp: string
35
+ ov?: OutputValidator | null
36
+ /** registered path pattern (e.g. "/users/:id"); intern trees omit this */
37
+ rp?: string
38
38
  }
39
39
 
40
40
  export type { OutputSchemaDef }
@@ -108,399 +108,6 @@ export function createNode(): TreeNode {
108
108
  }
109
109
  }
110
110
 
111
- const UNSET_FN = null as unknown as RouteHandler["fn"]
112
-
113
- /** Dense array, or unique-value table + per-row indices when that JSON is smaller. */
114
- export type PackedColumn<T> = readonly T[] | { readonly t: readonly T[]; readonly x: readonly number[] }
115
-
116
- /** Columnar packed route table consumed by `assembleRouteTree`. */
117
- export type PackedRouteTable = {
118
- /** Boundary error key. A string applies to every route. */
119
- b?: Array<string | null> | string
120
- /** Per-route error-key lists. When `k` is set, each row is the extras beyond `k`. */
121
- e?: PackedColumn<readonly string[] | null>
122
- /** Per-route input schemas. */
123
- i?: PackedColumn<unknown>
124
- /** Error keys shared by every route; prepended onto each `e` row at inflate. */
125
- k?: readonly string[]
126
- /** Per-route meta objects. Keys are `u` indices when `u` is set. */
127
- m?: PackedColumn<Record<string, unknown> | null>
128
- /** Per-route output schemas. */
129
- o?: PackedColumn<unknown>
130
- /** Route selectors (`METHOD /path`), same order as the other columns. */
131
- s: readonly string[]
132
- /** Meta key dictionary; packed meta objects use decimal index keys. */
133
- u?: readonly string[]
134
- }
135
-
136
- const SCHEMA_KEY_PACK = new Map<string, string>([
137
- ["$ref", "$"],
138
- ["additionalItems", "ai"],
139
- ["additionalProperties", "ap"],
140
- ["allOf", "ao"],
141
- ["anyOf", "yo"],
142
- ["const", "c"],
143
- ["contains", "cs"],
144
- ["contentEncoding", "ce"],
145
- ["contentMediaType", "cm"],
146
- ["contentSchema", "cc"],
147
- ["default", "d"],
148
- ["definitions", "df"],
149
- ["dependentRequired", "dr"],
150
- ["dependentSchemas", "dh"],
151
- ["description", "ds"],
152
- ["else", "el"],
153
- ["enum", "en"],
154
- ["examples", "ex"],
155
- ["exclusiveMaximum", "xm"],
156
- ["exclusiveMinimum", "xn"],
157
- ["format", "f"],
158
- ["if", "if"],
159
- ["items", "it"],
160
- ["maxItems", "xi"],
161
- ["maxLength", "xl"],
162
- ["maxProperties", "xp"],
163
- ["maximum", "xa"],
164
- ["minItems", "ni"],
165
- ["minLength", "nl"],
166
- ["minProperties", "np"],
167
- ["minimum", "na"],
168
- ["multipleOf", "mo"],
169
- ["not", "nt"],
170
- ["nullable", "nu"],
171
- ["oneOf", "oo"],
172
- ["pattern", "pt"],
173
- ["patternProperties", "pp"],
174
- ["prefixItems", "pi"],
175
- ["properties", "p"],
176
- ["propertyNames", "pn"],
177
- ["required", "r"],
178
- ["then", "th"],
179
- ["title", "ti"],
180
- ["type", "t"],
181
- ["unevaluatedItems", "ue"],
182
- ["unevaluatedProperties", "up"],
183
- ["uniqueItems", "ui"],
184
- ])
185
-
186
- function invertStringMap(src: Map<string, string>): Map<string, string> {
187
- const out = new Map<string, string>()
188
- for (const [long, short] of src) out.set(short, long)
189
- return out
190
- }
191
-
192
- const SCHEMA_KEY_UNPACK = invertStringMap(SCHEMA_KEY_PACK)
193
-
194
- const SCHEMA_TYPE_PACK = new Map<string, string>([
195
- ["array", "a"],
196
- ["boolean", "b"],
197
- ["integer", "i"],
198
- ["null", "l"],
199
- ["number", "n"],
200
- ["object", "o"],
201
- ["string", "s"],
202
- ])
203
- const SCHEMA_TYPE_UNPACK = invertStringMap(SCHEMA_TYPE_PACK)
204
-
205
- const SCHEMA_FORMAT_PACK = new Map<string, string>([
206
- ["binary", "bin"],
207
- ["date", "d"],
208
- ["date-time", "dt"],
209
- ["email", "e"],
210
- ["hostname", "hn"],
211
- ["int32", "i3"],
212
- ["int64", "i6"],
213
- ["ipv4", "v4"],
214
- ["ipv6", "v6"],
215
- ["time", "tm"],
216
- ["uri", "u"],
217
- ["uri-reference", "ur"],
218
- ["uuid", "id"],
219
- ])
220
- const SCHEMA_FORMAT_UNPACK = invertStringMap(SCHEMA_FORMAT_PACK)
221
-
222
- const INPUT_KEY_PACK = new Map<string, string>([
223
- ["cookies", "c"],
224
- ["form", "f"],
225
- ["headers", "h"],
226
- ["json", "j"],
227
- ["params", "p"],
228
- ["search", "q"],
229
- ])
230
- const INPUT_KEY_UNPACK = invertStringMap(INPUT_KEY_PACK)
231
-
232
- const OUTPUT_CT_PACK = new Map<string, string>([
233
- ["application/cbor", "cb"],
234
- ["application/json", "j"],
235
- ["application/msgpack", "mp"],
236
- ["application/octet-stream", "os"],
237
- ["application/pdf", "pf"],
238
- ["application/xml", "xm"],
239
- ["redirect", "rd"],
240
- ["text/csv", "csv"],
241
- ["text/event-stream", "sse"],
242
- ["text/html", "ht"],
243
- ["text/plain", "tp"],
244
- ])
245
- const OUTPUT_CT_UNPACK = invertStringMap(OUTPUT_CT_PACK)
246
-
247
- const SCHEMA_NAME_CHILDREN = new Set(["$defs", "definitions", "dependentSchemas", "patternProperties", "properties"])
248
- const SCHEMA_NODE_CHILDREN = new Set([
249
- "$defs",
250
- "additionalItems",
251
- "additionalProperties",
252
- "allOf",
253
- "anyOf",
254
- "contains",
255
- "contentSchema",
256
- "definitions",
257
- "dependentSchemas",
258
- "else",
259
- "if",
260
- "items",
261
- "not",
262
- "oneOf",
263
- "patternProperties",
264
- "prefixItems",
265
- "properties",
266
- "propertyNames",
267
- "then",
268
- "unevaluatedItems",
269
- "unevaluatedProperties",
270
- ])
271
-
272
- function isPlainObject(value: unknown): value is Record<string, unknown> {
273
- return value !== null && typeof value === "object" && !Array.isArray(value)
274
- }
275
-
276
- function remapLeaf(value: unknown, map: Map<string, string>): unknown {
277
- if (typeof value === "string") return map.get(value) ?? value
278
- if (Array.isArray(value)) {
279
- return value.map((item) => (typeof item === "string" ? (map.get(item) ?? item) : item))
280
- }
281
- return value
282
- }
283
-
284
- function isNullTypeSchema(value: unknown): boolean {
285
- return isPlainObject(value) && Object.keys(value).length === 1 && value.type === "null"
286
- }
287
-
288
- function packJsonSchema(node: unknown): unknown {
289
- if (Array.isArray(node)) return node.map(packJsonSchema)
290
- if (!isPlainObject(node)) return node
291
-
292
- const oneOf = node.oneOf
293
- if (Array.isArray(oneOf) && oneOf.length === 2 && Object.keys(node).length === 1) {
294
- if (isPlainObject(oneOf[0]) && isNullTypeSchema(oneOf[1])) {
295
- const packed = packJsonSchema(oneOf[0])
296
- if (isPlainObject(packed)) {
297
- packed.n1 = 1
298
- return packed
299
- }
300
- }
301
- if (isNullTypeSchema(oneOf[0]) && isPlainObject(oneOf[1])) {
302
- const packed = packJsonSchema(oneOf[1])
303
- if (isPlainObject(packed)) {
304
- packed.n1 = 0
305
- return packed
306
- }
307
- }
308
- }
309
-
310
- const out: Record<string, unknown> = Object.create(null)
311
- for (const [key, value] of Object.entries(node)) {
312
- const nextKey = SCHEMA_KEY_PACK.get(key) ?? key
313
- if (key === "type") {
314
- out[nextKey] = remapLeaf(value, SCHEMA_TYPE_PACK)
315
- continue
316
- }
317
- if (key === "format") {
318
- out[nextKey] = remapLeaf(value, SCHEMA_FORMAT_PACK)
319
- continue
320
- }
321
- if (SCHEMA_NAME_CHILDREN.has(key) && isPlainObject(value)) {
322
- const inner: Record<string, unknown> = Object.create(null)
323
- for (const [name, child] of Object.entries(value)) inner[name] = packJsonSchema(child)
324
- out[nextKey] = inner
325
- continue
326
- }
327
- if (SCHEMA_NODE_CHILDREN.has(key)) {
328
- out[nextKey] = packJsonSchema(value)
329
- continue
330
- }
331
- out[nextKey] = value
332
- }
333
- return out
334
- }
335
-
336
- function unpackJsonSchema(node: unknown): unknown {
337
- if (Array.isArray(node)) return node.map(unpackJsonSchema)
338
- if (!isPlainObject(node)) return node
339
-
340
- if (Object.hasOwn(node, "n1")) {
341
- const copy: Record<string, unknown> = Object.create(null)
342
- for (const [key, value] of Object.entries(node)) {
343
- if (key !== "n1") copy[key] = value
344
- }
345
- const rest = unpackJsonSchema(copy)
346
- const nullBranch = { type: "null" }
347
- return node.n1 === 0 ? { oneOf: [nullBranch, rest] } : { oneOf: [rest, nullBranch] }
348
- }
349
-
350
- const out: Record<string, unknown> = Object.create(null)
351
- for (const [key, value] of Object.entries(node)) {
352
- const nextKey = SCHEMA_KEY_UNPACK.get(key) ?? key
353
- if (nextKey === "type") {
354
- out[nextKey] = remapLeaf(value, SCHEMA_TYPE_UNPACK)
355
- continue
356
- }
357
- if (nextKey === "format") {
358
- out[nextKey] = remapLeaf(value, SCHEMA_FORMAT_UNPACK)
359
- continue
360
- }
361
- if (SCHEMA_NAME_CHILDREN.has(nextKey) && isPlainObject(value)) {
362
- const inner: Record<string, unknown> = Object.create(null)
363
- for (const [name, child] of Object.entries(value)) inner[name] = unpackJsonSchema(child)
364
- out[nextKey] = inner
365
- continue
366
- }
367
- if (SCHEMA_NODE_CHILDREN.has(nextKey)) {
368
- out[nextKey] = unpackJsonSchema(value)
369
- continue
370
- }
371
- out[nextKey] = value
372
- }
373
- return out
374
- }
375
-
376
- export function packInputSchemas(iv: unknown): unknown {
377
- if (iv === null || typeof iv !== "object") return iv
378
- const out: Record<string, unknown> = Object.create(null)
379
- for (const [source, schema] of Object.entries(iv as Record<string, unknown>)) {
380
- out[INPUT_KEY_PACK.get(source) ?? source] = packJsonSchema(schema)
381
- }
382
- return out
383
- }
384
-
385
- export function unpackInputSchemas(iv: unknown): unknown {
386
- if (iv === null || typeof iv !== "object") return iv
387
- const out: Record<string, unknown> = Object.create(null)
388
- for (const [source, schema] of Object.entries(iv as Record<string, unknown>)) {
389
- out[INPUT_KEY_UNPACK.get(source) ?? source] = unpackJsonSchema(schema)
390
- }
391
- return out
392
- }
393
-
394
- export function packOutputSchemas(os: unknown): unknown {
395
- if (os === null || typeof os !== "object") return os
396
- const out: Record<string, unknown> = Object.create(null)
397
- for (const [contentType, statuses] of Object.entries(os as Record<string, unknown>)) {
398
- const packedCt = OUTPUT_CT_PACK.get(contentType) ?? contentType
399
- if (contentType === "redirect" || statuses === null || typeof statuses !== "object") {
400
- out[packedCt] = statuses
401
- continue
402
- }
403
- const inner: Record<string, unknown> = Object.create(null)
404
- for (const [statusKey, schema] of Object.entries(statuses as Record<string, unknown>)) {
405
- inner[statusKey] = schema === true ? true : packJsonSchema(schema)
406
- }
407
- out[packedCt] = inner
408
- }
409
- return out
410
- }
411
-
412
- export function unpackOutputSchemas(os: unknown): unknown {
413
- if (os === null || typeof os !== "object") return os
414
- const out: Record<string, unknown> = Object.create(null)
415
- for (const [contentType, statuses] of Object.entries(os as Record<string, unknown>)) {
416
- const longCt = OUTPUT_CT_UNPACK.get(contentType) ?? contentType
417
- if (longCt === "redirect" || statuses === null || typeof statuses !== "object") {
418
- out[longCt] = statuses
419
- continue
420
- }
421
- const inner: Record<string, unknown> = Object.create(null)
422
- for (const [statusKey, schema] of Object.entries(statuses as Record<string, unknown>)) {
423
- inner[statusKey] = schema === true ? true : unpackJsonSchema(schema)
424
- }
425
- out[longCt] = inner
426
- }
427
- return out
428
- }
429
-
430
- function columnAt<T>(col: PackedColumn<T> | undefined, index: number, fallback: T): T {
431
- if (col === undefined) return fallback
432
- if (!Array.isArray(col) && "x" in col) {
433
- const value = col.t[col.x[index]]
434
- return (value === undefined ? fallback : value) as T
435
- }
436
- const value = col[index]
437
- return (value === undefined ? fallback : value) as T
438
- }
439
-
440
- function mapColumn<T, U>(col: PackedColumn<T> | undefined, fn: (value: T) => U): PackedColumn<U> | undefined {
441
- if (col === undefined) return undefined
442
- if (!Array.isArray(col) && "x" in col) {
443
- return { t: col.t.map(fn), x: col.x }
444
- }
445
- return (col as readonly T[]).map(fn)
446
- }
447
-
448
- function unpackMeta(
449
- mt: Record<string, unknown> | null,
450
- names: readonly string[] | undefined,
451
- ): Record<string, unknown> | null {
452
- if (mt === null || names === undefined) return mt
453
- const out: Record<string, unknown> = Object.create(null)
454
- for (const [key, value] of Object.entries(mt)) {
455
- const idx = Number(key)
456
- const name = Number.isInteger(idx) ? names[idx] : undefined
457
- out[name ?? key] = value
458
- }
459
- return out
460
- }
461
-
462
- /**
463
- * Inflate a packed route table into a live route tree.
464
- * Each handler and each `ek` Set is a unique object so `.routeTree()` patches
465
- * cannot cross-wire routes.
466
- */
467
- export function assembleRouteTree(table: PackedRouteTable): RouteTree {
468
- const root = createNode()
469
- const handlers: Record<string, RouteHandler> = Object.create(null)
470
- const meta: Record<string, Record<string, unknown>> = Object.create(null)
471
- const selectors = table.s
472
- const sharedEk = table.k ?? []
473
- const inputCol = mapColumn(table.i, unpackInputSchemas)
474
- const outputCol = mapColumn(table.o, unpackOutputSchemas)
475
- const metaCol = mapColumn(table.m, (mt) => unpackMeta(mt, table.u))
476
- for (let i = 0; i < selectors.length; i++) {
477
- const sel = selectors[i]
478
- const bek = typeof table.b === "string" ? table.b : (table.b?.[i] ?? null)
479
- const extra = columnAt(table.e, i, [] as readonly string[])
480
- const mt = columnAt(metaCol, i, null)
481
- const handler: RouteHandler = {
482
- bek,
483
- ef: null,
484
- ek: new Set(sharedEk.length === 0 ? (extra ?? []) : [...sharedEk, ...(extra ?? [])]),
485
- fn: UNSET_FN,
486
- iv: columnAt(inputCol, i, null) as RouteHandler["iv"],
487
- mt,
488
- mw: [],
489
- os: columnAt(outputCol, i, null) as RouteHandler["os"],
490
- ov: null,
491
- rp: "",
492
- }
493
- handlers[sel] = handler
494
- if (mt !== null) meta[sel] = mt
495
- const sp = sel.indexOf(" ")
496
- if (sp === -1) {
497
- throw new Error(`Invalid route selector: ${sel}`)
498
- }
499
- insertRoute(root, sel.slice(0, sp) as HttpMethod, sel.slice(sp + 1), handler)
500
- }
501
- return { handlers, meta, root }
502
- }
503
-
504
111
  function splitSegments(path: string): string[] {
505
112
  return path.split("/").filter((s) => s.length > 0)
506
113
  }