argot-cli 0.0.0-stage → 0.4.0-beta
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -0
- package/LICENSE +13 -0
- package/README.md +2 -2
- package/package.json +23 -4
- package/src/arg_parser.ts +586 -0
- package/src/errors.ts +272 -0
- package/src/mod.ts +49 -0
- package/src/parser_config.ts +104 -0
- package/src/read_config.ts +14 -0
- package/src/types.ts +262 -0
- package/src/utils.ts +219 -0
- package/tsconfig.json +35 -0
package/src/utils.ts
ADDED
|
@@ -0,0 +1,219 @@
|
|
|
1
|
+
import {
|
|
2
|
+
AliasTargetNotFoundError,
|
|
3
|
+
ConfigError,
|
|
4
|
+
InvalidAliasTargetError,
|
|
5
|
+
InvalidFloatError,
|
|
6
|
+
InvalidIntError,
|
|
7
|
+
InvalidOptionTypeError,
|
|
8
|
+
MissingOptionPropertyError,
|
|
9
|
+
MissingOptionTypeError,
|
|
10
|
+
UnsafeIntegerError,
|
|
11
|
+
} from './errors.ts';
|
|
12
|
+
import type {
|
|
13
|
+
ConfigEntry,
|
|
14
|
+
OptionType,
|
|
15
|
+
AliasEntry,
|
|
16
|
+
FloatEntry,
|
|
17
|
+
TextEntry,
|
|
18
|
+
IntEntry,
|
|
19
|
+
ListEntry,
|
|
20
|
+
ConfigEntries,
|
|
21
|
+
} from './types.ts';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Parses a string as an integer.
|
|
25
|
+
* The value must consist only of an optional sign followed by digits.
|
|
26
|
+
* The resulting number must be safely representable by the runtime.
|
|
27
|
+
* Throws:
|
|
28
|
+
* - InvalidIntError if the value is not a valid integer string
|
|
29
|
+
* - UnsafeIntegerError if the parsed value cannot be safely represented
|
|
30
|
+
*/
|
|
31
|
+
export function parseIntStrict(value: string): number {
|
|
32
|
+
if (!/^(-|\+)?\d+$/.test(value)) {
|
|
33
|
+
throw new InvalidIntError(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const num = Number(value);
|
|
37
|
+
if (!Number.isSafeInteger(num)) {
|
|
38
|
+
throw new UnsafeIntegerError(num);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return num;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Parses a string as a floating-point number.
|
|
46
|
+
* The value must consist only of:
|
|
47
|
+
* - an optional sign
|
|
48
|
+
* - digits
|
|
49
|
+
* - an optional fractional part
|
|
50
|
+
* - an optional exponent
|
|
51
|
+
*
|
|
52
|
+
* Whitespace, Infinity and NaN are not allowed.
|
|
53
|
+
*
|
|
54
|
+
* Throws:
|
|
55
|
+
* - InvalidFloatError if the value is not a valid float string
|
|
56
|
+
*/
|
|
57
|
+
export function parseFloatStrict(value: string): number {
|
|
58
|
+
if (!/^(-|\+)?(\d+(\.\d+)?|\.\d+)([eE](-|\+)?\d+)?$/.test(value)) {
|
|
59
|
+
throw new InvalidFloatError(value);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const num = Number(value);
|
|
63
|
+
if (!Number.isFinite(num)) {
|
|
64
|
+
throw new InvalidFloatError(value);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
return num;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Validates a single configuration entry.
|
|
72
|
+
* The entry must be a labeled object containing at least the "option"
|
|
73
|
+
* and "type" properties. Additional properties are validated according
|
|
74
|
+
* to the entry type.
|
|
75
|
+
* Throws:
|
|
76
|
+
* - TypeError if the entry is not an object or contains invalid values
|
|
77
|
+
* - MissingOptionPropertyError if a required property is missing
|
|
78
|
+
* - InvalidOptionTypeError if the option type is not supported
|
|
79
|
+
*/
|
|
80
|
+
export function validateEntry(name: string, entry: ConfigEntry): void {
|
|
81
|
+
if (entry == null || typeof entry !== 'object') {
|
|
82
|
+
throw new TypeError('option config entry must be an object');
|
|
83
|
+
} else if (!Object.hasOwn(entry, 'type')) {
|
|
84
|
+
throw new MissingOptionTypeError(name);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const tag: OptionType = entry.type;
|
|
88
|
+
|
|
89
|
+
switch (tag) {
|
|
90
|
+
case 'flag':
|
|
91
|
+
case 'count': {
|
|
92
|
+
/* these types have no extra mandatory values */
|
|
93
|
+
break;
|
|
94
|
+
}
|
|
95
|
+
case 'text': {
|
|
96
|
+
if (!Object.hasOwn(entry, 'default'))
|
|
97
|
+
break;
|
|
98
|
+
|
|
99
|
+
const { default: def } = entry as TextEntry;
|
|
100
|
+
if (typeof def !== 'string')
|
|
101
|
+
throw new TypeError('default value must be a string');
|
|
102
|
+
|
|
103
|
+
break;
|
|
104
|
+
}
|
|
105
|
+
case 'int': {
|
|
106
|
+
if (!Object.hasOwn(entry, 'default'))
|
|
107
|
+
break;
|
|
108
|
+
|
|
109
|
+
const { default: def } = entry as IntEntry;
|
|
110
|
+
if (typeof def !== 'number' || !Number.isInteger(def))
|
|
111
|
+
throw new TypeError('default value must be an integer');
|
|
112
|
+
|
|
113
|
+
break;
|
|
114
|
+
}
|
|
115
|
+
case 'float': {
|
|
116
|
+
if (!Object.hasOwn(entry, 'default'))
|
|
117
|
+
break;
|
|
118
|
+
|
|
119
|
+
const { default: def } = entry as FloatEntry;
|
|
120
|
+
if (typeof def !== 'number' || !Number.isFinite(def))
|
|
121
|
+
throw new TypeError('default value must be a number');
|
|
122
|
+
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
case 'list': {
|
|
126
|
+
if (!Object.hasOwn(entry, 'sep'))
|
|
127
|
+
break;
|
|
128
|
+
|
|
129
|
+
const { sep } = entry as ListEntry;
|
|
130
|
+
if (typeof sep !== 'string')
|
|
131
|
+
throw new TypeError('sep value must be a string');
|
|
132
|
+
|
|
133
|
+
break;
|
|
134
|
+
}
|
|
135
|
+
case 'alias': {
|
|
136
|
+
if (!Object.hasOwn(entry, 'target')) {
|
|
137
|
+
throw new MissingOptionPropertyError(name, 'target');
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
const { target } = entry as AliasEntry;
|
|
141
|
+
if (typeof target !== 'string')
|
|
142
|
+
throw new TypeError('target value must be a string');
|
|
143
|
+
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
default: {
|
|
147
|
+
throw new InvalidOptionTypeError(tag);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Validates a set of configuration entries.
|
|
154
|
+
* Each entry is validated individually. Alias entries are validated
|
|
155
|
+
* after all entries have been processed to ensure that:
|
|
156
|
+
* - the target exists
|
|
157
|
+
* - the target is not itself an alias
|
|
158
|
+
* Throws:
|
|
159
|
+
* - Any error raised by validateEntry
|
|
160
|
+
* - AliasTargetNotFoundError if an alias target does not exist
|
|
161
|
+
* - InvalidAliasTargetError if an alias targets another alias
|
|
162
|
+
*/
|
|
163
|
+
export function validateEntries(entries: ConfigEntries): void {
|
|
164
|
+
const aliases: [string, string][] = [];
|
|
165
|
+
|
|
166
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
167
|
+
validateEntry(name, entry);
|
|
168
|
+
const tag: OptionType = entry.type;
|
|
169
|
+
if (tag === 'alias') {
|
|
170
|
+
const { target } = entry as AliasEntry;
|
|
171
|
+
aliases.push([name, target]);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
for (const [name, target] of aliases) {
|
|
176
|
+
if (!Object.hasOwn(entries, target)) {
|
|
177
|
+
throw new AliasTargetNotFoundError(name, target);
|
|
178
|
+
}
|
|
179
|
+
const targetEntry: ConfigEntry = entries[target]!;
|
|
180
|
+
if (targetEntry.type === 'alias') {
|
|
181
|
+
throw new InvalidAliasTargetError(name, target);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function validateEntriesAggregate(entries: ConfigEntries): void {
|
|
187
|
+
const aliases: [string, string][] = [];
|
|
188
|
+
const error = new ConfigError('parser configuration is not valid');
|
|
189
|
+
|
|
190
|
+
for (const [name, entry] of Object.entries(entries)) {
|
|
191
|
+
try {
|
|
192
|
+
validateEntry(name, entry);
|
|
193
|
+
} catch (err) {
|
|
194
|
+
error.push(err as Error);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
const tag: OptionType = entry.type;
|
|
198
|
+
if (tag === 'alias') {
|
|
199
|
+
const { target } = entry as AliasEntry;
|
|
200
|
+
aliases.push([name, target]);
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
for (const [name, target] of aliases) {
|
|
205
|
+
if (!Object.hasOwn(entries, target)) {
|
|
206
|
+
error.push(new AliasTargetNotFoundError(name, target));
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const targetEntry: ConfigEntry = entries[target]!;
|
|
210
|
+
if (targetEntry.type === 'alias') {
|
|
211
|
+
error.push(new InvalidAliasTargetError(name, target));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (error.errors.length === 1)
|
|
216
|
+
throw error.errors[0];
|
|
217
|
+
else if (error.errors.length > 1)
|
|
218
|
+
throw error;
|
|
219
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"target": "es2022",
|
|
5
|
+
"lib": ["es2022"],
|
|
6
|
+
"types": ["node"],
|
|
7
|
+
"module": "nodenext",
|
|
8
|
+
"rootDir": "./src/",
|
|
9
|
+
"moduleResolution": "nodenext",
|
|
10
|
+
"outDir": "./dist/",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"esModuleInterop": true,
|
|
13
|
+
"forceConsistentCasingInFileNames": true,
|
|
14
|
+
"strict": true,
|
|
15
|
+
"noImplicitAny": true,
|
|
16
|
+
"strictNullChecks": true,
|
|
17
|
+
"strictFunctionTypes": true,
|
|
18
|
+
"strictBindCallApply": true,
|
|
19
|
+
"strictPropertyInitialization": true,
|
|
20
|
+
"useUnknownInCatchVariables": true,
|
|
21
|
+
"alwaysStrict": true,
|
|
22
|
+
"noUnusedLocals": true,
|
|
23
|
+
"noUnusedParameters": true,
|
|
24
|
+
"noImplicitReturns": true,
|
|
25
|
+
"noFallthroughCasesInSwitch": false,
|
|
26
|
+
"noUncheckedIndexedAccess": true,
|
|
27
|
+
"noImplicitOverride": true,
|
|
28
|
+
"noPropertyAccessFromIndexSignature": false,
|
|
29
|
+
"allowUnusedLabels": false,
|
|
30
|
+
"allowUnreachableCode": false,
|
|
31
|
+
"skipLibCheck": true,
|
|
32
|
+
"rewriteRelativeImportExtensions": true
|
|
33
|
+
},
|
|
34
|
+
"include": ["src/*.ts"]
|
|
35
|
+
}
|