1pub mod backend;
2pub mod emit;
3pub mod enum_gen;
4pub mod flags;
5pub mod message;
6pub mod newtype;
7pub mod types;
8pub mod union_gen;
9
10pub use backend::TypeScriptBackend;
11
12use vexil_lang::ir::{CompiledSchema, TypeDef};
13
14#[derive(Debug, Clone, PartialEq, thiserror::Error)]
15pub enum CodegenError {
16 #[error("unresolved type {type_id:?} referenced by {referenced_by}")]
17 UnresolvedType {
18 type_id: vexil_lang::ir::TypeId,
19 referenced_by: String,
20 },
21}
22
23pub fn generate(compiled: &CompiledSchema) -> Result<String, CodegenError> {
25 generate_with_imports(compiled, None)
26}
27
28pub(crate) fn generate_with_imports(
32 compiled: &CompiledSchema,
33 import_paths: Option<&std::collections::HashMap<String, String>>,
34) -> Result<String, CodegenError> {
35 let mut w = emit::CodeWriter::new();
36
37 w.line("// Code generated by vexilc. DO NOT EDIT.");
39 let ns = compiled.namespace.join(".");
40 w.line(&format!("// Source: {ns}"));
41 w.blank();
42
43 w.line("import { BitReader, BitWriter } from '@vexil/runtime';");
45
46 if let Some(paths) = import_paths {
48 let mut imports: Vec<(&String, &String)> = paths.iter().collect();
49 imports.sort_by_key(|(name, _)| (*name).clone());
50 for (type_name, module_path) in imports {
51 w.line(&format!(
52 "import {{ {type_name}, encode{type_name}, decode{type_name} }} from '{module_path}';",
53 ));
54 }
55 }
56 w.blank();
57
58 let hash = vexil_lang::canonical::schema_hash(compiled);
60 let hash_str = hash
61 .iter()
62 .map(|b| format!("0x{b:02x}"))
63 .collect::<Vec<_>>()
64 .join(", ");
65 w.line(&format!(
66 "export const SCHEMA_HASH = new Uint8Array([{hash_str}]);"
67 ));
68
69 if let Some(ref version) = compiled.annotations.version {
71 w.line(&format!("export const SCHEMA_VERSION = '{version}';"));
72 }
73
74 for &type_id in &compiled.declarations {
76 let typedef =
77 compiled
78 .registry
79 .get(type_id)
80 .ok_or_else(|| CodegenError::UnresolvedType {
81 type_id,
82 referenced_by: "declarations".to_string(),
83 })?;
84
85 w.blank();
86 let type_name = type_name_of(typedef);
87 w.line(&format!("// \u{2500}\u{2500} {type_name} \u{2500}\u{2500}"));
88
89 match typedef {
90 TypeDef::Message(msg) => {
91 message::emit_message(&mut w, msg, &compiled.registry);
92 }
93 TypeDef::Enum(en) => {
94 enum_gen::emit_enum(&mut w, en, &compiled.registry);
95 }
96 TypeDef::Flags(fl) => {
97 flags::emit_flags(&mut w, fl, &compiled.registry);
98 }
99 TypeDef::Union(un) => {
100 union_gen::emit_union(&mut w, un, &compiled.registry);
101 }
102 TypeDef::Newtype(nt) => {
103 newtype::emit_newtype(&mut w, nt, &compiled.registry);
104 }
105 TypeDef::Config(cfg) => {
106 message::emit_config(&mut w, cfg, &compiled.registry);
107 }
108 _ => {} }
110 }
111
112 Ok(w.finish())
113}
114
115pub(crate) fn type_name_of(typedef: &TypeDef) -> &str {
117 match typedef {
118 TypeDef::Message(m) => m.name.as_str(),
119 TypeDef::Enum(e) => e.name.as_str(),
120 TypeDef::Flags(f) => f.name.as_str(),
121 TypeDef::Union(u) => u.name.as_str(),
122 TypeDef::Newtype(n) => n.name.as_str(),
123 TypeDef::Config(c) => c.name.as_str(),
124 _ => "Unknown",
125 }
126}
127
128#[cfg(test)]
129mod tests {
130 use super::*;
131
132 #[test]
133 fn generate_produces_output() {
134 let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
135 let compiled = result.compiled.unwrap();
136 let code = generate(&compiled).unwrap();
137 assert!(code.contains("export interface Foo"));
138 assert!(code.contains("export function encodeFoo"));
139 assert!(code.contains("export function decodeFoo"));
140 assert!(code.contains("import { BitReader, BitWriter } from '@vexil/runtime'"));
141 }
142
143 #[test]
144 fn generate_with_no_imports_matches_generate() {
145 let result = vexil_lang::compile("namespace test.gen\nmessage Foo { x @0 : u32 }");
146 let compiled = result.compiled.unwrap();
147 let without = generate(&compiled).unwrap();
148 let with = generate_with_imports(&compiled, None).unwrap();
149 assert_eq!(without, with);
150 }
151
152 #[test]
153 fn ts_backend_generate_matches_free_function() {
154 use vexil_lang::codegen::CodegenBackend;
155 let result = vexil_lang::compile("namespace test.backend\nmessage Foo { x @0 : u32 }");
156 let compiled = result.compiled.unwrap();
157 let free_fn = generate(&compiled).unwrap();
158 let backend = crate::backend::TypeScriptBackend;
159 let trait_fn = backend.generate(&compiled).unwrap();
160 assert_eq!(free_fn, trait_fn);
161 }
162}