(raw: unknown, schema: JsonSchemaNode | undefined)
| 96 | * - recurses into object properties and array items |
| 97 | */ |
| 98 | export function coerceArgsToSchema(raw: unknown, schema: JsonSchemaNode | undefined): unknown { |
| 99 | if (!schema) return raw; |
| 100 | const t = primaryType(schema); |
| 101 | |
| 102 | if (t === 'object' || (!t && schema.properties)) { |
| 103 | let val: unknown = raw; |
| 104 | if (typeof val === 'string') { |
| 105 | const parsed = tryParseJson(val); |
| 106 | if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) val = parsed; |
| 107 | else return raw; |
| 108 | } |
| 109 | if (val == null || typeof val !== 'object' || Array.isArray(val)) return val; |
| 110 | const props = schema.properties ?? {}; |
| 111 | const out: Record<string, unknown> = { ...(val as Record<string, unknown>) }; |
| 112 | for (const key of Object.keys(props)) { |
| 113 | if (key in out) out[key] = coerceArgsToSchema(out[key], props[key]); |
| 114 | } |
| 115 | return out; |
| 116 | } |
| 117 | |
| 118 | if (t === 'array') { |
| 119 | let val: unknown = raw; |
| 120 | if (typeof val === 'string') { |
| 121 | const parsed = tryParseJson(val); |
| 122 | // Only adopt a parsed value when it's actually an array; otherwise keep the raw |
| 123 | // string so the wrap-as-single-element step below turns "a.ts" into ["a.ts"]. |
| 124 | if (Array.isArray(parsed)) val = parsed; |
| 125 | } |
| 126 | if (!Array.isArray(val)) { |
| 127 | // A lone scalar where an array was expected — wrap it. This is the single most |
| 128 | // common local-model array mistake (`"paths": "a.ts"` for a list of paths). |
| 129 | if (val == null) return val; |
| 130 | val = [val]; |
| 131 | } |
| 132 | const items = schema.items; |
| 133 | return (val as unknown[]).map(v => coerceArgsToSchema(v, items)); |
| 134 | } |
| 135 | |
| 136 | if (t === 'number' || t === 'integer') { |
| 137 | if (typeof raw === 'string' && raw.trim() !== '' && Number.isFinite(Number(raw))) { |
| 138 | const n = Number(raw); |
| 139 | return t === 'integer' ? Math.trunc(n) : n; |
| 140 | } |
| 141 | return raw; |
| 142 | } |
| 143 | |
| 144 | if (t === 'boolean') { |
| 145 | if (raw === 'true' || raw === 'True' || raw === '1') return true; |
| 146 | if (raw === 'false' || raw === 'False' || raw === '0') return false; |
| 147 | return raw; |
| 148 | } |
| 149 | |
| 150 | if (t === 'string') { |
| 151 | if (typeof raw === 'number' || typeof raw === 'boolean') return String(raw); |
| 152 | return raw; |
| 153 | } |
| 154 | |
| 155 | return raw; |
no test coverage detected