Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rewrite package on typescript #23

Open
wants to merge 4 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Expand Up @@ -3,4 +3,5 @@ node_js:
- '6'
- '8'
script:
- npm build
- npm test
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А здесь у тебя за счет pretest получится двойной запуск build скрипта.

1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ Config is described with a combination of a functions:
var parser = root(section({
system: section({
parallelLimit: option({
defaultValue: 0,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 – не самый удачный пример дефолта. Кстати, в гермионе у этой опции дефолт Infinity.

parseEnv: Number,
parseCli: Number,
validate: function() {...}
Expand Down
59 changes: 38 additions & 21 deletions lib/core.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,36 @@
const _ = require('lodash');
const {buildLazyObject, forceParsing} = require('./lazy');
const {MissingOptionError, UnknownKeysError} = require('./errors');
const initLocator = require('./locator');
import _ from 'lodash';
OldSkyTree marked this conversation as resolved.
Show resolved Hide resolved

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

import type {Rooted, Parser} from './types/common';
import type {LazyObject} from './types/lazy';
import type {Locator} from './types/locator';
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';
import type {Map} from './types/utils';

/**
* Single option
*/
function option({
export function option<Value, Result, MappedValue = Value>({
defaultValue,
parseCli = _.identity,
parseEnv = _.identity,
validate = _.noop,
map: mapFunc = _.identity,
isDeprecated = false
}) {
}: OptionParserConfig<Value, MappedValue, Result> = {}): OptionParser<MappedValue, Result> {
const validateFunc: typeof validate = validate;

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

let value, isSetByUser = true;
let value: unknown, isSetByUser = true;
if (locator.cliOption !== undefined) {
value = parseCli(locator.cliOption);
} else if (locator.envVar !== undefined) {
Expand All @@ -38,7 +50,7 @@ function option({
console.warn(`Using "${locator.name}" option is deprecated`);
}

validate(value, config, currNode, {isSetByUser});
validateFunc(value, config, currNode, {isSetByUser});

return mapFunc(value, config, currNode, {isSetByUser});
};
Expand All @@ -48,13 +60,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<Config, Result>(properties: SectionProperties<Config, Result>): SectionParser<Config, Result> {
const expectedKeys = _.keys(properties) as Array<keyof Config>;

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}`)
Expand All @@ -63,6 +77,7 @@ function section(properties) {

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

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

Expand All @@ -76,32 +91,34 @@ function section(properties) {
* Object with user-specified keys and values,
* parsed by valueParser.
*/
function map(valueParser, defaultValue) {
export function map<SubConfig, Result>(
valueParser: Parser<SubConfig, Result>,
defaultValue: Map<SubConfig>
): MapParser<Map<SubConfig>, Result> {
return (locator, config) => {
if (locator.option === undefined) {
if (!defaultValue) {
return {};
return {} as LazyObject<Map<SubConfig>>;
}
locator = locator.resetOption(defaultValue);
}

const optionsToParse = Object.keys(locator.option);
const lazyResult = buildLazyObject(optionsToParse, (key) => {
const optionsToParse = Object.keys(locator.option as Map<SubConfig>);
const lazyResult = buildLazyObject<Map<SubConfig>>(optionsToParse, (key) => {
return () => valueParser(locator.nested(key), config);
});

_.set(config, locator.name, lazyResult);

return lazyResult;
};
}

function root(rootParser, {envPrefix, cliPrefix}) {
export function root<Config, Result = Config>(rootParser: RootParser<Config, Result>, {envPrefix, cliPrefix}: RootPrefixes = {}): ConfigParser<Config> {
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 Locator<Config>, {} as Rooted<Result>);

return forceParsing(parsed);
};
}

module.exports = {option, section, map, root};
14 changes: 8 additions & 6 deletions 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 {
optionName: string;

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

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

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

А это зачем?


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

module.exports = {MissingOptionError, UnknownKeysError};
9 changes: 2 additions & 7 deletions lib/index.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,2 @@
const {root, section, map, option} = require('./core');
const {MissingOptionError, UnknownKeysError} = require('./errors');

module.exports = {
root, section, map, option,
MissingOptionError, UnknownKeysError
};
export {root, section, map, option} from './core';
export {MissingOptionError, UnknownKeysError} from './errors';
38 changes: 25 additions & 13 deletions lib/lazy.ts
Original file line number Diff line number Diff line change
@@ -1,38 +1,50 @@
const _ = require('lodash');
import _ from 'lodash';

const isLazy = Symbol('isLazy');
import type {LazyObject} from './types/lazy';

function buildLazyObject(keys, getKeyGetter) {
export const isLazy = Symbol('isLazy');

type SimpleOrLazyObject<T> = T | LazyObject<T>;

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

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

return target;
}

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

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

Object.defineProperty(object, key, {
get() {
get(): T[keyof T] {
if (!defined) {
defined = true;
value = getter();
if (_.isObject(value) && value[isLazy]) {
value = forceParsing(value);
}
const val = getter();

value = isLazyObject(val) ? forceParsing(val) : val;
}

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

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

function hasOwnProperty<T extends object, K extends PropertyKey>(obj: T, prop: K): obj is T & Record<K, unknown> {
return Object.prototype.hasOwnProperty.call(obj, prop);
}
30 changes: 17 additions & 13 deletions lib/locator.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
const _ = require('lodash');
import _ from 'lodash';

function parseArgv(argv, cliPrefix) {
import type {LocatorArg, Locator, Node, Prefixes} from './types/locator';

function parseArgv(argv: Array<string>, cliPrefix: string): Array<string> {
return argv.reduce(function(argv, arg) {
if (!arg.startsWith(cliPrefix) || !_.includes(arg, '=')) {
return argv.concat(arg);
Expand All @@ -11,20 +13,22 @@ function parseArgv(argv, cliPrefix) {
const value = parts.slice(1).join('=');

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

module.exports = function({options, env, argv, envPrefix = '', cliPrefix = '--'}) {
export = function initLocator<Options>({options, env, argv, envPrefix = '', cliPrefix = '--'}: LocatorArg<Options>): Locator<Options> {
const parsedArgv = parseArgv(argv, cliPrefix);

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

const envName = envPrefix + _.snakeCase(stringSubKey);
const cliFlag = cliPrefix + _.kebabCase(stringSubKey);

const argIndex = parsedArgv.lastIndexOf(cliFlag);
const subOption = _.get(option, subKey);
const newName = namePrefix ? `${namePrefix}.${subKey}` : subKey;
const subOption: Options[Key] = _.get(option, subKey);
const newName = namePrefix ? `${namePrefix}.${stringSubKey}` : stringSubKey;

return mkLocator(
{
Expand All @@ -43,11 +47,11 @@ module.exports = function({options, env, argv, envPrefix = '', cliPrefix = '--'}
};
}

function mkLocator(base, prefixes) {
function mkLocator<Options>(base: Node<Options>, prefixes: Prefixes): Locator<Options> {
return _.extend(base, {
nested: getNested(base.option, prefixes),
resetOption: function(newOptions) {
return _.extend({}, base, {
resetOption: function(newOptions: Options): Locator<Options> {
return _.extend({}, base as Locator<Options>, {
option: newOptions,
nested: getNested(newOptions, prefixes)
});
Expand Down
12 changes: 12 additions & 0 deletions lib/types/common.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import type {LazyObject} from './lazy';
import type {MapParser} from './map';
import type {OptionParser} from './option';
import type {SectionParser} from './section';

export type ParsedConfig<Config> = {[Key in keyof Config]: LazyObject<Config[Key]>};

export type Parser<Config, Result> = OptionParser<Config, Result> | SectionParser<Config, Result> | MapParser<Config, Result>;

export interface Rooted<T> {
root: T;
}
5 changes: 5 additions & 0 deletions lib/types/lazy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type {isLazy} from '../lazy';

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

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

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

export interface Node<Options> {
name: string;
parent: string | null;
option: Options;
envVar?: string;
cliOption?: string;
}

export interface Locator<Options> extends Node<Options> {
nested<Key extends keyof Options>(key: Key): Locator<Options[Key]>;
resetOption(newOption: Options): Locator<Options>;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Как-то не вяжется, что аргумент называется newOption, а тип Options – во множественном числе (как будто это массив).

}
7 changes: 7 additions & 0 deletions lib/types/map.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import type {Rooted} from './common';
import type {LazyObject} from './lazy';
import type {Locator} from './locator';

export interface MapParser<Config, Result> {
(locator: Locator<Config>, config: Rooted<Result>): LazyObject<Config>;
}
19 changes: 19 additions & 0 deletions lib/types/option.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import type {Rooted} from './common';
import type {Locator} from './locator';

interface MetaInfo {
isSetByUser: boolean;
}

export interface OptionParserConfig<Value, MappedValue, Result> {
defaultValue?: Value | ((config: Result, currNode: any) => Value);
parseCli?: (input?: string) => Value | undefined;
parseEnv?: (input?: string) => Value | undefined;
validate?: (value: unknown, config: Result, currNode: any, meta: MetaInfo) => asserts value is Value;
map?(value: Value, config: Result, currNode: any, meta: MetaInfo): MappedValue;
isDeprecated?: boolean;
}

export interface OptionParser<Value, Result> {
(locator: Locator<Value>, config: Rooted<Result>): Value;
}
20 changes: 20 additions & 0 deletions lib/types/root.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import type {MapParser} from './map';
import type {SectionParser} from './section';
import type {DeepPartial} from './utils';

export interface ConfigParserArg<Options> {
options: Options;
env: NodeJS.ProcessEnv;
argv: NodeJS.Process['argv'];
}

export interface ConfigParser<Config> {
(arg: ConfigParserArg<DeepPartial<Config>>): Config;
}

export interface RootPrefixes {
envPrefix?: string;
cliPrefix?: string;
}

export type RootParser<Config, Result> = SectionParser<Config, Result> | MapParser<Config, Result>;
Loading