diff --git a/packages/patchlogr-core/src/index.ts b/packages/patchlogr-core/src/index.ts index e69de29..4cc1e96 100644 --- a/packages/patchlogr-core/src/index.ts +++ b/packages/patchlogr-core/src/index.ts @@ -0,0 +1 @@ +export * from "./partition"; diff --git a/packages/patchlogr-core/src/partition/__tests__/partitionByMethod.test.ts b/packages/patchlogr-core/src/partition/__tests__/partitionByMethod.test.ts index 294ebba..44bbeab 100644 --- a/packages/patchlogr-core/src/partition/__tests__/partitionByMethod.test.ts +++ b/packages/patchlogr-core/src/partition/__tests__/partitionByMethod.test.ts @@ -1,6 +1,7 @@ import type { CanonicalSpec } from "@patchlogr/types"; import { describe, expect, test } from "vitest"; import { partitionByMethod } from "../partitionByMethod"; +import type { HashInternalNode } from "../partition"; describe("partitionByMethod", () => { test("should group by HTTPMethod", () => { @@ -25,13 +26,22 @@ describe("partitionByMethod", () => { }, }; - const partitions = partitionByMethod(spec).partitions; - expect(partitions).toHaveLength(1); - expect(partitions.get("GET")).toHaveLength(2); - expect(partitions.get("GET")?.[0]?.operationKey).toBe("GET /user"); - expect(partitions.get("GET")?.[1]?.operationKey).toBe( - "GET /user/{userId}", - ); + const result = partitionByMethod(spec); + expect(result.root.type).toBe("node"); + expect(result.root.key).toBe("root"); + + const root = result.root as HashInternalNode; + expect(root.children).toHaveLength(1); + + const getMethodNode = root.children.find( + (child) => child.key === "GET", + ) as HashInternalNode; + + expect(getMethodNode).toBeDefined(); + expect(getMethodNode.type).toBe("node"); + expect(getMethodNode.children).toHaveLength(2); + expect(getMethodNode.children[0]?.key).toBe("GET /user"); + expect(getMethodNode.children[1]?.key).toBe("GET /user/{userId}"); }); test("should group by multiple HTTPMethods", () => { @@ -56,14 +66,26 @@ describe("partitionByMethod", () => { }, }; - const partitions = partitionByMethod(spec).partitions; + const result = partitionByMethod(spec); + expect(result.root.type).toBe("node"); + + const root = result.root as HashInternalNode; + expect(root.children).toHaveLength(2); + + const getMethodNode = root.children.find( + (child) => child.key === "GET", + ) as HashInternalNode; + const postMethodNode = root.children.find( + (child) => child.key === "POST", + ) as HashInternalNode; + + expect(getMethodNode).toBeDefined(); + expect(postMethodNode).toBeDefined(); + + expect(getMethodNode.children).toHaveLength(1); + expect(getMethodNode.children[0]?.key).toBe("GET /user"); - expect(partitions).toHaveLength(2); - expect(partitions.get("GET")).toHaveLength(1); - expect(partitions.get("POST")).toHaveLength(1); - expect(partitions.get("GET")?.[0]?.operationKey).toBe("GET /user"); - expect(partitions.get("POST")?.[0]?.operationKey).toBe( - "POST /auth/login", - ); + expect(postMethodNode.children).toHaveLength(1); + expect(postMethodNode.children[0]?.key).toBe("POST /auth/login"); }); }); diff --git a/packages/patchlogr-core/src/partition/__tests__/partitionByTag.test.ts b/packages/patchlogr-core/src/partition/__tests__/partitionByTag.test.ts index d546346..cb35c33 100644 --- a/packages/patchlogr-core/src/partition/__tests__/partitionByTag.test.ts +++ b/packages/patchlogr-core/src/partition/__tests__/partitionByTag.test.ts @@ -1,6 +1,7 @@ import type { CanonicalSpec } from "@patchlogr/types"; import { describe, expect, test } from "vitest"; import { DEFAULT_TAG, partitionByTag } from "../partitionByTag"; +import type { HashInternalNode } from "../partition"; describe("partitionByTag", () => { test("should group by first tag", () => { @@ -25,13 +26,21 @@ describe("partitionByTag", () => { }, }; - const partitions = partitionByTag(spec).partitions; - expect(partitions).toHaveLength(1); - expect(partitions.get("user")).toHaveLength(2); - expect(partitions.get("user")?.[0]?.operationKey).toBe("GET /user"); - expect(partitions.get("user")?.[1]?.operationKey).toBe( - "GET /user/{userId}", - ); + const result = partitionByTag(spec); + expect(result.root.type).toBe("node"); + expect(result.root.key).toBe("root"); + + const root = result.root as HashInternalNode; + expect(root.children).toHaveLength(1); + + const userTagNode = root.children.find( + (child) => child.key === "user", + ) as HashInternalNode; + + expect(userTagNode.type).toBe("node"); + expect(userTagNode.children).toHaveLength(2); + expect(userTagNode.children[0]?.key).toBe("GET /user"); + expect(userTagNode.children[1]?.key).toBe("GET /user/{userId}"); }); test("should group by multiple tags", () => { @@ -56,15 +65,24 @@ describe("partitionByTag", () => { }, }; - const partitions = partitionByTag(spec).partitions; + const result = partitionByTag(spec); + expect(result.root.type).toBe("node"); + + const root = result.root as HashInternalNode; + expect(root.children).toHaveLength(2); - expect(partitions).toHaveLength(2); - expect(partitions.get("user")).toHaveLength(1); - expect(partitions.get("auth")).toHaveLength(1); - expect(partitions.get("user")?.[0]?.operationKey).toBe("GET /user"); - expect(partitions.get("auth")?.[0]?.operationKey).toBe( - "POST /auth/login", - ); + const userTagNode = root.children.find( + (child) => child.key === "user", + ) as HashInternalNode; + const authTagNode = root.children.find( + (child) => child.key === "auth", + ) as HashInternalNode; + + expect(userTagNode.children).toHaveLength(1); + expect(userTagNode.children[0]?.key).toBe("GET /user"); + + expect(authTagNode.children).toHaveLength(1); + expect(authTagNode.children[0]?.key).toBe("POST /auth/login"); }); test("should group into default tag if tag not exists", () => { @@ -81,12 +99,18 @@ describe("partitionByTag", () => { }, }; - const partitions = partitionByTag(spec).partitions; + const result = partitionByTag(spec); + expect(result.root.type).toBe("node"); + + const root = result.root as HashInternalNode; + expect(root.children).toHaveLength(1); + + const defaultTagNode = root.children.find( + (child) => child.key === DEFAULT_TAG, + ) as HashInternalNode; - expect(partitions).toHaveLength(1); - expect(partitions.get(DEFAULT_TAG)).toHaveLength(1); - expect(partitions.get(DEFAULT_TAG)?.[0]?.operationKey).toBe( - "GET /user", - ); + expect(defaultTagNode.type).toBe("node"); + expect(defaultTagNode.children).toHaveLength(1); + expect(defaultTagNode.children[0]?.key).toBe("GET /user"); }); }); diff --git a/packages/patchlogr-core/src/partition/index.ts b/packages/patchlogr-core/src/partition/index.ts new file mode 100644 index 0000000..f70810c --- /dev/null +++ b/packages/patchlogr-core/src/partition/index.ts @@ -0,0 +1,4 @@ +export type { Hash, HashNode, PartitionedSpec } from "./partition"; + +export { partitionByMethod } from "./partitionByMethod"; +export { partitionByTag } from "./partitionByTag"; diff --git a/packages/patchlogr-core/src/partition/partition.ts b/packages/patchlogr-core/src/partition/partition.ts index 2fd755b..59bb6c1 100644 --- a/packages/patchlogr-core/src/partition/partition.ts +++ b/packages/patchlogr-core/src/partition/partition.ts @@ -1,10 +1,24 @@ -export type Partition = { - hash: string; - operationKey: string; +export type Hash = string; + +export type HashInternalNode = { + type: "node"; + key: K; + hash: Hash; + children: HashNode[]; +}; + +export type HashLeafNode = { + type: "leaf"; + key: K; + hash: Hash; + value: V; }; -export type PartitionedSpec = { - hash: string; +export type HashNode = + | HashInternalNode + | HashLeafNode; + +export type PartitionedSpec = { + root: HashNode; metadata: Record; - partitions: Map; }; diff --git a/packages/patchlogr-core/src/partition/partitionByMethod.ts b/packages/patchlogr-core/src/partition/partitionByMethod.ts index 8804bb0..a6f954b 100644 --- a/packages/patchlogr-core/src/partition/partitionByMethod.ts +++ b/packages/patchlogr-core/src/partition/partitionByMethod.ts @@ -1,27 +1,70 @@ -import type { CanonicalSpec, HTTPMethod } from "@patchlogr/types"; -import type { Partition, PartitionedSpec } from "./partition"; +import type { + CanonicalSpec, + HTTPMethod, + CanonicalOperation, +} from "@patchlogr/types"; +import type { PartitionedSpec, HashNode } from "./partition"; import { createSHA256Hash } from "../utils/createHash"; import { stableStringify } from "../utils/stableStringify"; -export function partitionByMethod(spec: CanonicalSpec): PartitionedSpec { - const partitions = new Map(); +export function partitionByMethod( + spec: CanonicalSpec, +): PartitionedSpec { + const methodGroups = new Map< + HTTPMethod, + Array<{ key: string; operation: CanonicalOperation }> + >(); Object.entries(spec.operations).forEach(([key, operation]) => { - const hash = createSHA256Hash(stableStringify(operation)); + if (!methodGroups.has(operation.method)) { + methodGroups.set(operation.method, []); + } + methodGroups.get(operation.method)?.push({ + key: key, + operation, + }); + }); + + const methodNodes: HashNode[] = []; + + methodGroups.forEach((operations, method) => { + const operationLeaves: HashNode[] = + operations.map(({ key, operation }) => ({ + type: "leaf", + key, + hash: createSHA256Hash(stableStringify(operation)), + value: operation, + })); + + const methodHash = createSHA256Hash( + stableStringify(operationLeaves.map((leaf) => leaf.hash)), + ); - if (!partitions.has(operation.method)) - partitions.set(operation.method, [{ hash, operationKey: key }]); - else - partitions.get(operation.method)?.push({ hash, operationKey: key }); + methodNodes.push({ + type: "node", + key: method, + hash: methodHash, + children: operationLeaves, + }); }); + const rootHash = createSHA256Hash( + stableStringify(methodNodes.map((node) => node.hash)), + ); + + const root: HashNode = { + type: "node", + key: "root", + hash: rootHash, + children: methodNodes, + }; + return { - hash: createSHA256Hash(stableStringify(spec)), + root, metadata: { ...spec.info, ...spec.security, }, - partitions, }; } diff --git a/packages/patchlogr-core/src/partition/partitionByTag.ts b/packages/patchlogr-core/src/partition/partitionByTag.ts index f8a9699..9989f37 100644 --- a/packages/patchlogr-core/src/partition/partitionByTag.ts +++ b/packages/patchlogr-core/src/partition/partitionByTag.ts @@ -1,29 +1,74 @@ -import type { CanonicalSpec } from "@patchlogr/types"; -import type { Partition, PartitionedSpec } from "./partition"; +import type { + CanonicalSpec, + CanonicalOperation, + OperationKey, +} from "@patchlogr/types"; +import type { PartitionedSpec, HashNode } from "./partition"; import { createSHA256Hash } from "../utils/createHash"; import { stableStringify } from "../utils/stableStringify"; export const DEFAULT_TAG = "__DEFAULT__"; -export function partitionByTag(spec: CanonicalSpec): PartitionedSpec { - const partitions = new Map(); +export function partitionByTag( + spec: CanonicalSpec, +): PartitionedSpec { + const tagGroups = new Map< + string, + Array<{ key: string; operation: CanonicalOperation }> + >(); Object.entries(spec.operations).forEach(([key, operation]) => { const tag = operation.doc?.tags?.[0] || DEFAULT_TAG; - const hash = createSHA256Hash(stableStringify(operation)); - if (!partitions.has(tag)) - partitions.set(tag, [{ hash, operationKey: key }]); - else partitions.get(tag)?.push({ hash, operationKey: key }); + if (!tagGroups.has(tag)) { + tagGroups.set(tag, []); + } + tagGroups.get(tag)?.push({ + key: key as OperationKey, + operation, + }); }); + const tagNodes: HashNode[] = []; + + tagGroups.forEach((operations, tag) => { + const operationLeaves: HashNode[] = + operations.map(({ key, operation }) => ({ + type: "leaf", + key, + hash: createSHA256Hash(stableStringify(operation)), + value: operation, + })); + + const tagHash = createSHA256Hash( + stableStringify(operationLeaves.map((leaf) => leaf.hash)), + ); + + tagNodes.push({ + type: "node", + key: tag, + hash: tagHash, + children: operationLeaves, + }); + }); + + const rootHash = createSHA256Hash( + stableStringify(tagNodes.map((node) => node.hash)), + ); + + const root: HashNode = { + type: "node", + key: "root", + hash: rootHash, + children: tagNodes, + }; + return { - hash: createSHA256Hash(stableStringify(spec)), + root, metadata: { ...spec.info, ...spec.security, }, - partitions, }; } diff --git a/packages/patchlogr-core/src/utils/stableStringify.ts b/packages/patchlogr-core/src/utils/stableStringify.ts index f9143b3..bd3a068 100644 --- a/packages/patchlogr-core/src/utils/stableStringify.ts +++ b/packages/patchlogr-core/src/utils/stableStringify.ts @@ -1,4 +1,4 @@ -export function stableStringify(obj: Record): string { +export function stableStringify(obj: any): string { return JSON.stringify(sortObjectKeys(obj)); }