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