Skip to content

Commit

Permalink
Implement code points validator
Browse files Browse the repository at this point in the history
  • Loading branch information
tats-u committed Oct 20, 2024
1 parent 414aa98 commit e3e8736
Show file tree
Hide file tree
Showing 17 changed files with 1,021 additions and 0 deletions.
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

0 comments on commit e3e8736

Please sign in to comment.