Skip to content

Commit d079f1e

Browse files
committed
Refactored, abstracting flag functionality into its own class
1 parent e204e28 commit d079f1e

File tree

5 files changed

+474
-372
lines changed

5 files changed

+474
-372
lines changed

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,8 @@ Args.alias({
4444
// Optionally specify a list of possible flag values (autocreates the flag if it does not already exist, updates options if it does)
4545
Args.setOptions('name', 'john', 'jane')
4646

47-
// Restrict a flag to a single value (applies when the same flag is passed multiple times).
48-
Args.single('c')
47+
// Allow a flag to accept multiple values (applies when the same flag is defined multiple times).
48+
Args.allowMultiple('c')
4949

5050
// Do not allow unrecognized flags
5151
Args.disallowUnrecognized()
@@ -91,7 +91,7 @@ Args.configure({
9191
},
9292
c: {
9393
default: 'test',
94-
single: true
94+
allowMultiple: true
9595
},
9696
input: {
9797
alias: 'in'
@@ -146,7 +146,7 @@ The following attributes configuration attributes can be set for each flag:
146146
- `type` - The data type. Supports primitives like `Boolean` or their text (typeof) equivalent (i.e. "`boolean`").
147147
- `alias` - A string representing an alternative name for the flag.
148148
- `aliases` - Support for multiple aliases.
149-
- `single` - If a flag is specified more than one, only a single value (the last one specified) will be used.
149+
- `allowMultiple` - If a flag is specified more than once, capture all values (instead of only the last one specified).
150150
- `options` - An array of valid values for the flag.
151151

152152
### configure({...})
@@ -162,7 +162,7 @@ Args.configure({
162162
default: value,
163163
type: string_or_primitive, // example: 'boolean' or Boolean
164164
alias: string,
165-
single: true/false,
165+
allowMultiple: true/false,
166166
options: [...]
167167
}, {
168168
...
@@ -194,9 +194,9 @@ Identify aliases for recognized flags.
194194

195195
Automatically executes `recognize` for any flags specified amongst the defaults.
196196

197-
### single('flag1', 'flag2', ...)
197+
### allowMultiple('flag1', 'flag2', ...)
198198

199-
By default, a flag can be passed in multiple times providing multiple values for the same flag. This method can be used to retrieve only one value (the last one specified).
199+
By default, if the same flag is defined multiple times, only the last value is recognized. Setting `allowMultiple` on a flag will capture all values (as an array).
200200

201201
Automatically executes `recognize` for any flags specified amongst the defaults.
202202

flag.js

Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
export default class Flag {
2+
#name
3+
#description
4+
#default = null
5+
#alias = new Set()
6+
#required = false
7+
#type = String
8+
#allowMultipleValues = false
9+
#strictTypes = true
10+
#enum = new Set()
11+
#value = null
12+
#violations = new Set()
13+
#recognized = false
14+
15+
constructor (cfg = {}) {
16+
if (typeof cfg === 'string') {
17+
cfg = { name: cfg }
18+
}
19+
20+
if (!cfg.name) {
21+
throw new Error('Flag name is required.')
22+
}
23+
24+
this.#name = cfg.name.replace(/^-+/gi, '').trim()
25+
26+
if (cfg.hasOwnProperty('description')) {
27+
this.#description = cfg.description
28+
}
29+
30+
if (cfg.hasOwnProperty('default')) {
31+
this.#default = cfg.default
32+
}
33+
34+
if (cfg.hasOwnProperty('alias')) {
35+
this.createAlias(cfg.alias)
36+
}
37+
38+
if (cfg.hasOwnProperty('aliases')) {
39+
this.createAlias(cfg.aliases)
40+
}
41+
42+
if (cfg.hasOwnProperty('required')) {
43+
this.#required = cfg.required
44+
}
45+
46+
if (cfg.hasOwnProperty('type')) {
47+
this.type = cfg.type
48+
}
49+
50+
if (cfg.hasOwnProperty('allowMultipleValues')) {
51+
this.#allowMultipleValues = cfg.allowMultipleValues
52+
}
53+
54+
if (cfg.hasOwnProperty('strictTypes')) {
55+
this.#strictTypes = cfg.strictTypes
56+
}
57+
58+
if (cfg.hasOwnProperty('options')) {
59+
this.options = cfg.options
60+
}
61+
}
62+
63+
get recognized () {
64+
return this.#recognized
65+
}
66+
67+
set recognized (value) {
68+
this.#recognized = value
69+
}
70+
71+
get required () {
72+
return this.#required
73+
}
74+
75+
set required (value) {
76+
this.#required = value
77+
}
78+
79+
get valid () {
80+
const value = this.value
81+
82+
this.#violations = new Set()
83+
84+
if (this.#required) {
85+
if (this.#allowMultipleValues ? this.value.length === 0 : this.value === null) {
86+
this.#violations = new Set([`"${this.#name}" is required.`])
87+
return false
88+
}
89+
}
90+
91+
if (this.#enum.size > 0) {
92+
if (this.#allowMultipleValues) {
93+
const invalid = value.filter(item => !this.#enum.has(item))
94+
95+
if (invalid.length > 0) {
96+
invalid.forEach(v => this.#violations.add(`"${v}" is invalid. Expected one of: ${Array.from(this.#enum).join(', ')}`))
97+
return false
98+
}
99+
} else if (!this.#enum.has(value)) {
100+
this.#violations.add(`"${value}" is invalid. Expected one of: ${ Array.from(this.#enum).join(', ') }`)
101+
return false
102+
}
103+
}
104+
105+
if (this.#strictTypes) {
106+
const type = this.type
107+
108+
if (type !== 'any' && type !== '*') {
109+
if (this.#allowMultipleValues) {
110+
const invalidTypes = value.filter(item => typeof item !== type)
111+
112+
if (invalidTypes.length > 0) {
113+
invalidTypes.forEach(v => this.#violations.add(`"${v}" should be a ${ type }, not ${ typeof v }.`))
114+
return false
115+
}
116+
} else if (value !== null && typeof value !== type) {
117+
this.#violations.add(`"${value}" should be a ${ type }, not ${typeof value}.`)
118+
return false
119+
}
120+
}
121+
}
122+
123+
return true
124+
}
125+
126+
get violations () {
127+
if (this.valid) {
128+
return []
129+
}
130+
131+
return Array.from(this.#violations)
132+
}
133+
134+
get type () {
135+
return this.#type.name.split(/\s+/)[0].toLowerCase()
136+
}
137+
138+
set type (value) {
139+
if (typeof value === 'string') {
140+
switch (value.trim().toLowerCase()) {
141+
case 'number':
142+
case 'integer':
143+
case 'float':
144+
case 'double':
145+
this.#type = Number
146+
break
147+
case 'bigint':
148+
this.#type = BigInt
149+
break
150+
case 'boolean':
151+
this.#type = Boolean
152+
break
153+
default:
154+
this.#type = String
155+
}
156+
} else {
157+
this.#type = value
158+
}
159+
}
160+
161+
get strictTypes () {
162+
return this.#strictTypes
163+
}
164+
165+
set strictTypes (value) {
166+
if (typeof value !== 'boolean') {
167+
throw new Error('strictTypes must be a boolean value.')
168+
}
169+
170+
this.#strictTypes = value
171+
}
172+
173+
get name () {
174+
return this.#name
175+
}
176+
177+
set name (value) {
178+
this.#name = value.trim()
179+
}
180+
181+
get description() {
182+
return this.#name
183+
}
184+
185+
set description(value) {
186+
this.#description = value.trim()
187+
}
188+
189+
get value () {
190+
if (this.#allowMultipleValues && (this.#value === null)) {
191+
if (this.#default === null) {
192+
return []
193+
}
194+
195+
if (!Array.isArray(this.#default)) {
196+
return [this.#default]
197+
}
198+
}
199+
200+
return this.#value || this.#default
201+
}
202+
203+
set value (value) {
204+
if (this.#allowMultipleValues) {
205+
if (Array.isArray(value)) {
206+
this.#value = value
207+
return
208+
}
209+
210+
this.#value = this.#value || []
211+
this.#value.push(value)
212+
} else {
213+
this.#value = value
214+
}
215+
}
216+
217+
get options () {
218+
return Array.from(this.#enum)
219+
}
220+
221+
set options (value) {
222+
if (typeof value === 'string') {
223+
value = value.split(',').map(option => option.trim())
224+
}
225+
226+
this.#enum = new Set(value)
227+
}
228+
229+
get aliases () {
230+
return Array.from(this.#alias)
231+
}
232+
233+
hasAlias (alias) {
234+
return this.#alias.has(alias)
235+
}
236+
237+
createAlias() {
238+
for (const alias of arguments) {
239+
if (Array.isArray(alias)) {
240+
this.createAlias(...alias)
241+
} else {
242+
this.#alias.add(alias.replace(/^-+/gi, ''))
243+
}
244+
}
245+
}
246+
247+
allowMultipleValues () {
248+
if (!this.#allowMultipleValues) {
249+
if (this.#value !== null) {
250+
this.#value = [this.#value]
251+
}
252+
253+
if (this.#default !== null) {
254+
this.#default = [this.#default]
255+
}
256+
257+
this.#allowMultipleValues = true
258+
}
259+
}
260+
261+
preventMultipleValues () {
262+
if (this.#allowMultipleValues) {
263+
if (this.#value !== null) {
264+
this.#value = this.#value.pop()
265+
}
266+
267+
if (this.#default !== null) {
268+
this.#default = this.#default.pop()
269+
}
270+
271+
this.#allowMultipleValues = false
272+
}
273+
}
274+
}

0 commit comments

Comments
 (0)