Skip to content

add base64url action #1052

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 6 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
49 changes: 49 additions & 0 deletions library/src/actions/base64Url/base64Url.test-d.ts
Original file line number Diff line number Diff line change
@@ -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<string, undefined>;
expectTypeOf(base64Url<string>()).toEqualTypeOf<Action>();
expectTypeOf(
base64Url<string, undefined>(undefined)
).toEqualTypeOf<Action>();
});

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

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

describe('should infer correct types', () => {
type Action = Base64UrlAction<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<
Base64UrlIssue<string>
>();
});
});
});
183 changes: 183 additions & 0 deletions library/src/actions/base64Url/base64Url.test.ts
Original file line number Diff line number Diff line change
@@ -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<Base64UrlAction<string, never>, '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<string, undefined> = {
...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<string, string>);
});

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

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<Base64UrlIssue<string>, '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
]);
});
});
});
106 changes: 106 additions & 0 deletions library/src/actions/base64Url/base64Url.ts
Original file line number Diff line number Diff line change
@@ -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<TInput extends string>
extends BaseIssue<TInput> {
/**
* 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<Base64UrlIssue<TInput>> | undefined,
> extends BaseValidation<TInput, TInput, Base64UrlIssue<TInput>> {
/**
* 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<TInput extends string>(): 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<Base64UrlIssue<TInput>> | undefined,
>(message: TMessage): Base64UrlAction<TInput, TMessage>;

// @__NO_SIDE_EFFECTS__
export function base64Url(
message?: ErrorMessage<Base64UrlIssue<string>>
): Base64UrlAction<string, ErrorMessage<Base64UrlIssue<string>> | 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;
},
};
}
1 change: 1 addition & 0 deletions library/src/actions/base64Url/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './base64Url.ts';
1 change: 1 addition & 0 deletions library/src/actions/index.ts
Original file line number Diff line number Diff line change
@@ -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';
Expand Down
6 changes: 6 additions & 0 deletions library/src/regex.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*/
Expand Down
Loading