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