Skip to content
File

Blob: src/lib/codec-errors.ts

typescript116 lines
1import { EnGardeAssertionError } from "@cloudflare/util-en-garde";
2 
3export interface CodecIssueSummary {
4 path: string;
5 expected: string | null;
6 message: string | null;
7}
8 
9interface CodecIssueSummaryContainer {
10 issues: CodecIssueSummary[];
11}
12 
13type EnGardeIssueLike = EnGardeAssertionError["errors"][number];
14type ContextEntryLike = EnGardeIssueLike["context"][number];
15 
16const isCodecIssueSummary = (value: unknown): value is CodecIssueSummary =>
17 !!value &&
18 typeof value === "object" &&
19 "path" in value &&
20 "expected" in value &&
21 "message" in value &&
22 typeof value.path === "string" &&
23 (value.expected === null || typeof value.expected === "string") &&
24 (value.message === null || typeof value.message === "string");
25 
26const isCodecIssueSummaryContainer = (value: unknown): value is CodecIssueSummaryContainer =>
27 !!value &&
28 typeof value === "object" &&
29 "issues" in value &&
30 Array.isArray(value.issues) &&
31 value.issues.every(isCodecIssueSummary);
32 
33const formatPath = (context: readonly ContextEntryLike[] | undefined): string => {
34 if (!context) {
35 return "";
36 }
37 
38 const keys = context.reduce<string[]>((result, entry) => {
39 const key = entry.key?.trim();
40 if (!key) {
41 return result;
42 }
43 
44 if (result[result.length - 1] === key) {
45 return result;
46 }
47 
48 result.push(key);
49 return result;
50 }, []);
51 
52 return keys.reduce((path, key) => {
53 if (/^\d+$/u.test(key)) {
54 return `${path}[${key}]`;
55 }
56 
57 return path.length === 0 ? key : `${path}.${key}`;
58 }, "");
59};
60 
61const summarizeEnGardeIssue = (issue: EnGardeIssueLike): CodecIssueSummary => {
62 const context = issue.context;
63 const expected = context[context.length - 1]?.type?.name ?? null;
64 
65 return {
66 path: formatPath(context),
67 expected,
68 message: issue.message ?? null,
69 };
70};
71 
72const getCodecIssueSummaries = (value: unknown): CodecIssueSummary[] | null => {
73 if (value instanceof EnGardeAssertionError) {
74 return value.errors.map((issue) => summarizeEnGardeIssue(issue));
75 }
76 
77 if (isCodecIssueSummaryContainer(value)) {
78 return value.issues;
79 }
80 
81 return null;
82};
83 
84const formatIssueSummary = (issue: CodecIssueSummary): string => {
85 const subject = issue.path || "value";
86 
87 if (issue.message) {
88 return `${subject}: ${issue.message}`;
89 }
90 
91 if (issue.expected) {
92 return `${subject}: expected ${issue.expected}`;
93 }
94 
95 return `${subject}: invalid value`;
96};
97 
98export const formatCodecIssues = (value: unknown): string | null => {
99 const issues = getCodecIssueSummaries(value);
100 if (!issues || issues.length === 0) {
101 return null;
102 }
103 
104 const messages = Array.from(new Set(issues.map(formatIssueSummary)));
105 return messages.join("; ");
106};
107 
108export const toCodecIssueDetails = (value: unknown): unknown => {
109 const issues = getCodecIssueSummaries(value);
110 if (!issues) {
111 return value;
112 }
113 
114 return { issues } satisfies CodecIssueSummaryContainer;
115};