-
Notifications
You must be signed in to change notification settings - Fork 0
/
options-getter.ts
223 lines (208 loc) · 6.92 KB
/
options-getter.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
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
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
import { Client } from 'src/core/client';
import { OptionValues } from 'src/core/parse';
import { CamelCase, camelCase } from 'src/utils/camel-case';
import { MaybeReadonly } from 'src/utils/types';
import { OptionGetter, createOptionGetter } from './option-getter';
import {
OptionConfig,
OptionPrimitiveType,
OptionType,
OptionsConfig,
} from './types';
/**
* Configuration options for the {@linkcode createOptionsGetter} function.
* @group Options
*/
interface CreateOptionsGetterOptions<
TOptionsConfig extends OptionsConfig,
TOptions extends OptionValues = {},
> {
/** The options config. */
optionsConfig: TOptionsConfig;
/** The initial option values. */
optionValues?: TOptions;
/** The client to use for prompting. */
client?: Client;
/**
* A function to call when the user cancels a prompt. By default, this will
* exit the process.
*/
onPromptCancel?: () => void;
}
/**
* Converts command options to a getter object.
*
* This function transforms an `Options` object into a
* `CommandOptionsGetter` object that has getter methods for each of the
* options. The getters can then be used to retrieve the values of the options
* dynamically. If an option has defined aliases, the returned getter will have
* additional getter methods for each alias, all pointing to the original
* option's value.
*
* Additionally, the returned object has a `get` method, which accepts an array
* of option keys and returns an object with the corresponding camelCased
* key-value pairs.
*
* @example
* const optionsConfig = {
* f: {
* type: 'string',
* alias: ['foo'],
* default: 'default foo'
* },
* };
* const optionsGetter = createOptionsGetter({ optionsConfig });
* const val = await optionsGetter.foo(); // 'default foo'
*
* @group Options
*/
// TODO: Cache the result of this function
export function createOptionsGetter<
TKey extends string = string,
TType extends OptionType = OptionType,
TOptionsConfig extends OptionsConfig<TKey, TType> = OptionsConfig<
TKey,
TType
>,
TOptionValues extends OptionValues = OptionValues,
>({
client = new Client(),
optionsConfig,
optionValues = {} as TOptionValues,
onPromptCancel = process.exit,
}: CreateOptionsGetterOptions<
TOptionsConfig,
TOptionValues
>): OptionsGetter<TOptionsConfig> {
// create a new getter object with the values
const getter: OptionsGetter = {
values: { ...optionValues },
// getter for all option values
get: async (keys: string[]) => {
const result: Record<string, unknown> = {};
// get the value for each key
for (const key of keys) {
const value = await getter[key]?.();
result[key] = value;
result[camelCase(key)] = value;
}
return result;
},
} as any;
// iterate over all keys in the options config
for (const configKey in optionsConfig) {
// get the config for the option's key
const config = optionsConfig[configKey];
// get all keys for the option, including the option key, aliases, and
// camelCased versions of each
let allKeysForOption: string[] = [configKey, ...(config.alias || [])];
allKeysForOption = [
...allKeysForOption,
...allKeysForOption.map((key) => camelCase(key)),
];
// loop through the keys once to find the first one with an entry in
// optionValues
let keyWithValue: string | undefined;
for (const key of allKeysForOption) {
if (key in optionValues) {
keyWithValue = key;
break;
}
}
// loop through the keys again to set values and create getters
for (const key of allKeysForOption) {
// set values
// to be set if there was a keyWithValue?
getter.values[key] = keyWithValue
? optionValues[keyWithValue]
: (config.default as OptionPrimitiveType | undefined);
// create a getter fn for the key
const getterFn = createOptionGetter({
name: key,
config,
client,
value: keyWithValue ? optionValues[keyWithValue] : undefined,
onPromptCancel,
});
// wrap the getter function to update the values object
const wrappedGetterFn = async (...args: Parameters<typeof getterFn>) => {
const value = (await getterFn(...args)) as OptionPrimitiveType;
// TODO: Update all keys for the option? This would add more overhead to
// the getter, but could potentially improve the experience for complex
// commands chains that may access the same option multiple times with
// different keys.
getter.values[key] = value;
return value;
};
getter[key] = wrappedGetterFn;
}
}
return getter as OptionsGetter<TOptionsConfig>;
}
/**
* An object that can be used to dynamically retrieve the values of command
* options, including aliases. Options can be retrieved by their original key,
* any of their aliases, or camelCased versions of either.
* @group Options
*/
export type OptionsGetter<TOptions extends OptionsConfig = OptionsConfig> = {
[K in keyof TOptions as
| K
| OptionAlias<TOptions[K]>
| CamelCase<K | OptionAlias<TOptions[K]>>]: OptionGetter<
CommandOptionType<TOptions[K]>
>;
} & {
/**
* Get the values of the specified options. This is useful when you want to
* get the values of multiple options at once.
* @param optionNames - The names of the options to get.
* @returns An object with the values of the specified options keyed by
* both their original keys and camelCased keys.
*/
get<K extends keyof TOptions | OptionAlias<TOptions[keyof TOptions]>>(
optionNames: K[],
): Promise<{
[O in K as O | CamelCase<O>]: CommandOptionsTypes<TOptions>[O];
}>;
/**
* Direct access to the values of the options, keyed by their original keys,
* aliases, and camelCased versions of both. This is useful for checking
* option values without triggering any validation or prompting.
*/
readonly values: {
[K in keyof TOptions as
| K
| OptionAlias<TOptions[K]>
| CamelCase<K | OptionAlias<TOptions[K]>>]: CommandOptionType<
TOptions[K]
>;
};
};
/**
* Get a union of all aliases for an option.
* @group Options
*/
export type OptionAlias<T extends OptionConfig> = T extends {
alias: string[];
}
? T['alias'][number]
: never;
/**
* Get the primitive type for an option considering whether it is required or
* has a default value. If neither is true, the type is the primitive type
* unioned with `undefined`.
*/
type CommandOptionType<T extends OptionConfig> = T['required'] extends true
? OptionPrimitiveType<T['type']>
: T['default'] extends MaybeReadonly<OptionPrimitiveType<T['type']>>
? OptionPrimitiveType<T['type']>
: OptionPrimitiveType<T['type']> | undefined;
/**
* Get the primitive type for each option in an options config.
*/
type CommandOptionsTypes<T extends OptionsConfig> = {
[K in keyof T]: CommandOptionType<T[K]>;
} & {
[K in keyof T as OptionAlias<T[K]>]: CommandOptionType<T[K]>;
};