diff options
Diffstat (limited to 'src/api/index.js')
| -rw-r--r-- | src/api/index.js | 111 |
1 files changed, 111 insertions, 0 deletions
diff --git a/src/api/index.js b/src/api/index.js new file mode 100644 index 0000000..3fdf430 --- /dev/null +++ b/src/api/index.js @@ -0,0 +1,111 @@ +// Registry of `resource.method` methods. +// +// Each module exports an object { 'resource.method': definition }. A definition: +// description text for `wa schema` / --help +// params { name: { type, required?, default?, description, enum? } } +// type: string | integer | number | boolean | array | object | path | chat +// path -> the CLI resolves it to an absolute path (relative to the CLI cwd) +// chat -> the daemon resolves it to a chatId (JID, number or exact name) +// columns default columns for --format table/csv +// ready false if it works without a ready session (default true) +// stream true if it emits events until the client disconnects +// handler async (ctx, params, conn) => result (only used by the daemon) +// +// Adding a method only takes adding it here: the daemon exposes it on the socket and +// the CLI lists it in the help and accepts it as `wa <resource> <method>`. + +import path from 'node:path'; +import { WaError } from '../rpc.js'; +import chats from './chats.js'; +import contacts from './contacts.js'; +import messages from './messages.js'; +import session from './session.js'; + +export const METHODS = { ...session, ...chats, ...messages, ...contacts }; + +export function resources() { + const out = {}; + for (const [name, def] of Object.entries(METHODS)) { + const [res, meth] = name.split('.'); + (out[res] ||= {})[meth] = def; + } + return out; +} + +export function getMethod(name) { + const def = METHODS[name]; + if (!def) { + throw new WaError('EINVAL', `unknown method: ${name} (see \`wa schema\`)`); + } + return def; +} + +// Serializable description (no handler), for `wa schema`. +export function schema(name) { + const { handler, ...rest } = getMethod(name); + return { method: name, ready: rest.ready !== false, ...rest }; +} + +// Spanish yes (si/sí) is accepted as well. +const TRUE = new Set(['true', '1', 'yes', 'si', 'sí']); +const FALSE = new Set(['false', '0', 'no']); + +// Validates and normalizes parameters. `cwd` is used to resolve path-typed ones. +export function validate(name, params = {}, { cwd } = {}) { + const def = getMethod(name); + const spec = def.params || {}; + if (params === null || typeof params !== 'object' || Array.isArray(params)) { + throw new WaError('EINVAL', `${name}: --params must be a JSON object`); + } + const out = {}; + for (const key of Object.keys(params)) { + if (!spec[key]) { + const valid = Object.keys(spec); + throw new WaError('EINVAL', `${name}: unknown parameter "${key}"` + + (valid.length ? ` (valid: ${valid.join(', ')})` : ' (takes no parameters)')); + } + } + for (const [key, p] of Object.entries(spec)) { + let v = params[key]; + if (v === undefined || v === null) { + if (p.required) throw new WaError('EINVAL', `${name}: missing parameter "${key}"`); + if (p.default !== undefined) out[key] = p.default; + continue; + } + const bad = () => new WaError('EINVAL', `${name}: "${key}" must be ${p.type}`); + switch (p.type) { + case 'string': + case 'chat': + if (typeof v === 'number') v = String(v); + if (typeof v !== 'string' || v === '') throw bad(); + break; + case 'path': + if (typeof v !== 'string' || v === '') throw bad(); + if (cwd) v = path.resolve(cwd, v.replace(/^~(?=$|\/)/, process.env.HOME)); + break; + case 'integer': + case 'number': + if (typeof v === 'string' && v.trim() !== '') v = Number(v); + if (typeof v !== 'number' || Number.isNaN(v)) throw bad(); + if (p.type === 'integer' && !Number.isInteger(v)) throw bad(); + break; + case 'boolean': + if (typeof v === 'string' && TRUE.has(v.toLowerCase())) v = true; + else if (typeof v === 'string' && FALSE.has(v.toLowerCase())) v = false; + if (typeof v !== 'boolean') throw bad(); + break; + case 'array': + if (typeof v === 'string') v = v.split(',').map((s) => s.trim()).filter(Boolean); + if (!Array.isArray(v)) throw bad(); + break; + case 'object': + if (typeof v !== 'object' || Array.isArray(v)) throw bad(); + break; + } + if (p.enum && !p.enum.includes(v)) { + throw new WaError('EINVAL', `${name}: "${key}" must be one of: ${p.enum.join(', ')}`); + } + out[key] = v; + } + return out; +} |