Skip to content

Commit

Permalink
feat: rewrite to typescript
Browse files Browse the repository at this point in the history
  • Loading branch information
oldskytree committed Nov 16, 2021
1 parent 37508fd commit d2792ec
Show file tree
Hide file tree
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
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
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) {
Expand All @@ -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);
};
Expand All @@ -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}`)
Expand All @@ -56,6 +71,7 @@ function section(properties) {

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

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

Expand All @@ -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);
Expand All @@ -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';
Expand All @@ -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';
Expand All @@ -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(
{
Expand All @@ -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);
}
});
}
Expand All @@ -58,10 +64,9 @@ module.exports = function({options, env, argv, envPrefix = '', cliPrefix = '--'}
cliOption: undefined
},
{
namePrefix: '',
namePrefix: 'root',
envPrefix,
cliPrefix
}
);
};

Loading

0 comments on commit d2792ec

Please sign in to comment.