@mandujs/core 0.4.3 → 0.5.1

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 CHANGED
@@ -1,41 +1,41 @@
1
- {
2
- "name": "@mandujs/core",
3
- "version": "0.4.3",
4
- "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
- "type": "module",
6
- "main": "./src/index.ts",
7
- "types": "./src/index.ts",
8
- "exports": {
9
- ".": "./src/index.ts",
10
- "./client": "./src/client/index.ts",
11
- "./*": "./src/*"
12
- },
13
- "files": [
14
- "src/**/*"
15
- ],
16
- "keywords": [
17
- "mandu",
18
- "framework",
19
- "agent",
20
- "ai",
21
- "code-generation"
22
- ],
23
- "repository": {
24
- "type": "git",
25
- "url": "https://github.com/konamgil/mandu.git",
26
- "directory": "packages/core"
27
- },
28
- "author": "konamgil",
29
- "license": "MIT",
30
- "publishConfig": {
31
- "access": "public"
32
- },
33
- "engines": {
34
- "bun": ">=1.0.0"
35
- },
36
- "peerDependencies": {
37
- "react": ">=18.0.0",
38
- "react-dom": ">=18.0.0",
39
- "zod": ">=3.0.0"
40
- }
41
- }
1
+ {
2
+ "name": "@mandujs/core",
3
+ "version": "0.5.1",
4
+ "description": "Mandu Framework Core - Spec, Generator, Guard, Runtime",
5
+ "type": "module",
6
+ "main": "./src/index.ts",
7
+ "types": "./src/index.ts",
8
+ "exports": {
9
+ ".": "./src/index.ts",
10
+ "./client": "./src/client/index.ts",
11
+ "./*": "./src/*"
12
+ },
13
+ "files": [
14
+ "src/**/*"
15
+ ],
16
+ "keywords": [
17
+ "mandu",
18
+ "framework",
19
+ "agent",
20
+ "ai",
21
+ "code-generation"
22
+ ],
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "https://github.com/konamgil/mandu.git",
26
+ "directory": "packages/core"
27
+ },
28
+ "author": "konamgil",
29
+ "license": "MIT",
30
+ "publishConfig": {
31
+ "access": "public"
32
+ },
33
+ "engines": {
34
+ "bun": ">=1.0.0"
35
+ },
36
+ "peerDependencies": {
37
+ "react": ">=18.0.0",
38
+ "react-dom": ">=18.0.0",
39
+ "zod": ">=3.0.0"
40
+ }
41
+ }
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Mandu Contract Module
3
+ * Contract-first API 정의 시스템
4
+ */
5
+
6
+ export * from "./schema";
7
+ export * from "./types";
8
+ export * from "./validator";
9
+
10
+ import type { ContractDefinition, ContractInstance } from "./schema";
11
+
12
+ /**
13
+ * Create a Mandu API Contract
14
+ *
15
+ * Contract-first 방식으로 API 스키마를 정의합니다.
16
+ * 정의된 스키마는 다음에 활용됩니다:
17
+ * - TypeScript 타입 추론 (Slot에서 자동 완성)
18
+ * - 런타임 요청/응답 검증
19
+ * - OpenAPI 문서 자동 생성
20
+ * - Guard의 Contract-Slot 일관성 검사
21
+ *
22
+ * @example
23
+ * ```typescript
24
+ * import { z } from "zod";
25
+ * import { Mandu } from "@mandujs/core";
26
+ *
27
+ * const UserSchema = z.object({
28
+ * id: z.string().uuid(),
29
+ * email: z.string().email(),
30
+ * name: z.string().min(2),
31
+ * });
32
+ *
33
+ * export default Mandu.contract({
34
+ * description: "사용자 관리 API",
35
+ * tags: ["users"],
36
+ *
37
+ * request: {
38
+ * GET: {
39
+ * query: z.object({
40
+ * page: z.coerce.number().default(1),
41
+ * limit: z.coerce.number().default(10),
42
+ * }),
43
+ * },
44
+ * POST: {
45
+ * body: UserSchema.omit({ id: true }),
46
+ * },
47
+ * },
48
+ *
49
+ * response: {
50
+ * 200: z.object({ data: z.array(UserSchema) }),
51
+ * 201: z.object({ data: UserSchema }),
52
+ * 400: z.object({ error: z.string() }),
53
+ * 404: z.object({ error: z.string() }),
54
+ * },
55
+ * });
56
+ * ```
57
+ */
58
+ export function createContract<T extends ContractDefinition>(definition: T): T & ContractInstance {
59
+ return {
60
+ ...definition,
61
+ _validated: false,
62
+ };
63
+ }
@@ -0,0 +1,110 @@
1
+ /**
2
+ * Mandu Contract Schema Types
3
+ * API 계약 정의를 위한 타입 시스템
4
+ */
5
+
6
+ import type { z } from "zod";
7
+
8
+ /** HTTP Methods supported in contracts */
9
+ export type ContractMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
10
+
11
+ /** Request schema for a specific HTTP method */
12
+ export interface MethodRequestSchema {
13
+ /** Query parameters schema */
14
+ query?: z.ZodTypeAny;
15
+ /** Request body schema */
16
+ body?: z.ZodTypeAny;
17
+ /** Path parameters schema (for nested routes) */
18
+ params?: z.ZodTypeAny;
19
+ /** Request headers schema */
20
+ headers?: z.ZodTypeAny;
21
+ }
22
+
23
+ /** Nested route schema (e.g., ":id" for /users/:id) */
24
+ export interface NestedRouteSchema extends MethodRequestSchema {
25
+ GET?: MethodRequestSchema;
26
+ POST?: MethodRequestSchema;
27
+ PUT?: MethodRequestSchema;
28
+ PATCH?: MethodRequestSchema;
29
+ DELETE?: MethodRequestSchema;
30
+ }
31
+
32
+ /** Request schemas by method */
33
+ export interface ContractRequestSchema {
34
+ GET?: MethodRequestSchema;
35
+ POST?: MethodRequestSchema;
36
+ PUT?: MethodRequestSchema;
37
+ PATCH?: MethodRequestSchema;
38
+ DELETE?: MethodRequestSchema;
39
+ /** Nested routes (e.g., ":id" for /users/:id) */
40
+ [key: string]: MethodRequestSchema | NestedRouteSchema | undefined;
41
+ }
42
+
43
+ /** Response schemas by status code */
44
+ export interface ContractResponseSchema {
45
+ 200?: z.ZodTypeAny;
46
+ 201?: z.ZodTypeAny;
47
+ 204?: z.ZodTypeAny;
48
+ 400?: z.ZodTypeAny;
49
+ 401?: z.ZodTypeAny;
50
+ 403?: z.ZodTypeAny;
51
+ 404?: z.ZodTypeAny;
52
+ 500?: z.ZodTypeAny;
53
+ [statusCode: number]: z.ZodTypeAny | undefined;
54
+ }
55
+
56
+ /** Full contract schema definition */
57
+ export interface ContractSchema {
58
+ /** API description */
59
+ description?: string;
60
+ /** Tags for grouping (e.g., OpenAPI tags) */
61
+ tags?: string[];
62
+ /** Request schemas by method */
63
+ request: ContractRequestSchema;
64
+ /** Response schemas by status code */
65
+ response: ContractResponseSchema;
66
+ }
67
+
68
+ /** Contract definition input (what user provides) */
69
+ export interface ContractDefinition {
70
+ description?: string;
71
+ tags?: string[];
72
+ request: ContractRequestSchema;
73
+ response: ContractResponseSchema;
74
+ }
75
+
76
+ /** Contract instance with metadata */
77
+ export interface ContractInstance extends ContractSchema {
78
+ /** Unique identifier (derived from route id) */
79
+ _id?: string;
80
+ /** Whether this contract is validated */
81
+ _validated?: boolean;
82
+ }
83
+
84
+ /** Validation error detail */
85
+ export interface ContractValidationIssue {
86
+ /** Field path (e.g., "body.email") */
87
+ path: (string | number)[];
88
+ /** Error message */
89
+ message: string;
90
+ /** Zod error code */
91
+ code: string;
92
+ }
93
+
94
+ /** Validation error by type */
95
+ export interface ContractValidationError {
96
+ /** Error type (query, body, params, headers, response) */
97
+ type: "query" | "body" | "params" | "headers" | "response";
98
+ /** Validation issues */
99
+ issues: ContractValidationIssue[];
100
+ }
101
+
102
+ /** Validation result */
103
+ export interface ContractValidationResult {
104
+ /** Whether validation succeeded */
105
+ success: boolean;
106
+ /** Validation errors if failed */
107
+ errors?: ContractValidationError[];
108
+ /** Parsed/transformed data if successful */
109
+ data?: unknown;
110
+ }
@@ -0,0 +1,134 @@
1
+ /**
2
+ * Mandu Contract Type Inference
3
+ * Contract 스키마에서 TypeScript 타입 추론
4
+ */
5
+
6
+ import type { z } from "zod";
7
+ import type {
8
+ ContractSchema,
9
+ ContractRequestSchema,
10
+ ContractResponseSchema,
11
+ MethodRequestSchema,
12
+ } from "./schema";
13
+
14
+ /**
15
+ * Extract inferred type from Zod schema
16
+ */
17
+ type InferZod<T> = T extends z.ZodTypeAny ? z.infer<T> : never;
18
+
19
+ /**
20
+ * Infer request schema types for a single method
21
+ */
22
+ type InferMethodRequest<T extends MethodRequestSchema | undefined> = T extends MethodRequestSchema
23
+ ? {
24
+ query: T["query"] extends z.ZodTypeAny ? z.infer<T["query"]> : undefined;
25
+ body: T["body"] extends z.ZodTypeAny ? z.infer<T["body"]> : undefined;
26
+ params: T["params"] extends z.ZodTypeAny ? z.infer<T["params"]> : undefined;
27
+ headers: T["headers"] extends z.ZodTypeAny ? z.infer<T["headers"]> : undefined;
28
+ }
29
+ : undefined;
30
+
31
+ /**
32
+ * Infer all request schemas
33
+ */
34
+ type InferContractRequest<T extends ContractRequestSchema> = {
35
+ [K in keyof T]: InferMethodRequest<T[K] extends MethodRequestSchema ? T[K] : undefined>;
36
+ };
37
+
38
+ /**
39
+ * Infer all response schemas
40
+ */
41
+ type InferContractResponse<T extends ContractResponseSchema> = {
42
+ [K in keyof T]: T[K] extends z.ZodTypeAny ? z.infer<T[K]> : never;
43
+ };
44
+
45
+ /**
46
+ * Infer full contract types
47
+ *
48
+ * @example
49
+ * ```typescript
50
+ * const contract = Mandu.contract({
51
+ * request: {
52
+ * GET: { query: z.object({ page: z.number() }) },
53
+ * POST: { body: z.object({ name: z.string() }) },
54
+ * },
55
+ * response: {
56
+ * 200: z.object({ data: z.array(z.string()) }),
57
+ * },
58
+ * });
59
+ *
60
+ * type Contract = InferContract<typeof contract>;
61
+ * // {
62
+ * // request: {
63
+ * // GET: { query: { page: number }, body: undefined, params: undefined, headers: undefined },
64
+ * // POST: { query: undefined, body: { name: string }, params: undefined, headers: undefined },
65
+ * // },
66
+ * // response: {
67
+ * // 200: { data: string[] },
68
+ * // },
69
+ * // }
70
+ * ```
71
+ */
72
+ export type InferContract<T extends ContractSchema> = {
73
+ request: InferContractRequest<T["request"]>;
74
+ response: InferContractResponse<T["response"]>;
75
+ description: T["description"];
76
+ tags: T["tags"];
77
+ };
78
+
79
+ /**
80
+ * Extract query type for a specific method
81
+ */
82
+ export type InferQuery<
83
+ T extends ContractSchema,
84
+ M extends keyof T["request"]
85
+ > = T["request"][M] extends MethodRequestSchema
86
+ ? T["request"][M]["query"] extends z.ZodTypeAny
87
+ ? z.infer<T["request"][M]["query"]>
88
+ : undefined
89
+ : undefined;
90
+
91
+ /**
92
+ * Extract body type for a specific method
93
+ */
94
+ export type InferBody<
95
+ T extends ContractSchema,
96
+ M extends keyof T["request"]
97
+ > = T["request"][M] extends MethodRequestSchema
98
+ ? T["request"][M]["body"] extends z.ZodTypeAny
99
+ ? z.infer<T["request"][M]["body"]>
100
+ : undefined
101
+ : undefined;
102
+
103
+ /**
104
+ * Extract params type for a specific method
105
+ */
106
+ export type InferParams<
107
+ T extends ContractSchema,
108
+ M extends keyof T["request"]
109
+ > = T["request"][M] extends MethodRequestSchema
110
+ ? T["request"][M]["params"] extends z.ZodTypeAny
111
+ ? z.infer<T["request"][M]["params"]>
112
+ : undefined
113
+ : undefined;
114
+
115
+ /**
116
+ * Extract response type for a specific status code
117
+ */
118
+ export type InferResponse<
119
+ T extends ContractSchema,
120
+ S extends keyof T["response"]
121
+ > = T["response"][S] extends z.ZodTypeAny ? z.infer<T["response"][S]> : never;
122
+
123
+ /**
124
+ * Helper type to get all defined methods in a contract
125
+ */
126
+ export type ContractMethods<T extends ContractSchema> = Extract<
127
+ keyof T["request"],
128
+ "GET" | "POST" | "PUT" | "PATCH" | "DELETE"
129
+ >;
130
+
131
+ /**
132
+ * Helper type to get all defined status codes in a contract
133
+ */
134
+ export type ContractStatusCodes<T extends ContractSchema> = keyof T["response"];
@@ -0,0 +1,257 @@
1
+ /**
2
+ * Mandu Contract Validator
3
+ * 런타임 요청/응답 검증
4
+ */
5
+
6
+ import type { z } from "zod";
7
+ import type {
8
+ ContractSchema,
9
+ ContractValidationResult,
10
+ ContractValidationError,
11
+ ContractValidationIssue,
12
+ MethodRequestSchema,
13
+ } from "./schema";
14
+
15
+ /**
16
+ * Parse query string from URL
17
+ */
18
+ function parseQueryString(url: string): Record<string, string | string[]> {
19
+ const result: Record<string, string | string[]> = {};
20
+ try {
21
+ const urlObj = new URL(url);
22
+ urlObj.searchParams.forEach((value, key) => {
23
+ const existing = result[key];
24
+ if (existing !== undefined) {
25
+ if (Array.isArray(existing)) {
26
+ existing.push(value);
27
+ } else {
28
+ result[key] = [existing, value];
29
+ }
30
+ } else {
31
+ result[key] = value;
32
+ }
33
+ });
34
+ } catch {
35
+ // Invalid URL, return empty object
36
+ }
37
+ return result;
38
+ }
39
+
40
+ /**
41
+ * Convert Zod error to validation issues
42
+ */
43
+ function zodErrorToIssues(error: z.ZodError): ContractValidationIssue[] {
44
+ return error.errors.map((issue) => ({
45
+ path: issue.path,
46
+ message: issue.message,
47
+ code: issue.code,
48
+ }));
49
+ }
50
+
51
+ /**
52
+ * Contract Validator
53
+ * Validates requests and responses against contract schemas
54
+ */
55
+ export class ContractValidator {
56
+ constructor(private contract: ContractSchema) {}
57
+
58
+ /**
59
+ * Validate incoming request against contract
60
+ * @param req - The incoming request
61
+ * @param method - HTTP method
62
+ * @param pathParams - Path parameters extracted by router
63
+ */
64
+ async validateRequest(
65
+ req: Request,
66
+ method: string,
67
+ pathParams: Record<string, string> = {}
68
+ ): Promise<ContractValidationResult> {
69
+ const methodSchema = this.contract.request[method] as MethodRequestSchema | undefined;
70
+ if (!methodSchema) {
71
+ // No schema defined for this method, pass through
72
+ return { success: true };
73
+ }
74
+
75
+ const errors: ContractValidationError[] = [];
76
+
77
+ // Validate query parameters
78
+ if (methodSchema.query) {
79
+ const query = parseQueryString(req.url);
80
+ const result = methodSchema.query.safeParse(query);
81
+ if (!result.success) {
82
+ errors.push({
83
+ type: "query",
84
+ issues: zodErrorToIssues(result.error),
85
+ });
86
+ }
87
+ }
88
+
89
+ // Validate path parameters
90
+ if (methodSchema.params) {
91
+ const result = methodSchema.params.safeParse(pathParams);
92
+ if (!result.success) {
93
+ errors.push({
94
+ type: "params",
95
+ issues: zodErrorToIssues(result.error),
96
+ });
97
+ }
98
+ }
99
+
100
+ // Validate headers
101
+ if (methodSchema.headers) {
102
+ const headers: Record<string, string> = {};
103
+ req.headers.forEach((value, key) => {
104
+ headers[key.toLowerCase()] = value;
105
+ });
106
+ const result = methodSchema.headers.safeParse(headers);
107
+ if (!result.success) {
108
+ errors.push({
109
+ type: "headers",
110
+ issues: zodErrorToIssues(result.error),
111
+ });
112
+ }
113
+ }
114
+
115
+ // Validate body (for methods that have body)
116
+ if (methodSchema.body) {
117
+ try {
118
+ const contentType = req.headers.get("content-type") || "";
119
+ let body: unknown;
120
+
121
+ if (contentType.includes("application/json")) {
122
+ body = await req.clone().json();
123
+ } else if (contentType.includes("application/x-www-form-urlencoded")) {
124
+ const formData = await req.clone().formData();
125
+ body = Object.fromEntries(formData.entries());
126
+ } else if (contentType.includes("multipart/form-data")) {
127
+ const formData = await req.clone().formData();
128
+ body = Object.fromEntries(formData.entries());
129
+ } else {
130
+ // Try JSON as default
131
+ try {
132
+ body = await req.clone().json();
133
+ } catch {
134
+ body = await req.clone().text();
135
+ }
136
+ }
137
+
138
+ const result = methodSchema.body.safeParse(body);
139
+ if (!result.success) {
140
+ errors.push({
141
+ type: "body",
142
+ issues: zodErrorToIssues(result.error),
143
+ });
144
+ }
145
+ } catch (error) {
146
+ errors.push({
147
+ type: "body",
148
+ issues: [
149
+ {
150
+ path: [],
151
+ message: `Failed to parse request body: ${error instanceof Error ? error.message : "Unknown error"}`,
152
+ code: "invalid_type",
153
+ },
154
+ ],
155
+ });
156
+ }
157
+ }
158
+
159
+ if (errors.length > 0) {
160
+ return { success: false, errors };
161
+ }
162
+
163
+ return { success: true };
164
+ }
165
+
166
+ /**
167
+ * Validate response against contract (development mode)
168
+ * @param responseBody - The response body (already parsed)
169
+ * @param statusCode - HTTP status code
170
+ */
171
+ validateResponse(responseBody: unknown, statusCode: number): ContractValidationResult {
172
+ const responseSchema = this.contract.response[statusCode];
173
+ if (!responseSchema) {
174
+ // No schema defined for this status code, pass through
175
+ return { success: true };
176
+ }
177
+
178
+ const result = responseSchema.safeParse(responseBody);
179
+ if (!result.success) {
180
+ return {
181
+ success: false,
182
+ errors: [
183
+ {
184
+ type: "response",
185
+ issues: zodErrorToIssues(result.error),
186
+ },
187
+ ],
188
+ };
189
+ }
190
+
191
+ return { success: true, data: result.data };
192
+ }
193
+
194
+ /**
195
+ * Get all defined methods in this contract
196
+ */
197
+ getMethods(): string[] {
198
+ return Object.keys(this.contract.request).filter((key) =>
199
+ ["GET", "POST", "PUT", "PATCH", "DELETE"].includes(key)
200
+ );
201
+ }
202
+
203
+ /**
204
+ * Get all defined status codes in this contract
205
+ */
206
+ getStatusCodes(): number[] {
207
+ return Object.keys(this.contract.response)
208
+ .map((k) => parseInt(k, 10))
209
+ .filter((n) => !isNaN(n));
210
+ }
211
+
212
+ /**
213
+ * Check if method has request schema
214
+ */
215
+ hasMethodSchema(method: string): boolean {
216
+ return !!this.contract.request[method];
217
+ }
218
+
219
+ /**
220
+ * Check if status code has response schema
221
+ */
222
+ hasResponseSchema(statusCode: number): boolean {
223
+ return !!this.contract.response[statusCode];
224
+ }
225
+
226
+ /**
227
+ * Get the underlying contract schema
228
+ */
229
+ getSchema(): ContractSchema {
230
+ return this.contract;
231
+ }
232
+ }
233
+
234
+ /**
235
+ * Format validation errors for HTTP response
236
+ */
237
+ export function formatValidationErrors(errors: ContractValidationError[]): {
238
+ error: string;
239
+ details: Array<{
240
+ type: string;
241
+ issues: Array<{
242
+ path: string;
243
+ message: string;
244
+ }>;
245
+ }>;
246
+ } {
247
+ return {
248
+ error: "Validation Error",
249
+ details: errors.map((e) => ({
250
+ type: e.type,
251
+ issues: e.issues.map((issue) => ({
252
+ path: issue.path.join(".") || "(root)",
253
+ message: issue.message,
254
+ })),
255
+ })),
256
+ };
257
+ }
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { ManduContext, NEXT_SYMBOL, ValidationError } from "./context";
7
7
  import { ErrorClassifier, formatErrorResponse, ErrorCode } from "../error";
8
+ import { createContract, type ContractDefinition, type ContractInstance } from "../contract";
8
9
 
9
10
  /** Handler function type */
10
11
  export type Handler = (ctx: ManduContext) => Response | Promise<Response>;
@@ -318,6 +319,55 @@ export const Mandu = {
318
319
  return new ManduFilling<TLoaderData>();
319
320
  },
320
321
 
322
+ /**
323
+ * Create an API contract (schema-first definition)
324
+ *
325
+ * Contract-first 방식으로 API 스키마를 정의합니다.
326
+ * 정의된 스키마는 다음에 활용됩니다:
327
+ * - TypeScript 타입 추론 (Slot에서 자동 완성)
328
+ * - 런타임 요청/응답 검증
329
+ * - OpenAPI 문서 자동 생성
330
+ * - Guard의 Contract-Slot 일관성 검사
331
+ *
332
+ * @example
333
+ * ```typescript
334
+ * import { z } from "zod";
335
+ * import { Mandu } from "@mandujs/core";
336
+ *
337
+ * const UserSchema = z.object({
338
+ * id: z.string().uuid(),
339
+ * email: z.string().email(),
340
+ * name: z.string().min(2),
341
+ * });
342
+ *
343
+ * export default Mandu.contract({
344
+ * description: "사용자 관리 API",
345
+ * tags: ["users"],
346
+ *
347
+ * request: {
348
+ * GET: {
349
+ * query: z.object({
350
+ * page: z.coerce.number().default(1),
351
+ * limit: z.coerce.number().default(10),
352
+ * }),
353
+ * },
354
+ * POST: {
355
+ * body: UserSchema.omit({ id: true }),
356
+ * },
357
+ * },
358
+ *
359
+ * response: {
360
+ * 200: z.object({ data: z.array(UserSchema) }),
361
+ * 201: z.object({ data: UserSchema }),
362
+ * 400: z.object({ error: z.string() }),
363
+ * },
364
+ * });
365
+ * ```
366
+ */
367
+ contract<T extends ContractDefinition>(definition: T): T & ContractInstance {
368
+ return createContract(definition);
369
+ },
370
+
321
371
  /**
322
372
  * Create context manually (for testing)
323
373
  */