Skip to content

Commit

Permalink
Fix implementation of nonOptional, nonNullable and nonNullish #909
Browse files Browse the repository at this point in the history
  • Loading branch information
fabian-hiller committed Nov 10, 2024
1 parent d23a2c6 commit aca6266
Show file tree
Hide file tree
Showing 16 changed files with 302 additions and 74 deletions.
1 change: 1 addition & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ All notable changes to the library will be documented in this file.
- Change type signature of `partialCheck` and `partialCheckAsync` action to add `.pathList` property in a type-safe way
- Change type signature of `findItem` action to support type predicates (issue #867)
- Refactor `bytes`, `maxBytes`, `minBytes` and `notBytes` action
- Fix implementation of `nonOptional`, `nonOptionalAsync`, `nonNullable`, `nonNullableAsync`, `nonNullish` and `nonNullishAsync` schema in edge cases (issue #909)

## v0.42.1 (September 20, 2024)

Expand Down
38 changes: 34 additions & 4 deletions library/src/schemas/nonNullable/nonNullable.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, expect, test } from 'vitest';
import { transform } from '../../actions/index.ts';
import { pipe } from '../../methods/index.ts';
import type { FailureDataset } from '../../types/index.ts';
import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts';
import { nullish, type NullishSchema } from '../nullish/index.ts';
import { string, type StringSchema } from '../string/index.ts';
Expand Down Expand Up @@ -72,16 +75,43 @@ describe('nonNullable', () => {
});

describe('should return dataset with issues', () => {
const schema = nonNullable(nullish(string()), 'message');
const baseIssue: Omit<NonNullableIssue, 'input' | 'received'> = {
const nonNullableIssue: NonNullableIssue = {
kind: 'schema',
type: 'non_nullable',
input: null,
received: 'null',
expected: '!null',
message: 'message',
requirement: undefined,
path: undefined,
issues: undefined,
lang: undefined,
abortEarly: undefined,
abortPipeEarly: undefined,
};

test('for null', () => {
expectSchemaIssue(schema, baseIssue, [null]);
test('for null input', () => {
expectSchemaIssue(
nonNullable(nullish(string()), 'message'),
nonNullableIssue,
[null]
);
});

test('for null output', () => {
expect(
nonNullable(
pipe(
string(),
transform(() => null)
),
'message'
)['~run']({ value: 'foo' }, {})
).toStrictEqual({
typed: false,
value: null,
issues: [nonNullableIssue],
} satisfies FailureDataset<NonNullableIssue>);
});
});
});
16 changes: 10 additions & 6 deletions library/src/schemas/nonNullable/nonNullable.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
BaseIssue,
BaseSchema,
ErrorMessage,
FailureDataset,
OutputDataset,
} from '../../types/index.ts';
import { _addIssue, _getStandardProps } from '../../utils/index.ts';
import type {
Expand Down Expand Up @@ -88,15 +88,19 @@ export function nonNullable(
return _getStandardProps(this);
},
'~run'(dataset, config) {
// If value is `null`, add issue and return dataset
// If value is not `null`, run wrapped schema
if (dataset.value !== null) {
this.wrapped['~run'](dataset, config);
}

// If value is `null`, add issue to dataset
if (dataset.value === null) {
_addIssue(this, 'type', dataset, config);
// @ts-expect-error
return dataset as FailureDataset<NonNullableIssue>;
}

// Otherwise, return dataset of wrapped schema
return this.wrapped['~run'](dataset, config);
// Return output dataset
// @ts-expect-error
return dataset as OutputDataset<unknown, BaseIssue<unknown>>;
},
};
}
38 changes: 34 additions & 4 deletions library/src/schemas/nonNullable/nonNullableAsync.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, expect, test } from 'vitest';
import { transform } from '../../actions/index.ts';
import { pipeAsync } from '../../methods/index.ts';
import type { FailureDataset } from '../../types/index.ts';
import {
expectNoSchemaIssueAsync,
expectSchemaIssueAsync,
Expand Down Expand Up @@ -78,16 +81,43 @@ describe('nonNullableAsync', () => {
});

describe('should return dataset with issues', () => {
const schema = nonNullableAsync(nullishAsync(string()), 'message');
const baseIssue: Omit<NonNullableIssue, 'input' | 'received'> = {
const nonNullableIssue: NonNullableIssue = {
kind: 'schema',
type: 'non_nullable',
input: null,
received: 'null',
expected: '!null',
message: 'message',
requirement: undefined,
path: undefined,
issues: undefined,
lang: undefined,
abortEarly: undefined,
abortPipeEarly: undefined,
};

test('for null', async () => {
await expectSchemaIssueAsync(schema, baseIssue, [null]);
test('for null input', async () => {
await expectSchemaIssueAsync(
nonNullableAsync(nullishAsync(string()), 'message'),
nonNullableIssue,
[null]
);
});

test('for null output', async () => {
expect(
await nonNullableAsync(
pipeAsync(
string(),
transform(() => null)
),
'message'
)['~run']({ value: 'foo' }, {})
).toStrictEqual({
typed: false,
value: null,
issues: [nonNullableIssue],
} satisfies FailureDataset<NonNullableIssue>);
});
});
});
16 changes: 10 additions & 6 deletions library/src/schemas/nonNullable/nonNullableAsync.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import type {
BaseSchema,
BaseSchemaAsync,
ErrorMessage,
FailureDataset,
OutputDataset,
} from '../../types/index.ts';
import { _addIssue, _getStandardProps } from '../../utils/index.ts';
import type {
Expand Down Expand Up @@ -101,15 +101,19 @@ export function nonNullableAsync(
return _getStandardProps(this);
},
async '~run'(dataset, config) {
// If value is `null`, add issue and return dataset
// If value is not `null`, run wrapped schema
if (dataset.value !== null) {
await this.wrapped['~run'](dataset, config);
}

// If value is `null`, add issue to dataset
if (dataset.value === null) {
_addIssue(this, 'type', dataset, config);
// @ts-expect-error
return dataset as FailureDataset<NonNullableIssue>;
}

// Otherwise, return dataset of wrapped schema
return this.wrapped['~run'](dataset, config);
// Return output dataset
// @ts-expect-error
return dataset as OutputDataset<unknown, BaseIssue<unknown>>;
},
};
}
5 changes: 1 addition & 4 deletions library/src/schemas/nonNullable/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ export type InferNonNullableOutput<
TWrapped extends
| BaseSchema<unknown, unknown, BaseIssue<unknown>>
| BaseSchemaAsync<unknown, unknown, BaseIssue<unknown>>,
> =
// FIXME: For schemas that transform the input to `null`, this
// implementation may result in an incorrect output type
NonNullable<InferOutput<TWrapped>>;
> = NonNullable<InferOutput<TWrapped>>;

/**
* Infer non nullable issue type.
Expand Down
54 changes: 49 additions & 5 deletions library/src/schemas/nonNullish/nonNullish.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, expect, test } from 'vitest';
import { transform } from '../../actions/index.ts';
import { pipe } from '../../methods/index.ts';
import type { FailureDataset } from '../../types/index.ts';
import { expectNoSchemaIssue, expectSchemaIssue } from '../../vitest/index.ts';
import { nullish, type NullishSchema } from '../nullish/index.ts';
import { string, type StringSchema } from '../string/index.ts';
Expand Down Expand Up @@ -72,20 +75,61 @@ describe('nonNullish', () => {
});

describe('should return dataset with issues', () => {
const schema = nonNullish(nullish(string()), 'message');
const baseIssue: Omit<NonNullishIssue, 'input' | 'received'> = {
kind: 'schema',
type: 'non_nullish',
expected: '(!null & !undefined)',
message: 'message',
requirement: undefined,
path: undefined,
issues: undefined,
lang: undefined,
abortEarly: undefined,
abortPipeEarly: undefined,
};

test('for null', () => {
expectSchemaIssue(schema, baseIssue, [null]);
test('for null input', () => {
expectSchemaIssue(nonNullish(nullish(string()), 'message'), baseIssue, [
null,
]);
});

test('for undefined', () => {
expectSchemaIssue(schema, baseIssue, [undefined]);
test('for undefined input', () => {
expectSchemaIssue(nonNullish(nullish(string()), 'message'), baseIssue, [
undefined,
]);
});

test('for null output', () => {
expect(
nonNullish(
pipe(
string(),
transform(() => null)
),
'message'
)['~run']({ value: 'foo' }, {})
).toStrictEqual({
typed: false,
value: null,
issues: [{ ...baseIssue, input: null, received: 'null' }],
} satisfies FailureDataset<NonNullishIssue>);
});

test('for undefined output', () => {
expect(
nonNullish(
pipe(
string(),
transform(() => undefined)
),
'message'
)['~run']({ value: 'foo' }, {})
).toStrictEqual({
typed: false,
value: undefined,
issues: [{ ...baseIssue, input: undefined, received: 'undefined' }],
} satisfies FailureDataset<NonNullishIssue>);
});
});
});
16 changes: 10 additions & 6 deletions library/src/schemas/nonNullish/nonNullish.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import type {
BaseIssue,
BaseSchema,
ErrorMessage,
FailureDataset,
OutputDataset,
} from '../../types/index.ts';
import { _addIssue, _getStandardProps } from '../../utils/index.ts';
import type {
Expand Down Expand Up @@ -88,15 +88,19 @@ export function nonNullish(
return _getStandardProps(this);
},
'~run'(dataset, config) {
// If value is `null` or `undefined`, add issue and return dataset
// If value is not `null` and `undefined`, run wrapped schema
if (!(dataset.value === null || dataset.value === undefined)) {
this.wrapped['~run'](dataset, config);
}

// If value is `null` or `undefined`, add issue to dataset
if (dataset.value === null || dataset.value === undefined) {
_addIssue(this, 'type', dataset, config);
// @ts-expect-error
return dataset as FailureDataset<NonNullishIssue>;
}

// Otherwise, return dataset of wrapped schema
return this.wrapped['~run'](dataset, config);
// Return output dataset
// @ts-expect-error
return dataset as OutputDataset<unknown, BaseIssue<unknown>>;
},
};
}
58 changes: 53 additions & 5 deletions library/src/schemas/nonNullish/nonNullishAsync.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { describe, expect, test } from 'vitest';
import { transform } from '../../actions/index.ts';
import { pipeAsync } from '../../methods/index.ts';
import type { FailureDataset } from '../../types/index.ts';
import {
expectNoSchemaIssueAsync,
expectSchemaIssueAsync,
Expand Down Expand Up @@ -78,20 +81,65 @@ describe('nonNullishAsync', () => {
});

describe('should return dataset with issues', () => {
const schema = nonNullishAsync(nullishAsync(string()), 'message');
const baseIssue: Omit<NonNullishIssue, 'input' | 'received'> = {
kind: 'schema',
type: 'non_nullish',
expected: '(!null & !undefined)',
message: 'message',
requirement: undefined,
path: undefined,
issues: undefined,
lang: undefined,
abortEarly: undefined,
abortPipeEarly: undefined,
};

test('for null', async () => {
await expectSchemaIssueAsync(schema, baseIssue, [null]);
test('for null input', async () => {
await expectSchemaIssueAsync(
nonNullishAsync(nullishAsync(string()), 'message'),
baseIssue,
[null]
);
});

test('for undefined', async () => {
await expectSchemaIssueAsync(schema, baseIssue, [undefined]);
test('for undefined input', async () => {
await expectSchemaIssueAsync(
nonNullishAsync(nullishAsync(string()), 'message'),
baseIssue,
[undefined]
);
});

test('for null output', async () => {
expect(
await nonNullishAsync(
pipeAsync(
string(),
transform(() => null)
),
'message'
)['~run']({ value: 'foo' }, {})
).toStrictEqual({
typed: false,
value: null,
issues: [{ ...baseIssue, input: null, received: 'null' }],
} satisfies FailureDataset<NonNullishIssue>);
});

test('for undefined output', async () => {
expect(
await nonNullishAsync(
pipeAsync(
string(),
transform(() => undefined)
),
'message'
)['~run']({ value: 'foo' }, {})
).toStrictEqual({
typed: false,
value: undefined,
issues: [{ ...baseIssue, input: undefined, received: 'undefined' }],
} satisfies FailureDataset<NonNullishIssue>);
});
});
});
Loading

0 comments on commit aca6266

Please sign in to comment.