hyperstack_interpreter/
typescript.rs

1use crate::ast::*;
2use std::collections::{BTreeMap, HashSet};
3
4/// Output structure for TypeScript generation
5#[derive(Debug, Clone)]
6pub struct TypeScriptOutput {
7    pub interfaces: String,
8    pub stack_definition: String,
9    pub imports: String,
10}
11
12impl TypeScriptOutput {
13    pub fn full_file(&self) -> String {
14        format!(
15            "{}\n\n{}\n\n{}",
16            self.imports, self.interfaces, self.stack_definition
17        )
18    }
19}
20
21/// Configuration for TypeScript generation
22#[derive(Debug, Clone)]
23pub struct TypeScriptConfig {
24    pub package_name: String,
25    pub generate_helpers: bool,
26    pub interface_prefix: String,
27    pub export_const_name: String,
28}
29
30impl Default for TypeScriptConfig {
31    fn default() -> Self {
32        Self {
33            package_name: "hyperstack-react".to_string(),
34            generate_helpers: true,
35            interface_prefix: "".to_string(),
36            export_const_name: "STACK".to_string(),
37        }
38    }
39}
40
41/// Trait for generating TypeScript code from AST components
42pub trait TypeScriptGenerator {
43    fn generate_typescript(&self, config: &TypeScriptConfig) -> String;
44}
45
46/// Trait for generating TypeScript interfaces
47pub trait TypeScriptInterfaceGenerator {
48    fn generate_interface(&self, name: &str, config: &TypeScriptConfig) -> String;
49}
50
51/// Trait for generating TypeScript type mappings
52pub trait TypeScriptTypeMapper {
53    fn to_typescript_type(&self) -> String;
54}
55
56/// Main TypeScript compiler for stream specs
57pub struct TypeScriptCompiler<S> {
58    spec: TypedStreamSpec<S>,
59    entity_name: String,
60    config: TypeScriptConfig,
61    idl: Option<serde_json::Value>, // IDL for enum type generation
62    handlers_json: Option<serde_json::Value>, // Raw handlers for event interface generation
63}
64
65impl<S> TypeScriptCompiler<S> {
66    pub fn new(spec: TypedStreamSpec<S>, entity_name: String) -> Self {
67        Self {
68            spec,
69            entity_name,
70            config: TypeScriptConfig::default(),
71            idl: None,
72            handlers_json: None,
73        }
74    }
75
76    pub fn with_config(mut self, config: TypeScriptConfig) -> Self {
77        self.config = config;
78        self
79    }
80
81    pub fn with_idl(mut self, idl: Option<serde_json::Value>) -> Self {
82        self.idl = idl;
83        self
84    }
85
86    pub fn with_handlers_json(mut self, handlers: Option<serde_json::Value>) -> Self {
87        self.handlers_json = handlers;
88        self
89    }
90
91    pub fn compile(&self) -> TypeScriptOutput {
92        let imports = self.generate_imports();
93        let interfaces = self.generate_interfaces();
94        let stack_definition = self.generate_stack_definition();
95
96        TypeScriptOutput {
97            imports,
98            interfaces,
99            stack_definition,
100        }
101    }
102
103    fn generate_imports(&self) -> String {
104        format!(
105            "import {{ defineStack, createStateView, createListView }} from '{}';",
106            self.config.package_name
107        )
108    }
109
110    fn generate_interfaces(&self) -> String {
111        let mut interfaces = Vec::new();
112        let mut processed_types = HashSet::new();
113        let mut all_sections: BTreeMap<String, Vec<TypeScriptField>> = BTreeMap::new();
114
115        // Collect all interface sections from all handlers
116        for handler in &self.spec.handlers {
117            let interface_sections = self.extract_interface_sections(handler);
118
119            for (section_name, mut fields) in interface_sections {
120                all_sections
121                    .entry(section_name)
122                    .or_default()
123                    .append(&mut fields);
124            }
125        }
126
127        // Deduplicate fields within each section and generate interfaces
128        for (section_name, fields) in all_sections {
129            if processed_types.insert(section_name.clone()) {
130                let deduplicated_fields = self.deduplicate_fields(fields);
131                let interface =
132                    self.generate_interface_from_fields(&section_name, &deduplicated_fields);
133                interfaces.push(interface);
134            }
135        }
136
137        // Generate main entity interface
138        let main_interface = self.generate_main_entity_interface();
139        interfaces.push(main_interface);
140
141        // Generate nested interfaces for resolved types (instructions, accounts, etc.)
142        let nested_interfaces = self.generate_nested_interfaces();
143        interfaces.extend(nested_interfaces);
144
145        // Generate EventWrapper interface if there are any event types
146        if self.has_event_types() {
147            interfaces.push(self.generate_event_wrapper_interface());
148        }
149
150        interfaces.join("\n\n")
151    }
152
153    fn deduplicate_fields(&self, mut fields: Vec<TypeScriptField>) -> Vec<TypeScriptField> {
154        let mut seen = HashSet::new();
155        let mut unique_fields = Vec::new();
156
157        // Sort fields by name for consistent output
158        fields.sort_by(|a, b| a.name.cmp(&b.name));
159
160        for field in fields {
161            if seen.insert(field.name.clone()) {
162                unique_fields.push(field);
163            }
164        }
165
166        unique_fields
167    }
168
169    fn extract_interface_sections(
170        &self,
171        handler: &TypedHandlerSpec<S>,
172    ) -> BTreeMap<String, Vec<TypeScriptField>> {
173        let mut sections: BTreeMap<String, Vec<TypeScriptField>> = BTreeMap::new();
174
175        for mapping in &handler.mappings {
176            let parts: Vec<&str> = mapping.target_path.split('.').collect();
177
178            if parts.len() > 1 {
179                // Nested field (e.g., "status.current")
180                let section_name = parts[0];
181                let field_name = parts[1];
182
183                let ts_field = TypeScriptField {
184                    name: field_name.to_string(),
185                    ts_type: self.mapping_to_typescript_type(mapping),
186                    optional: self.is_field_optional(mapping),
187                    description: None,
188                };
189
190                sections
191                    .entry(section_name.to_string())
192                    .or_default()
193                    .push(ts_field);
194            } else {
195                // Top-level field
196                let ts_field = TypeScriptField {
197                    name: mapping.target_path.clone(),
198                    ts_type: self.mapping_to_typescript_type(mapping),
199                    optional: self.is_field_optional(mapping),
200                    description: None,
201                };
202
203                sections
204                    .entry("Root".to_string())
205                    .or_default()
206                    .push(ts_field);
207            }
208        }
209
210        // Add any unmapped fields that exist in the original spec
211        // These are fields without #[map] or #[event] attributes
212        self.add_unmapped_fields(&mut sections);
213
214        sections
215    }
216
217    fn add_unmapped_fields(&self, sections: &mut BTreeMap<String, Vec<TypeScriptField>>) {
218        // NEW: Enhanced approach using AST type information if available
219        if !self.spec.sections.is_empty() {
220            // Use type information from the enhanced AST
221            for section in &self.spec.sections {
222                let section_fields = sections.entry(section.name.clone()).or_default();
223
224                for field_info in &section.fields {
225                    // Check if field is already mapped
226                    let already_exists = section_fields.iter().any(|f| {
227                        f.name == field_info.field_name
228                            || f.name == to_camel_case(&field_info.field_name)
229                    });
230
231                    if !already_exists {
232                        section_fields.push(TypeScriptField {
233                            name: field_info.field_name.clone(),
234                            ts_type: self.field_type_info_to_typescript(field_info),
235                            optional: field_info.is_optional,
236                            description: None,
237                        });
238                    }
239                }
240            }
241        } else {
242            // FALLBACK: Use field mappings from spec if sections aren't available yet
243            for (field_path, field_type_info) in &self.spec.field_mappings {
244                let parts: Vec<&str> = field_path.split('.').collect();
245                if parts.len() > 1 {
246                    let section_name = parts[0];
247                    let field_name = parts[1];
248
249                    let section_fields = sections.entry(section_name.to_string()).or_default();
250
251                    let already_exists = section_fields
252                        .iter()
253                        .any(|f| f.name == field_name || f.name == to_camel_case(field_name));
254
255                    if !already_exists {
256                        section_fields.push(TypeScriptField {
257                            name: field_name.to_string(),
258                            ts_type: self.base_type_to_typescript(
259                                &field_type_info.base_type,
260                                field_type_info.is_array,
261                            ),
262                            optional: field_type_info.is_optional,
263                            description: None,
264                        });
265                    }
266                }
267            }
268        }
269    }
270
271    fn generate_interface_from_fields(&self, name: &str, fields: &[TypeScriptField]) -> String {
272        // Generate more descriptive interface names
273        let interface_name = if name == "Root" {
274            format!(
275                "{}{}",
276                self.config.interface_prefix,
277                to_pascal_case(&self.entity_name)
278            )
279        } else {
280            // Create compound names like GameEvents, GameStatus, etc.
281            // Extract the base name (e.g., "Game" from "TestGame" or "SettlementGame")
282            let base_name = if self.entity_name.contains("Game") {
283                "Game"
284            } else {
285                &self.entity_name
286            };
287            format!(
288                "{}{}{}",
289                self.config.interface_prefix,
290                base_name,
291                to_pascal_case(name)
292            )
293        };
294
295        let field_definitions: Vec<String> = fields
296            .iter()
297            .map(|field| {
298                let optional_marker = if field.optional { "?" } else { "" };
299                // Convert snake_case to camelCase for field names
300                let field_name = to_camel_case(&field.name);
301                format!("  {}{}: {};", field_name, optional_marker, field.ts_type)
302            })
303            .collect();
304
305        format!(
306            "export interface {} {{\n{}\n}}",
307            interface_name,
308            field_definitions.join("\n")
309        )
310    }
311
312    fn generate_main_entity_interface(&self) -> String {
313        let entity_name = to_pascal_case(&self.entity_name);
314
315        // Extract all top-level sections from the handlers
316        let mut sections = BTreeMap::new();
317
318        for handler in &self.spec.handlers {
319            for mapping in &handler.mappings {
320                let parts: Vec<&str> = mapping.target_path.split('.').collect();
321                if parts.len() > 1 {
322                    sections.insert(parts[0], true);
323                }
324            }
325        }
326
327        // NEW: Use actual section names from enhanced AST if available
328        if !self.spec.sections.is_empty() {
329            for section in &self.spec.sections {
330                sections.insert(&section.name, true);
331            }
332        } else {
333            // FALLBACK: Extract section names from field mappings
334            for mapping in &self.spec.handlers {
335                for field_mapping in &mapping.mappings {
336                    let parts: Vec<&str> = field_mapping.target_path.split('.').collect();
337                    if parts.len() > 1 {
338                        sections.insert(parts[0], true);
339                    }
340                }
341            }
342        }
343
344        let mut fields = Vec::new();
345
346        // Add each section as a field in the main interface (sorted by key)
347        for section in sections.keys() {
348            // Generate proper interface names like GameEvents, GameStatus, etc.
349            let base_name = if self.entity_name.contains("Game") {
350                "Game"
351            } else {
352                &self.entity_name
353            };
354            let section_interface_name = format!("{}{}", base_name, to_pascal_case(section));
355            fields.push(format!(
356                "  {}: {};",
357                to_camel_case(section),
358                section_interface_name
359            ));
360        }
361
362        // If no sections found, create a basic structure
363        if fields.is_empty() {
364            fields.push("  // Generated interface - extend as needed".to_string());
365        }
366
367        format!(
368            "export interface {} {{\n{}\n}}",
369            entity_name,
370            fields.join("\n")
371        )
372    }
373
374    fn generate_stack_definition(&self) -> String {
375        let stack_name = to_kebab_case(&self.entity_name);
376        let entity_pascal = to_pascal_case(&self.entity_name);
377        let export_name = format!(
378            "{}_{}",
379            self.entity_name.to_uppercase(),
380            self.config.export_const_name
381        );
382
383        let _views = self.generate_view_definitions();
384        let helpers = if self.config.generate_helpers {
385            self.generate_helper_functions()
386        } else {
387            String::new()
388        };
389
390        let helpers_section = if helpers.is_empty() {
391            String::new()
392        } else {
393            format!(",\n  helpers: {{\n{}\n  }}", helpers)
394        };
395
396        format!(
397            r#"export const {} = defineStack({{
398  name: '{}',
399  views: {{
400    {}: {{
401      state: createStateView<{}>('{}/state'),
402      list: createListView<{}>('{}/list')
403    }}
404  }}{}
405}});"#,
406            export_name,
407            stack_name,
408            to_camel_case(&self.entity_name),
409            entity_pascal,
410            self.entity_name,
411            entity_pascal,
412            self.entity_name,
413            helpers_section
414        )
415    }
416
417    fn generate_view_definitions(&self) -> String {
418        // For now, generate basic state and list views
419        // This can be enhanced to generate specific views based on the spec
420        to_camel_case(&self.entity_name)
421    }
422
423    fn generate_helper_functions(&self) -> String {
424        let mut helpers = Vec::new();
425
426        // Generate helpers based on field types and transformations
427        for handler in &self.spec.handlers {
428            for mapping in &handler.mappings {
429                if let Some(helper) = self.generate_helper_for_mapping(mapping) {
430                    helpers.push(helper);
431                }
432            }
433        }
434
435        helpers.join(",\n")
436    }
437
438    fn generate_helper_for_mapping(&self, mapping: &TypedFieldMapping<S>) -> Option<String> {
439        // Generate helpers based on transformations or field types
440        if let Some(transform) = &mapping.transform {
441            match transform {
442                Transformation::HexEncode => {
443                    let helper_name = format!(
444                        "format{}",
445                        to_pascal_case(&mapping.target_path.replace(".", ""))
446                    );
447                    Some(format!(
448                        "    {}: (value: string) => value.startsWith('0x') ? value : `0x${{value}}`",
449                        helper_name
450                    ))
451                }
452                Transformation::HexDecode => {
453                    let helper_name = format!(
454                        "decode{}",
455                        to_pascal_case(&mapping.target_path.replace(".", ""))
456                    );
457                    Some(format!(
458                        "    {}: (value: string) => value.startsWith('0x') ? value.slice(2) : value",
459                        helper_name
460                    ))
461                }
462                _ => None,
463            }
464        } else {
465            None
466        }
467    }
468
469    fn mapping_to_typescript_type(&self, mapping: &TypedFieldMapping<S>) -> String {
470        // First, try to resolve from AST field mappings
471        if let Some(field_info) = self.spec.field_mappings.get(&mapping.target_path) {
472            let ts_type = self.field_type_info_to_typescript(field_info);
473
474            // If it's an Append strategy, wrap in array
475            if matches!(mapping.population, PopulationStrategy::Append) {
476                return if ts_type.ends_with("[]") {
477                    ts_type
478                } else {
479                    format!("{}[]", ts_type)
480                };
481            }
482
483            return ts_type;
484        }
485
486        // Fallback to legacy inference
487        match &mapping.population {
488            PopulationStrategy::Append => {
489                // For arrays, try to infer the element type
490                match &mapping.source {
491                    MappingSource::AsEvent { .. } => "any[]".to_string(),
492                    _ => "any[]".to_string(),
493                }
494            }
495            _ => {
496                // Infer type from source and field name
497                let base_type = match &mapping.source {
498                    MappingSource::FromSource { .. } => {
499                        self.infer_type_from_field_name(&mapping.target_path)
500                    }
501                    MappingSource::Constant(value) => value_to_typescript_type(value),
502                    MappingSource::AsEvent { .. } => "any".to_string(),
503                    _ => "any".to_string(),
504                };
505
506                // Apply transformations to type
507                if let Some(transform) = &mapping.transform {
508                    match transform {
509                        Transformation::HexEncode | Transformation::HexDecode => {
510                            "string".to_string()
511                        }
512                        Transformation::Base58Encode | Transformation::Base58Decode => {
513                            "string".to_string()
514                        }
515                        Transformation::ToString => "string".to_string(),
516                        Transformation::ToNumber => "number".to_string(),
517                    }
518                } else {
519                    base_type
520                }
521            }
522        }
523    }
524
525    /// Convert FieldTypeInfo from AST to TypeScript type string
526    fn field_type_info_to_typescript(&self, field_info: &FieldTypeInfo) -> String {
527        // If we have resolved type information (complex types from IDL), use it
528        if let Some(resolved) = &field_info.resolved_type {
529            let interface_name = self.resolved_type_to_interface_name(resolved);
530
531            // Wrap in EventWrapper if it's an event type
532            let base_type = if resolved.is_event || (resolved.is_instruction && field_info.is_array)
533            {
534                format!("EventWrapper<{}>", interface_name)
535            } else {
536                interface_name
537            };
538
539            // Handle optional and array
540            let with_array = if field_info.is_array {
541                format!("{}[]", base_type)
542            } else {
543                base_type
544            };
545
546            return with_array;
547        }
548
549        // Check if this is an event field (has BaseType::Any or BaseType::Array with Value inner type)
550        // We can detect event fields by looking for them in handlers with AsEvent mappings
551        if field_info.base_type == BaseType::Any
552            || (field_info.base_type == BaseType::Array
553                && field_info.inner_type.as_deref() == Some("Value"))
554        {
555            if let Some(event_type) = self.find_event_interface_for_field(&field_info.field_name) {
556                return if field_info.is_array {
557                    format!("{}[]", event_type)
558                } else if field_info.is_optional {
559                    format!("{} | null", event_type)
560                } else {
561                    event_type
562                };
563            }
564        }
565
566        // Use base type mapping
567        self.base_type_to_typescript(&field_info.base_type, field_info.is_array)
568    }
569
570    /// Find the generated event interface name for a given field
571    fn find_event_interface_for_field(&self, field_name: &str) -> Option<String> {
572        // Use the raw JSON handlers if available
573        let handlers = self.handlers_json.as_ref()?.as_array()?;
574
575        // Look through handlers to find event mappings for this field
576        for handler in handlers {
577            if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
578                for mapping in mappings {
579                    if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
580                        // Check if this mapping targets our field (e.g., "events.created")
581                        let target_parts: Vec<&str> = target_path.split('.').collect();
582                        if let Some(target_field) = target_parts.last() {
583                            if *target_field == field_name {
584                                // Check if this is an event mapping
585                                if let Some(source) = mapping.get("source") {
586                                    if self.extract_event_data(source).is_some() {
587                                        // Generate the interface name (e.g., "created" -> "CreatedEvent")
588                                        return Some(format!(
589                                            "{}Event",
590                                            to_pascal_case(field_name)
591                                        ));
592                                    }
593                                }
594                            }
595                        }
596                    }
597                }
598            }
599        }
600        None
601    }
602
603    /// Generate TypeScript interface name from resolved type
604    fn resolved_type_to_interface_name(&self, resolved: &ResolvedStructType) -> String {
605        to_pascal_case(&resolved.type_name)
606    }
607
608    /// Generate nested interfaces for all resolved types in the AST
609    fn generate_nested_interfaces(&self) -> Vec<String> {
610        let mut interfaces = Vec::new();
611        let mut generated_types = HashSet::new();
612
613        // Collect all resolved types from all sections
614        for section in &self.spec.sections {
615            for field_info in &section.fields {
616                if let Some(resolved) = &field_info.resolved_type {
617                    let type_name = resolved.type_name.clone();
618
619                    // Only generate each type once
620                    if generated_types.insert(type_name) {
621                        let interface = self.generate_interface_for_resolved_type(resolved);
622                        interfaces.push(interface);
623                    }
624                }
625            }
626        }
627
628        // Generate event interfaces from instruction handlers
629        interfaces.extend(self.generate_event_interfaces(&mut generated_types));
630
631        // Also generate all enum types from the IDL (even if not directly referenced)
632        if let Some(idl_value) = &self.idl {
633            if let Some(types_array) = idl_value.get("types").and_then(|v| v.as_array()) {
634                for type_def in types_array {
635                    if let (Some(type_name), Some(type_obj)) = (
636                        type_def.get("name").and_then(|v| v.as_str()),
637                        type_def.get("type").and_then(|v| v.as_object()),
638                    ) {
639                        if type_obj.get("kind").and_then(|v| v.as_str()) == Some("enum") {
640                            // Only generate if not already generated
641                            if generated_types.insert(type_name.to_string()) {
642                                if let Some(variants) =
643                                    type_obj.get("variants").and_then(|v| v.as_array())
644                                {
645                                    let variant_names: Vec<String> = variants
646                                        .iter()
647                                        .filter_map(|v| {
648                                            v.get("name")
649                                                .and_then(|n| n.as_str())
650                                                .map(|s| s.to_string())
651                                        })
652                                        .collect();
653
654                                    if !variant_names.is_empty() {
655                                        let interface_name = to_pascal_case(type_name);
656                                        let variant_strings: Vec<String> = variant_names
657                                            .iter()
658                                            .map(|v| format!("\"{}\"", to_pascal_case(v)))
659                                            .collect();
660
661                                        let enum_type = format!(
662                                            "export type {} = {};",
663                                            interface_name,
664                                            variant_strings.join(" | ")
665                                        );
666                                        interfaces.push(enum_type);
667                                    }
668                                }
669                            }
670                        }
671                    }
672                }
673            }
674        }
675
676        interfaces
677    }
678
679    /// Generate TypeScript interfaces for event types from instruction handlers
680    fn generate_event_interfaces(&self, generated_types: &mut HashSet<String>) -> Vec<String> {
681        let mut interfaces = Vec::new();
682
683        // Use the raw JSON handlers if available
684        let handlers = match &self.handlers_json {
685            Some(h) => h.as_array(),
686            None => return interfaces,
687        };
688
689        let handlers_array = match handlers {
690            Some(arr) => arr,
691            None => return interfaces,
692        };
693
694        // Look through handlers to find instruction-based event mappings
695        for handler in handlers_array {
696            // Check if this handler has event mappings
697            if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
698                for mapping in mappings {
699                    if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
700                        // Check if the target is an event field (contains ".events." or starts with "events.")
701                        if target_path.contains(".events.") || target_path.starts_with("events.") {
702                            // Check if the source is AsEvent
703                            if let Some(source) = mapping.get("source") {
704                                if let Some(event_data) = self.extract_event_data(source) {
705                                    // Extract instruction name from handler source
706                                    if let Some(handler_source) = handler.get("source") {
707                                        if let Some(instruction_name) =
708                                            self.extract_instruction_name(handler_source)
709                                        {
710                                            // Generate interface name from target path (e.g., "events.created" -> "CreatedEvent")
711                                            let event_field_name =
712                                                target_path.split('.').next_back().unwrap_or("");
713                                            let interface_name = format!(
714                                                "{}Event",
715                                                to_pascal_case(event_field_name)
716                                            );
717
718                                            // Only generate once
719                                            if generated_types.insert(interface_name.clone()) {
720                                                if let Some(interface) = self
721                                                    .generate_event_interface_from_idl(
722                                                        &interface_name,
723                                                        &instruction_name,
724                                                        &event_data,
725                                                    )
726                                                {
727                                                    interfaces.push(interface);
728                                                }
729                                            }
730                                        }
731                                    }
732                                }
733                            }
734                        }
735                    }
736                }
737            }
738        }
739
740        interfaces
741    }
742
743    /// Extract event field data from a mapping source
744    fn extract_event_data(
745        &self,
746        source: &serde_json::Value,
747    ) -> Option<Vec<(String, Option<String>)>> {
748        if let Some(as_event) = source.get("AsEvent") {
749            if let Some(fields) = as_event.get("fields").and_then(|f| f.as_array()) {
750                let mut event_fields = Vec::new();
751                for field in fields {
752                    if let Some(from_source) = field.get("FromSource") {
753                        if let Some(path) = from_source
754                            .get("path")
755                            .and_then(|p| p.get("segments"))
756                            .and_then(|s| s.as_array())
757                        {
758                            // Get the last segment as the field name (e.g., ["data", "game_id"] -> "game_id")
759                            if let Some(field_name) = path.last().and_then(|v| v.as_str()) {
760                                let transform = from_source
761                                    .get("transform")
762                                    .and_then(|t| t.as_str())
763                                    .map(|s| s.to_string());
764                                event_fields.push((field_name.to_string(), transform));
765                            }
766                        }
767                    }
768                }
769                return Some(event_fields);
770            }
771        }
772        None
773    }
774
775    /// Extract instruction name from handler source
776    fn extract_instruction_name(&self, source: &serde_json::Value) -> Option<String> {
777        if let Some(source_obj) = source.get("Source") {
778            if let Some(type_name) = source_obj.get("type_name").and_then(|t| t.as_str()) {
779                // Convert "CreateGameIxState" -> "create_game"
780                if let Some(instruction_part) = type_name.strip_suffix("IxState") {
781                    return Some(to_snake_case(instruction_part));
782                }
783            }
784        }
785        None
786    }
787
788    /// Generate a TypeScript interface for an event from IDL instruction data
789    fn generate_event_interface_from_idl(
790        &self,
791        interface_name: &str,
792        instruction_name: &str,
793        captured_fields: &[(String, Option<String>)],
794    ) -> Option<String> {
795        // If no fields are captured, generate an empty interface
796        if captured_fields.is_empty() {
797            return Some(format!("export interface {} {{}}", interface_name));
798        }
799
800        let idl_value = self.idl.as_ref()?;
801        let instructions = idl_value.get("instructions")?.as_array()?;
802
803        // Find the instruction in the IDL
804        for instruction in instructions {
805            if let Some(name) = instruction.get("name").and_then(|n| n.as_str()) {
806                if name == instruction_name {
807                    // Get the args
808                    if let Some(args) = instruction.get("args").and_then(|a| a.as_array()) {
809                        let mut fields = Vec::new();
810
811                        // Only include captured fields
812                        for (field_name, transform) in captured_fields {
813                            // Find this arg in the instruction
814                            for arg in args {
815                                if let Some(arg_name) = arg.get("name").and_then(|n| n.as_str()) {
816                                    if arg_name == field_name {
817                                        if let Some(arg_type) = arg.get("type") {
818                                            let ts_type = self.idl_type_to_typescript(
819                                                arg_type,
820                                                transform.as_deref(),
821                                            );
822                                            let camel_name = to_camel_case(field_name);
823                                            fields.push(format!("  {}: {};", camel_name, ts_type));
824                                        }
825                                        break;
826                                    }
827                                }
828                            }
829                        }
830
831                        if !fields.is_empty() {
832                            return Some(format!(
833                                "export interface {} {{\n{}\n}}",
834                                interface_name,
835                                fields.join("\n")
836                            ));
837                        }
838                    }
839                }
840            }
841        }
842
843        None
844    }
845
846    /// Convert an IDL type (from JSON) to TypeScript, considering transforms
847    fn idl_type_to_typescript(
848        &self,
849        idl_type: &serde_json::Value,
850        transform: Option<&str>,
851    ) -> String {
852        #![allow(clippy::only_used_in_recursion)]
853        // If there's a HexEncode transform, the result is always a string
854        if transform == Some("HexEncode") {
855            return "string".to_string();
856        }
857
858        // Handle different IDL type formats
859        if let Some(type_str) = idl_type.as_str() {
860            return match type_str {
861                "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" => {
862                    "number".to_string()
863                }
864                "f32" | "f64" => "number".to_string(),
865                "bool" => "boolean".to_string(),
866                "string" => "string".to_string(),
867                "pubkey" | "publicKey" => "string".to_string(),
868                "bytes" => "string".to_string(),
869                _ => "any".to_string(),
870            };
871        }
872
873        // Handle complex types (option, vec, etc.)
874        if let Some(type_obj) = idl_type.as_object() {
875            if let Some(option_type) = type_obj.get("option") {
876                let inner = self.idl_type_to_typescript(option_type, None);
877                return format!("{} | null", inner);
878            }
879            if let Some(vec_type) = type_obj.get("vec") {
880                let inner = self.idl_type_to_typescript(vec_type, None);
881                return format!("{}[]", inner);
882            }
883        }
884
885        "any".to_string()
886    }
887
888    /// Generate a TypeScript interface from a resolved struct type
889    fn generate_interface_for_resolved_type(&self, resolved: &ResolvedStructType) -> String {
890        let interface_name = to_pascal_case(&resolved.type_name);
891
892        // Handle enums as TypeScript union types
893        if resolved.is_enum {
894            let variants: Vec<String> = resolved
895                .enum_variants
896                .iter()
897                .map(|v| format!("\"{}\"", to_pascal_case(v)))
898                .collect();
899
900            return format!("export type {} = {};", interface_name, variants.join(" | "));
901        }
902
903        // Handle structs as interfaces
904        let fields: Vec<String> = resolved
905            .fields
906            .iter()
907            .map(|field| {
908                let field_name = to_camel_case(&field.field_name);
909                let optional_marker = if field.is_optional { "?" } else { "" };
910                let ts_type = self.resolved_field_to_typescript(field);
911                format!("  {}{}: {};", field_name, optional_marker, ts_type)
912            })
913            .collect();
914
915        format!(
916            "export interface {} {{\n{}\n}}",
917            interface_name,
918            fields.join("\n")
919        )
920    }
921
922    /// Convert a resolved field to TypeScript type
923    fn resolved_field_to_typescript(&self, field: &ResolvedField) -> String {
924        let base_ts = self.base_type_to_typescript(&field.base_type, false);
925
926        if field.is_array {
927            format!("{}[]", base_ts)
928        } else {
929            base_ts
930        }
931    }
932
933    /// Check if the spec has any event types
934    fn has_event_types(&self) -> bool {
935        for section in &self.spec.sections {
936            for field_info in &section.fields {
937                if let Some(resolved) = &field_info.resolved_type {
938                    if resolved.is_event || (resolved.is_instruction && field_info.is_array) {
939                        return true;
940                    }
941                }
942            }
943        }
944        false
945    }
946
947    /// Generate the EventWrapper interface
948    fn generate_event_wrapper_interface(&self) -> String {
949        r#"/**
950 * Wrapper for event data that includes context metadata.
951 * Events are automatically wrapped in this structure at runtime.
952 */
953export interface EventWrapper<T> {
954  /** Unix timestamp when the event was processed */
955  timestamp: number;
956  /** The event-specific data */
957  data: T;
958  /** Optional blockchain slot number */
959  slot?: number;
960  /** Optional transaction signature */
961  signature?: string;
962}"#
963        .to_string()
964    }
965
966    fn infer_type_from_field_name(&self, field_name: &str) -> String {
967        let lower_name = field_name.to_lowercase();
968
969        // Special case for event fields - these are typically Option<Value> and should be 'any'
970        if lower_name.contains("events.") {
971            // For fields in the events section, default to 'any' since they're typically Option<Value>
972            return "any".to_string();
973        }
974
975        // Common patterns for type inference
976        if lower_name.contains("id")
977            || lower_name.contains("count")
978            || lower_name.contains("number")
979            || lower_name.contains("timestamp")
980            || lower_name.contains("time")
981            || lower_name.contains("at")
982            || lower_name.contains("volume")
983            || lower_name.contains("amount")
984            || lower_name.contains("ev")
985            || lower_name.contains("fee")
986            || lower_name.contains("payout")
987            || lower_name.contains("distributed")
988            || lower_name.contains("claimable")
989            || lower_name.contains("total")
990            || lower_name.contains("rate")
991            || lower_name.contains("ratio")
992            || lower_name.contains("current")
993            || lower_name.contains("state")
994        {
995            "number".to_string()
996        } else if lower_name.contains("status")
997            || lower_name.contains("hash")
998            || lower_name.contains("address")
999            || lower_name.contains("key")
1000        {
1001            "string".to_string()
1002        } else {
1003            "any".to_string()
1004        }
1005    }
1006
1007    fn is_field_optional(&self, mapping: &TypedFieldMapping<S>) -> bool {
1008        // Most fields should be optional by default since we're dealing with Option<T> types
1009        match &mapping.source {
1010            // Constants are typically non-optional
1011            MappingSource::Constant(_) => false,
1012            // Events are typically optional (Option<Value>)
1013            MappingSource::AsEvent { .. } => true,
1014            // For source fields, default to optional since most Rust fields are Option<T>
1015            MappingSource::FromSource { .. } => true,
1016            // Other cases default to optional
1017            _ => true,
1018        }
1019    }
1020
1021    /// Convert language-agnostic base types to TypeScript types
1022    fn base_type_to_typescript(&self, base_type: &BaseType, is_array: bool) -> String {
1023        let base_ts_type = match base_type {
1024            BaseType::Integer => "number",
1025            BaseType::Float => "number",
1026            BaseType::String => "string",
1027            BaseType::Boolean => "boolean",
1028            BaseType::Timestamp => "number", // Unix timestamps as numbers
1029            BaseType::Binary => "string",    // Base64 encoded strings
1030            BaseType::Pubkey => "string",    // Solana public keys as Base58 strings
1031            BaseType::Array => "any[]",      // Default array type
1032            BaseType::Object => "Record<string, any>", // Generic object
1033            BaseType::Any => "any",
1034        };
1035
1036        if is_array && !matches!(base_type, BaseType::Array) {
1037            format!("{}[]", base_ts_type)
1038        } else {
1039            base_ts_type.to_string()
1040        }
1041    }
1042}
1043
1044/// Represents a TypeScript field in an interface
1045#[derive(Debug, Clone)]
1046struct TypeScriptField {
1047    name: String,
1048    ts_type: String,
1049    optional: bool,
1050    #[allow(dead_code)]
1051    description: Option<String>,
1052}
1053
1054/// Convert serde_json::Value to TypeScript type string
1055fn value_to_typescript_type(value: &serde_json::Value) -> String {
1056    match value {
1057        serde_json::Value::Number(_) => "number".to_string(),
1058        serde_json::Value::String(_) => "string".to_string(),
1059        serde_json::Value::Bool(_) => "boolean".to_string(),
1060        serde_json::Value::Array(_) => "any[]".to_string(),
1061        serde_json::Value::Object(_) => "Record<string, any>".to_string(),
1062        serde_json::Value::Null => "null".to_string(),
1063    }
1064}
1065
1066/// Convert snake_case to PascalCase
1067fn to_pascal_case(s: &str) -> String {
1068    s.split(['_', '-', '.'])
1069        .map(|word| {
1070            let mut chars = word.chars();
1071            match chars.next() {
1072                None => String::new(),
1073                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1074            }
1075        })
1076        .collect()
1077}
1078
1079/// Convert snake_case to camelCase
1080fn to_camel_case(s: &str) -> String {
1081    let pascal = to_pascal_case(s);
1082    let mut chars = pascal.chars();
1083    match chars.next() {
1084        None => String::new(),
1085        Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1086    }
1087}
1088
1089/// Convert PascalCase/camelCase to snake_case
1090fn to_snake_case(s: &str) -> String {
1091    let mut result = String::new();
1092
1093    for ch in s.chars() {
1094        if ch.is_uppercase() {
1095            if !result.is_empty() {
1096                result.push('_');
1097            }
1098            result.push(ch.to_lowercase().next().unwrap());
1099        } else {
1100            result.push(ch);
1101        }
1102    }
1103
1104    result
1105}
1106
1107/// Convert PascalCase/camelCase to kebab-case
1108fn to_kebab_case(s: &str) -> String {
1109    let mut result = String::new();
1110
1111    for ch in s.chars() {
1112        if ch.is_uppercase() && !result.is_empty() {
1113            result.push('-');
1114        }
1115        result.push(ch.to_lowercase().next().unwrap());
1116    }
1117
1118    result
1119}
1120
1121/// CLI-friendly function to generate TypeScript from a spec function
1122/// This will be used by the CLI tool to generate TypeScript from discovered specs
1123pub fn generate_typescript_from_spec_fn<F, S>(
1124    spec_fn: F,
1125    entity_name: String,
1126    config: Option<TypeScriptConfig>,
1127) -> Result<TypeScriptOutput, String>
1128where
1129    F: Fn() -> TypedStreamSpec<S>,
1130{
1131    let spec = spec_fn();
1132    let compiler =
1133        TypeScriptCompiler::new(spec, entity_name).with_config(config.unwrap_or_default());
1134
1135    Ok(compiler.compile())
1136}
1137
1138/// Write TypeScript output to a file
1139pub fn write_typescript_to_file(
1140    output: &TypeScriptOutput,
1141    path: &std::path::Path,
1142) -> Result<(), std::io::Error> {
1143    std::fs::write(path, output.full_file())
1144}
1145
1146/// Generate TypeScript from a SerializableStreamSpec (for CLI use)
1147/// This allows the CLI to compile TypeScript without needing the typed spec
1148pub fn compile_serializable_spec(
1149    spec: SerializableStreamSpec,
1150    entity_name: String,
1151    config: Option<TypeScriptConfig>,
1152) -> Result<TypeScriptOutput, String> {
1153    // Extract IDL and convert to serde_json::Value
1154    let idl = spec
1155        .idl
1156        .as_ref()
1157        .and_then(|idl_snapshot| serde_json::to_value(idl_snapshot).ok());
1158
1159    // Extract handlers as JSON
1160    let handlers = serde_json::to_value(&spec.handlers).ok();
1161
1162    // Convert SerializableStreamSpec to TypedStreamSpec
1163    // We use () as the phantom type since it won't be used
1164    let typed_spec: TypedStreamSpec<()> = TypedStreamSpec::from_serializable(spec);
1165
1166    let compiler = TypeScriptCompiler::new(typed_spec, entity_name)
1167        .with_idl(idl)
1168        .with_handlers_json(handlers)
1169        .with_config(config.unwrap_or_default());
1170
1171    Ok(compiler.compile())
1172}
1173
1174#[cfg(test)]
1175mod tests {
1176    use super::*;
1177
1178    #[test]
1179    fn test_case_conversions() {
1180        assert_eq!(to_pascal_case("settlement_game"), "SettlementGame");
1181        assert_eq!(to_camel_case("settlement_game"), "settlementGame");
1182        assert_eq!(to_kebab_case("SettlementGame"), "settlement-game");
1183    }
1184
1185    #[test]
1186    fn test_value_to_typescript_type() {
1187        assert_eq!(value_to_typescript_type(&serde_json::json!(42)), "number");
1188        assert_eq!(
1189            value_to_typescript_type(&serde_json::json!("hello")),
1190            "string"
1191        );
1192        assert_eq!(
1193            value_to_typescript_type(&serde_json::json!(true)),
1194            "boolean"
1195        );
1196        assert_eq!(value_to_typescript_type(&serde_json::json!([])), "any[]");
1197    }
1198}