Skip to content

Commit

Permalink
[snipsonian-core] added some extra object utility functions
Browse files Browse the repository at this point in the history
  • Loading branch information
Ben Verbist committed Sep 27, 2022
1 parent e264c3c commit eda48bd
Show file tree
Hide file tree
Showing 16 changed files with 331 additions and 4 deletions.
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
v14.18.2
16.15.1
3 changes: 2 additions & 1 deletion packages/snipsonian-core/src/object/cloneObjectDataProps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
* So if the input object contains e.g. a function, the cloned object would not contain that property.
*/
import isObjectPure from '../is/isObjectPure';
import { TAnyObject } from '../typings/object';

export default function cloneObjectDataProps(obj: object): object {
export default function cloneObjectDataProps<Obj = TAnyObject>(obj: Obj): Obj {
if (!isObjectPure(obj)) {
return null;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import getPossiblyNestedObjectPropValue from './getPossiblyNestedObjectPropValue';

describe('getPossiblyNestedObjectPropValue()', () => {
const TEST_OBJECT = {
someField: 'someValue',
levelOne: {
levelTwo: {
levelTwoField: false,
levelThree: {
deepField: 'qwerty',
otherDeepField: 123,
},
},
levelOneField: 'qed',
levelTwoArray: [{
fieldInArray: 'abc',
}],
},
};

it('returns a - possibly nested - field value that matches the input path part(s)', () => {
expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'someField',
)).toEqual('someValue');

expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'levelOne', 'levelOneField',
)).toEqual('qed');

expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'levelOne', 'levelTwo', 'levelTwoField',
)).toEqual(false);
expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'levelOne', 'levelTwo', 'levelThree', 'deepField',
)).toEqual('qwerty');
expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'levelOne', 'levelTwo', 'levelThree', 'otherDeepField',
)).toEqual(123);
expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'levelOne', 'levelTwoArray', '0', 'fieldInArray',
)).toEqual('abc');

expect(getPossiblyNestedObjectPropValue(
TEST_OBJECT,
'levelOne', 'levelTwo', 'levelThree',
)).toEqual({
deepField: 'qwerty',
otherDeepField: 123,
});
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { TAnyObject } from '../../typings/object';
import isArray from '../../is/isArray';
import isObjectPure from '../../is/isObjectPure';

// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function getPossiblyNestedObjectPropValue(obj: TAnyObject, ...pathParts: string[]): any {
if (!obj || (!isArray(obj) && !isObjectPure(obj))) {
return null;
}

if (pathParts.length === 1) {
return obj[pathParts[0]];
}

const [firstPathPart, ...deeperPathParts] = pathParts;

return getPossiblyNestedObjectPropValue(
obj[firstPathPart] as TAnyObject,
...deeperPathParts,
);
}
14 changes: 14 additions & 0 deletions packages/snipsonian-core/src/object/keyVals/flipObjectKeyVals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* E.g. input object { abc: 'def' } will result in { def: 'abc' }
*/
export default function flipObjectKeyVals<Key extends string = string, Val extends string = string>(
obj: Record<Key, Val>,
): Record<Val, Key> {
return Object.entries<Val>(obj).reduce(
(accumulator, [key, val]) => {
accumulator[val] = key as Key;
return accumulator;
},
{} as Record<Val, Key>,
);
}
10 changes: 10 additions & 0 deletions packages/snipsonian-core/src/object/keyVals/getObjectKeyVals.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { TAnyObject } from '../../typings/object';
import { IKeyValuePair } from '../../typings/patterns';

export default function getObjectKeyVals<Value = unknown>(obj: TAnyObject<Value>): IKeyValuePair<Value>[] {
return Object.keys(obj)
.map((key) => ({
key,
value: obj[key],
}));
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
import updateObjectField from './updateObjectField';
import cloneObjectDataProps from '../cloneObjectDataProps';

describe('updateObjectField()', () => {
const COMPLEX_OBJ = {
name: 'Doe',
firstName: 'John',
meta: {
version: 3,
languages: ['nl', 'en'],
job: {
category: 'IT',
location: 'Leuven',
},
children: [{ name: 'Kimberly', age: 10 }, { name: 'Kelly', age: 8 }],
pets: {
0: { name: 'barkly', type: 'dog' },
1: { name: 'kitkat', type: 'cat' },
},
},
};

it('updates the specified field when it is just a top field', () => {
const obj = cloneObjectDataProps(COMPLEX_OBJ);
const expectedObj = cloneObjectDataProps(COMPLEX_OBJ);

const actual = updateObjectField({ obj, fieldToUpdateRef: 'firstName', val: 'Jane' });

expectedObj.firstName = 'Jane';
expect(actual).toEqual(expectedObj);
});

it('updates nested object properties', () => {
const obj = cloneObjectDataProps(COMPLEX_OBJ);
const expectedObj = cloneObjectDataProps(COMPLEX_OBJ);

const actual = updateObjectField({ obj, fieldToUpdateRef: 'meta.version', val: 44 });

expectedObj.meta.version = 44;
expect(actual).toEqual(expectedObj);
});

it('updates nested array values', () => {
const obj = cloneObjectDataProps(COMPLEX_OBJ);
const expectedObj = cloneObjectDataProps(COMPLEX_OBJ);

const actual = updateObjectField({ obj, fieldToUpdateRef: 'meta.languages[1]', val: 'fr' });

expectedObj.meta.languages = ['nl', 'fr'];
expect(actual).toEqual(expectedObj);
});

it('updates nested arraylike object values', () => {
const obj = cloneObjectDataProps(COMPLEX_OBJ);
const expectedObj = cloneObjectDataProps(COMPLEX_OBJ);

const actual = updateObjectField({ obj, fieldToUpdateRef: 'meta.pets.1.name', val: 'twix' });

expectedObj.meta.pets[1] = { name: 'twix', type: 'cat' };
expect(actual).toEqual(expectedObj);
});

it('updates deeply nested values', () => {
const obj = cloneObjectDataProps(COMPLEX_OBJ);
const expectedObj = cloneObjectDataProps(COMPLEX_OBJ);

const actualTemp = updateObjectField({ obj, fieldToUpdateRef: 'meta.job.category', val: 'HR' });
const actual = updateObjectField({ obj: actualTemp, fieldToUpdateRef: 'meta.children[1].age', val: 21 });

expectedObj.meta.job.category = 'HR';
expectedObj.meta.children[1].age = 21;
expect(actual).toEqual(expectedObj);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import isObject from '../../is/isObject';
import { TAnyObject } from '../../typings/object';
import getPossiblyNestedObjectPropValue from '../filtering/getPossiblyNestedObjectPropValue';

/**
* 'fieldToUpdateRef' can be something like e.g. "parentField[0].childField".
* This function would then update the 'childField' property of the first element of a
* 'parentField' array (which should be a property of the input 'obj').
*/
export default function updateObjectField({
obj,
fieldToUpdateRef,
val,
}: {
obj: TAnyObject;
fieldToUpdateRef: string;
val: unknown;
}): TAnyObject {
if (!isObject(obj)) {
return obj;
}

const lastArraySeparator = fieldToUpdateRef.lastIndexOf('[');
const lastObjSeparator = fieldToUpdateRef.lastIndexOf('.');

const splitIndex = Math.max(lastArraySeparator, lastObjSeparator);

if (splitIndex === -1) {
// eslint-disable-next-line no-param-reassign
obj[fieldToUpdateRef] = val;
return obj;
}

const parentRef = fieldToUpdateRef.substring(0, splitIndex);
const remainingRef = fieldToUpdateRef.substring(splitIndex + 1);
const childKey = (splitIndex === lastArraySeparator)
? remainingRef.substring(0, remainingRef.indexOf(']'))
: remainingRef;

const getFieldsFromParentRefRegex = /([^.[\]]+)/g;
const pathParts = parentRef.match(getFieldsFromParentRefRegex);
const parent = getPossiblyNestedObjectPropValue(obj, ...pathParts) as TAnyObject;

parent[childKey] = val;

return obj;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import doesAnyObjectValueMatchFilter from './doesAnyObjectValueMatchFilter';

describe('doesAnyObjectValueMatchFilter()', () => {
const testObject = {
id: 3,
name: 'testObject',
description: 'test',
hiddenKey: 'neverShareThisKey',
};

it('returns true if any property of the input object matches the input string filter', () => {
expect(doesAnyObjectValueMatchFilter(testObject, '3')).toBeTruthy();
expect(doesAnyObjectValueMatchFilter(testObject, 'test')).toBeTruthy();
expect(doesAnyObjectValueMatchFilter(testObject, 'wrong')).toBeFalsy();
});

it('allows to ignore some object properties so that they are not matched against the filter', () => {
const filter = 'neverShare';

expect(doesAnyObjectValueMatchFilter(
testObject,
filter,
)).toBeTruthy();

expect(doesAnyObjectValueMatchFilter(
testObject,
filter,
{ fieldsToIgnore: ['hiddenKey'] },
)).toBeFalsy();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import isString from '../../is/isString';
import isSetString from '../../string/isSetString';
import { TAnyObject } from '../../typings/object';
import getObjectKeyVals from '../keyVals/getObjectKeyVals';
import escapeSpecialCharsForRegex from '../../regex/escapeSpecialCharsForRegex';

export default function doesAnyObjectValueMatchFilter(
obj: TAnyObject,
filterValue: string,
options: { fieldsToIgnore?: string[] } = {},
): boolean {
if (!isSetString(filterValue)) {
/* no filter set, so object matches */
return true;
}

const filterRegex = new RegExp(escapeSpecialCharsForRegex(filterValue), 'i');
const { fieldsToIgnore } = options;

return getObjectKeyVals(obj)
.filter(({ key }) => !fieldsToIgnore || fieldsToIgnore.indexOf(key) === -1)
.some(({ value = '' }) => {
const fieldValue = isString(value)
? value
: value.toString();

return fieldValue.search(filterRegex) > -1;
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
import isObjectWithProps from './isObjectWithProps';

describe('isObjectWithProps()', () => {
it('returns only true if the input is an object with at least 1 property', () => {
expect(isObjectWithProps({ someProp: false })).toEqual(true);
expect(isObjectWithProps({ a: 1, b: 'xyz' })).toEqual(true);

expect(isObjectWithProps({})).toEqual(false);
expect(isObjectWithProps(undefined)).toEqual(false);
expect(isObjectWithProps(null)).toEqual(false);
expect(isObjectWithProps([])).toEqual(false);
expect(isObjectWithProps('str')).toEqual(false);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import isObjectPure from '../../is/isObjectPure';
import isEmptyObject from './isEmptyObject';

export default function isObjectWithProps(val: unknown): boolean {
return isObjectPure(val) && !isEmptyObject(val);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import isSet from '../../is/isSet';
import isObject from '../../is/isObject';

export default function isSetObject(val: unknown) {
return isSet(val) && isObject(val);
}
14 changes: 14 additions & 0 deletions packages/snipsonian-core/src/regex/escapeSpecialCharsForRegex.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
/**
* To be used when e.g. a user-input-string has to be turned into a regex for client-side-searching
*/
export default function escapeSpecialCharsForRegex(input: string) {
if (!input) {
return input;
}

return input
.replaceAll('.', '\\.')
.replaceAll('+', '\\+')
.replaceAll('*', '\\*')
.replaceAll('?', '\\?');
}
2 changes: 1 addition & 1 deletion packages/snipsonian-core/src/typings/patterns.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export interface IKeyValuePair<Key = string, Value = string> {
export interface IKeyValuePair<Value = string, Key = string> {
key: Key;
value: Value;
}
5 changes: 4 additions & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,10 @@
"es2015",
"es2016",
"es2017",
"es2018"
"es2018",
"es2019",
"es2020",
"es2021"
],
"target": "es2017", /* Specify ECMAScript target version: "ES3" (default), "ES5", "ES6"/"ES2015", "ES2016", "ES2017" or "ESNext". */
/* Note: "ESNext" targets latest supported ES proposed features. */
Expand Down

0 comments on commit eda48bd

Please sign in to comment.