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(_) => {
Ok("interface{}".to_string())
}
Type::TaggedUnion { .. } => {
Ok("interface{}".to_string())
}
Type::Reference(name) => Ok(name.clone()),
Type::Contract { .. } => {
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);
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 {
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()))?;
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()))?;
}
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()))?;
}
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");
}
}