File size: 5,487 Bytes
1477a90 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 | import type { ZodType } from 'zod'
type ZodDef = {
type: string
innerType?: ZodType
valueType?: ZodType
element?: ZodType
options?: ZodType[]
entries?: Record<string, string>
}
function def(schema: ZodType | undefined): ZodDef | undefined {
return (schema as { def?: ZodDef } | undefined)?.def
}
function shapeOf(
schema: ZodType | undefined,
): Record<string, ZodType> | undefined {
return (schema as { shape?: Record<string, ZodType> } | undefined)?.shape
}
/**
* A value satisfying a schema's structure, with camelCase object keys (the emitted
* schemas are camelCase) and a deliberately underscore-containing record key so the
* casing assertion can tell a leaked field key from a preserved user key.
*
* Every field whose sample is defined is populated (so optional camelCase fields are
* exercised). A key whose sample comes back undefined is omitted rather than set to
* undefined, so a discriminated-union variant always carries its discriminator
* literal and the mapper can resolve every variant it descends into. The depth cap
* bounds the self-referential filter schemas.
*/
export function sampleCamel(schema: ZodType | undefined, depth = 0): unknown {
const d = def(schema)
if (!d || depth > 12) {
return undefined
}
switch (d.type) {
case 'object': {
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(shapeOf(schema) ?? {})) {
const value = sampleCamel(v, depth + 1)
if (value !== undefined) {
out[k] = value
}
}
return out
}
case 'optional':
case 'nullable':
case 'default':
return sampleCamel(d.innerType, depth)
case 'array':
return [sampleCamel(d.element, depth + 1)]
case 'record':
return { user_key_a: sampleCamel(d.valueType, depth + 1) }
case 'union':
return sampleCamel(d.options?.[0], depth)
case 'literal':
return (schema as { value?: unknown }).value
case 'enum':
// Without this, enum fields fall out of every sample entirely — and a
// required-with-default enum (e.g. an invoice line's `category`) then
// breaks response round-trip identity, because toWire materializes the
// default the sample never carried.
return Object.values(d.entries ?? {})[0]
case 'string':
return 's'
case 'int':
case 'number':
return 1
case 'bigint':
return 1n
case 'boolean':
return true
case 'date':
return new Date(0)
default:
return undefined
}
}
// Snake-cased counterpart for response samples: object keys are snake_cased so
// fromWire has wire-shaped input. Record user keys and literal values are kept.
export function sampleSnake(schema: ZodType | undefined, depth = 0): unknown {
const value = sampleCamel(schema, depth)
return toSnakeKeys(value)
}
function toSnakeKeys(value: unknown): unknown {
if (value instanceof Date) {
// Wire samples carry the RFC 3339 string a JSON payload would.
return value.toISOString()
}
if (typeof value === 'bigint') {
// Wire samples carry the JSON number an int64 payload would.
return Number(value)
}
if (Array.isArray(value)) {
return value.map(toSnakeKeys)
}
if (value && typeof value === 'object') {
const out: Record<string, unknown> = {}
for (const [k, v] of Object.entries(value)) {
const key = k.startsWith('user_key_')
? k
: k.replace(/([A-Z])/g, (_m, c: string) => `_${c.toLowerCase()}`)
out[key] = toSnakeKeys(v)
}
return out
}
return value
}
/**
* Every object key in the value, except keys of a `user_key_*` map (preserved
* record keys) and date values. Used to assert no casing leaks past the mapper.
*/
export function collectFieldKeys(
value: unknown,
keys: string[] = [],
): string[] {
if (Array.isArray(value)) {
for (const item of value) {
collectFieldKeys(item, keys)
}
return keys
}
if (value && typeof value === 'object' && !(value instanceof Date)) {
for (const [k, v] of Object.entries(value)) {
if (k.startsWith('user_key_')) {
// Preserved user record key — not itself a schema field, so it's excluded
// from the leak check. Its value can still be model-shaped (e.g. a
// governance feature-access record value), so keep walking into it —
// skipping the value too would blind the leak check to casing bugs in
// any schema field nested under a record.
collectFieldKeys(v, keys)
continue
}
keys.push(k)
collectFieldKeys(v, keys)
}
}
return keys
}
/** Per-op schemas grouped by base name, from the generated schemas module. */
export function operationSchemaPairs(
schemas: Record<string, unknown>,
): Array<{ base: string; body?: ZodType; response?: ZodType }> {
const bases = new Map<string, { body?: ZodType; response?: ZodType }>()
for (const [name, schema] of Object.entries(schemas)) {
const bodyMatch = name.match(/^(.*)Body$/)
const responseMatch = name.match(/^(.*)Response$/)
if (bodyMatch) {
const entry = bases.get(bodyMatch[1]) ?? {}
entry.body = schema as ZodType
bases.set(bodyMatch[1], entry)
} else if (responseMatch) {
const entry = bases.get(responseMatch[1]) ?? {}
entry.response = schema as ZodType
bases.set(responseMatch[1], entry)
}
}
return [...bases.entries()].map(([base, v]) => ({ base, ...v }))
}
|