-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathlib.ts
106 lines (94 loc) · 4.24 KB
/
lib.ts
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
// --- Types & constants ---------------------------------------------------- //
/**
* Validates a string with custom logic. If a string is returned, that value is
* used as a fallback in case the substitution string is `undefined`.
*/
export type Validator = (message: string | undefined) => boolean | string;
export type Substitution<T extends string> = RequiredSubstitution<T> | OptionalSubstitution<T>;
export type RequiredSubstitution<T extends string> = { [P in T]: string | (() => string) };
export type OptionalSubstitution<T extends string> = { [P in T]?: string | (() => string) };
export type WidenSubstitutions<T extends Substitution<any>, K extends Substitution<any> | void> = K extends void
? T
: T & K;
// --- MessageMap class ----------------------------------------------------- //
export class MessageMap<TSubstitutions extends Substitution<any> | void = void> {
/** The underlying base message string. */
private message: string;
/** Configured substitutions. */
private substitutions: { [key: string]: Validator } = {};
/**
* Creates an instance of `MessageMap`, a strongly-typed interface for building dynamic strings. Your base `message`
* may contain substrings like: `"%name"`, which is replaced by a substitution keyed as `name`. So `"Hello, %name!"`
* might form into `"Hello, Nancy!"` or `"Hello, George!"`.
*
* @param message - The base message.
* @return An instance of `MessageMap`
*/
constructor(message: string) {
this.message = message;
}
/**
* Registers an optional substitution.
*
* @param name - The name by which to identify this substitution in the `message` string.
* @param validator - An optional validation function to test replacement strings against.
* @return A new instance of `MessageMap` with an expanded type signature.
*/
public optional<T extends string>(
name: T,
validator: Validator = () => true,
): MessageMap<WidenSubstitutions<OptionalSubstitution<T>, TSubstitutions>> {
const nextInst = new MessageMap(this.message);
nextInst.substitutions = { ...this.substitutions };
nextInst.substitutions[name] = validator;
return nextInst as any;
}
/**
* Registers a required substitution.
*
* @param name - The name by which to identify this substitution in the `message` string.
* @param validator - An optional validation function to test replacement strings against.
* @return A new instance of `MessageMap` with an expanded type signature.
*/
public required<T extends string>(
name: T,
validator: Validator = message => typeof message !== 'undefined' && message !== null,
): MessageMap<WidenSubstitutions<RequiredSubstitution<T>, TSubstitutions>> {
const nextInst = new MessageMap(this.message);
nextInst.substitutions = { ...this.substitutions };
nextInst.substitutions[name] = validator;
return nextInst as any;
}
/**
* Transform the underlying `message` into a string with interpolated
* substitutions.
*
* @param substitutions - A plain object of substitutions to replace keys in
* the `message` string.
* @return The formed and validated string.
*/
public toString<T extends TSubstitutions>(
...substitutions: T extends RequiredSubstitution<any> ? [TSubstitutions] : [TSubstitutions | void]
): string {
let result = this.message;
const substitutionsQualified: any = !!substitutions.length ? substitutions[0] : {};
for (const [name, validator] of Object.entries(this.substitutions)) {
const replacer = substitutionsQualified[name];
const replacement = typeof replacer === 'function' ? replacer() : replacer;
const isValid = validator(replacement);
if (typeof isValid !== 'string' && !isValid) {
const sub = `"%${name}"`;
const msg = `"${this.message}"`;
const received =
typeof replacement === 'undefined' || replacement === null ? String(replacement) : `"${replacement}"`;
throw new Error(`[MessageMap] Validation failed.\n\nSubstitution: ${sub} in ${msg}\nReceived: ${received}\n`);
}
if (replacement) {
result = result.replace(`%${name}`, replacement);
} else if (typeof isValid === 'string') {
result = result.replace(`%${name}`, isValid);
}
}
return result;
}
}