1use crate::error::Result;
2use crate::templates;
3use rohas_parser::{Api, Event, FieldType, Model, Schema, WebSocket};
4use std::fs;
5use std::path::Path;
6
7pub fn generate_models(schema: &Schema, output_dir: &Path) -> Result<()> {
8 let models_dir = output_dir.join("generated/models");
9
10 for model in &schema.models {
11 let content = generate_model_content(model);
12 let file_name = format!("{}.ts", templates::to_snake_case(&model.name));
13 fs::write(models_dir.join(file_name), content)?;
14 }
15
16 Ok(())
17}
18
19fn generate_model_content(model: &Model) -> String {
20 let mut content = String::new();
21
22 content.push_str("import { z } from 'zod';\n\n");
23
24 content.push_str(&format!("export interface {} {{\n", model.name));
25
26 for field in &model.fields {
27 let ts_type = field.field_type.to_typescript();
28 let optional = if field.optional { "?" } else { "" };
29 content.push_str(&format!(" {}{}: {};\n", field.name, optional, ts_type));
30 }
31
32 content.push_str("}\n\n");
33
34 content.push_str(&format!(
36 "export const {}Schema = z.object({{\n",
37 model.name
38 ));
39 for field in &model.fields {
40 let zod_type = field_type_to_zod(&field.field_type, field.optional);
41 content.push_str(&format!(" {}: {},\n", field.name, zod_type));
42 }
43 content.push_str("});\n\n");
44
45 content.push_str(&format!(
46 "export function is{}(obj: any): obj is {} {{\n",
47 model.name, model.name
48 ));
49 content.push_str(&format!(
50 " return {}Schema.safeParse(obj).success;\n",
51 model.name
52 ));
53 content.push_str("}\n");
54
55 content
56}
57
58fn field_type_to_zod(field_type: &rohas_parser::FieldType, optional: bool) -> String {
59 use rohas_parser::FieldType;
60
61 let zod_type = match field_type {
62 FieldType::Int | FieldType::Float => "z.number()".to_string(),
63 FieldType::String => "z.string()".to_string(),
64 FieldType::Boolean => "z.boolean()".to_string(),
65 FieldType::DateTime => "z.date()".to_string(),
66 FieldType::Json => "z.any()".to_string(),
67 FieldType::Custom(name) => format!("{}Schema", name),
68 FieldType::Array(inner) => {
69 let inner_zod = field_type_to_zod(inner, false);
70 format!("z.array({})", inner_zod)
71 }
72 };
73
74 if optional {
75 format!("{}.optional()", zod_type)
76 } else {
77 zod_type
78 }
79}
80
81pub fn generate_dtos(schema: &Schema, output_dir: &Path) -> Result<()> {
82 let dto_dir = output_dir.join("generated/dto");
83
84 for input in &schema.inputs {
85 let content = generate_model_content(&rohas_parser::Model {
86 name: input.name.clone(),
87 fields: input.fields.clone(),
88 attributes: vec![],
89 });
90 let file_name = format!("{}.ts", templates::to_snake_case(&input.name));
91 fs::write(dto_dir.join(file_name), content)?;
92 }
93
94 Ok(())
95}
96
97pub fn generate_apis(schema: &Schema, output_dir: &Path) -> Result<()> {
98 let api_dir = output_dir.join("generated/api");
99
100 for api in &schema.apis {
101 let content = generate_api_content(api);
102 let file_name = format!("{}.ts", templates::to_snake_case(&api.name));
103 fs::write(api_dir.join(file_name), content)?;
104 }
105
106 let handlers_dir = output_dir.join("handlers/api");
107 for api in &schema.apis {
108 let file_name = format!("{}.ts", &api.name);
109 let handler_path = handlers_dir.join(&file_name);
110
111 if !handler_path.exists() {
112 let content = generate_api_handler_stub(api);
113 fs::write(handler_path, content)?;
114 }
115 }
116
117 Ok(())
118}
119
120fn generate_api_content(api: &Api) -> String {
121 let mut content = String::new();
122
123 content.push_str("import { z } from 'zod';\n");
124
125 let request_type = format!("{}Request", api.name);
126 let response_type = format!("{}Response", api.name);
127 let handler_type = format!("{}Handler", api.name);
128
129 let response_is_primitive = is_primitive_type(&api.response);
130
131 if !response_is_primitive {
132 content.push_str(&format!(
133 "import {{ {}, {}Schema }} from '@generated/models/{}';\n",
134 api.response,
135 api.response,
136 templates::to_snake_case(&api.response)
137 ));
138 }
139
140 if let Some(body) = &api.body {
141 let body_is_primitive = is_primitive_type(body);
142 if !body_is_primitive {
143 if body.ends_with("Input") {
144 content.push_str(&format!(
145 "import {{ {}, {}Schema }} from '@generated/dto/{}';\n",
146 body,
147 body,
148 templates::to_snake_case(body)
149 ));
150 } else {
151 content.push_str(&format!(
152 "import {{ {}, {}Schema }} from '@generated/models/{}';\n",
153 body,
154 body,
155 templates::to_snake_case(body)
156 ));
157 }
158 }
159 }
160
161 if !content.is_empty() {
162 content.push_str("\n");
163 }
164
165 let path_params = extract_path_params(&api.path);
166
167 content.push_str(&format!("export interface {} {{\n", request_type));
168
169 for param in &path_params {
170 content.push_str(&format!(" {}: string;\n", param));
171 }
172
173 if let Some(body) = &api.body {
174 let ts_type = if is_primitive_type(body) {
175 primitive_to_typescript(body)
176 } else {
177 body.to_string()
178 };
179 content.push_str(&format!(" body: {};\n", ts_type));
180 }
181
182 content.push_str(" queryParams?: Record<string, string>;\n");
183
184 content.push_str("}\n\n");
185
186 content.push_str(&format!(
188 "export const {}Schema = z.object({{\n",
189 request_type
190 ));
191 for param in &path_params {
192 content.push_str(&format!(" {}: z.string(),\n", param));
193 }
194 if let Some(body) = &api.body {
195 let body_is_primitive = is_primitive_type(body);
196 if body_is_primitive {
197 let zod_type = match body.as_str() {
198 "String" => "z.string()",
199 "Int" | "Float" => "z.number()",
200 "Boolean" => "z.boolean()",
201 "DateTime" | "Date" => "z.date()",
202 _ => "z.any()",
203 };
204 content.push_str(&format!(" body: {},\n", zod_type));
205 } else {
206 if body.ends_with("Input") {
207 content.push_str(&format!(" body: {}Schema,\n", body));
208 } else {
209 content.push_str(&format!(" body: {}Schema,\n", body));
210 }
211 }
212 }
213 content.push_str(" queryParams: z.record(z.string()).optional(),\n");
214 content.push_str("});\n\n");
215
216 let response_ts_type = if response_is_primitive {
217 primitive_to_typescript(&api.response)
218 } else {
219 api.response.clone()
220 };
221
222 content.push_str(&format!("export interface {} {{\n", response_type));
223 content.push_str(&format!(" data: {};\n", response_ts_type));
224 content.push_str("}\n\n");
225
226 let response_zod_type = if response_is_primitive {
228 match api.response.as_str() {
229 "String" => "z.string()".to_string(),
230 "Int" | "Float" => "z.number()".to_string(),
231 "Boolean" => "z.boolean()".to_string(),
232 "DateTime" | "Date" => "z.date()".to_string(),
233 _ => "z.any()".to_string(),
234 }
235 } else {
236 format!("{}Schema", api.response)
237 };
238 content.push_str(&format!(
239 "export const {}Schema = z.object({{\n",
240 response_type
241 ));
242 content.push_str(&format!(" data: {},\n", response_zod_type));
243 content.push_str("});\n\n");
244
245 content.push_str(&format!(
246 "export type {} = (req: {}) => Promise<{}>;\n",
247 handler_type, request_type, response_type
248 ));
249
250 content
251}
252
253fn is_primitive_type(type_name: &str) -> bool {
254 matches!(
255 type_name,
256 "String" | "Int" | "Float" | "Boolean" | "DateTime" | "Date"
257 )
258}
259
260fn primitive_to_typescript(type_name: &str) -> String {
261 match type_name {
262 "String" => "string".to_string(),
263 "Int" | "Float" => "number".to_string(),
264 "Boolean" => "boolean".to_string(),
265 "DateTime" | "Date" => "Date".to_string(),
266 _ => type_name.to_string(),
267 }
268}
269
270fn extract_path_params(path: &str) -> Vec<String> {
274 let mut params = Vec::new();
275 let mut in_param = false;
276 let mut current_param = String::new();
277
278 for ch in path.chars() {
279 match ch {
280 '{' => {
281 in_param = true;
282 current_param.clear();
283 }
284 '}' => {
285 if in_param && !current_param.is_empty() {
286 params.push(current_param.clone());
287 }
288 in_param = false;
289 }
290 _ if in_param => {
291 current_param.push(ch);
292 }
293 _ => {}
294 }
295 }
296
297 params
298}
299
300fn generate_api_handler_stub(api: &Api) -> String {
301 let mut content = String::new();
302
303 let request_type = format!("{}Request", api.name);
304 let response_type = format!("{}Response", api.name);
305 let handler_name = format!("handle{}", api.name);
306
307 content.push_str(&format!(
308 "import {{ {}, {} }} from '@generated/api/{}';\n",
309 request_type,
310 response_type,
311 templates::to_snake_case(&api.name)
312 ));
313 content.push_str("import { State } from '@generated/state';\n\n");
314
315 content.push_str(&format!(
316 "export async function {}(req: {}, state: State): Promise<{}> {{\n",
317 handler_name, request_type, response_type
318 ));
319 content.push_str(" // TODO: Implement handler logic\n");
320 content.push_str(" // For auto-triggers (defined in schema triggers): use state.setPayload('EventName', {...})\n");
321 content.push_str(" // For manual triggers: use state.triggerEvent('EventName', {...})\n");
322 content.push_str(" throw new Error('Not implemented');\n");
323 content.push_str("}\n");
324
325 content
326}
327
328pub fn generate_events(schema: &Schema, output_dir: &Path) -> Result<()> {
329 let events_dir = output_dir.join("generated/events");
330
331 for event in &schema.events {
332 let content = generate_event_content(event);
333 let file_name = format!("{}.ts", templates::to_snake_case(&event.name));
334 fs::write(events_dir.join(file_name), content)?;
335 }
336
337 let handlers_dir = output_dir.join("handlers/events");
339 for event in &schema.events {
340 for handler in &event.handlers {
341 let file_name = format!("{}.ts", handler);
342 let handler_path = handlers_dir.join(&file_name);
343
344 if !handler_path.exists() {
345 let content = generate_event_handler_stub(event, handler);
346 fs::write(handler_path, content)?;
347 }
348 }
349 }
350
351 Ok(())
352}
353
354fn payload_type_to_zod(type_name: &str) -> String {
355 match type_name {
356 "String" => "z.string()".to_string(),
357 "Int" | "Float" => "z.number()".to_string(),
358 "Boolean" | "Bool" => "z.boolean()".to_string(),
359 "DateTime" | "Date" => "z.date()".to_string(),
360 _ => format!("{}Schema", type_name),
361 }
362}
363
364fn generate_event_content(event: &Event) -> String {
365 let mut content = String::new();
366
367 content.push_str("import { z } from 'zod';\n");
368
369 let payload_is_primitive = is_primitive_type(&event.payload);
370
371 if payload_is_primitive {
372 content.push_str("\n");
373 } else {
374 content.push_str(&format!(
375 "import {{ {}, {}Schema }} from '@generated/models/{}';\n\n",
376 event.payload,
377 event.payload,
378 templates::to_snake_case(&event.payload)
379 ));
380 }
381
382 let payload_ts_type = if payload_is_primitive {
383 primitive_to_typescript(&event.payload)
384 } else {
385 event.payload.clone()
386 };
387
388 content.push_str(&format!("export interface {} {{\n", event.name));
389 content.push_str(&format!(" payload: {};\n", payload_ts_type));
390 content.push_str(" timestamp: Date;\n");
391 content.push_str("}\n\n");
392
393 let payload_zod_type = payload_type_to_zod(&event.payload);
395 content.push_str(&format!(
396 "export const {}Schema = z.object({{\n",
397 event.name
398 ));
399 content.push_str(&format!(" payload: {},\n", payload_zod_type));
400 content.push_str(" timestamp: z.date(),\n");
401 content.push_str("});\n\n");
402
403 content.push_str(&format!(
404 "export type {}Handler = (event: {}) => Promise<void>;\n",
405 event.name, event.name
406 ));
407
408 content
409}
410
411fn generate_event_handler_stub(event: &Event, handler_name: &str) -> String {
412 let mut content = String::new();
413
414 content.push_str(&format!(
415 "import {{ {} }} from '@generated/events/{}';\n\n",
416 event.name,
417 templates::to_snake_case(&event.name)
418 ));
419
420 content.push_str(&format!(
421 "export async function {}(event: {}): Promise<void> {{\n",
422 handler_name, event.name
423 ));
424 content.push_str(" // TODO: Implement event handler\n");
425 content.push_str(&format!(" console.log('Handling event:', event);\n"));
426 content.push_str("}\n");
427
428 content
429}
430
431pub fn generate_crons(schema: &Schema, output_dir: &Path) -> Result<()> {
432 let cron_dir = output_dir.join("generated/cron");
433
434 for cron in &schema.crons {
435 let content = format!(
436 "export interface {} {{\n schedule: string;\n}}\n",
437 cron.name
438 );
439 let file_name = format!("{}.ts", templates::to_snake_case(&cron.name));
440 fs::write(cron_dir.join(file_name), content)?;
441 }
442
443 let handlers_dir = output_dir.join("handlers/cron");
445 for cron in &schema.crons {
446 let file_name = format!("{}.ts", templates::to_snake_case(&cron.name));
447 let handler_path = handlers_dir.join(&file_name);
448
449 if !handler_path.exists() {
450 let content = format!(
451 "export async function handle{}(): Promise<void> {{\n // TODO: Implement cron job\n console.log('Running cron: {}');\n}}\n",
452 cron.name, cron.name
453 );
454 fs::write(handler_path, content)?;
455 }
456 }
457
458 Ok(())
459}
460
461pub fn generate_websockets(schema: &Schema, output_dir: &Path) -> Result<()> {
462 let ws_dir = output_dir.join("generated/websockets");
463
464 for ws in &schema.websockets {
465 let content = generate_websocket_content(ws);
466 let file_name = format!("{}.ts", templates::to_snake_case(&ws.name));
467 fs::write(ws_dir.join(file_name), content)?;
468 }
469
470 let handlers_dir = output_dir.join("handlers/websockets");
471 for ws in &schema.websockets {
472 if !ws.on_connect.is_empty() {
473 for handler in &ws.on_connect {
474 let file_name = format!("{}.ts", handler);
475 let handler_path = handlers_dir.join(&file_name);
476 if !handler_path.exists() {
477 let content = generate_websocket_handler_stub(ws, "onConnect", handler);
478 fs::write(handler_path, content)?;
479 }
480 }
481 }
482 if !ws.on_message.is_empty() {
483 for handler in &ws.on_message {
484 let file_name = format!("{}.ts", handler);
485 let handler_path = handlers_dir.join(&file_name);
486 if !handler_path.exists() {
487 let content = generate_websocket_handler_stub(ws, "onMessage", handler);
488 fs::write(handler_path, content)?;
489 }
490 }
491 }
492 if !ws.on_disconnect.is_empty() {
493 for handler in &ws.on_disconnect {
494 let file_name = format!("{}.ts", handler);
495 let handler_path = handlers_dir.join(&file_name);
496 if !handler_path.exists() {
497 let content = generate_websocket_handler_stub(ws, "onDisconnect", handler);
498 fs::write(handler_path, content)?;
499 }
500 }
501 }
502 }
503
504 Ok(())
505}
506
507pub fn generate_middlewares(schema: &Schema, output_dir: &Path) -> Result<()> {
508 use std::collections::HashSet;
509
510 let mut middleware_names = HashSet::new();
511
512 for api in &schema.apis {
513 for middleware in &api.middlewares {
514 middleware_names.insert(middleware.clone());
515 }
516 }
517
518 for ws in &schema.websockets {
519 for middleware in &ws.middlewares {
520 middleware_names.insert(middleware.clone());
521 }
522 }
523
524 if middleware_names.is_empty() {
525 return Ok(());
526 }
527
528 let middlewares_dir = output_dir.join("middlewares");
529 for middleware_name in middleware_names {
530 let file_name = format!("{}.ts", middleware_name);
531 let middleware_path = middlewares_dir.join(&file_name);
532
533 if !middleware_path.exists() {
534 let content = generate_middleware_stub(&middleware_name);
535 fs::write(middleware_path, content)?;
536 }
537 }
538
539 Ok(())
540}
541
542fn generate_middleware_stub(middleware_name: &str) -> String {
543 let mut content = String::new();
544
545 content.push_str("import { State } from '@generated/state';\n\n");
546
547 content.push_str("export interface MiddlewareContext {\n");
548 content.push_str(" payload?: any;\n");
549 content.push_str(" query_params?: Record<string, string>;\n");
550 content.push_str(" connection?: any;\n");
551 content.push_str(" websocket_name?: string;\n");
552 content.push_str(" api_name?: string;\n");
553 content.push_str(" trace_id?: string;\n");
554 content.push_str("}\n\n");
555
556 content.push_str(&format!(
557 "export async function {}Middleware(\n",
558 middleware_name
559 ));
560 content.push_str(" context: MiddlewareContext,\n");
561 content.push_str(" state: State\n");
562 content.push_str("): Promise<MiddlewareContext | null> {\n");
563 content.push_str(" /**\n");
564 content.push_str(&format!(" * Middleware function for {}.\n", middleware_name));
565 content.push_str(" * \n");
566 content.push_str(" * @param context - Request context containing:\n");
567 content.push_str(" * - payload: Request payload (for APIs)\n");
568 content.push_str(" * - query_params: Query parameters (for APIs)\n");
569 content.push_str(" * - connection: WebSocket connection info (for WebSockets)\n");
570 content.push_str(" * - websocket_name: WebSocket name (for WebSockets)\n");
571 content.push_str(" * - api_name: API name (for APIs)\n");
572 content.push_str(" * - trace_id: Trace ID\n");
573 content.push_str(" * @param state - State object for logging and triggering events\n");
574 content.push_str(" * @returns Modified context with 'payload' and/or 'query_params' keys,\n");
575 content.push_str(" * or null to pass through unchanged. Throw an error to reject the request.\n");
576 content.push_str(" * \n");
577 content.push_str(" * To reject the request, throw an error:\n");
578 content.push_str(" * throw new Error('Access denied');\n");
579 content.push_str(" * \n");
580 content.push_str(" * To modify the request:\n");
581 content.push_str(" * return {\n");
582 content.push_str(" * ...context,\n");
583 content.push_str(" * payload: modifiedPayload,\n");
584 content.push_str(" * query_params: modifiedQueryParams\n");
585 content.push_str(" * };\n");
586 content.push_str(" */\n");
587 content.push_str(" // TODO: Implement middleware logic\n");
588 content.push_str(" // Example: Validate authentication\n");
589 content.push_str(" // Example: Rate limiting\n");
590 content.push_str(" // Example: Logging\n");
591 content.push_str(" // Example: Modify payload/query_params\n");
592 content.push_str(" \n");
593 content.push_str(" // Pass through unchanged\n");
594 content.push_str(" return null;\n");
595 content.push_str("}\n");
596
597 content
598}
599
600fn generate_websocket_content(ws: &WebSocket) -> String {
601 let mut content = String::new();
602
603 content.push_str("import { z } from 'zod';\n");
604
605 if let Some(message_type) = &ws.message {
606 let message_field_type = FieldType::from_str(message_type);
607 let is_custom_type = matches!(message_field_type, FieldType::Custom(_));
608 if is_custom_type {
609 content.push_str(&format!(
610 "import {{ {}, {}Schema }} from '@generated/dto/{}';\n",
611 message_type,
612 message_type,
613 templates::to_snake_case(message_type)
614 ));
615 }
616 }
617
618 content.push_str("\n");
619
620 if let Some(message_type) = &ws.message {
621 let message_field_type = FieldType::from_str(message_type);
622 let ts_type = message_field_type.to_typescript();
623 content.push_str(&format!("export interface {}Message {{\n", ws.name));
624 content.push_str(&format!(" data: {};\n", ts_type));
625 content.push_str(" timestamp: Date;\n");
626 content.push_str("}\n\n");
627
628 let zod_type = if matches!(message_field_type, FieldType::Custom(_)) {
630 format!("{}Schema", message_type)
631 } else {
632 field_type_to_zod(&message_field_type, false)
633 };
634 content.push_str(&format!(
635 "export const {}MessageSchema = z.object({{\n",
636 ws.name
637 ));
638 content.push_str(&format!(" data: {},\n", zod_type));
639 content.push_str(" timestamp: z.date(),\n");
640 content.push_str("});\n\n");
641 } else {
642 content.push_str(&format!("export interface {}Message {{\n", ws.name));
643 content.push_str(" data: any;\n");
644 content.push_str(" timestamp: Date;\n");
645 content.push_str("}\n\n");
646
647 content.push_str(&format!(
648 "export const {}MessageSchema = z.object({{\n",
649 ws.name
650 ));
651 content.push_str(" data: z.any(),\n");
652 content.push_str(" timestamp: z.date(),\n");
653 content.push_str("});\n\n");
654 }
655
656 content.push_str(&format!("export interface {}Connection {{\n", ws.name));
658 content.push_str(" connectionId: string;\n");
659 content.push_str(" path: string;\n");
660 content.push_str(" connectedAt: Date;\n");
661 content.push_str("}\n\n");
662
663 content.push_str(&format!(
664 "export const {}ConnectionSchema = z.object({{\n",
665 ws.name
666 ));
667 content.push_str(" connectionId: z.string(),\n");
668 content.push_str(" path: z.string(),\n");
669 content.push_str(" connectedAt: z.date(),\n");
670 content.push_str("});\n");
671
672 content
673}
674
675fn generate_websocket_handler_stub(
676 ws: &WebSocket,
677 handler_type: &str,
678 handler_name: &str,
679) -> String {
680 let mut content = String::new();
681
682 content.push_str(&format!(
683 "import {{ {}Message, {}Connection }} from '@generated/websockets/{}';\n",
684 ws.name,
685 ws.name,
686 templates::to_snake_case(&ws.name)
687 ));
688 content.push_str("import { State } from '@generated/state';\n\n");
689
690 match handler_type {
691 "onConnect" => {
692 content.push_str(&format!(
693 "export async function {}(connection: {}Connection, state: State): Promise<{}Message | null> {{\n",
694 handler_name,
695 ws.name,
696 ws.name
697 ));
698 content.push_str(" // TODO: Implement onConnect handler\n");
699 content
700 .push_str(" // Return a message to send to the client on connection, or null\n");
701 content.push_str(&format!(
702 " console.log('Client connected:', connection.connectionId);\n"
703 ));
704 content.push_str(" return null;\n");
705 content.push_str("}\n");
706 }
707 "onMessage" => {
708 content.push_str(&format!(
709 "export async function {}(message: {}Message, connection: {}Connection, state: State): Promise<{}Message | null> {{\n",
710 handler_name,
711 ws.name,
712 ws.name,
713 ws.name
714 ));
715 content.push_str(" // TODO: Implement onMessage handler\n");
716 content.push_str(" // Return a message to send back to the client, or null\n");
717 content.push_str(&format!(
718 " console.log('Received message:', message.data);\n"
719 ));
720 content.push_str(" // For auto-triggers (defined in schema triggers): use state.setPayload('EventName', {...})\n");
721 content
722 .push_str(" // For manual triggers: use state.triggerEvent('EventName', {...})\n");
723 content.push_str(" return null;\n");
724 content.push_str("}\n");
725 }
726 "onDisconnect" => {
727 content.push_str(&format!(
728 "export async function {}(connection: {}Connection, state: State): Promise<void> {{\n",
729 handler_name,
730 ws.name
731 ));
732 content.push_str(" // TODO: Implement onDisconnect handler\n");
733 content.push_str(&format!(
734 " console.log('Client disconnected:', connection.connectionId);\n"
735 ));
736 content.push_str("}\n");
737 }
738 _ => {}
739 }
740
741 content
742}
743
744pub fn generate_state(output_dir: &Path) -> Result<()> {
745 let generated_dir = output_dir.join("generated");
746 let content = r#"export interface TriggeredEvent {
747 eventName: string;
748 payload: any;
749}
750
751/**
752 * Logger for handlers to emit structured logs.
753 */
754export class Logger {
755 private handlerName: string;
756 private logFn?: (level: string, handler: string, message: string, fields: Record<string, any>) => void;
757
758 constructor(handlerName: string, logFn?: (level: string, handler: string, message: string, fields: Record<string, any>) => void) {
759 this.handlerName = handlerName;
760 this.logFn = logFn;
761 }
762
763 /**
764 * Log an info message.
765 *
766 * @param message - Log message
767 * @param fields - Additional fields to include in the log
768 */
769 info(message: string, fields?: Record<string, any>): void {
770 if (this.logFn) {
771 this.logFn("info", this.handlerName, message, fields || {});
772 } else {
773 console.log(`[${this.handlerName}] ${message}`, fields || {});
774 }
775 }
776
777 /**
778 * Log an error message.
779 *
780 * @param message - Log message
781 * @param fields - Additional fields to include in the log
782 */
783 error(message: string, fields?: Record<string, any>): void {
784 if (this.logFn) {
785 this.logFn("error", this.handlerName, message, fields || {});
786 } else {
787 console.error(`[${this.handlerName}] ${message}`, fields || {});
788 }
789 }
790
791 /**
792 * Log a warning message.
793 *
794 * @param message - Log message
795 * @param fields - Additional fields to include in the log
796 */
797 warning(message: string, fields?: Record<string, any>): void {
798 if (this.logFn) {
799 this.logFn("warn", this.handlerName, message, fields || {});
800 } else {
801 console.warn(`[${this.handlerName}] ${message}`, fields || {});
802 }
803 }
804
805 /**
806 * Log a warning message (alias for warning).
807 *
808 * @param message - Log message
809 * @param fields - Additional fields to include in the log
810 */
811 warn(message: string, fields?: Record<string, any>): void {
812 this.warning(message, fields);
813 }
814
815 /**
816 * Log a debug message.
817 *
818 * @param message - Log message
819 * @param fields - Additional fields to include in the log
820 */
821 debug(message: string, fields?: Record<string, any>): void {
822 if (this.logFn) {
823 this.logFn("debug", this.handlerName, message, fields || {});
824 } else {
825 console.debug(`[${this.handlerName}] ${message}`, fields || {});
826 }
827 }
828
829 /**
830 * Log a trace message.
831 *
832 * @param message - Log message
833 * @param fields - Additional fields to include in the log
834 */
835 trace(message: string, fields?: Record<string, any>): void {
836 if (this.logFn) {
837 this.logFn("trace", this.handlerName, message, fields || {});
838 } else {
839 console.trace(`[${this.handlerName}] ${message}`, fields || {});
840 }
841 }
842}
843
844/**
845 * Context object for handlers to trigger events and access runtime state.
846 */
847export class State {
848 private triggers: TriggeredEvent[] = [];
849 private autoTriggerPayloads: Map<string, any> = new Map();
850 public logger: Logger;
851
852 constructor(handlerName?: string, logFn?: (level: string, handler: string, message: string, fields: Record<string, any>) => void) {
853 this.logger = new Logger(handlerName || "unknown", logFn);
854 }
855
856 /**
857 * Manually trigger an event with the given payload.
858 *
859 * Use this for events that are NOT defined in the schema's triggers list.
860 *
861 * @param eventName - Name of the event to trigger
862 * @param payload - Event payload data (will be serialized to JSON)
863 */
864 triggerEvent(eventName: string, payload: any): void {
865 this.triggers.push({
866 eventName,
867 payload,
868 });
869 }
870
871 /**
872 * Set the payload for an auto-triggered event.
873 *
874 * Use this for events that ARE defined in the schema's triggers list.
875 * The event will be automatically triggered after the handler completes,
876 * using the payload you set here.
877 *
878 * @param eventName - Name of the event (must match a trigger in schema)
879 * @param payload - Event payload data (will be serialized to JSON)
880 */
881 setPayload(eventName: string, payload: any): void {
882 this.autoTriggerPayloads.set(eventName, payload);
883 }
884
885 /**
886 * Get all manually triggered events. Used internally by the runtime.
887 */
888 getTriggers(): TriggeredEvent[] {
889 return [...this.triggers];
890 }
891
892 /**
893 * Get payload for an auto-triggered event. Used internally by the runtime.
894 */
895 getAutoTriggerPayload(eventName: string): any | undefined {
896 return this.autoTriggerPayloads.get(eventName);
897 }
898
899 /**
900 * Get all auto-trigger payloads. Used internally by the runtime.
901 */
902 getAllAutoTriggerPayloads(): Map<string, any> {
903 return new Map(this.autoTriggerPayloads);
904 }
905}
906"#;
907
908 fs::write(generated_dir.join("state.ts"), content)?;
909 Ok(())
910}
911
912pub fn generate_index(schema: &Schema, output_dir: &Path) -> Result<()> {
913 let mut content = String::new();
914
915 content.push_str("export * from './state';\n\n");
916
917 content.push_str("// Models\n");
918 for model in &schema.models {
919 content.push_str(&format!(
920 "export * from './models/{}';\n",
921 templates::to_snake_case(&model.name)
922 ));
923 }
924
925 content.push_str("\n// DTOs\n");
926 for input in &schema.inputs {
927 content.push_str(&format!(
928 "export * from './dto/{}';\n",
929 templates::to_snake_case(&input.name)
930 ));
931 }
932
933 content.push_str("\n// APIs\n");
934 for api in &schema.apis {
935 content.push_str(&format!(
936 "export * from './api/{}';\n",
937 templates::to_snake_case(&api.name)
938 ));
939 }
940
941 content.push_str("\n// Events\n");
942 for event in &schema.events {
943 content.push_str(&format!(
944 "export * from './events/{}';\n",
945 templates::to_snake_case(&event.name)
946 ));
947 }
948
949 content.push_str("\n// WebSockets\n");
950 for ws in &schema.websockets {
951 content.push_str(&format!(
952 "export * from './websockets/{}';\n",
953 templates::to_snake_case(&ws.name)
954 ));
955 }
956
957 fs::write(output_dir.join("generated/index.ts"), content)?;
958
959 Ok(())
960}