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 {
149 parts.push("optional".to_string());
150 }
151
152 parts.push(type_str);
154
155 if let Some(default) = &field.default {
157 let default_str = format_json_value(default, indent_level);
158 parts.push(format!("default = {}", default_str));
159 }
160
161 if let Some(desc) = &field.description {
163 parts.push(format!("doc {}", self.format_doc(desc)));
164 }
165
166 Ok(parts.join(" | "))
167 }
168}
169
170fn format_json_value(value: &serde_json::Value, indent_level: usize) -> String {
172 match value {
173 serde_json::Value::Null => "null".to_string(),
174 serde_json::Value::Bool(b) => b.to_string(),
175 serde_json::Value::Number(n) => n.to_string(),
176 serde_json::Value::String(s) => format!("\"{}\"", s.replace('"', "\\\"")),
177 serde_json::Value::Array(arr) => {
178 let items: Vec<String> = arr
179 .iter()
180 .map(|v| format_json_value(v, indent_level))
181 .collect();
182 format!("[{}]", items.join(", "))
183 }
184 serde_json::Value::Object(obj) => {
185 if obj.is_empty() {
186 "{}".to_string()
187 } else {
188 let indent = " ".repeat((indent_level + 1) * 2);
189 let mut items = Vec::new();
190 for (k, v) in obj {
191 items.push(format!(
192 "{}{} = {}",
193 indent,
194 k,
195 format_json_value(v, indent_level + 1)
196 ));
197 }
198 format!(
199 "{{\n{}\n{}}}",
200 items.join(",\n"),
201 " ".repeat(indent_level * 2)
202 )
203 }
204 }
205 }
206}
207
208impl Default for NickelCodegen {
209 fn default() -> Self {
210 Self::new()
211 }
212}
213
214impl Codegen for NickelCodegen {
215 fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
216 let mut output = String::new();
217
218 for module in &ir.modules {
219 writeln!(output, "# Module: {}", module.name)
221 .map_err(|e| CodegenError::Generation(e.to_string()))?;
222 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
223
224 if !module.imports.is_empty() {
226 for import in &module.imports {
227 writeln!(
228 output,
229 "let {} = import \"{}\" in",
230 import.alias.as_ref().unwrap_or(&import.path),
231 import.path
232 )
233 .map_err(|e| CodegenError::Generation(e.to_string()))?;
234 }
235 writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
236 }
237
238 writeln!(output, "{{")?;
240
241 for (idx, type_def) in module.types.iter().enumerate() {
242 if let Some(doc) = &type_def.documentation {
244 for line in doc.lines() {
245 writeln!(output, "{}# {}", self.indent(1), line)
246 .map_err(|e| CodegenError::Generation(e.to_string()))?;
247 }
248 }
249
250 let type_str = self.type_to_nickel(&type_def.ty, module, 1)?;
252
253 if matches!(type_def.ty, Type::Record { .. }) {
255 write!(output, " {} = ", type_def.name)?;
257 writeln!(output, "{},", type_str)?;
258 } else {
259 writeln!(output, " {} = {},", type_def.name, type_str)?;
260 }
261
262 if idx < module.types.len() - 1 {
264 writeln!(output)?;
265 }
266 }
267
268 if !module.constants.is_empty() {
270 writeln!(output)?; for constant in &module.constants {
273 if let Some(doc) = &constant.documentation {
274 writeln!(output, " # {}", doc)
275 .map_err(|e| CodegenError::Generation(e.to_string()))?;
276 }
277
278 let value_str = format_json_value(&constant.value, 1);
279 writeln!(output, " {} = {},", constant.name, value_str)
280 .map_err(|e| CodegenError::Generation(e.to_string()))?;
281 }
282 }
283
284 writeln!(output, "}}")?;
285 }
286
287 Ok(output)
288 }
289}
290
291#[cfg(test)]
292mod tests {
293 use super::*;
294 use amalgam_core::ir::{Metadata, Module};
295 use std::collections::BTreeMap;
296
297 fn create_test_module() -> Module {
298 Module {
299 name: "test".to_string(),
300 imports: Vec::new(),
301 types: Vec::new(),
302 constants: Vec::new(),
303 metadata: Metadata {
304 source_language: None,
305 source_file: None,
306 version: None,
307 generated_at: None,
308 custom: BTreeMap::new(),
309 },
310 }
311 }
312
313 #[test]
314 fn test_simple_type_generation() {
315 let mut codegen = NickelCodegen::new();
316 let module = create_test_module();
317
318 assert_eq!(
319 codegen.type_to_nickel(&Type::String, &module, 0).unwrap(),
320 "String"
321 );
322 assert_eq!(
323 codegen.type_to_nickel(&Type::Number, &module, 0).unwrap(),
324 "Number"
325 );
326 assert_eq!(
327 codegen.type_to_nickel(&Type::Bool, &module, 0).unwrap(),
328 "Bool"
329 );
330 assert_eq!(
331 codegen.type_to_nickel(&Type::Any, &module, 0).unwrap(),
332 "Dyn"
333 );
334 }
335
336 #[test]
337 fn test_array_generation() {
338 let mut codegen = NickelCodegen::new();
339 let module = create_test_module();
340 let array_type = Type::Array(Box::new(Type::String));
341 assert_eq!(
342 codegen.type_to_nickel(&array_type, &module, 0).unwrap(),
343 "Array String"
344 );
345 }
346
347 #[test]
348 fn test_optional_generation() {
349 let mut codegen = NickelCodegen::new();
350 let module = create_test_module();
351 let optional_type = Type::Optional(Box::new(Type::String));
352 assert_eq!(
353 codegen.type_to_nickel(&optional_type, &module, 0).unwrap(),
354 "String | Null"
355 );
356 }
357
358 #[test]
359 fn test_doc_formatting() {
360 let codegen = NickelCodegen::new();
361
362 assert_eq!(codegen.format_doc("Short doc"), "\"Short doc\"");
364
365 let multiline = "This is a\nmultiline doc";
367 assert_eq!(
368 codegen.format_doc(multiline),
369 "m%\"\nThis is a\nmultiline doc\n\"%"
370 );
371
372 assert_eq!(
374 codegen.format_doc("Doc with \"quotes\""),
375 "\"Doc with \\\"quotes\\\"\""
376 );
377 }
378}