Skip to content

Commit

Permalink
feat(assert): add isNetworkPort, isHttpMethod (#1157)
Browse files Browse the repository at this point in the history
* fix(assert): isNumberSafeInt

* feat: add http method and network port guards
  • Loading branch information
belgattitude authored Apr 28, 2024
1 parent 0939128 commit 0d3b113
Show file tree
Hide file tree
Showing 22 changed files with 361 additions and 11 deletions.
5 changes: 5 additions & 0 deletions .changeset/funny-coats-remember.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@httpx/assert": patch
---

Fix isPlainObject when testing Object.create(null)
5 changes: 5 additions & 0 deletions .changeset/twenty-bulldogs-jump.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@httpx/assert": minor
---

Add network port and http methods typeguard and assertions
5 changes: 5 additions & 0 deletions .changeset/young-insects-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@httpx/assert": patch
---

Fix isNumberSafeInt return
48 changes: 48 additions & 0 deletions docs/src/pages/assert/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -343,6 +343,54 @@ isEan13('1234567890128'); // 👈 will check digit too
assertEan13('1234567890128');
```

### Network

#### isNetWorkPort

Check whether the value is a valid tcp/udp network port (>=0 ... <=65535)

```typescript
import { isNetworkPort } from "@httpx/assert";
import { assertNetworkPort } from "@httpx/assert";
import { type NetworkPort } from "@httpx/assert";

isNetworkPort(443); // 👈 weak opaque type is NetworkPort
assertNetworkPort(443);
```

### Http

#### isHttpMethod

Check whether the value is a specific http method (case-insensitive).

```typescript
import { isHttpMethod } from "@httpx/assert";
import { assertHttpMethod } from "@httpx/assert";
import { type HttpMethod } from "@httpx/assert";

const value: unknown = 'GET';

isHttpMethod('GET', value); // 👈 weak opaque type is HttpMethod
assertHttpMethod('GET', value);
```

#### isValidHttpMethod

Check whether the value is a valid http method (case-insensitive).

```typescript
import { isHttpValidMethod } from "@httpx/assert";
import { assertHttpValidMethod } from "@httpx/assert";
import { type HttpMethod } from "@httpx/assert";

const value: unknown = 'GET';

isHttpValidMethod(value); // 👈 weak opaque type is HttpMethod
assertHttpValidMethod(value);
```


## Bundle size

Code and bundler have been tuned to target a minimal compressed footprint
Expand Down
6 changes: 3 additions & 3 deletions packages/assert/.size-limit.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,13 +7,13 @@ module.exports = [
name: 'Everything (ESM)',
path: ['dist/index.mjs'],
import: "*",
limit: '1700B',
limit: '1900B',
},
{
name: 'Only isPlainObject (ESM)',
path: ['dist/index.mjs'],
import: "{ isPlainObject }",
limit: "60B",
limit: "76B",
},
{
name: 'Only isUuid (ESM)',
Expand All @@ -31,7 +31,7 @@ module.exports = [
name: 'Only assertPlainObject (ESM)',
path: ['dist/index.mjs'],
import: "{ assertPlainObject }",
limit: "452B",
limit: "461B",
},
{
name: 'Everything (CJS)',
Expand Down
49 changes: 48 additions & 1 deletion packages/assert/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -286,9 +286,9 @@ isUuid('90123e1c-7512-523e-bb28-76fab9f2f73d'); // 👉 valid uuid v1, 3, 4 or 5
assertUuid('90123e1c-7512-523e-bb28-76fab9f2f73d');

// With version
isUuid('90123e1c-7512-523e-bb28-76fab9f2f73d');
assertUuid('90123e1c-7512-523e-bb28-76fab9f2f73d');
assertUuidV5('90123e1c-7512-523e-bb28-76fab9f2f73d')
isUuid('90123e1c-7512-523e-bb28-76fab9f2f73d');
isUuidV4('d9428888-122b-11e1-b85c-61cd3cbb3210'); // 👈 or isUuidV1(''), isUuidV3(''), isUuidV5('')...;

// Utils
Expand All @@ -309,6 +309,53 @@ isEan13('1234567890128'); // 👈 will check digit too
assertEan13('1234567890128');
```

### Network

#### isNetWorkPort

Check whether the value is a valid tcp/udp network port (>=0 ... <=65535)

```typescript
import { isNetworkPort } from "@httpx/assert";
import { assertNetworkPort } from "@httpx/assert";
import { type NetworkPort } from "@httpx/assert";

isNetworkPort(443); // 👈 weak opaque type is NetworkPort
assertNetworkPort(443);
```

### Http

#### isHttpMethod

Check whether the value is a specific http method (case-insensitive).

```typescript
import { isHttpMethod } from "@httpx/assert";
import { assertHttpMethod } from "@httpx/assert";
import { type HttpMethod } from "@httpx/assert";

const value: unknown = 'GET';

isHttpMethod('GET', value); // 👈 weak opaque type is HttpMethod
assertHttpMethod('GET', value);
```

#### isValidHttpMethod

Check whether the value is a valid http method (case-insensitive).

```typescript
import { isHttpValidMethod } from "@httpx/assert";
import { assertHttpValidMethod } from "@httpx/assert";
import { type HttpMethod } from "@httpx/assert";

const value: unknown = 'GET';

isHttpValidMethod(value); // 👈 weak opaque type is HttpMethod
assertHttpValidMethod(value);
```

## Bundle size

Code and bundler have been tuned to target a minimal compressed footprint
Expand Down
38 changes: 38 additions & 0 deletions packages/assert/src/__tests__/http.asserts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { assertHttpMethod, assertHttpValidMethod } from '../http.asserts';

describe('http assertions tests', () => {
describe('assertHttpValidMethod', () => {
it('should not throw when given value is a valid http method', () => {
expect(() => assertHttpValidMethod('get')).not.toThrow();
expect(() => assertHttpValidMethod('GET')).not.toThrow();
});
it('should throw when not a valid http method', () => {
expect(() => assertHttpValidMethod('glue')).toThrow(
new TypeError(
'Value is expected to be an http method, got: string(length:4)'
)
);
});
it('should throw custom error when value is invalid', () => {
const e = new Error('cool');
expect(() => assertHttpValidMethod([], () => e)).toThrow(e);
});
});
describe('assertHttpMethod', () => {
it('should not throw when given value is a valid http method', () => {
expect(() => assertHttpMethod('GET', 'get')).not.toThrow();
expect(() => assertHttpMethod('POST', 'POST')).not.toThrow();
});
it('should throw when not a valid http method', () => {
expect(() => assertHttpMethod('GET', 'glue')).toThrow(
new TypeError(
"Value is expected to be an http 'GET' method, got: string(length:4)"
)
);
});
it('should throw custom error when value is invalid', () => {
const e = new Error('cool');
expect(() => assertHttpMethod('GET', [], () => e)).toThrow(e);
});
});
});
49 changes: 49 additions & 0 deletions packages/assert/src/__tests__/http.guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'vitest';

import { isHttpMethod, isHttpValidMethod } from '../http.guards';
import type { HttpMethod } from '../http.types';

describe('Http typeguards tests', () => {
describe('isHttpValidMethod', () => {
it.each([
[false, null],
[false, ''],
[false, 'GETT'],
[false, []],
[false, BigInt(10)],
[false, new Date()],
[true, 'GET'],
[true, 'PUT'],
[true, 'POST'],
[true, 'TRACE'],
[true, 'DELETE'],
[true, 'HEAD'],
[true, 'OPTIONS'],
[true, 'CONNECT'],
[true, 'get'],
[true, 'put'],
[true, 'post'],
[true, 'trace'],
[true, 'delete'],
[true, 'head'],
[true, 'options'],
[true, 'connect'],
])('should return %s when %s is given', (expected, v) => {
expect(isHttpValidMethod(v)).toBe(expected);
});
});
describe('isHttpMethod', () => {
it.each([
[false, 'GET', 'POST'],
[false, undefined, 'POST'],
[true, 'GET', 'GET'],
[true, 'get', 'GET'],
[true, 'post', 'POST'],
] as [expectation: boolean, v: unknown, method: HttpMethod][])(
'should return %s when %s is given against %s',
(expected, v, method) => {
expect(isHttpMethod(method, v)).toBe(expected);
}
);
});
});
20 changes: 20 additions & 0 deletions packages/assert/src/__tests__/network.asserts.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { assertNetworkPort } from '../network.asserts';

describe('Network assertions tests', () => {
it('should not throw when given value is a valid http method', () => {
expect(() => assertNetworkPort(0)).not.toThrow();
expect(() => assertNetworkPort(65_535)).not.toThrow();
expect(() => assertNetworkPort(443)).not.toThrow();
});
it('should throw when not a valid http method', () => {
expect(() => assertNetworkPort(1_234_567)).toThrow(
new TypeError(
'Value is expected to be a network port, got: number(length:7)'
)
);
});
it('should throw custom error when value is invalid', () => {
const e = new Error('cool');
expect(() => assertNetworkPort([], () => e)).toThrow(e);
});
});
20 changes: 20 additions & 0 deletions packages/assert/src/__tests__/network.guard.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { describe, expect, it } from 'vitest';

import { isNetworkPort } from '../network.guards';

describe('Network typeguards tests', () => {
describe('isNetworkPort', () => {
it.each([
[false, null],
[false, undefined],
[false, 128_000],
[false, BigInt(10)],
[false, new Date()],
[true, 0],
[true, 65_535],
[true, 80],
])('should return %s when %s is given', (expected, v) => {
expect(isNetworkPort(v)).toBe(expected);
});
});
});
1 change: 1 addition & 0 deletions packages/assert/src/__tests__/object.guards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ describe('Object typeguards tests', () => {
[new Error(), false],
[new Set(), false],
[new Request('http://localhost'), false],
[Object.create(null), false],
[/(\d+)/, false],
[new Promise(() => {}), false],
['hello', false],
Expand Down
36 changes: 36 additions & 0 deletions packages/assert/src/http.asserts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import { isHttpMethod, isHttpValidMethod } from './http.guards';
import type { HttpMethod } from './http.types';
import { formatErrMsg } from './messages/errorMessages';
import type { MsgOrErrorFactory } from './types/internal.types';
import { createAssertException } from './utils/createAssertException';
/**
* Assert the value is a valid http method (case-insensitive)
* @throws TypeError
*/
export function assertHttpValidMethod(
v: unknown,
msgOrErrorFactory?: MsgOrErrorFactory
): asserts v is HttpMethod {
if (!isHttpValidMethod(v)) {
throw createAssertException(
msgOrErrorFactory,
formatErrMsg('http method', v)
);
}
}

/**
* @throws TypeError
*/
export function assertHttpMethod<T extends HttpMethod>(
method: T,
v: unknown,
msgOrErrorFactory?: MsgOrErrorFactory
): asserts v is T {
if (!isHttpMethod(method, v)) {
throw createAssertException(
msgOrErrorFactory,
formatErrMsg(`http '${method}' method`, v)
);
}
}
11 changes: 11 additions & 0 deletions packages/assert/src/http.consts.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
export const httpMethods = [
'GET',
'POST',
'HEAD',
'PUT',
'DELETE',
'CONNECT',
'OPTIONS',
'PATCH',
'TRACE',
] as const;
20 changes: 20 additions & 0 deletions packages/assert/src/http.guards.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import { httpMethods } from './http.consts';
import type { HttpMethod } from './http.types';

/**
* Check whether the value is a valid http method (GET, PUT...) in
* a case-insensitive manner.
*/
export const isHttpValidMethod = (v: unknown): v is HttpMethod => {
return (
typeof v === 'string' &&
(httpMethods as unknown as string[]).includes(v.toUpperCase())
);
};

export const isHttpMethod = <T extends HttpMethod>(
method: T,
v: unknown
): v is T => {
return typeof v === 'string' && method === v.toUpperCase();
};
10 changes: 10 additions & 0 deletions packages/assert/src/http.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export type HttpMethod =
| 'GET'
| 'POST'
| 'HEAD'
| 'PUT'
| 'DELETE'
| 'CONNECT'
| 'OPTIONS'
| 'PATCH'
| 'TRACE';
10 changes: 10 additions & 0 deletions packages/assert/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,5 +30,15 @@ export * from './string.asserts';
export * from './string.guards';
export type * from './string.types';

// http
export * from './http.asserts';
export * from './http.guards';
export type * from './http.types';

// network
export * from './network.asserts';
export * from './network.guards';
export type * from './network.types';

// types
export * from './types.asserts';
Loading

0 comments on commit 0d3b113

Please sign in to comment.