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

Add new issue reason for non-matching variant key #277

Closed
wants to merge 2 commits into from
Closed
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
66 changes: 65 additions & 1 deletion library/src/schemas/variant/variant.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { parse } from '../../methods/index.ts';
import { parse, safeParse } from '../../methods/index.ts';
import { boolean } from '../boolean/index.ts';
import { literal } from '../literal/index.ts';
import { number } from '../number/index.ts';
Expand Down Expand Up @@ -50,4 +50,68 @@ describe('variant', () => {
)
).toThrowError(error);
});

test('should create the correct issue when passing a non object value', () => {
const schema = variant('type', [
object({ type: literal('a'), val: string() }),
object({ type: literal('b'), val: number() }),
]);

const result = safeParse(schema, true) as Record<string, unknown>;

expect(result.issues).toEqual([
{
validation: 'variant',
reason: 'type',
message: 'Invalid type',
input: true,
origin: 'value',
},
]);
});

test('should create the correct issue when passing an object value with non matching variant key', () => {
const schema = variant('type', [
object({ type: literal('a'), val: string() }),
object({ type: literal('b'), val: number() }),
]);

const result1 = safeParse(schema, { type: 'c', val: false }) as Record<
string,
unknown
>;

expect(result1.issues).toEqual([
{
validation: 'variant',
reason: 'invalid_variant_key',
message: 'Invalid variant key',
input: 'c',
origin: 'value',
path: [
{
type: 'object',
key: 'type',
value: 'c',
input: { type: 'c', val: false },
},
],
requirement: schema.options,
},
]);

const result2 = safeParse(schema, {}) as Record<string, unknown>;

expect(result2.issues).toEqual([
{
validation: 'variant',
reason: 'invalid_variant_key',
message: 'Invalid variant key',
input: undefined,
origin: 'value',
path: [{ type: 'object', key: 'type', value: undefined, input: {} }],
requirement: schema.options,
},
]);
});
});
42 changes: 33 additions & 9 deletions library/src/schemas/variant/variant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import type {
Output,
} from '../../types/index.ts';
import { getSchemaIssues, getOutput, getIssues } from '../../utils/index.ts';
import type { ObjectSchema } from '../object/index.ts';
import type { ObjectPathItem, ObjectSchema } from '../object/index.ts';

/**
* Variant option type.
Expand Down Expand Up @@ -78,7 +78,7 @@ export function variant<
message,
_parse(input, info) {
// Check type of input
if (!input || typeof input !== 'object' || !(this.key in input)) {
if (!input || typeof input !== 'object') {
return getSchemaIssues(info, 'type', 'variant', this.message, input);
}

Expand All @@ -91,7 +91,8 @@ export function variant<
for (const schema of options) {
// If it is an object schema, parse discriminator key
if (schema.type === 'object') {
const result = schema.entries[this.key]._parse(
const variantKeySchema = schema.entries[this.key];
const result = variantKeySchema._parse(
(input as Record<TKey, unknown>)[this.key],
info
);
Expand Down Expand Up @@ -131,12 +132,35 @@ export function variant<
// Parse options recursively
parseOptions(this.options);

// Return output or issues
return output
? getOutput(output[0])
: issues
? getIssues(issues)
: getSchemaIssues(info, 'type', 'variant', this.message, input);
// Return output
if (output) {
return getOutput(output[0]);
}

// Return variant issues
if (issues) {
return getIssues(issues);
}

// Return new issue for non matching variant key
const inputRecord = input as Record<string, unknown>;
const pathItem: ObjectPathItem = {
type: 'object',
input: inputRecord,
key: this.key,
value: inputRecord[this.key],
};
const nonMatchingKeyIssues = getSchemaIssues(
info,
'invalid_variant_key',
'variant',
'Invalid variant key',
inputRecord[this.key],
undefined,
this.options
);
nonMatchingKeyIssues.issues[0].path = [pathItem];
return nonMatchingKeyIssues;
},
};
}
Expand Down
72 changes: 71 additions & 1 deletion library/src/schemas/variant/variantAsync.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, test } from 'vitest';
import { parseAsync } from '../../methods/index.ts';
import { parseAsync, safeParseAsync } from '../../methods/index.ts';
import { boolean } from '../boolean/index.ts';
import { literal } from '../literal/index.ts';
import { number } from '../number/index.ts';
Expand Down Expand Up @@ -52,4 +52,74 @@ describe('variantAsync', () => {
)
).rejects.toThrowError(error);
});

test('should create the correct issue when passing a non object value', async () => {
const schema = variantAsync('type', [
object({ type: literal('a'), val: string() }),
object({ type: literal('b'), val: number() }),
]);

const result = (await safeParseAsync(schema, true)) as Record<
string,
unknown
>;

expect(result.issues).toEqual([
{
validation: 'variant',
reason: 'type',
message: 'Invalid type',
input: true,
origin: 'value',
},
]);
});

test('should create the correct issue when passing an object value with non matching variant key', async () => {
const schema = variantAsync('type', [
object({ type: literal('a'), val: string() }),
object({ type: literal('b'), val: number() }),
]);

const result1 = (await safeParseAsync(schema, {
type: 'c',
val: false,
})) as Record<string, unknown>;

expect(result1.issues).toEqual([
{
validation: 'variant',
reason: 'invalid_variant_key',
message: 'Invalid variant key',
input: 'c',
origin: 'value',
path: [
{
type: 'object',
key: 'type',
value: 'c',
input: { type: 'c', val: false },
},
],
requirement: schema.options,
},
]);

const result2 = (await safeParseAsync(schema, {})) as Record<
string,
unknown
>;

expect(result2.issues).toEqual([
{
validation: 'variant',
reason: 'invalid_variant_key',
message: 'Invalid variant key',
input: undefined,
origin: 'value',
path: [{ type: 'object', key: 'type', value: undefined, input: {} }],
requirement: schema.options,
},
]);
});
});
46 changes: 37 additions & 9 deletions library/src/schemas/variant/variantAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,11 @@ import type {
Output,
} from '../../types/index.ts';
import { getSchemaIssues, getOutput, getIssues } from '../../utils/index.ts';
import type { ObjectSchema, ObjectSchemaAsync } from '../object/index.ts';
import type {
ObjectPathItem,
ObjectSchema,
ObjectSchemaAsync,
} from '../object/index.ts';

/**
* Variant option async type.
Expand Down Expand Up @@ -80,7 +84,7 @@ export function variantAsync<
message,
async _parse(input, info) {
// Check type of input
if (!input || typeof input !== 'object' || !(this.key in input)) {
if (!input || typeof input !== 'object') {
return getSchemaIssues(info, 'type', 'variant', this.message, input);
}

Expand All @@ -93,7 +97,8 @@ export function variantAsync<
for (const schema of options) {
// If it is an object schema, parse discriminator key
if (schema.type === 'object') {
const result = await schema.entries[this.key]._parse(
const variantKeySchema = schema.entries[this.key];
const result = await variantKeySchema._parse(
(input as Record<TKey, unknown>)[this.key],
info
);
Expand Down Expand Up @@ -133,12 +138,35 @@ export function variantAsync<
// Parse options recursively
await parseOptions(this.options);

// Return output or issues
return output
? getOutput(output[0])
: issues
? getIssues(issues)
: getSchemaIssues(info, 'type', 'variant', this.message, input);
// Return output
if (output) {
return getOutput(output[0]);
}

// Return variant issues
if (issues) {
return getIssues(issues);
}

// Return new issue for non matching variant key
const inputRecord = input as Record<string, unknown>;
const pathItem: ObjectPathItem = {
type: 'object',
input: inputRecord,
key: this.key,
value: inputRecord[this.key],
};
const nonMatchingKeyIssues = getSchemaIssues(
info,
'invalid_variant_key',
'variant',
'Invalid variant key',
inputRecord[this.key],
undefined,
this.options
);
nonMatchingKeyIssues.issues[0].path = [pathItem];
return nonMatchingKeyIssues;
},
};
}
Expand Down
3 changes: 2 additions & 1 deletion library/src/types/issues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@ export type IssueReason =
| 'tuple'
| 'undefined'
| 'unknown'
| 'type';
| 'type'
| 'invalid_variant_key';

/**
* Issue origin type.
Expand Down
5 changes: 4 additions & 1 deletion library/src/utils/getSchemaIssues/getSchemaIssues.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { getErrorMessage } from '../getErrorMessage/getErrorMessage.ts';
* @param message The error message.
* @param input The input value.
* @param issues The sub issues.
* @param requirement The requirement.
*
* @returns The schema result object.
*/
Expand All @@ -24,7 +25,8 @@ export function getSchemaIssues(
validation: string,
message: ErrorMessage,
input: unknown,
issues?: Issues
issues?: Issues,
requirement?: unknown
): { issues: Issues } {
// Note: The issue is deliberately not constructed with the spread operator
// for performance reasons
Expand All @@ -40,6 +42,7 @@ export function getSchemaIssues(
abortEarly: info?.abortEarly,
abortPipeEarly: info?.abortPipeEarly,
skipPipe: info?.skipPipe,
requirement,
},
],
};
Expand Down
Loading