fn.ts (2.50 KB)
import type * as z from "zod";
import { ValidationError } from "./errors";
import { Err, type InferErr, type InferOk, type Result } from "./result";

/**
 * Unwrap Promise if present, otherwise return T.
 */
type Awaited<T> = T extends Promise<infer U> ? U : T;

/**
 * Extract the inner Result from a potentially Promise-wrapped Result.
 */
type UnwrapResult<T> = Awaited<T> extends Result<infer O, infer E>
  ? Result<O, E>
  : never;

/**
 * Create a validated function that returns a Result.
 *
 * @param schema - Zod schema for input validation
 * @param cb - Callback that receives validated input and returns a Result (sync or async)
 * @returns Function that validates input and returns Result with ValidationError in error channel
 *
 * @example
 * ```typescript
 * // Sync example
 * const validateUser = fn(
 *   z.object({ name: z.string() }),
 *   (input) => Result.ok({ id: 1, ...input })
 * );
 *
 * // Async example
 * const createUser = fn(
 *   z.object({ name: z.string(), email: z.string().email() }),
 *   async (input) => {
 *     if (await emailExists(input.email)) {
 *       return Result.err(new EmailExistsError({ email: input.email }));
 *     }
 *     return Result.ok({ id: 1, ...input });
 *   }
 * );
 *
 * // Returns Promise<Result<User, ValidationError | EmailExistsError>>
 * const result = await createUser({ name: "test", email: "test@example.com" });
 * ```
 */
export function fn<
  T extends z.ZodType,
  Ret extends Result<unknown, unknown> | Promise<Result<unknown, unknown>>,
>(
  schema: T,
  cb: (input: z.infer<T>) => Ret
): {
  (
    input: z.infer<T>
  ): Promise<
    Result<
      InferOk<UnwrapResult<Ret>>,
      ValidationError | InferErr<UnwrapResult<Ret>>
    >
  >;
  force: (input: z.infer<T>) => Ret;
  schema: T;
} {
  const result = (input: unknown) => {
    const parsed = schema.safeParse(input);
    if (!parsed.success) {
      const err = new Err(
        new ValidationError({
          message: parsed.error.message,
          issues: parsed.error.issues,
        })
      );
      // Always wrap in Promise.resolve().then() to match async callback behavior
      // This is more reliable than checking constructor.name which fails with
      // arrow functions, bound functions, and minified code
      return Promise.resolve(err);
    }
    return Promise.resolve(cb(parsed.data));
  };

  result.force = (input: z.infer<T>): Ret => cb(input);
  result.schema = schema;

  // biome-ignore lint/suspicious/noExplicitAny: Complex type inference requires this cast
  return result as any;
}