1use crate::ast::*;
2use std::collections::{BTreeMap, HashSet};
3
4#[derive(Debug, Clone)]
6pub struct TypeScriptOutput {
7 pub interfaces: String,
8 pub stack_definition: String,
9 pub imports: String,
10}
11
12impl TypeScriptOutput {
13 pub fn full_file(&self) -> String {
14 format!(
15 "{}\n\n{}\n\n{}",
16 self.imports, self.interfaces, self.stack_definition
17 )
18 }
19}
20
21#[derive(Debug, Clone)]
23pub struct TypeScriptConfig {
24 pub package_name: String,
25 pub generate_helpers: bool,
26 pub interface_prefix: String,
27 pub export_const_name: String,
28}
29
30impl Default for TypeScriptConfig {
31 fn default() -> Self {
32 Self {
33 package_name: "hyperstack-react".to_string(),
34 generate_helpers: true,
35 interface_prefix: "".to_string(),
36 export_const_name: "STACK".to_string(),
37 }
38 }
39}
40
41pub trait TypeScriptGenerator {
43 fn generate_typescript(&self, config: &TypeScriptConfig) -> String;
44}
45
46pub trait TypeScriptInterfaceGenerator {
48 fn generate_interface(&self, name: &str, config: &TypeScriptConfig) -> String;
49}
50
51pub trait TypeScriptTypeMapper {
53 fn to_typescript_type(&self) -> String;
54}
55
56pub struct TypeScriptCompiler<S> {
58 spec: TypedStreamSpec<S>,
59 entity_name: String,
60 config: TypeScriptConfig,
61 idl: Option<serde_json::Value>, handlers_json: Option<serde_json::Value>, }
64
65impl<S> TypeScriptCompiler<S> {
66 pub fn new(spec: TypedStreamSpec<S>, entity_name: String) -> Self {
67 Self {
68 spec,
69 entity_name,
70 config: TypeScriptConfig::default(),
71 idl: None,
72 handlers_json: None,
73 }
74 }
75
76 pub fn with_config(mut self, config: TypeScriptConfig) -> Self {
77 self.config = config;
78 self
79 }
80
81 pub fn with_idl(mut self, idl: Option<serde_json::Value>) -> Self {
82 self.idl = idl;
83 self
84 }
85
86 pub fn with_handlers_json(mut self, handlers: Option<serde_json::Value>) -> Self {
87 self.handlers_json = handlers;
88 self
89 }
90
91 pub fn compile(&self) -> TypeScriptOutput {
92 let imports = self.generate_imports();
93 let interfaces = self.generate_interfaces();
94 let stack_definition = self.generate_stack_definition();
95
96 TypeScriptOutput {
97 imports,
98 interfaces,
99 stack_definition,
100 }
101 }
102
103 fn generate_imports(&self) -> String {
104 String::new()
106 }
107
108 fn generate_view_helpers(&self) -> String {
109 r#"// ============================================================================
110// View Definition Types (framework-agnostic)
111// ============================================================================
112
113/** View definition with embedded entity type */
114export interface ViewDef<T, TMode extends 'state' | 'list'> {
115 readonly mode: TMode;
116 readonly view: string;
117 /** Phantom field for type inference - not present at runtime */
118 readonly _entity?: T;
119}
120
121/** Helper to create typed state view definitions */
122function stateView<T>(view: string): ViewDef<T, 'state'> {
123 return { mode: 'state', view } as const;
124}
125
126/** Helper to create typed list view definitions */
127function listView<T>(view: string): ViewDef<T, 'list'> {
128 return { mode: 'list', view } as const;
129}"#
130 .to_string()
131 }
132
133 fn generate_interfaces(&self) -> String {
134 let mut interfaces = Vec::new();
135 let mut processed_types = HashSet::new();
136 let mut all_sections: BTreeMap<String, Vec<TypeScriptField>> = BTreeMap::new();
137
138 for handler in &self.spec.handlers {
140 let interface_sections = self.extract_interface_sections(handler);
141
142 for (section_name, mut fields) in interface_sections {
143 all_sections
144 .entry(section_name)
145 .or_default()
146 .append(&mut fields);
147 }
148 }
149
150 for (section_name, fields) in all_sections {
153 if !is_root_section(§ion_name) && processed_types.insert(section_name.clone()) {
154 let deduplicated_fields = self.deduplicate_fields(fields);
155 let interface =
156 self.generate_interface_from_fields(§ion_name, &deduplicated_fields);
157 interfaces.push(interface);
158 }
159 }
160
161 let main_interface = self.generate_main_entity_interface();
163 interfaces.push(main_interface);
164
165 let nested_interfaces = self.generate_nested_interfaces();
167 interfaces.extend(nested_interfaces);
168
169 if self.has_event_types() {
171 interfaces.push(self.generate_event_wrapper_interface());
172 }
173
174 interfaces.join("\n\n")
175 }
176
177 fn deduplicate_fields(&self, mut fields: Vec<TypeScriptField>) -> Vec<TypeScriptField> {
178 let mut seen = HashSet::new();
179 let mut unique_fields = Vec::new();
180
181 fields.sort_by(|a, b| a.name.cmp(&b.name));
183
184 for field in fields {
185 if seen.insert(field.name.clone()) {
186 unique_fields.push(field);
187 }
188 }
189
190 unique_fields
191 }
192
193 fn extract_interface_sections(
194 &self,
195 handler: &TypedHandlerSpec<S>,
196 ) -> BTreeMap<String, Vec<TypeScriptField>> {
197 let mut sections: BTreeMap<String, Vec<TypeScriptField>> = BTreeMap::new();
198
199 for mapping in &handler.mappings {
200 let parts: Vec<&str> = mapping.target_path.split('.').collect();
201
202 if parts.len() > 1 {
203 let section_name = parts[0];
205 let field_name = parts[1];
206
207 let ts_field = TypeScriptField {
208 name: field_name.to_string(),
209 ts_type: self.mapping_to_typescript_type(mapping),
210 optional: self.is_field_optional(mapping),
211 description: None,
212 };
213
214 sections
215 .entry(section_name.to_string())
216 .or_default()
217 .push(ts_field);
218 } else {
219 let ts_field = TypeScriptField {
221 name: mapping.target_path.clone(),
222 ts_type: self.mapping_to_typescript_type(mapping),
223 optional: self.is_field_optional(mapping),
224 description: None,
225 };
226
227 sections
228 .entry("Root".to_string())
229 .or_default()
230 .push(ts_field);
231 }
232 }
233
234 self.add_unmapped_fields(&mut sections);
237
238 sections
239 }
240
241 fn add_unmapped_fields(&self, sections: &mut BTreeMap<String, Vec<TypeScriptField>>) {
242 if !self.spec.sections.is_empty() {
244 for section in &self.spec.sections {
246 let section_fields = sections.entry(section.name.clone()).or_default();
247
248 for field_info in §ion.fields {
249 let already_exists = section_fields.iter().any(|f| {
251 f.name == field_info.field_name
252 || f.name == to_camel_case(&field_info.field_name)
253 });
254
255 if !already_exists {
256 section_fields.push(TypeScriptField {
257 name: field_info.field_name.clone(),
258 ts_type: self.field_type_info_to_typescript(field_info),
259 optional: field_info.is_optional,
260 description: None,
261 });
262 }
263 }
264 }
265 } else {
266 for (field_path, field_type_info) in &self.spec.field_mappings {
268 let parts: Vec<&str> = field_path.split('.').collect();
269 if parts.len() > 1 {
270 let section_name = parts[0];
271 let field_name = parts[1];
272
273 let section_fields = sections.entry(section_name.to_string()).or_default();
274
275 let already_exists = section_fields
276 .iter()
277 .any(|f| f.name == field_name || f.name == to_camel_case(field_name));
278
279 if !already_exists {
280 section_fields.push(TypeScriptField {
281 name: field_name.to_string(),
282 ts_type: self.base_type_to_typescript(
283 &field_type_info.base_type,
284 field_type_info.is_array,
285 ),
286 optional: field_type_info.is_optional,
287 description: None,
288 });
289 }
290 }
291 }
292 }
293 }
294
295 fn generate_interface_from_fields(&self, name: &str, fields: &[TypeScriptField]) -> String {
296 let interface_name = if name == "Root" {
298 format!(
299 "{}{}",
300 self.config.interface_prefix,
301 to_pascal_case(&self.entity_name)
302 )
303 } else {
304 let base_name = if self.entity_name.contains("Game") {
307 "Game"
308 } else {
309 &self.entity_name
310 };
311 format!(
312 "{}{}{}",
313 self.config.interface_prefix,
314 base_name,
315 to_pascal_case(name)
316 )
317 };
318
319 let field_definitions: Vec<String> = fields
322 .iter()
323 .map(|field| {
324 let field_name = to_camel_case(&field.name);
325 let ts_type = if field.optional {
326 format!("{} | null", field.ts_type)
328 } else {
329 field.ts_type.clone()
330 };
331 format!(" {}?: {};", field_name, ts_type)
332 })
333 .collect();
334
335 format!(
336 "export interface {} {{\n{}\n}}",
337 interface_name,
338 field_definitions.join("\n")
339 )
340 }
341
342 fn generate_main_entity_interface(&self) -> String {
343 let entity_name = to_pascal_case(&self.entity_name);
344
345 let mut sections = BTreeMap::new();
347
348 for handler in &self.spec.handlers {
349 for mapping in &handler.mappings {
350 let parts: Vec<&str> = mapping.target_path.split('.').collect();
351 if parts.len() > 1 {
352 sections.insert(parts[0], true);
353 }
354 }
355 }
356
357 if !self.spec.sections.is_empty() {
358 for section in &self.spec.sections {
359 sections.insert(§ion.name, true);
360 }
361 } else {
362 for mapping in &self.spec.handlers {
363 for field_mapping in &mapping.mappings {
364 let parts: Vec<&str> = field_mapping.target_path.split('.').collect();
365 if parts.len() > 1 {
366 sections.insert(parts[0], true);
367 }
368 }
369 }
370 }
371
372 let mut fields = Vec::new();
373
374 for section in sections.keys() {
377 if !is_root_section(section) {
378 let base_name = if self.entity_name.contains("Game") {
379 "Game"
380 } else {
381 &self.entity_name
382 };
383 let section_interface_name = format!("{}{}", base_name, to_pascal_case(section));
384 fields.push(format!(
385 " {}?: {};",
386 to_camel_case(section),
387 section_interface_name
388 ));
389 }
390 }
391
392 for section in &self.spec.sections {
395 if is_root_section(§ion.name) {
396 for field in §ion.fields {
397 let field_name = to_camel_case(&field.field_name);
398 let base_ts_type = self.field_type_info_to_typescript(field);
399 let ts_type = if field.is_optional {
400 format!("{} | null", base_ts_type)
401 } else {
402 base_ts_type
403 };
404 fields.push(format!(" {}?: {};", field_name, ts_type));
405 }
406 }
407 }
408
409 if fields.is_empty() {
410 fields.push(" // Generated interface - extend as needed".to_string());
411 }
412
413 format!(
414 "export interface {} {{\n{}\n}}",
415 entity_name,
416 fields.join("\n")
417 )
418 }
419
420 fn generate_stack_definition(&self) -> String {
421 let stack_name = to_kebab_case(&self.entity_name);
422 let entity_pascal = to_pascal_case(&self.entity_name);
423 let export_name = format!(
424 "{}_{}",
425 self.entity_name.to_uppercase(),
426 self.config.export_const_name
427 );
428
429 let view_helpers = self.generate_view_helpers();
430
431 format!(
432 r#"{}
433
434// ============================================================================
435// Stack Definition
436// ============================================================================
437
438/** Stack definition for {} */
439export const {} = {{
440 name: '{}',
441 views: {{
442 {}: {{
443 state: stateView<{}>('{}/state'),
444 list: listView<{}>('{}/list'),
445 }},
446 }},
447}} as const;
448
449/** Type alias for the stack */
450export type {}Stack = typeof {};"#,
451 view_helpers,
452 entity_pascal,
453 export_name,
454 stack_name,
455 to_camel_case(&self.entity_name),
456 entity_pascal,
457 self.entity_name,
458 entity_pascal,
459 self.entity_name,
460 entity_pascal,
461 export_name
462 )
463 }
464
465 fn mapping_to_typescript_type(&self, mapping: &TypedFieldMapping<S>) -> String {
466 if let Some(field_info) = self.spec.field_mappings.get(&mapping.target_path) {
468 let ts_type = self.field_type_info_to_typescript(field_info);
469
470 if matches!(mapping.population, PopulationStrategy::Append) {
472 return if ts_type.ends_with("[]") {
473 ts_type
474 } else {
475 format!("{}[]", ts_type)
476 };
477 }
478
479 return ts_type;
480 }
481
482 match &mapping.population {
484 PopulationStrategy::Append => {
485 match &mapping.source {
487 MappingSource::AsEvent { .. } => "any[]".to_string(),
488 _ => "any[]".to_string(),
489 }
490 }
491 _ => {
492 let base_type = match &mapping.source {
494 MappingSource::FromSource { .. } => {
495 self.infer_type_from_field_name(&mapping.target_path)
496 }
497 MappingSource::Constant(value) => value_to_typescript_type(value),
498 MappingSource::AsEvent { .. } => "any".to_string(),
499 _ => "any".to_string(),
500 };
501
502 if let Some(transform) = &mapping.transform {
504 match transform {
505 Transformation::HexEncode | Transformation::HexDecode => {
506 "string".to_string()
507 }
508 Transformation::Base58Encode | Transformation::Base58Decode => {
509 "string".to_string()
510 }
511 Transformation::ToString => "string".to_string(),
512 Transformation::ToNumber => "number".to_string(),
513 }
514 } else {
515 base_type
516 }
517 }
518 }
519 }
520
521 fn field_type_info_to_typescript(&self, field_info: &FieldTypeInfo) -> String {
523 if let Some(resolved) = &field_info.resolved_type {
525 let interface_name = self.resolved_type_to_interface_name(resolved);
526
527 let base_type = if resolved.is_event || (resolved.is_instruction && field_info.is_array)
529 {
530 format!("EventWrapper<{}>", interface_name)
531 } else {
532 interface_name
533 };
534
535 let with_array = if field_info.is_array {
537 format!("{}[]", base_type)
538 } else {
539 base_type
540 };
541
542 return with_array;
543 }
544
545 if field_info.base_type == BaseType::Any
548 || (field_info.base_type == BaseType::Array
549 && field_info.inner_type.as_deref() == Some("Value"))
550 {
551 if let Some(event_type) = self.find_event_interface_for_field(&field_info.field_name) {
552 return if field_info.is_array {
553 format!("{}[]", event_type)
554 } else if field_info.is_optional {
555 format!("{} | null", event_type)
556 } else {
557 event_type
558 };
559 }
560 }
561
562 self.base_type_to_typescript(&field_info.base_type, field_info.is_array)
564 }
565
566 fn find_event_interface_for_field(&self, field_name: &str) -> Option<String> {
568 let handlers = self.handlers_json.as_ref()?.as_array()?;
570
571 for handler in handlers {
573 if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
574 for mapping in mappings {
575 if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
576 let target_parts: Vec<&str> = target_path.split('.').collect();
578 if let Some(target_field) = target_parts.last() {
579 if *target_field == field_name {
580 if let Some(source) = mapping.get("source") {
582 if self.extract_event_data(source).is_some() {
583 return Some(format!(
585 "{}Event",
586 to_pascal_case(field_name)
587 ));
588 }
589 }
590 }
591 }
592 }
593 }
594 }
595 }
596 None
597 }
598
599 fn resolved_type_to_interface_name(&self, resolved: &ResolvedStructType) -> String {
601 to_pascal_case(&resolved.type_name)
602 }
603
604 fn generate_nested_interfaces(&self) -> Vec<String> {
606 let mut interfaces = Vec::new();
607 let mut generated_types = HashSet::new();
608
609 for section in &self.spec.sections {
611 for field_info in §ion.fields {
612 if let Some(resolved) = &field_info.resolved_type {
613 let type_name = resolved.type_name.clone();
614
615 if generated_types.insert(type_name) {
617 let interface = self.generate_interface_for_resolved_type(resolved);
618 interfaces.push(interface);
619 }
620 }
621 }
622 }
623
624 interfaces.extend(self.generate_event_interfaces(&mut generated_types));
626
627 if let Some(idl_value) = &self.idl {
629 if let Some(types_array) = idl_value.get("types").and_then(|v| v.as_array()) {
630 for type_def in types_array {
631 if let (Some(type_name), Some(type_obj)) = (
632 type_def.get("name").and_then(|v| v.as_str()),
633 type_def.get("type").and_then(|v| v.as_object()),
634 ) {
635 if type_obj.get("kind").and_then(|v| v.as_str()) == Some("enum") {
636 if generated_types.insert(type_name.to_string()) {
638 if let Some(variants) =
639 type_obj.get("variants").and_then(|v| v.as_array())
640 {
641 let variant_names: Vec<String> = variants
642 .iter()
643 .filter_map(|v| {
644 v.get("name")
645 .and_then(|n| n.as_str())
646 .map(|s| s.to_string())
647 })
648 .collect();
649
650 if !variant_names.is_empty() {
651 let interface_name = to_pascal_case(type_name);
652 let variant_strings: Vec<String> = variant_names
653 .iter()
654 .map(|v| format!("\"{}\"", to_pascal_case(v)))
655 .collect();
656
657 let enum_type = format!(
658 "export type {} = {};",
659 interface_name,
660 variant_strings.join(" | ")
661 );
662 interfaces.push(enum_type);
663 }
664 }
665 }
666 }
667 }
668 }
669 }
670 }
671
672 interfaces
673 }
674
675 fn generate_event_interfaces(&self, generated_types: &mut HashSet<String>) -> Vec<String> {
677 let mut interfaces = Vec::new();
678
679 let handlers = match &self.handlers_json {
681 Some(h) => h.as_array(),
682 None => return interfaces,
683 };
684
685 let handlers_array = match handlers {
686 Some(arr) => arr,
687 None => return interfaces,
688 };
689
690 for handler in handlers_array {
692 if let Some(mappings) = handler.get("mappings").and_then(|m| m.as_array()) {
694 for mapping in mappings {
695 if let Some(target_path) = mapping.get("target_path").and_then(|t| t.as_str()) {
696 if target_path.contains(".events.") || target_path.starts_with("events.") {
698 if let Some(source) = mapping.get("source") {
700 if let Some(event_data) = self.extract_event_data(source) {
701 if let Some(handler_source) = handler.get("source") {
703 if let Some(instruction_name) =
704 self.extract_instruction_name(handler_source)
705 {
706 let event_field_name =
708 target_path.split('.').next_back().unwrap_or("");
709 let interface_name = format!(
710 "{}Event",
711 to_pascal_case(event_field_name)
712 );
713
714 if generated_types.insert(interface_name.clone()) {
716 if let Some(interface) = self
717 .generate_event_interface_from_idl(
718 &interface_name,
719 &instruction_name,
720 &event_data,
721 )
722 {
723 interfaces.push(interface);
724 }
725 }
726 }
727 }
728 }
729 }
730 }
731 }
732 }
733 }
734 }
735
736 interfaces
737 }
738
739 fn extract_event_data(
741 &self,
742 source: &serde_json::Value,
743 ) -> Option<Vec<(String, Option<String>)>> {
744 if let Some(as_event) = source.get("AsEvent") {
745 if let Some(fields) = as_event.get("fields").and_then(|f| f.as_array()) {
746 let mut event_fields = Vec::new();
747 for field in fields {
748 if let Some(from_source) = field.get("FromSource") {
749 if let Some(path) = from_source
750 .get("path")
751 .and_then(|p| p.get("segments"))
752 .and_then(|s| s.as_array())
753 {
754 if let Some(field_name) = path.last().and_then(|v| v.as_str()) {
756 let transform = from_source
757 .get("transform")
758 .and_then(|t| t.as_str())
759 .map(|s| s.to_string());
760 event_fields.push((field_name.to_string(), transform));
761 }
762 }
763 }
764 }
765 return Some(event_fields);
766 }
767 }
768 None
769 }
770
771 fn extract_instruction_name(&self, source: &serde_json::Value) -> Option<String> {
773 if let Some(source_obj) = source.get("Source") {
774 if let Some(type_name) = source_obj.get("type_name").and_then(|t| t.as_str()) {
775 if let Some(instruction_part) = type_name.strip_suffix("IxState") {
777 return Some(to_snake_case(instruction_part));
778 }
779 }
780 }
781 None
782 }
783
784 fn generate_event_interface_from_idl(
786 &self,
787 interface_name: &str,
788 instruction_name: &str,
789 captured_fields: &[(String, Option<String>)],
790 ) -> Option<String> {
791 if captured_fields.is_empty() {
793 return Some(format!("export interface {} {{}}", interface_name));
794 }
795
796 let idl_value = self.idl.as_ref()?;
797 let instructions = idl_value.get("instructions")?.as_array()?;
798
799 for instruction in instructions {
801 if let Some(name) = instruction.get("name").and_then(|n| n.as_str()) {
802 if name == instruction_name {
803 if let Some(args) = instruction.get("args").and_then(|a| a.as_array()) {
805 let mut fields = Vec::new();
806
807 for (field_name, transform) in captured_fields {
809 for arg in args {
811 if let Some(arg_name) = arg.get("name").and_then(|n| n.as_str()) {
812 if arg_name == field_name {
813 if let Some(arg_type) = arg.get("type") {
814 let ts_type = self.idl_type_to_typescript(
815 arg_type,
816 transform.as_deref(),
817 );
818 let camel_name = to_camel_case(field_name);
819 fields.push(format!(" {}: {};", camel_name, ts_type));
820 }
821 break;
822 }
823 }
824 }
825 }
826
827 if !fields.is_empty() {
828 return Some(format!(
829 "export interface {} {{\n{}\n}}",
830 interface_name,
831 fields.join("\n")
832 ));
833 }
834 }
835 }
836 }
837 }
838
839 None
840 }
841
842 fn idl_type_to_typescript(
844 &self,
845 idl_type: &serde_json::Value,
846 transform: Option<&str>,
847 ) -> String {
848 #![allow(clippy::only_used_in_recursion)]
849 if transform == Some("HexEncode") {
851 return "string".to_string();
852 }
853
854 if let Some(type_str) = idl_type.as_str() {
856 return match type_str {
857 "u8" | "u16" | "u32" | "u64" | "u128" | "i8" | "i16" | "i32" | "i64" | "i128" => {
858 "number".to_string()
859 }
860 "f32" | "f64" => "number".to_string(),
861 "bool" => "boolean".to_string(),
862 "string" => "string".to_string(),
863 "pubkey" | "publicKey" => "string".to_string(),
864 "bytes" => "string".to_string(),
865 _ => "any".to_string(),
866 };
867 }
868
869 if let Some(type_obj) = idl_type.as_object() {
871 if let Some(option_type) = type_obj.get("option") {
872 let inner = self.idl_type_to_typescript(option_type, None);
873 return format!("{} | null", inner);
874 }
875 if let Some(vec_type) = type_obj.get("vec") {
876 let inner = self.idl_type_to_typescript(vec_type, None);
877 return format!("{}[]", inner);
878 }
879 }
880
881 "any".to_string()
882 }
883
884 fn generate_interface_for_resolved_type(&self, resolved: &ResolvedStructType) -> String {
886 let interface_name = to_pascal_case(&resolved.type_name);
887
888 if resolved.is_enum {
890 let variants: Vec<String> = resolved
891 .enum_variants
892 .iter()
893 .map(|v| format!("\"{}\"", to_pascal_case(v)))
894 .collect();
895
896 return format!("export type {} = {};", interface_name, variants.join(" | "));
897 }
898
899 let fields: Vec<String> = resolved
902 .fields
903 .iter()
904 .map(|field| {
905 let field_name = to_camel_case(&field.field_name);
906 let base_ts_type = self.resolved_field_to_typescript(field);
907 let ts_type = if field.is_optional {
908 format!("{} | null", base_ts_type)
909 } else {
910 base_ts_type
911 };
912 format!(" {}?: {};", field_name, ts_type)
913 })
914 .collect();
915
916 format!(
917 "export interface {} {{\n{}\n}}",
918 interface_name,
919 fields.join("\n")
920 )
921 }
922
923 fn resolved_field_to_typescript(&self, field: &ResolvedField) -> String {
925 let base_ts = self.base_type_to_typescript(&field.base_type, false);
926
927 if field.is_array {
928 format!("{}[]", base_ts)
929 } else {
930 base_ts
931 }
932 }
933
934 fn has_event_types(&self) -> bool {
936 for section in &self.spec.sections {
937 for field_info in §ion.fields {
938 if let Some(resolved) = &field_info.resolved_type {
939 if resolved.is_event || (resolved.is_instruction && field_info.is_array) {
940 return true;
941 }
942 }
943 }
944 }
945 false
946 }
947
948 fn generate_event_wrapper_interface(&self) -> String {
950 r#"/**
951 * Wrapper for event data that includes context metadata.
952 * Events are automatically wrapped in this structure at runtime.
953 */
954export interface EventWrapper<T> {
955 /** Unix timestamp when the event was processed */
956 timestamp: number;
957 /** The event-specific data */
958 data: T;
959 /** Optional blockchain slot number */
960 slot?: number;
961 /** Optional transaction signature */
962 signature?: string;
963}"#
964 .to_string()
965 }
966
967 fn infer_type_from_field_name(&self, field_name: &str) -> String {
968 let lower_name = field_name.to_lowercase();
969
970 if lower_name.contains("events.") {
972 return "any".to_string();
974 }
975
976 if lower_name.contains("id")
978 || lower_name.contains("count")
979 || lower_name.contains("number")
980 || lower_name.contains("timestamp")
981 || lower_name.contains("time")
982 || lower_name.contains("at")
983 || lower_name.contains("volume")
984 || lower_name.contains("amount")
985 || lower_name.contains("ev")
986 || lower_name.contains("fee")
987 || lower_name.contains("payout")
988 || lower_name.contains("distributed")
989 || lower_name.contains("claimable")
990 || lower_name.contains("total")
991 || lower_name.contains("rate")
992 || lower_name.contains("ratio")
993 || lower_name.contains("current")
994 || lower_name.contains("state")
995 {
996 "number".to_string()
997 } else if lower_name.contains("status")
998 || lower_name.contains("hash")
999 || lower_name.contains("address")
1000 || lower_name.contains("key")
1001 {
1002 "string".to_string()
1003 } else {
1004 "any".to_string()
1005 }
1006 }
1007
1008 fn is_field_optional(&self, mapping: &TypedFieldMapping<S>) -> bool {
1009 match &mapping.source {
1011 MappingSource::Constant(_) => false,
1013 MappingSource::AsEvent { .. } => true,
1015 MappingSource::FromSource { .. } => true,
1017 _ => true,
1019 }
1020 }
1021
1022 fn base_type_to_typescript(&self, base_type: &BaseType, is_array: bool) -> String {
1024 let base_ts_type = match base_type {
1025 BaseType::Integer => "number",
1026 BaseType::Float => "number",
1027 BaseType::String => "string",
1028 BaseType::Boolean => "boolean",
1029 BaseType::Timestamp => "number", BaseType::Binary => "string", BaseType::Pubkey => "string", BaseType::Array => "any[]", BaseType::Object => "Record<string, any>", BaseType::Any => "any",
1035 };
1036
1037 if is_array && !matches!(base_type, BaseType::Array) {
1038 format!("{}[]", base_ts_type)
1039 } else {
1040 base_ts_type.to_string()
1041 }
1042 }
1043}
1044
1045#[derive(Debug, Clone)]
1047struct TypeScriptField {
1048 name: String,
1049 ts_type: String,
1050 optional: bool,
1051 #[allow(dead_code)]
1052 description: Option<String>,
1053}
1054
1055fn value_to_typescript_type(value: &serde_json::Value) -> String {
1057 match value {
1058 serde_json::Value::Number(_) => "number".to_string(),
1059 serde_json::Value::String(_) => "string".to_string(),
1060 serde_json::Value::Bool(_) => "boolean".to_string(),
1061 serde_json::Value::Array(_) => "any[]".to_string(),
1062 serde_json::Value::Object(_) => "Record<string, any>".to_string(),
1063 serde_json::Value::Null => "null".to_string(),
1064 }
1065}
1066
1067fn to_pascal_case(s: &str) -> String {
1069 s.split(['_', '-', '.'])
1070 .map(|word| {
1071 let mut chars = word.chars();
1072 match chars.next() {
1073 None => String::new(),
1074 Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
1075 }
1076 })
1077 .collect()
1078}
1079
1080fn to_camel_case(s: &str) -> String {
1082 let pascal = to_pascal_case(s);
1083 let mut chars = pascal.chars();
1084 match chars.next() {
1085 None => String::new(),
1086 Some(first) => first.to_lowercase().collect::<String>() + chars.as_str(),
1087 }
1088}
1089
1090fn to_snake_case(s: &str) -> String {
1092 let mut result = String::new();
1093
1094 for ch in s.chars() {
1095 if ch.is_uppercase() {
1096 if !result.is_empty() {
1097 result.push('_');
1098 }
1099 result.push(ch.to_lowercase().next().unwrap());
1100 } else {
1101 result.push(ch);
1102 }
1103 }
1104
1105 result
1106}
1107
1108fn is_root_section(name: &str) -> bool {
1110 name.eq_ignore_ascii_case("root")
1111}
1112
1113fn to_kebab_case(s: &str) -> String {
1115 let mut result = String::new();
1116
1117 for ch in s.chars() {
1118 if ch.is_uppercase() && !result.is_empty() {
1119 result.push('-');
1120 }
1121 result.push(ch.to_lowercase().next().unwrap());
1122 }
1123
1124 result
1125}
1126
1127pub fn generate_typescript_from_spec_fn<F, S>(
1130 spec_fn: F,
1131 entity_name: String,
1132 config: Option<TypeScriptConfig>,
1133) -> Result<TypeScriptOutput, String>
1134where
1135 F: Fn() -> TypedStreamSpec<S>,
1136{
1137 let spec = spec_fn();
1138 let compiler =
1139 TypeScriptCompiler::new(spec, entity_name).with_config(config.unwrap_or_default());
1140
1141 Ok(compiler.compile())
1142}
1143
1144pub fn write_typescript_to_file(
1146 output: &TypeScriptOutput,
1147 path: &std::path::Path,
1148) -> Result<(), std::io::Error> {
1149 std::fs::write(path, output.full_file())
1150}
1151
1152pub fn compile_serializable_spec(
1155 spec: SerializableStreamSpec,
1156 entity_name: String,
1157 config: Option<TypeScriptConfig>,
1158) -> Result<TypeScriptOutput, String> {
1159 let idl = spec
1161 .idl
1162 .as_ref()
1163 .and_then(|idl_snapshot| serde_json::to_value(idl_snapshot).ok());
1164
1165 let handlers = serde_json::to_value(&spec.handlers).ok();
1167
1168 let typed_spec: TypedStreamSpec<()> = TypedStreamSpec::from_serializable(spec);
1171
1172 let compiler = TypeScriptCompiler::new(typed_spec, entity_name)
1173 .with_idl(idl)
1174 .with_handlers_json(handlers)
1175 .with_config(config.unwrap_or_default());
1176
1177 Ok(compiler.compile())
1178}
1179
1180#[cfg(test)]
1181mod tests {
1182 use super::*;
1183
1184 #[test]
1185 fn test_case_conversions() {
1186 assert_eq!(to_pascal_case("settlement_game"), "SettlementGame");
1187 assert_eq!(to_camel_case("settlement_game"), "settlementGame");
1188 assert_eq!(to_kebab_case("SettlementGame"), "settlement-game");
1189 }
1190
1191 #[test]
1192 fn test_value_to_typescript_type() {
1193 assert_eq!(value_to_typescript_type(&serde_json::json!(42)), "number");
1194 assert_eq!(
1195 value_to_typescript_type(&serde_json::json!("hello")),
1196 "string"
1197 );
1198 assert_eq!(
1199 value_to_typescript_type(&serde_json::json!(true)),
1200 "boolean"
1201 );
1202 assert_eq!(value_to_typescript_type(&serde_json::json!([])), "any[]");
1203 }
1204}