Replace entityBaseReviver with io-ts decoder

This commit is contained in:
Michael Hopcroft 2020-03-29 17:37:58 -07:00
Родитель e03038b720
Коммит 45baa755d1
9 изменённых файлов: 94 добавлений и 100 удалений

Просмотреть файл

@ -37,8 +37,8 @@ function main(argv: string[]) {
program.version(apiVersion);
program
.command('connect [server]')
.description('Connect to a Laboratory service or print connection info.')
.command('connect [service]')
.description('Connect to a Laboratory [service] or print connection info.')
.action(connect);
program

Просмотреть файл

@ -10,6 +10,7 @@
* Server Error: listen EADDRINUSE: address already in use :::3000
* Enforce workflow for status changes (e.g. disallow complete to created)
* Authentication
* x Replace entityBaseReviver with io-ts decoder.
* x REVIEW use of TypeError instead of IllegalOperationError or EntityNotFoundError
* x REVIEW: net.Server vs http.Server
* Are README.md dependencies correct? Does the user have to install anything else?
@ -59,7 +60,7 @@
* deploy command
* Bash completion api
* = usage configuration in yargs
* = Consider using luxon in reviver - probably can't since io-ts uses Date
* x Consider using luxon in reviver - probably can't since io-ts uses Date
* x Set version
* x examples in usage()
* x Spike commander as replacement for yargs
@ -183,10 +184,11 @@
* check sqlite error behavior on string value (varchar(256)) too long.
* dateColumn decorator does not seem the apply to createdAt and updatedAt
* Investigate schema verification in jsonColumn decorator.
* Consider removing luxon
* x Consider removing luxon - may be using for custom io-ts codec.
* Duplicate code
* toPOJO()
* assertDeepEqual()
* Accidentally PUT benchmark to candidates seems error-prone. Also PUT suite/candidate to candidate/suite.
* Investigate BSON. https://www.npmjs.com/package/bson
*

Просмотреть файл

@ -28,32 +28,10 @@ import {
validate,
} from '../logic';
import { entityBaseReviver } from '../server';
// tslint:disable-next-line:no-any
function jsonParser(data: any): any {
try {
return JSON.parse(data, entityBaseReviver);
} catch (e) {
// Not all payloads are JSON. Some POSTs and PUTS return "OK"
return data;
}
}
const config: AxiosRequestConfig = {
// TODO: put credentials here.
};
const configForGet: AxiosRequestConfig = {
...config,
transformResponse: [jsonParser],
};
const configForPatchPostPut: AxiosRequestConfig = {
...config,
transformResponse: [jsonParser],
};
export class LaboratoryClient implements ILaboratory {
endpoint: string;
@ -61,8 +39,6 @@ export class LaboratoryClient implements ILaboratory {
this.endpoint = endpoint;
}
// TODO: error handling pattern/strategy
/////////////////////////////////////////////////////////////////////////////
//
// Benchmarks
@ -70,7 +46,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allBenchmarks(): Promise<IBenchmark[]> {
const url = new URL('benchmarks', this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const benchmarks = validate(BenchmarkArrayType, response.data);
return benchmarks;
}
@ -78,7 +54,7 @@ export class LaboratoryClient implements ILaboratory {
async oneBenchmark(rawName: string): Promise<IBenchmark> {
const name = normalizeName(rawName);
const url = new URL(`benchmarks/${name}`, this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const benchmark = validate(BenchmarkType, response.data);
return benchmark;
}
@ -93,7 +69,7 @@ export class LaboratoryClient implements ILaboratory {
throw new IllegalOperationError(message);
}
const url = new URL(`benchmarks/${name}`, this.endpoint);
await axios.put(url.toString(), benchmark, configForPatchPostPut);
await axios.put(url.toString(), benchmark, config);
}
/////////////////////////////////////////////////////////////////////////////
@ -103,7 +79,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allCandidates(): Promise<ICandidate[]> {
const url = new URL('candidates', this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const candidates = validate(CandidateArrayType, response.data);
return candidates;
}
@ -111,7 +87,7 @@ export class LaboratoryClient implements ILaboratory {
async oneCandidate(rawName: string): Promise<ICandidate> {
const name = normalizeName(rawName);
const url = new URL(`candidates/${name}`, this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const candidate = validate(CandidateType, response.data);
return candidate;
}
@ -126,7 +102,7 @@ export class LaboratoryClient implements ILaboratory {
throw new IllegalOperationError(message);
}
const url = new URL(`candidates/${name}`, this.endpoint);
await axios.put(url.toString(), candidate, configForPatchPostPut);
await axios.put(url.toString(), candidate, config);
}
/////////////////////////////////////////////////////////////////////////////
@ -136,7 +112,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allSuites(): Promise<ISuite[]> {
const url = new URL('suites', this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const suites = validate(SuiteArrayType, response.data);
return suites;
}
@ -144,7 +120,7 @@ export class LaboratoryClient implements ILaboratory {
async oneSuite(rawName: string): Promise<ISuite> {
const name = normalizeName(rawName);
const url = new URL(`suites/${name}`, this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const suite = validate(SuiteType, response.data);
return suite;
}
@ -156,7 +132,7 @@ export class LaboratoryClient implements ILaboratory {
throw new IllegalOperationError(message);
}
const url = new URL(`suites/${name}`, this.endpoint);
await axios.put(url.toString(), suite, configForPatchPostPut);
await axios.put(url.toString(), suite, config);
}
/////////////////////////////////////////////////////////////////////////////
@ -166,7 +142,7 @@ export class LaboratoryClient implements ILaboratory {
/////////////////////////////////////////////////////////////////////////////
async allRuns(): Promise<IRun[]> {
const url = new URL('runs', this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const runs = validate(RunArrayType, response.data);
return runs;
}
@ -174,18 +150,14 @@ export class LaboratoryClient implements ILaboratory {
async oneRun(rawName: string): Promise<IRun> {
const name = normalizeRunName(rawName);
const url = new URL(`runs/${name}`, this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const run = validate(RunType, response.data);
return run;
}
async createRunRequest(spec: IRunRequest): Promise<IRun> {
const url = new URL('runs', this.endpoint);
const response = await axios.post(
url.toString(),
spec,
configForPatchPostPut
);
const response = await axios.post(url.toString(), spec, config);
const run = validate(RunType, response.data);
return run;
}
@ -194,21 +166,21 @@ export class LaboratoryClient implements ILaboratory {
const name = normalizeRunName(rawName);
const url = new URL(`runs/${name}`, this.endpoint);
const body: IUpdateRunStatus = { status };
await axios.patch(url.toString(), body, configForPatchPostPut);
await axios.patch(url.toString(), body, config);
}
async reportRunResults(rawName: string, measures: Measures): Promise<void> {
const name = normalizeRunName(rawName);
const url = new URL(`runs/${name}/results`, this.endpoint);
const body: IReportRunResults = { measures };
await axios.patch(url.toString(), body, configForPatchPostPut);
await axios.patch(url.toString(), body, config);
}
async allRunResults(benchmark: string, suite: string): Promise<IResult[]> {
const b = normalizeName(benchmark);
const s = normalizeName(suite);
const url = new URL(`runs/${b}/${s}`, this.endpoint);
const response = await axios.get(url.toString(), configForGet);
const response = await axios.get(url.toString(), config);
const results = validate(ResultArrayType, response.data);
return results;
}

Просмотреть файл

@ -1,16 +1,19 @@
import { either } from 'fp-ts/lib/Either';
import * as t from 'io-ts';
import { DateTime } from 'luxon';
export const apiVersion = '0.0.1';
// tslint:disable-next-line:variable-name
const DateType = new t.Type<Date, Date, unknown>(
'Date2',
(input: unknown): input is Date => input instanceof Date,
// `t.success` and `t.failure` are helpers used to build `Either` instances
(input, context) =>
input instanceof Date ? t.success(input) : t.failure(input, context),
// `A` and `O` are the same, so `encode` is just the identity function
t.identity
const DateType = new t.Type<Date, string, unknown>(
'Date',
(u): u is Date => u instanceof Date,
(u, c) =>
either.chain(t.string.validate(u, c), s => {
const d = DateTime.fromISO(s);
return d.isValid ? t.success(d.toJSDate()) : t.failure(u, c);
}),
a => a.toISOString()
);
///////////////////////////////////////////////////////////////////////////////

Просмотреть файл

@ -27,8 +27,8 @@ export async function createApp(lab: ILaboratory): Promise<express.Express> {
app.use(
// tslint:disable-next-line: deprecation
bodyParser.json({
// TODO: REVIEW: magic number 100kb
limit: '100kb',
reviver: entityBaseReviver,
})
);
@ -61,12 +61,3 @@ export async function createApp(lab: ILaboratory): Promise<express.Express> {
return app;
}
// tslint:disable-next-line:no-any
export function entityBaseReviver(key: string, value: any) {
if (key === 'updatedAt' || key === 'createdAt') {
return new Date(value);
} else {
return value;
}
}

Просмотреть файл

@ -7,10 +7,14 @@ import { LaboratoryClient } from '../../../../src/laboratory/client';
import { benchmark1, candidate1, run1, suite1, runRequest1 } from '../data';
import {
RunStatus,
IUpdateRunStatus,
BenchmarkType,
CandidateType,
IReportRunResults,
IUpdateRunStatus,
Measures,
RunStatus,
SuiteType,
validate,
} from '../../../../src';
chai.use(chaiAsPromised);
@ -54,7 +58,8 @@ describe('laboratory/client', () => {
const client = new LaboratoryClient(endpoint);
await client.upsertBenchmark(benchmark1);
assert.deepEqual(request!, benchmark1);
const observed = validate(BenchmarkType, request!);
assert.deepEqual(observed, benchmark1);
});
it('upsertBenchmark() - name mismatch', async () => {
@ -107,7 +112,8 @@ describe('laboratory/client', () => {
const client = new LaboratoryClient(endpoint);
await client.upsertCandidate(candidate1);
assert.deepEqual(request!, candidate1);
const observed = validate(CandidateType, request!);
assert.deepEqual(observed, candidate1);
});
it('upsertCandidate() - name mismatch', async () => {
@ -160,7 +166,8 @@ describe('laboratory/client', () => {
const client = new LaboratoryClient(endpoint);
await client.upsertSuite(suite1);
assert.deepEqual(request!, suite1);
const observed = validate(SuiteType, request!);
assert.deepEqual(observed, suite1);
});
it('upsertSuite() - name mismatch', async () => {

Просмотреть файл

@ -19,6 +19,11 @@ import {
export const serviceURL = 'http://localhost:3000'; // TODO: plumb real url.
export const blobBase = 'http://blobs';
export const timestamps = {
createdAt: new Date('2020-03-19T21:37:31.452Z'),
updatedAt: new Date('2020-03-20T22:37:31.452Z'),
};
export const pipelines: IPipeline[] = [
{
mode: 'mode1',
@ -39,6 +44,7 @@ export const benchmark1: IBenchmark = {
author: 'author1',
version: apiVersion,
pipelines,
...timestamps,
};
export const benchmark2: IBenchmark = {
@ -46,12 +52,14 @@ export const benchmark2: IBenchmark = {
author: 'author2',
version: apiVersion,
pipelines,
...timestamps,
};
export const benchmark3: IBenchmark = {
name: 'benchmark3',
author: 'author3',
version: apiVersion,
...timestamps,
pipelines,
};
@ -62,6 +70,7 @@ export const candidate1: ICandidate = {
benchmark: 'benchmark1',
mode: 'mode1',
image: 'candidate1-image',
...timestamps,
};
export const candidate2: ICandidate = {
@ -71,6 +80,7 @@ export const candidate2: ICandidate = {
benchmark: 'benchmark1',
mode: 'mode1',
image: 'candidate2-image',
...timestamps,
};
export const candidate3: ICandidate = {
@ -80,6 +90,7 @@ export const candidate3: ICandidate = {
benchmark: 'benchmark1',
mode: 'mode1',
image: 'candidate3-image',
...timestamps,
};
export const suite1: ISuite = {
@ -88,6 +99,7 @@ export const suite1: ISuite = {
version: apiVersion,
benchmark: 'benchmark1',
mode: 'mode1',
...timestamps,
};
export const suite2: ISuite = {
@ -96,6 +108,7 @@ export const suite2: ISuite = {
version: apiVersion,
benchmark: 'benchmark1',
mode: 'mode1',
...timestamps,
};
export const suite3: ISuite = {
@ -104,6 +117,7 @@ export const suite3: ISuite = {
version: apiVersion,
benchmark: 'benchmark1',
mode: 'mode1',
...timestamps,
};
export const runRequest1: IRunRequest = {
@ -121,4 +135,5 @@ export const run1: IRun = {
suite: suite1,
blob: new URL(runid, blobBase).toString(),
status: RunStatus.CREATED,
...timestamps,
};

Просмотреть файл

@ -74,8 +74,6 @@ describe('laboratory/benchmarks', () => {
});
it('upsertBenchmark()', async () => {
console.log('benchmark');
await lab.upsertBenchmark(benchmark1);
const results1 = await lab.allBenchmarks();
assertDeepEqual(results1, [benchmark1]);

Просмотреть файл

@ -23,6 +23,7 @@ import { createApp } from '../../../../src/laboratory/server';
interface XMLHttpRequest {}
import {
BenchmarkType,
IBenchmark,
ICandidate,
IReportRunResults,
@ -33,6 +34,10 @@ import {
IUpdateRunStatus,
Measures,
RunStatus,
validate,
CandidateType,
SuiteType,
RunType,
} from '../../../../src';
import { benchmark1, candidate1, run1, suite1 } from '../data';
@ -69,20 +74,20 @@ describe('laboratory/server', () => {
it('oneBenchmark()', async () => {
const lab = new MockLaboratory();
const expected = 'benchmark1';
let observed: string | undefined;
let observedName: string | undefined;
lab.oneBenchmark = async (name: string): Promise<IBenchmark> => {
observed = name;
observedName = name;
return benchmark1;
};
chai
.request(await createApp(lab))
.get(`/benchmarks/${expected}`)
.get(`/benchmarks/${benchmark1.name}`)
.end((err, res) => {
assert.equal(res.status, 200);
assertDeepEqual(res.body, benchmark1);
assert.equal(observed, expected);
const observed = validate(BenchmarkType, res.body);
assert.deepEqual(observed, benchmark1);
assert.equal(observedName, benchmark1.name);
});
});
@ -138,20 +143,20 @@ describe('laboratory/server', () => {
it('oneCandidate()', async () => {
const lab = new MockLaboratory();
const expected = 'candidate1';
let observed: string | undefined;
let observedName: string | undefined;
lab.oneCandidate = async (name: string): Promise<ICandidate> => {
observed = name;
observedName = name;
return candidate1;
};
chai
.request(await createApp(lab))
.get(`/candidates/${expected}`)
.get(`/candidates/${candidate1.name}`)
.end((err, res) => {
assert.equal(res.status, 200);
assertDeepEqual(res.body, candidate1);
assert.equal(observed, expected);
const observed = validate(CandidateType, res.body);
assert.deepEqual(observed, candidate1);
assert.equal(observedName, candidate1.name);
});
});
@ -207,20 +212,20 @@ describe('laboratory/server', () => {
it('oneSuite()', async () => {
const lab = new MockLaboratory();
const expected = 'suite1';
let observed: string | undefined;
let observedName: string | undefined;
lab.oneSuite = async (name: string): Promise<ISuite> => {
observed = name;
observedName = name;
return suite1;
};
chai
.request(await createApp(lab))
.get(`/suites/${expected}`)
.get(`/suites/${suite1.name}`)
.end((err, res) => {
assert.equal(res.status, 200);
assertDeepEqual(res.body, suite1);
assert.equal(observed, expected);
const observed = validate(SuiteType, res.body);
assert.deepEqual(observed, suite1);
assert.equal(observedName, suite1.name);
});
});
@ -273,20 +278,20 @@ describe('laboratory/server', () => {
it('oneRun()', async () => {
const lab = new MockLaboratory();
const expected = 'run1';
let observed: string | undefined;
let observedName: string | undefined;
lab.oneRun = async (name: string): Promise<IRun> => {
observed = name;
observedName = name;
return run1;
};
chai
.request(await createApp(lab))
.get(`/runs/${expected}`)
.get(`/runs/${run1.name}`)
.end((err, res) => {
assert.equal(res.status, 200);
assertDeepEqual(res.body, run1);
assert.equal(observed, expected);
const observed = validate(RunType, res.body);
assert.deepEqual(observed, run1);
assert.equal(observedName, run1.name);
});
});
@ -298,10 +303,10 @@ describe('laboratory/server', () => {
suite: run1.suite.name,
};
let observed: IRun;
let observedRequest: IRunRequest;
lab.createRunRequest = async (spec: IRunRequest): Promise<IRun> => {
observed = run1;
return observed;
observedRequest = spec;
return run1;
};
chai
@ -310,8 +315,9 @@ describe('laboratory/server', () => {
.send(runRequest)
.end((err, res) => {
assert.equal(res.status, 200);
assert.deepEqual(observedRequest, runRequest);
const observed = validate(RunType, res.body);
assert.deepEqual(observed, run1);
assertDeepEqual(res.body, run1);
});
});