Skip to content

Commit 5983aa9

Browse files
committed
Add config normalizer
1 parent c2e2fdb commit 5983aa9

File tree

2 files changed

+414
-0
lines changed

2 files changed

+414
-0
lines changed

lib/src/normalizer.test.ts

Lines changed: 237 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,237 @@
1+
import { describe, it, expect, vi } from "vitest";
2+
import { normalizeConfig } from "./normalizer";
3+
import { basicMatcher } from "./matcher";
4+
import type { Config } from "./types";
5+
6+
describe("normalizeConfig", () => {
7+
it("uses default strategy = merge when not provided", async () => {
8+
const result = await normalizeConfig({});
9+
expect(result.rules.default).toEqual(["merge"]);
10+
});
11+
12+
it("accepts single default strategy", async () => {
13+
const result = await normalizeConfig({ defaultStrategy: "replace" });
14+
expect(result.rules.default).toEqual(["replace"]);
15+
});
16+
17+
it("accepts multiple default strategies", async () => {
18+
const result = await normalizeConfig({ defaultStrategy: ["merge", "replace"] });
19+
expect(result.rules.default).toEqual(["merge", "replace"]);
20+
});
21+
22+
it("classifies rules from byStrategy into exactFields", async () => {
23+
const config: Config<any> = {
24+
byStrategy: {
25+
replace: ["foo", "bar!"],
26+
},
27+
};
28+
const result = await normalizeConfig(config);
29+
expect(result.rules.exactFields.foo[0].strategies).toEqual(["replace"]);
30+
expect(result.rules.exactFields.bar[0].important).toBe(true);
31+
});
32+
33+
it("classifies dotted keys as exact", async () => {
34+
const config: Config<any> = {
35+
byStrategy: {
36+
merge: ["a.b.c"],
37+
},
38+
};
39+
const result = await normalizeConfig(config);
40+
expect(result.rules.exact["a.b.c"]).toHaveLength(1);
41+
});
42+
43+
it("classifies wildcard keys as patterns", async () => {
44+
const config: Config<any> = {
45+
byStrategy: {
46+
merge: ["foo.*.bar"],
47+
},
48+
};
49+
const result = await normalizeConfig(config);
50+
expect(result.rules.patterns["foo.*.bar"]).toHaveLength(1);
51+
});
52+
53+
it("classifies bracket keys into patterns", async () => {
54+
const config: Config<any> = {
55+
byStrategy: {
56+
merge: ["[id]"],
57+
},
58+
};
59+
const result = await normalizeConfig(config);
60+
expect(result.rules.patterns["**.id.**"]).toHaveLength(1);
61+
});
62+
63+
it("throws on invalid bracket key", () => {
64+
const config: Config<any> = {
65+
byStrategy: {
66+
merge: ["[a.b]"],
67+
},
68+
};
69+
expect(() => normalizeConfig(config)).rejects.toThrow(/Invalid bracket form/);
70+
});
71+
72+
it("throws on empty rule key", () => {
73+
const config: Config<any> = {
74+
byStrategy: {
75+
merge: ["!"],
76+
},
77+
};
78+
expect(() => normalizeConfig(config)).rejects.toThrow(/Invalid rule key/);
79+
});
80+
81+
it("expands rules tree into exact entries", async () => {
82+
const config: Config<any> = {
83+
rules: {
84+
user: {
85+
name: ["replace"],
86+
profile: {
87+
age: ["merge"],
88+
},
89+
},
90+
},
91+
};
92+
const result = await normalizeConfig(config);
93+
expect(result.rules.exact["user.name"][0].strategies).toEqual(["replace"]);
94+
expect(result.rules.exact["user.profile.age"][0].strategies).toEqual(["merge"]);
95+
});
96+
97+
it("applies include/exclude defaults", async () => {
98+
const result = await normalizeConfig({});
99+
expect(result.include).toContain("**/*");
100+
expect(result.exclude).toContain("node_modules/**");
101+
});
102+
103+
it("uses provided include/exclude", async () => {
104+
const result = await normalizeConfig({
105+
include: ["src/**"],
106+
exclude: ["dist/**"],
107+
});
108+
expect(result.include).toEqual(["src/**"]);
109+
expect(result.exclude).toEqual(["dist/**"]);
110+
});
111+
112+
it("fileFilter includes allowed files and excludes others", async () => {
113+
const result = await normalizeConfig({
114+
include: ["src/**"],
115+
exclude: ["src/ignore/**"],
116+
matcher: "micromatch",
117+
});
118+
expect(result.fileFilter("src/index.ts")).toBe(true);
119+
expect(result.fileFilter("src/ignore/file.ts")).toBe(false);
120+
expect(result.fileFilter("other/file.ts")).toBe(false);
121+
});
122+
123+
it("fileFilter includes allowed files and excludes others", async () => {
124+
const result = await normalizeConfig({
125+
include: ["src/**"],
126+
exclude: ["src/ignore/**"],
127+
});
128+
expect(result.fileFilter("src/index.ts")).toBe(true);
129+
expect(result.fileFilter("src/ignore/file.ts")).toBe(false);
130+
expect(result.fileFilter("other/file.ts")).toBe(false);
131+
});
132+
133+
it("uses basicMatcher by default", async () => {
134+
const result = await normalizeConfig({});
135+
expect(result.matcher).toBe(basicMatcher);
136+
});
137+
138+
it("accepts custom matcher instance", async () => {
139+
const fakeMatcher = {
140+
isMatch: () => true,
141+
};
142+
const result = await normalizeConfig({ matcher: fakeMatcher });
143+
expect(result.matcher).toBe(fakeMatcher);
144+
});
145+
146+
it("throws if strategy name ends with '!'", async () => {
147+
const config: Config<any> = {
148+
byStrategy: {
149+
"merge!": ["foo"],
150+
},
151+
};
152+
await expect(normalizeConfig(config)).rejects.toThrow(/must not end with "!"/);
153+
});
154+
155+
it("throws on empty rule key '!'", async () => {
156+
const config: Config<any> = {
157+
byStrategy: {
158+
merge: ["!"],
159+
},
160+
};
161+
await expect(normalizeConfig(config)).rejects.toThrow(/Invalid rule key/);
162+
});
163+
164+
it("marks important from byStrategy rule with '!'", async () => {
165+
const config: Config<any> = {
166+
byStrategy: {
167+
replace: ["foo!"],
168+
},
169+
};
170+
const result = await normalizeConfig(config);
171+
expect(result.rules.exactFields.foo[0].important).toBe(true);
172+
});
173+
174+
it("marks important from rules tree key with '!'", async () => {
175+
const config: Config<any> = {
176+
rules: {
177+
user: {
178+
"id!": ["replace"],
179+
},
180+
},
181+
};
182+
const result = await normalizeConfig(config);
183+
expect(result.rules.exact["user.id"][0].important).toBe(true);
184+
});
185+
186+
it("throws on invalid bracket form with dot", async () => {
187+
const config: Config<any> = {
188+
byStrategy: {
189+
merge: ["[a.b]"],
190+
},
191+
};
192+
await expect(normalizeConfig(config)).rejects.toThrow(/Invalid bracket form/);
193+
});
194+
195+
it("throws on empty bracket form", async () => {
196+
const config: Config<any> = {
197+
byStrategy: {
198+
merge: ["[]"],
199+
},
200+
};
201+
await expect(normalizeConfig(config)).rejects.toThrow(/Invalid bracket form/);
202+
});
203+
204+
it("throws on invalid escaped dot in bracket form", async () => {
205+
const config: Config<any> = {
206+
byStrategy: {
207+
merge: ["[a.b]"],
208+
},
209+
};
210+
await expect(normalizeConfig(config)).rejects.toThrow(/Invalid bracket form/);
211+
});
212+
213+
it("expands deeply nested rule tree", async () => {
214+
const config: Config<any> = {
215+
rules: {
216+
root: {
217+
child: {
218+
leaf: ["merge"],
219+
},
220+
},
221+
},
222+
};
223+
const result = await normalizeConfig(config);
224+
expect(result.rules.exact["root.child.leaf"][0].strategies).toEqual(["merge"]);
225+
});
226+
227+
it("push appends to same key", async () => {
228+
const config: Config<any> = {
229+
byStrategy: {
230+
merge: ["dup"],
231+
replace: ["dup"],
232+
},
233+
};
234+
const result = await normalizeConfig(config);
235+
expect(result.rules.exactFields.dup.map(r => r.strategies[0])).toEqual(["merge", "replace"]);
236+
});
237+
});

0 commit comments

Comments
 (0)