-
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
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
// SPDX-FileCopyrightText: 2024 Shota FUJI <pockawoooh@gmail.com>
//
// SPDX-License-Identifier: Apache-2.0
interface Context {
fieldPath: readonly string[];
}
function getFieldName(ctx: Context): string {
if (ctx.fieldPath.length === 0) {
return "<rootObject>";
}
return ctx.fieldPath.join(".");
}
class MacanaCLIConfigParsingError extends Error {}
class FieldConstraintError extends MacanaCLIConfigParsingError {
constructor(ctx: Context, beWhat: string) {
super(
`Invalid config: "${getFieldName(ctx)}" MUST be ${beWhat}`,
);
}
}
class UnexpectedFieldTypeError extends MacanaCLIConfigParsingError {
constructor(ctx: Context, expected: string, found: string) {
super(
`Invalid config: "${
getFieldName(ctx)
}" MUST be ${expected} but found ${found}`,
);
}
}
export type Parser<T> = (x: unknown, ctx: Context) => T;
export function parse<T>(x: unknown, parser: Parser<T>): T {
return parser(x, {
fieldPath: [],
});
}
export interface ObjectParserOptions {
optional?: boolean;
}
export function object<T>(
fields: { [K in keyof T]: Parser<T[K]> },
options?: { optional: true },
): Parser<Partial<T>>;
export function object<T>(
fields: { [K in keyof T]: Parser<T[K]> },
options: { optional: false },
): Parser<T>;
export function object<T>(
fields: { [K in keyof T]: Parser<T[K]> },
{ optional = true }: ObjectParserOptions = {},
): Parser<Partial<T>> {
return (x, ctx) => {
if (typeof x !== "object") {
throw new UnexpectedFieldTypeError(ctx, "object", typeof x);
}
if (!x) {
throw new UnexpectedFieldTypeError(ctx, "object", "null");
}
const obj: Partial<T> = {};
for (const fieldName in fields) {
if (fieldName in x) {
obj[fieldName] = fields[fieldName]((x as T)[fieldName], {
fieldPath: [...ctx.fieldPath, fieldName],
});
continue;
}
if (!optional) {
throw new MacanaCLIConfigParsingError(
`Invalid config: "${
getFieldName(ctx)
}" MUST have "${fieldName}" field`,
);
}
}
return obj;
};
}
export function record<K extends string, V>(
keyParser: Parser<K>,
valueParser: Parser<V>,
): Parser<Record<K, V>> {
return (x, ctx) => {
if (typeof x !== "object") {
throw new UnexpectedFieldTypeError(ctx, "object(record)", typeof x);
}
if (!x) {
throw new UnexpectedFieldTypeError(ctx, "object(record)", "null");
}
const rec: Partial<Record<K, V>> = {};
for (const key in x) {
const parsedKey = keyParser(key, {
fieldPath: [...ctx.fieldPath, "(key)"],
});
const parsedValue = valueParser((x as Record<K, V>)[key as K], {
fieldPath: [...ctx.fieldPath, key],
});
rec[parsedKey] = parsedValue;
}
return rec as Record<K, V>;
};
}
export interface StringParserOptions {
nonEmpty?: boolean;
trim?: boolean;
}
export function string(
{ nonEmpty = false, trim = false }: StringParserOptions = {},
): Parser<string> {
return (x, ctx) => {
if (typeof x !== "string") {
throw new UnexpectedFieldTypeError(ctx, "string", typeof x);
}
const value = trim ? x.trim() : x;
if (nonEmpty && !value) {
throw new FieldConstraintError(ctx, "non-empty string");
}
return value;
};
}
export const boolean: Parser<boolean> = (x, ctx) => {
if (typeof x !== "boolean") {
throw new UnexpectedFieldTypeError(ctx, "boolean", typeof x);
}
return x;
};
export function map<A, B>(parser: Parser<A>, fn: (a: A) => B): Parser<B> {
return (x, ctx) => {
return fn(parser(x, ctx));
};
}