Skip to content

Commit

Permalink
Define host, hostname, and origin variable decoders
Browse files Browse the repository at this point in the history
  • Loading branch information
ryota-ka committed Feb 21, 2022
1 parent 449c065 commit 3dc0399
Show file tree
Hide file tree
Showing 7 changed files with 261 additions and 0 deletions.
84 changes: 84 additions & 0 deletions src/VariableDecoder/host.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as fc from 'fast-check';
import * as E from 'fp-ts/Either';

import { DecodeFailed } from '..';
import { Variable } from '../Variable';

import { host } from './host';

describe(host, () => {
it('accepts an arbitrary host string', () => {
const decoder = host();

fc.assert(
fc.property(
fc
.tuple(
fc.webUrl(),
fc.nat().filter((n) => n < 65536),
)
.map(([url, port]) => `${new URL(url).host}:${port}`),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.right(str));
},
),
);
});

it('requires a port number', () => {
const decoder = host();

fc.assert(
fc.property(
fc.webUrl().map((url) => new URL(url).hostname),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.left(new DecodeFailed(variable, 'must be a valid host')));
},
),
);
});

it('rejects an arbitrary URL string', () => {
const decoder = host();

fc.assert(
fc.property(
fc.webUrl().map((str) => {
const url = new URL(str);
url.port = '80';
return url.href;
}),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.left(new DecodeFailed(variable, 'must be a valid host')));
},
),
);
});

it('rejects the trailing pathname', () => {
const decoder = host();

fc.assert(
fc.property(
fc
.tuple(
fc.webUrl().filter((url) => new URL(url).pathname !== '/'),
fc.nat().filter((n) => n < 65536),
)

.map(([orig, port]) => {
const url = new URL(orig);
url.port = String(port);
return url.host + url.pathname;
}),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.left(new DecodeFailed(variable, 'must be a valid host')));
},
),
);
});
});
26 changes: 26 additions & 0 deletions src/VariableDecoder/host.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as RE from 'fp-ts/ReaderEither';

import { ask, decodeFailed, validate, VariableDecoder } from './VariableDecoder';

/**
* Decodes a host.
* A host consists of a hostname and a port number.
*/
const host = (): VariableDecoder<string> =>
pipe(
RE.Do,
RE.bind('value', () => ask()),
RE.bind('url', ({ value }) =>
pipe(
O.tryCatch(() => RE.of(new URL(`https://${value}/`))),
O.getOrElse(() => decodeFailed('must be a valid host')),
),
),
RE.chain(validate(({ url }) => url.port !== '', 'must be a valid host')),
RE.chain(validate(({ value, url }) => url.host === value, 'must be a valid host')),
RE.map(({ value }) => value),
);

export { host };
55 changes: 55 additions & 0 deletions src/VariableDecoder/hostname.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import * as fc from 'fast-check';
import * as E from 'fp-ts/Either';

import { DecodeFailed } from '..';
import { Variable } from '../Variable';

import { hostname } from './hostname';

describe(hostname, () => {
it('accepts an arbitrary URL string', () => {
const decoder = hostname();

fc.assert(
fc.property(
fc.webUrl().map((url) => new URL(url).hostname),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.right(str));
},
),
);
});

it('rejects an arbitrary host string with a port number', () => {
const decoder = hostname();

fc.assert(
fc.property(
fc
.tuple(
fc.webUrl(),
fc.nat().filter((n) => n < 65536),
)
.map(([url, port]) => `${new URL(url).hostname}:${port}`),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(
E.left(new DecodeFailed(variable, 'must be a valid hostname')),
);
},
),
);
});

it('rejects an arbitrary URL string', () => {
const decoder = hostname();

fc.assert(
fc.property(fc.webUrl(), (str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.left(new DecodeFailed(variable, 'must be a valid hostname')));
}),
);
});
});
24 changes: 24 additions & 0 deletions src/VariableDecoder/hostname.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as RE from 'fp-ts/ReaderEither';

import { ask, decodeFailed, validate, VariableDecoder } from './VariableDecoder';

/**
* Decodes a hostname.
*/
const hostname = (): VariableDecoder<string> =>
pipe(
RE.Do,
RE.bind('value', () => ask()),
RE.bind('url', ({ value }) =>
pipe(
O.tryCatch(() => RE.of(new URL(`https://${value}:80/`))),
O.getOrElse(() => decodeFailed('must be a valid hostname')),
),
),
RE.chain(validate(({ value, url }) => url.hostname === value, 'must be a valid hostname')),
RE.map(({ value }) => value),
);

export { hostname };
3 changes: 3 additions & 0 deletions src/VariableDecoder/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,14 @@ export { base64 } from './base64';
export { bigInt } from './bigInt';
export { boolean } from './boolean';
export { hex } from './hex';
export { host } from './host';
export { hostname } from './hostname';
export { integer } from './integer';
export { keyOf } from './keyOf';
export { literal } from './literal';
export { natural } from './natural';
export { number } from './number';
export { origin } from './origin';
export { port } from './port';
export { string } from './string';
export { url } from './url';
Expand Down
44 changes: 44 additions & 0 deletions src/VariableDecoder/origin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import * as fc from 'fast-check';
import * as E from 'fp-ts/Either';

import { DecodeFailed } from '..';
import { Variable } from '../Variable';

import { origin } from './origin';

describe(origin, () => {
it('accepts an arbitrary origin string', () => {
const decoder = origin();

fc.assert(
fc.property(
fc
.tuple(
fc.webUrl(),
fc.nat().filter((n) => n < 65536),
)
.map(([url, port]) => `${new URL(url).origin}:${port}`),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(E.right(str));
},
),
);
});

it('rejects the complete URL', () => {
const decoder = origin();

fc.assert(
fc.property(
fc.webUrl().filter((url) => new URL(url).pathname !== '/'),
(str) => {
const variable = new Variable('KEY', str);
expect(decoder(variable)).toStrictEqual(
E.left(new DecodeFailed(variable, 'must be a valid URL origin')),
);
},
),
);
});
});
25 changes: 25 additions & 0 deletions src/VariableDecoder/origin.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { pipe } from 'fp-ts/function';
import * as O from 'fp-ts/Option';
import * as RE from 'fp-ts/ReaderEither';

import { ask, decodeFailed, validate, VariableDecoder } from './VariableDecoder';

/**
* Decodes a URL origin (scheme + hostname + port).
*/
const origin = (): VariableDecoder<string> =>
pipe(
RE.Do,
RE.bind('value', () => ask()),
RE.bind('url', ({ value }) =>
pipe(
O.tryCatch(() => RE.of(new URL(value))),
O.getOrElse(() => decodeFailed('must be a valid URL origin')),
),
),
RE.chain(validate(({ url }) => url.pathname !== '', 'must not have a pathname')),
RE.chain(validate(({ value, url }) => url.origin === value, 'must be a valid URL origin')),
RE.map(({ value }) => value),
);

export { origin };

0 comments on commit 3dc0399

Please sign in to comment.