1
0
mirror of https://github.com/lensapp/lens.git synced 2025-05-20 05:10:56 +00:00

Introduce way to create hierarchical composites from a flat array

Co-authored-by: Janne Savolainen <janne.savolainen@live.fi>

Signed-off-by: Iku-turso <mikko.aspiala@gmail.com>
This commit is contained in:
Iku-turso 2022-10-04 15:57:56 +03:00 committed by Janne Savolainen
parent 3add4255cf
commit 043afc3ac8
No known key found for this signature in database
GPG Key ID: 8C6CFB2FFFE8F68A
8 changed files with 716 additions and 0 deletions

View File

@ -0,0 +1,52 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite";
import getComposite from "../get-composite";
import { findComposite } from "./find-composite";
describe("find-composite", () => {
let composite: Composite<{ id: string; parentId?: string; someProperty: string }>;
beforeEach(() => {
composite = getComposite({
source: [
{ id: "some-root-id", someProperty: "some-value" },
{ id: "some-child-id", parentId: "some-root-id", someProperty: "some-value" },
{ id: "some-irrelevant-grandchild-id", parentId: "some-child-id", someProperty: "some-value" },
{ id: "some-grandchild-id", parentId: "some-child-id", someProperty: "some-value" },
],
rootId: "some-root-id",
});
});
it("when finding root using path, does so", () => {
const actual = findComposite("some-root-id")(composite);
expect(actual?.id).toBe("some-root-id");
});
it("when finding child using path, does so", () => {
const actual = findComposite("some-root-id.some-child-id")(composite);
expect(actual?.id).toBe("some-child-id");
});
it("when finding grandchild using path, does so", () => {
const actual = findComposite(
"some-root-id.some-child-id.some-grandchild-id",
)(composite);
expect(actual?.id).toBe("some-grandchild-id");
});
it("when finding with non existing path, returns undefined", () => {
const actual = findComposite("some-root-id.some-non-existing-path")(
composite,
);
expect(actual).toBe(undefined);
});
});

View File

@ -0,0 +1,11 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite";
import { normalizeComposite } from "../normalize-composite/normalize-composite";
export const findComposite =
(path: string) =>
<T>(composite: Composite<T>): Composite<T> | undefined =>
new Map(normalizeComposite(composite)).get(path);

View File

@ -0,0 +1,61 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import getComposite from "../get-composite";
import { getCompositePaths } from "./get-composite-paths";
describe("get-composite-paths", () => {
it("given composite with ordered children, returns ordered paths", () => {
const someRootItem = {
id: "some-root-id",
};
const someItem1 = {
id: "some-id-1",
parentId: "some-root-id",
orderNumber: 1,
};
const someItem2 = {
id: "some-id-2",
parentId: "some-root-id",
orderNumber: 2,
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-id-1",
orderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-id-1",
orderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someItem2,
someItem1,
someChildItem2,
someChildItem1,
];
const composite = getComposite({
source: items,
});
const actual = getCompositePaths(composite);
expect(actual).toEqual([
"some-root-id",
"some-root-id.some-id-1",
"some-root-id.some-id-1.some-child-id-1",
"some-root-id.some-id-1.some-child-id-2",
"some-root-id.some-id-2",
]);
});
});

View File

@ -0,0 +1,28 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import { flatMap } from "lodash/fp";
import type { Composite } from "../get-composite";
export const getCompositePaths = (
composite: Composite<any>,
previousPath: string[] = [],
): string[] => {
const currentPath = [...previousPath, composite.id];
const currentPathString = currentPath.join(".");
return [
currentPathString,
...pipeline(
composite.children,
flatMap((childComposite) =>
getCompositePaths(childComposite, currentPath),
),
),
];
};

View File

@ -0,0 +1,380 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { sortBy } from "lodash/fp";
import getComposite from "./get-composite";
import { getCompositePaths } from "./get-composite-paths/get-composite-paths";
describe("get-composite", () => {
it("given items and a specified root id, creates a composite", () => {
const someRootItem = {
someId: "some-root-id",
someParentId: undefined,
someProperty: "some-root-content",
};
const someItem = {
someId: "some-id",
someParentId: "some-root-id",
someProperty: "some-content",
};
const someNestedItem = {
someId: "some-nested-id",
someParentId: "some-id",
someProperty: "some-nested-content",
};
const items = [someRootItem, someItem, someNestedItem];
const composite = getComposite({
source: items,
rootId: "some-root-id",
getId: (x) => x.someId,
getParentId: (x) => x.someParentId,
});
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given items and an unspecified root id and single item without parent as root, creates a composite", () => {
const someRootItem = {
someId: "some-root-id",
someProperty: "some-root-content",
// Notice: no "someParentId" makes this the root.
someParentId: undefined,
};
const someItem = {
someId: "some-id",
someParentId: "some-root-id",
someProperty: "some-content",
};
const someNestedItem = {
someId: "some-nested-id",
someParentId: "some-id",
someProperty: "some-nested-content",
};
const items = [someRootItem, someItem, someNestedItem];
const composite = getComposite({
source: items,
// Notice: no root id
// rootId: "some-root-id",
getId: (x) => x.someId,
getParentId: (x) => x.someParentId,
});
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given items and an unspecified root id and multiple items without parent as root, throws", () => {
const someRootItem = {
someId: "some-root-id",
// Notice: no "someParentId" makes this a root.
someParentId: undefined,
};
const someOtherRootItem = {
someId: "some-other-root-id",
// Notice: no "someParentId" makes also this a root.
someParentId: undefined,
};
const items = [someRootItem, someOtherRootItem];
expect(() => {
getComposite({
source: items,
// Notice: no root id
// rootId: "some-root-id",
getId: (x) => x.someId,
getParentId: (x) => x.someParentId,
});
}).toThrow(
'Tried to get a composite, but multiple roots where encountered: "some-root-id", "some-other-root-id"',
);
});
it("given non-unique ids, throws", () => {
const someItem = {
someId: "some-id",
someParentId: "irrelevant",
};
const someOtherItem = {
someId: "some-id",
someParentId: "irrelevant",
};
const items = [someItem, someOtherItem];
expect(() => {
getComposite({
source: items,
rootId: "irrelevant",
getId: (x) => x.someId,
getParentId: (x) => x.someParentId,
});
}).toThrow(
'Tried to get a composite but encountered non-unique ids: "some-id"',
);
});
it("given missing parent ids, throws", () => {
const someItem = {
someId: "some-id",
someParentId: undefined,
};
const someItemWithMissingParentId = {
someId: "some-other-id",
someParentId: "some-missing-id",
};
const items = [someItem, someItemWithMissingParentId];
expect(() => {
getComposite({
source: items,
rootId: "irrelevant",
getId: (x) => x.someId,
getParentId: (x) => x.someParentId,
});
}).toThrow(
'Tried to get a composite but encountered missing parent ids: "some-missing-id"',
);
});
it("given undefined ids, throws", () => {
const root = {
someParentId: undefined,
someId: "some-root",
};
const someItem = {
someParentId: "some-root",
someId: undefined,
};
const someOtherItem = {
someParentId: "some-root",
someId: undefined,
};
const items = [root, someItem, someOtherItem];
expect(() => {
getComposite({
source: items,
rootId: "some-root",
getId: (x) => x.someId as any,
getParentId: (x) => x.someParentId,
});
}).toThrow("Tried to get a composite but encountered 2 undefined ids");
});
it("given items with default properties for id and parentId, creates a composite", () => {
const someRootItem = {
id: "some-root-id",
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
};
const someNestedItem = {
id: "some-nested-id",
parentId: "some-id",
};
const items = [someRootItem, someItem, someNestedItem];
const composite = getComposite({
source: items,
// Notice: no need for functions
// getId: (x) => x.id,
// getParentId: (x) => x.parentId,
});
expect(composite).toEqual({
id: "some-root-id",
value: someRootItem,
children: [
{
id: "some-id",
parentId: "some-root-id",
value: someItem,
children: [
{
id: "some-nested-id",
parentId: "some-id",
value: someNestedItem,
children: [],
},
],
},
],
});
});
it("given explicitly ordered items, creates a composite with ordered children", () => {
const someRootItem = {
id: "some-root-id",
someOrderNumber: 1,
};
const someItem1 = {
id: "some-id-1",
parentId: "some-root-id",
someOrderNumber: 1,
};
const someItem2 = {
id: "some-id-2",
parentId: "some-root-id",
someOrderNumber: 2,
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-id-1",
someOrderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-id-1",
someOrderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someItem2,
someItem1,
someChildItem2,
someChildItem1,
];
const composite = getComposite({
source: items,
// Note: this is the explicit function to order a composite's children.
getOrderedChildren: (things) =>
sortBy((thing) => thing.someOrderNumber, things),
});
const orderedPaths = getCompositePaths(composite);
expect(orderedPaths).toEqual([
"some-root-id",
"some-root-id.some-id-1",
"some-root-id.some-id-1.some-child-id-1",
"some-root-id.some-id-1.some-child-id-2",
"some-root-id.some-id-2",
]);
});
it("given implicitly ordered items, creates a composite with ordered children", () => {
const someRootItem = {
id: "some-root-id",
orderNumber: 1,
};
const someItem1 = {
id: "some-id-1",
parentId: "some-root-id",
orderNumber: 1,
};
const someItem2 = {
id: "some-id-2",
parentId: "some-root-id",
orderNumber: 2,
};
const someChildItem1 = {
id: "some-child-id-1",
parentId: "some-id-1",
orderNumber: 1,
};
const someChildItem2 = {
id: "some-child-id-2",
parentId: "some-id-1",
orderNumber: 2,
};
const items = [
someRootItem,
// Note: not in order yet.
someItem2,
someItem1,
someChildItem2,
someChildItem1,
];
const composite = getComposite({
source: items,
// Note: without explicit getOrderedChildren for ordering, implicit default value of "orderNumber" will be used, if it exists.
// getOrderedChildren: things => sortBy(thing => thing.orderNumber, things),
});
const orderedPaths = getCompositePaths(composite);
expect(orderedPaths).toEqual([
"some-root-id",
"some-root-id.some-id-1",
"some-root-id.some-id-1.some-child-id-1",
"some-root-id.some-id-1.some-child-id-2",
"some-root-id.some-id-2",
]);
});
});

View File

@ -0,0 +1,120 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { pipeline } from "@ogre-tools/fp";
import {
countBy,
filter,
toPairs,
nth,
map,
uniq,
without,
compact,
get,
sortBy,
} from "lodash/fp";
export interface Composite<T> {
id: string;
parentId: string | undefined;
value: T;
children: Composite<T>[];
}
export default <T>({
source,
rootId,
getId = get("id"),
getParentId = get("parentId"),
getOrderedChildren = (things: T[]) => sortBy("orderNumber", things),
}: {
source: T[];
rootId?: string;
getId?: (thing: T) => string;
getParentId?: (thing: T) => string | undefined;
getOrderedChildren?: (things: T[]) => T[];
}) => {
const undefinedIds = pipeline(
source,
filter((x) => getId(x) === undefined),
);
if(undefinedIds.length) {
throw new Error(
`Tried to get a composite but encountered ${undefinedIds.length} undefined ids`,
);
}
const duplicateIds = pipeline(
source,
countBy(getId),
toPairs,
filter(([, count]) => count > 1),
map(nth(0)),
);
if (duplicateIds.length) {
throw new Error(
`Tried to get a composite but encountered non-unique ids: "${duplicateIds
.map((x) => String(x))
.join('", "')}"`,
);
}
const allIds = pipeline(source, map(getId));
const allParentIds = pipeline(source, map(getParentId), uniq, compact);
const unknownParentIds = without(allIds, allParentIds);
if (unknownParentIds.length) {
throw new Error(
`Tried to get a composite but encountered missing parent ids: "${unknownParentIds
.map((x) => String(x))
.join('", "')}"`,
);
}
const toComposite = (thing: T): Composite<T> => {
const thingId = getId(thing);
return {
id: thingId,
parentId: getParentId(thing),
value: thing,
children: pipeline(
source,
filter((childThing) => {
const parentId = getParentId(childThing);
return parentId !== undefined && parentId === thingId;
}),
getOrderedChildren,
map(toComposite),
),
};
};
const isRootId = rootId
? (thing: T) => getId(thing) === rootId
: (thing: T) => getParentId(thing) === undefined;
const roots = source.filter(isRootId);
if (roots.length > 1) {
throw new Error(
`Tried to get a composite, but multiple roots where encountered: "${roots
.map(getId)
.join('", "')}"`,
);
}
return toComposite(roots[0]);
};

View File

@ -0,0 +1,44 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import { normalizeComposite } from "./normalize-composite";
import getComposite from "../get-composite";
describe("normalize-composite", () => {
it("given a composite, flattens it to path and composite", () => {
const someRootItem = {
id: "some-root-id",
parentId: undefined,
};
const someItem = {
id: "some-id",
parentId: "some-root-id",
};
const someNestedItem = {
id: "some-child-id",
parentId: "some-id",
};
const items = [someRootItem, someItem, someNestedItem];
const composite = getComposite({
source: items,
});
const actual = normalizeComposite(composite);
expect(actual).toEqual([
["some-root-id", expect.objectContaining({ value: someRootItem })],
["some-root-id.some-id", expect.objectContaining({ value: someItem })],
[
"some-root-id.some-id.some-child-id",
expect.objectContaining({ value: someNestedItem }),
],
]);
});
});

View File

@ -0,0 +1,20 @@
/**
* Copyright (c) OpenLens Authors. All rights reserved.
* Licensed under MIT License. See LICENSE in root directory for more information.
*/
import type { Composite } from "../get-composite";
export const normalizeComposite = <T>(
composite: Composite<T>,
previousPath: string[] = [],
): (readonly [path: string, composite: Composite<T>])[] => {
const currentPath = [...previousPath, composite.id];
const pathAndCompositeTuple = [currentPath.join("."), composite] as const;
return [
pathAndCompositeTuple,
...composite.children.flatMap((x) => normalizeComposite(x, currentPath)),
];
};