Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: rewrite to typescript
Browse files Browse the repository at this point in the history
oldskytree committed Nov 16, 2021
1 parent 37508fd commit d2792ec
Showing 28 changed files with 466 additions and 348 deletions.
1 change: 1 addition & 0 deletions .eslintignore
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
build
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
node_modules
build
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -3,4 +3,5 @@ node_js:
- '6'
- '8'
script:
- npm build
- npm test
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -8,6 +8,7 @@ Config is described with a combination of a functions:
var parser = root(section({
system: section({
parallelLimit: option({
defaultValue: 0,
parseEnv: Number,
parseCli: Number,
validate: function() {...}
61 changes: 39 additions & 22 deletions lib/core.js → lib/core.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,35 @@
const _ = require('lodash');
const {buildLazyObject, forceParsing} = require('./lazy');
const {MissingOptionError, UnknownKeysError} = require('./errors');
const initLocator = require('./locator');
import _ from 'lodash';

import { MissingOptionError, UnknownKeysError } from './errors';
import { buildLazyObject, forceParsing } from './lazy';
import initLocator from './locator';

import type { LazyObject } from '../types/lazy';
import type { RootParsedConfig } from '../types/common';
import type { MapParser } from '../types/map';
import type { OptionParser, OptionParserConfig } from '../types/option';
import type { RootParser, RootPrefixes, ConfigParser } from '../types/root';
import type { SectionParser, SectionProperties } from '../types/section';

type Parser<T, R = any> = OptionParser<T, R> | SectionParser<T, R> | MapParser<T, R>;

/**
* Single option
*/
function option({
export function option<T, S = T, R = any>({
defaultValue,
parseCli = _.identity,
parseEnv = _.identity,
validate = _.noop,
map: mapFunc = _.identity
}) {
}: OptionParserConfig<T, S, R>): OptionParser<S, R> {
const validateFunc: typeof validate = validate;

return (locator, parsed) => {
const config = parsed.root;
const currNode = locator.parent ? _.get(config, locator.parent) : config;
const currNode = locator.parent ? _.get(parsed, locator.parent) : config;

let value;
let value: unknown;
if (locator.cliOption !== undefined) {
value = parseCli(locator.cliOption);
} else if (locator.envVar !== undefined) {
@@ -31,7 +43,8 @@ function option({
} else {
throw new MissingOptionError(locator.name);
}
validate(value, config, currNode);

validateFunc(value, config, currNode);

return mapFunc(value, config, currNode);
};
@@ -41,13 +54,15 @@ function option({
* Object with fixed properties.
* Any unknown property will be reported as error.
*/
function section(properties) {
const expectedKeys = _.keys(properties);
export function section<T, R = any>(properties: SectionProperties<T, R>): SectionParser<T, R> {
const expectedKeys = _.keys(properties) as Array<keyof T>;

return (locator, config) => {
const unknownKeys = _.difference(
_.keys(locator.option),
expectedKeys
expectedKeys as Array<string>
);

if (unknownKeys.length > 0) {
throw new UnknownKeysError(
unknownKeys.map((key) => `${locator.name}.${key}`)
@@ -56,6 +71,7 @@ function section(properties) {

const lazyResult = buildLazyObject(expectedKeys, (key) => {
const parser = properties[key];

return () => parser(locator.nested(key), config);
});

@@ -69,17 +85,20 @@ function section(properties) {
* Object with user-specified keys and values,
* parsed by valueParser.
*/
function map(valueParser, defaultValue) {
export function map<T extends Record<string, any>, V extends T[string] = T[string], R = any>(
valueParser: Parser<V, R>,
defaultValue: Record<string, V>
): MapParser<Record<string, V>, R> {
return (locator, config) => {
if (locator.option === undefined) {
if (!defaultValue) {
return {};
return {} as LazyObject<T>;
}
locator = locator.resetOption(defaultValue);
}

const optionsToParse = Object.keys(locator.option);
const lazyResult = buildLazyObject(optionsToParse, (key) => {
const optionsToParse = Object.keys(locator.option as Record<string, V>);
const lazyResult = buildLazyObject<Record<string, V>>(optionsToParse, (key) => {
return () => valueParser(locator.nested(key), config);
});
_.set(config, locator.name, lazyResult);
@@ -88,13 +107,11 @@ function map(valueParser, defaultValue) {
};
}

function root(rootParser, {envPrefix, cliPrefix}) {
export function root<T>(rootParser: RootParser<T>, {envPrefix, cliPrefix}: RootPrefixes): ConfigParser<T> {
return ({options, env, argv}) => {
const rootLocator = initLocator({options, env, argv, envPrefix, cliPrefix});
const parsed = {};
rootParser(rootLocator, parsed);
return forceParsing(parsed.root);
const parsed = rootParser(rootLocator, {} as RootParsedConfig<T>);

return forceParsing(parsed);
};
}

module.exports = {option, section, map, root};
14 changes: 8 additions & 6 deletions lib/errors.js → lib/errors.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
class MissingOptionError extends Error {
constructor(optionName) {
export class MissingOptionError extends Error {
public optionName: string;

constructor(optionName: string) {
const message = `${optionName} is required`;
super(message);
this.name = 'MissingOptionError';
@@ -10,8 +12,10 @@ class MissingOptionError extends Error {
}
}

class UnknownKeysError extends Error {
constructor(keys) {
export class UnknownKeysError extends Error {
public keys: Array<string>;

constructor(keys: Array<string>) {
const message = `Unknown options: ${keys.join(', ')}`;
super(message);
this.name = 'UnknownKeysError';
@@ -21,5 +25,3 @@ class UnknownKeysError extends Error {
Error.captureStackTrace(this, UnknownKeysError);
}
}

module.exports = {MissingOptionError, UnknownKeysError};
7 changes: 0 additions & 7 deletions lib/index.js

This file was deleted.

2 changes: 2 additions & 0 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
export { root, section, map, option } from './core';
export { MissingOptionError, UnknownKeysError } from './errors';
38 changes: 0 additions & 38 deletions lib/lazy.js

This file was deleted.

52 changes: 52 additions & 0 deletions lib/lazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
import _ from 'lodash';

import type { LazyObject } from '../types/lazy';

export const isLazy = Symbol('isLazy');

export function buildLazyObject<T>(keys: Array<keyof T>, getKeyGetter: (key: keyof T) => () => (T[keyof T] | LazyObject<T[keyof T]>)): LazyObject<T> {
const target = {
[isLazy]: true
} as LazyObject<T>;

for (const key of keys) {
defineLazy(target, key, getKeyGetter(key));
}

return target;
}

export function forceParsing<T>(lazyObject: LazyObject<T>): T {
return _.cloneDeep(lazyObject);
}

function defineLazy<T>(object: LazyObject<T>, key: keyof T, getter: () => T[keyof T] | LazyObject<T[keyof T]>): void {
let defined = false;
let value: T[keyof T];

Object.defineProperty(object, key, {
get(): T[keyof T] {
if (!defined) {
defined = true;
const val = getter();

if (isLazyObject(val)) {
value = forceParsing(val);
} else {
value = val;
}
}

return value;
},
enumerable: true
});
}

function isLazyObject<T>(value: T): value is LazyObject<T> {
return _.isObject(value) && hasOwnProperty(value, isLazy) && value[isLazy] === true;
}

function hasOwnProperty<T extends {}>(obj: T, prop: PropertyKey): obj is T & Record<typeof prop, unknown> {
return obj.hasOwnProperty(prop);
}
37 changes: 21 additions & 16 deletions lib/locator.js → lib/locator.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,33 @@
const _ = require('lodash');
import _ from 'lodash';

module.exports = function({options, env, argv, envPrefix = '', cliPrefix = '--'}) {
argv = argv.reduce(function(argv, arg) {
import type { DeepPartial } from '../types/utils';
import type { LocatorArg, Locator, Node, Prefixes } from '../types/locator';

function parseArgv(argv: Array<string>): Array<string> {
return argv.reduce(function(argv, arg) {
if (!_.includes(arg, '=')) {
return argv.concat(arg);
}

const parts = arg.split('=');
const option = parts[0];
const value = parts.slice(1).join('=');

return argv.concat(option, value);
}, []);
}, [] as Array<string>);
}

function getNested(option, {namePrefix, envPrefix, cliPrefix}) {
export default function<T>({options, env, argv, envPrefix = '', cliPrefix = '--'}: LocatorArg<T>): Locator<T> {
argv = parseArgv(argv);

function getNested<T extends {}>(option: DeepPartial<T> | undefined, {namePrefix, envPrefix, cliPrefix}: Prefixes): (key: keyof T) => Locator<T[keyof T]> {
return (subKey) => {
const envName = envPrefix + _.snakeCase(subKey);
const cliFlag = cliPrefix + _.kebabCase(subKey);
const envName = envPrefix + _.snakeCase(subKey.toString());
const cliFlag = cliPrefix + _.kebabCase(subKey.toString());

const argIndex = argv.lastIndexOf(cliFlag);
const subOption = _.get(option, subKey);
const newName = namePrefix ? `${namePrefix}.${subKey}` : subKey;
const newName = namePrefix ? `${namePrefix}.${subKey}` : subKey.toString();

return mkLocator(
{
@@ -37,14 +46,11 @@ module.exports = function({options, env, argv, envPrefix = '', cliPrefix = '--'}
};
}

function mkLocator(base, prefixes) {
function mkLocator<T>(base: Node<T>, prefixes: Prefixes): Locator<T> {
return _.extend(base, {
nested: getNested(base.option, prefixes),
resetOption: function(newOptions) {
return _.extend({}, base, {
option: newOptions,
nested: getNested(newOptions, prefixes)
});
resetOption: function<T>(newOptions: T): Locator<T> {
return mkLocator({ ...base, option: newOptions }, prefixes);
}
});
}
@@ -58,10 +64,9 @@ module.exports = function({options, env, argv, envPrefix = '', cliPrefix = '--'}
cliOption: undefined
},
{
namePrefix: '',
namePrefix: 'root',
envPrefix,
cliPrefix
}
);
};

426 changes: 225 additions & 201 deletions package-lock.json

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -2,21 +2,25 @@
"name": "gemini-configparser",
"version": "1.1.0",
"description": "Config parser module for gemini",
"main": "lib/index.js",
"types": "./typings/index.d.ts",
"main": "build/index.js",
"author": "Sergey Tatarintsev <sevinf@yandex-team.ru> (https://github.com/SevInf)",
"license": "MIT",
"devDependencies": {
"@tsconfig/recommended": "^1.0.1",
"@types/lodash": "^4.14.176",
"@types/node": "^16.11.6",
"chai": "^4.1.2",
"eslint": "^4.9.0",
"eslint-config-gemini-testing": "^2.0.0",
"mocha": "^4.0.1",
"sinon": "^4.0.1"
"sinon": "^4.0.1",
"typescript": "^4.4.4"
},
"dependencies": {
"lodash": "^4.17.4"
},
"scripts": {
"build": "tsc",
"test": "npm run lint && npm run test-unit",
"lint": "eslint .",
"test-unit": "mocha test"
2 changes: 1 addition & 1 deletion test/lazy.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const {buildLazyObject, forceParsing} = require('../lib/lazy');
const {buildLazyObject, forceParsing} = require('../build/lazy');

describe('build lazy object', () => {
it('should build an object with given keys', () => {
10 changes: 5 additions & 5 deletions test/locator.js
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const locator = require('../lib/locator');
const locator = require('../build/locator').default;

function locatorWithOptions(options) {
return locator({options, env: {}, argv: []});
@@ -37,20 +37,20 @@ describe('locator', () => {
it('should return nested name after nest call', () => {
const pointer = locatorWithOptions({});
const childPointer = pointer.nested('key');
assert.propertyVal(childPointer, 'name', 'key');
assert.propertyVal(childPointer, 'name', 'root.key');
});

it('should return empty parent for root children', () => {
it('should return parent for root children', () => {
const pointer = locatorWithOptions({someKey: 'someVal'});
const childPointer = pointer.nested('key');
assert.propertyVal(childPointer, 'parent', '');
assert.propertyVal(childPointer, 'parent', 'root');
});

it('should return parent name for not root children', () => {
const pointer = locatorWithOptions({someKey: 'someVal'});
const childPointer = pointer.nested('child');
const subChildPointer = childPointer.nested('subChild');
assert.propertyVal(subChildPointer, 'parent', 'child');
assert.propertyVal(subChildPointer, 'parent', 'root.child');
});

it('should return env var value after nested call', () => {
4 changes: 2 additions & 2 deletions test/map.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {map} = require('../lib/core');
const {forceParsing} = require('../lib/lazy');
const {map} = require('../build/core');
const {forceParsing} = require('../build/lazy');
const _ = require('lodash');

describe('map', () => {
8 changes: 4 additions & 4 deletions test/option.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {option} = require('../lib/core');
const {MissingOptionError} = require('../lib/errors');
const {option} = require('../build/core');
const {MissingOptionError} = require('../build/errors');

describe('option', () => {
const LAZY_CONFIG = {
@@ -130,7 +130,7 @@ describe('option', () => {
}
};

parser({parent: 'topLevel'}, config);
parser({parent: 'root.topLevel'}, config);

assert.calledWith(defaultValStub, config.root, {subLevel: 'subLevelVal'});
});
@@ -178,7 +178,7 @@ describe('option', () => {
}
};

parser({option: 'value', parent: 'topLevel'}, config);
parser({option: 'value', parent: 'root.topLevel'}, config);

assert.calledWith(callback, 'value', config.root, {subLevel: 'subLevelVal'});
});
4 changes: 2 additions & 2 deletions test/section.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
const {section} = require('../lib/core');
const {forceParsing} = require('../lib/lazy');
const {section} = require('../build/core');
const {forceParsing} = require('../build/lazy');

function stubLocator(locatorKeys) {
return Object.assign({
13 changes: 13 additions & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"extends": "@tsconfig/recommended/tsconfig.json",
"include": ["lib", "types"],
"exclude": ["build", "test"],
"compilerOptions": {
"outDir": "build",
"types": ["node"],
"allowJs": true,
"noImplicitReturns": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
10 changes: 10 additions & 0 deletions types/common.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { LazyObject } from './lazy';
import type { MapParser } from './map';
import type { OptionParser } from './option';
import type { SectionParser } from './section';

export type ParsedConfig<T> = {[K in keyof T]: LazyObject<T[K]>};

export type RootParsedConfig<T> = ParsedConfig<{root: LazyObject<T>}>;

export type Parser<T, R = any> = OptionParser<T, R> | SectionParser<T, R> | MapParser<T, R>;
5 changes: 5 additions & 0 deletions types/lazy.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { isLazy } from "../lib/lazy";

export type LazyObject<T> = T & {
[isLazy]: true;
};
21 changes: 21 additions & 0 deletions types/locator.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { DeepPartial } from './utils';
import type { RootPrefixes, ConfigParserArg } from './root';

export type LocatorArg<T> = RootPrefixes & ConfigParserArg<T>;

export type Prefixes = Required<RootPrefixes> & {
namePrefix: string;
};

export type Node<T> = {
name: string;
parent: string;
option?: DeepPartial<T>;
envVar?: string;
cliOption?: string;
};

export interface Locator<T> extends Node<T> {
nested: (key: keyof T) => Locator<T[keyof T]>;
resetOption: <T>(newOption: T) => Locator<T>;
}
5 changes: 5 additions & 0 deletions types/map.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type { RootParsedConfig } from "./common";
import type { LazyObject } from "./lazy";
import type { Locator } from "./locator";

export type MapParser<T, R = any> = (locator: Locator<T>, config: RootParsedConfig<R>) => LazyObject<T>;
13 changes: 13 additions & 0 deletions types/option.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { RootParsedConfig } from './common';
import type { LazyObject } from './lazy';
import type { Locator } from './locator';

export type OptionParserConfig<T, S, R = any, P = any> = {
defaultValue: T | ((config: LazyObject<R>, currNode: LazyObject<R> | LazyObject<P>) => T);
parseCli?: (input: string) => T;
parseEnv?: (input: string) => T;
validate?: (value: unknown, config: LazyObject<R>, currNode: LazyObject<R> | LazyObject<P>) => asserts value is T;
map?(value: T, config: LazyObject<R>, currNode: LazyObject<R> | LazyObject<P>): S;
};

export type OptionParser<T, R = any> = (locator: Locator<T>, config: RootParsedConfig<R>) => T;
18 changes: 18 additions & 0 deletions types/root.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import type { DeepPartial } from './utils';
import type { MapParser } from './map';
import type { SectionParser } from './section';

export type ConfigParserArg<T> = {
options: DeepPartial<T>;
env: NodeJS.ProcessEnv;
argv: NodeJS.Process["argv"];
};

export type ConfigParser<T> = (arg: ConfigParserArg<T>) => T;

export type RootPrefixes = {
envPrefix?: string;
cliPrefix?: string;
};

export type RootParser<T> = SectionParser<T, T> | MapParser<T, T>;
6 changes: 6 additions & 0 deletions types/section.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { Parser, RootParsedConfig } from './common';
import type { LazyObject } from './lazy';
import type { Locator } from './locator';

export type SectionProperties<T, R = any> = {[K in keyof T]: Parser<T[K], R>};
export type SectionParser<T, R = any> = (locator: Locator<T>, config: RootParsedConfig<R>) => LazyObject<T>;
3 changes: 3 additions & 0 deletions types/utils.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type DeepPartial<T> = {
[K in keyof T]?: T[K] extends {} ? DeepPartial<T[K]> : T[K];
}
41 changes: 0 additions & 41 deletions typings/index.d.ts

This file was deleted.

0 comments on commit d2792ec

Please sign in to comment.