diff --git a/library/src/actions/base64Url/base64Url.test-d.ts b/library/src/actions/base64Url/base64Url.test-d.ts new file mode 100644 index 000000000..bf03ecce2 --- /dev/null +++ b/library/src/actions/base64Url/base64Url.test-d.ts @@ -0,0 +1,49 @@ +import { describe, expectTypeOf, test } from 'vitest'; +import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts'; +import { + base64Url, + type Base64UrlAction, + type Base64UrlIssue, +} from './base64Url.ts'; + +describe('base64Url', () => { + describe('should return action object', () => { + test('with undefined message', () => { + type Action = Base64UrlAction; + expectTypeOf(base64Url()).toEqualTypeOf(); + expectTypeOf( + base64Url(undefined) + ).toEqualTypeOf(); + }); + + test('with string message', () => { + expectTypeOf(base64Url('message')).toEqualTypeOf< + Base64UrlAction + >(); + }); + + test('with function message', () => { + expectTypeOf( + base64Url string>(() => 'message') + ).toEqualTypeOf string>>(); + }); + }); + + describe('should infer correct types', () => { + type Action = Base64UrlAction; + + test('of input', () => { + expectTypeOf>().toEqualTypeOf(); + }); + + test('of output', () => { + expectTypeOf>().toEqualTypeOf(); + }); + + test('of issue', () => { + expectTypeOf>().toEqualTypeOf< + Base64UrlIssue + >(); + }); + }); +}); diff --git a/library/src/actions/base64Url/base64Url.test.ts b/library/src/actions/base64Url/base64Url.test.ts new file mode 100644 index 000000000..686236bf6 --- /dev/null +++ b/library/src/actions/base64Url/base64Url.test.ts @@ -0,0 +1,183 @@ +import { describe, expect, test } from 'vitest'; +import { BASE64_URL_REGEX } from '../../regex.ts'; +import type { StringIssue } from '../../schemas/index.ts'; +import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts'; +import { + base64Url, + type Base64UrlAction, + type Base64UrlIssue, +} from './base64Url.ts'; + +describe('base64Url', () => { + describe('should return action object', () => { + const baseAction: Omit, 'message'> = { + kind: 'validation', + type: 'base64_url', + reference: base64Url, + expects: null, + requirement: BASE64_URL_REGEX, + async: false, + '~run': expect.any(Function), + }; + + test('with undefined message', () => { + const action: Base64UrlAction = { + ...baseAction, + message: undefined, + }; + expect(base64Url()).toStrictEqual(action); + expect(base64Url(undefined)).toStrictEqual(action); + }); + + test('with string message', () => { + expect(base64Url('message')).toStrictEqual({ + ...baseAction, + message: 'message', + } satisfies Base64UrlAction); + }); + + test('with function message', () => { + const message = () => 'message'; + expect(base64Url(message)).toStrictEqual({ + ...baseAction, + message, + } satisfies Base64UrlAction); + }); + }); + + describe('should return dataset without issues', () => { + const action = base64Url(); + + 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 empty string', () => { + expectNoActionIssue(action, ['']); + }); + + test('for Base64url strings', () => { + expectNoActionIssue(action, [ + // ---- Necessary valid test vectors from https://datatracker.ietf.org/doc/html/rfc4648#section-10 ---- + // 'f' + 'Zg==', + 'Zg', + // 'fo' + 'Zm8=', + 'Zm8', + // 'foo' + 'Zm9v', + // 'foob' + 'Zm9vYg==', + 'Zm9vYg', + // 'fooba' + 'Zm9vYmE=', + 'Zm9vYmE', + // 'foobar' + 'Zm9vYmFy', + // ---- Other custom tests ---- + // ÿÿÿy + 'w7_Dv8O_eQ==', + 'w7_Dv8O_eQ', + // ~test~123v + 'fnRlc3R-MTIzdg==', + 'fnRlc3R-MTIzdg', + // valibot + 'dmFsaWJvdA==', + 'dmFsaWJvdA', + // 🌮 + '8J-Mrg==', + '8J-Mrg', + // 1234567890 + 'MTIzNDU2Nzg5MA==', + 'MTIzNDU2Nzg5MA', + // ~`!@#$%^&*()_-+={[}]\|;:"',<.>/? + 'fmAhQCMkJV4mKigpXy0rPXtbfV1cfDs6IicsPC4-Lz8=', + 'fmAhQCMkJV4mKigpXy0rPXtbfV1cfDs6IicsPC4-Lz8', + // Hello, I am Valibot and I would like to help you validate data easily using a schema. + 'SGVsbG8sIEkgYW0gVmFsaWJvdCBhbmQgSSB3b3VsZCBsaWtlIHRvIGhlbHAgeW91IHZhbGlkYXRlIGRhdGEgZWFzaWx5IHVzaW5nIGEgc2NoZW1hLg==', + 'SGVsbG8sIEkgYW0gVmFsaWJvdCBhbmQgSSB3b3VsZCBsaWtlIHRvIGhlbHAgeW91IHZhbGlkYXRlIGRhdGEgZWFzaWx5IHVzaW5nIGEgc2NoZW1hLg', + ]); + }); + }); + + describe('should return dataset with issues', () => { + const action = base64Url('message'); + const baseIssue: Omit, 'input' | 'received'> = { + kind: 'validation', + type: 'base64_url', + expected: null, + message: 'message', + requirement: BASE64_URL_REGEX, + }; + + test('for blank strings', () => { + expectActionIssue(action, baseIssue, [' ', '\n']); + }); + + test('for invalid chars', () => { + expectActionIssue(action, baseIssue, [ + 'foo`', // ` + 'foo~', // ~ + 'foo!', // ! + 'foo@', // @ + 'foo#', // # + 'foo$', // $ + 'foo%', // % + 'foo^', // ^ + 'foo&', // & + 'foo*', // * + 'foo(', // ( + 'foo)', // ) + 'foo+', // + + 'foo[', // [ + 'foo]', // ] + 'foo{', // { + 'foo}', // } + 'foo\\', // \ + 'foo|', // | + 'foo;', // ; + 'foo:', // : + "foo'", // ' + 'foo"', // " + 'foo,', // , + 'foo.', // . + 'foo<', // < + 'foo>', // > + 'foo?', // ? + 'foo/', // / + ]); + }); + + test('for invalid padding', () => { + expectActionIssue(action, baseIssue, [ + 'dmFsaWJvdA=', // = missing + 'dmFsaWJvdA===', // = extra + 'Zm9vYmE==', // = extra + ]); + }); + + test('for invalid length or character position', () => { + expectActionIssue(action, baseIssue, [ + '12345', // contains 5 characters + 'a==A', // `=` is used in between the characters + ]); + }); + }); +}); diff --git a/library/src/actions/base64Url/base64Url.ts b/library/src/actions/base64Url/base64Url.ts new file mode 100644 index 000000000..b9e1aa924 --- /dev/null +++ b/library/src/actions/base64Url/base64Url.ts @@ -0,0 +1,106 @@ +import { BASE64_URL_REGEX } from '../../regex.ts'; +import type { + BaseIssue, + BaseValidation, + ErrorMessage, +} from '../../types/index.ts'; +import { _addIssue } from '../../utils/index.ts'; + +/** + * Base64Url issue interface. + */ +export interface Base64UrlIssue + extends BaseIssue { + /** + * The issue kind. + */ + readonly kind: 'validation'; + /** + * The issue type. + */ + readonly type: 'base64_url'; + /** + * The expected property. + */ + readonly expected: null; + /** + * The received property. + */ + readonly received: `"${string}"`; + /** + * The base64 url regex. + */ + readonly requirement: RegExp; +} + +/** + * Base64Url action interface. + */ +export interface Base64UrlAction< + TInput extends string, + TMessage extends ErrorMessage> | undefined, +> extends BaseValidation> { + /** + * The action type. + */ + readonly type: 'base64_url'; + /** + * The action reference. + */ + readonly reference: typeof base64Url; + /** + * The expected property. + */ + readonly expects: null; + /** + * The base64 url regex. + */ + readonly requirement: RegExp; + /** + * The error message. + */ + readonly message: TMessage; +} + +/** + * Creates a [base64 url](https://en.wikipedia.org/wiki/Base64#URL_applications) validation action. + * + * @returns A base64 url action. + */ +export function base64Url(): Base64UrlAction< + TInput, + undefined +>; + +/** + * Creates a [base64 url](https://en.wikipedia.org/wiki/Base64#URL_applications) validation action. + * + * @param message The error message. + * + * @returns A base64 url action. + */ +export function base64Url< + TInput extends string, + const TMessage extends ErrorMessage> | undefined, +>(message: TMessage): Base64UrlAction; + +// @__NO_SIDE_EFFECTS__ +export function base64Url( + message?: ErrorMessage> +): Base64UrlAction> | undefined> { + return { + kind: 'validation', + type: 'base64_url', + reference: base64Url, + async: false, + expects: null, + requirement: BASE64_URL_REGEX, + message, + '~run'(dataset, config) { + if (dataset.typed && !this.requirement.test(dataset.value)) { + _addIssue(this, 'Base64Url', dataset, config); + } + return dataset; + }, + }; +} diff --git a/library/src/actions/base64Url/index.ts b/library/src/actions/base64Url/index.ts new file mode 100644 index 000000000..3dc880965 --- /dev/null +++ b/library/src/actions/base64Url/index.ts @@ -0,0 +1 @@ +export * from './base64Url.ts'; diff --git a/library/src/actions/index.ts b/library/src/actions/index.ts index 095dd721e..a515a4643 100644 --- a/library/src/actions/index.ts +++ b/library/src/actions/index.ts @@ -1,6 +1,7 @@ export * from './args/index.ts'; export * from './await/index.ts'; export * from './base64/index.ts'; +export * from './base64Url/index.ts'; export * from './bic/index.ts'; export * from './brand/index.ts'; export * from './bytes/index.ts'; diff --git a/library/src/regex.ts b/library/src/regex.ts index 3e161e9cd..ea97fa7ab 100644 --- a/library/src/regex.ts +++ b/library/src/regex.ts @@ -4,6 +4,12 @@ export const BASE64_REGEX: RegExp = /^(?:[\da-z+/]{4})*(?:[\da-z+/]{2}==|[\da-z+/]{3}=)?$/iu; +/** + * [Base64url](https://datatracker.ietf.org/doc/html/rfc4648#section-5) regex. + */ +export const BASE64_URL_REGEX: RegExp = + /^(?:[\w-]{4})*(?:[\w-]{2}(?:==)?|[\w-]{3}=?)?$/iu; + /** * [BIC](https://en.wikipedia.org/wiki/ISO_9362) regex. */ diff --git a/website/src/routes/api/(actions)/base64Url/index.mdx b/website/src/routes/api/(actions)/base64Url/index.mdx new file mode 100644 index 000000000..7af232865 --- /dev/null +++ b/website/src/routes/api/(actions)/base64Url/index.mdx @@ -0,0 +1,66 @@ +--- +title: base64Url +description: Creates a base64 url validation action. +source: /actions/base64Url/base64Url.ts +contributors: + - EltonLobo07 +--- + +import { ApiList, Property } from '~/components'; +import { properties } from './properties'; + +# base64Url + +Creates a [base64 url](https://en.wikipedia.org/wiki/Base64#URL_applications) validation action. + +```ts +const Action = v.base64Url(message); +``` + +## Generics + +- `TInput` +- `TMessage` + +## Parameters + +- `message` + +### Explanation + +With `base64Url` you can validate the formatting of a string. If the input is not a base64 url string, you can use `message` to customize the error message. + +## Returns + +- `Action` + +## Examples + +The following examples show how `base64Url` can be used. + +### Base64Url schema + +Schema to validate a base64 url string. + +```ts +const Base64UrlSchema = v.pipe( + v.string(), + v.base64Url('The data is badly encoded.') +); +``` + +## Related + +The following APIs can be combined with `base64Url`. + +### Schemas + + + +### Methods + + + +### Utils + + diff --git a/website/src/routes/api/(actions)/base64Url/properties.ts b/website/src/routes/api/(actions)/base64Url/properties.ts new file mode 100644 index 000000000..05fc18bfb --- /dev/null +++ b/website/src/routes/api/(actions)/base64Url/properties.ts @@ -0,0 +1,58 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TInput: { + modifier: 'extends', + type: 'string', + }, + TMessage: { + modifier: 'extends', + type: { + type: 'union', + options: [ + { + type: 'custom', + name: 'ErrorMessage', + href: '../ErrorMessage/', + generics: [ + { + type: 'custom', + name: 'Base64UrlIssue', + href: '../Base64UrlIssue/', + generics: [ + { + type: 'custom', + name: 'TInput', + }, + ], + }, + ], + }, + 'undefined', + ], + }, + }, + message: { + type: { + type: 'custom', + name: 'TMessage', + }, + }, + Action: { + type: { + type: 'custom', + name: 'Base64UrlAction', + href: '../Base64UrlAction/', + generics: [ + { + type: 'custom', + name: 'TInput', + }, + { + type: 'custom', + name: 'TMessage', + }, + ], + }, + }, +}; diff --git a/website/src/routes/api/(types)/Base64UrlAction/index.mdx b/website/src/routes/api/(types)/Base64UrlAction/index.mdx new file mode 100644 index 000000000..9fb52f253 --- /dev/null +++ b/website/src/routes/api/(types)/Base64UrlAction/index.mdx @@ -0,0 +1,27 @@ +--- +title: Base64UrlAction +description: Base64Url action interface. +contributors: + - EltonLobo07 +--- + +import { Property } from '~/components'; +import { properties } from './properties'; + +# Base64UrlAction + +Base64Url action interface. + +## Generics + +- `TInput` +- `TMessage` + +## Definition + +- `Base64UrlAction` + - `type` + - `reference` + - `expects` + - `requirement` + - `message` diff --git a/website/src/routes/api/(types)/Base64UrlAction/properties.ts b/website/src/routes/api/(types)/Base64UrlAction/properties.ts new file mode 100644 index 000000000..0d52f7f58 --- /dev/null +++ b/website/src/routes/api/(types)/Base64UrlAction/properties.ts @@ -0,0 +1,93 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TInput: { + modifier: 'extends', + type: 'string', + }, + TMessage: { + modifier: 'extends', + type: { + type: 'union', + options: [ + { + type: 'custom', + name: 'ErrorMessage', + href: '../ErrorMessage/', + generics: [ + { + type: 'custom', + name: 'Base64UrlIssue', + href: '../Base64UrlIssue/', + generics: [ + { + type: 'custom', + name: 'TInput', + }, + ], + }, + ], + }, + 'undefined', + ], + }, + }, + BaseValidation: { + modifier: 'extends', + type: { + type: 'custom', + name: 'BaseValidation', + href: '../BaseValidation/', + generics: [ + { + type: 'custom', + name: 'TInput', + }, + { + type: 'custom', + name: 'TInput', + }, + { + type: 'custom', + name: 'Base64UrlIssue', + href: '../Base64UrlIssue/', + generics: [ + { + type: 'custom', + name: 'TInput', + }, + ], + }, + ], + }, + }, + type: { + type: { + type: 'string', + value: 'base64_url', + }, + }, + reference: { + type: { + type: 'custom', + modifier: 'typeof', + name: 'base64Url', + href: '../base64Url/', + }, + }, + expects: { + type: 'null', + }, + requirement: { + type: { + type: 'custom', + name: 'RegExp', + }, + }, + message: { + type: { + type: 'custom', + name: 'TMessage', + }, + }, +}; diff --git a/website/src/routes/api/(types)/Base64UrlIssue/index.mdx b/website/src/routes/api/(types)/Base64UrlIssue/index.mdx new file mode 100644 index 000000000..55afe840e --- /dev/null +++ b/website/src/routes/api/(types)/Base64UrlIssue/index.mdx @@ -0,0 +1,26 @@ +--- +title: Base64UrlIssue +description: Base64Url issue interface. +contributors: + - EltonLobo07 +--- + +import { Property } from '~/components'; +import { properties } from './properties'; + +# Base64UrlIssue + +Base64Url issue interface. + +## Generics + +- `TInput` + +## Definition + +- `Base64UrlIssue` + - `kind` + - `type` + - `expected` + - `received` + - `requirement` diff --git a/website/src/routes/api/(types)/Base64UrlIssue/properties.ts b/website/src/routes/api/(types)/Base64UrlIssue/properties.ts new file mode 100644 index 000000000..2e6be084a --- /dev/null +++ b/website/src/routes/api/(types)/Base64UrlIssue/properties.ts @@ -0,0 +1,59 @@ +import type { PropertyProps } from '~/components'; + +export const properties: Record = { + TInput: { + modifier: 'extends', + type: 'string', + }, + BaseIssue: { + modifier: 'extends', + type: { + type: 'custom', + name: 'BaseIssue', + href: '../BaseIssue/', + generics: [ + { + type: 'custom', + name: 'TInput', + }, + ], + }, + }, + kind: { + type: { + type: 'string', + value: 'validation', + }, + }, + type: { + type: { + type: 'string', + value: 'base64_url', + }, + }, + expected: { + type: 'null', + }, + received: { + type: { + type: 'template', + parts: [ + { + type: 'string', + value: '"', + }, + 'string', + { + type: 'string', + value: '"', + }, + ], + }, + }, + requirement: { + type: { + type: 'custom', + name: 'RegExp', + }, + }, +}; diff --git a/website/src/routes/api/menu.md b/website/src/routes/api/menu.md index fa8096268..30b4c961e 100644 --- a/website/src/routes/api/menu.md +++ b/website/src/routes/api/menu.md @@ -77,6 +77,7 @@ - [args](/api/args/) - [base64](/api/base64/) +- [base64Url](/api/base64Url/) - [bic](/api/bic/) - [brand](/api/brand/) - [bytes](/api/bytes/) @@ -258,6 +259,8 @@ - [AwaitActionAsync](/api/AwaitActionAsync/) - [Base64Action](/api/Base64Action/) - [Base64Issue](/api/Base64Issue/) +- [Base64UrlAction](/api/Base64UrlAction/) +- [Base64UrlIssue](/api/Base64UrlIssue/) - [BaseIssue](/api/BaseIssue/) - [BaseMetadata](/api/BaseMetadata/) - [BaseSchema](/api/BaseSchema/)