Skip to main content

rohas_codegen/
typescript.rs

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    // Generate zod schema
35    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    // Generate zod schema for request
187    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    // Generate zod schema for response
227    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
270/// Extract path parameters from a path string
271/// e.g., "/test/{name}" -> ["name"]
272/// e.g., "/users/{id}/posts/{postId}" -> ["id", "postId"]
273fn 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    // Generate handler stubs
338    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    // Generate zod schema for event
394    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    // Generate handler stubs
444    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        // Generate zod schema
629        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    // Generate connection type
657    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}