Skip to content

Commit

Permalink
Merge pull request #853 from remonke/grapheme-count-validators
Browse files Browse the repository at this point in the history
Add `*graphemes` validation actions.
  • Loading branch information
fabian-hiller authored Oct 11, 2024
2 parents 17c38fe + 01a4d24 commit 2cf6025
Show file tree
Hide file tree
Showing 65 changed files with 2,850 additions and 1 deletion.
1 change: 1 addition & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ All notable changes to the library will be documented in this file.
## v1.0.0 (Month DD, YYYY)

- Add `checkItemsAsync` action (pull request #856)
- Add `graphemes`, `maxGraphemes`, `minGraphemes` and `notGraphemes` action (pull request #853)
- Change types and implementation to support Standard Schema

## v0.42.1 (September 20, 2024)
Expand Down
50 changes: 50 additions & 0 deletions library/src/actions/graphemes/graphemes.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 {
graphemes,
type GraphemesAction,
type GraphemesIssue,
} from './graphemes.ts';

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

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

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

describe('should infer correct types', () => {
type Input = 'example string';
type Action = GraphemesAction<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<
GraphemesIssue<Input, 5>
>();
});
});
});
108 changes: 108 additions & 0 deletions library/src/actions/graphemes/graphemes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import { describe, expect, test } from 'vitest';
import type { StringIssue } from '../../schemas/index.ts';
import { _getGraphemeCount } from '../../utils/index.ts';
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts';
import {
graphemes,
type GraphemesAction,
type GraphemesIssue,
} from './graphemes.ts';

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

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

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

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

describe('should return dataset without issues', () => {
const action = graphemes(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, ['😀👋🏼🧩👩🏻‍🏫🫥']);
});
});

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

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

test('for invalid emoji', () => {
expectActionIssue(
action,
baseIssue,
['😀', '😀👋🏼🧩👩🏻‍🏫', '😀👋🏼🧩👩🏻‍🏫🫥🫠', '😀👋🏼🧩👩🏻‍🏫🫥🫠🧑‍💻👻🥎'],
(value) => `${_getGraphemeCount(value)}`
);
});
});
});
128 changes: 128 additions & 0 deletions library/src/actions/graphemes/graphemes.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, _getGraphemeCount } from '../../utils/index.ts';

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

/**
* Graphemes action type.
*/
export interface GraphemesAction<
TInput extends string,
TRequirement extends number,
TMessage extends
| ErrorMessage<GraphemesIssue<TInput, TRequirement>>
| undefined,
> extends BaseValidation<TInput, TInput, GraphemesIssue<TInput, TRequirement>> {
/**
* The action type.
*/
readonly type: 'graphemes';
/**
* The action reference.
*/
readonly reference: typeof graphemes;
/**
* The expected property.
*/
readonly expects: `${TRequirement}`;
/**
* The required graphemes.
*/
readonly requirement: TRequirement;
/**
* The error message.
*/
readonly message: TMessage;
}

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

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

export function graphemes(
requirement: number,
message?: ErrorMessage<GraphemesIssue<string, number>>
): GraphemesAction<
string,
number,
ErrorMessage<GraphemesIssue<string, number>> | undefined
> {
return {
kind: 'validation',
type: 'graphemes',
reference: graphemes,
async: false,
expects: `${requirement}`,
requirement,
message,
'~validate'(dataset, config) {
if (dataset.typed) {
const count = _getGraphemeCount(dataset.value);
if (count !== this.requirement) {
_addIssue(this, 'graphemes', dataset, config, {
received: `${count}`,
});
}
}
return dataset;
},
};
}
1 change: 1 addition & 0 deletions library/src/actions/graphemes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './graphemes.ts';
4 changes: 4 additions & 0 deletions library/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export * from './excludes/index.ts';
export * from './filterItems/index.ts';
export * from './findItem/index.ts';
export * from './finite/index.ts';
export * from './graphemes/index.ts';
export * from './hash/index.ts';
export * from './hexadecimal/index.ts';
export * from './hexColor/index.ts';
Expand All @@ -40,12 +41,14 @@ export * from './mac48/index.ts';
export * from './mac64/index.ts';
export * from './mapItems/index.ts';
export * from './maxBytes/index.ts';
export * from './maxGraphemes/index.ts';
export * from './maxLength/index.ts';
export * from './maxSize/index.ts';
export * from './maxValue/index.ts';
export * from './metadata/index.ts';
export * from './mimeType/index.ts';
export * from './minBytes/index.ts';
export * from './minGraphemes/index.ts';
export * from './minLength/index.ts';
export * from './minSize/index.ts';
export * from './minValue/index.ts';
Expand All @@ -54,6 +57,7 @@ export * from './nanoid/index.ts';
export * from './nonEmpty/index.ts';
export * from './normalize/index.ts';
export * from './notBytes/index.ts';
export * from './notGraphemes/index.ts';
export * from './notLength/index.ts';
export * from './notSize/index.ts';
export * from './notValue/index.ts';
Expand Down
1 change: 1 addition & 0 deletions library/src/actions/maxGraphemes/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './maxGraphemes.ts';
50 changes: 50 additions & 0 deletions library/src/actions/maxGraphemes/maxGraphemes.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 {
maxGraphemes,
type MaxGraphemesAction,
type MaxGraphemesIssue,
} from './maxGraphemes.ts';

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

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

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

describe('should infer correct types', () => {
type Input = 'example string';
type Action = MaxGraphemesAction<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<
MaxGraphemesIssue<Input, 10>
>();
});
});
});
Loading

0 comments on commit 2cf6025

Please sign in to comment.