Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions packages/patchlogr-core/src/diff/__tests__/detectVersionBump.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import { describe, test, expect } from "vitest";
import { detectVersionBump } from "../detectVersionBump.js";
import type { SpecChangeSet } from "../diffChangeSet.js";

describe("detectVersionBump", () => {
describe("none", () => {
test("변경사항이 없으면 none을 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "abc",
changes: [],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("none");
expect(result.isBreaking).toBe(false);
expect(result.reasons).toHaveLength(0);
});
});

describe("major (breaking changes)", () => {
test("removed 타입은 major를 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "removed",
path: [],
key: "GET /users",
baseHash: "hash1",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("major");
expect(result.isBreaking).toBe(true);
expect(result.reasons).toHaveLength(1);
});

test("type_changed는 major를 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "type_changed",
path: [],
key: "GET /users",
baseHash: "hash1",
headHash: "hash2",
baseNodeType: "leaf",
headNodeType: "node",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("major");
expect(result.isBreaking).toBe(true);
expect(result.reasons).toHaveLength(1);
});
});

describe("minor", () => {
test("added 타입은 minor를 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "added",
path: [],
key: "POST /users",
headHash: "hash1",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("minor");
expect(result.isBreaking).toBe(false);
expect(result.reasons).toHaveLength(1);
});

test("modified 타입은 minor를 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "modified",
path: [],
key: "GET /users",
baseHash: "hash1",
headHash: "hash2",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("minor");
expect(result.isBreaking).toBe(false);
expect(result.reasons).toHaveLength(1);
});
});

describe("patch", () => {
// TODO: Operation 내부 변경 분석 기능 추가 시 patch 케이스 개선 필요
// (현재는 알 수 없는 ChangeType만 patch로 분류됨)
test("알 수 없는 타입은 patch를 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "unknown" as any,
path: [],
key: "GET /users",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("patch");
expect(result.isBreaking).toBe(false);
expect(result.reasons).toHaveLength(1);
});
});

describe("version bump 우선순위", () => {
test("major > minor: major가 있으면 major를 반환한다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "added",
path: [],
key: "POST /users",
headHash: "hash1",
},
{
type: "removed",
path: [],
key: "DELETE /users",
baseHash: "hash2",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.recommendedBump).toBe("major");
expect(result.isBreaking).toBe(true);
expect(result.reasons).toHaveLength(2);
});

test("reasons에는 해당 레벨의 모든 변경 이유가 포함된다", () => {
const changeSet: SpecChangeSet = {
baseHash: "abc",
headHash: "def",
changes: [
{
type: "removed",
path: [],
key: "DELETE /users",
baseHash: "hash1",
},
{
type: "removed",
path: [],
key: "DELETE /posts",
baseHash: "hash2",
},
],
};

const result = detectVersionBump(changeSet);

expect(result.reasons).toHaveLength(2);
});
});
});
48 changes: 48 additions & 0 deletions packages/patchlogr-core/src/diff/classifyChange.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { VersionBump } from "./detectVersionBump";
import type { SpecChange } from "./diffChangeSet";

export type ChangeClassification = {
level: VersionBump;
reason: string;
};

export function classifyChange<K>(change: SpecChange<K>): ChangeClassification {
const pathStr = change.path.map(String).join(" > ");
const keyStr = String(change.key);

switch (change.type) {
case "removed":
// operation 삭제, 필드 삭제 등
return {
level: "major",
reason: `Removed: ${pathStr} > ${keyStr}`,
};

case "type_changed":
// node, leaf 타입 변경
return {
level: "major",
reason: `Type changed at: ${pathStr} > ${keyStr} (${change.baseNodeType} → ${change.headNodeType})`,
};

case "added":
// 새로운 operation, 필드 추가
return {
level: "minor",
reason: `Added: ${pathStr} > ${keyStr}`,
};

case "modified":
// TODO: required 추가 => major, description 변경 => patch ...
return {
level: "minor",
reason: `Modified: ${pathStr} > ${keyStr}`,
};

default:
return {
level: "patch",
reason: `Unknown change at: ${pathStr} > ${keyStr}`,
};
}
}
44 changes: 44 additions & 0 deletions packages/patchlogr-core/src/diff/detectVersionBump.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { classifyChange } from "./classifyChange.js";
import type { SpecChangeSet } from "./diffChangeSet.js";

export type VersionBump = "major" | "minor" | "patch" | "none";

export type VersionBumpResult = {
recommendedBump: VersionBump;
isBreaking: boolean;
reasons: string[];
};

export function detectVersionBump<K>(
changeSet: SpecChangeSet<K>,
): VersionBumpResult {
if (changeSet.changes.length === 0) {
return {
recommendedBump: "none",
isBreaking: false,
reasons: [],
};
}

const reasons: string[] = [];
let maxBump: VersionBump = "none";

for (const change of changeSet.changes) {
const bump = classifyChange(change);

if (bump.level === "major") {
maxBump = "major";
} else if (bump.level === "minor" && maxBump !== "major") {
maxBump = "minor";
} else if (bump.level === "patch" && maxBump === "none") {
maxBump = "patch";
}
reasons.push(bump.reason);
}

return {
recommendedBump: maxBump,
isBreaking: maxBump === "major",
reasons,
};
}
1 change: 1 addition & 0 deletions packages/patchlogr-core/src/diff/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ export * from "./diffLeafNodes";
export * from "./diffTypeChange";
export * from "./diffChildNodes";
export * from "./diffSpec";
export * from "./detectVersionBump";
4 changes: 2 additions & 2 deletions packages/patchlogr-core/src/partition/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export type { Hash, HashNode } from "./types/hashNode";
export type { PartitionedSpec } from "./types/partitionedSpec";
export type { Hash, HashNode } from "./types/HashNode";
export type { PartitionedSpec } from "./types/PartitionedSpec";

export { partitionByMethod } from "./partitionByMethod";
export { partitionByTag } from "./partitionByTag";
Expand Down
8 changes: 4 additions & 4 deletions packages/patchlogr-core/src/partition/partitionByMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,9 @@ import type {
CanonicalOperation,
} from "@patchlogr/types";

import type { HashNode } from "./types/hashNode";
import type { PartitionedSpec } from "./types/partitionedSpec";
import type { HashObject } from "./types/hashObject";
import type { HashNode } from "./types/HashNode";
import type { PartitionedSpec } from "./types/PartitionedSpec";
import type { HashObject } from "./types/HashObject";

import { createSHA256Hash } from "../utils/createHash";
import stableStringify from "fast-json-stable-stringify";
Expand Down Expand Up @@ -38,7 +38,7 @@ export function partitionByMethod(
const hash = createSHA256Hash(stableStringify(operation));
hashObjects.push({ hash, data: operation });
return {
type: "leaf" as const,
type: "leaf",
key,
hash,
};
Expand Down
6 changes: 3 additions & 3 deletions packages/patchlogr-core/src/partition/partitionByTag.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import type { CanonicalSpec, CanonicalOperation } from "@patchlogr/types";

import type { PartitionedSpec } from "./types/partitionedSpec";
import type { HashNode } from "./types/hashNode";
import type { HashObject } from "./types/hashObject";
import type { PartitionedSpec } from "./types/PartitionedSpec";
import type { HashNode } from "./types/HashNode";
import type { HashObject } from "./types/HashObject";

import { createSHA256Hash } from "../utils/createHash";
import stableStringify from "fast-json-stable-stringify";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { HashNode } from "./hashNode";
import type { HashObject } from "./hashObject";
import type { HashNode } from "./HashNode";
import type { HashObject } from "./HashObject";

export type PartitionedSpec<K = string, V = unknown> = {
root: HashNode<K, V>;
Expand Down
2 changes: 1 addition & 1 deletion packages/patchlogr-core/src/partition/utils/createNode.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { HashNode } from "../types/hashNode";
import type { HashNode } from "../types/HashNode";
import { createSHA256Hash } from "../../utils/createHash";
import stableStringify from "fast-json-stable-stringify";

Expand Down
10 changes: 10 additions & 0 deletions packages/patchlogr-core/src/storage/ContentAddressableStorage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { HashObject } from "../partition/types/HashObject";

export interface ContentAddressableStorage<T = string> {
get(hash: string): Promise<T | null>;

has(hash: string): Promise<boolean>;

put(entry: HashObject<T>): Promise<void>;
putMany(entries: HashObject<T>[]): Promise<void>;
}
Loading