Skip to content

Commit fda733c

Browse files
authored
Merge pull request #18 from toothlessdev/17-refactor-to-recursive-hash-tree-structure
Refactor to recursive hash tree structure
2 parents 320f059 + b887d6d commit fda733c

File tree

8 files changed

+217
-64
lines changed

8 files changed

+217
-64
lines changed
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from "./partition";

packages/patchlogr-core/src/partition/__tests__/partitionByMethod.test.ts

Lines changed: 37 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CanonicalSpec } from "@patchlogr/types";
22
import { describe, expect, test } from "vitest";
33
import { partitionByMethod } from "../partitionByMethod";
4+
import type { HashInternalNode } from "../partition";
45

56
describe("partitionByMethod", () => {
67
test("should group by HTTPMethod", () => {
@@ -25,13 +26,22 @@ describe("partitionByMethod", () => {
2526
},
2627
};
2728

28-
const partitions = partitionByMethod(spec).partitions;
29-
expect(partitions).toHaveLength(1);
30-
expect(partitions.get("GET")).toHaveLength(2);
31-
expect(partitions.get("GET")?.[0]?.operationKey).toBe("GET /user");
32-
expect(partitions.get("GET")?.[1]?.operationKey).toBe(
33-
"GET /user/{userId}",
34-
);
29+
const result = partitionByMethod(spec);
30+
expect(result.root.type).toBe("node");
31+
expect(result.root.key).toBe("root");
32+
33+
const root = result.root as HashInternalNode;
34+
expect(root.children).toHaveLength(1);
35+
36+
const getMethodNode = root.children.find(
37+
(child) => child.key === "GET",
38+
) as HashInternalNode;
39+
40+
expect(getMethodNode).toBeDefined();
41+
expect(getMethodNode.type).toBe("node");
42+
expect(getMethodNode.children).toHaveLength(2);
43+
expect(getMethodNode.children[0]?.key).toBe("GET /user");
44+
expect(getMethodNode.children[1]?.key).toBe("GET /user/{userId}");
3545
});
3646

3747
test("should group by multiple HTTPMethods", () => {
@@ -56,14 +66,26 @@ describe("partitionByMethod", () => {
5666
},
5767
};
5868

59-
const partitions = partitionByMethod(spec).partitions;
69+
const result = partitionByMethod(spec);
70+
expect(result.root.type).toBe("node");
71+
72+
const root = result.root as HashInternalNode;
73+
expect(root.children).toHaveLength(2);
74+
75+
const getMethodNode = root.children.find(
76+
(child) => child.key === "GET",
77+
) as HashInternalNode;
78+
const postMethodNode = root.children.find(
79+
(child) => child.key === "POST",
80+
) as HashInternalNode;
81+
82+
expect(getMethodNode).toBeDefined();
83+
expect(postMethodNode).toBeDefined();
84+
85+
expect(getMethodNode.children).toHaveLength(1);
86+
expect(getMethodNode.children[0]?.key).toBe("GET /user");
6087

61-
expect(partitions).toHaveLength(2);
62-
expect(partitions.get("GET")).toHaveLength(1);
63-
expect(partitions.get("POST")).toHaveLength(1);
64-
expect(partitions.get("GET")?.[0]?.operationKey).toBe("GET /user");
65-
expect(partitions.get("POST")?.[0]?.operationKey).toBe(
66-
"POST /auth/login",
67-
);
88+
expect(postMethodNode.children).toHaveLength(1);
89+
expect(postMethodNode.children[0]?.key).toBe("POST /auth/login");
6890
});
6991
});

packages/patchlogr-core/src/partition/__tests__/partitionByTag.test.ts

Lines changed: 45 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CanonicalSpec } from "@patchlogr/types";
22
import { describe, expect, test } from "vitest";
33
import { DEFAULT_TAG, partitionByTag } from "../partitionByTag";
4+
import type { HashInternalNode } from "../partition";
45

56
describe("partitionByTag", () => {
67
test("should group by first tag", () => {
@@ -25,13 +26,21 @@ describe("partitionByTag", () => {
2526
},
2627
};
2728

28-
const partitions = partitionByTag(spec).partitions;
29-
expect(partitions).toHaveLength(1);
30-
expect(partitions.get("user")).toHaveLength(2);
31-
expect(partitions.get("user")?.[0]?.operationKey).toBe("GET /user");
32-
expect(partitions.get("user")?.[1]?.operationKey).toBe(
33-
"GET /user/{userId}",
34-
);
29+
const result = partitionByTag(spec);
30+
expect(result.root.type).toBe("node");
31+
expect(result.root.key).toBe("root");
32+
33+
const root = result.root as HashInternalNode;
34+
expect(root.children).toHaveLength(1);
35+
36+
const userTagNode = root.children.find(
37+
(child) => child.key === "user",
38+
) as HashInternalNode;
39+
40+
expect(userTagNode.type).toBe("node");
41+
expect(userTagNode.children).toHaveLength(2);
42+
expect(userTagNode.children[0]?.key).toBe("GET /user");
43+
expect(userTagNode.children[1]?.key).toBe("GET /user/{userId}");
3544
});
3645

3746
test("should group by multiple tags", () => {
@@ -56,15 +65,24 @@ describe("partitionByTag", () => {
5665
},
5766
};
5867

59-
const partitions = partitionByTag(spec).partitions;
68+
const result = partitionByTag(spec);
69+
expect(result.root.type).toBe("node");
70+
71+
const root = result.root as HashInternalNode;
72+
expect(root.children).toHaveLength(2);
6073

61-
expect(partitions).toHaveLength(2);
62-
expect(partitions.get("user")).toHaveLength(1);
63-
expect(partitions.get("auth")).toHaveLength(1);
64-
expect(partitions.get("user")?.[0]?.operationKey).toBe("GET /user");
65-
expect(partitions.get("auth")?.[0]?.operationKey).toBe(
66-
"POST /auth/login",
67-
);
74+
const userTagNode = root.children.find(
75+
(child) => child.key === "user",
76+
) as HashInternalNode;
77+
const authTagNode = root.children.find(
78+
(child) => child.key === "auth",
79+
) as HashInternalNode;
80+
81+
expect(userTagNode.children).toHaveLength(1);
82+
expect(userTagNode.children[0]?.key).toBe("GET /user");
83+
84+
expect(authTagNode.children).toHaveLength(1);
85+
expect(authTagNode.children[0]?.key).toBe("POST /auth/login");
6886
});
6987

7088
test("should group into default tag if tag not exists", () => {
@@ -81,12 +99,18 @@ describe("partitionByTag", () => {
8199
},
82100
};
83101

84-
const partitions = partitionByTag(spec).partitions;
102+
const result = partitionByTag(spec);
103+
expect(result.root.type).toBe("node");
104+
105+
const root = result.root as HashInternalNode;
106+
expect(root.children).toHaveLength(1);
107+
108+
const defaultTagNode = root.children.find(
109+
(child) => child.key === DEFAULT_TAG,
110+
) as HashInternalNode;
85111

86-
expect(partitions).toHaveLength(1);
87-
expect(partitions.get(DEFAULT_TAG)).toHaveLength(1);
88-
expect(partitions.get(DEFAULT_TAG)?.[0]?.operationKey).toBe(
89-
"GET /user",
90-
);
112+
expect(defaultTagNode.type).toBe("node");
113+
expect(defaultTagNode.children).toHaveLength(1);
114+
expect(defaultTagNode.children[0]?.key).toBe("GET /user");
91115
});
92116
});
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
export type { Hash, HashNode, PartitionedSpec } from "./partition";
2+
3+
export { partitionByMethod } from "./partitionByMethod";
4+
export { partitionByTag } from "./partitionByTag";
Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,24 @@
1-
export type Partition = {
2-
hash: string;
3-
operationKey: string;
1+
export type Hash = string;
2+
3+
export type HashInternalNode<K = string, V = unknown> = {
4+
type: "node";
5+
key: K;
6+
hash: Hash;
7+
children: HashNode<K, V>[];
8+
};
9+
10+
export type HashLeafNode<K = string, V = unknown> = {
11+
type: "leaf";
12+
key: K;
13+
hash: Hash;
14+
value: V;
415
};
516

6-
export type PartitionedSpec = {
7-
hash: string;
17+
export type HashNode<K = string, V = unknown> =
18+
| HashInternalNode<K, V>
19+
| HashLeafNode<K, V>;
20+
21+
export type PartitionedSpec<K, V = unknown> = {
22+
root: HashNode<K, V>;
823
metadata: Record<string, unknown>;
9-
partitions: Map<string, Partition[]>;
1024
};
Lines changed: 54 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,70 @@
1-
import type { CanonicalSpec, HTTPMethod } from "@patchlogr/types";
2-
import type { Partition, PartitionedSpec } from "./partition";
1+
import type {
2+
CanonicalSpec,
3+
HTTPMethod,
4+
CanonicalOperation,
5+
} from "@patchlogr/types";
6+
import type { PartitionedSpec, HashNode } from "./partition";
37

48
import { createSHA256Hash } from "../utils/createHash";
59
import { stableStringify } from "../utils/stableStringify";
610

7-
export function partitionByMethod(spec: CanonicalSpec): PartitionedSpec {
8-
const partitions = new Map<HTTPMethod, Partition[]>();
11+
export function partitionByMethod(
12+
spec: CanonicalSpec,
13+
): PartitionedSpec<string, CanonicalOperation> {
14+
const methodGroups = new Map<
15+
HTTPMethod,
16+
Array<{ key: string; operation: CanonicalOperation }>
17+
>();
918

1019
Object.entries(spec.operations).forEach(([key, operation]) => {
11-
const hash = createSHA256Hash(stableStringify(operation));
20+
if (!methodGroups.has(operation.method)) {
21+
methodGroups.set(operation.method, []);
22+
}
23+
methodGroups.get(operation.method)?.push({
24+
key: key,
25+
operation,
26+
});
27+
});
28+
29+
const methodNodes: HashNode<string, CanonicalOperation>[] = [];
30+
31+
methodGroups.forEach((operations, method) => {
32+
const operationLeaves: HashNode<string, CanonicalOperation>[] =
33+
operations.map(({ key, operation }) => ({
34+
type: "leaf",
35+
key,
36+
hash: createSHA256Hash(stableStringify(operation)),
37+
value: operation,
38+
}));
39+
40+
const methodHash = createSHA256Hash(
41+
stableStringify(operationLeaves.map((leaf) => leaf.hash)),
42+
);
1243

13-
if (!partitions.has(operation.method))
14-
partitions.set(operation.method, [{ hash, operationKey: key }]);
15-
else
16-
partitions.get(operation.method)?.push({ hash, operationKey: key });
44+
methodNodes.push({
45+
type: "node",
46+
key: method,
47+
hash: methodHash,
48+
children: operationLeaves,
49+
});
1750
});
1851

52+
const rootHash = createSHA256Hash(
53+
stableStringify(methodNodes.map((node) => node.hash)),
54+
);
55+
56+
const root: HashNode<string, CanonicalOperation> = {
57+
type: "node",
58+
key: "root",
59+
hash: rootHash,
60+
children: methodNodes,
61+
};
62+
1963
return {
20-
hash: createSHA256Hash(stableStringify(spec)),
64+
root,
2165
metadata: {
2266
...spec.info,
2367
...spec.security,
2468
},
25-
partitions,
2669
};
2770
}
Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,29 +1,74 @@
1-
import type { CanonicalSpec } from "@patchlogr/types";
2-
import type { Partition, PartitionedSpec } from "./partition";
1+
import type {
2+
CanonicalSpec,
3+
CanonicalOperation,
4+
OperationKey,
5+
} from "@patchlogr/types";
6+
import type { PartitionedSpec, HashNode } from "./partition";
37

48
import { createSHA256Hash } from "../utils/createHash";
59
import { stableStringify } from "../utils/stableStringify";
610

711
export const DEFAULT_TAG = "__DEFAULT__";
812

9-
export function partitionByTag(spec: CanonicalSpec): PartitionedSpec {
10-
const partitions = new Map<string, Partition[]>();
13+
export function partitionByTag(
14+
spec: CanonicalSpec,
15+
): PartitionedSpec<string, CanonicalOperation> {
16+
const tagGroups = new Map<
17+
string,
18+
Array<{ key: string; operation: CanonicalOperation }>
19+
>();
1120

1221
Object.entries(spec.operations).forEach(([key, operation]) => {
1322
const tag = operation.doc?.tags?.[0] || DEFAULT_TAG;
14-
const hash = createSHA256Hash(stableStringify(operation));
1523

16-
if (!partitions.has(tag))
17-
partitions.set(tag, [{ hash, operationKey: key }]);
18-
else partitions.get(tag)?.push({ hash, operationKey: key });
24+
if (!tagGroups.has(tag)) {
25+
tagGroups.set(tag, []);
26+
}
27+
tagGroups.get(tag)?.push({
28+
key: key as OperationKey,
29+
operation,
30+
});
1931
});
2032

33+
const tagNodes: HashNode<string, CanonicalOperation>[] = [];
34+
35+
tagGroups.forEach((operations, tag) => {
36+
const operationLeaves: HashNode<string, CanonicalOperation>[] =
37+
operations.map(({ key, operation }) => ({
38+
type: "leaf",
39+
key,
40+
hash: createSHA256Hash(stableStringify(operation)),
41+
value: operation,
42+
}));
43+
44+
const tagHash = createSHA256Hash(
45+
stableStringify(operationLeaves.map((leaf) => leaf.hash)),
46+
);
47+
48+
tagNodes.push({
49+
type: "node",
50+
key: tag,
51+
hash: tagHash,
52+
children: operationLeaves,
53+
});
54+
});
55+
56+
const rootHash = createSHA256Hash(
57+
stableStringify(tagNodes.map((node) => node.hash)),
58+
);
59+
60+
const root: HashNode<string, CanonicalOperation> = {
61+
type: "node",
62+
key: "root",
63+
hash: rootHash,
64+
children: tagNodes,
65+
};
66+
2167
return {
22-
hash: createSHA256Hash(stableStringify(spec)),
68+
root,
2369
metadata: {
2470
...spec.info,
2571
...spec.security,
2672
},
27-
partitions,
2873
};
2974
}

packages/patchlogr-core/src/utils/stableStringify.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
export function stableStringify(obj: Record<string, unknown>): string {
1+
export function stableStringify(obj: any): string {
22
return JSON.stringify(sortObjectKeys(obj));
33
}
44

0 commit comments

Comments
 (0)