Skip to main content

hyperstack_interpreter/
typescript.rs

1use crate::ast::*;
2use std::collections::{BTreeMap, BTreeSet, 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    pub schema_names: Vec<String>,
11}
12
13impl TypeScriptOutput {
14    pub fn full_file(&self) -> String {
15        format!(
16            "{}\n\n{}\n\n{}",
17            self.imports, self.interfaces, self.stack_definition
18        )
19    }
20}
21
22/// Configuration for TypeScript generation
23#[derive(Debug, Clone)]
24pub struct TypeScriptConfig {
25    pub package_name: String,
26    pub generate_helpers: bool,
27    pub interface_prefix: String,
28    pub export_const_name: String,
29    /// WebSocket URL for the stack. If None, generates a placeholder comment.
30    pub url: Option<String>,
31}
32
33impl Default for TypeScriptConfig {
34    fn default() -> Self {
35        Self {
36            package_name: "hyperstack-react".to_string(),
37            generate_helpers: true,
38            interface_prefix: "".to_string(),
39            export_const_name: "STACK".to_string(),
40            url: None,
41        }
42    }
43}
44
45/// Trait for generating TypeScript code from AST components
46pub trait TypeScriptGenerator {
47    fn generate_typescript(&self, config: &TypeScriptConfig) -> String;
48}
49
50/// Trait for generating TypeScript interfaces
51pub trait TypeScriptInterfaceGenerator {
52    fn generate_interface(&self, name: &str, config: &TypeScriptConfig) -> String;
53}
54
55/// Trait for generating TypeScript type mappings
56pub trait TypeScriptTypeMapper {
57    fn to_typescript_type(&self) -> String;
58}
59
60/// Main TypeScript compiler for stream specs
61pub struct TypeScriptCompiler<S> {
62    spec: TypedStreamSpec<S>,
63    entity_name: String,
64    config: TypeScriptConfig,
65    idl: Option<serde_json::Value>, // IDL for enum type generation
66    handlers_json: Option<serde_json::Value>, // Raw handlers for event interface generation
67    views: Vec<ViewDef>,            // View definitions for derived views
68}
69
70impl<S> TypeScriptCompiler<S> {
71    pub fn new(spec: TypedStreamSpec<S>, entity_name: String) -> Self {
72        Self {
73            spec,
74            entity_name,
75            config: TypeScriptConfig::default(),
76            idl: None,
77            handlers_json: None,
78            views: Vec::new(),
79        }
80    }
81
82    pub fn with_config(mut self, config: TypeScriptConfig) -> Self {
83        self.config = config;
84        self
85    }
86
87    pub fn with_idl(mut self, idl: Option<serde_json::Value>) -> Self {
88        self.idl = idl;
89        self
90    }
91
92    pub fn with_handlers_json(mut self, handlers: Option<serde_json::Value>) -> Self {
93        self.handlers_json = handlers;
94        self
95    }
96
97    pub fn with_views(mut self, views: Vec<ViewDef>) -> Self {
98        self.views = views;
99        self
100    }
101
102    pub fn compile(&self) -> TypeScriptOutput {
103        let imports = self.generate_imports();
104        let interfaces = self.generate_interfaces();
105        let schema_output = self.generate_schemas();
106        let combined_interfaces = if schema_output.definitions.is_empty() {
107            interfaces
108        } else if interfaces.is_empty() {
109            schema_output.definitions.clone()
110        } else {
111            format!("{}\n\n{}", interfaces, schema_output.definitions)
112        };
113        let stack_definition = self.generate_stack_definition();
114
115        TypeScriptOutput {
116            imports,
117            interfaces: combined_interfaces,
118            stack_definition,
119            schema_names: schema_output.names,
120        }
121    }
122
123    fn generate_imports(&self) -> String {
124        "import { z } from 'zod';".to_string()
125    }
126
127    fn generate_view_helpers(&self) -> String {
128        r#"// ============================================================================
129// View Definition Types (framework-agnostic)
130// ============================================================================
131
132/** View definition with embedded entity type */
133export interface ViewDef<T, TMode extends 'state' | 'list'> {
134  readonly mode: TMode;
135  readonly view: string;
136  /** Phantom field for type inference - not present at runtime */
137  readonly _entity?: T;
138}
139
140/** Helper to create typed state view definitions (keyed lookups) */
141function stateView<T>(view: string): ViewDef<T, 'state'> {
142  return { mode: 'state', view } as const;
143}
144
145/** Helper to create typed list view definitions (collections) */
146function listView<T>(view: string): ViewDef<T, 'list'> {
147  return { mode: 'list', view } as const;
148}"#
149        .to_string()
150    }
151
152    fn generate_interfaces(&self) -> String {
153        let mut interfaces = Vec::new();
154        let mut processed_types = HashSet::new();
155        let all_sections = self.collect_interface_sections();
156
157        // Deduplicate fields within each section and generate interfaces
158        // Skip root section - its fields will be flattened into main entity interface
159        for (section_name, fields) in all_sections {
160            if !is_root_section(&section_name) && processed_types.insert(section_name.clone()) {
161                let deduplicated_fields = self.deduplicate_fields(fields);
162                let interface =
163                    self.generate_interface_from_fields(&section_name, &deduplicated_fields);
164                interfaces.push(interface);
165            }
166        }
167
168        // Generate main entity interface
169        let main_interface = self.generate_main_entity_interface();
170        interfaces.push(main_interface);
171
172        let nested_interfaces = self.generate_nested_interfaces();
173        interfaces.extend(nested_interfaces);
174
175        let builtin_interfaces = self.generate_builtin_resolver_interfaces();
176        interfaces.extend(builtin_interfaces);
177
178        if self.has_event_types() {
179            interfaces.push(self.generate_event_wrapper_interface());
180        }
181
182        interfaces.join("\n\n")
183    }
184
185    fn collect_interface_sections(&self) -> BTreeMap<String, Vec<TypeScriptField>> {
186        let mut all_sections: BTreeMap<String, Vec<TypeScriptField>> = BTreeMap::new();
187
188        // Collect all interface sections from all handlers
189        for handler in &self.spec.handlers {
190            let interface_sections = self.extract_interface_sections_from_handler(handler);
191
192            for (section_name, mut fields) in interface_sections {
193                all_sections
194                    .entry(section_name)
195                    .or_default()
196                    .append(&mut fields);
197            }
198        }
199
200        // Add unmapped fields from spec.sections ONCE (not per handler)
201        // These are fields without #[map] or #[event] attributes
202        self.add_unmapped_fields(&mut all_sections);
203
204        all_sections
205    }
206
207    fn deduplicate_fields(&self, mut fields: Vec<TypeScriptField>) -> Vec<TypeScriptField> {
208        let mut seen = HashSet::new();
209        let mut unique_fields = Vec::new();
210
211        // Sort fields by name for consistent output
212        fields.sort_by(|a, b| a.name.cmp(&b.name));
213
214        for field in fields {
215            if seen.insert(field.name.clone()) {
216                unique_fields.push(field);
217            }
218        }
219
220        unique_fields
221    }
222
223    fn extract_interface_sections_from_handler(
224        &self,
225        handler: &TypedHandlerSpec<S>,
226    ) -> BTreeMap<String, Vec<TypeScriptField>> {
227        let mut sections: BTreeMap<String, Vec<TypeScriptField>> = BTreeMap::new();
228
229        for mapping in &handler.mappings {
230            if !mapping.emit {
231                continue;
232            }
233            let parts: Vec<&str> = mapping.target_path.split('.').collect();
234
235            if parts.len() > 1 {
236                let section_name = parts[0];
237                let field_name = parts[1];
238
239                let ts_field = TypeScriptField {
240                    name: field_name.to_string(),
241                    ts_type: self.mapping_to_typescript_type(mapping),
242                    optional: self.is_field_optional(mapping),
243                    description: None,
244                };
245
246                sections
247                    .entry(section_name.to_string())
248                    .or_default()
249                    .push(ts_field);
250            } else {
251                let ts_field = TypeScriptField {
252                    name: mapping.target_path.clone(),
253                    ts_type: self.mapping_to_typescript_type(mapping),
254                    optional: self.is_field_optional(mapping),
255                    description: None,
256                };
257
258                sections
259                    .entry("Root".to_string())
260                    .or_default()
261                    .push(ts_field);
262            }
263        }
264
265        sections
266    }
267
268    fn add_unmapped_fields(&self, sections: &mut BTreeMap<String, Vec<TypeScriptField>>) {
269        // NEW: Enhanced approach using AST type information if available
270        if !self.spec.sections.is_empty() {
271            // Use type information from the enhanced AST
272            for section in &self.spec.sections {
273                let section_fields = sections.entry(section.name.clone()).or_default();
274
275                for field_info in &section.fields {
276                    if !field_info.emit {
277                        continue;
278                    }
279                    // Check if field is already mapped
280                    let already_exists = section_fields
281                        .iter()
282                        .any(|f| f.name == field_info.field_name);
283
284                    if !already_exists {
285                        section_fields.push(TypeScriptField {
286                            name: field_info.field_name.clone(),
287                            ts_type: self.field_type_info_to_typescript(field_info),
288                            optional: field_info.is_optional,
289                            description: None,
290                        });
291                    }
292                }
293            }
294        } else {
295            // FALLBACK: Use field mappings from spec if sections aren't available yet
296            for (field_path, field_type_info) in &self.spec.field_mappings {
297                if !field_type_info.emit {
298                    continue;
299                }
300                let parts: Vec<&str> = field_path.split('.').collect();
301                if parts.len() > 1 {
302                    let section_name = parts[0];
303                    let field_name = parts[1];
304
305                    let section_fields = sections.entry(section_name.to_string()).or_default();
306
307                    let already_exists = section_fields.iter().any(|f| f.name == field_name);
308
309                    if !already_exists {
310                        section_fields.push(TypeScriptField {
311                            name: field_name.to_string(),
312                            ts_type: self.base_type_to_typescript(
313                                &field_type_info.base_type,
314                                field_type_info.is_array,
315                            ),
316                            optional: field_type_info.is_optional,
317                            description: None,
318                        });
319                    }
320                }
321            }
322        }
323    }
324
325    fn generate_interface_from_fields(&self, name: &str, fields: &[TypeScriptField]) -> String {
326        let interface_name = self.section_interface_name(name);
327
328        // All fields are optional (?) since we receive patches - field may not yet exist
329        // For spec-optional fields, we use `T | null` to distinguish "explicitly null" from "not received"
330        let field_definitions: Vec<String> = fields
331            .iter()
332            .map(|field| {
333                let ts_type = if field.optional {
334                    // Spec-optional: can be explicitly null
335                    format!("{} | null", field.ts_type)
336                } else {
337                    field.ts_type.clone()
338                };
339                format!("  {}?: {};", field.name, ts_type)
340            })
341            .collect();
342
343        format!(
344            "export interface {} {{\n{}\n}}",
345            interface_name,
346            field_definitions.join("\n")
347        )
348    }
349
350    fn section_interface_name(&self, name: &str) -> String {
351        if name == "Root" {
352            format!(
353                "{}{}",
354                self.config.interface_prefix,
355                to_pascal_case(&self.entity_name)
356            )
357        } else {
358            // Create compound names like GameEvents, GameStatus, etc.
359            // Extract the base name (e.g., "Game" from "TestGame" or "SettlementGame")
360            let base_name = if self.entity_name.contains("Game") {
361                "Game"
362            } else {
363                &self.entity_name
364            };
365            format!(
366                "{}{}{}",
367                self.config.interface_prefix,
368                base_name,
369                to_pascal_case(name)
370            )
371        }
372    }
373
374    fn generate_main_entity_interface(&self) -> String {
375        let entity_name = to_pascal_case(&self.entity_name);
376
377        // Extract all top-level sections from the handlers
378        let mut sections = BTreeMap::new();
379
380        for handler in &self.spec.handlers {
381            for mapping in &handler.mappings {
382                if !mapping.emit {
383                    continue;
384                }
385                let parts: Vec<&str> = mapping.target_path.split('.').collect();
386                if parts.len() > 1 {
387                    sections.insert(parts[0], true);
388                }
389            }
390        }
391
392        if !self.spec.sections.is_empty() {
393            for section in &self.spec.sections {
394                if section.fields.iter().any(|field| field.emit) {
395                    sections.insert(&section.name, true);
396                }
397            }
398        } else {
399            for mapping in &self.spec.handlers {
400                for field_mapping in &mapping.mappings {
401                    if !field_mapping.emit {
402                        continue;
403                    }
404                    let parts: Vec<&str> = field_mapping.target_path.split('.').collect();
405                    if parts.len() > 1 {
406                        sections.insert(parts[0], true);
407                    }
408                }
409            }
410        }
411
412        let mut fields = Vec::new();
413
414        // Add non-root sections as nested interface references
415        // All fields are optional since we receive patches
416        for section in sections.keys() {
417            if !is_root_section(section) {
418                let base_name = if self.entity_name.contains("Game") {
419                    "Game"
420                } else {
421                    &self.entity_name
422                };
423                let section_interface_name = format!("{}{}", base_name, to_pascal_case(section));
424                // Keep section field names as-is (snake_case from AST)
425                fields.push(format!("  {}?: {};", section, section_interface_name));
426            }
427        }
428
429        // Flatten root section fields directly into main interface
430        // All fields are optional (?) since we receive patches
431        for section in &self.spec.sections {
432            if is_root_section(&section.name) {
433                for field in &section.fields {
434                    if !field.emit {
435                        continue;
436                    }
437                    let base_ts_type = self.field_type_info_to_typescript(field);
438                    let ts_type = if field.is_optional {
439                        format!("{} | null", base_ts_type)
440                    } else {
441                        base_ts_type
442                    };
443                    fields.push(format!("  {}?: {};", field.field_name, ts_type));
444                }
445            }
446        }
447
448        if fields.is_empty() {
449            fields.push("  // Generated interface - extend as needed".to_string());
450        }
451
452        format!(
453            "export interface {} {{\n{}\n}}",
454            entity_name,
455            fields.join("\n")
456        )
457    }
458
459    fn generate_schemas(&self) -> SchemaOutput {
460        let mut definitions = Vec::new();
461        let mut names = Vec::new();
462        let mut seen = HashSet::new();
463
464        let mut push_schema = |schema_name: String, definition: String| {
465            if seen.insert(schema_name.clone()) {
466                names.push(schema_name);
467                definitions.push(definition);
468            }
469        };
470
471        for (schema_name, definition) in self.generate_builtin_resolver_schemas() {
472            push_schema(schema_name, definition);
473        }
474
475        if self.has_event_types() {
476            push_schema(
477                "EventWrapperSchema".to_string(),
478                self.generate_event_wrapper_schema(),
479            );
480        }
481
482        for (schema_name, definition) in self.generate_resolved_type_schemas() {
483            push_schema(schema_name, definition);
484        }
485
486        for (schema_name, definition) in self.generate_event_schemas() {
487            push_schema(schema_name, definition);
488        }
489
490        for (schema_name, definition) in self.generate_idl_enum_schemas() {
491            push_schema(schema_name, definition);
492        }
493
494        let all_sections = self.collect_interface_sections();
495
496        for (section_name, fields) in &all_sections {
497            if is_root_section(section_name) {
498                continue;
499            }
500            let deduplicated_fields = self.deduplicate_fields(fields.clone());
501            let interface_name = self.section_interface_name(section_name);
502            let schema_definition =
503                self.generate_schema_for_fields(&interface_name, &deduplicated_fields, false);
504            push_schema(format!("{}Schema", interface_name), schema_definition);
505        }
506
507        let entity_name = to_pascal_case(&self.entity_name);
508        let main_fields = self.collect_main_entity_fields();
509        let entity_schema = self.generate_schema_for_fields(&entity_name, &main_fields, false);
510        push_schema(format!("{}Schema", entity_name), entity_schema);
511
512        let completed_schema = self.generate_completed_entity_schema(&entity_name);
513        push_schema(format!("{}CompletedSchema", entity_name), completed_schema);
514
515        SchemaOutput {
516            definitions: definitions.join("\n\n"),
517            names,
518        }
519    }
520
521    fn generate_event_wrapper_schema(&self) -> String {
522        r#"export const EventWrapperSchema = <T extends z.ZodTypeAny>(data: T) => z.object({
523  timestamp: z.number(),
524  data,
525  slot: z.number().optional(),
526  signature: z.string().optional(),
527});"#
528            .to_string()
529    }
530
531    fn generate_builtin_resolver_schemas(&self) -> Vec<(String, String)> {
532        let mut schemas = Vec::new();
533        let registry = crate::resolvers::builtin_resolver_registry();
534
535        for resolver in registry.definitions() {
536            if self.uses_builtin_type(resolver.output_type()) {
537                if let Some(schema) = resolver.typescript_schema() {
538                    schemas.push((schema.name.to_string(), schema.definition.to_string()));
539                }
540            }
541        }
542
543        schemas
544    }
545
546    fn uses_builtin_type(&self, type_name: &str) -> bool {
547        for section in &self.spec.sections {
548            for field in &section.fields {
549                if field.inner_type.as_deref() == Some(type_name) {
550                    return true;
551                }
552            }
553        }
554        false
555    }
556
557    fn generate_builtin_resolver_interfaces(&self) -> Vec<String> {
558        let mut interfaces = Vec::new();
559        let registry = crate::resolvers::builtin_resolver_registry();
560
561        for resolver in registry.definitions() {
562            if self.uses_builtin_type(resolver.output_type()) {
563                if let Some(interface) = resolver.typescript_interface() {
564                    interfaces.push(interface.to_string());
565                }
566            }
567        }
568
569        interfaces
570    }
571
572    fn collect_main_entity_fields(&self) -> Vec<TypeScriptField> {
573        let mut sections = BTreeMap::new();
574
575        for handler in &self.spec.handlers {
576            for mapping in &handler.mappings {
577                if !mapping.emit {
578                    continue;
579                }
580                let parts: Vec<&str> = mapping.target_path.split('.').collect();
581                if parts.len() > 1 {
582                    sections.insert(parts[0], true);
583                }
584            }
585        }
586
587        if !self.spec.sections.is_empty() {
588            for section in &self.spec.sections {
589                if section.fields.iter().any(|field| field.emit) {
590                    sections.insert(&section.name, true);
591                }
592            }
593        } else {
594            for mapping in &self.spec.handlers {
595                for field_mapping in &mapping.mappings {
596                    if !field_mapping.emit {
597                        continue;
598                    }
599                    let parts: Vec<&str> = field_mapping.target_path.split('.').collect();
600                    if parts.len() > 1 {
601                        sections.insert(parts[0], true);
602                    }
603                }
604            }
605        }
606
607        let mut fields = Vec::new();
608
609        for section in sections.keys() {
610            if !is_root_section(section) {
611                let base_name = if self.entity_name.contains("Game") {
612                    "Game"
613                } else {
614                    &self.entity_name
615                };
616                let section_interface_name = format!("{}{}", base_name, to_pascal_case(section));
617                fields.push(TypeScriptField {
618                    name: section.to_string(),
619                    ts_type: section_interface_name,
620                    optional: false,
621                    description: None,
622                });
623            }
624        }
625
626        for section in &self.spec.sections {
627            if is_root_section(&section.name) {
628                for field in &section.fields {
629                    if !field.emit {
630                        continue;
631                    }
632                    fields.push(TypeScriptField {
633                        name: field.field_name.clone(),
634                        ts_type: self.field_type_info_to_typescript(field),
635                        optional: field.is_optional,
636                        description: None,
637                    });
638                }
639            }
640        }
641
642        fields
643    }
644
645    fn generate_schema_for_fields(
646        &self,
647        name: &str,
648        fields: &[TypeScriptField],
649        required: bool,
650    ) -> String {
651        let mut field_definitions = Vec::new();
652
653        for field in fields {
654            let base_schema = self.typescript_type_to_zod(&field.ts_type);
655            let schema = if required {
656                base_schema
657            } else {
658                let with_nullable = if field.optional {
659                    format!("{}.nullable()", base_schema)
660                } else {
661                    base_schema
662                };
663                format!("{}.optional()", with_nullable)
664            };
665
666            field_definitions.push(format!("  {}: {},", field.name, schema));
667        }
668
669        format!(
670            "export const {}Schema = z.object({{\n{}\n}});",
671            name,
672            field_definitions.join("\n")
673        )
674    }
675
676    fn generate_completed_entity_schema(&self, entity_name: &str) -> String {
677        let main_fields = self.collect_main_entity_fields();
678        self.generate_schema_for_fields(&format!("{}Completed", entity_name), &main_fields, true)
679    }
680
681    fn generate_resolved_type_schemas(&self) -> Vec<(String, String)> {
682        let mut schemas = Vec::new();
683        let mut generated_types = HashSet::new();
684
685        for section in &self.spec.sections {
686            for field_info in &section.fields {
687                if let Some(resolved) = &field_info.resolved_type {
688                    let type_name = to_pascal_case(&resolved.type_name);
689
690                    if !generated_types.insert(type_name.clone()) {
691                        continue;
692                    }
693
694                    if resolved.is_enum {
695                        let variants: Vec<String> = resolved
696                            .enum_variants
697                            .iter()
698                            .map(|v| format!("\"{}\"", to_pascal_case(v)))
699                            .collect();
700                        let schema = if variants.is_empty() {
701                            format!("export const {}Schema = z.string();", type_name)
702                        } else {
703                            format!(
704                                "export const {}Schema = z.enum([{}]);",
705                                type_name,
706                                variants.join(", ")
707                            )
708                        };
709                        schemas.push((format!("{}Schema", type_name), schema));
710                        continue;
711                    }
712
713                    let mut field_definitions = Vec::new();
714                    for field in &resolved.fields {
715                        let base = self.resolved_field_to_zod(field);
716                        let schema = if field.is_optional {
717                            format!("{}.nullable().optional()", base)
718                        } else {
719                            format!("{}.optional()", base)
720                        };
721                        field_definitions.push(format!("  {}: {},", field.field_name, schema));
722                    }
723
724                    let schema = format!(
725                        "export const {}Schema = z.object({{\n{}\n}});",
726                        type_name,
727                        field_definitions.join("\n")
728                    );
729                    schemas.push((format!("{}Schema", type_name), schema));
730                }
731            }
732        }
733
734        schemas
735    }
736
737    fn generate_event_schemas(&self) -> Vec<(String, String)> {
738        let mut schemas = Vec::new();
739        let mut generated_types = HashSet::new();
740
741        let handlers = match &self.handlers_json {
742            Some(h) => h.as_array(),
743            None => return schemas,
744        };
745
746        let handlers_array = match handlers {
747            Some(arr) => arr,
748            None => return schemas,
749        };
750
751        for handler in handlers_array {
752            if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
753                for mapping in mappings {
754                    if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
755                        if target_path.contains(".events.") || target_path.starts_with("events.") {
756                            if let Some(source) = mapping.get("source") {
757                                if let Some(event_data) = self.extract_event_data(source) {
758                                    if let Some(handler_source) = handler.get("source") {
759                                        if let Some(instruction_name) =
760                                            self.extract_instruction_name(handler_source)
761                                        {
762                                            let event_field_name =
763                                                target_path.split('.').next_back().unwrap_or("");
764                                            let interface_name = format!(
765                                                "{}Event",
766                                                to_pascal_case(event_field_name)
767                                            );
768
769                                            if generated_types.insert(interface_name.clone()) {
770                                                if let Some(schema) = self
771                                                    .generate_event_schema_from_idl(
772                                                        &interface_name,
773                                                        &instruction_name,
774                                                        &event_data,
775                                                    )
776                                                {
777                                                    schemas.push((
778                                                        format!("{}Schema", interface_name),
779                                                        schema,
780                                                    ));
781                                                }
782                                            }
783                                        }
784                                    }
785                                }
786                            }
787                        }
788                    }
789                }
790            }
791        }
792
793        schemas
794    }
795
796    fn generate_event_schema_from_idl(
797        &self,
798        interface_name: &str,
799        rust_instruction_name: &str,
800        captured_fields: &[(String, Option<String>)],
801    ) -> Option<String> {
802        if captured_fields.is_empty() {
803            return Some(format!(
804                "export const {}Schema = z.object({{}});",
805                interface_name
806            ));
807        }
808
809        let idl_value = self.idl.as_ref()?;
810        let instructions = idl_value.get("instructions")?.as_array()?;
811
812        let instruction = self.find_instruction_in_idl(instructions, rust_instruction_name)?;
813        let args = instruction.get("args")?.as_array()?;
814
815        let mut fields = Vec::new();
816        for (field_name, transform) in captured_fields {
817            for arg in args {
818                if let Some(arg_name) = arg.get("name").and_then(|n| n.as_str()) {
819                    if arg_name == field_name {
820                        if let Some(arg_type) = arg.get("type") {
821                            let ts_type =
822                                self.idl_type_to_typescript(arg_type, transform.as_deref());
823                            let schema = self.typescript_type_to_zod(&ts_type);
824                            fields.push(format!("  {}: {},", field_name, schema));
825                        }
826                        break;
827                    }
828                }
829            }
830        }
831
832        Some(format!(
833            "export const {}Schema = z.object({{\n{}\n}});",
834            interface_name,
835            fields.join("\n")
836        ))
837    }
838
839    fn generate_idl_enum_schemas(&self) -> Vec<(String, String)> {
840        let mut schemas = Vec::new();
841        let mut generated_types = HashSet::new();
842
843        let idl_value = match &self.idl {
844            Some(idl) => idl,
845            None => return schemas,
846        };
847
848        let types_array = match idl_value.get("types").and_then(|v| v.as_array()) {
849            Some(types) => types,
850            None => return schemas,
851        };
852
853        for type_def in types_array {
854            if let (Some(type_name), Some(type_obj)) = (
855                type_def.get("name").and_then(|v| v.as_str()),
856                type_def.get("type").and_then(|v| v.as_object()),
857            ) {
858                if type_obj.get("kind").and_then(|v| v.as_str()) == Some("enum") {
859                    if !generated_types.insert(type_name.to_string()) {
860                        continue;
861                    }
862                    if let Some(variants) = type_obj.get("variants").and_then(|v| v.as_array()) {
863                        let variant_names: Vec<String> = variants
864                            .iter()
865                            .filter_map(|v| v.get("name").and_then(|n| n.as_str()))
866                            .map(|s| format!("\"{}\"", to_pascal_case(s)))
867                            .collect();
868
869                        let interface_name = to_pascal_case(type_name);
870                        let schema = if variant_names.is_empty() {
871                            format!("export const {}Schema = z.string();", interface_name)
872                        } else {
873                            format!(
874                                "export const {}Schema = z.enum([{}]);",
875                                interface_name,
876                                variant_names.join(", ")
877                            )
878                        };
879                        schemas.push((format!("{}Schema", interface_name), schema));
880                    }
881                }
882            }
883        }
884
885        schemas
886    }
887
888    fn typescript_type_to_zod(&self, ts_type: &str) -> String {
889        let trimmed = ts_type.trim();
890
891        if let Some(inner) = trimmed.strip_suffix("[]") {
892            return format!("z.array({})", self.typescript_type_to_zod(inner));
893        }
894
895        if let Some(inner) = trimmed.strip_prefix("EventWrapper<") {
896            if let Some(inner) = inner.strip_suffix('>') {
897                return format!("EventWrapperSchema({})", self.typescript_type_to_zod(inner));
898            }
899        }
900
901        match trimmed {
902            "string" => "z.string()".to_string(),
903            "number" => "z.number()".to_string(),
904            "boolean" => "z.boolean()".to_string(),
905            "any" => "z.any()".to_string(),
906            "Record<string, any>" => "z.record(z.any())".to_string(),
907            _ => format!("{}Schema", trimmed),
908        }
909    }
910
911    fn resolved_field_to_zod(&self, field: &ResolvedField) -> String {
912        let base = self.base_type_to_zod(&field.base_type);
913        if field.is_array {
914            format!("z.array({})", base)
915        } else {
916            base
917        }
918    }
919
920    fn generate_stack_definition(&self) -> String {
921        let stack_name = to_kebab_case(&self.entity_name);
922        let entity_pascal = to_pascal_case(&self.entity_name);
923        let export_name = format!(
924            "{}_{}",
925            self.entity_name.to_uppercase(),
926            self.config.export_const_name
927        );
928
929        let view_helpers = self.generate_view_helpers();
930        let derived_views = self.generate_derived_view_entries();
931        let schema_names = self.generate_schemas().names;
932        let mut unique_schemas: BTreeSet<String> = BTreeSet::new();
933        for name in schema_names {
934            unique_schemas.insert(name);
935        }
936        let schemas_block = if unique_schemas.is_empty() {
937            String::new()
938        } else {
939            let schema_entries: Vec<String> = unique_schemas
940                .iter()
941                .map(|name| format!("    {}: {},", name.trim_end_matches("Schema"), name))
942                .collect();
943            format!("\n  schemas: {{\n{}\n  }},", schema_entries.join("\n"))
944        };
945
946        // Generate URL line - either actual URL or placeholder comment
947        let url_line = match &self.config.url {
948            Some(url) => format!("  url: '{}',", url),
949            None => "  // url: 'wss://your-stack-url.stack.usehyperstack.com', // TODO: Set after first deployment".to_string(),
950        };
951
952        format!(
953            r#"{}
954
955// ============================================================================
956// Stack Definition
957// ============================================================================
958
959/** Stack definition for {} */
960export const {} = {{
961  name: '{}',
962{}
963  views: {{
964    {}: {{
965      state: stateView<{}>('{}/state'),
966      list: listView<{}>('{}/list'),{}
967    }},
968  }},{}
969}} as const;
970
971/** Type alias for the stack */
972export type {}Stack = typeof {};
973
974/** Default export for convenience */
975export default {};"#,
976            view_helpers,
977            entity_pascal,
978            export_name,
979            stack_name,
980            url_line,
981            self.entity_name,
982            entity_pascal,
983            self.entity_name,
984            entity_pascal,
985            self.entity_name,
986            derived_views,
987            schemas_block,
988            entity_pascal,
989            export_name,
990            export_name
991        )
992    }
993
994    fn generate_derived_view_entries(&self) -> String {
995        let derived_views: Vec<&ViewDef> = self
996            .views
997            .iter()
998            .filter(|v| {
999                !v.id.ends_with("/state")
1000                    && !v.id.ends_with("/list")
1001                    && v.id.starts_with(&self.entity_name)
1002            })
1003            .collect();
1004
1005        if derived_views.is_empty() {
1006            return String::new();
1007        }
1008
1009        let entity_pascal = to_pascal_case(&self.entity_name);
1010        let mut entries = Vec::new();
1011
1012        for view in derived_views {
1013            let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
1014
1015            entries.push(format!(
1016                "\n      {}: listView<{}>('{}'),",
1017                view_name, entity_pascal, view.id
1018            ));
1019        }
1020
1021        entries.join("")
1022    }
1023
1024    fn mapping_to_typescript_type(&self, mapping: &TypedFieldMapping<S>) -> String {
1025        // First, try to resolve from AST field mappings
1026        if let Some(field_info) = self.spec.field_mappings.get(&mapping.target_path) {
1027            let ts_type = self.field_type_info_to_typescript(field_info);
1028
1029            // If it's an Append strategy, wrap in array
1030            if matches!(mapping.population, PopulationStrategy::Append) {
1031                return if ts_type.ends_with("[]") {
1032                    ts_type
1033                } else {
1034                    format!("{}[]", ts_type)
1035                };
1036            }
1037
1038            return ts_type;
1039        }
1040
1041        // Fallback to legacy inference
1042        match &mapping.population {
1043            PopulationStrategy::Append => {
1044                // For arrays, try to infer the element type
1045                match &mapping.source {
1046                    MappingSource::AsEvent { .. } => "any[]".to_string(),
1047                    _ => "any[]".to_string(),
1048                }
1049            }
1050            _ => {
1051                // Infer type from source and field name
1052                let base_type = match &mapping.source {
1053                    MappingSource::FromSource { .. } => {
1054                        self.infer_type_from_field_name(&mapping.target_path)
1055                    }
1056                    MappingSource::Constant(value) => value_to_typescript_type(value),
1057                    MappingSource::AsEvent { .. } => "any".to_string(),
1058                    _ => "any".to_string(),
1059                };
1060
1061                // Apply transformations to type
1062                if let Some(transform) = &mapping.transform {
1063                    match transform {
1064                        Transformation::HexEncode | Transformation::HexDecode => {
1065                            "string".to_string()
1066                        }
1067                        Transformation::Base58Encode | Transformation::Base58Decode => {
1068                            "string".to_string()
1069                        }
1070                        Transformation::ToString => "string".to_string(),
1071                        Transformation::ToNumber => "number".to_string(),
1072                    }
1073                } else {
1074                    base_type
1075                }
1076            }
1077        }
1078    }
1079
1080    fn field_type_info_to_typescript(&self, field_info: &FieldTypeInfo) -> String {
1081        if let Some(resolved) = &field_info.resolved_type {
1082            let interface_name = self.resolved_type_to_interface_name(resolved);
1083
1084            let base_type = if resolved.is_event || (resolved.is_instruction && field_info.is_array)
1085            {
1086                format!("EventWrapper<{}>", interface_name)
1087            } else {
1088                interface_name
1089            };
1090
1091            let with_array = if field_info.is_array {
1092                format!("{}[]", base_type)
1093            } else {
1094                base_type
1095            };
1096
1097            return with_array;
1098        }
1099
1100        if let Some(inner_type) = &field_info.inner_type {
1101            if is_builtin_resolver_type(inner_type) {
1102                return inner_type.clone();
1103            }
1104        }
1105
1106        if field_info.base_type == BaseType::Any
1107            || (field_info.base_type == BaseType::Array
1108                && field_info.inner_type.as_deref() == Some("Value"))
1109        {
1110            if let Some(event_type) = self.find_event_interface_for_field(&field_info.field_name) {
1111                return if field_info.is_array {
1112                    format!("{}[]", event_type)
1113                } else if field_info.is_optional {
1114                    format!("{} | null", event_type)
1115                } else {
1116                    event_type
1117                };
1118            }
1119        }
1120
1121        self.base_type_to_typescript(&field_info.base_type, field_info.is_array)
1122    }
1123
1124    /// Find the generated event interface name for a given field
1125    fn find_event_interface_for_field(&self, field_name: &str) -> Option<String> {
1126        // Use the raw JSON handlers if available
1127        let handlers = self.handlers_json.as_ref()?.as_array()?;
1128
1129        // Look through handlers to find event mappings for this field
1130        for handler in handlers {
1131            if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
1132                for mapping in mappings {
1133                    if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
1134                        // Check if this mapping targets our field (e.g., "events.created")
1135                        let target_parts: Vec<&str> = target_path.split('.').collect();
1136                        if let Some(target_field) = target_parts.last() {
1137                            if *target_field == field_name {
1138                                // Check if this is an event mapping
1139                                if let Some(source) = mapping.get("source") {
1140                                    if self.extract_event_data(source).is_some() {
1141                                        // Generate the interface name (e.g., "created" -> "CreatedEvent")
1142                                        return Some(format!(
1143                                            "{}Event",
1144                                            to_pascal_case(field_name)
1145                                        ));
1146                                    }
1147                                }
1148                            }
1149                        }
1150                    }
1151                }
1152            }
1153        }
1154        None
1155    }
1156
1157    /// Generate TypeScript interface name from resolved type
1158    fn resolved_type_to_interface_name(&self, resolved: &ResolvedStructType) -> String {
1159        to_pascal_case(&resolved.type_name)
1160    }
1161
1162    /// Generate nested interfaces for all resolved types in the AST
1163    fn generate_nested_interfaces(&self) -> Vec<String> {
1164        let mut interfaces = Vec::new();
1165        let mut generated_types = HashSet::new();
1166
1167        // Collect all resolved types from all sections
1168        for section in &self.spec.sections {
1169            for field_info in &section.fields {
1170                if let Some(resolved) = &field_info.resolved_type {
1171                    let type_name = resolved.type_name.clone();
1172
1173                    // Only generate each type once
1174                    if generated_types.insert(type_name) {
1175                        let interface = self.generate_interface_for_resolved_type(resolved);
1176                        interfaces.push(interface);
1177                    }
1178                }
1179            }
1180        }
1181
1182        // Generate event interfaces from instruction handlers
1183        interfaces.extend(self.generate_event_interfaces(&mut generated_types));
1184
1185        // Also generate all enum types from the IDL (even if not directly referenced)
1186        if let Some(idl_value) = &self.idl {
1187            if let Some(types_array) = idl_value.get("types").and_then(|v| v.as_array()) {
1188                for type_def in types_array {
1189                    if let (Some(type_name), Some(type_obj)) = (
1190                        type_def.get("name").and_then(|v| v.as_str()),
1191                        type_def.get("type").and_then(|v| v.as_object()),
1192                    ) {
1193                        if type_obj.get("kind").and_then(|v| v.as_str()) == Some("enum") {
1194                            // Only generate if not already generated
1195                            if generated_types.insert(type_name.to_string()) {
1196                                if let Some(variants) =
1197                                    type_obj.get("variants").and_then(|v| v.as_array())
1198                                {
1199                                    let variant_names: Vec<String> = variants
1200                                        .iter()
1201                                        .filter_map(|v| {
1202                                            v.get("name")
1203                                                .and_then(|n| n.as_str())
1204                                                .map(|s| s.to_string())
1205                                        })
1206                                        .collect();
1207
1208                                    if !variant_names.is_empty() {
1209                                        let interface_name = to_pascal_case(type_name);
1210                                        let variant_strings: Vec<String> = variant_names
1211                                            .iter()
1212                                            .map(|v| format!("\"{}\"", to_pascal_case(v)))
1213                                            .collect();
1214
1215                                        let enum_type = format!(
1216                                            "export type {} = {};",
1217                                            interface_name,
1218                                            variant_strings.join(" | ")
1219                                        );
1220                                        interfaces.push(enum_type);
1221                                    }
1222                                }
1223                            }
1224                        }
1225                    }
1226                }
1227            }
1228        }
1229
1230        interfaces
1231    }
1232
1233    /// Generate TypeScript interfaces for event types from instruction handlers
1234    fn generate_event_interfaces(&self, generated_types: &mut HashSet<String>) -> Vec<String> {
1235        let mut interfaces = Vec::new();
1236
1237        // Use the raw JSON handlers if available
1238        let handlers = match &self.handlers_json {
1239            Some(h) => h.as_array(),
1240            None => return interfaces,
1241        };
1242
1243        let handlers_array = match handlers {
1244            Some(arr) => arr,
1245            None => return interfaces,
1246        };
1247
1248        // Look through handlers to find instruction-based event mappings
1249        for handler in handlers_array {
1250            // Check if this handler has event mappings
1251            if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
1252                for mapping in mappings {
1253                    if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
1254                        // Check if the target is an event field (contains ".events." or starts with "events.")
1255                        if target_path.contains(".events.") || target_path.starts_with("events.") {
1256                            // Check if the source is AsEvent
1257                            if let Some(source) = mapping.get("source") {
1258                                if let Some(event_data) = self.extract_event_data(source) {
1259                                    // Extract instruction name from handler source
1260                                    if let Some(handler_source) = handler.get("source") {
1261                                        if let Some(instruction_name) =
1262                                            self.extract_instruction_name(handler_source)
1263                                        {
1264                                            // Generate interface name from target path (e.g., "events.created" -> "CreatedEvent")
1265                                            let event_field_name =
1266                                                target_path.split('.').next_back().unwrap_or("");
1267                                            let interface_name = format!(
1268                                                "{}Event",
1269                                                to_pascal_case(event_field_name)
1270                                            );
1271
1272                                            // Only generate once
1273                                            if generated_types.insert(interface_name.clone()) {
1274                                                if let Some(interface) = self
1275                                                    .generate_event_interface_from_idl(
1276                                                        &interface_name,
1277                                                        &instruction_name,
1278                                                        &event_data,
1279                                                    )
1280                                                {
1281                                                    interfaces.push(interface);
1282                                                }
1283                                            }
1284                                        }
1285                                    }
1286                                }
1287                            }
1288                        }
1289                    }
1290                }
1291            }
1292        }
1293
1294        interfaces
1295    }
1296
1297    /// Extract event field data from a mapping source
1298    fn extract_event_data(
1299        &self,
1300        source: &serde_json::Value,
1301    ) -> Option<Vec<(String, Option<String>)>> {
1302        if let Some(as_event) = source.get("AsEvent") {
1303            if let Some(fields) = as_event.get("fields").and_then(|f| f.as_array()) {
1304                let mut event_fields = Vec::new();
1305                for field in fields {
1306                    if let Some(from_source) = field.get("FromSource") {
1307                        if let Some(path) = from_source
1308                            .get("path")
1309                            .and_then(|p| p.get("segments"))
1310                            .and_then(|s| s.as_array())
1311                        {
1312                            // Get the last segment as the field name (e.g., ["data", "game_id"] -> "game_id")
1313                            if let Some(field_name) = path.last().and_then(|v| v.as_str()) {
1314                                let transform = from_source
1315                                    .get("transform")
1316                                    .and_then(|t| t.as_str())
1317                                    .map(|s| s.to_string());
1318                                event_fields.push((field_name.to_string(), transform));
1319                            }
1320                        }
1321                    }
1322                }
1323                return Some(event_fields);
1324            }
1325        }
1326        None
1327    }
1328
1329    /// Extract instruction name from handler source, returning the raw PascalCase name
1330    fn extract_instruction_name(&self, source: &serde_json::Value) -> Option<String> {
1331        if let Some(source_obj) = source.get("Source") {
1332            if let Some(type_name) = source_obj.get("type_name").and_then(|t| t.as_str()) {
1333                let instruction_part =
1334                    crate::event_type_helpers::strip_event_type_suffix(type_name);
1335                return Some(instruction_part.to_string());
1336            }
1337        }
1338        None
1339    }
1340
1341    /// Find an instruction in the IDL by name, handling different naming conventions.
1342    /// IDLs may use snake_case (pumpfun: "admin_set_creator") or camelCase (ore: "claimSol").
1343    /// The input name comes from Rust types which are PascalCase ("AdminSetCreator", "ClaimSol").
1344    fn find_instruction_in_idl<'a>(
1345        &self,
1346        instructions: &'a [serde_json::Value],
1347        rust_name: &str,
1348    ) -> Option<&'a serde_json::Value> {
1349        let normalized_search = normalize_for_comparison(rust_name);
1350
1351        for instruction in instructions {
1352            if let Some(idl_name) = instruction.get("name").and_then(|n| n.as_str()) {
1353                if normalize_for_comparison(idl_name) == normalized_search {
1354                    return Some(instruction);
1355                }
1356            }
1357        }
1358        None
1359    }
1360
1361    /// Generate a TypeScript interface for an event from IDL instruction data
1362    fn generate_event_interface_from_idl(
1363        &self,
1364        interface_name: &str,
1365        rust_instruction_name: &str,
1366        captured_fields: &[(String, Option<String>)],
1367    ) -> Option<String> {
1368        if captured_fields.is_empty() {
1369            return Some(format!("export interface {} {{}}", interface_name));
1370        }
1371
1372        let idl_value = self.idl.as_ref()?;
1373        let instructions = idl_value.get("instructions")?.as_array()?;
1374
1375        let instruction = self.find_instruction_in_idl(instructions, rust_instruction_name)?;
1376        let args = instruction.get("args")?.as_array()?;
1377
1378        let mut fields = Vec::new();
1379        for (field_name, transform) in captured_fields {
1380            for arg in args {
1381                if let Some(arg_name) = arg.get("name").and_then(|n| n.as_str()) {
1382                    if arg_name == field_name {
1383                        if let Some(arg_type) = arg.get("type") {
1384                            let ts_type =
1385                                self.idl_type_to_typescript(arg_type, transform.as_deref());
1386                            fields.push(format!("  {}: {};", field_name, ts_type));
1387                        }
1388                        break;
1389                    }
1390                }
1391            }
1392        }
1393
1394        if !fields.is_empty() {
1395            return Some(format!(
1396                "export interface {} {{\n{}\n}}",
1397                interface_name,
1398                fields.join("\n")
1399            ));
1400        }
1401
1402        None
1403    }
1404
1405    /// Convert an IDL type (from JSON) to TypeScript, considering transforms
1406    fn idl_type_to_typescript(
1407        &self,
1408        idl_type: &serde_json::Value,
1409        transform: Option<&str>,
1410    ) -> String {
1411        #![allow(clippy::only_used_in_recursion)]
1412        // If there's a HexEncode transform, the result is always a string
1413        if transform == Some("HexEncode") {
1414            return "string".to_string();
1415        }
1416
1417        // Handle different IDL type formats
1418        if let Some(type_str) = idl_type.as_str() {
1419            return match type_str {
1420                "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" => {
1421                    "number".to_string()
1422                }
1423                "f32" | "f64" => "number".to_string(),
1424                "bool" => "boolean".to_string(),
1425                "string" => "string".to_string(),
1426                "pubkey" | "publicKey" => "string".to_string(),
1427                "bytes" => "string".to_string(),
1428                _ => "any".to_string(),
1429            };
1430        }
1431
1432        // Handle complex types (option, vec, etc.)
1433        if let Some(type_obj) = idl_type.as_object() {
1434            if let Some(option_type) = type_obj.get("option") {
1435                let inner = self.idl_type_to_typescript(option_type, None);
1436                return format!("{} | null", inner);
1437            }
1438            if let Some(vec_type) = type_obj.get("vec") {
1439                let inner = self.idl_type_to_typescript(vec_type, None);
1440                return format!("{}[]", inner);
1441            }
1442        }
1443
1444        "any".to_string()
1445    }
1446
1447    /// Generate a TypeScript interface from a resolved struct type
1448    fn generate_interface_for_resolved_type(&self, resolved: &ResolvedStructType) -> String {
1449        let interface_name = to_pascal_case(&resolved.type_name);
1450
1451        // Handle enums as TypeScript union types
1452        if resolved.is_enum {
1453            let variants: Vec<String> = resolved
1454                .enum_variants
1455                .iter()
1456                .map(|v| format!("\"{}\"", to_pascal_case(v)))
1457                .collect();
1458
1459            return format!("export type {} = {};", interface_name, variants.join(" | "));
1460        }
1461
1462        // Handle structs as interfaces
1463        // All fields are optional since we receive patches
1464        let fields: Vec<String> = resolved
1465            .fields
1466            .iter()
1467            .map(|field| {
1468                let base_ts_type = self.resolved_field_to_typescript(field);
1469                let ts_type = if field.is_optional {
1470                    format!("{} | null", base_ts_type)
1471                } else {
1472                    base_ts_type
1473                };
1474                format!("  {}?: {};", field.field_name, ts_type)
1475            })
1476            .collect();
1477
1478        format!(
1479            "export interface {} {{\n{}\n}}",
1480            interface_name,
1481            fields.join("\n")
1482        )
1483    }
1484
1485    /// Convert a resolved field to TypeScript type
1486    fn resolved_field_to_typescript(&self, field: &ResolvedField) -> String {
1487        let base_ts = self.base_type_to_typescript(&field.base_type, false);
1488
1489        if field.is_array {
1490            format!("{}[]", base_ts)
1491        } else {
1492            base_ts
1493        }
1494    }
1495
1496    /// Check if the spec has any event types
1497    fn has_event_types(&self) -> bool {
1498        for section in &self.spec.sections {
1499            for field_info in &section.fields {
1500                if let Some(resolved) = &field_info.resolved_type {
1501                    if resolved.is_event || (resolved.is_instruction && field_info.is_array) {
1502                        return true;
1503                    }
1504                }
1505            }
1506        }
1507        false
1508    }
1509
1510    /// Generate the EventWrapper interface
1511    fn generate_event_wrapper_interface(&self) -> String {
1512        r#"/**
1513 * Wrapper for event data that includes context metadata.
1514 * Events are automatically wrapped in this structure at runtime.
1515 */
1516export interface EventWrapper<T> {
1517  /** Unix timestamp when the event was processed */
1518  timestamp: number;
1519  /** The event-specific data */
1520  data: T;
1521  /** Optional blockchain slot number */
1522  slot?: number;
1523  /** Optional transaction signature */
1524  signature?: string;
1525}"#
1526        .to_string()
1527    }
1528
1529    fn infer_type_from_field_name(&self, field_name: &str) -> String {
1530        let lower_name = field_name.to_lowercase();
1531
1532        // Special case for event fields - these are typically Option<Value> and should be 'any'
1533        if lower_name.contains("events.") {
1534            // For fields in the events section, default to 'any' since they're typically Option<Value>
1535            return "any".to_string();
1536        }
1537
1538        // Common patterns for type inference
1539        if lower_name.contains("id")
1540            || lower_name.contains("count")
1541            || lower_name.contains("number")
1542            || lower_name.contains("timestamp")
1543            || lower_name.contains("time")
1544            || lower_name.contains("at")
1545            || lower_name.contains("volume")
1546            || lower_name.contains("amount")
1547            || lower_name.contains("ev")
1548            || lower_name.contains("fee")
1549            || lower_name.contains("payout")
1550            || lower_name.contains("distributed")
1551            || lower_name.contains("claimable")
1552            || lower_name.contains("total")
1553            || lower_name.contains("rate")
1554            || lower_name.contains("ratio")
1555            || lower_name.contains("current")
1556            || lower_name.contains("state")
1557        {
1558            "number".to_string()
1559        } else if lower_name.contains("status")
1560            || lower_name.contains("hash")
1561            || lower_name.contains("address")
1562            || lower_name.contains("key")
1563        {
1564            "string".to_string()
1565        } else {
1566            "any".to_string()
1567        }
1568    }
1569
1570    fn is_field_optional(&self, mapping: &TypedFieldMapping<S>) -> bool {
1571        // Most fields should be optional by default since we're dealing with Option<T> types
1572        match &mapping.source {
1573            // Constants are typically non-optional
1574            MappingSource::Constant(_) => false,
1575            // Events are typically optional (Option<Value>)
1576            MappingSource::AsEvent { .. } => true,
1577            // For source fields, default to optional since most Rust fields are Option<T>
1578            MappingSource::FromSource { .. } => true,
1579            // Other cases default to optional
1580            _ => true,
1581        }
1582    }
1583
1584    /// Convert language-agnostic base types to TypeScript types
1585    fn base_type_to_typescript(&self, base_type: &BaseType, is_array: bool) -> String {
1586        let base_ts_type = match base_type {
1587            BaseType::Integer => "number",
1588            BaseType::Float => "number",
1589            BaseType::String => "string",
1590            BaseType::Boolean => "boolean",
1591            BaseType::Timestamp => "number", // Unix timestamps as numbers
1592            BaseType::Binary => "string",    // Base64 encoded strings
1593            BaseType::Pubkey => "string",    // Solana public keys as Base58 strings
1594            BaseType::Array => "any[]",      // Default array type
1595            BaseType::Object => "Record<string, any>", // Generic object
1596            BaseType::Any => "any",
1597        };
1598
1599        if is_array && !matches!(base_type, BaseType::Array) {
1600            format!("{}[]", base_ts_type)
1601        } else {
1602            base_ts_type.to_string()
1603        }
1604    }
1605
1606    /// Convert language-agnostic base types to Zod schema expressions
1607    fn base_type_to_zod(&self, base_type: &BaseType) -> String {
1608        match base_type {
1609            BaseType::Integer | BaseType::Float | BaseType::Timestamp => "z.number()".to_string(),
1610            BaseType::String | BaseType::Pubkey | BaseType::Binary => "z.string()".to_string(),
1611            BaseType::Boolean => "z.boolean()".to_string(),
1612            BaseType::Array => "z.array(z.any())".to_string(),
1613            BaseType::Object => "z.record(z.any())".to_string(),
1614            BaseType::Any => "z.any()".to_string(),
1615        }
1616    }
1617}
1618
1619/// Represents a TypeScript field in an interface
1620#[derive(Debug, Clone)]
1621struct TypeScriptField {
1622    name: String,
1623    ts_type: String,
1624    optional: bool,
1625    #[allow(dead_code)]
1626    description: Option<String>,
1627}
1628
1629#[derive(Debug, Clone)]
1630struct SchemaOutput {
1631    definitions: String,
1632    names: Vec<String>,
1633}
1634
1635/// Convert serde_json::Value to TypeScript type string
1636fn value_to_typescript_type(value: &serde_json::Value) -> String {
1637    match value {
1638        serde_json::Value::Number(_) => "number".to_string(),
1639        serde_json::Value::String(_) => "string".to_string(),
1640        serde_json::Value::Bool(_) => "boolean".to_string(),
1641        serde_json::Value::Array(_) => "any[]".to_string(),
1642        serde_json::Value::Object(_) => "Record<string, any>".to_string(),
1643        serde_json::Value::Null => "null".to_string(),
1644    }
1645}
1646
1647/// Convert snake_case to PascalCase
1648fn to_pascal_case(s: &str) -> String {
1649    s.split(['_', '-', '.'])
1650        .map(|word| {
1651            let mut chars = word.chars();
1652            match chars.next() {
1653                None => String::new(),
1654                Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1655            }
1656        })
1657        .collect()
1658}
1659
1660/// Normalize a name for case-insensitive comparison across naming conventions.
1661/// Removes underscores and converts to lowercase: "claim_sol", "claimSol", "ClaimSol" all become "claimsol"
1662fn normalize_for_comparison(s: &str) -> String {
1663    s.chars()
1664        .filter(|c| *c != '_')
1665        .flat_map(|c| c.to_lowercase())
1666        .collect()
1667}
1668
1669fn is_root_section(name: &str) -> bool {
1670    name.eq_ignore_ascii_case("root")
1671}
1672
1673fn is_builtin_resolver_type(type_name: &str) -> bool {
1674    crate::resolvers::is_resolver_output_type(type_name)
1675}
1676
1677/// Convert PascalCase/camelCase to kebab-case
1678fn to_kebab_case(s: &str) -> String {
1679    let mut result = String::new();
1680
1681    for ch in s.chars() {
1682        if ch.is_uppercase() && !result.is_empty() {
1683            result.push('-');
1684        }
1685        result.push(ch.to_lowercase().next().unwrap());
1686    }
1687
1688    result
1689}
1690
1691/// CLI-friendly function to generate TypeScript from a spec function
1692/// This will be used by the CLI tool to generate TypeScript from discovered specs
1693pub fn generate_typescript_from_spec_fn<F, S>(
1694    spec_fn: F,
1695    entity_name: String,
1696    config: Option<TypeScriptConfig>,
1697) -> Result<TypeScriptOutput, String>
1698where
1699    F: Fn() -> TypedStreamSpec<S>,
1700{
1701    let spec = spec_fn();
1702    let compiler =
1703        TypeScriptCompiler::new(spec, entity_name).with_config(config.unwrap_or_default());
1704
1705    Ok(compiler.compile())
1706}
1707
1708/// Write TypeScript output to a file
1709pub fn write_typescript_to_file(
1710    output: &TypeScriptOutput,
1711    path: &std::path::Path,
1712) -> Result<(), std::io::Error> {
1713    std::fs::write(path, output.full_file())
1714}
1715
1716/// Generate TypeScript from a SerializableStreamSpec (for CLI use)
1717/// This allows the CLI to compile TypeScript without needing the typed spec
1718pub fn compile_serializable_spec(
1719    spec: SerializableStreamSpec,
1720    entity_name: String,
1721    config: Option<TypeScriptConfig>,
1722) -> Result<TypeScriptOutput, String> {
1723    let idl = spec
1724        .idl
1725        .as_ref()
1726        .and_then(|idl_snapshot| serde_json::to_value(idl_snapshot).ok());
1727
1728    let handlers = serde_json::to_value(&spec.handlers).ok();
1729    let views = spec.views.clone();
1730
1731    let typed_spec: TypedStreamSpec<()> = TypedStreamSpec::from_serializable(spec);
1732
1733    let compiler = TypeScriptCompiler::new(typed_spec, entity_name)
1734        .with_idl(idl)
1735        .with_handlers_json(handlers)
1736        .with_views(views)
1737        .with_config(config.unwrap_or_default());
1738
1739    Ok(compiler.compile())
1740}
1741
1742#[derive(Debug, Clone)]
1743pub struct TypeScriptStackConfig {
1744    pub package_name: String,
1745    pub generate_helpers: bool,
1746    pub export_const_name: String,
1747    pub url: Option<String>,
1748}
1749
1750impl Default for TypeScriptStackConfig {
1751    fn default() -> Self {
1752        Self {
1753            package_name: "hyperstack-react".to_string(),
1754            generate_helpers: true,
1755            export_const_name: "STACK".to_string(),
1756            url: None,
1757        }
1758    }
1759}
1760
1761#[derive(Debug, Clone)]
1762pub struct TypeScriptStackOutput {
1763    pub interfaces: String,
1764    pub stack_definition: String,
1765    pub imports: String,
1766}
1767
1768impl TypeScriptStackOutput {
1769    pub fn full_file(&self) -> String {
1770        let mut parts = Vec::new();
1771        if !self.imports.is_empty() {
1772            parts.push(self.imports.as_str());
1773        }
1774        if !self.interfaces.is_empty() {
1775            parts.push(self.interfaces.as_str());
1776        }
1777        if !self.stack_definition.is_empty() {
1778            parts.push(self.stack_definition.as_str());
1779        }
1780        parts.join("\n\n")
1781    }
1782}
1783
1784/// Compile a full SerializableStackSpec (multi-entity) into a single TypeScript file.
1785///
1786/// Generates:
1787/// - Interfaces for ALL entities (OreRound, OreTreasury, OreMiner, etc.)
1788/// - A single unified stack definition with nested views per entity
1789/// - View helpers (stateView, listView)
1790pub fn compile_stack_spec(
1791    stack_spec: SerializableStackSpec,
1792    config: Option<TypeScriptStackConfig>,
1793) -> Result<TypeScriptStackOutput, String> {
1794    let config = config.unwrap_or_default();
1795    let stack_name = &stack_spec.stack_name;
1796    let stack_kebab = to_kebab_case(stack_name);
1797
1798    // 1. Compile each entity's interfaces using existing per-entity compiler
1799    let mut all_interfaces = Vec::new();
1800    let mut entity_names = Vec::new();
1801    let mut schema_names: Vec<String> = Vec::new();
1802
1803    for entity_spec in &stack_spec.entities {
1804        let mut spec = entity_spec.clone();
1805        // Inject stack-level IDL if entity doesn't have its own
1806        if spec.idl.is_none() {
1807            spec.idl = stack_spec.idls.first().cloned();
1808        }
1809        let entity_name = spec.state_name.clone();
1810        entity_names.push(entity_name.clone());
1811
1812        let per_entity_config = TypeScriptConfig {
1813            package_name: config.package_name.clone(),
1814            generate_helpers: false,
1815            interface_prefix: String::new(),
1816            export_const_name: config.export_const_name.clone(),
1817            url: config.url.clone(),
1818        };
1819
1820        let output = compile_serializable_spec(spec, entity_name, Some(per_entity_config))?;
1821
1822        // Only take the interfaces part (not the stack_definition — we generate our own)
1823        if !output.interfaces.is_empty() {
1824            all_interfaces.push(output.interfaces);
1825        }
1826
1827        schema_names.extend(output.schema_names);
1828    }
1829
1830    let interfaces = all_interfaces.join("\n\n");
1831
1832    // 2. Generate unified stack definition with all entity views
1833    let stack_definition = generate_stack_definition_multi(
1834        stack_name,
1835        &stack_kebab,
1836        &stack_spec.entities,
1837        &entity_names,
1838        &stack_spec.pdas,
1839        &stack_spec.program_ids,
1840        &schema_names,
1841        &config,
1842    );
1843
1844    let imports = if stack_spec.pdas.values().any(|p| !p.is_empty()) {
1845        "import { z } from 'zod';\nimport { pda, literal, account, arg, bytes } from 'hyperstack-typescript';".to_string()
1846    } else {
1847        "import { z } from 'zod';".to_string()
1848    };
1849
1850    Ok(TypeScriptStackOutput {
1851        imports,
1852        interfaces,
1853        stack_definition,
1854    })
1855}
1856
1857/// Write stack-level TypeScript output to a file
1858pub fn write_stack_typescript_to_file(
1859    output: &TypeScriptStackOutput,
1860    path: &std::path::Path,
1861) -> Result<(), std::io::Error> {
1862    std::fs::write(path, output.full_file())
1863}
1864
1865/// Generate a unified stack definition for multiple entities.
1866///
1867/// Produces something like:
1868/// ```typescript
1869/// export const ORE_STACK = {
1870///   name: 'ore',
1871///   url: 'wss://ore.stack.usehyperstack.com',
1872///   views: {
1873///     OreRound: {
1874///       state: stateView<OreRound>('OreRound/state'),
1875///       list: listView<OreRound>('OreRound/list'),
1876///       latest: listView<OreRound>('OreRound/latest'),
1877///     },
1878///     OreTreasury: {
1879///       state: stateView<OreTreasury>('OreTreasury/state'),
1880///     },
1881///     OreMiner: {
1882///       state: stateView<OreMiner>('OreMiner/state'),
1883///       list: listView<OreMiner>('OreMiner/list'),
1884///     },
1885///   },
1886/// } as const;
1887/// ```
1888#[allow(clippy::too_many_arguments)]
1889fn generate_stack_definition_multi(
1890    stack_name: &str,
1891    stack_kebab: &str,
1892    entities: &[SerializableStreamSpec],
1893    entity_names: &[String],
1894    pdas: &BTreeMap<String, BTreeMap<String, PdaDefinition>>,
1895    program_ids: &[String],
1896    schema_names: &[String],
1897    config: &TypeScriptStackConfig,
1898) -> String {
1899    let export_name = format!(
1900        "{}_{}",
1901        to_screaming_snake_case(stack_name),
1902        config.export_const_name
1903    );
1904
1905    let view_helpers = generate_view_helpers_static();
1906
1907    let url_line = match &config.url {
1908        Some(url) => format!("  url: '{}',", url),
1909        None => "  // url: 'wss://your-stack-url.stack.usehyperstack.com', // TODO: Set after first deployment".to_string(),
1910    };
1911
1912    // Generate views block for each entity
1913    let mut entity_view_blocks = Vec::new();
1914    for (i, entity_spec) in entities.iter().enumerate() {
1915        let entity_name = &entity_names[i];
1916        let entity_pascal = to_pascal_case(entity_name);
1917
1918        let mut view_entries = Vec::new();
1919
1920        view_entries.push(format!(
1921            "      state: stateView<{entity}>('{entity_name}/state'),",
1922            entity = entity_pascal,
1923            entity_name = entity_name
1924        ));
1925
1926        view_entries.push(format!(
1927            "      list: listView<{entity}>('{entity_name}/list'),",
1928            entity = entity_pascal,
1929            entity_name = entity_name
1930        ));
1931
1932        for view in &entity_spec.views {
1933            if !view.id.ends_with("/state")
1934                && !view.id.ends_with("/list")
1935                && view.id.starts_with(entity_name)
1936            {
1937                let view_name = view.id.split('/').nth(1).unwrap_or("unknown");
1938                view_entries.push(format!(
1939                    "      {}: listView<{entity}>('{}'),",
1940                    view_name,
1941                    view.id,
1942                    entity = entity_pascal
1943                ));
1944            }
1945        }
1946
1947        entity_view_blocks.push(format!(
1948            "    {}: {{\n{}\n    }},",
1949            entity_name,
1950            view_entries.join("\n")
1951        ));
1952    }
1953
1954    let views_body = entity_view_blocks.join("\n");
1955
1956    let pdas_block = generate_pdas_block(pdas, program_ids);
1957
1958    let mut unique_schemas: BTreeSet<String> = BTreeSet::new();
1959    for name in schema_names {
1960        unique_schemas.insert(name.clone());
1961    }
1962    let schemas_block = if unique_schemas.is_empty() {
1963        String::new()
1964    } else {
1965        let schema_entries: Vec<String> = unique_schemas
1966            .iter()
1967            .map(|name| format!("    {}: {},", name.trim_end_matches("Schema"), name))
1968            .collect();
1969        format!("\n  schemas: {{\n{}\n  }},", schema_entries.join("\n"))
1970    };
1971
1972    let entity_types: Vec<String> = entity_names.iter().map(|n| to_pascal_case(n)).collect();
1973
1974    format!(
1975        r#"{view_helpers}
1976
1977// ============================================================================
1978// Stack Definition
1979// ============================================================================
1980
1981/** Stack definition for {stack_name} with {entity_count} entities */
1982export const {export_name} = {{
1983  name: '{stack_kebab}',
1984{url_line}
1985  views: {{
1986{views_body}
1987  }},{schemas_section}{pdas_section}
1988}} as const;
1989
1990/** Type alias for the stack */
1991export type {stack_name}Stack = typeof {export_name};
1992
1993/** Entity types in this stack */
1994export type {stack_name}Entity = {entity_union};
1995
1996/** Default export for convenience */
1997export default {export_name};"#,
1998        view_helpers = view_helpers,
1999        stack_name = stack_name,
2000        entity_count = entities.len(),
2001        export_name = export_name,
2002        stack_kebab = stack_kebab,
2003        url_line = url_line,
2004        views_body = views_body,
2005        schemas_section = schemas_block,
2006        pdas_section = pdas_block,
2007        entity_union = entity_types.join(" | "),
2008    )
2009}
2010
2011fn generate_pdas_block(
2012    pdas: &BTreeMap<String, BTreeMap<String, PdaDefinition>>,
2013    program_ids: &[String],
2014) -> String {
2015    if pdas.is_empty() {
2016        return String::new();
2017    }
2018
2019    let mut program_blocks = Vec::new();
2020
2021    for (program_name, program_pdas) in pdas {
2022        if program_pdas.is_empty() {
2023            continue;
2024        }
2025
2026        let program_id = program_ids.first().cloned().unwrap_or_default();
2027
2028        let mut pda_entries = Vec::new();
2029        for (pda_name, pda_def) in program_pdas {
2030            let seeds_str = pda_def
2031                .seeds
2032                .iter()
2033                .map(|seed| match seed {
2034                    PdaSeedDef::Literal { value } => format!("literal('{}')", value),
2035                    PdaSeedDef::AccountRef { account_name } => {
2036                        format!("account('{}')", account_name)
2037                    }
2038                    PdaSeedDef::ArgRef { arg_name, arg_type } => {
2039                        if let Some(t) = arg_type {
2040                            format!("arg('{}', '{}')", arg_name, t)
2041                        } else {
2042                            format!("arg('{}')", arg_name)
2043                        }
2044                    }
2045                    PdaSeedDef::Bytes { value } => {
2046                        let bytes_arr: Vec<String> = value.iter().map(|b| b.to_string()).collect();
2047                        format!("bytes(new Uint8Array([{}]))", bytes_arr.join(", "))
2048                    }
2049                })
2050                .collect::<Vec<_>>()
2051                .join(", ");
2052
2053            let pid = pda_def.program_id.as_ref().unwrap_or(&program_id);
2054            pda_entries.push(format!(
2055                "      {}: pda('{}', {}),",
2056                pda_name, pid, seeds_str
2057            ));
2058        }
2059
2060        program_blocks.push(format!(
2061            "    {}: {{\n{}\n    }},",
2062            program_name,
2063            pda_entries.join("\n")
2064        ));
2065    }
2066
2067    if program_blocks.is_empty() {
2068        return String::new();
2069    }
2070
2071    format!("\n  pdas: {{\n{}\n  }},", program_blocks.join("\n"))
2072}
2073
2074fn generate_view_helpers_static() -> String {
2075    r#"// ============================================================================
2076// View Definition Types (framework-agnostic)
2077// ============================================================================
2078
2079/** View definition with embedded entity type */
2080export interface ViewDef<T, TMode extends 'state' | 'list'> {
2081  readonly mode: TMode;
2082  readonly view: string;
2083  /** Phantom field for type inference - not present at runtime */
2084  readonly _entity?: T;
2085}
2086
2087/** Helper to create typed state view definitions (keyed lookups) */
2088function stateView<T>(view: string): ViewDef<T, 'state'> {
2089  return { mode: 'state', view } as const;
2090}
2091
2092/** Helper to create typed list view definitions (collections) */
2093function listView<T>(view: string): ViewDef<T, 'list'> {
2094  return { mode: 'list', view } as const;
2095}"#
2096    .to_string()
2097}
2098
2099/// Convert PascalCase to SCREAMING_SNAKE_CASE (e.g., "OreStream" -> "ORE_STREAM")
2100fn to_screaming_snake_case(s: &str) -> String {
2101    let mut result = String::new();
2102    for (i, ch) in s.chars().enumerate() {
2103        if ch.is_uppercase() && i > 0 {
2104            result.push('_');
2105        }
2106        result.push(ch.to_uppercase().next().unwrap());
2107    }
2108    result
2109}
2110
2111#[cfg(test)]
2112mod tests {
2113    use super::*;
2114
2115    #[test]
2116    fn test_case_conversions() {
2117        assert_eq!(to_pascal_case("settlement_game"), "SettlementGame");
2118        assert_eq!(to_kebab_case("SettlementGame"), "settlement-game");
2119    }
2120
2121    #[test]
2122    fn test_normalize_for_comparison() {
2123        assert_eq!(normalize_for_comparison("claim_sol"), "claimsol");
2124        assert_eq!(normalize_for_comparison("claimSol"), "claimsol");
2125        assert_eq!(normalize_for_comparison("ClaimSol"), "claimsol");
2126        assert_eq!(
2127            normalize_for_comparison("admin_set_creator"),
2128            "adminsetcreator"
2129        );
2130        assert_eq!(
2131            normalize_for_comparison("AdminSetCreator"),
2132            "adminsetcreator"
2133        );
2134    }
2135
2136    #[test]
2137    fn test_value_to_typescript_type() {
2138        assert_eq!(value_to_typescript_type(&serde_json::json!(42)), "number");
2139        assert_eq!(
2140            value_to_typescript_type(&serde_json::json!("hello")),
2141            "string"
2142        );
2143        assert_eq!(
2144            value_to_typescript_type(&serde_json::json!(true)),
2145            "boolean"
2146        );
2147        assert_eq!(value_to_typescript_type(&serde_json::json!([])), "any[]");
2148    }
2149
2150    #[test]
2151    fn test_derived_view_codegen() {
2152        let spec = SerializableStreamSpec {
2153            state_name: "OreRound".to_string(),
2154            program_id: None,
2155            idl: None,
2156            identity: IdentitySpec {
2157                primary_keys: vec!["id".to_string()],
2158                lookup_indexes: vec![],
2159            },
2160            handlers: vec![],
2161            sections: vec![],
2162            field_mappings: BTreeMap::new(),
2163            resolver_hooks: vec![],
2164            resolver_specs: vec![],
2165            instruction_hooks: vec![],
2166            computed_fields: vec![],
2167            computed_field_specs: vec![],
2168            content_hash: None,
2169            views: vec![
2170                ViewDef {
2171                    id: "OreRound/latest".to_string(),
2172                    source: ViewSource::Entity {
2173                        name: "OreRound".to_string(),
2174                    },
2175                    pipeline: vec![ViewTransform::Last],
2176                    output: ViewOutput::Single,
2177                },
2178                ViewDef {
2179                    id: "OreRound/top10".to_string(),
2180                    source: ViewSource::Entity {
2181                        name: "OreRound".to_string(),
2182                    },
2183                    pipeline: vec![ViewTransform::Take { count: 10 }],
2184                    output: ViewOutput::Collection,
2185                },
2186            ],
2187        };
2188
2189        let output =
2190            compile_serializable_spec(spec, "OreRound".to_string(), None).expect("should compile");
2191
2192        let stack_def = &output.stack_definition;
2193
2194        assert!(
2195            stack_def.contains("listView<OreRound>('OreRound/latest')"),
2196            "Expected 'latest' derived view using listView, got:\n{}",
2197            stack_def
2198        );
2199        assert!(
2200            stack_def.contains("listView<OreRound>('OreRound/top10')"),
2201            "Expected 'top10' derived view using listView, got:\n{}",
2202            stack_def
2203        );
2204        assert!(
2205            stack_def.contains("latest:"),
2206            "Expected 'latest' key, got:\n{}",
2207            stack_def
2208        );
2209        assert!(
2210            stack_def.contains("top10:"),
2211            "Expected 'top10' key, got:\n{}",
2212            stack_def
2213        );
2214        assert!(
2215            stack_def.contains("function listView<T>(view: string): ViewDef<T, 'list'>"),
2216            "Expected listView helper function, got:\n{}",
2217            stack_def
2218        );
2219    }
2220}