305 lines
11 KiB
Rust
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()
|
|
}
|