/** * Key conversion between the SDK's camelCase declarations and the platform's * snake_case API. Purely mechanical — the same rule the HTTP client applies — * so a field added to the API is usable from the SDK the day it ships, under * its camelCase twin, with no glossary and no SDK release. * * JSON Schema subtrees are never touched: property names are the author's * (and already snake_case by convention), and schema keywords such as * `additionalProperties`, `visibleOn` or `$schema` must survive verbatim. */ const SCHEMA_KEYS = new Set([ "schema", "values", "uiSchema", "ui_schema", "parameters", "results", ]); export const snakeCase = (key: string): string => key .replace(/([a-z0-9])([A-Z])/g, "$1_$2") .replace(/([A-Z])([A-Z][a-z])/g, "$1_$2") .toLowerCase(); export const camelCase = (key: string): string => key.replace(/_([a-z0-9])/g, (_, c: string) => c.toUpperCase()); type Json = unknown; /** * Deep camelCase → snake_case on object keys. Values under a schema-carrying * key (`schema`, `values`, `uiSchema`) are copied as they are. `parameters` * and `results` are action-spec envelopes: their own keys convert (`schema`), * what is inside `schema` does not. */ export function toApiKeys(value: Json, insideSchema = false): Json { if (Array.isArray(value)) return value.map((v) => toApiKeys(v, insideSchema)); if (!value || typeof value !== "object") return value; const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { if (v === undefined) continue; if (insideSchema) { out[k] = toApiKeys(v, true); continue; } const key = snakeCase(k); if ( k === "schema" || k === "values" || k === "uiSchema" || k === "ui_schema" ) { out[key] = v; // verbatim } else if (k === "parameters" || k === "results") { out[key] = toApiKeys(v, false); } else { out[key] = toApiKeys(v, false); } } return out; } /** Deep snake_case → camelCase on object keys, with the same schema exemption. */ export function fromApiKeys(value: Json): Json { if (Array.isArray(value)) return value.map(fromApiKeys); if (!value || typeof value !== "object") return value; const out: Record = {}; for (const [k, v] of Object.entries(value as Record)) { const key = camelCase(k); out[key] = SCHEMA_KEYS.has(k) && k !== "parameters" && k !== "results" ? v : fromApiKeys(v); } return out; } /** The plural the manifest uses for a kind: `service_specification` → `service_specifications`. */ export function pluralKind(kind: string): string { if (/[^aeiou]y$/.test(kind)) return kind.slice(0, -1) + "ies"; if (/(s|x|ch|sh)$/.test(kind)) return kind + "es"; return kind + "s"; }