Skip to content

Commit

Permalink
feat(typescript): improve types and methods definitions
Browse files Browse the repository at this point in the history
  • Loading branch information
themgoncalves committed Feb 12, 2021
1 parent 4bc1060 commit c1e28c1
Show file tree
Hide file tree
Showing 18 changed files with 240 additions and 104 deletions.
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ const source = {
};

dot.pick(source, 'person.name');
//outputs { firstName: 'John', lastName: 'Doe' }
//output { firstName: 'John', lastName: 'Doe' }

dot.pick(source, 'person.address[0].street');
//outputs "Infinite Loop"
//output "Infinite Loop"
```

### Parsing an object
Expand All @@ -76,7 +76,7 @@ const source = {

dot.parse(source);

/* outputs
/* output
{
person: {
name: {
Expand Down Expand Up @@ -116,7 +116,7 @@ const source = {

dot.parse(source);

/* outputs
/* output
[
{
"postalCode": 95014,
Expand Down Expand Up @@ -147,7 +147,7 @@ const value = 'John Doe';

dot.parseKey(path, value);

/* outputs
/* output
{
person: {
name: 'John Doe',
Expand Down
21 changes: 11 additions & 10 deletions src/parse-key.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,25 @@
import getArrayIndex from './utils/get-array-index';
import getKey from './utils/get-key';
import shallowCopy from './utils/shallow-copy';

/**
* Parse object key from dot notation
* Parse an object key from dot notation
* @example
* parseKey('person.name', 'John Doe');
* // outputs { person: { name: 'John Doe' } }
* // output { person: { name: 'John Doe' } }
* parseKey('person.alias[]', 'John Doe');
* // outputs { person: { alias: ['John Doe] } }
* @param {string} path - Dot notation object path
* @param {any} value - Dot notation path value
* // output { person: { alias: ['John Doe'] } }
* @param {string} path - Dot notation path
* @param {unknown} value
* @returns {object}
*/
const parseKey = <T>(path: string, value: unknown): T extends [] ? T[] : T => {
const [current, remaining] = getKey(path);
const match = getArrayIndex(current);
const parseKey = <T>(path: string, value: unknown): T extends [] ? Array<T> : T => {
const [key, remainingPath] = getKey(path);
const hasArrayNotation = getArrayIndex(key);

const mount = (): T => (remaining ? parseKey<T>(remaining, value) : value) as T;
const compiledValue = (remainingPath ? parseKey<T>(remainingPath, value) : shallowCopy(value)) as T;

return (match ? [mount()] : { [current]: mount() }) as T extends [] ? T[] : T;
return (hasArrayNotation ? [compiledValue] : { [key]: compiledValue }) as T extends [] ? Array<T> : T;
};

export default parseKey;
26 changes: 12 additions & 14 deletions src/parse.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@ import getArrayIndex from './utils/get-array-index';
import is from './utils/is';
import merge from './utils/merge';
import createPathBreadcrumb from './utils/create-path-breadcrumb';
import shallowCopy from './utils/shallow-copy';

const compile = (
const compileEntry = (
source: Record<string, unknown> | unknown[],
instructions: string[],
value: unknown,
Expand All @@ -29,7 +30,7 @@ const compile = (
} else {
const hasChild = instructions.length > 1;

const result = hasChild ? compile((data as unknown[])[+idx] as unknown[], instructions.slice(1), value) : value;
const result = hasChild ? compileEntry(data[+idx] as unknown[], instructions.slice(1), value) : value;

if (is.object(result)) {
data[+idx] = { ...result };
Expand All @@ -46,29 +47,29 @@ const compile = (
/**
* Parse object from dot notation
* @template T
* @param {object} source - Dot notation object
* @returns {object}
* @param source
* @param {Object.<string, unknown>} source
* @return {T|T[]}
*/
const parse = <T>(source: Record<string, unknown>): T extends [] ? T[] : T => {
const paths = Object.keys(source);
const content = shallowCopy(source);

let result: unknown = getArrayIndex(createPathBreadcrumb(paths[0])[0]) ? [] : {};
const paths = Object.keys(content);

let result = getArrayIndex(createPathBreadcrumb(paths[0])[0]) ? [] : {};

for (let i = 0; i < paths.length; i += 1) {
const path = paths[i];
const hasArrayNotation = getArrayIndex(path);
const value = source[path];
const value = shallowCopy(content[path]);

let parsedValue = parseKey(path, value);

if (hasArrayNotation) {
const commonPath = path.substr(0, hasArrayNotation.index);
const workingPath = createPathBreadcrumb(path.replace(commonPath, ''));
const workingNode: unknown[] = commonPath ? pick(result, commonPath) || [] : (result as unknown[]);
const workingNode = commonPath ? pick<unknown[]>(result, commonPath) || [] : result;

parsedValue = compile(workingNode, workingPath, value);
parsedValue = compileEntry(workingNode, workingPath, value);

if (commonPath) {
parsedValue = parseKey(commonPath, parsedValue);
Expand All @@ -79,10 +80,7 @@ const parse = <T>(source: Record<string, unknown>): T extends [] ? T[] : T => {
result = parsedValue;
}

result = merge<Record<string, unknown>>(
result as Partial<Record<string, unknown>>,
parsedValue as Partial<Record<string, unknown>>,
);
result = merge(result, parsedValue);
}

return result as T extends [] ? T[] : T;
Expand Down
17 changes: 12 additions & 5 deletions src/pick.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,14 @@ describe('pick()', () => {
expect(pick(source, pathHasATypo)).toBe(undefined);
});

const expects: Record<string, unknown> = {
it('should return undefined when source is not an object nor array', () => {
const content = 'hello world';
const path = 'greeting';

expect(pick(content, path)).toBeUndefined();
});

const expects = {
array: source.array,
'array.[0]': source.array[0],
'array.[0].[0]': source.array[0][0],
Expand All @@ -107,7 +114,7 @@ describe('pick()', () => {
});

describe('object path', () => {
const expects: Record<string, unknown> = {
const expects = {
person: source.person,
'person.name': source.person.name,
'person.name.firstName': source.person.name.firstName,
Expand All @@ -126,7 +133,7 @@ describe('pick()', () => {
});

describe('array path', () => {
const expects: Record<string, unknown> = {
const expects = {
array: source.array,
'array[0]': source.array[0],
'array[0][0]': source.array[0][0],
Expand All @@ -144,7 +151,7 @@ describe('pick()', () => {
});
});

const expectsArray: Record<string, unknown> = {
const expectsArray = {
'[0]': array[0],
'[0][0]': array[0][0],
'[0][0][0]': array[0][0][0],
Expand All @@ -163,7 +170,7 @@ describe('pick()', () => {
});

describe('nested path', () => {
const paths: Record<string, unknown> = {
const paths = {
'person.address[0]': source.person.address[0],
'person.address[0].postalCode': source.person.address[0].postalCode,
'person.address[1]': source.person.address[1],
Expand Down
39 changes: 21 additions & 18 deletions src/pick.ts
Original file line number Diff line number Diff line change
@@ -1,48 +1,51 @@
import getArrayIndex from './utils/get-array-index';
import getKey from './utils/get-key';
import is from './utils/is';
import shallowCopy from './utils/shallow-copy';

/**
* Pick
* @template T, S
* @description Reads value from object using dot notation path as key
* Pick value at a given dot notation path
* @template T
* @param {Object.<string, unknown> | Object.<string, unknown[]} source
* @param {string} path
* @param {T} source
* @returns {T} value
*/
export const pick = <T = unknown, S = Record<string, unknown | unknown[]> | unknown[]>(source: S, path: string): T => {
const pick = <T>(source: Record<string, unknown> | Array<Record<string, unknown>>, path: string): T => {
if (is.nullOrUndefined(path) || !path.trim()) {
throw new SyntaxError(`A dot notation path was expected, but instead got "${path}"`);
}

const content = shallowCopy(source) as Record<string, unknown>;

// eslint-disable-next-line prefer-const
let [current, remaining] = getKey(path) as [string | number, string | undefined];
let [key, remainingPath]: [string | number, string | undefined] = getKey(path);

const match = getArrayIndex(current.toString());
const hasArrayNotation = getArrayIndex(key.toString());

if (match) {
const { 1: index } = match;
if (hasArrayNotation) {
const { 1: idx } = hasArrayNotation;

if (!index) {
if (!idx) {
throw new SyntaxError(`An array index was expected but nothing was found at "${path}"`);
}

if (Number.isNaN(+index)) {
throw new TypeError(`Array index must a positive integer "${index}"`);
if (Number.isNaN(+idx)) {
throw new TypeError(`Array index must a positive integer "${idx}"`);
}

if (+index < 0) {
throw new RangeError(`Array index must be equal or greater than 0, but instead got "${index}"`);
if (+idx < 0) {
throw new RangeError(`Array index must be equal or greater than 0, but instead got "${idx}"`);
}

current = +index;
// replace key with array index value
key = +idx;
}

if (!remaining || !(source as Record<string, T | unknown>)[current]) {
return (source as Record<string, T | unknown>)[current] as T;
if (!remainingPath || is.nullOrUndefined(content[key])) {
return content[key] as T;
}

return pick<T, S>((source as Record<string, T | unknown>)[current] as S, remaining);
return pick<T>(content[key] as Record<string, unknown>, remainingPath);
};

export default pick;
2 changes: 1 addition & 1 deletion src/utils/create-path-breadcrumb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import getKey from './get-key';
const createPathBreadcrumb = (path: string): string[] => {
return (
path
.split(getKey.regex)
.split(getKey.regexp)
.filter(Boolean)
// add default index for empty array notation
.map((p) => (p === '[]' ? '[0]' : p))
Expand Down
10 changes: 5 additions & 5 deletions src/utils/get-array-index.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
/**
* Ger Array Index
* @description get array index from given string
* @description get array index from dot notation path string
* @param {string} str
* @return {string[]|null}
* @return {RegExpExecArray|null}
*/
const getArrayIndex = (str: string): RegExpExecArray | null => {
return getArrayIndex.regexNaNIndex.exec(str) || getArrayIndex.regexIntegerIndex.exec(str);
return getArrayIndex.regexpNaNIndex.exec(str) || getArrayIndex.regexpIntIndex.exec(str);
};

getArrayIndex.regexIntegerIndex = /\[([-]*\d*)\]/g;
getArrayIndex.regexpIntIndex = /\[(-*\d*)]/g;

getArrayIndex.regexNaNIndex = /\[([^\]]*)\]/;
getArrayIndex.regexpNaNIndex = /\[([^\]]*)]/;

export default getArrayIndex;
1 change: 1 addition & 0 deletions src/utils/get-key.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ describe('utils/getKey()', () => {

Object.keys(expectations).forEach((path) => {
it(`should parse "${path} accordingly`, () => {
// console.table(dotNotationPath(path));
expect(getKey(path)).toStrictEqual(expectations[path]);
});
});
Expand Down
4 changes: 2 additions & 2 deletions src/utils/get-key.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,11 @@
* @returns {[string, string | undefined]} - returns key, remaining dot notation path and isArray,
*/
const getKey = (value: string): [string, string | undefined] => {
const [current, ...remaining] = value.split(getKey.regex).filter(Boolean);
const [current, ...remaining] = value.split(getKey.regexp).filter(Boolean);

return [current, remaining.length ? remaining.join('.') : undefined];
};

getKey.regex = /\.|(\[[^\]]*\])|(\[[-]*\d*\])/;
getKey.regexp = /\.|(\[[^\]]*])|(\[-*\d*])/;

export default getKey;
Loading

0 comments on commit c1e28c1

Please sign in to comment.