Skip to content

Commit

Permalink
feat: add required option (#2)
Browse files Browse the repository at this point in the history
  • Loading branch information
privatenumber committed Dec 7, 2021
1 parent fa7e869 commit 179d739
Show file tree
Hide file tree
Showing 5 changed files with 120 additions and 14 deletions.
28 changes: 24 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,8 @@ const parsed = typeFlag(process.argv.slice(2), {

someBoolean: {
type: Boolean,
alias: 'b'
alias: 'b',
required: true
},

// Wrap with an array to indicate an array type
Expand All @@ -46,16 +47,16 @@ const parsed = typeFlag(process.argv.slice(2), {
}
})

console.log(parsed.flags.someString[0])
console.log(parsed.flags.someString)
```

`parsed` resolves to the following type:
```ts
{
flags: {
someString: string | undefined;
someNumber: boolean | undefined;
someBoolean: number | undefined;
someNumber: number | undefined;
someBoolean: boolean;
stringArray: string[];
numberArray: number[];
};
Expand All @@ -68,6 +69,24 @@ console.log(parsed.flags.someString[0])

### Usage

#### Required flags
Non-array types can be `undefined` unless `required: true` is set:
```ts
const parsed = typeFlag(process.argv.slice(2), {
someNumber: {
type: Number,
required: true
}
})
```

When a flag is required, the return type can no longer be `undefined`:
```ts
{
someNumber: number;
}
```

#### kebab-case flags mapped to camelCase
When passing in the flags, they can be in kebab-case and will automatically map to the camelCase equivalent.
```sh
Expand Down Expand Up @@ -253,6 +272,7 @@ type FlagSchema = {
[flagName: string]: TypeFunction | [TypeFunction] | {
type: TypeFunction | [TypeFunction];
alias?: string;
required?: true;
};
};
```
Expand Down
3 changes: 3 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
mapAliases,
parseFlag,
getDefaultFromTypeWithValue,
validateFlags,
} from './utils';

const isAliasPattern = /^-[\da-z]+/i;
Expand Down Expand Up @@ -154,6 +155,8 @@ function typeFlag<Schemas extends Flags>(
expectingValue();
}

validateFlags(schemas, flags);

return {
flags,
unknownFlags,
Expand Down
7 changes: 6 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ export type TypeFunctionArray<T = any> = [TypeFunction<T>];
export type FlagSchema = {
type: TypeFunction | TypeFunctionArray;
alias?: string;
required?: true;
};

export type FlagTypeOrSchema = TypeFunction | TypeFunctionArray | FlagSchema;
Expand All @@ -21,7 +22,11 @@ export type InferFlagType<
Flag extends FlagTypeOrSchema
> = Flag extends (TypeFunction<infer T> | { type: TypeFunction<infer T> })
// Type function return-type
? (T | undefined)
? (
Flag extends { required: true }
? T
: T | undefined
)

// Type function return-type in array
: (
Expand Down
47 changes: 38 additions & 9 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,26 +78,26 @@ export function mapAliases<Schemas extends Flags>(
schema: FlagSchema;
}>();

for (const name in schemas) {
if (!hasOwn(schemas, name)) {
for (const flagName in schemas) {
if (!hasOwn(schemas, flagName)) {
continue;
}

validateFlagName(schemas, name);
validateFlagName(schemas, flagName);

const schema = schemas[name] as FlagSchema;
const schema = schemas[flagName] as FlagSchema;
if (schema && typeof schema === 'object') {
const { alias } = schema;
if (alias) {
assert(alias.length > 0, `Invalid flag alias ${stringify(name)}: flag alias cannot be empty`);
assert(alias.length === 1, `Invalid flag alias ${stringify(name)}: flag aliases can only be a single-character`);
assert(alias.length > 0, `Invalid flag alias ${stringify(flagName)}: flag alias cannot be empty`);
assert(alias.length === 1, `Invalid flag alias ${stringify(flagName)}: flag aliases can only be a single-character`);
assert(
!aliases.has(alias),
`Flag collision: Alias "${alias}" is already used`,
);

aliases.set(alias, {
name,
name: flagName,
schema,
});
}
Expand All @@ -112,10 +112,12 @@ const isArrayType = (schema: FlagTypeOrSchema) => {
return false;
}

return (Array.isArray(schema) || Array.isArray(schema.type));
return Array.isArray(schema) || Array.isArray(schema.type);
};

export const createFlagsObject = <Schemas extends Flags>(schema: Flags) => {
export const createFlagsObject = <Schemas extends Flags>(
schema: Schemas,
) => {
const flags: Record<string, any> = {};

for (const flag in schema) {
Expand Down Expand Up @@ -143,3 +145,30 @@ export const getDefaultFromTypeWithValue = (

return value;
};

export const validateFlags = <Schemas extends Flags>(
schemas: Schemas,
flags: Record<keyof Schemas, any>,
) => {
for (const flagName in schemas) {
if (!hasOwn(schemas, flagName)) {
continue;
}

const schema = schemas[flagName];

if (
schema
&& ('required' in schema)
&& schema.required
) {
const value = flags[flagName];
if (
value === undefined
|| (Array.isArray(value) && value.length === 0)
) {
throw new Error(`Missing required option "--${flagName}"`);
}
}
}
};
49 changes: 49 additions & 0 deletions tests/type-flag.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,15 @@ describe('Parsing', () => {
expect<string | undefined>(parsed.flags.someString).toEqual('4');
});

test('kebab-case flags', () => {
const parsed = typeFlag(['--some-string=2', '--someString=3', '--some-string=4'], {
'some-string': String,
});

expect<string | undefined>(parsed.flags['some-string']).toEqual('4');
expect(!('someString' in parsed.flags)).toBe(true);
});

test('flag=', () => {
const parsed = typeFlag(['--string=hello', '-s=bye', '--string=', '--boolean=true', '--boolean=false', '--boolean=', 'world', '--number=3.14', '--number='], {
string: {
Expand Down Expand Up @@ -293,4 +302,44 @@ describe('Parsing', () => {
expect<number[]>(parsed.flags.numberArray).toEqual([1, 2]);
expect<string[]>(parsed._).toEqual(['world']);
});

describe('Required flag', () => {
test('Types and parsing', () => {
const parsed = typeFlag(['--string', 'hello', '--boolean', '--number', '1'], {
string: {
type: String,
required: true,
},
boolean: {
type: Boolean,
required: true,
},
number: Number,
});

expect<string>(parsed.flags.string).toEqual('hello');
expect<boolean>(parsed.flags.boolean).toEqual(true);
expect<number | undefined>(parsed.flags.number).toEqual(1);
});

test('Throw on missing', () => {
expect(() => {
typeFlag([], {
flagA: {
type: String,
required: true,
},
});
}).toThrow(/* 'Missing required option "--flagA"' */);

expect(() => {
typeFlag([], {
flagA: {
type: [String],
required: true,
},
});
}).toThrow(/* 'Missing required option "--flagA"' */);
});
});
});

0 comments on commit 179d739

Please sign in to comment.