Files
harmony/opnsense-codegen/src/codegen.rs

305 lines
11 KiB
Rust

use crate::ir::{EnumIR, FieldIR, ModelIR, StructIR, StructKind};
use std::fmt::{Result as FmtResult, Write};
pub struct CodeGenerator {
output: String,
}
impl CodeGenerator {
pub fn new() -> Self {
Self {
output: String::new(),
}
}
pub fn generate(&mut self, model: &ModelIR) -> FmtResult {
let module_name = derive_module_name(&model.root_struct_name);
writeln!(self.output, "//! Auto-generated from OPNsense model XML")?;
writeln!(
self.output,
"//! Mount: `{}` — Version: `{}`",
model.mount, model.version
)?;
writeln!(self.output, "//!")?;
writeln!(
self.output,
"//! **DO NOT EDIT** — produced by opnsense-codegen"
)?;
writeln!(self.output)?;
writeln!(self.output, "use serde::{{Deserialize, Serialize}};")?;
writeln!(self.output, "use std::collections::HashMap;")?;
writeln!(self.output)?;
writeln!(
self.output,
"// ═══════════════════════════════════════════════════════════════════════════"
)?;
writeln!(self.output, "// Enums")?;
writeln!(
self.output,
"// ═══════════════════════════════════════════════════════════════════════════"
)?;
writeln!(self.output)?;
for enum_ir in &model.enums {
self.generate_enum(enum_ir)?;
}
writeln!(self.output)?;
writeln!(
self.output,
"// ═══════════════════════════════════════════════════════════════════════════"
)?;
writeln!(self.output, "// Structs")?;
writeln!(
self.output,
"// ═══════════════════════════════════════════════════════════════════════════"
)?;
writeln!(self.output)?;
for struct_ir in &model.structs {
self.generate_struct(struct_ir, model)?;
}
writeln!(self.output)?;
writeln!(
self.output,
"// ═══════════════════════════════════════════════════════════════════════════"
)?;
writeln!(self.output, "// API Wrapper")?;
writeln!(
self.output,
"// ═══════════════════════════════════════════════════════════════════════════"
)?;
writeln!(self.output)?;
let response_name = format!("{}Response", model.root_struct_name);
let api_key = if model.api_key.is_empty() {
model.mount.trim_start_matches('/').replace('/', "")
} else {
model.api_key.clone()
};
writeln!(
self.output,
"/// Wrapper matching the OPNsense GET response envelope."
)?;
writeln!(
self.output,
"/// `GET /api/{}/get` returns {{ \"{}\": {{ ... }} }}",
api_key, api_key
)?;
writeln!(
self.output,
"#[derive(Debug, Clone, Serialize, Deserialize)]"
)?;
writeln!(self.output, "pub struct {} {{", response_name)?;
writeln!(
self.output,
" pub {}: {},",
api_key, model.root_struct_name
)?;
writeln!(self.output, "}}")?;
Ok(())
}
fn generate_enum(&mut self, enum_ir: &EnumIR) -> FmtResult {
let snake_name = to_snake_case(&enum_ir.name);
writeln!(self.output, "/// {}", enum_ir.name)?;
writeln!(self.output, "#[derive(Debug, Clone, PartialEq, Eq, Hash)]")?;
writeln!(self.output, "pub enum {} {{", enum_ir.name)?;
for variant in &enum_ir.variants {
writeln!(self.output, " {},", variant.rust_name)?;
}
writeln!(self.output, "}}")?;
writeln!(self.output)?;
writeln!(self.output, "pub(crate) mod serde_{} {{", snake_name)?;
writeln!(self.output, " use super::{};", enum_ir.name)?;
writeln!(
self.output,
" use serde::{{Deserialize, Deserializer, Serializer}};"
)?;
writeln!(self.output)?;
writeln!(self.output, " pub fn serialize<S: Serializer>(")?;
writeln!(self.output, " value: &Option<{}>,", enum_ir.name)?;
writeln!(self.output, " serializer: S,")?;
writeln!(self.output, " ) -> Result<S::Ok, S::Error> {{")?;
writeln!(
self.output,
" serializer.serialize_str(match value {{"
)?;
for variant in &enum_ir.variants {
writeln!(
self.output,
" Some({}::{}) => \"{}\",",
enum_ir.name, variant.rust_name, variant.wire_value
)?;
}
writeln!(self.output, " None => \"\",")?;
writeln!(self.output, " }})")?;
writeln!(self.output, " }}")?;
writeln!(self.output)?;
writeln!(
self.output,
" pub fn deserialize<'de, D: Deserializer<'de>>("
)?;
writeln!(self.output, " deserializer: D,")?;
writeln!(
self.output,
" ) -> Result<Option<{}>, D::Error> {{",
enum_ir.name
)?;
writeln!(
self.output,
" let v = serde_json::Value::deserialize(deserializer)?;"
)?;
writeln!(self.output, " match v {{")?;
writeln!(
self.output,
" serde_json::Value::String(s) => match s.as_str() {{"
)?;
for variant in &enum_ir.variants {
writeln!(
self.output,
" \"{}\" => Ok(Some({}::{})),",
variant.wire_value, enum_ir.name, variant.rust_name
)?;
}
writeln!(self.output, " \"\" => Ok(None),")?;
writeln!(
self.output,
" other => Err(serde::de::Error::custom(format!("
)?;
writeln!(
self.output,
" \"unknown {} variant: {{}}\", other",
enum_ir.name
)?;
writeln!(self.output, " ))),")?;
writeln!(self.output, " }},")?;
writeln!(
self.output,
" serde_json::Value::Null => Ok(None),"
)?;
writeln!(
self.output,
" _ => Err(serde::de::Error::custom(\"expected string for {}\")),",
enum_ir.name
)?;
writeln!(self.output, " }}")?;
writeln!(self.output, " }}")?;
writeln!(self.output, "}}")?;
writeln!(self.output)?;
Ok(())
}
fn generate_struct(&mut self, struct_ir: &StructIR, model: &ModelIR) -> FmtResult {
match struct_ir.kind {
StructKind::Root => {
writeln!(self.output, "/// Root model for `{}`", model.mount)?;
writeln!(
self.output,
"#[derive(Debug, Clone, Serialize, Deserialize)]"
)?;
writeln!(self.output, "pub struct {} {{", struct_ir.name)?;
for field in &struct_ir.fields {
self.generate_field(field)?;
}
writeln!(self.output, "}}")?;
}
StructKind::Container => {
let doc = struct_ir
.json_key
.as_ref()
.map(|k| format!("Container for `{}`", k))
.unwrap_or_else(|| "Container".to_string());
writeln!(self.output, "/// {}", doc)?;
writeln!(
self.output,
"#[derive(Debug, Clone, Serialize, Deserialize)]"
)?;
writeln!(self.output, "pub struct {} {{", struct_ir.name)?;
for field in &struct_ir.fields {
self.generate_field(field)?;
}
writeln!(self.output, "}}")?;
}
StructKind::ArrayItem => {
writeln!(
self.output,
"/// Array item for `{}`",
struct_ir.json_key.as_deref().unwrap_or("items")
)?;
writeln!(
self.output,
"#[derive(Debug, Clone, Serialize, Deserialize)]"
)?;
writeln!(self.output, "pub struct {} {{", struct_ir.name)?;
for field in &struct_ir.fields {
self.generate_field(field)?;
}
writeln!(self.output, "}}")?;
}
}
writeln!(self.output)?;
Ok(())
}
fn generate_field(&mut self, field: &FieldIR) -> FmtResult {
if let Some(ref doc) = field.doc {
writeln!(self.output, " /// {}", doc)?;
}
if let Some(ref serde_with) = field.serde_with {
if field.required {
writeln!(self.output, " #[serde(with = \"{}\")]", serde_with)?;
} else {
writeln!(
self.output,
" #[serde(default, with = \"{}\")]",
serde_with
)?;
}
} else if field.field_kind.as_deref() == Some("array_field") {
writeln!(self.output, " #[serde(default)]")?;
} else if !field.required {
writeln!(self.output, " #[serde(default)]")?;
}
writeln!(self.output, " pub {}: {},", field.name, field.rust_type)?;
writeln!(self.output)?;
Ok(())
}
pub fn into_output(self) -> String {
self.output
}
}
fn to_snake_case(s: &str) -> String {
let mut result = String::new();
for (i, c) in s.chars().enumerate() {
if c.is_uppercase() && i > 0 {
result.push('_');
}
result.push(c.to_ascii_lowercase());
}
result
}
pub fn derive_module_name(struct_name: &str) -> String {
to_snake_case(struct_name)
}
pub fn generate(model: &ModelIR) -> String {
let mut generator = CodeGenerator::new();
generator
.generate(model)
.expect("generation should not fail");
generator.into_output()
}