Skip to main content

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