Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix issues with enum options filtering logic in enum_ function #941

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 64 additions & 15 deletions library/src/schemas/enum/enum.test-d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,46 +3,95 @@ import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
import { enum_, type EnumIssue, type EnumSchema } from './enum.ts';

describe('enum_', () => {
enum options {
option1 = 'foo',
option2 = 'bar',
enum normalOptions {
options1 = 'foo',
options2 = 'bar',
options3 = 'baz',
}
type NormalOptions = typeof normalOptions;
enum abnormalOptions {
option1 = 1.7976931348623157e308,
option2 = 5e-324,
option3 = 'baz',
'Infinity' = 1,
'-Infinity' = 2,
'NaN' = 3,
}
type Options = typeof options;
type AbnormalOptions = typeof abnormalOptions;
const enumLikeObject = {
1: 'foo',
'1.7976931348623157e+308': 'bar',
'1.7976931348623157e308': 'baz',
'5e-324': 'qux',
NaN: 'NaN',
Infinity: 'Infinity',
} as const;
type EnumLikeObject = typeof enumLikeObject;

describe('should return schema object', () => {
test('with undefined message', () => {
type Schema = EnumSchema<Options, undefined>;
expectTypeOf(enum_(options)).toEqualTypeOf<Schema>();
expectTypeOf(enum_(options, undefined)).toEqualTypeOf<Schema>();
type AbnormalSchema = EnumSchema<AbnormalOptions, undefined>;
expectTypeOf(enum_(abnormalOptions)).toEqualTypeOf<AbnormalSchema>();
expectTypeOf(
enum_(abnormalOptions, undefined)
).toEqualTypeOf<AbnormalSchema>();
type NormalSchema = EnumSchema<NormalOptions, undefined>;
expectTypeOf(enum_(normalOptions)).toEqualTypeOf<NormalSchema>();
expectTypeOf(
enum_(normalOptions, undefined)
).toEqualTypeOf<NormalSchema>();
});

test('with string message', () => {
expectTypeOf(enum_(options, 'message')).toEqualTypeOf<
EnumSchema<Options, 'message'>
expectTypeOf(enum_(abnormalOptions, 'message')).toEqualTypeOf<
EnumSchema<AbnormalOptions, 'message'>
>();
expectTypeOf(enum_(normalOptions, 'message')).toEqualTypeOf<
EnumSchema<NormalOptions, 'message'>
>();
});

test('with function message', () => {
expectTypeOf(enum_(options, () => 'message')).toEqualTypeOf<
EnumSchema<Options, () => string>
expectTypeOf(enum_(abnormalOptions, () => 'message')).toEqualTypeOf<
EnumSchema<AbnormalOptions, () => string>
>();
expectTypeOf(enum_(normalOptions, () => 'message')).toEqualTypeOf<
EnumSchema<NormalOptions, () => string>
>();
});
});

describe('should infer correct types', () => {
type Schema = EnumSchema<Options, undefined>;
type AbnormalSchema = EnumSchema<AbnormalOptions, undefined>;
type NormalSchema = EnumSchema<NormalOptions, undefined>;
type EnumLikeObjectSchema = EnumSchema<EnumLikeObject, undefined>;

test('of input', () => {
expectTypeOf<InferInput<Schema>>().toEqualTypeOf<options>();
expectTypeOf<InferInput<AbnormalSchema>>().toEqualTypeOf<
| abnormalOptions.option1
| abnormalOptions.option2
| abnormalOptions.option3
>();
expectTypeOf<InferInput<NormalSchema>>().toEqualTypeOf<normalOptions>;
expectTypeOf<InferInput<EnumLikeObjectSchema>>().toEqualTypeOf<'baz'>;
});

test('of output', () => {
expectTypeOf<InferOutput<Schema>>().toEqualTypeOf<options>();
expectTypeOf<InferOutput<AbnormalSchema>>().toEqualTypeOf<
| abnormalOptions.option1
| abnormalOptions.option2
| abnormalOptions.option3
>();
expectTypeOf<InferOutput<NormalSchema>>().toEqualTypeOf<normalOptions>();
expectTypeOf<InferOutput<EnumLikeObjectSchema>>().toEqualTypeOf<'baz'>();
});

test('of issue', () => {
expectTypeOf<InferIssue<Schema>>().toEqualTypeOf<EnumIssue>();
expectTypeOf<InferIssue<AbnormalSchema>>().toEqualTypeOf<EnumIssue>();
expectTypeOf<InferIssue<NormalSchema>>().toEqualTypeOf<EnumIssue>();
expectTypeOf<
InferIssue<EnumLikeObjectSchema>
>().toEqualTypeOf<EnumIssue>();
});
});
});
81 changes: 81 additions & 0 deletions library/src/schemas/enum/enum.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,15 @@ describe('enum_', () => {
option3 = 'baz',
}
type Options = typeof options;
const enumLikeObject = {
1: 'foo',
'1.7976931348623157e+308': 'bar',
'1.7976931348623157e308': 'baz',
'5e-324': 'qux',
NaN: 'NaN',
Infinity: 'Infinity',
} as const;
type EnumLikeObject = typeof enumLikeObject;

describe('should return schema object', () => {
const baseSchema: Omit<EnumSchema<Options, never>, 'message'> = {
Expand Down Expand Up @@ -53,6 +62,47 @@ describe('enum_', () => {
} satisfies EnumSchema<Options, typeof message>);
});
});
describe('should return schema object for enum like object', () => {
const baseSchema: Omit<EnumSchema<EnumLikeObject, never>, 'message'> = {
kind: 'schema',
type: 'enum',
reference: enum_,
expects: '"baz"',
enum: enumLikeObject,
options: ['baz'],
async: false,
'~standard': {
version: 1,
vendor: 'valibot',
validate: expect.any(Function),
},
'~run': expect.any(Function),
};

test('with undefined message', () => {
const schema: EnumSchema<EnumLikeObject, undefined> = {
...baseSchema,
message: undefined,
};
expect(enum_(enumLikeObject)).toStrictEqual(schema);
expect(enum_(enumLikeObject, undefined)).toStrictEqual(schema);
});

test('with string message', () => {
expect(enum_(enumLikeObject, 'message')).toStrictEqual({
...baseSchema,
message: 'message',
} satisfies EnumSchema<EnumLikeObject, 'message'>);
});

test('with function message', () => {
const message = () => 'message';
expect(enum_(enumLikeObject, message)).toStrictEqual({
...baseSchema,
message,
} satisfies EnumSchema<EnumLikeObject, typeof message>);
});
});

describe('should return dataset without issues', () => {
test('for valid options', () => {
Expand All @@ -69,6 +119,18 @@ describe('enum_', () => {
});
});

describe('should return dataset without issues for enum like object', () => {
test('for valid options', () => {
expectNoSchemaIssue(enum_(enumLikeObject), [
enumLikeObject['1.7976931348623157e308'],
]);
});

test('for valid values', () => {
expectNoSchemaIssue(enum_(enumLikeObject), ['baz']);
});
});

describe('should return dataset with issues', () => {
const schema = enum_(options, 'message');
const baseIssue: Omit<EnumIssue, 'input' | 'received'> = {
Expand Down Expand Up @@ -137,4 +199,23 @@ describe('enum_', () => {
expectSchemaIssue(schema, baseIssue, [{}, { key: 'value' }]);
});
});
describe('should return dataset with issues for enum like object', () => {
const schema = enum_(enumLikeObject, 'message');
const baseIssue: Omit<EnumIssue, 'input' | 'received'> = {
kind: 'schema',
type: 'enum',
expected: '"baz"',
message: 'message',
};

test('for invalid options', () => {
expectSchemaIssue(schema, baseIssue, [
'foo',
'bar',
'qux',
'NaN',
'Infinity',
]);
});
});
});
31 changes: 28 additions & 3 deletions library/src/schemas/enum/enum.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,31 @@ export interface Enum {
[key: string]: string | number;
}

type IsNumericString<K extends string> = K extends
| 'NaN'
| 'Infinity'
| '-Infinity'
? true
: K extends `${infer V extends number}`
? `${V}` extends K
? true
: false
: false;
/**
* enum_ only accepts enum values of string & non-numeric keys.
* This type-level function filters out numeric keys including Infinity and NaN.
*
* @example FilterEnumKeys<'foo' | '1' | 'Infinity' | 'NaN'> will be 'foo'.
*/
type FilterEnumKeys<K extends string> = K extends K
? IsNumericString<K> extends true
? never
: K
: never;
type EnumValues<TEnum extends Enum> = TEnum[FilterEnumKeys<
string & keyof TEnum
>];

/**
* Enum issue type.
*/
Expand All @@ -42,7 +67,7 @@ export interface EnumIssue extends BaseIssue<unknown> {
export interface EnumSchema<
TEnum extends Enum,
TMessage extends ErrorMessage<EnumIssue> | undefined,
> extends BaseSchema<TEnum[keyof TEnum], TEnum[keyof TEnum], EnumIssue> {
> extends BaseSchema<EnumValues<TEnum>, EnumValues<TEnum>, EnumIssue> {
/**
* The schema type.
*/
Expand All @@ -58,7 +83,7 @@ export interface EnumSchema<
/**
* The enum options.
*/
readonly options: TEnum[keyof TEnum][];
readonly options: EnumValues<TEnum>[];
/**
* The error message.
*/
Expand Down Expand Up @@ -94,7 +119,7 @@ export function enum_(
message?: ErrorMessage<EnumIssue>
): EnumSchema<Enum, ErrorMessage<EnumIssue> | undefined> {
const options = Object.entries(enum__)
.filter(([key]) => isNaN(+key))
.filter(([key]) => (+key).toString() !== key)
.map(([, value]) => value);
return {
kind: 'schema',
Expand Down