Skip to main content

vexil_codegen_go/
backend.rs

1use std::collections::{BTreeMap, HashMap, HashSet};
2use std::path::PathBuf;
3
4use vexil_lang::codegen::{CodegenBackend, CodegenError};
5use vexil_lang::ir::{CompiledSchema, ResolvedType, TypeDef, TypeId};
6use vexil_lang::project::ProjectResult;
7
8/// Go code-generation backend for Vexil schemas.
9///
10/// Generates Go structs, enums (const blocks), flags, unions (interface + variant
11/// structs), and Pack/Unpack methods using the `github.com/vexil-lang/vexil/packages/runtime-go`
12/// package.
13#[derive(Debug, Clone, Copy)]
14pub struct GoBackend;
15
16impl CodegenBackend for GoBackend {
17    fn name(&self) -> &str {
18        "go"
19    }
20
21    fn file_extension(&self) -> &str {
22        "go"
23    }
24
25    fn generate(&self, compiled: &CompiledSchema) -> Result<String, CodegenError> {
26        crate::generate(compiled).map_err(|e| CodegenError::BackendSpecific(Box::new(e)))
27    }
28
29    fn generate_project(
30        &self,
31        result: &ProjectResult,
32    ) -> Result<BTreeMap<PathBuf, String>, CodegenError> {
33        let mut files = BTreeMap::new();
34
35        // Step 1: Build a global type_name -> Go package path map.
36        let mut global_type_map: HashMap<String, String> = HashMap::new();
37        for (ns, compiled) in &result.schemas {
38            let segments: Vec<&str> = ns.split('.').collect();
39            let go_pkg = segments.join("/");
40            for &type_id in &compiled.declarations {
41                if let Some(typedef) = compiled.registry.get(type_id) {
42                    let name = crate::type_name_of(typedef);
43                    global_type_map.insert(name.to_string(), go_pkg.clone());
44                }
45            }
46        }
47
48        for (ns, compiled) in &result.schemas {
49            let segments: Vec<&str> = ns.split('.').collect();
50            if segments.is_empty() {
51                continue;
52            }
53            let file_name = segments[segments.len() - 1];
54            let dir_segments = &segments[..segments.len() - 1];
55
56            // Step 2: Build import_types for this schema.
57            let declared_ids: HashSet<TypeId> = compiled.declarations.iter().copied().collect();
58
59            let mut import_types: HashMap<String, String> = HashMap::new();
60            for &type_id in &compiled.declarations {
61                if let Some(typedef) = compiled.registry.get(type_id) {
62                    collect_named_ids_from_typedef(typedef, &declared_ids, |imported_id| {
63                        if let Some(imported_def) = compiled.registry.get(imported_id) {
64                            let name = crate::type_name_of(imported_def);
65                            if let Some(go_path) = global_type_map.get(name) {
66                                import_types.insert(name.to_string(), go_path.clone());
67                            }
68                        }
69                    });
70                }
71            }
72
73            // Generate code with cross-file imports.
74            let imports = if import_types.is_empty() {
75                None
76            } else {
77                Some(&import_types)
78            };
79            let code = crate::generate_with_imports(compiled, imports)
80                .map_err(|e| CodegenError::BackendSpecific(Box::new(e)))?;
81
82            let mut file_path = PathBuf::new();
83            for seg in dir_segments {
84                file_path.push(seg);
85            }
86            file_path.push(format!("{file_name}.go"));
87            files.insert(file_path, code);
88        }
89
90        // Go doesn't need barrel/index files — imports are package-level
91
92        Ok(files)
93    }
94}
95
96/// Collect all `ResolvedType::Named(id)` from a TypeDef where `id` is NOT in
97/// the declared set (i.e., it's an imported type).
98fn collect_named_ids_from_typedef(
99    typedef: &TypeDef,
100    declared: &HashSet<TypeId>,
101    mut on_import: impl FnMut(TypeId),
102) {
103    match typedef {
104        TypeDef::Message(msg) => {
105            for f in &msg.fields {
106                collect_named_ids_from_resolved(&f.resolved_type, declared, &mut on_import);
107            }
108        }
109        TypeDef::Union(un) => {
110            for v in &un.variants {
111                for f in &v.fields {
112                    collect_named_ids_from_resolved(&f.resolved_type, declared, &mut on_import);
113                }
114            }
115        }
116        TypeDef::Newtype(nt) => {
117            collect_named_ids_from_resolved(&nt.inner_type, declared, &mut on_import);
118        }
119        TypeDef::Config(cfg) => {
120            for f in &cfg.fields {
121                collect_named_ids_from_resolved(&f.resolved_type, declared, &mut on_import);
122            }
123        }
124        _ => {}
125    }
126}
127
128fn collect_named_ids_from_resolved(
129    ty: &ResolvedType,
130    declared: &HashSet<TypeId>,
131    on_import: &mut impl FnMut(TypeId),
132) {
133    match ty {
134        ResolvedType::Named(id) => {
135            if !declared.contains(id) {
136                on_import(*id);
137            }
138        }
139        ResolvedType::Optional(inner) | ResolvedType::Array(inner) => {
140            collect_named_ids_from_resolved(inner, declared, on_import);
141        }
142        ResolvedType::Map(k, v) | ResolvedType::Result(k, v) => {
143            collect_named_ids_from_resolved(k, declared, on_import);
144            collect_named_ids_from_resolved(v, declared, on_import);
145        }
146        _ => {}
147    }
148}