Skip to content

Commit 89161fe

Browse files
authored
Merge pull request #26 from toothlessdev/25-detect-version-bump-for-api-changes
Canonical Spec 변경에 따른 version bump 와 reason 계산로직 구현
2 parents 52b51d5 + d6126e6 commit 89161fe

12 files changed

Lines changed: 306 additions & 12 deletions

File tree

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
import { describe, test, expect } from "vitest";
2+
import { detectVersionBump } from "../detectVersionBump.js";
3+
import type { SpecChangeSet } from "../diffChangeSet.js";
4+
5+
describe("detectVersionBump", () => {
6+
describe("none", () => {
7+
test("변경사항이 없으면 none을 반환한다", () => {
8+
const changeSet: SpecChangeSet = {
9+
baseHash: "abc",
10+
headHash: "abc",
11+
changes: [],
12+
};
13+
14+
const result = detectVersionBump(changeSet);
15+
16+
expect(result.recommendedBump).toBe("none");
17+
expect(result.isBreaking).toBe(false);
18+
expect(result.reasons).toHaveLength(0);
19+
});
20+
});
21+
22+
describe("major (breaking changes)", () => {
23+
test("removed 타입은 major를 반환한다", () => {
24+
const changeSet: SpecChangeSet = {
25+
baseHash: "abc",
26+
headHash: "def",
27+
changes: [
28+
{
29+
type: "removed",
30+
path: [],
31+
key: "GET /users",
32+
baseHash: "hash1",
33+
},
34+
],
35+
};
36+
37+
const result = detectVersionBump(changeSet);
38+
39+
expect(result.recommendedBump).toBe("major");
40+
expect(result.isBreaking).toBe(true);
41+
expect(result.reasons).toHaveLength(1);
42+
});
43+
44+
test("type_changed는 major를 반환한다", () => {
45+
const changeSet: SpecChangeSet = {
46+
baseHash: "abc",
47+
headHash: "def",
48+
changes: [
49+
{
50+
type: "type_changed",
51+
path: [],
52+
key: "GET /users",
53+
baseHash: "hash1",
54+
headHash: "hash2",
55+
baseNodeType: "leaf",
56+
headNodeType: "node",
57+
},
58+
],
59+
};
60+
61+
const result = detectVersionBump(changeSet);
62+
63+
expect(result.recommendedBump).toBe("major");
64+
expect(result.isBreaking).toBe(true);
65+
expect(result.reasons).toHaveLength(1);
66+
});
67+
});
68+
69+
describe("minor", () => {
70+
test("added 타입은 minor를 반환한다", () => {
71+
const changeSet: SpecChangeSet = {
72+
baseHash: "abc",
73+
headHash: "def",
74+
changes: [
75+
{
76+
type: "added",
77+
path: [],
78+
key: "POST /users",
79+
headHash: "hash1",
80+
},
81+
],
82+
};
83+
84+
const result = detectVersionBump(changeSet);
85+
86+
expect(result.recommendedBump).toBe("minor");
87+
expect(result.isBreaking).toBe(false);
88+
expect(result.reasons).toHaveLength(1);
89+
});
90+
91+
test("modified 타입은 minor를 반환한다", () => {
92+
const changeSet: SpecChangeSet = {
93+
baseHash: "abc",
94+
headHash: "def",
95+
changes: [
96+
{
97+
type: "modified",
98+
path: [],
99+
key: "GET /users",
100+
baseHash: "hash1",
101+
headHash: "hash2",
102+
},
103+
],
104+
};
105+
106+
const result = detectVersionBump(changeSet);
107+
108+
expect(result.recommendedBump).toBe("minor");
109+
expect(result.isBreaking).toBe(false);
110+
expect(result.reasons).toHaveLength(1);
111+
});
112+
});
113+
114+
describe("patch", () => {
115+
// TODO: Operation 내부 변경 분석 기능 추가 시 patch 케이스 개선 필요
116+
// (현재는 알 수 없는 ChangeType만 patch로 분류됨)
117+
test("알 수 없는 타입은 patch를 반환한다", () => {
118+
const changeSet: SpecChangeSet = {
119+
baseHash: "abc",
120+
headHash: "def",
121+
changes: [
122+
{
123+
type: "unknown" as any,
124+
path: [],
125+
key: "GET /users",
126+
},
127+
],
128+
};
129+
130+
const result = detectVersionBump(changeSet);
131+
132+
expect(result.recommendedBump).toBe("patch");
133+
expect(result.isBreaking).toBe(false);
134+
expect(result.reasons).toHaveLength(1);
135+
});
136+
});
137+
138+
describe("version bump 우선순위", () => {
139+
test("major > minor: major가 있으면 major를 반환한다", () => {
140+
const changeSet: SpecChangeSet = {
141+
baseHash: "abc",
142+
headHash: "def",
143+
changes: [
144+
{
145+
type: "added",
146+
path: [],
147+
key: "POST /users",
148+
headHash: "hash1",
149+
},
150+
{
151+
type: "removed",
152+
path: [],
153+
key: "DELETE /users",
154+
baseHash: "hash2",
155+
},
156+
],
157+
};
158+
159+
const result = detectVersionBump(changeSet);
160+
161+
expect(result.recommendedBump).toBe("major");
162+
expect(result.isBreaking).toBe(true);
163+
expect(result.reasons).toHaveLength(2);
164+
});
165+
166+
test("reasons에는 해당 레벨의 모든 변경 이유가 포함된다", () => {
167+
const changeSet: SpecChangeSet = {
168+
baseHash: "abc",
169+
headHash: "def",
170+
changes: [
171+
{
172+
type: "removed",
173+
path: [],
174+
key: "DELETE /users",
175+
baseHash: "hash1",
176+
},
177+
{
178+
type: "removed",
179+
path: [],
180+
key: "DELETE /posts",
181+
baseHash: "hash2",
182+
},
183+
],
184+
};
185+
186+
const result = detectVersionBump(changeSet);
187+
188+
expect(result.reasons).toHaveLength(2);
189+
});
190+
});
191+
});
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { VersionBump } from "./detectVersionBump";
2+
import type { SpecChange } from "./diffChangeSet";
3+
4+
export type ChangeClassification = {
5+
level: VersionBump;
6+
reason: string;
7+
};
8+
9+
export function classifyChange<K>(change: SpecChange<K>): ChangeClassification {
10+
const pathStr = change.path.map(String).join(" > ");
11+
const keyStr = String(change.key);
12+
13+
switch (change.type) {
14+
case "removed":
15+
// operation 삭제, 필드 삭제 등
16+
return {
17+
level: "major",
18+
reason: `Removed: ${pathStr} > ${keyStr}`,
19+
};
20+
21+
case "type_changed":
22+
// node, leaf 타입 변경
23+
return {
24+
level: "major",
25+
reason: `Type changed at: ${pathStr} > ${keyStr} (${change.baseNodeType}${change.headNodeType})`,
26+
};
27+
28+
case "added":
29+
// 새로운 operation, 필드 추가
30+
return {
31+
level: "minor",
32+
reason: `Added: ${pathStr} > ${keyStr}`,
33+
};
34+
35+
case "modified":
36+
// TODO: required 추가 => major, description 변경 => patch ...
37+
return {
38+
level: "minor",
39+
reason: `Modified: ${pathStr} > ${keyStr}`,
40+
};
41+
42+
default:
43+
return {
44+
level: "patch",
45+
reason: `Unknown change at: ${pathStr} > ${keyStr}`,
46+
};
47+
}
48+
}
Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
import { classifyChange } from "./classifyChange.js";
2+
import type { SpecChangeSet } from "./diffChangeSet.js";
3+
4+
export type VersionBump = "major" | "minor" | "patch" | "none";
5+
6+
export type VersionBumpResult = {
7+
recommendedBump: VersionBump;
8+
isBreaking: boolean;
9+
reasons: string[];
10+
};
11+
12+
export function detectVersionBump<K>(
13+
changeSet: SpecChangeSet<K>,
14+
): VersionBumpResult {
15+
if (changeSet.changes.length === 0) {
16+
return {
17+
recommendedBump: "none",
18+
isBreaking: false,
19+
reasons: [],
20+
};
21+
}
22+
23+
const reasons: string[] = [];
24+
let maxBump: VersionBump = "none";
25+
26+
for (const change of changeSet.changes) {
27+
const bump = classifyChange(change);
28+
29+
if (bump.level === "major") {
30+
maxBump = "major";
31+
} else if (bump.level === "minor" && maxBump !== "major") {
32+
maxBump = "minor";
33+
} else if (bump.level === "patch" && maxBump === "none") {
34+
maxBump = "patch";
35+
}
36+
reasons.push(bump.reason);
37+
}
38+
39+
return {
40+
recommendedBump: maxBump,
41+
isBreaking: maxBump === "major",
42+
reasons,
43+
};
44+
}

packages/patchlogr-core/src/diff/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@ export * from "./diffLeafNodes";
44
export * from "./diffTypeChange";
55
export * from "./diffChildNodes";
66
export * from "./diffSpec";
7+
export * from "./detectVersionBump";

packages/patchlogr-core/src/partition/index.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
export type { Hash, HashNode } from "./types/hashNode";
2-
export type { PartitionedSpec } from "./types/partitionedSpec";
1+
export type { Hash, HashNode } from "./types/HashNode";
2+
export type { PartitionedSpec } from "./types/PartitionedSpec";
33

44
export { partitionByMethod } from "./partitionByMethod";
55
export { partitionByTag } from "./partitionByTag";

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,9 @@ import type {
44
CanonicalOperation,
55
} from "@patchlogr/types";
66

7-
import type { HashNode } from "./types/hashNode";
8-
import type { PartitionedSpec } from "./types/partitionedSpec";
9-
import type { HashObject } from "./types/hashObject";
7+
import type { HashNode } from "./types/HashNode";
8+
import type { PartitionedSpec } from "./types/PartitionedSpec";
9+
import type { HashObject } from "./types/HashObject";
1010

1111
import { createSHA256Hash } from "../utils/createHash";
1212
import stableStringify from "fast-json-stable-stringify";
@@ -38,7 +38,7 @@ export function partitionByMethod(
3838
const hash = createSHA256Hash(stableStringify(operation));
3939
hashObjects.push({ hash, data: operation });
4040
return {
41-
type: "leaf" as const,
41+
type: "leaf",
4242
key,
4343
hash,
4444
};

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

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { CanonicalSpec, CanonicalOperation } from "@patchlogr/types";
22

3-
import type { PartitionedSpec } from "./types/partitionedSpec";
4-
import type { HashNode } from "./types/hashNode";
5-
import type { HashObject } from "./types/hashObject";
3+
import type { PartitionedSpec } from "./types/PartitionedSpec";
4+
import type { HashNode } from "./types/HashNode";
5+
import type { HashObject } from "./types/HashObject";
66

77
import { createSHA256Hash } from "../utils/createHash";
88
import stableStringify from "fast-json-stable-stringify";

packages/patchlogr-core/src/partition/types/hashNode.ts renamed to packages/patchlogr-core/src/partition/types/HashNode.ts

File renamed without changes.

packages/patchlogr-core/src/partition/types/hashObject.ts renamed to packages/patchlogr-core/src/partition/types/HashObject.ts

File renamed without changes.

packages/patchlogr-core/src/partition/types/partitionedSpec.ts renamed to packages/patchlogr-core/src/partition/types/PartitionedSpec.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
import type { HashNode } from "./hashNode";
2-
import type { HashObject } from "./hashObject";
1+
import type { HashNode } from "./HashNode";
2+
import type { HashObject } from "./HashObject";
33

44
export type PartitionedSpec<K = string, V = unknown> = {
55
root: HashNode<K, V>;

0 commit comments

Comments
 (0)