Skip to content

Commit

Permalink
feat(modules): better runtime option checking
Browse files Browse the repository at this point in the history
  • Loading branch information
gadicc committed Jan 30, 2021
1 parent 084990d commit dffa79a
Show file tree
Hide file tree
Showing 13 changed files with 217 additions and 37 deletions.
9 changes: 8 additions & 1 deletion bin/yahoo-finance.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,14 @@ function decodeArgs(stringArgs) {
(async function() {
const args = decodeArgs(argsAsStrings);

const result = await yahooFinance[moduleName](...args);
let result;
try {
result = await yahooFinance[moduleName](...args);
} catch (error) {
// No need for full stack trace for CLI scripts
console.error("Exiting with " + error.name + ": " + error.message);
process.exit(1);
}

if (process.stdout.isTTY)
console.dir(result, { depth: null, colors: true });
Expand Down
102 changes: 102 additions & 0 deletions schema.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,18 @@
{
"$schema": "http://json-schema.org/draft-07/schema#",
"definitions": {
"AutocOptions": {
"additionalProperties": false,
"properties": {
"lang": {
"type": "string"
},
"region": {
"type": "number"
}
},
"type": "object"
},
"AutocResult": {
"additionalProperties": false,
"properties": {
Expand Down Expand Up @@ -52,6 +64,57 @@
],
"type": "object"
},
"HistoricalOptions": {
"additionalProperties": false,
"properties": {
"events": {
"type": "string"
},
"includeAdjustedClose": {
"type": "boolean"
},
"interval": {
"enum": [
"1d",
"1wk",
"1mo"
],
"type": "string"
},
"period1": {
"anyOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "string"
},
{
"type": "number"
}
]
},
"period2": {
"anyOf": [
{
"format": "date-time",
"type": "string"
},
{
"type": "string"
},
{
"type": "number"
}
]
}
},
"required": [
"period1"
],
"type": "object"
},
"HistoricalResult": {
"items": {
"$ref": "#/definitions/HistoricalRow"
Expand Down Expand Up @@ -537,6 +600,45 @@
],
"type": "object"
},
"SearchOptions": {
"additionalProperties": false,
"properties": {
"enableCb": {
"type": "boolean"
},
"enableEnhancedTrivialQuery": {
"type": "boolean"
},
"enableFuzzyQuery": {
"type": "boolean"
},
"enableNavLinks": {
"type": "boolean"
},
"lang": {
"type": "string"
},
"multiQuoteQueryId": {
"type": "string"
},
"newsCount": {
"type": "number"
},
"newsQueryId": {
"type": "string"
},
"quotesCount": {
"type": "number"
},
"quotesQueryId": {
"type": "string"
},
"region": {
"type": "string"
}
},
"type": "object"
},
"SearchQuoteNonYahoo": {
"additionalProperties": false,
"properties": {
Expand Down
13 changes: 10 additions & 3 deletions src/lib/errors.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,11 @@
class BadRequestError extends Error {}
class HTTPError extends Error {}
class BadRequestError extends Error { name = "BadRequestError" }
class HTTPError extends Error { name = "HTTPError" }
class FailedYahooValidationError extends Error { name = "FailedYahooValidationError" }
class InvalidOptionsError extends Error { name = "InvalidOptionsError" }

module.exports = { BadRequestError, HTTPError };
module.exports = {
BadRequestError,
HTTPError,
FailedYahooValidationError,
InvalidOptionsError,
};
47 changes: 31 additions & 16 deletions src/lib/validate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,35 +4,50 @@ import addFormats from 'ajv-formats';
//import schema from '../../schema.json';
const schema = require('../../schema.json');
const pkg = require('../../package.json');
import { InvalidOptionsError, FailedYahooValidationError } from './errors';

const ajv = new Ajv();
addFormats(ajv);

ajv.addSchema(schema);

function validate(object: object, key: string): void {
const logObj = process.stdout.isTTY
? (obj:any) => console.dir(obj, { depth: 4, colors: true })
: (obj:any) => console.log(JSON.stringify(obj, null, 2));

function validate(object: object, key: string, module?: string): void {
const validator = ajv.getSchema(key);
if (!validator)
throw new Error("No such schema with key: " + key);

const valid = validator(object);
if (valid) return;

const title = encodeURIComponent("Failed validation: " + key);
console.error("The following result did not validate with schema: " + key);
console.error(object);
console.error(validator.errors);
console.error("You should handle occassional errors in your code, however if ");
console.error("this happens every time, probably Yahoo have changed their API ");
console.error("and node-yahoo-finance2 needs to be updated. Please see if ");
console.error("anyone has reported this previously:");
console.error();
console.error(` ${pkg.repository}/issues?q=is%3Aissue+${title}`);
console.error();
console.error("or open a new issue:");
console.error();
console.error(` ${pkg.repository}/issues/new?title=${title}`);
throw new Error("Failed Yahoo Schema validation");
if (!module) {

const title = encodeURIComponent("Failed validation: " + key);
console.error("The following result did not validate with schema: " + key);
logObj(object);
logObj(validator.errors);
console.error("You should handle occassional errors in your code, however if ");
console.error("this happens every time, probably Yahoo have changed their API ");
console.error("and node-yahoo-finance2 needs to be updated. Please see if ");
console.error("anyone has reported this previously:");
console.error();
console.error(` ${pkg.repository}/issues?q=is%3Aissue+${title}`);
console.error();
console.error("or open a new issue:");
console.error();
console.error(` ${pkg.repository}/issues/new?title=${title}`);
throw new FailedYahooValidationError("Failed Yahoo Schema validation");

} else /* if (type === 'options') */ {

console.error(`[yahooFinance.${module}] Invalid options ("${key}")`);
logObj({ input: object, errors: validator.errors });
throw new InvalidOptionsError(`yahooFinance.${module} called with invalid options.`);

}
}

export default validate;
11 changes: 11 additions & 0 deletions src/modules/autoc.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import autoc from './autoc';
const { InvalidOptionsError } = require('../lib/errors');

describe('autoc', () => {

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

});
9 changes: 6 additions & 3 deletions src/modules/autoc.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ import yahooFinanceFetch from '../lib/yahooFinanceFetch';
import validate from '../lib/validate';

const QUERY_URL = 'https://autoc.finance.yahoo.com/autoc';
const QUERY_SCHEMA_KEY = "#/definitions/YahooFinanceAutocResultSet";
const QUERY_OPTIONS_SCHEMA_KEY = "#/definitions/AutocOptions"
const QUERY_RESULT_SCHEMA_KEY = "#/definitions/AutocResultSet";

export interface AutocResultSet {
Query: string;
Expand All @@ -18,7 +19,7 @@ export interface AutocResult {
typeDisp: string; // "Equity"
}

interface AutocOptions {
export interface AutocOptions {
region?: number; // 1
lang?: string; // "en"
}
Expand All @@ -33,6 +34,8 @@ async function autoc(
queryOptionsOverrides: AutocOptions = {},
fetchOptions?: object
): Promise<AutocResultSet> {
validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'autoc');

const queryOptions = {
query,
...queryOptionsDefaults,
Expand All @@ -42,7 +45,7 @@ async function autoc(
const result = await yahooFinanceFetch(QUERY_URL, queryOptions, fetchOptions);

if (result.ResultSet) {
validate(result.ResultSet, QUERY_SCHEMA_KEY);
validate(result.ResultSet, QUERY_RESULT_SCHEMA_KEY);
return result.ResultSet;
}

Expand Down
11 changes: 11 additions & 0 deletions src/modules/historical.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import historical from './historical';
const { InvalidOptionsError } = require('../lib/errors');

describe('historical', () => {

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

});
7 changes: 5 additions & 2 deletions src/modules/historical.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,8 @@ import validate from '../lib/validate';
import csv2json from '../lib/csv2json';

const QUERY_URL = 'https://query1.finance.yahoo.com/v7/finance/download';
const QUERY_SCHEMA_KEY = "#/definitions/HistoricalResult";
const QUERY_RESULT_SCHEMA_KEY = "#/definitions/HistoricalResult";
const QUERY_OPTIONS_SCHEMA_KEY = '#/definitions/HistoricalOptions';

export type HistoricalResult = Array<HistoricalRow>;

Expand All @@ -17,7 +18,7 @@ export interface HistoricalRow {
volume: number;
}

interface HistoricalOptions {
export interface HistoricalOptions {
period1: Date | string | number;
period2?: Date | string | number;
interval?: '1d' | '1wk' | '1mo'; // '1d', TODO all | types
Expand All @@ -36,6 +37,8 @@ export default async function historical(
queryOptionsOverrides: HistoricalOptions,
fetchOptions?: object
): Promise<HistoricalResult> {
validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'historical');

const queryOptions: HistoricalOptions = {
...queryOptionsDefaults,
...queryOptionsOverrides
Expand Down
11 changes: 11 additions & 0 deletions src/modules/quoteSummary.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import quoteSummary from './quoteSummary';
const { InvalidOptionsError } = require('../lib/errors');

describe('quoteSummary', () => {

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

});
7 changes: 5 additions & 2 deletions src/modules/quoteSummary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ import yahooFinanceFetch = require('../lib/yahooFinanceFetch');
import validate from '../lib/validate';

const QUERY_URL = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary';
const QUERY_SCHEMA_KEY = "#/definitions/QuoteSummaryResultOrig";
const QUERY_OPTIONS_SCHEMA_KEY = '#/definitions/QuoteSummaryOptions'
const QUERY_RESULT_SCHEMA_KEY = "#/definitions/QuoteSummaryResultOrig";

/*
const QUOTESUMMARY_MODULES = [
Expand Down Expand Up @@ -149,6 +150,8 @@ export default async function quoteSummary(
queryOptionsOverrides: QuoteSummaryOptions = {},
fetchOptions?: object
): Promise<QuoteSummaryResult> {
validate(queryOptionsOverrides, QUERY_OPTIONS_SCHEMA_KEY, 'quoteSummary');

const queryOptions = {
...queryOptionsDefaults,
...queryOptionsOverrides
Expand Down Expand Up @@ -181,7 +184,7 @@ export default async function quoteSummary(

const actualResult = qsResult.result[0];

validate(actualResult, QUERY_SCHEMA_KEY);
validate(actualResult, QUERY_RESULT_SCHEMA_KEY);

for (let key of DATEFIELDS) {
const value:number|undefined = dotProp.get(actualResult, key);
Expand Down
7 changes: 0 additions & 7 deletions src/modules/search.spec.js

This file was deleted.

11 changes: 11 additions & 0 deletions src/modules/search.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import search from './search';
const { InvalidOptionsError } = require('../lib/errors');

describe('search', () => {

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

});
Loading

0 comments on commit dffa79a

Please sign in to comment.