1use crate::package_mode::PackageMode;
4use crate::resolver::{ResolutionContext, TypeResolver};
5use crate::{Codegen, CodegenError};
6use amalgam_core::{
7 types::{Field, Type},
8 IR,
9};
10use std::fmt::Write;
11
12pub struct NickelCodegen {
13 indent_size: usize,
14 resolver: TypeResolver,
15 package_mode: PackageMode,
16}
17
18impl NickelCodegen {
19 pub fn new() -> Self {
20 Self {
21 indent_size: 2,
22 resolver: TypeResolver::new(),
23 package_mode: PackageMode::default(),
24 }
25 }
26
27 pub fn with_package_mode(mut self, mode: PackageMode) -> Self {
28 self.package_mode = mode;
29 self
30 }
31
32 fn indent(&self, level: usize) -> String {
33 " ".repeat(level * self.indent_size)
34 }
35
36 fn format_doc(&self, doc: &str) -> String {
39 if doc.contains('\n') || doc.len() > 80 {
40 format!("m%\"\n{}\n\"%", doc.trim())
42 } else {
43 format!("\"{}\"", doc.replace('"', "\\\""))
45 }
46 }
47
48 fn type_to_nickel(
49 &mut self,
50 ty: &Type,
51 module: &amalgam_core::ir::Module,
52 indent_level: usize,
53 ) -> Result<String, CodegenError> {
54 match ty {
55 Type::String => Ok("String".to_string()),
56 Type::Number => Ok("Number".to_string()),
57 Type::Integer => Ok("Number".to_string()), Type::Bool => Ok("Bool".to_string()),
59 Type::Null => Ok("Null".to_string()),
60 Type::Any => Ok("Dyn".to_string()),
61
62 Type::Array(elem) => {
63 let elem_type = self.type_to_nickel(elem, module, indent_level)?;
64 Ok(format!("Array {}", elem_type))
65 }
66
67 Type::Map { value, .. } => {
68 let value_type = self.type_to_nickel(value, module, indent_level)?;
69 Ok(format!("{{ _ : {} }}", value_type))
70 }
71
72 Type::Optional(inner) => {
73 let inner_type = self.type_to_nickel(inner, module, indent_level)?;
74 Ok(format!("{} | Null", inner_type))
75 }
76
77 Type::Record { fields, open } => {
78 if fields.is_empty() && *open {
79 return Ok("{ .. }".to_string());
80 }
81
82 let mut result = String::from("{\n");
83
84 let mut sorted_fields: Vec<_> = fields.iter().collect();
86 sorted_fields.sort_by_key(|(name, _)| *name);
87
88 for (name, field) in sorted_fields {
89 let field_str = self.field_to_nickel(name, field, module, indent_level + 1)?;
90 result.push_str(&field_str);
91 result.push_str(",\n");
92 }
93
94 if *open {
95 result.push_str(&format!("{}.. | Dyn,\n", self.indent(indent_level + 1)));
96 }
97
98 result.push_str(&self.indent(indent_level));
99 result.push('}');
100 Ok(result)
101 }
102
103 Type::Union(types) => {
104 let type_strs: Result<Vec<_>, _> = types
105 .iter()
106 .map(|t| self.type_to_nickel(t, module, indent_level))
107 .collect();
108 Ok(type_strs?.join(" | "))
109 }
110
111 Type::TaggedUnion {
112 tag_field,
113 variants,
114 } => {
115 let mut contracts = Vec::new();
116 for (tag, variant_type) in variants {
117 let variant_str = self.type_to_nickel(variant_type, module, indent_level)?;
118 contracts.push(format!("({} == \"{}\" && {})", tag_field, tag, variant_str));
119 }
120 Ok(contracts.join(" | "))
121 }
122
123 Type::Reference(name) => {
124 let context = ResolutionContext {
126 current_group: None, current_version: None,
128 current_kind: None,
129 };
130 Ok(self.resolver.resolve(name, module, &context))
131 }
132
133 Type::Contract { base, predicate } => {
134 let base_type = self.type_to_nickel(base, module, indent_level)?;
135 Ok(format!("{} | Contract({})", base_type, predicate))
136 }
137 }
138 }
139
140 fn field_to_nickel(
141 &mut self,
142 name: &str,
143 field: &Field,
144 module: &amalgam_core::ir::Module,
145 indent_level: usize,
146 ) -> Result<String, CodegenError> {
147 let indent = self.indent(indent_level);
148 let type_str = self.type_to_nickel(&field.ty, module, indent_level)?;
149
150 let mut parts = Vec::new();
151
152 parts.push(format!("{}{}", indent, name));
154
155 if !field.required && field.default.is_none() {
158 parts.push("optional".to_string());
159 }
160
161 parts.push(type_str);
163
164 if let Some(desc) = &field.description {
166 parts.push(format!("doc {}", self.format_doc(desc)));
167 }
168
169 if let Some(default) = &field.default {
171 let default_str = format_json_value(default, indent_level);
172 parts.push(format!("default = {}", default_str));
173 }
174
175 Ok(parts.join(" | "))
176 }
177}
178
179fn format_json_value(value: &serde_json::Value, indent_level: usize) -> String {
181 match value {
182 serde_json::Value::Null => "null".to_string(),
183 serde_json::Value::Bool(b) => b.to_string(),
184 serde_json::Value::Number(n) => n.to_string(),
185 serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
186 serde_json::Value::Array(arr) => {
187 let items: Vec<String> = arr
188 .iter()
189 .map(|v| format_json_value(v, indent_level))
190 .collect();
191 format!("[{}]", items.join(", "))
192 }
193 serde_json::Value::Object(obj) => {
194 if obj.is_empty() {
195 "{}".to_string()
196 } else {
197 let indent = " ".repeat((indent_level + 1) * 2);
198 let mut items = Vec::new();
199 for (k, v) in obj {
200 items.push(format!(
201 "{}{} = {}",
202 indent,
203 k,
204 format_json_value(v, indent_level + 1)
205 ));
206 }
207 format!(
208 "{{\n{}\n{}}}",
209 items.join(",\n"),
210 " ".repeat(indent_level * 2)
211 )
212 }
213 }
214 }
215}
216
217impl Default for NickelCodegen {
218 fn default() -> Self {
219 Self::new()
220 }
221}
222
223impl Codegen for NickelCodegen {
224 fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
225 let mut output = String::new();
226
227 for module in &ir.modules {
228 writeln!(output, "# Module: {}", module.name)
230 .map_err(|e| CodegenError::Generation(e.to_string()))?;
231 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
232
233 if !module.imports.is_empty() {
235 for import in &module.imports {
236 let import_path = self.package_mode.convert_import(&import.path);
238
239 let import_statement =
242 if !import_path.contains('/') && import_path.starts_with('"') {
243 format!(
245 "let {} = import {} in",
246 import
247 .alias
248 .as_ref()
249 .unwrap_or(&import.path.replace('/', "_")),
250 import_path
251 )
252 } else {
253 format!(
255 "let {} = import \"{}\" in",
256 import
257 .alias
258 .as_ref()
259 .unwrap_or(&import.path.replace('/', "_")),
260 import_path
261 )
262 };
263
264 writeln!(output, "{}", import_statement)
265 .map_err(|e| CodegenError::Generation(e.to_string()))?;
266 }
267 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
268 }
269
270 writeln!(output, "{{")?;
272
273 for (idx, type_def) in module.types.iter().enumerate() {
274 if let Some(doc) = &type_def.documentation {
276 for line in doc.lines() {
277 writeln!(output, "{}# {}", self.indent(1), line)
278 .map_err(|e| CodegenError::Generation(e.to_string()))?;
279 }
280 }
281
282 let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
284
285 if matches!(type_def.ty, Type::Record { .. }) {
287 write!(output, " {} = ", type_def.name)?;
289 writeln!(output, "{},", type_str)?;
290 } else {
291 writeln!(output, " {} = {},", type_def.name, type_str)?;
292 }
293
294 if idx < module.types.len() - 1 {
296 writeln!(output)?;
297 }
298 }
299
300 if !module.constants.is_empty() {
302 writeln!(output)?; for constant in &module.constants {
305 if let Some(doc) = &constant.documentation {
306 writeln!(output, " # {}", doc)
307 .map_err(|e| CodegenError::Generation(e.to_string()))?;
308 }
309
310 let value_str = format_json_value(&constant.value, 1);
311 writeln!(output, " {} = {},", constant.name, value_str)
312 .map_err(|e| CodegenError::Generation(e.to_string()))?;
313 }
314 }
315
316 writeln!(output, "}}")?;
317 }
318
319 Ok(output)
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326 use amalgam_core::ir::{Metadata, Module};
327 use std::collections::BTreeMap;
328
329 fn create_test_module() -> Module {
330 Module {
331 name: "test".to_string(),
332 imports: Vec::new(),
333 types: Vec::new(),
334 constants: Vec::new(),
335 metadata: Metadata {
336 source_language: None,
337 source_file: None,
338 version: None,
339 generated_at: None,
340 custom: BTreeMap::new(),
341 },
342 }
343 }
344
345 #[test]
346 fn test_simple_type_generation() {
347 let mut codegen = NickelCodegen::new();
348 let module = create_test_module();
349
350 assert_eq!(
351 codegen.type_to_nickel(&Type::String, &module, 0).unwrap(),
352 "String"
353 );
354 assert_eq!(
355 codegen.type_to_nickel(&Type::Number, &module, 0).unwrap(),
356 "Number"
357 );
358 assert_eq!(
359 codegen.type_to_nickel(&Type::Bool, &module, 0).unwrap(),
360 "Bool"
361 );
362 assert_eq!(
363 codegen.type_to_nickel(&Type::Any, &module, 0).unwrap(),
364 "Dyn"
365 );
366 }
367
368 #[test]
369 fn test_array_generation() {
370 let mut codegen = NickelCodegen::new();
371 let module = create_test_module();
372 let array_type = Type::Array(Box::new(Type::String));
373 assert_eq!(
374 codegen.type_to_nickel(&array_type, &module, 0).unwrap(),
375 "Array String"
376 );
377 }
378
379 #[test]
380 fn test_optional_generation() {
381 let mut codegen = NickelCodegen::new();
382 let module = create_test_module();
383 let optional_type = Type::Optional(Box::new(Type::String));
384 assert_eq!(
385 codegen.type_to_nickel(&optional_type, &module, 0).unwrap(),
386 "String | Null"
387 );
388 }
389
390 #[test]
391 fn test_doc_formatting() {
392 let codegen = NickelCodegen::new();
393
394 assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
396
397 let multiline = "This is a\nmultiline doc";
399 assert_eq!(
400 codegen.format_doc(multiline),
401 "m%\"\nThis is a\nmultiline doc\n\"%"
402 );
403
404 assert_eq!(
406 codegen.format_doc("Doc with \"quotes\""),
407 "\"Doc with \\\"quotes\\\"\""
408 );
409 }
410}