Skip to content

Feat: Add minDigitChars #1111

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

Open
wants to merge 2 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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ server

# Tests
coverage
tsconfig.vitest-temp.json

# Backups
backups
Expand Down
1 change: 1 addition & 0 deletions library/src/actions/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ export * from './metadata/index.ts';
export * from './mimeType/index.ts';
export * from './minBytes/index.ts';
export * from './minEntries/index.ts';
export * from './minDigitChars/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/minDigitChars/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './minDigitChars.ts';
50 changes: 50 additions & 0 deletions library/src/actions/minDigitChars/minDigitChars.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 {
minDigitChars,
type MinDigitCharsAction,
type MinDigitCharsIssue,
} from './minDigitChars.ts';

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

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

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

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

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

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

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

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

describe('should return dataset without issues', () => {
const action = minDigitChars(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', '123456', '45foobarbaz123']);
});
});

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

test('for invalid strings', () => {
expectActionIssue(
action,
baseIssue,
['', 'foo', 'abc1234'],
(value) => `${_getDigitCount(value)}`
);
});
});
});
135 changes: 135 additions & 0 deletions library/src/actions/minDigitChars/minDigitChars.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
import type {
BaseIssue,
BaseValidation,
ErrorMessage,
} from '../../types/index.ts';
import { _addIssue, _getDigitCount } from '../../utils/index.ts';

/**
* Min digits issue interface.
*/
export interface MinDigitCharsIssue<
TInput extends string,
TRequirement extends number,
> extends BaseIssue<TInput> {
/**
* The issue kind.
*/
readonly kind: 'validation';
/**
* The issue type.
*/
readonly type: 'min_digits';
/**
* The expected property.
*/
readonly expected: `>=${TRequirement}`;
/**
* The received property.
*/
readonly received: `${number}`;
/**
* The minimum digits.
*/
readonly requirement: TRequirement;
}

/**
* Min digits action interface.
*/
export interface MinDigitCharsAction<
TInput extends string,
TRequirement extends number,
TMessage extends
| ErrorMessage<MinDigitCharsIssue<TInput, TRequirement>>
| undefined,
> extends BaseValidation<
TInput,
TInput,
MinDigitCharsIssue<TInput, TRequirement>
> {
/**
* The action type.
*/
readonly type: 'min_digits';
/**
* The action reference.
*/
readonly reference: typeof minDigitChars;
/**
* The expected property.
*/
readonly expects: `>=${TRequirement}`;
/**
* The minimum digits.
*/
readonly requirement: TRequirement;
/**
* The error message.
*/
readonly message: TMessage;
}

/**
* Creates a min digits validation action.
*
* @param requirement The minimum digits.
*
* @returns A min digits action.
*/
export function minDigitChars<
TInput extends string,
const TRequirement extends number,
>(
requirement: TRequirement
): MinDigitCharsAction<TInput, TRequirement, undefined>;

/**
* Creates a min digits validation action.
*
* @param requirement The minimum digits.
* @param message The error message.
*
* @returns A min digits action.
*/
export function minDigitChars<
TInput extends string,
const TRequirement extends number,
const TMessage extends
| ErrorMessage<MinDigitCharsIssue<TInput, TRequirement>>
| undefined,
>(
requirement: TRequirement,
message: TMessage
): MinDigitCharsAction<TInput, TRequirement, TMessage>;

// @__NO_SIDE_EFFECTS__
export function minDigitChars(
requirement: number,
message?: ErrorMessage<MinDigitCharsIssue<string, number>>
): MinDigitCharsAction<
string,
number,
ErrorMessage<MinDigitCharsIssue<string, number>> | undefined
> {
return {
kind: 'validation',
type: 'min_digits',
reference: minDigitChars,
async: false,
expects: `>=${requirement}`,
requirement,
message,
'~run'(dataset, config) {
if (dataset.typed) {
const count = _getDigitCount(dataset.value);
if (count < this.requirement) {
_addIssue(this, 'digits', dataset, config, {
received: `${count}`,
});
}
}
return dataset;
},
};
}
14 changes: 14 additions & 0 deletions library/src/utils/_getDigitCount/_getDigitCount.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import { describe, expect, test } from 'vitest';
import { _getDigitCount } from './_getDigitCount.ts';

describe('_getDigitCount', () => {
test('should return digit count', () => {
expect(_getDigitCount('')).toBe(0);
expect(_getDigitCount('hello world')).toBe(0);
expect(_getDigitCount('he22o world')).toBe(2);
expect(_getDigitCount('hello world111')).toBe(3);
expect(_getDigitCount('111hello world111')).toBe(6);
expect(_getDigitCount('123')).toBe(3);
expect(_getDigitCount('😀1')).toBe(1);
});
});
13 changes: 13 additions & 0 deletions library/src/utils/_getDigitCount/_getDigitCount.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/**
* Returns the digit count of the input.
*
* @param input The input to be measured.
*
* @returns The digit count.
*
* @internal
*/
// @__NO_SIDE_EFFECTS__
export function _getDigitCount(input: string): number {
return (input.match(/\d/gu) || []).length;
}
1 change: 1 addition & 0 deletions library/src/utils/_getDigitCount/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './_getDigitCount.ts';
1 change: 1 addition & 0 deletions library/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export * from './_addIssue/index.ts';
export * from './_getByteCount/index.ts';
export * from './_getDigitCount/index.ts';
export * from './_getGraphemeCount/index.ts';
export * from './_getStandardProps/index.ts';
export * from './_getWordCount/index.ts';
Expand Down
Loading