Spaces:
Paused
Paused
File size: 6,789 Bytes
e25a730 | 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 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 | /**
* Tuple schema conversion β bridges JSON Schema `prefixItems` (tuple) to
* object-based representation that Codex upstream accepts.
*
* Request side: convertTupleSchemas() rewrites prefixItems β properties with numeric keys
* Response side: reconvertTupleValues() restores {"0":β¦,"1":β¦} back to [β¦,β¦]
*/
type Schema = Record<string, unknown>;
function isRecord(v: unknown): v is Record<string, unknown> {
return typeof v === "object" && v !== null && !Array.isArray(v);
}
// ββ Detection ββββββββββββββββββββββββββββββββββββββββββββββββββββββ
/** Returns true if the schema tree contains any `prefixItems` node. */
export function hasTupleSchemas(schema: Schema): boolean {
return walk(schema, new Set());
}
function walk(node: Schema, seen: Set<object>): boolean {
if (seen.has(node)) return false;
seen.add(node);
if (Array.isArray(node.prefixItems)) return true;
// properties
if (isRecord(node.properties)) {
for (const v of Object.values(node.properties)) {
if (isRecord(v) && walk(v, seen)) return true;
}
}
// items
if (isRecord(node.items) && walk(node.items as Schema, seen)) return true;
// combinators
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
if (Array.isArray(node[key])) {
for (const entry of node[key] as unknown[]) {
if (isRecord(entry) && walk(entry, seen)) return true;
}
}
}
// $defs / definitions
for (const key of ["$defs", "definitions"] as const) {
if (isRecord(node[key])) {
for (const v of Object.values(node[key] as Schema)) {
if (isRecord(v) && walk(v, seen)) return true;
}
}
}
// conditional
for (const key of ["if", "then", "else", "not"] as const) {
if (isRecord(node[key]) && walk(node[key] as Schema, seen)) return true;
}
return false;
}
// ββ Request-side conversion ββββββββββββββββββββββββββββββββββββββββ
/**
* Recursively convert `prefixItems` tuple schemas to equivalent object schemas.
* Input must be a clone β this function mutates in place and returns the same reference.
*/
export function convertTupleSchemas(node: Schema): Schema {
return convertWalk(node, new Set());
}
function convertWalk(node: Schema, seen: Set<object>): Schema {
if (seen.has(node)) return node;
seen.add(node);
// Convert this node if it has prefixItems
if (Array.isArray(node.prefixItems)) {
const items = node.prefixItems as unknown[];
const properties: Record<string, unknown> = {};
const required: string[] = [];
for (let i = 0; i < items.length; i++) {
const key = String(i);
properties[key] = isRecord(items[i]) ? convertWalk(items[i] as Schema, seen) : items[i];
required.push(key);
}
node.type = "object";
node.properties = properties;
node.required = required;
node.additionalProperties = false;
delete node.prefixItems;
delete node.items;
return node;
}
// Recurse into properties
if (isRecord(node.properties)) {
for (const [k, v] of Object.entries(node.properties)) {
if (isRecord(v)) node.properties[k] = convertWalk(v, seen);
}
}
// Recurse into items
if (isRecord(node.items)) {
node.items = convertWalk(node.items as Schema, seen);
}
// Recurse into combinators
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
if (Array.isArray(node[key])) {
node[key] = (node[key] as unknown[]).map((entry) =>
isRecord(entry) ? convertWalk(entry, seen) : entry,
);
}
}
// Recurse into $defs / definitions
for (const key of ["$defs", "definitions"] as const) {
if (isRecord(node[key])) {
const defs = node[key] as Schema;
for (const [k, v] of Object.entries(defs)) {
if (isRecord(v)) defs[k] = convertWalk(v, seen);
}
}
}
// Recurse into conditional
for (const key of ["if", "then", "else", "not"] as const) {
if (isRecord(node[key])) {
node[key] = convertWalk(node[key] as Schema, seen);
}
}
return node;
}
// ββ Response-side reconversion βββββββββββββββββββββββββββββββββββββ
/**
* Schema-guided recursive reconversion: turn {"0":β¦,"1":β¦} objects back to arrays
* wherever the *original* schema had `prefixItems`.
*/
export function reconvertTupleValues(data: unknown, schema: Schema, rootSchema?: Schema): unknown {
const root = rootSchema ?? schema;
// Resolve $ref
if (typeof schema.$ref === "string") {
const resolved = resolveRef(schema.$ref, root);
if (resolved) return reconvertTupleValues(data, resolved, root);
return data;
}
// Tuple node: original schema has prefixItems β data should be {"0":β¦,"1":β¦} β convert to array
if (Array.isArray(schema.prefixItems) && isRecord(data)) {
const items = schema.prefixItems as unknown[];
const result: unknown[] = [];
for (let i = 0; i < items.length; i++) {
const key = String(i);
const val = data[key];
const itemSchema = items[i];
result.push(isRecord(itemSchema) ? reconvertTupleValues(val, itemSchema, root) : val);
}
return result;
}
// Object with properties β recurse into each property
if (isRecord(schema.properties) && isRecord(data)) {
const result: Record<string, unknown> = { ...data };
for (const [key, propSchema] of Object.entries(schema.properties)) {
if (key in result && isRecord(propSchema)) {
result[key] = reconvertTupleValues(result[key], propSchema, root);
}
}
return result;
}
// Array with items schema β recurse into each element
if (isRecord(schema.items) && Array.isArray(data)) {
return data.map((el) => reconvertTupleValues(el, schema.items as Schema, root));
}
// Combinators β try to find matching branch (heuristic: first branch that has prefixItems)
for (const key of ["oneOf", "anyOf", "allOf"] as const) {
if (Array.isArray(schema[key])) {
for (const branch of schema[key] as unknown[]) {
if (isRecord(branch) && hasTupleSchemas(branch)) {
return reconvertTupleValues(data, branch, root);
}
}
}
}
return data;
}
function resolveRef(ref: string, root: Schema): Schema | undefined {
// Only handle internal refs: #/$defs/Name or #/definitions/Name
const match = ref.match(/^#\/(\$defs|definitions)\/(.+)$/);
if (!match) return undefined;
const defs = root[match[1]];
if (!isRecord(defs)) return undefined;
const resolved = defs[match[2]];
return isRecord(resolved) ? resolved : undefined;
}
|