Skip to content

Commit

Permalink
Add object utilities flattenArray, flattenObject, unflattenObject (#426)
Browse files Browse the repository at this point in the history
* Working

* Self-CR

* Format and remove no-longer-true comment

* Switch to Github Actions

* Fix workflows

* Prettier

* Self-CR
  • Loading branch information
rkuykendall committed Jan 11, 2022
1 parent 0aee881 commit f6fd7c4
Show file tree
Hide file tree
Showing 5 changed files with 147 additions and 0 deletions.
47 changes: 47 additions & 0 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: CI
on: [push]
jobs:
build:
runs-on: ubuntu-latest

steps:
- name: Begin CI...
uses: actions/checkout@v2

- name: Use Node 12
uses: actions/setup-node@v1
with:
node-version: 12.x

- name: Use cached node_modules
uses: actions/cache@v1
with:
path: node_modules
key: nodeModules-${{ hashFiles('yarn.lock') }}
restore-keys: |
nodeModules-
- name: Install dependencies
run: yarn install --frozen-lockfile
env:
CI: true

- name: Lint
run: yarn lint
env:
CI: true

- name: Test
run: yarn test --ci --coverage --maxWorkers=2 --colors
env:
CI: true

- name: Coverage
uses: coverallsapp/github-action@master
with:
github-token: ${{ secrets.GITHUB_TOKEN }}

- name: Build
run: yarn build
env:
CI: true
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ _book
.cache
source.html
*.log
build

# OS X
.DS_Store
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from './date';
export * from './formatting';
export * from './utils';
export * from './validation';
export * from './objects';
59 changes: 59 additions & 0 deletions src/objects.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import { flatten as flattenArray, isArray, isPlainObject, set } from 'lodash';

// Complex types sourced from
// https://flut1.medium.com/deep-flatten-typescript-types-with-finite-recursion-cb79233d93ca

type NonObjectKeysOf<T> = {
[K in keyof T]: T[K] extends Array<any> ? K : T[K] extends object ? never : K;
}[keyof T];

type ValuesOf<T> = T[keyof T];

type ObjectValuesOf<T> = Exclude<Extract<ValuesOf<T>, object>, Array<any>>;

type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;

type Flatten<T> = Pick<T, NonObjectKeysOf<T>> & UnionToIntersection<ObjectValuesOf<T>>;

function mergeObjects<A extends object, B extends object>(objectA: A, objectB: B): A & B {
return { ...objectA, ...objectB };
}

const _hasUnflattenedValues = (value: unknown): boolean => {
return (isArray(value) || isPlainObject(value)) && !!Object.keys(value).length;
};

function _flattenObject<T>(input: T, parentKey: string): Flatten<T> {
const _getFlatKey = (key: string) => {
if (isArray(input)) {
return `${parentKey}[${key}]`;
}

if (parentKey) {
return `${parentKey}.${key}`;
}

return key;
};

return Object.entries(input).reduce((output: Flatten<T>, [key, value]) => {
const flatKey = _getFlatKey(key);

if (_hasUnflattenedValues(value)) {
const flatValues = _flattenObject(value, flatKey);
return mergeObjects(output, flatValues);
}

return mergeObjects(output, { [flatKey]: value });
}, {});
}

function flattenObject<T extends object>(input: T): Flatten<T> {
return _flattenObject(input, '');
}

function unflattenObject(object: Object) {
return Object.entries(flattenObject(object)).reduce((objOut, [key, value]) => set(objOut, key, value), {});
}

export { flattenArray, flattenObject, unflattenObject };
39 changes: 39 additions & 0 deletions test/objects.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { flattenObject, unflattenObject } from '../src/objects';

const COMPLETELY_FLATTENED_OBJECTS: Array<[object, object]> = [
[{ x: 'y' }, { x: 'y' }],
[{ 'x[0]': 'y' }, { x: ['y'] }],
[{ 'x[0]': 'y', 'x[1]': 'z' }, { x: ['y', 'z'] }],
[{ 'x[0].a': 'b' }, { x: [{ a: 'b' }] }],
[{ 'x.y[0]': 'z' }, { x: { y: ['z'] } }],
[
{ 'a[0].b[0].c': 'x', 'a[0].b[1].c': 'y', 'a[1].d': 'z' },
{
a: [{ b: [{ c: 'x' }, { c: 'y' }] }, { d: 'z' }],
},
],
],
PARTIALLY_FLATTENED_OBJECTS: Array<[object, object]> = [
[{ 'x[0]': { a: 'b' } }, { x: [{ a: 'b' }] }],
[{ x: { 'y[0]': 'z' } }, { x: { y: ['z'] } }],
[
{ 'a[0]': { 'b[0].c': 'x' }, 'a[0].b[1]': { c: 'y' }, 'a[1].d': 'z' },
{ a: [{ b: [{ c: 'x' }, { c: 'y' }] }, { d: 'z' }] },
],
];

describe('flattenObject', () => {
it(`Correctly flattens objects`, () => {
COMPLETELY_FLATTENED_OBJECTS.forEach(([flat, deep]) => {
expect(flattenObject(deep)).toStrictEqual(flat);
});
});
});

describe('unflattenObject', () => {
it(`Correctly unflattens objects`, () => {
[...COMPLETELY_FLATTENED_OBJECTS, ...PARTIALLY_FLATTENED_OBJECTS].forEach(([flat, deep]) => {
expect(unflattenObject(flat)).toStrictEqual(deep);
});
});
});

0 comments on commit f6fd7c4

Please sign in to comment.