Skip to content

Commit

Permalink
chore(modules): refactor validate/moduleExec for clarity, logging
Browse files Browse the repository at this point in the history
  • Loading branch information
gadicc committed Feb 7, 2021
1 parent e991219 commit d8dc750
Show file tree
Hide file tree
Showing 9 changed files with 175 additions and 69 deletions.
48 changes: 47 additions & 1 deletion src/lib/moduleExec.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,59 @@ import _moduleExec from './moduleExec';
const yf = {
_env,
_fetch,
_opts: { validation: { logErrors: true }},
_opts: { validation: { logErrors: true, logOptionsErrors: false }},
_moduleExec,
search
};

describe('moduleExec', () => {

describe('options validation', () => {

it('throws InvalidOptions on invalid options', async () => {
const rwo = (options:any) => yf.search('symbol', options);
await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError)
});

it('logs errors on invalid options when logOptionsErrors = true', async () => {
yf._opts.validation.logOptionsErrors = true;
const realConsole = console;
const fakeConsole = { error: jest.fn(), log: jest.fn(), dir: jest.fn() };

/* @ts-ignore */
console = fakeConsole;
const rwo = (options:any) => yf.search('symbol', options);
await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError)
console = realConsole;

expect(
fakeConsole.log.mock.calls.length +
fakeConsole.error.mock.calls.length +
fakeConsole.dir.mock.calls.length
).toBeGreaterThan(1);
yf._opts.validation.logOptionsErrors = false;
});

it('does not log errors on invalid options when logOptionsErrors = false', async () => {
yf._opts.validation.logOptionsErrors = false;
const realConsole = console;
const fakeConsole = { error: jest.fn(), log: jest.fn(), dir: jest.fn() };

/* @ts-ignore */
console = fakeConsole;
const rwo = (options:any) => yf.search('symbol', options);
await expect(rwo({ invalid: true })).rejects.toThrow(InvalidOptionsError)
console = realConsole;

expect(
fakeConsole.log.mock.calls.length +
fakeConsole.error.mock.calls.length +
fakeConsole.dir.mock.calls.length
).toBe(0);
});

});

describe('result validation', () => {

it('throws on unexpected input', async () => {
Expand Down
37 changes: 26 additions & 11 deletions src/lib/moduleExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import validateAndCoerceTypes from './validateAndCoerceTypes';
import csv2json from './csv2json';
import _opts from './options';

interface TransformFunc {
// The consuming module itself will have a stricter return type.
Expand Down Expand Up @@ -95,30 +96,38 @@ interface ModuleExecOptions {
type ThisWithFetch = { [key: string]: any; _moduleExec: Function };

export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOptions) {
const query = opts.query;
const queryOpts = opts.query;
const moduleOpts = opts.moduleOptions;
const moduleName = opts.moduleName;
const resultOpts = opts.result;

// Check that query options passed by the user are valid for this module
validateAndCoerceTypes(query.overrides, query.schemaKey, opts.moduleName);
validateAndCoerceTypes({
source: moduleName,
type: 'options',
object: queryOpts.overrides,
schemaKey: queryOpts.schemaKey,
options: this._opts ? this._opts.validation : _opts.validation
});

let queryOptions = {
...query.defaults, // Module defaults e.g. { period: '1wk', lang: 'en' }
...query.runtime, // Runtime params e.g. { q: query }
...query.overrides, // User supplied options that override above
...queryOpts.defaults, // Module defaults e.g. { period: '1wk', lang: 'en' }
...queryOpts.runtime, // Runtime params e.g. { q: query }
...queryOpts.overrides, // User supplied options that override above
};

/*
* Called with the merged (defaults,runtime,overrides) before running
* the query. Useful to transform options we allow but not Yahoo, e.g.
* allow a "2020-01-01" date but transform this to a UNIX epoch.
*/
if (query.transformWith)
queryOptions = query.transformWith(queryOptions);
if (queryOpts.transformWith)
queryOptions = queryOpts.transformWith(queryOptions);

// this._fetch is lib/yahooFinanceFetch
let result = await this._fetch(query.url, queryOptions, moduleOpts, query.fetchType);
let result = await this._fetch(queryOpts.url, queryOptions, moduleOpts, queryOpts.fetchType);

if (query.fetchType === 'csv')
if (queryOpts.fetchType === 'csv')
result = csv2json(result);

/*
Expand All @@ -133,7 +142,7 @@ export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOp
|| moduleOpts.validateResult === true;

const validationOpts = {
...this._opts?.validation,
...(this._opts ? this._opts.validation : _opts.validation),
// Set logErrors=false if validateResult=false
logErrors: validateResult ? this._opts?.validation?.logErrors : false,
};
Expand All @@ -156,7 +165,13 @@ export default async function moduleExec(this: ThisWithFetch, opts: ModuleExecOp
* database, etc. Otherwise you'll receive an error.
*/
try {
validateAndCoerceTypes(result, opts.result.schemaKey, undefined, validationOpts);
validateAndCoerceTypes({
source: moduleName,
type: 'result',
object: result,
schemaKey: resultOpts.schemaKey,
options: validationOpts
});
} catch (error) {
if (validateResult)
throw error;
Expand Down
3 changes: 2 additions & 1 deletion src/lib/options.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
export default {
validation: {
logErrors: true
logErrors: true,
logOptionsErrors: true,
}
}
93 changes: 73 additions & 20 deletions src/lib/validateAndCoerceTypes.spec.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
import validateAndCoerceTypes, { ajv } from './validateAndCoerceTypes';
import { InvalidOptionsError, FailedYahooValidationError } from './errors';

const QUERY_RESULT_SCHEMA_KEY = "#/definitions/QuoteSummaryResult";

const defaultOptions = {
logErrors: false
const defParams = {
source: "validateAndCoerceTypes.spec.js",
schemaKey: "#/definitions/QuoteSummaryResult",
options: {
logErrors: true,
logOptionsErrors: true,
}
};

const priceResult = {
Expand Down Expand Up @@ -58,15 +61,15 @@ describe('validateAndCoerceTypes', () => {
it('passes regular numbers', () => {
const result = Object.assign({}, priceResult);
result.price = Object.assign({}, result.price);
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({ ...defParams, type: 'result', object: result });
expect(result.price.priceHint).toBe(2);
});

it('corerces rawNumberObjs', () => {
const result = Object.assign({}, priceResult);
result.price = Object.assign({}, result.price);
result.price.postMarketChangePercent = { raw: 0.006599537, fmt: "6.5%" }
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({ ...defParams, type: 'result', object: result });
expect(result.price.postMarketChangePercent).toBe(0.006599537);
});

Expand All @@ -76,7 +79,10 @@ describe('validateAndCoerceTypes', () => {
/* @ts-ignore */
result.price.postMarketChangePercent = true;
expect(
() => validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions)
() => validateAndCoerceTypes({
...defParams, type: 'result', object: result,
options: { ...defParams.options, logErrors: false }
})
).toThrow(/Failed Yahoo Schema/);
});

Expand All @@ -86,7 +92,23 @@ describe('validateAndCoerceTypes', () => {
/* @ts-ignore */
result.price.postMarketChangePercent = { raw: "a string" };
expect(
() => validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions)
() => validateAndCoerceTypes({
...defParams, type: 'result', object: result,
options: { ...defParams.options, logErrors: false }
})
).toThrow(/Failed Yahoo Schema/);
});

it('fails if string returns a NaN', () => {
const result = Object.assign({}, priceResult);
result.price = Object.assign({}, result.price);
/* @ts-ignore */
result.price.postMarketChangePercent = "not-a-number";
expect(
() => validateAndCoerceTypes({
...defParams, type: 'result', object: result,
options: { ...defParams.options, logErrors: false }
})
).toThrow(/Failed Yahoo Schema/);
});

Expand All @@ -100,7 +122,7 @@ describe('validateAndCoerceTypes', () => {
// @ts-ignore
result.price.regularMarketTime = { raw: 1612313997 };

validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({ ...defParams, type: 'result', object: result });
// @ts-ignore
expect(result.price.regularMarketTime.getTime())
.toBe(1612313997 * 1000);
Expand All @@ -109,8 +131,7 @@ describe('validateAndCoerceTypes', () => {
it('coerces epochs', () => {
const result = Object.assign({}, priceResult);
result.price = Object.assign({}, result.price);
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
// @ts-ignore
validateAndCoerceTypes({ ...defParams, type: 'result', object: result });
// @ts-ignore
expect(result.price.regularMarketTime.getTime())
.toBe(new Date(priceResult.price.regularMarketTime).getTime());
Expand All @@ -119,7 +140,7 @@ describe('validateAndCoerceTypes', () => {
it('coerces recognizable date string', () => {
const result = Object.assign({}, priceResult);
result.price = Object.assign({}, result.price);
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({ ...defParams, type: 'result', object: result });
// @ts-ignore
expect(result.price.regularMarketTime.getTime())
.toBe(new Date(priceResult.price.regularMarketTime).getTime());
Expand All @@ -131,7 +152,10 @@ describe('validateAndCoerceTypes', () => {
/* @ts-ignore */
result.price.postMarketTime = "clearly not a date";
expect(
() => validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions)
() => validateAndCoerceTypes({
...defParams, type: 'result', object: result,
options: { ...defParams.options, logErrors: false }
})
).toThrow(/Failed Yahoo Schema/);
});

Expand All @@ -141,7 +165,7 @@ describe('validateAndCoerceTypes', () => {
const date = new Date();
// @ts-ignore
result.price.postMarketTime = date;
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({ ...defParams, type: 'result', object: result });
expect(result.price.postMarketTime).toBe(date);
});

Expand All @@ -152,7 +176,14 @@ describe('validateAndCoerceTypes', () => {
it('fails on invalid options usage', () => {
const options = { period1: true };
expect(
() => validateAndCoerceTypes(options, "#/definitions/HistoricalOptions", "historical")
() => validateAndCoerceTypes({
...defParams,
object: options,
type: 'options',
schemaKey: "#/definitions/HistoricalOptions",
source: "historical-in-validate.spec",
options: { ...defParams.options, logOptionsErrors: false }
})
).toThrow(InvalidOptionsError)
});

Expand All @@ -164,7 +195,10 @@ describe('validateAndCoerceTypes', () => {

let error: FailedYahooValidationError;
try {
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({
...defParams, object: result, type: 'result',
options: { ...defParams.options, logErrors: false }
});
} catch (e) {
error = e;
}
Expand Down Expand Up @@ -192,7 +226,13 @@ describe('validateAndCoerceTypes', () => {

it('fails on invalid schema key', () => {
expect(
() => validateAndCoerceTypes({}, "SOME_MISSING_KEY")
() => validateAndCoerceTypes({
...defParams,
object: {},
type: 'result',
schemaKey: "SOME_MISSING_KEY",
options: { ...defParams.options, logErrors: false }
})
).toThrow(/No such schema/)
});

Expand All @@ -212,7 +252,12 @@ describe('validateAndCoerceTypes', () => {
/* @ts-ignore */
console = fakeConsole;
expect(
() => validateAndCoerceTypes({ a: 1 }, QUERY_RESULT_SCHEMA_KEY, undefined, { logErrors: true })
() => validateAndCoerceTypes({
...defParams,
object: { a: 1 },
type: 'result',
options: { ...defParams.options, logErrors: true }
})
).toThrow("Failed Yahoo Schema validation");
console = origConsole;

Expand All @@ -226,7 +271,12 @@ describe('validateAndCoerceTypes', () => {
/* @ts-ignore */
console = fakeConsole;
expect(
() => validateAndCoerceTypes({ a: 1 }, QUERY_RESULT_SCHEMA_KEY, undefined, { logErrors: false })
() => validateAndCoerceTypes({
...defParams,
object: { a: 1 },
type: 'result',
options: { ...defParams.options, logErrors: false }
})
).toThrow("Failed Yahoo Schema validation");
console = origConsole;

Expand All @@ -240,7 +290,10 @@ describe('validateAndCoerceTypes', () => {

let error;
try {
validateAndCoerceTypes(result, QUERY_RESULT_SCHEMA_KEY, undefined, defaultOptions);
validateAndCoerceTypes({
...defParams, object: result, type: "result",
options: { ...defParams.options, logErrors: false }
});
} catch (e) {
error = e;
}
Expand Down
Loading

0 comments on commit d8dc750

Please sign in to comment.