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