Skip to content
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

Added nanoid() validation action #789

Merged
merged 8 commits into from
Aug 27, 2024
1 change: 1 addition & 0 deletions library/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ All notable changes to the library will be documented in this file.

## vX.X.X (Month DD, YYYY)

- Add `nanoid` action to validate Nano IDs (pull request #789)
- Add `undefinedable` and `undefinedableAsync` schema (issue #385)

## v0.39.0 (August 24, 2024)
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 @@ -48,6 +48,7 @@ export * from './minLength/index.ts';
export * from './minSize/index.ts';
export * from './minValue/index.ts';
export * from './multipleOf/index.ts';
export * from './nanoid/index.ts';
export * from './nonEmpty/index.ts';
export * from './normalize/index.ts';
export * from './notBytes/index.ts';
Expand Down
1 change: 1 addition & 0 deletions library/src/actions/nanoid/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './nanoid.ts';
43 changes: 43 additions & 0 deletions library/src/actions/nanoid/nanoid.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { describe, expectTypeOf, test } from 'vitest';
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
import { nanoid, type NanoIDAction, type NanoIDIssue } from './nanoid.ts';

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

test('with string message', () => {
expectTypeOf(nanoid<string, 'message'>('message')).toEqualTypeOf<
NanoIDAction<string, 'message'>
>();
});

test('with function message', () => {
expectTypeOf(nanoid<string, () => string>(() => 'message')).toEqualTypeOf<
NanoIDAction<string, () => string>
>();
});
});

describe('should infer correct types', () => {
type Action = NanoIDAction<string, undefined>;

test('of input', () => {
expectTypeOf<InferInput<Action>>().toEqualTypeOf<string>();
});

test('of output', () => {
expectTypeOf<InferOutput<Action>>().toEqualTypeOf<string>();
});

test('of issue', () => {
expectTypeOf<InferIssue<Action>>().toEqualTypeOf<NanoIDIssue<string>>();
});
});
});
111 changes: 111 additions & 0 deletions library/src/actions/nanoid/nanoid.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import { describe, expect, test } from 'vitest';
import { NANO_ID_REGEX } from '../../regex.ts';
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts';
import { nanoid, type NanoIDAction, type NanoIDIssue } from './nanoid.ts';

describe('nanoid', () => {
describe('should return action object', () => {
const baseAction: Omit<NanoIDAction<string, never>, 'message'> = {
kind: 'validation',
type: 'nanoid',
reference: nanoid,
expects: null,
requirement: NANO_ID_REGEX,
async: false,
_run: expect.any(Function),
};

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

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

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

describe('should return dataset without issues', () => {
const action = nanoid();

test('for untyped inputs', () => {
expect(action._run({ typed: false, value: null }, {})).toStrictEqual({
typed: false,
value: null,
});
});

test('for normal Nano IDs', () => {
expectNoActionIssue(action, [
'NOi6NWfhDRpgzBYFRR-uE',
'D7j9AWMA6anLPDE2_2uHz',
'g_Se_MXrTmRJpmcp8cN5m',
'Oc0XNYtCgyrX-x2T33z3E',
'gGCr-6yBmZkOTJQ1oLAFr',
]);
});

test('for single char', () => {
expectNoActionIssue(action, ['a', 'z', 'A', 'Z', '0', '9', '_', '-']);
});

test('for two chars', () => {
expectNoActionIssue(action, ['aa', 'zz', 'AZ', '09', '_-', '9A']);
});

test('for long IDs', () => {
expectNoActionIssue(action, [
'0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_-',
]);
});
});

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

test('for empty strings', () => {
expectActionIssue(action, baseIssue, ['', ' ', '\n']);
});

test('for blank spaces', () => {
expectActionIssue(action, baseIssue, [
' vM7SGqVFmPS5tw7fII-G',
'BImGM 7USGakXaVhydHgO',
'LBjowKnkbk95kK3IoUV7 ',
]);
});

test('for special chars', () => {
expectActionIssue(action, baseIssue, [
'@1o5BK76uGc-mbqeprAvX',
'#Lcb2qbTsjS98y9Vf-G15',
'$3WZ4tXxsuiDBezXIJKlP',
'%gSjBHLFDO67bE-nbgBRi',
'&2zYmqr0APdImhdxC69t4',
'–gGCr6yBmZkOTJQ1oLAFr',
]);
});
});
});
105 changes: 105 additions & 0 deletions library/src/actions/nanoid/nanoid.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { NANO_ID_REGEX } from '../../regex.ts';
import type {
BaseIssue,
BaseValidation,
Dataset,
ErrorMessage,
} from '../../types/index.ts';
import { _addIssue } from '../../utils/index.ts';

/**
* Nano ID issue type.
*/
export interface NanoIDIssue<TInput extends string> extends BaseIssue<TInput> {
/**
* The issue kind.
*/
readonly kind: 'validation';
/**
* The issue type.
*/
readonly type: 'nanoid';
/**
* The expected property.
*/
readonly expected: null;
/**
* The received property.
*/
readonly received: string;
/**
* The Nano ID regex.
*/
readonly requirement: RegExp;
}

/**
* Nano ID action type.
*/
export interface NanoIDAction<
TInput extends string,
TMessage extends ErrorMessage<NanoIDIssue<TInput>> | undefined,
> extends BaseValidation<TInput, TInput, NanoIDIssue<TInput>> {
/**
* The action type.
*/
readonly type: 'nanoid';
/**
* The action reference.
*/
readonly reference: typeof nanoid;
/**
* The expected property.
*/
readonly expects: null;
/**
* The Nano ID regex.
*/
readonly requirement: RegExp;
/**
* The error message.
*/
readonly message: TMessage;
}

/**
* Creates a [Nano ID](https://github.com/ai/nanoid) validation action.
*
* @returns A Nano ID action.
*/
export function nanoid<TInput extends string>(): NanoIDAction<
TInput,
undefined
>;

/**
* Creates a [Nano ID](https://github.com/ai/nanoid) validation action.
*
* @param message The error message.
*
* @returns A Nano ID action.
*/
export function nanoid<
TInput extends string,
const TMessage extends ErrorMessage<NanoIDIssue<TInput>> | undefined,
>(message: TMessage): NanoIDAction<TInput, TMessage>;

export function nanoid(
message?: ErrorMessage<NanoIDIssue<string>>
): NanoIDAction<string, ErrorMessage<NanoIDIssue<string>> | undefined> {
return {
kind: 'validation',
type: 'nanoid',
reference: nanoid,
async: false,
expects: null,
requirement: NANO_ID_REGEX,
message,
_run(dataset, config) {
if (dataset.typed && !this.requirement.test(dataset.value)) {
_addIssue(this, 'Nano ID', dataset, config);
}
return dataset as Dataset<string, NanoIDIssue<string>>;
},
};
}
5 changes: 5 additions & 0 deletions library/src/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,11 @@ export const MAC64_REGEX: RegExp =
export const MAC_REGEX: RegExp =
/^(?:[\da-f]{2}:){5}[\da-f]{2}$|^(?:[\da-f]{2}-){5}[\da-f]{2}$|^(?:[\da-f]{4}\.){2}[\da-f]{4}$|^(?:[\da-f]{2}:){7}[\da-f]{2}$|^(?:[\da-f]{2}-){7}[\da-f]{2}$|^(?:[\da-f]{4}\.){3}[\da-f]{4}$|^(?:[\da-f]{4}:){3}[\da-f]{4}$/iu;

/**
* [Nano ID](https://github.com/ai/nanoid) regex.
*/
export const NANO_ID_REGEX: RegExp = /^[\w-]+$/u;

/**
* [Octal](https://en.wikipedia.org/wiki/Octal) regex.
*/
Expand Down
9 changes: 8 additions & 1 deletion website/src/routes/api/(actions)/cuid2/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ contributors:
- fabian-hiller
---

import { Link } from '@builder.io/qwik-city';
import { ApiList, Property } from '~/components';
import { properties } from './properties';

Expand All @@ -30,6 +31,8 @@ const Action = v.cuid2<TInput, TMessage>(message);

With `cuid2` you can validate the formatting of a string. If the input is not an Cuid2, you can use `message` to customize the error message.

> Since Cuid2s are not limited to a fixed length, it is recommended to combine `cuid2` with <Link href="../length/">`length`</Link> to ensure the correct length.

## Returns

- `Action` <Property {...properties.Action} />
Expand All @@ -43,7 +46,11 @@ The following examples show how `cuid2` can be used.
Schema to validate an Cuid2.

```ts
const Cuid2Schema = v.pip(v.string(), v.cuid2('The Cuid2 is badly formatted.'));
const Cuid2Schema = v.pipe(
v.string(),
v.cuid2('The Cuid2 is badly formatted.'),
v.length(10, 'The Cuid2 must be 10 characters long.')
);
```

## Related
Expand Down
2 changes: 1 addition & 1 deletion website/src/routes/api/(actions)/isoTime/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ The following examples show how `isoTime` can be used.
Schema to validate an ISO time.

```ts
const IsoTimeSchema = v.pip(
const IsoTimeSchema = v.pipe(
v.string(),
v.isoTime('The time is badly formatted.')
);
Expand Down
Loading