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