Initial Commit

This commit is contained in:
2026-05-27 23:29:53 +01:00
commit fbc70f96e6
10000 changed files with 1102152 additions and 0 deletions

View File

@@ -0,0 +1,7 @@
export {
contactIdSchema,
groupNameSchema,
macOSContactsConfigSchema,
parseOrThrow,
searchQuerySchema,
} from "./schemas.js";

View File

@@ -0,0 +1,84 @@
# schemas
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## Purpose
Zod schemas guarding every public input boundary of `@suaveplan/macos-contacts`. Validation runs before any side effect; failures raise a typed `MacOSContactsError(INVALID_INPUT)` with the underlying `ZodError` chained as `cause` and the failing field paths embedded in the message. This pattern keeps consumers from having to import `zod` to dispatch on validation errors, and keeps the bridge's `try { runner.run(...) }` blocks focused on platform failures, not shape failures.
## Features
- `macOSContactsConfigSchema` — validates the optional `{ timeoutMs, batchSize }` bundle accepted by `MacOSContactsBridge`.
- `searchQuerySchema` — non-empty trimmed string; used by `searchContacts`.
- `groupNameSchema` — non-empty trimmed string; used by `getGroupMembers`.
- `contactIdSchema` — non-empty trimmed string; used by `getContactById`.
- `parseOrThrow(schema, input, label)` — generic helper. Returns the parsed value on success, throws `MacOSContactsError(INVALID_INPUT)` on failure.
## Usage: Basic
```ts
import {
macOSContactsConfigSchema,
parseOrThrow,
} from "@suaveplan/macos-contacts";
const config = parseOrThrow(
macOSContactsConfigSchema,
{ timeoutMs: 30_000 },
"myCaller",
);
```
## Usage: Advanced
```ts
import z from "zod";
import {
MacOSContactsError,
parseOrThrow,
searchQuerySchema,
} from "@suaveplan/macos-contacts";
const richQuerySchema = z.object({
query: searchQuerySchema,
limit: z.number().int().min(1).max(500),
});
try {
const parsed = parseOrThrow(richQuerySchema, input, "rich.search");
await bridge.searchContacts(parsed.query);
} catch (err) {
if (err instanceof MacOSContactsError && err.code === "MACOS_CONTACTS.INVALID_INPUT") {
return form.showValidation(err.message);
}
throw err;
}
```
## API Reference
### `macOSContactsConfigSchema`
`z.object({ timeoutMs: z.number().int().positive().optional(), batchSize: z.number().int().positive().optional() })`. Both fields are optional; defaults are filled in by `MacOSContactsBridge`.
### `searchQuerySchema` / `groupNameSchema` / `contactIdSchema`
`z.string().trim().min(1)`. Each schema rejects empty / whitespace-only input.
### `parseOrThrow<T>(schema: z.ZodType<T>, input: unknown, label: string): T`
Runs `schema.safeParse(input)`. On success returns the parsed value (with whatever transformations the schema applied, e.g. `.trim()`). On failure constructs a `MacOSContactsError` with:
- `code === "MACOS_CONTACTS.INVALID_INPUT"`,
- `message === "<label> input failed validation: <field>: <issue>; <field>: <issue>"`,
- `cause === <ZodError>`.
The `label` argument is the human-readable name of the call site (e.g. `"MacOSContactsBridge.constructor"`); it shows up first in the message so log readers can tell which boundary failed.
## Implementation Notes
The `parseOrThrow` helper is intentionally generic over the schema's output type so callers do not lose Zod's type inference. The error message lists every failing path so consumers can render granular validation feedback without re-running the parser.
Schemas live in their own module rather than being co-located with the bridge so unit tests can exercise schema-level invariants (empty query rejection, positive `timeoutMs` requirement) without instantiating the bridge. The bridge itself calls `parseOrThrow` exclusively — it never reaches for `schema.parse` or `schema.safeParse` directly, which keeps the error-mapping policy in one place.

View File

@@ -0,0 +1,87 @@
import { describe, expect, it } from "@suaveplan/testing/runner";
import z from "zod";
import { MacOSContactsError } from "../error-codes/error-codes.js";
import {
contactIdSchema,
groupNameSchema,
macOSContactsConfigSchema,
parseOrThrow,
searchQuerySchema,
} from "./schemas.js";
describe("schemas", () => {
it("macOSContactsConfigSchema accepts a valid bundle", {
timeout: 5000,
}, () => {
const parsed = macOSContactsConfigSchema.parse({
timeoutMs: 30_000,
batchSize: 500,
});
expect(parsed.timeoutMs).toBe(30_000);
});
it("macOSContactsConfigSchema rejects non-positive timeoutMs", {
timeout: 5000,
}, () => {
const r = macOSContactsConfigSchema.safeParse({ timeoutMs: -1 });
expect(r.success).toBe(false);
});
it("searchQuerySchema rejects empty / whitespace", {
timeout: 5000,
}, () => {
expect(searchQuerySchema.safeParse("").success).toBe(false);
expect(searchQuerySchema.safeParse(" ").success).toBe(false);
expect(searchQuerySchema.safeParse("a").success).toBe(true);
});
it("groupNameSchema rejects empty / whitespace", {
timeout: 5000,
}, () => {
expect(groupNameSchema.safeParse("").success).toBe(false);
expect(groupNameSchema.safeParse(" ").success).toBe(false);
expect(groupNameSchema.safeParse("Family").success).toBe(true);
});
it("contactIdSchema rejects empty / whitespace", {
timeout: 5000,
}, () => {
expect(contactIdSchema.safeParse("").success).toBe(false);
expect(contactIdSchema.safeParse("c1").success).toBe(true);
});
it("parseOrThrow returns the parsed value on success", {
timeout: 5000,
}, () => {
const out = parseOrThrow(z.string(), "x", "label");
expect(out).toBe("x");
});
it("parseOrThrow throws MacOSContactsError(INVALID_INPUT) with cause", {
timeout: 5000,
}, () => {
let caught: unknown;
try {
parseOrThrow(searchQuerySchema, "", "search");
} catch (e) {
caught = e;
}
expect(caught).toBeInstanceOf(MacOSContactsError);
const err = caught as MacOSContactsError;
expect(err.code).toBe("MACOS_CONTACTS.INVALID_INPUT");
expect(err.cause).toBeInstanceOf(z.ZodError);
expect(err.message).toContain("search input failed validation");
});
it("parseOrThrow lists (root) when the failure has no path", {
timeout: 5000,
}, () => {
let caught: unknown;
try {
parseOrThrow(z.number(), "not-a-number", "label");
} catch (e) {
caught = e;
}
expect((caught as MacOSContactsError).message).toContain("(root)");
});
});

View File

@@ -0,0 +1,42 @@
import z from "zod";
import {
MACOS_CONTACTS_CODES,
MacOSContactsError,
} from "../error-codes/error-codes.js";
/** Zod schema for {@link MacOSContactsConfig}. */
export const macOSContactsConfigSchema = z.object({
timeoutMs: z.number().int().positive().optional(),
batchSize: z.number().int().positive().optional(),
});
/** Zod schema for a non-empty search query string. */
export const searchQuerySchema = z.string().trim().min(1);
/** Zod schema for a non-empty group name string. */
export const groupNameSchema = z.string().trim().min(1);
/** Zod schema for a non-empty contact-id string. */
export const contactIdSchema = z.string().trim().min(1);
/**
* Parse `input` with `schema`; on failure raise a typed
* `MacOSContactsError(INVALID_INPUT)` whose `cause` is the underlying
* `ZodError` and whose message lists the failing paths.
*/
export function parseOrThrow<T>(
schema: z.ZodType<T>,
input: unknown,
label: string
): T {
const result = schema.safeParse(input);
if (result.success) return result.data;
const issues = result.error.issues
.map((i) => `${i.path.join(".") || "(root)"}: ${i.message}`)
.join("; ");
throw new MacOSContactsError(
MACOS_CONTACTS_CODES.INVALID_INPUT,
`${label} input failed validation: ${issues}`,
{ cause: result.error }
);
}