amalgam-codegen 0.6.4

Code generators for Nickel and Go for amalgam
Documentation
//! Go code generator

use crate::{Codegen, CodegenError};
use amalgam_core::{
    types::{Field, Type},
    IR,
};
use std::fmt::Write;

pub struct GoCodegen {
    indent_size: usize,
}

impl GoCodegen {
    pub fn new() -> Self {
        Self { indent_size: 4 }
    }

    fn indent(&self, level: usize) -> String {
        " ".repeat(level * self.indent_size)
    }

    fn type_to_go(&self, ty: &Type) -> Result<String, CodegenError> {
        match ty {
            Type::String => Ok("string".to_string()),
            Type::Number => Ok("float64".to_string()),
            Type::Integer => Ok("int64".to_string()),
            Type::Bool => Ok("bool".to_string()),
            Type::Null => Ok("interface{}".to_string()),
            Type::Any => Ok("interface{}".to_string()),

            Type::Array(elem) => {
                let elem_type = self.type_to_go(elem)?;
                Ok(format!("[]{}", elem_type))
            }

            Type::Map { key, value } => {
                let key_type = self.type_to_go(key)?;
                let value_type = self.type_to_go(value)?;
                Ok(format!("map[{}]{}", key_type, value_type))
            }

            Type::Optional(inner) => {
                let inner_type = self.type_to_go(inner)?;
                Ok(format!("*{}", inner_type))
            }

            Type::Record { fields, .. } => {
                let mut result = String::from("struct {\n");
                for (name, field) in fields {
                    let field_str = self.field_to_go(name, field, 1)?;
                    result.push_str(&field_str);
                    result.push('\n');
                }
                result.push_str(&self.indent(0));
                result.push('}');
                Ok(result)
            }

            Type::Union(_) => {
                // Go doesn't have union types, use interface{}
                Ok("interface{}".to_string())
            }

            Type::TaggedUnion { .. } => {
                // Would need to generate interface with type assertion
                Ok("interface{}".to_string())
            }

            Type::Reference(name) => Ok(name.clone()),

            Type::Contract { .. } => {
                // Contracts become interfaces in Go
                Ok("interface{}".to_string())
            }
        }
    }

    fn field_to_go(
        &self,
        name: &str,
        field: &Field,
        indent_level: usize,
    ) -> Result<String, CodegenError> {
        let indent = self.indent(indent_level);
        let go_name = self.to_go_field_name(name);
        let type_str = self.type_to_go(&field.ty)?;

        let mut result = format!("{}{} {}", indent, go_name, type_str);

        // Add JSON tag
        let mut tags = Vec::new();
        tags.push(format!("json:\"{}\"", name));

        if !field.required {
            tags[0] = format!("json:\"{},omitempty\"", name);
        }

        if !tags.is_empty() {
            result.push_str(&format!(" `{}`", tags.join(" ")));
        }

        if let Some(desc) = &field.description {
            result = format!("{}// {}\n{}", indent, desc, result);
        }

        Ok(result)
    }

    fn to_go_field_name(&self, name: &str) -> String {
        // Convert to Go public field name (capitalize first letter)
        let mut chars = name.chars();
        match chars.next() {
            None => String::new(),
            Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
        }
    }
}

impl Default for GoCodegen {
    fn default() -> Self {
        Self::new()
    }
}

impl Codegen for GoCodegen {
    fn generate(&mut self, ir: &IR) -> Result<String, CodegenError> {
        let mut output = String::new();

        for module in &ir.modules {
            writeln!(output, "// Code generated by amalgam. DO NOT EDIT.")
                .map_err(|e| CodegenError::Generation(e.to_string()))?;
            writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;

            writeln!(output, "package {}", module.name)
                .map_err(|e| CodegenError::Generation(e.to_string()))?;
            writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;

            // Generate imports
            if !module.imports.is_empty() {
                writeln!(output, "import (")
                    .map_err(|e| CodegenError::Generation(e.to_string()))?;
                for import in &module.imports {
                    writeln!(output, "{}\"{}\"", self.indent(1), import.path)
                        .map_err(|e| CodegenError::Generation(e.to_string()))?;
                }
                writeln!(output, ")").map_err(|e| CodegenError::Generation(e.to_string()))?;
                writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
            }

            // Generate type definitions
            for type_def in &module.types {
                if let Some(doc) = &type_def.documentation {
                    writeln!(output, "// {}", doc)
                        .map_err(|e| CodegenError::Generation(e.to_string()))?;
                }

                let type_str = self.type_to_go(&type_def.ty)?;
                writeln!(output, "type {} {}", type_def.name, type_str)
                    .map_err(|e| CodegenError::Generation(e.to_string()))?;
                writeln!(output).map_err(|e| CodegenError::Generation(e.to_string()))?;
            }

            // Generate constants
            if !module.constants.is_empty() {
                writeln!(output, "const (").map_err(|e| CodegenError::Generation(e.to_string()))?;
                for constant in &module.constants {
                    if let Some(doc) = &constant.documentation {
                        writeln!(output, "{}// {}", self.indent(1), doc)
                            .map_err(|e| CodegenError::Generation(e.to_string()))?;
                    }

                    writeln!(
                        output,
                        "{}{} = {}",
                        self.indent(1),
                        constant.name,
                        serde_json::to_string(&constant.value)
                            .unwrap_or_else(|_| "nil".to_string())
                    )
                    .map_err(|e| CodegenError::Generation(e.to_string()))?;
                }
                writeln!(output, ")").map_err(|e| CodegenError::Generation(e.to_string()))?;
            }
        }

        Ok(output)
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_simple_type_generation() {
        let codegen = GoCodegen::new();

        assert_eq!(codegen.type_to_go(&Type::String).unwrap(), "string");
        assert_eq!(codegen.type_to_go(&Type::Number).unwrap(), "float64");
        assert_eq!(codegen.type_to_go(&Type::Integer).unwrap(), "int64");
        assert_eq!(codegen.type_to_go(&Type::Bool).unwrap(), "bool");
        assert_eq!(codegen.type_to_go(&Type::Any).unwrap(), "interface{}");
    }

    #[test]
    fn test_array_generation() {
        let codegen = GoCodegen::new();
        let array_type = Type::Array(Box::new(Type::String));
        assert_eq!(codegen.type_to_go(&array_type).unwrap(), "[]string");
    }

    #[test]
    fn test_map_generation() {
        let codegen = GoCodegen::new();
        let map_type = Type::Map {
            key: Box::new(Type::String),
            value: Box::new(Type::Integer),
        };
        assert_eq!(codegen.type_to_go(&map_type).unwrap(), "map[string]int64");
    }
}