Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
b15edab
Implement code points validator
tats-u Oct 20, 2024
27b88b3
Simplify
tats-u Oct 21, 2024
71b4dd2
Reduce count operations
tats-u Oct 22, 2024
f724c33
'~validate' => '~run' & format
tats-u Apr 29, 2025
abec9d1
Merge branch 'main' into code-points
fabian-hiller Jun 17, 2025
88fa1e0
Add docs
tats-u Jun 22, 2025
0ec38d6
Add notCodePoints
tats-u Jun 22, 2025
e06ec84
Add docs for types
tats-u Jul 5, 2025
fa19dac
Merge branch 'main' into code-points
yslpn Jul 18, 2026
dca7b64
Apply easy-to-fix obvious suggestions from code review
tats-u Jul 19, 2026
84a6bdd
Add missing `// @__NO_SIDE_EFFECTS__`
tats-u Jul 20, 2026
a1327f1
Remove duplicated file name suffix
tats-u Jul 20, 2026
f781906
Skip check at last code unit
tats-u Jul 20, 2026
3160d12
Add comments
tats-u Jul 20, 2026
726647b
Remove extra import
tats-u Jul 20, 2026
ef0a0dc
Move `@__NO_SIDE_EFFECTS__`
tats-u Jul 20, 2026
0637c96
Fix wrong hrefs
tats-u Jul 20, 2026
e369008
Fix wrong schemas
tats-u Jul 20, 2026
654b8f4
Fix grammers
tats-u Jul 20, 2026
c52ffc2
Fix indent inconsistency
tats-u Jul 20, 2026
5427714
Fix interface JSDoc
tats-u Jul 20, 2026
6398c4b
Change codePoints to code points in JSDoc
tats-u Jul 20, 2026
5a2aafa
Change type to interface in JSDoc
tats-u Jul 20, 2026
d4d8c0a
Fix "invalid ..." label
tats-u Jul 20, 2026
44f491d
Update docs
tats-u Aug 5, 2026
6846d19
Merge branch 'main' into code-points
yslpn Aug 6, 2026
fcf3141
docs: update API documentation to include codePoints and improve desc…
yslpn Aug 6, 2026
378c2e5
fix: correct issue type string from 'code_points' to 'code points'
yslpn Aug 6, 2026
3e8028f
docs: enhance explanations for code points validation actions
yslpn Aug 6, 2026
ee49c4d
feat: add code points actions support for to-json-schema
yslpn Aug 6, 2026
f64f7ed
fix(to-json-schema): preserve composed action constraints
yslpn Aug 6, 2026
112a73d
Revert "fix(to-json-schema): preserve composed action constraints"
yslpn Aug 6, 2026
0cb49aa
Revert "feat: add code points actions support for to-json-schema"
yslpn Aug 6, 2026
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('codePoints', () => {
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,
'~run': 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['~run']({ 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)}`
);
});
});
});
133 changes: 133 additions & 0 deletions library/src/actions/codePoints/codePoints.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import type {
BaseIssue,
BaseValidation,
ErrorMessage,
} from '../../types/index.ts';
import { _addIssue, _getCodePointCount } from '../../utils/index.ts';

/**
* Code points issue interface.
*/
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 code points.
*/
readonly requirement: TRequirement;
}

/**
* Code points action interface.
*/
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>;

// @__NO_SIDE_EFFECTS__
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,
'~run'(dataset, config) {
if (dataset.typed) {
const count = _getCodePointCount(dataset.value);
if (count !== this.requirement) {
_addIssue(this, 'code points', 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';
4 changes: 4 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 @@ -54,6 +55,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 './maxEntries/index.ts';
export * from './maxGraphemes/index.ts';
export * from './maxLength/index.ts';
Expand All @@ -63,6 +65,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 './minEntries/index.ts';
export * from './minGraphemes/index.ts';
export * from './minLength/index.ts';
Expand All @@ -74,6 +77,7 @@ export * from './nanoid/index.ts';
export * from './nonEmpty/index.ts';
export * from './normalize/index.ts';
export * from './notBytes/index.ts';
export * from './notCodePoints/index.ts';
export * from './notEntries/index.ts';
export * from './notGraphemes/index.ts';
export * from './notLength/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