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

Feat: Add CIDR related actions #849

Open
wants to merge 8 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 library/src/actions/ipCidr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ipCidr.ts';
43 changes: 43 additions & 0 deletions library/src/actions/ipCidr/ipCidr.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 { ipCidr, type IpCidrAction, type IpCidrIssue } from './ipCidr.ts';

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

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

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

describe('should infer correct types', () => {
type Action = IpCidrAction<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<IpCidrIssue<string>>();
});
});
});
138 changes: 138 additions & 0 deletions library/src/actions/ipCidr/ipCidr.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,138 @@
import { describe, expect, test } from 'vitest';
import { IP_CIDR_REGEX } from '../../regex.ts';
import { expectActionIssue, expectNoActionIssue } from '../../vitest/index.ts';
import { ipCidr, type IpCidrAction, type IpCidrIssue } from './ipCidr.ts';

// TODO: Improve tests to cover all possible scenarios based on the regex used.

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

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

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

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

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

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

test('for IPv4 address', () => {
expectNoActionIssue(action, [
'192.168.1.1/0',
'127.0.0.1/9',
'0.0.0.0/10',
'255.255.255.255/11',
'237.84.2.178/19',
'89.207.132.170/20',
'237.84.2.178/21',
'55.151.133.223/29',
'244.178.44.111/30',
'234.218.86.91/32',
]);
});

test('for IPv6 address', () => {
expectNoActionIssue(action, [
'2001:0db8:85a3:0000:0000:8a2e:0370:7334/0',
'FE80:0000:0000:0000:0202:B3FF:FE1E:8329/9',
'fe80::1ff:fe23:4567:890a/10',
'2001:db8:85a3:8d3:1319:8a2e:370:7348/11',
'e05b:3266:e43f:3fdf:a34c:c11:dbc4:349f/19',
'f053:688e:431:c36b:a452:425:8c88:713e/20',
'620a:9614:8852:a772:9c03:fd43:34a2:3c91/21',
'58e9:b974:fd6a:ff97:5376:22c2:321f:2144/99',
'7066:1b31:6757:e5cf:bd06:a46b:2e97:838f/100',
'3640:328f:e171:975:cbd1:1ac:5e72:7bfd/101',
'f9c9:1876:9b59:c6f3:8a36:44a7:382d:1f50/110',
'f48:9dbd:ae0e:4586:8209:38a4:a191:2b0e/119',
'c159:7a14:58ba:ca7c:d3d3:98ce:6978:68e3/120',
'3457:589a:2291:1598:be2:16d7:4902:1e37/121',
'5eb9:bc94:d0fb:3f9a:173a:74c1:86ca:1ef7/128',
]);
});
});

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

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

test('for invalid IPv4 address', () => {
expectActionIssue(action, baseIssue, [
'1/24',
'-1.0.0.0/24',
'0..0.0.0/12',
'1234.0.0.0/16',
'256.256.256.256/24',
'1.2.3/32',
'0.0.0.0.0/12',
'a.a.a.a/10',
'237.84.2.178/33',
'82.78.37.80/-1',
'82.78.37.80/40',
'82.78.37.80/128',
]);
});

test('for invalid IPv6 address', () => {
expectActionIssue(action, baseIssue, [
'd329:1be4:25b4:db47:a9d1:dc71:4926:992c:14af/32',
'd5e7:7214:2b78::3906:85e6:53cc:709:32ba/40',
'8f69::c757:395e:976e::3441/64',
'54cb::473f:d516:0.255.256.22/72',
'54cb::473f:d516:192.168.1/86',
'test:test:test:test:test:test:test:test/24',
'f48:9dbd:ae0e:4586:8209:38a4:a191:2b0e/129',
'5b54:5d3e:b867:f367:2d53:8b26:ce9/-1',
'f9c9:1876:9b59:c6f3:8a36:44a7:382d:1f50/130',
'f48:9dbd:ae0e:4586:8209:38a4:a191:2b0e/200',
'f48:9dbd:ae0e:4586:8209:38a4:a191:2b0e/1128',
]);
});
});
});
105 changes: 105 additions & 0 deletions library/src/actions/ipCidr/ipCidr.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
import { IP_CIDR_REGEX } from '../../regex.ts';
import type {
BaseIssue,
BaseValidation,
Dataset,
ErrorMessage,
} from '../../types/index.ts';
import { _addIssue } from '../../utils/index.ts';

/**
* IP CIDR issue type.
*/
export interface IpCidrIssue<TInput extends string> extends BaseIssue<TInput> {
/**
* The issue kind.
*/
readonly kind: 'validation';
/**
* The issue type.
*/
readonly type: 'ip_cidr';
/**
* The expected property.
*/
readonly expected: null;
/**
* The received property.
*/
readonly received: `"${string}"`;
/**
* The IP CIDR regex.
*/
readonly requirement: RegExp;
}

/**
* IP CIDR action type.
*/
export interface IpCidrAction<
TInput extends string,
TMessage extends ErrorMessage<IpCidrIssue<TInput>> | undefined,
> extends BaseValidation<TInput, TInput, IpCidrIssue<TInput>> {
/**
* The action type.
*/
readonly type: 'ip_cidr';
/**
* The action reference.
*/
readonly reference: typeof ipCidr;
/**
* The expected property.
*/
readonly expects: null;
/**
* The IP CIDR regex.
*/
readonly requirement: RegExp;
/**
* The error message.
*/
readonly message: TMessage;
}

/**
* Creates an [IP address](https://en.wikipedia.org/wiki/IP_address) in [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) validation action.
*
* @returns An IP CIDR action.
*/
export function ipCidr<TInput extends string>(): IpCidrAction<
TInput,
undefined
>;

/**
* Creates an [IP address](https://en.wikipedia.org/wiki/IP_address) in [CIDR notation](https://en.wikipedia.org/wiki/Classless_Inter-Domain_Routing) validation action.
*
* @param message The error message.
*
* @returns An IP CIDR action.
*/
export function ipCidr<
TInput extends string,
const TMessage extends ErrorMessage<IpCidrIssue<TInput>> | undefined,
>(message: TMessage): IpCidrAction<TInput, TMessage>;

export function ipCidr(
message?: ErrorMessage<IpCidrIssue<string>>
): IpCidrAction<string, ErrorMessage<IpCidrIssue<string>> | undefined> {
return {
kind: 'validation',
type: 'ip_cidr',
reference: ipCidr,
async: false,
expects: null,
requirement: IP_CIDR_REGEX,
message,
_run(dataset, config) {
if (dataset.typed && !this.requirement.test(dataset.value)) {
_addIssue(this, 'IP-CIDR', dataset, config);
}
return dataset as Dataset<string, IpCidrIssue<string>>;
},
};
}
1 change: 1 addition & 0 deletions library/src/actions/ipv4Cidr/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from './ipv4Cidr.ts';
47 changes: 47 additions & 0 deletions library/src/actions/ipv4Cidr/ipv4Cidr.test-d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { describe, expectTypeOf, test } from 'vitest';
import type { InferInput, InferIssue, InferOutput } from '../../types/index.ts';
import {
ipv4Cidr,
type Ipv4CidrAction,
type Ipv4CidrIssue,
} from './ipv4Cidr.ts';

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

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

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

describe('should infer correct types', () => {
type Action = Ipv4CidrAction<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<Ipv4CidrIssue<string>>();
});
});
});
Loading