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