Skip to content

Commit

Permalink
Merge pull request #789 from jasperteo/feat-nanoid-validation
Browse files Browse the repository at this point in the history
Added nanoid() validation action
  • Loading branch information
fabian-hiller authored Aug 27, 2024
2 parents 6f470fd + 0a2cd85 commit 97b33f4
Show file tree
Hide file tree
Showing 27 changed files with 421 additions and 2 deletions.
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

0 comments on commit 97b33f4

Please sign in to comment.