Repository navigation
Expand file tree
/
Copy pathvalidation.ts
More file actions
119 lines (107 loc) · 3.22 KB
/
Copy pathvalidation.ts
File metadata and controls
119 lines (107 loc) · 3.22 KB
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
117
118
119
import { closest, distance } from "fastest-levenshtein";
import type { z } from "zod";
export type Diagnostic = {
level: "error" | "warning";
where: string;
message: string;
};
/**
* A `Validation` is either a Success containing a value of `A` and potentially some warnings,
* or a Failure containing merely a list of errors & warnings.
*/
export type Validation<A> =
| { ok: true; value: A; diagnostics: Diagnostic[] }
| { ok: false; diagnostics: Diagnostic[] };
export const error = (where: string, message: string): Diagnostic => ({
level: "error",
where,
message,
});
export const warning = (where: string, message: string): Diagnostic => ({
level: "warning",
where,
message,
});
export const ok = <A>(
value: A,
diagnostics: Diagnostic[] = [],
): Validation<A> => ({ ok: true, value, diagnostics });
export const fail = <A = never>(diagnostics: Diagnostic[]): Validation<A> => ({
ok: false,
diagnostics,
});
/** binds/flatMaps/andThen's a Validation, merging the diagnostics in both elements.
* yay monads!
*/
export function andThen<A, B>(
validation: Validation<A>,
next: (value: A) => Validation<B>,
): Validation<B> {
if (!validation.ok) {
return validation;
}
const result = next(validation.value);
const diagnostics = [...validation.diagnostics, ...result.diagnostics];
return result.ok
? { ok: true, value: result.value, diagnostics }
: { ok: false, diagnostics };
}
export const hasErrors = (diagnostics: Diagnostic[]): boolean =>
diagnostics.some((d) => d.level === "error");
/**
* Combines a list of independent Validations without stopping at the first failure,
* returning a list of all the successes and all of the diagnostics
*/
export function partition<A>(items: Validation<A>[]): {
values: A[];
diagnostics: Diagnostic[];
} {
return {
values: items.flatMap((item) => (item.ok ? [item.value] : [])),
diagnostics: items.flatMap((item) => item.diagnostics),
};
}
/** Adapts a zod `safeParse` result, turning each issue into an error at `where`. */
export function fromZod<T>(
result: z.ZodSafeParseResult<T>,
where: string,
): Validation<T> {
if (result.success) {
return ok(result.data);
}
return fail(
result.error.issues.map((issue) => {
const path = issue.path.map(String).join(".");
return error(where, path ? `${path}: ${issue.message}` : issue.message);
}),
);
}
/** Prints diagnostics, errors via `error()` and warnings via `warn()`. */
export function report(
diagnostics: Diagnostic[],
out: {
error: (message: string) => void;
warn: (message: string) => void;
} = console,
): void {
for (const d of diagnostics) {
const line = `${d.level}: ${d.where}: ${d.message}`;
(d.level === "error" ? out.error : out.warn)(line);
}
}
/**
* A ` (did you mean "x"?)` suffix naming the candidate closest to `name`, or an
* empty string if none is close enough to be a plausible typo.
*/
export function suggest(name: string, candidates: Iterable<string>): string {
const list = [...candidates];
if (list.length === 0) {
return "";
}
const lower = name.toLowerCase();
const best = closest(lower, list);
// (a swapped pair such as "rsut" counts as two edits, hence the rounding up)
return distance(lower, best) <= Math.ceil(name.length / 3)
? ` (did you mean "${best}"?)`
: "";
}