Skip to content

Commit

Permalink
chore: rewrite package on typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
oldskytree committed Sep 12, 2022
1 parent 0d500f4 commit 182037c
Show file tree
Hide file tree
Showing 26 changed files with 454 additions and 323 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
Expand Up @@ -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
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,
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';

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>;

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>;
}
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

0 comments on commit 182037c

Please sign in to comment.