All files / webview-app/src/schemas parsers.ts

100% Statements 34/34
100% Branches 21/21
100% Functions 9/9
100% Lines 34/34

Press n or j to go to the next uncovered block, b, p or k for the previous block.

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                    3x             3x       18x     15x                   11x             6x   6x 5x 5x     1x       4x   4x 3x 3x     1x       6x 3x 3x                   1x       1x       5x   5x 3x 3x     2x             20x 15x 15x     20x 20x       34x   34x 11x     23x    
import { type ZodError, z } from "zod";
 
import {
  type CanvasComponent,
  CanvasComponentSchema,
  type CanvasState,
  CanvasStateSchema,
} from "@/schemas/canvas";
import { type ExtensionMessage, MessageSchema } from "@/schemas/messages";
 
const HexColorSchema = z
  .string()
  .trim()
  .regex(/^#(?:[0-9a-fA-F]{3}|[0-9a-fA-F]{6})$/, {
    message: "Color must be #RGB or #RRGGBB",
  });
 
const NumberInputSchema = z
  .string()
  .trim()
  .min(1, { message: "Value is required" })
  .transform((value) => Number(value));
 
function clampNumber(value: number, min: number, max: number): number {
  return Math.min(max, Math.max(min, value));
}
 
export interface NumericInputOptions {
  min: number;
  max: number;
  integer?: boolean;
}
 
function logParseError(scope: string, error: ZodError, data: unknown) {
  console.error(`[zod] ${scope} parse failed`, {
    issues: error.issues,
    received: data,
  });
}
 
export function parseCanvasComponent(data: unknown): CanvasComponent | null {
  const result = CanvasComponentSchema.safeParse(data);
 
  if (!result.success) {
    logParseError("CanvasComponent", result.error, data);
    return null;
  }
 
  return result.data;
}
 
export function parseCanvasState(data: unknown): CanvasState | null {
  const result = CanvasStateSchema.safeParse(data);
 
  if (!result.success) {
    logParseError("CanvasState", result.error, data);
    return null;
  }
 
  return result.data;
}
 
export function parseMessage(data: unknown): ExtensionMessage | null {
  if (data && typeof data === "object" && "type" in data) {
    const candidateType = (data as { type?: unknown }).type;
    if (
      typeof candidateType === "string" &&
      ![
        "stateChanged",
        "toolbarCommand",
        "loadState",
        "configDefaults",
        "previewCodeResponse",
      ].includes(candidateType)
    ) {
      console.warn("[zod] Unknown extension message type ignored", {
        type: candidateType,
        received: data,
      });
      return null;
    }
  }
 
  const result = MessageSchema.safeParse(data);
 
  if (!result.success) {
    logParseError("ExtensionMessage", result.error, data);
    return null;
  }
 
  return result.data;
}
 
export function parseClampedNumericInput(
  rawValue: string,
  options: NumericInputOptions,
): number | null {
  const schema = NumberInputSchema.pipe(z.number().finite()).transform((value) => {
    const nextValue = options.integer ? Math.round(value) : value;
    return clampNumber(nextValue, options.min, options.max);
  });
 
  const result = schema.safeParse(rawValue);
  return result.success ? result.data : null;
}
 
export function parseHexColorInput(rawValue: string): string | null {
  const result = HexColorSchema.safeParse(rawValue);
 
  if (!result.success) {
    return null;
  }
 
  return result.data.toLowerCase();
}