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

Implement code points validator #888

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
50 changes: 50 additions & 0 deletions library/src/actions/codePoints/codePoints.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expectTypeOf, test } from 'vitest';
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
import {
codePoints,
type CodePointsAction,
type CodePointsIssue,
} from './codePoints.ts';

describe('codePoints', () => {
describe('should return action object', () => {
test('with undefined message', () => {
type Action = CodePointsAction<string, 10, undefined>;
expectTypeOf(codePoints<string, 10>(10)).toEqualTypeOf<Action>();
expectTypeOf(
codePoints<string, 10, undefined>(10, undefined)
).toEqualTypeOf<Action>();
});

test('with string message', () => {
expectTypeOf(
codePoints<string, 10, 'message'>(10, 'message')
).toEqualTypeOf<CodePointsAction<string, 10, 'message'>>();
});

test('with function message', () => {
expectTypeOf(
codePoints<string, 10, () => string>(10, () => 'message')
).toEqualTypeOf<CodePointsAction<string, 10, () => string>>();
});
});

describe('should infer correct types', () => {
type Input = 'example string';
type Action = CodePointsAction<Input, 5, undefined>;

test('of input', () => {
expectTypeOf<InferInput<Action>>().toEqualTypeOf<Input>();
});

test('of output', () => {
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<Input>();
});

test('of issue', () => {
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<
CodePointsIssue<Input, 5>
>();
});
});
});
121 changes: 121 additions & 0 deletions library/src/actions/codePoints/codePoints.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import { describe, expect, test } from 'vitest';
import type { StringIssue } from '../../schemas/index.ts';
import { _getCodePointCount } from '../../utils/index.ts';
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts';
import {
codePoints,
type CodePointsAction,
type CodePointsIssue,
} from './codePoints.ts';

describe('graphemes', () => {
describe('should return action object', () => {
const baseAction: Omit<CodePointsAction<string, 5, never>, 'message'> = {
kind: 'validation',
type: 'code_points',
reference: codePoints,
expects: '5',
requirement: 5,
async: false,
'~validate': expect.any(Function),
};

test('with undefined message', () => {
const action: CodePointsAction<string, 5, undefined> = {
...baseAction,
message: undefined,
};
expect(codePoints(5)).toStrictEqual(action);
expect(codePoints(5, undefined)).toStrictEqual(action);
});

test('with string message', () => {
expect(codePoints(5, 'message')).toStrictEqual({
...baseAction,
message: 'message',
} satisfies CodePointsAction<string, 5, string>);
});

test('with function message', () => {
const message = () => 'message';
expect(codePoints(5, message)).toStrictEqual({
...baseAction,
message,
} satisfies CodePointsAction<string, 5, typeof message>);
});
});

describe('should return dataset without issues', () => {
const action = codePoints(5);

test('for untyped inputs', () => {
const issues: [StringIssue] = [
{
kind: 'schema',
type: 'string',
input: null,
expected: 'string',
received: 'null',
message: 'message',
},
];
expect(
action['~validate']({ typed: false, value: null, issues }, {})
).toStrictEqual({
typed: false,
value: null,
issues,
});
});

test('for valid strings', () => {
expectNoActionIssue(action, ['12345', '12 45', '1234 ', 'hello']);
});

test('for valid emoji', () => {
expectNoActionIssue(action, ['👨🏽‍👩🏽', '😶‍🌫️😀', '😡👍😁😂😀', '0️⃣㊙️']);
});

test('for valid non-latin', () => {
expectNoActionIssue(action, ['あ𛀙よろし', '𠮷野家で𩸽', '葛󠄀城市!']);
});
});

describe('should return dataset with issues', () => {
const action = codePoints(5, 'message');
const baseIssue: Omit<CodePointsIssue<string, 5>, 'input' | 'received'> = {
kind: 'validation',
type: 'code_points',
expected: '5',
message: 'message',
requirement: 5,
};

test('for invalid strings', () => {
expectActionIssue(
action,
baseIssue,
['', ' ', '1', '1234', '123 ', '123456', '12 456', '123456789'],
(value) => `${_getCodePointCount(value)}`
);
});

test('for invalid emoji', () => {
expectActionIssue(
action,
baseIssue,
['😀👋🏼🧩👩🏻‍🏫🫥', '㊙️㊙️0️⃣1️⃣2️⃣'],
(value) => `${_getCodePointCount(value)}`
);
});

test('for invalid non-latin', () => {
expectActionIssue(
action,
baseIssue,
['竈門禰󠄀豆子', '葛󠄀城市'],
(value) => `${_getCodePointCount(value)}`
);
});
});
});
128 changes: 128 additions & 0 deletions library/src/actions/codePoints/codePoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
import type {
BaseIssue,
BaseValidation,
ErrorMessage,
} from '../../types/index.ts';
import { _addIssue, _getCodePointCount } from '../../utils/index.ts';

/**
* CodePoints issue type.
*/
export interface CodePointsIssue<
TInput extends string,
TRequirement extends number,
> extends BaseIssue<TInput> {
/**
* The issue kind.
*/
readonly kind: 'validation';
/**
* The issue type.
*/
readonly type: 'code_points';
/**
* The expected property.
*/
readonly expected: `${TRequirement}`;
/**
* The received property.
*/
readonly received: `${number}`;
/**
* The required codePoints.
*/
readonly requirement: TRequirement;
}

/**
* CodePoints action type.
*/
export interface CodePointsAction<
TInput extends string,
TRequirement extends number,
TMessage extends
| ErrorMessage<CodePointsIssue<TInput, TRequirement>>
| undefined,
> extends BaseValidation<TInput, TInput, CodePointsIssue<TInput, TRequirement>> {
/**
* The action type.
*/
readonly type: 'code_points';
/**
* The action reference.
*/
readonly reference: typeof codePoints;
/**
* The expected property.
*/
readonly expects: `${TRequirement}`;
/**
* The required code points.
*/
readonly requirement: TRequirement;
/**
* The error message.
*/
readonly message: TMessage;
}

/**
* Creates a code points validation action.
*
* @param requirement The required code points.
*
* @returns A code points action.
*/
export function codePoints<
TInput extends string,
const TRequirement extends number,
>(requirement: TRequirement): CodePointsAction<TInput, TRequirement, undefined>;

/**
* Creates a code points validation action.
*
* @param requirement The required code points.
* @param message The error message.
*
* @returns A code points action.
*/
export function codePoints<
TInput extends string,
const TRequirement extends number,
const TMessage extends
| ErrorMessage<CodePointsIssue<TInput, TRequirement>>
| undefined,
>(
requirement: TRequirement,
message: TMessage
): CodePointsAction<TInput, TRequirement, TMessage>;

export function codePoints(
requirement: number,
message?: ErrorMessage<CodePointsIssue<string, number>>
): CodePointsAction<
string,
number,
ErrorMessage<CodePointsIssue<string, number>> | undefined
> {
return {
kind: 'validation',
type: 'code_points',
reference: codePoints,
async: false,
expects: `${requirement}`,
requirement,
message,
'~validate'(dataset, config) {
if (dataset.typed) {
const count = _getCodePointCount(dataset.value);
if (count !== this.requirement) {
_addIssue(this, 'codePoints', dataset, config, {
received: `${count}`,
});
}
}
return dataset;
},
};
}
1 change: 1 addition & 0 deletions library/src/actions/codePoints/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './codePoints.ts';
3 changes: 3 additions & 0 deletions library/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export * from './brand/index.ts';
export * from './bytes/index.ts';
export * from './check/index.ts';
export * from './checkItems/index.ts';
export * from './codePoints/index.ts';
export * from './creditCard/index.ts';
export * from './cuid2/index.ts';
export * from './decimal/index.ts';
Expand Down Expand Up @@ -42,6 +43,7 @@ export * from './mac48/index.ts';
export * from './mac64/index.ts';
export * from './mapItems/index.ts';
export * from './maxBytes/index.ts';
export * from './maxCodePoints/index.ts';
export * from './maxGraphemes/index.ts';
export * from './maxLength/index.ts';
export * from './maxSize/index.ts';
Expand All @@ -50,6 +52,7 @@ export * from './maxWords/index.ts';
export * from './metadata/index.ts';
export * from './mimeType/index.ts';
export * from './minBytes/index.ts';
export * from './minCodePoints/index.ts';
export * from './minGraphemes/index.ts';
export * from './minLength/index.ts';
export * from './minSize/index.ts';
Expand Down
1 change: 1 addition & 0 deletions library/src/actions/maxCodePoints/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './maxCodePoints.ts';
50 changes: 50 additions & 0 deletions library/src/actions/maxCodePoints/maxCodePoints.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { describe, expectTypeOf, test } from 'vitest';
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
import {
maxCodePoints,
type MaxCodePointsAction,
type MaxCodePointsIssue,
} from './maxCodePoints.ts';

describe('maxCodePoints', () => {
describe('should return action object', () => {
test('with undefined message', () => {
type Action = MaxCodePointsAction<string, 10, undefined>;
expectTypeOf(maxCodePoints<string, 10>(10)).toEqualTypeOf<Action>();
expectTypeOf(
maxCodePoints<string, 10, undefined>(10, undefined)
).toEqualTypeOf<Action>();
});

test('with string message', () => {
expectTypeOf(
maxCodePoints<string, 10, 'message'>(10, 'message')
).toEqualTypeOf<MaxCodePointsAction<string, 10, 'message'>>();
});

test('with function message', () => {
expectTypeOf(
maxCodePoints<string, 10, () => string>(10, () => 'message')
).toEqualTypeOf<MaxCodePointsAction<string, 10, () => string>>();
});
});

describe('should infer correct types', () => {
type Input = 'example string';
type Action = MaxCodePointsAction<Input, 10, undefined>;

test('of input', () => {
expectTypeOf<InferInput<Action>>().toEqualTypeOf<Input>();
});

test('of output', () => {
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<Input>();
});

test('of issue', () => {
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<
MaxCodePointsIssue<Input, 10>
>();
});
});
});
Loading