The XML parser silently skipped container elements (like <source>, <destination>) nested inside ArrayField nodes because it only processed children with a type attribute. This caused generated structs to be missing nested fields, forcing opnsense-config to use json!() macros instead of typed structs. - Add container handling in ArrayField and custom *Field child loops - Add serialize function to opn_map serde helper (was deserialize-only) - Change opn_map serde attribute from deserialize_with to with - Regenerate all 9 model files with the fixes NatRuleRule now correctly has source/destination/created/updated container structs with all child fields.
1090 lines
40 KiB
Rust
1090 lines
40 KiB
Rust
use crate::ir::{EnumIR, FieldIR, ModelIR, StructIR, StructKind};
|
|
use std::collections::HashSet;
|
|
use std::fmt::{Result as FmtResult, Write};
|
|
|
|
/// Rust keywords that must be escaped with `r#` when used as field names.
|
|
const RUST_KEYWORDS: &[&str] = &[
|
|
"as", "async", "await", "break", "const", "continue", "crate", "dyn", "else", "enum", "extern",
|
|
"false", "fn", "for", "if", "impl", "in", "let", "loop", "match", "mod", "move", "mut", "pub",
|
|
"ref", "return", "self", "Self", "static", "struct", "super", "trait", "true", "type",
|
|
"unsafe", "use", "where", "while", "abstract", "become", "box", "do", "final", "macro",
|
|
"override", "priv", "typeof", "unsized", "virtual", "yield", "try", "union",
|
|
];
|
|
|
|
fn is_rust_keyword(name: &str) -> bool {
|
|
RUST_KEYWORDS.contains(&name)
|
|
}
|
|
|
|
pub struct CodeGenerator {
|
|
output: String,
|
|
/// Full module path prefix for `serde(with)` attributes,
|
|
/// e.g. `crate::generated::dnsmasq`
|
|
module_path: String,
|
|
}
|
|
|
|
impl CodeGenerator {
|
|
pub fn new(module_path: String) -> Self {
|
|
Self {
|
|
output: String::new(),
|
|
module_path,
|
|
}
|
|
}
|
|
|
|
pub fn generate(&mut self, model: &ModelIR) -> FmtResult {
|
|
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}};")?;
|
|
|
|
// Only import HashMap if any field uses it
|
|
let needs_hashmap = model.structs.iter().any(|s| {
|
|
s.fields
|
|
.iter()
|
|
.any(|f| f.field_kind.as_deref() == Some("array_field"))
|
|
});
|
|
if needs_hashmap {
|
|
writeln!(self.output, "use std::collections::HashMap;")?;
|
|
}
|
|
writeln!(self.output)?;
|
|
|
|
// Emit serde_helpers module with only the helpers actually used
|
|
self.emit_serde_helpers(model)?;
|
|
|
|
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(Default, 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(())
|
|
}
|
|
|
|
/// Scan all fields in the model to find which serde helpers are actually used,
|
|
/// then emit only those.
|
|
fn emit_serde_helpers(&mut self, model: &ModelIR) -> FmtResult {
|
|
let mut used: HashSet<&str> = HashSet::new();
|
|
|
|
for struct_ir in &model.structs {
|
|
for field in &struct_ir.fields {
|
|
if let Some(ref sw) = field.serde_with {
|
|
// Only collect the bare helper names (opn_*)
|
|
if sw.starts_with("opn_") {
|
|
used.insert(sw.as_str());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if used.is_empty() {
|
|
return Ok(());
|
|
}
|
|
|
|
writeln!(self.output, "pub mod serde_helpers {{")?;
|
|
|
|
if used.contains("opn_bool") {
|
|
self.emit_opn_bool()?;
|
|
}
|
|
if used.contains("opn_bool_req") {
|
|
self.emit_opn_bool_req()?;
|
|
}
|
|
if used.contains("opn_u16") {
|
|
self.emit_opn_u16()?;
|
|
}
|
|
if used.contains("opn_u32") {
|
|
self.emit_opn_u32()?;
|
|
}
|
|
if used.contains("opn_string") {
|
|
self.emit_opn_string()?;
|
|
}
|
|
if used.contains("opn_csv") {
|
|
self.emit_opn_csv()?;
|
|
}
|
|
|
|
// Always check if any array fields exist and emit opn_map
|
|
let has_array_fields = model.structs.iter().any(|s| {
|
|
s.fields
|
|
.iter()
|
|
.any(|f| f.field_kind.as_deref() == Some("array_field"))
|
|
});
|
|
if has_array_fields {
|
|
self.emit_opn_map()?;
|
|
}
|
|
|
|
writeln!(self.output, "}}")?;
|
|
writeln!(self.output)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_opn_bool(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_bool {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serializer}};"
|
|
)?;
|
|
writeln!(self.output, " pub fn serialize<S: Serializer>(")?;
|
|
writeln!(self.output, " value: &Option<bool>,")?;
|
|
writeln!(self.output, " serializer: S,")?;
|
|
writeln!(self.output, " ) -> Result<S::Ok, S::Error> {{")?;
|
|
writeln!(self.output, " match value {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(true) => serializer.serialize_str(\"1\"),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(false) => serializer.serialize_str(\"0\"),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" None => serializer.serialize_str(\"\"),"
|
|
)?;
|
|
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<bool>, D::Error> {{"
|
|
)?;
|
|
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() {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" \"1\" | \"true\" => Ok(Some(true)),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" \"0\" | \"false\" => Ok(Some(false)),"
|
|
)?;
|
|
writeln!(self.output, " \"\" => Ok(None),")?;
|
|
writeln!(
|
|
self.output,
|
|
" other => Err(serde::de::Error::custom(format!(\"invalid bool string: {{other}}\"))),"
|
|
)?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Bool(b) => Ok(Some(*b)),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Number(n) => match n.as_u64() {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(1) => Ok(Some(true)),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(0) => Ok(Some(false)),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(format!(\"invalid bool number: {{n}}\"))),"
|
|
)?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Null => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(\"expected string, bool, or number for bool field\")),"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_opn_bool_req(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_bool_req {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serializer}};"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" pub fn serialize<S: Serializer>(value: &bool, serializer: S) -> Result<S::Ok, S::Error> {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serializer.serialize_str(if *value {{ \"1\" }} else {{ \"0\" }})"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(
|
|
self.output,
|
|
" pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<bool, D::Error> {{"
|
|
)?;
|
|
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() {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" \"1\" | \"true\" => Ok(true),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" \"0\" | \"false\" => Ok(false),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" other => Err(serde::de::Error::custom(format!(\"invalid required bool: {{other}}\"))),"
|
|
)?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Bool(b) => Ok(*b),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Number(n) => match n.as_u64() {{"
|
|
)?;
|
|
writeln!(self.output, " Some(1) => Ok(true),")?;
|
|
writeln!(self.output, " Some(0) => Ok(false),")?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(format!(\"invalid required bool number: {{n}}\"))),"
|
|
)?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(\"expected string, bool, or number for required bool\")),"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_opn_u16(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_u16 {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serializer}};"
|
|
)?;
|
|
writeln!(self.output, " pub fn serialize<S: Serializer>(")?;
|
|
writeln!(self.output, " value: &Option<u16>,")?;
|
|
writeln!(self.output, " serializer: S,")?;
|
|
writeln!(self.output, " ) -> Result<S::Ok, S::Error> {{")?;
|
|
writeln!(self.output, " match value {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(v) => serializer.serialize_str(&v.to_string()),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" None => serializer.serialize_str(\"\"),"
|
|
)?;
|
|
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<u16>, D::Error> {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" let v = serde_json::Value::deserialize(deserializer)?;"
|
|
)?;
|
|
writeln!(self.output, " match &v {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) if s.is_empty() => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) => s.parse::<u16>().map(Some).map_err(serde::de::Error::custom),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Number(n) => n.as_u64().and_then(|n| u16::try_from(n).ok()).map(Some).ok_or_else(|| serde::de::Error::custom(\"number out of u16 range\")),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Null => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(\"expected string or number for u16\")),"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_opn_u32(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_u32 {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serializer}};"
|
|
)?;
|
|
writeln!(self.output, " pub fn serialize<S: Serializer>(")?;
|
|
writeln!(self.output, " value: &Option<u32>,")?;
|
|
writeln!(self.output, " serializer: S,")?;
|
|
writeln!(self.output, " ) -> Result<S::Ok, S::Error> {{")?;
|
|
writeln!(self.output, " match value {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(v) => serializer.serialize_str(&v.to_string()),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" None => serializer.serialize_str(\"\"),"
|
|
)?;
|
|
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<u32>, D::Error> {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" let v = serde_json::Value::deserialize(deserializer)?;"
|
|
)?;
|
|
writeln!(self.output, " match &v {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) if s.is_empty() => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) => s.parse::<u32>().map(Some).map_err(serde::de::Error::custom),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Number(n) => n.as_u64().and_then(|n| u32::try_from(n).ok()).map(Some).ok_or_else(|| serde::de::Error::custom(\"number out of u32 range\")),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Null => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(\"expected string or number for u32\")),"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_opn_string(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_string {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serializer}};"
|
|
)?;
|
|
writeln!(self.output, " pub fn serialize<S: Serializer>(")?;
|
|
writeln!(self.output, " value: &Option<String>,")?;
|
|
writeln!(self.output, " serializer: S,")?;
|
|
writeln!(self.output, " ) -> Result<S::Ok, S::Error> {{")?;
|
|
writeln!(self.output, " match value {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(v) => serializer.serialize_str(v),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" None => serializer.serialize_str(\"\"),"
|
|
)?;
|
|
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<String>, D::Error> {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" let v = serde_json::Value::deserialize(deserializer)?;"
|
|
)?;
|
|
writeln!(self.output, " match v {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) if s.is_empty() => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) => Ok(Some(s)),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Object(map) => {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" // OPNsense select widget: extract selected key"
|
|
)?;
|
|
writeln!(self.output, " let selected = map.iter()")?;
|
|
writeln!(
|
|
self.output,
|
|
" .find(|(_, v)| v.get(\"selected\").and_then(|s| s.as_i64()).unwrap_or(0) == 1)"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" .map(|(k, _)| k.clone())"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" .filter(|k| !k.is_empty());"
|
|
)?;
|
|
writeln!(self.output, " Ok(selected)")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Null => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Array(_) => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(\"expected string, object, or null\")),"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
fn emit_opn_csv(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_csv {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serializer}};"
|
|
)?;
|
|
writeln!(self.output, " pub fn serialize<S: Serializer>(")?;
|
|
writeln!(self.output, " value: &Option<Vec<String>>,")?;
|
|
writeln!(self.output, " serializer: S,")?;
|
|
writeln!(self.output, " ) -> Result<S::Ok, S::Error> {{")?;
|
|
writeln!(self.output, " match value {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(v) if !v.is_empty() => serializer.serialize_str(&v.join(\",\")),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => serializer.serialize_str(\"\"),"
|
|
)?;
|
|
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<Vec<String>>, D::Error> {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" let v = serde_json::Value::deserialize(deserializer)?;"
|
|
)?;
|
|
writeln!(self.output, " match v {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) if s.is_empty() => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) => Ok(Some(s.split(',').map(|item| item.trim().to_string()).collect())),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Array(arr) => {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" let items: Result<Vec<String>, _> = arr.into_iter().map(|v| match v {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::String(s) => Ok(s),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" other => Err(serde::de::Error::custom(format!(\"expected string in array, got: {{other}}\"))),"
|
|
)?;
|
|
writeln!(self.output, " }}).collect();")?;
|
|
writeln!(self.output, " let items = items?;")?;
|
|
writeln!(
|
|
self.output,
|
|
" if items.is_empty() {{ Ok(None) }} else {{ Ok(Some(items)) }}"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Object(map) => {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" // OPNsense select widget: {{\"key\": {{\"value\": \"...\", \"selected\": 1}}}}"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" let selected: Vec<String> = map.into_iter()"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" .filter(|(_, v)| v.get(\"selected\").and_then(|s| s.as_i64()).unwrap_or(0) == 1)"
|
|
)?;
|
|
writeln!(self.output, " .map(|(k, _)| k)")?;
|
|
writeln!(
|
|
self.output,
|
|
" .filter(|k| !k.is_empty())"
|
|
)?;
|
|
writeln!(self.output, " .collect();")?;
|
|
writeln!(
|
|
self.output,
|
|
" if selected.is_empty() {{ Ok(None) }} else {{ Ok(Some(selected)) }}"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Null => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" _ => Err(serde::de::Error::custom(\"expected string, array, or object for csv field\")),"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Emit `opn_map` — serializes/deserializes HashMap fields that OPNsense may return
|
|
/// as either `{}` (object) or `[]` (empty array).
|
|
fn emit_opn_map(&mut self) -> FmtResult {
|
|
writeln!(self.output, " pub mod opn_map {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" use serde::{{Deserialize, Deserializer, Serialize, Serializer}};"
|
|
)?;
|
|
writeln!(self.output, " use std::collections::HashMap;")?;
|
|
writeln!(self.output, " use std::fmt;")?;
|
|
writeln!(self.output, " use std::marker::PhantomData;")?;
|
|
writeln!(self.output)?;
|
|
writeln!(
|
|
self.output,
|
|
" pub fn deserialize<'de, D, V>(deserializer: D) -> Result<HashMap<String, V>, D::Error>"
|
|
)?;
|
|
writeln!(self.output, " where")?;
|
|
writeln!(self.output, " D: Deserializer<'de>,")?;
|
|
writeln!(self.output, " V: Deserialize<'de>,")?;
|
|
writeln!(self.output, " {{")?;
|
|
writeln!(
|
|
self.output,
|
|
" struct MapOrArray<V>(PhantomData<V>);"
|
|
)?;
|
|
writeln!(self.output)?;
|
|
writeln!(
|
|
self.output,
|
|
" impl<'de, V: Deserialize<'de>> serde::de::Visitor<'de> for MapOrArray<V> {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" type Value = HashMap<String, V>;"
|
|
)?;
|
|
writeln!(self.output)?;
|
|
writeln!(
|
|
self.output,
|
|
" fn expecting(&self, f: &mut fmt::Formatter) -> fmt::Result {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" f.write_str(\"a map or an empty array\")"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
writeln!(
|
|
self.output,
|
|
" fn visit_map<A: serde::de::MapAccess<'de>>(self, mut map: A) -> Result<Self::Value, A::Error> {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" let mut result = HashMap::new();"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" while let Some((k, v)) = map.next_entry()? {{"
|
|
)?;
|
|
writeln!(self.output, " result.insert(k, v);")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " Ok(result)")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
writeln!(
|
|
self.output,
|
|
" fn visit_seq<A: serde::de::SeqAccess<'de>>(self, mut seq: A) -> Result<Self::Value, A::Error> {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" // Accept empty arrays as empty maps"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" while seq.next_element::<serde::de::IgnoredAny>()?.is_some() {{}}"
|
|
)?;
|
|
writeln!(self.output, " Ok(HashMap::new())")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
writeln!(
|
|
self.output,
|
|
" deserializer.deserialize_any(MapOrArray(PhantomData))"
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output)?;
|
|
// serialize: HashMap<String, V> → JSON object
|
|
writeln!(
|
|
self.output,
|
|
" pub fn serialize<S, V>(map: &HashMap<String, V>, serializer: S) -> Result<S::Ok, S::Error>"
|
|
)?;
|
|
writeln!(self.output, " where")?;
|
|
writeln!(self.output, " S: Serializer,")?;
|
|
writeln!(self.output, " V: Serialize,")?;
|
|
writeln!(self.output, " {{")?;
|
|
writeln!(self.output, " map.serialize(serializer)")?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }}")?;
|
|
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,
|
|
" /// Preserves unrecognized wire values for safe round-tripping."
|
|
)?;
|
|
writeln!(self.output, " Other(String),")?;
|
|
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,
|
|
" Some({}::Other(s)) => s.as_str(),",
|
|
enum_ir.name
|
|
)?;
|
|
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 => Ok(Some({}::Other(other.to_string()))),",
|
|
enum_ir.name
|
|
)?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Object(map) => {{"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" // OPNsense select widget: {{\"key\": {{\"value\": \"...\", \"selected\": 1}}}}"
|
|
)?;
|
|
writeln!(self.output, " let selected_key = map.iter()")?;
|
|
writeln!(
|
|
self.output,
|
|
" .find(|(_, v)| v.get(\"selected\").and_then(|s| s.as_i64()).unwrap_or(0) == 1)"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" .map(|(k, _)| k.as_str());"
|
|
)?;
|
|
writeln!(self.output, " match selected_key {{")?;
|
|
for variant in &enum_ir.variants {
|
|
writeln!(
|
|
self.output,
|
|
" Some(\"{}\") => Ok(Some({}::{})),",
|
|
variant.wire_value, enum_ir.name, variant.rust_name
|
|
)?;
|
|
}
|
|
writeln!(
|
|
self.output,
|
|
" Some(\"\") | None => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(other) => Ok(Some({}::Other(other.to_string()))),",
|
|
enum_ir.name
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Null => Ok(None),"
|
|
)?;
|
|
// Array-style select widget: [{value, selected}, ...]
|
|
writeln!(
|
|
self.output,
|
|
" serde_json::Value::Array(arr) => {{"
|
|
)?;
|
|
writeln!(self.output, " let selected = arr.iter()")?;
|
|
writeln!(
|
|
self.output,
|
|
" .find(|v| v.get(\"selected\").and_then(|s| s.as_i64()).unwrap_or(0) == 1)"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" .and_then(|v| v.get(\"value\").and_then(|s| s.as_str()));"
|
|
)?;
|
|
writeln!(self.output, " match selected {{")?;
|
|
for variant in &enum_ir.variants {
|
|
writeln!(
|
|
self.output,
|
|
" Some(\"{}\") => Ok(Some({}::{})),",
|
|
variant.wire_value, enum_ir.name, variant.rust_name
|
|
)?;
|
|
}
|
|
writeln!(
|
|
self.output,
|
|
" Some(\"\") | None => Ok(None),"
|
|
)?;
|
|
writeln!(
|
|
self.output,
|
|
" Some(other) => Ok(Some({}::Other(other.to_string()))),",
|
|
enum_ir.name
|
|
)?;
|
|
writeln!(self.output, " }}")?;
|
|
writeln!(self.output, " }},")?;
|
|
writeln!(
|
|
self.output,
|
|
" other => Err(serde::de::Error::custom(format!(\"unexpected type for {}: {{:?}}\", other))),",
|
|
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)?;
|
|
}
|
|
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)?;
|
|
}
|
|
StructKind::ArrayItem => {
|
|
writeln!(
|
|
self.output,
|
|
"/// Array item for `{}`",
|
|
struct_ir.json_key.as_deref().unwrap_or("items")
|
|
)?;
|
|
}
|
|
}
|
|
writeln!(
|
|
self.output,
|
|
"#[derive(Default, 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)?;
|
|
}
|
|
|
|
let is_keyword = is_rust_keyword(&field.name);
|
|
let has_invalid_chars = field.name.contains('-') || field.name.contains('.');
|
|
let needs_rename = is_keyword || has_invalid_chars;
|
|
|
|
let field_ident = if is_keyword {
|
|
format!("r#{}", field.name)
|
|
} else if has_invalid_chars {
|
|
field.name.replace('-', "_").replace('.', "_")
|
|
} else {
|
|
field.name.clone()
|
|
};
|
|
|
|
// Build the serde attribute parts
|
|
let mut serde_parts: Vec<String> = Vec::new();
|
|
|
|
if needs_rename {
|
|
serde_parts.push(format!("rename = \"{}\"", field.name));
|
|
}
|
|
|
|
// Always add default — OPNsense API responses may omit fields
|
|
// even if the XML model marks them as required
|
|
serde_parts.push("default".to_string());
|
|
|
|
if field.field_kind.as_deref() == Some("array_field") {
|
|
// ArrayField: use opn_map to handle both {} and [] from the API
|
|
let map_path = format!("{}::serde_helpers::opn_map", self.module_path);
|
|
serde_parts.push(format!("with = \"{}\"", map_path));
|
|
} else if let Some(ref serde_with) = field.serde_with {
|
|
let full_path = self.resolve_serde_path(serde_with);
|
|
serde_parts.push(format!("with = \"{}\"", full_path));
|
|
}
|
|
|
|
if !serde_parts.is_empty() {
|
|
writeln!(self.output, " #[serde({})]", serde_parts.join(", "))?;
|
|
}
|
|
|
|
writeln!(self.output, " pub {}: {},", field_ident, field.rust_type)?;
|
|
writeln!(self.output)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Resolve a bare serde_with name to a full path.
|
|
/// - `opn_bool` → `{module_path}::serde_helpers::opn_bool`
|
|
/// - `serde_add_mac` → `{module_path}::serde_add_mac`
|
|
fn resolve_serde_path(&self, serde_with: &str) -> String {
|
|
if serde_with.starts_with("opn_") {
|
|
format!("{}::serde_helpers::{}", self.module_path, serde_with)
|
|
} else {
|
|
// Enum serde modules (e.g. serde_add_mac)
|
|
format!("{}::{}", self.module_path, serde_with)
|
|
}
|
|
}
|
|
|
|
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)
|
|
}
|
|
|
|
/// Generate Rust code from a ModelIR.
|
|
///
|
|
/// `module_path` is the full crate path prefix for `serde(with)` attributes,
|
|
/// e.g. `crate::generated::dnsmasq`. If None, it is auto-derived from the
|
|
/// root struct name as `crate::generated::{module_name}`.
|
|
pub fn generate(model: &ModelIR, module_path: Option<&str>) -> String {
|
|
let module_name = derive_module_name(&model.root_struct_name);
|
|
let path = module_path
|
|
.map(|s| s.to_string())
|
|
.unwrap_or_else(|| format!("crate::generated::{}", module_name));
|
|
|
|
let mut generator = CodeGenerator::new(path);
|
|
generator
|
|
.generate(model)
|
|
.expect("generation should not fail");
|
|
generator.into_output()
|
|
}
|
|
|
|
/// Scan a directory for generated `.rs` files and write a `mod.rs` that
|
|
/// re-exports all of them.
|
|
pub fn write_mod_rs(dir: &std::path::Path) -> std::io::Result<()> {
|
|
let mut modules: Vec<String> = Vec::new();
|
|
|
|
for entry in std::fs::read_dir(dir)? {
|
|
let entry = entry?;
|
|
let path = entry.path();
|
|
if path.extension().and_then(|e| e.to_str()) == Some("rs") {
|
|
if let Some(stem) = path.file_stem().and_then(|s| s.to_str()) {
|
|
if stem != "mod" {
|
|
modules.push(stem.to_string());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
modules.sort();
|
|
|
|
let mut content = String::new();
|
|
content.push_str("//! Auto-generated module index — DO NOT EDIT\n");
|
|
content.push_str("//!\n");
|
|
content.push_str("//! Produced by `opnsense-codegen`.\n\n");
|
|
for m in &modules {
|
|
content.push_str(&format!("pub mod {};\n", m));
|
|
}
|
|
|
|
std::fs::write(dir.join("mod.rs"), content)?;
|
|
Ok(())
|
|
}
|