Skip to content

Commit c00c425

Browse files
committed
Implement code points validator
1 parent 414aa98 commit c00c425

17 files changed

Lines changed: 1021 additions & 0 deletions
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expectTypeOf, test } from 'vitest';
2+
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
3+
import {
4+
codePoints,
5+
type CodePointsAction,
6+
type CodePointsIssue,
7+
} from './codePoints.ts';
8+
9+
describe('codePoints', () => {
10+
describe('should return action object', () => {
11+
test('with undefined message', () => {
12+
type Action = CodePointsAction<string, 10, undefined>;
13+
expectTypeOf(codePoints<string, 10>(10)).toEqualTypeOf<Action>();
14+
expectTypeOf(
15+
codePoints<string, 10, undefined>(10, undefined)
16+
).toEqualTypeOf<Action>();
17+
});
18+
19+
test('with string message', () => {
20+
expectTypeOf(
21+
codePoints<string, 10, 'message'>(10, 'message')
22+
).toEqualTypeOf<CodePointsAction<string, 10, 'message'>>();
23+
});
24+
25+
test('with function message', () => {
26+
expectTypeOf(
27+
codePoints<string, 10, () => string>(10, () => 'message')
28+
).toEqualTypeOf<CodePointsAction<string, 10, () => string>>();
29+
});
30+
});
31+
32+
describe('should infer correct types', () => {
33+
type Input = 'example string';
34+
type Action = CodePointsAction<Input, 5, undefined>;
35+
36+
test('of input', () => {
37+
expectTypeOf<InferInput<Action>>().toEqualTypeOf<Input>();
38+
});
39+
40+
test('of output', () => {
41+
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<Input>();
42+
});
43+
44+
test('of issue', () => {
45+
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<
46+
CodePointsIssue<Input, 5>
47+
>();
48+
});
49+
});
50+
});
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
import { describe, expect, test } from 'vitest';
2+
import type { StringIssue } from '../../schemas/index.ts';
3+
import { _getCodePointCount } from '../../utils/index.ts';
4+
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts';
5+
import {
6+
codePoints,
7+
type CodePointsAction,
8+
type CodePointsIssue,
9+
} from './codePoints.ts';
10+
11+
describe('graphemes', () => {
12+
describe('should return action object', () => {
13+
const baseAction: Omit<CodePointsAction<string, 5, never>, 'message'> = {
14+
kind: 'validation',
15+
type: 'code_points',
16+
reference: codePoints,
17+
expects: '5',
18+
requirement: 5,
19+
async: false,
20+
'~validate': expect.any(Function),
21+
};
22+
23+
test('with undefined message', () => {
24+
const action: CodePointsAction<string, 5, undefined> = {
25+
...baseAction,
26+
message: undefined,
27+
};
28+
expect(codePoints(5)).toStrictEqual(action);
29+
expect(codePoints(5, undefined)).toStrictEqual(action);
30+
});
31+
32+
test('with string message', () => {
33+
expect(codePoints(5, 'message')).toStrictEqual({
34+
...baseAction,
35+
message: 'message',
36+
} satisfies CodePointsAction<string, 5, string>);
37+
});
38+
39+
test('with function message', () => {
40+
const message = () => 'message';
41+
expect(codePoints(5, message)).toStrictEqual({
42+
...baseAction,
43+
message,
44+
} satisfies CodePointsAction<string, 5, typeof message>);
45+
});
46+
});
47+
48+
describe('should return dataset without issues', () => {
49+
const action = codePoints(5);
50+
51+
test('for untyped inputs', () => {
52+
const issues: [StringIssue] = [
53+
{
54+
kind: 'schema',
55+
type: 'string',
56+
input: null,
57+
expected: 'string',
58+
received: 'null',
59+
message: 'message',
60+
},
61+
];
62+
expect(
63+
action['~validate']({ typed: false, value: null, issues }, {})
64+
).toStrictEqual({
65+
typed: false,
66+
value: null,
67+
issues,
68+
});
69+
});
70+
71+
test('for valid strings', () => {
72+
expectNoActionIssue(action, ['12345', '12 45', '1234 ', 'hello']);
73+
});
74+
75+
test('for valid emoji', () => {
76+
expectNoActionIssue(action, ['👨🏽‍👩🏽', '😶‍🌫️😀', '😡👍😁😂😀', '0️⃣㊙️']);
77+
});
78+
79+
test('for valid non-latin', () => {
80+
expectNoActionIssue(action, ['あ𛀙よろし', '𠮷野家で𩸽', '葛󠄀城市!']);
81+
});
82+
});
83+
84+
describe('should return dataset with issues', () => {
85+
const action = codePoints(5, 'message');
86+
const baseIssue: Omit<CodePointsIssue<string, 5>, 'input' | 'received'> = {
87+
kind: 'validation',
88+
type: 'code_points',
89+
expected: '5',
90+
message: 'message',
91+
requirement: 5,
92+
};
93+
94+
test('for invalid strings', () => {
95+
expectActionIssue(
96+
action,
97+
baseIssue,
98+
['', ' ', '1', '1234', '123 ', '123456', '12 456', '123456789'],
99+
(value) => `${_getCodePointCount(value)}`
100+
);
101+
});
102+
103+
test('for invalid emoji', () => {
104+
expectActionIssue(
105+
action,
106+
baseIssue,
107+
['😀👋🏼🧩👩🏻‍🏫🫥', '㊙️㊙️0️⃣1️⃣2️⃣'],
108+
(value) => `${_getCodePointCount(value)}`
109+
);
110+
});
111+
112+
test('for invalid non-latin', () => {
113+
expectActionIssue(
114+
action,
115+
baseIssue,
116+
['竈門禰󠄀豆子', '葛󠄀城市'],
117+
(value) => `${_getCodePointCount(value)}`
118+
);
119+
});
120+
});
121+
});
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
import type {
2+
BaseIssue,
3+
BaseValidation,
4+
ErrorMessage,
5+
} from '../../types/index.ts';
6+
import { _addIssue, _getCodePointCount } from '../../utils/index.ts';
7+
8+
/**
9+
* CodePoints issue type.
10+
*/
11+
export interface CodePointsIssue<
12+
TInput extends string,
13+
TRequirement extends number,
14+
> extends BaseIssue<TInput> {
15+
/**
16+
* The issue kind.
17+
*/
18+
readonly kind: 'validation';
19+
/**
20+
* The issue type.
21+
*/
22+
readonly type: 'code_points';
23+
/**
24+
* The expected property.
25+
*/
26+
readonly expected: `${TRequirement}`;
27+
/**
28+
* The received property.
29+
*/
30+
readonly received: `${number}`;
31+
/**
32+
* The required codePoints.
33+
*/
34+
readonly requirement: TRequirement;
35+
}
36+
37+
/**
38+
* CodePoints action type.
39+
*/
40+
export interface CodePointsAction<
41+
TInput extends string,
42+
TRequirement extends number,
43+
TMessage extends
44+
| ErrorMessage<CodePointsIssue<TInput, TRequirement>>
45+
| undefined,
46+
> extends BaseValidation<TInput, TInput, CodePointsIssue<TInput, TRequirement>> {
47+
/**
48+
* The action type.
49+
*/
50+
readonly type: 'code_points';
51+
/**
52+
* The action reference.
53+
*/
54+
readonly reference: typeof codePoints;
55+
/**
56+
* The expected property.
57+
*/
58+
readonly expects: `${TRequirement}`;
59+
/**
60+
* The required code points.
61+
*/
62+
readonly requirement: TRequirement;
63+
/**
64+
* The error message.
65+
*/
66+
readonly message: TMessage;
67+
}
68+
69+
/**
70+
* Creates a code points validation action.
71+
*
72+
* @param requirement The required code points.
73+
*
74+
* @returns A code points action.
75+
*/
76+
export function codePoints<
77+
TInput extends string,
78+
const TRequirement extends number,
79+
>(requirement: TRequirement): CodePointsAction<TInput, TRequirement, undefined>;
80+
81+
/**
82+
* Creates a code points validation action.
83+
*
84+
* @param requirement The required code points.
85+
* @param message The error message.
86+
*
87+
* @returns A code points action.
88+
*/
89+
export function codePoints<
90+
TInput extends string,
91+
const TRequirement extends number,
92+
const TMessage extends
93+
| ErrorMessage<CodePointsIssue<TInput, TRequirement>>
94+
| undefined,
95+
>(
96+
requirement: TRequirement,
97+
message: TMessage
98+
): CodePointsAction<TInput, TRequirement, TMessage>;
99+
100+
export function codePoints(
101+
requirement: number,
102+
message?: ErrorMessage<CodePointsIssue<string, number>>
103+
): CodePointsAction<
104+
string,
105+
number,
106+
ErrorMessage<CodePointsIssue<string, number>> | undefined
107+
> {
108+
return {
109+
kind: 'validation',
110+
type: 'code_points',
111+
reference: codePoints,
112+
async: false,
113+
expects: `${requirement}`,
114+
requirement,
115+
message,
116+
'~validate'(dataset, config) {
117+
if (dataset.typed) {
118+
const count = _getCodePointCount(dataset.value);
119+
if (count !== this.requirement) {
120+
_addIssue(this, 'codePoints', dataset, config, {
121+
received: `${count}`,
122+
});
123+
}
124+
}
125+
return dataset;
126+
},
127+
};
128+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './codePoints.ts';

library/src/actions/index.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export * from './brand/index.ts';
66
export * from './bytes/index.ts';
77
export * from './check/index.ts';
88
export * from './checkItems/index.ts';
9+
export * from './codePoints/index.ts';
910
export * from './creditCard/index.ts';
1011
export * from './cuid2/index.ts';
1112
export * from './decimal/index.ts';
@@ -42,6 +43,7 @@ export * from './mac48/index.ts';
4243
export * from './mac64/index.ts';
4344
export * from './mapItems/index.ts';
4445
export * from './maxBytes/index.ts';
46+
export * from './maxCodePoints/index.ts';
4547
export * from './maxGraphemes/index.ts';
4648
export * from './maxLength/index.ts';
4749
export * from './maxSize/index.ts';
@@ -50,6 +52,7 @@ export * from './maxWords/index.ts';
5052
export * from './metadata/index.ts';
5153
export * from './mimeType/index.ts';
5254
export * from './minBytes/index.ts';
55+
export * from './minCodePoints/index.ts';
5356
export * from './minGraphemes/index.ts';
5457
export * from './minLength/index.ts';
5558
export * from './minSize/index.ts';
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './maxCodePoints.ts';
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { describe, expectTypeOf, test } from 'vitest';
2+
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
3+
import {
4+
maxCodePoints,
5+
type MaxCodePointsAction,
6+
type MaxCodePointsIssue,
7+
} from './maxCodePoints.ts';
8+
9+
describe('maxCodePoints', () => {
10+
describe('should return action object', () => {
11+
test('with undefined message', () => {
12+
type Action = MaxCodePointsAction<string, 10, undefined>;
13+
expectTypeOf(maxCodePoints<string, 10>(10)).toEqualTypeOf<Action>();
14+
expectTypeOf(
15+
maxCodePoints<string, 10, undefined>(10, undefined)
16+
).toEqualTypeOf<Action>();
17+
});
18+
19+
test('with string message', () => {
20+
expectTypeOf(
21+
maxCodePoints<string, 10, 'message'>(10, 'message')
22+
).toEqualTypeOf<MaxCodePointsAction<string, 10, 'message'>>();
23+
});
24+
25+
test('with function message', () => {
26+
expectTypeOf(
27+
maxCodePoints<string, 10, () => string>(10, () => 'message')
28+
).toEqualTypeOf<MaxCodePointsAction<string, 10, () => string>>();
29+
});
30+
});
31+
32+
describe('should infer correct types', () => {
33+
type Input = 'example string';
34+
type Action = MaxCodePointsAction<Input, 10, undefined>;
35+
36+
test('of input', () => {
37+
expectTypeOf<InferInput<Action>>().toEqualTypeOf<Input>();
38+
});
39+
40+
test('of output', () => {
41+
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<Input>();
42+
});
43+
44+
test('of issue', () => {
45+
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<
46+
MaxCodePointsIssue<Input, 10>
47+
>();
48+
});
49+
});
50+
});

0 commit comments

Comments
 (0)