@mandujs/core 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +41 -41
- package/src/contract/index.ts +63 -0
- package/src/contract/schema.ts +110 -0
- package/src/contract/types.ts +134 -0
- package/src/contract/validator.ts +257 -0
- package/src/filling/filling.ts +50 -0
- package/src/generator/contract-glue.ts +285 -0
- package/src/generator/generate.ts +83 -0
- package/src/generator/index.ts +1 -0
- package/src/generator/templates.ts +79 -4
- package/src/guard/check.ts +5 -0
- package/src/guard/contract-guard.ts +221 -0
- package/src/guard/index.ts +1 -0
- package/src/guard/rules.ts +21 -0
- package/src/index.ts +2 -0
- package/src/openapi/generator.ts +480 -0
- package/src/openapi/index.ts +6 -0
- package/src/spec/schema.ts +3 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Mandu Contract Guard
|
|
3
|
+
* Contract-Slot 일관성 검사
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { RoutesManifest } from "../spec/schema";
|
|
7
|
+
import type { GuardViolation } from "./rules";
|
|
8
|
+
import path from "path";
|
|
9
|
+
import fs from "fs/promises";
|
|
10
|
+
|
|
11
|
+
async function fileExists(filePath: string): Promise<boolean> {
|
|
12
|
+
try {
|
|
13
|
+
await fs.access(filePath);
|
|
14
|
+
return true;
|
|
15
|
+
} catch {
|
|
16
|
+
return false;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
async function readFileContent(filePath: string): Promise<string | null> {
|
|
21
|
+
try {
|
|
22
|
+
return await Bun.file(filePath).text();
|
|
23
|
+
} catch {
|
|
24
|
+
return null;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Extract HTTP methods from contract file content
|
|
30
|
+
* Looks for patterns like: GET: {, POST: {, etc.
|
|
31
|
+
*/
|
|
32
|
+
function extractContractMethods(content: string): string[] {
|
|
33
|
+
const methods: string[] = [];
|
|
34
|
+
const methodPattern = /\b(GET|POST|PUT|PATCH|DELETE)\s*:\s*\{/g;
|
|
35
|
+
let match;
|
|
36
|
+
|
|
37
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
38
|
+
if (!methods.includes(match[1])) {
|
|
39
|
+
methods.push(match[1]);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return methods;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Extract HTTP methods from slot file content
|
|
48
|
+
* Looks for patterns like: .get(, .post(, etc.
|
|
49
|
+
*/
|
|
50
|
+
function extractSlotMethods(content: string): string[] {
|
|
51
|
+
const methods: string[] = [];
|
|
52
|
+
const methodPattern = /\.(get|post|put|patch|delete)\s*\(/gi;
|
|
53
|
+
let match;
|
|
54
|
+
|
|
55
|
+
while ((match = methodPattern.exec(content)) !== null) {
|
|
56
|
+
const method = match[1].toUpperCase();
|
|
57
|
+
if (!methods.includes(method)) {
|
|
58
|
+
methods.push(method);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
return methods;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Contract-Slot consistency violations
|
|
67
|
+
*/
|
|
68
|
+
export interface ContractViolation extends GuardViolation {
|
|
69
|
+
routeId: string;
|
|
70
|
+
contractPath?: string;
|
|
71
|
+
slotPath?: string;
|
|
72
|
+
missingMethods?: string[];
|
|
73
|
+
undocumentedMethods?: string[];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Check if API route has a contract defined
|
|
78
|
+
* Rule: API routes should have contracts for type safety
|
|
79
|
+
*/
|
|
80
|
+
export async function checkMissingContract(
|
|
81
|
+
manifest: RoutesManifest,
|
|
82
|
+
rootDir: string
|
|
83
|
+
): Promise<ContractViolation[]> {
|
|
84
|
+
const violations: ContractViolation[] = [];
|
|
85
|
+
|
|
86
|
+
for (const route of manifest.routes) {
|
|
87
|
+
// Only check API routes
|
|
88
|
+
if (route.kind !== "api") continue;
|
|
89
|
+
|
|
90
|
+
// Skip if no slot (simple routes don't need contracts)
|
|
91
|
+
if (!route.slotModule) continue;
|
|
92
|
+
|
|
93
|
+
// Check if contract is defined
|
|
94
|
+
if (!route.contractModule) {
|
|
95
|
+
violations.push({
|
|
96
|
+
ruleId: "CONTRACT_MISSING",
|
|
97
|
+
routeId: route.id,
|
|
98
|
+
file: route.slotModule,
|
|
99
|
+
message: `API 라우트 "${route.id}"에 contract가 정의되지 않았습니다`,
|
|
100
|
+
suggestion: `spec/contracts/${route.id}.contract.ts 파일을 생성하고 manifest에 contractModule을 추가하세요`,
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
return violations;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Check if contract file exists
|
|
110
|
+
*/
|
|
111
|
+
export async function checkContractFileExists(
|
|
112
|
+
manifest: RoutesManifest,
|
|
113
|
+
rootDir: string
|
|
114
|
+
): Promise<ContractViolation[]> {
|
|
115
|
+
const violations: ContractViolation[] = [];
|
|
116
|
+
|
|
117
|
+
for (const route of manifest.routes) {
|
|
118
|
+
if (route.contractModule) {
|
|
119
|
+
const contractPath = path.join(rootDir, route.contractModule);
|
|
120
|
+
const exists = await fileExists(contractPath);
|
|
121
|
+
|
|
122
|
+
if (!exists) {
|
|
123
|
+
violations.push({
|
|
124
|
+
ruleId: "CONTRACT_NOT_FOUND",
|
|
125
|
+
routeId: route.id,
|
|
126
|
+
file: route.contractModule,
|
|
127
|
+
contractPath: route.contractModule,
|
|
128
|
+
message: `Contract 파일을 찾을 수 없습니다 (routeId: ${route.id})`,
|
|
129
|
+
suggestion: "mandu generate를 실행하여 contract 파일을 생성하세요",
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return violations;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Check Contract-Slot method consistency
|
|
140
|
+
* - Contract에 정의된 메서드는 Slot에 구현되어야 함
|
|
141
|
+
* - Slot에 구현된 메서드는 Contract에 정의되어야 함
|
|
142
|
+
*/
|
|
143
|
+
export async function checkContractSlotConsistency(
|
|
144
|
+
manifest: RoutesManifest,
|
|
145
|
+
rootDir: string
|
|
146
|
+
): Promise<ContractViolation[]> {
|
|
147
|
+
const violations: ContractViolation[] = [];
|
|
148
|
+
|
|
149
|
+
for (const route of manifest.routes) {
|
|
150
|
+
// Need both contract and slot for consistency check
|
|
151
|
+
if (!route.contractModule || !route.slotModule) continue;
|
|
152
|
+
|
|
153
|
+
const contractPath = path.join(rootDir, route.contractModule);
|
|
154
|
+
const slotPath = path.join(rootDir, route.slotModule);
|
|
155
|
+
|
|
156
|
+
const contractContent = await readFileContent(contractPath);
|
|
157
|
+
const slotContent = await readFileContent(slotPath);
|
|
158
|
+
|
|
159
|
+
// Skip if files don't exist (other rules will catch this)
|
|
160
|
+
if (!contractContent || !slotContent) continue;
|
|
161
|
+
|
|
162
|
+
const contractMethods = extractContractMethods(contractContent);
|
|
163
|
+
const slotMethods = extractSlotMethods(slotContent);
|
|
164
|
+
|
|
165
|
+
// Check for methods in contract but not in slot
|
|
166
|
+
const missingInSlot = contractMethods.filter((m) => !slotMethods.includes(m));
|
|
167
|
+
if (missingInSlot.length > 0) {
|
|
168
|
+
violations.push({
|
|
169
|
+
ruleId: "CONTRACT_METHOD_NOT_IMPLEMENTED",
|
|
170
|
+
routeId: route.id,
|
|
171
|
+
file: route.slotModule,
|
|
172
|
+
contractPath: route.contractModule,
|
|
173
|
+
slotPath: route.slotModule,
|
|
174
|
+
missingMethods: missingInSlot,
|
|
175
|
+
message: `Contract에 정의된 메서드가 Slot에 구현되지 않았습니다: ${missingInSlot.join(", ")}`,
|
|
176
|
+
suggestion: `${route.slotModule}에 .${missingInSlot.map((m) => m.toLowerCase()).join("(), .")}() 핸들러를 추가하세요`,
|
|
177
|
+
});
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check for methods in slot but not in contract (warning)
|
|
181
|
+
const undocumented = slotMethods.filter((m) => !contractMethods.includes(m));
|
|
182
|
+
if (undocumented.length > 0) {
|
|
183
|
+
violations.push({
|
|
184
|
+
ruleId: "CONTRACT_METHOD_UNDOCUMENTED",
|
|
185
|
+
routeId: route.id,
|
|
186
|
+
file: route.contractModule,
|
|
187
|
+
contractPath: route.contractModule,
|
|
188
|
+
slotPath: route.slotModule,
|
|
189
|
+
undocumentedMethods: undocumented,
|
|
190
|
+
message: `Slot에 구현된 메서드가 Contract에 문서화되지 않았습니다: ${undocumented.join(", ")}`,
|
|
191
|
+
suggestion: `${route.contractModule}에 ${undocumented.join(", ")} 스키마를 추가하세요`,
|
|
192
|
+
});
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return violations;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Run all contract-related guard checks
|
|
201
|
+
*/
|
|
202
|
+
export async function runContractGuardCheck(
|
|
203
|
+
manifest: RoutesManifest,
|
|
204
|
+
rootDir: string
|
|
205
|
+
): Promise<ContractViolation[]> {
|
|
206
|
+
const violations: ContractViolation[] = [];
|
|
207
|
+
|
|
208
|
+
// Check missing contracts (warning level)
|
|
209
|
+
// const missingContracts = await checkMissingContract(manifest, rootDir);
|
|
210
|
+
// violations.push(...missingContracts);
|
|
211
|
+
|
|
212
|
+
// Check contract file exists
|
|
213
|
+
const notFoundContracts = await checkContractFileExists(manifest, rootDir);
|
|
214
|
+
violations.push(...notFoundContracts);
|
|
215
|
+
|
|
216
|
+
// Check contract-slot consistency
|
|
217
|
+
const consistencyViolations = await checkContractSlotConsistency(manifest, rootDir);
|
|
218
|
+
violations.push(...consistencyViolations);
|
|
219
|
+
|
|
220
|
+
return violations;
|
|
221
|
+
}
|
package/src/guard/index.ts
CHANGED
package/src/guard/rules.ts
CHANGED
|
@@ -37,6 +37,27 @@ export const GUARD_RULES: Record<string, GuardRule> = {
|
|
|
37
37
|
name: "Slot File Not Found",
|
|
38
38
|
description: "spec에 명시된 slotModule 파일을 찾을 수 없습니다",
|
|
39
39
|
},
|
|
40
|
+
// Contract-related rules
|
|
41
|
+
CONTRACT_MISSING: {
|
|
42
|
+
id: "CONTRACT_MISSING",
|
|
43
|
+
name: "Contract Missing",
|
|
44
|
+
description: "API 라우트에 contract가 정의되지 않았습니다",
|
|
45
|
+
},
|
|
46
|
+
CONTRACT_NOT_FOUND: {
|
|
47
|
+
id: "CONTRACT_NOT_FOUND",
|
|
48
|
+
name: "Contract File Not Found",
|
|
49
|
+
description: "spec에 명시된 contractModule 파일을 찾을 수 없습니다",
|
|
50
|
+
},
|
|
51
|
+
CONTRACT_METHOD_NOT_IMPLEMENTED: {
|
|
52
|
+
id: "CONTRACT_METHOD_NOT_IMPLEMENTED",
|
|
53
|
+
name: "Contract Method Not Implemented",
|
|
54
|
+
description: "Contract에 정의된 메서드가 Slot에 구현되지 않았습니다",
|
|
55
|
+
},
|
|
56
|
+
CONTRACT_METHOD_UNDOCUMENTED: {
|
|
57
|
+
id: "CONTRACT_METHOD_UNDOCUMENTED",
|
|
58
|
+
name: "Contract Method Undocumented",
|
|
59
|
+
description: "Slot에 구현된 메서드가 Contract에 문서화되지 않았습니다",
|
|
60
|
+
},
|
|
40
61
|
};
|
|
41
62
|
|
|
42
63
|
export const FORBIDDEN_IMPORTS = ["fs", "child_process", "cluster", "worker_threads"];
|