proxmox/proxmox-section-config/src/typed.rs
Wolfgang Bumiller 61d2eb891e section-config: make typed data usable with .with_type_key()
The original typed section config data would insert and remove the
type properties. With the introduction of `.with_type_key()` this is
done on the parse/write side instead, so we need to be able to opt out
of this.

Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
2024-08-13 15:45:20 +02:00

466 lines
14 KiB
Rust

//! Support for `enum` typed section configs.
use std::collections::HashMap;
use anyhow::Error;
use serde::{de::DeserializeOwned, Serialize};
use serde_json::{json, Value};
use crate::SectionConfig;
use crate::SectionConfigData as RawSectionConfigData;
/// Implement this for an enum to allow it to be used as a section config.
pub trait ApiSectionDataEntry: Sized {
/// If the serde representation is internally tagged, this should be the name of the type
/// property.
const INTERNALLY_TAGGED: Option<&'static str> = None;
/// If the [`SectionConfig`] returned by the [`section_config()`][seccfg] method includes the
/// `.with_type_key()` properties correctly, this should be set to `true`, otherwise `false`
/// (which is the default).
///
/// [seccfg] ApiSectionDataEntry::section_config()
const SECION_CONFIG_USES_TYPE_KEY: bool = false;
/// Get the `SectionConfig` configuration for this enum.
fn section_config() -> &'static SectionConfig;
/// Maps an enum value to its type name.
fn section_type(&self) -> &'static str;
/// Provided. If necessary, insert the "type" property into an object and then deserialize it.
fn from_value(ty: String, mut value: Value) -> Result<Self, serde_json::Error>
where
Self: serde::de::DeserializeOwned,
{
if Self::SECION_CONFIG_USES_TYPE_KEY {
serde_json::from_value::<Self>(value)
} else if let Some(tag) = Self::INTERNALLY_TAGGED {
match &mut value {
Value::Object(obj) => {
obj.insert(tag.to_string(), ty.into());
serde_json::from_value::<Self>(value)
}
_ => {
use serde::ser::Error;
Err(serde_json::Error::custom(
"cannot add type property to non-object",
))
}
}
} else {
serde_json::from_value::<Self>(json!({ ty: value }))
}
}
/// Turn this entry into a pair of `(type, value)`.
/// Works for externally tagged objects and internally tagged objects, provided the
/// `INTERNALLY_TAGGED` value is set.
fn into_pair(self) -> Result<(String, Value), serde_json::Error>
where
Self: Serialize,
{
to_pair(
serde_json::to_value(self)?,
Self::INTERNALLY_TAGGED,
!Self::SECION_CONFIG_USES_TYPE_KEY,
)
}
/// Turn this entry into a pair of `(type, value)`.
/// Works for externally tagged objects and internally tagged objects, provided the
/// `INTERNALLY_TAGGED` value is set.
fn to_pair(&self) -> Result<(String, Value), serde_json::Error>
where
Self: Serialize,
{
to_pair(
serde_json::to_value(self)?,
Self::INTERNALLY_TAGGED,
!Self::SECION_CONFIG_USES_TYPE_KEY,
)
}
/// Provided. Shortcut for `Self::section_config().parse(filename, data)?.try_into()`.
fn parse_section_config(filename: &str, data: &str) -> Result<SectionConfigData<Self>, Error>
where
Self: serde::de::DeserializeOwned,
{
Ok(Self::section_config().parse(filename, data)?.try_into()?)
}
/// Provided. Shortcut for `Self::section_config().write(filename, &data.try_into()?)`.
fn write_section_config(filename: &str, data: &SectionConfigData<Self>) -> Result<String, Error>
where
Self: Serialize,
{
Self::section_config().write(filename, &data.try_into()?)
}
}
/// Turn an object into a `(type, value)` pair.
///
/// For internally tagged objects (`tag` is `Some`), the type is *extracted* first. It is then no
/// longer present in the object itself.
///
/// Otherwise, an externally typed object is expected, which means a map with a single entry, with
/// the type being the key.
///
/// Otherwise this will fail.
fn to_pair(
value: Value,
tag: Option<&'static str>,
strip_tag: bool,
) -> Result<(String, Value), serde_json::Error> {
use serde::ser::Error;
match (value, tag) {
(Value::Object(mut obj), Some(tag)) => {
let id = if strip_tag {
obj.remove(tag)
.ok_or_else(|| Error::custom(format!("tag {tag:?} missing in object")))?
} else {
obj.get(tag)
.ok_or_else(|| Error::custom(format!("tag {tag:?} missing in object")))?
.clone()
};
match id {
Value::String(id) => Ok((id, Value::Object(obj))),
_ => Err(Error::custom(format!(
"tag {tag:?} has invalid value (not a string)"
))),
}
}
(Value::Object(obj), None) if obj.len() == 1 => Ok(
obj.into_iter().next().unwrap(), // unwrap: we checked the length
),
_ => Err(Error::custom("unexpected serialization method")),
}
}
/// Typed variant of [`SectionConfigData`](proxmox_section_config::SectionConfigData).
/// This dereferences to the section hash for convenience.
#[derive(Debug, Clone)]
pub struct SectionConfigData<T> {
pub sections: HashMap<String, T>,
pub order: Vec<String>,
}
impl<T> Default for SectionConfigData<T> {
fn default() -> Self {
Self {
sections: HashMap::new(),
order: Vec::new(),
}
}
}
impl<T: ApiSectionDataEntry + DeserializeOwned> TryFrom<RawSectionConfigData>
for SectionConfigData<T>
{
type Error = serde_json::Error;
fn try_from(data: RawSectionConfigData) -> Result<Self, serde_json::Error> {
let sections =
data.sections
.into_iter()
.try_fold(HashMap::new(), |mut acc, (id, (ty, value))| {
acc.insert(id, T::from_value(ty, value)?);
Ok::<_, serde_json::Error>(acc)
})?;
Ok(Self {
sections,
order: data.order,
})
}
}
impl<T> std::ops::Deref for SectionConfigData<T> {
type Target = HashMap<String, T>;
fn deref(&self) -> &Self::Target {
&self.sections
}
}
impl<T> std::ops::DerefMut for SectionConfigData<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sections
}
}
impl<T: Serialize + ApiSectionDataEntry> TryFrom<SectionConfigData<T>> for RawSectionConfigData {
type Error = serde_json::Error;
fn try_from(data: SectionConfigData<T>) -> Result<Self, serde_json::Error> {
let sections =
data.sections
.into_iter()
.try_fold(HashMap::new(), |mut acc, (id, value)| {
acc.insert(id, value.into_pair()?);
Ok::<_, serde_json::Error>(acc)
})?;
Ok(Self {
sections,
order: data.order,
})
}
}
impl<T: Serialize + ApiSectionDataEntry> TryFrom<&SectionConfigData<T>> for RawSectionConfigData {
type Error = serde_json::Error;
fn try_from(data: &SectionConfigData<T>) -> Result<Self, serde_json::Error> {
let sections = data
.sections
.iter()
.try_fold(HashMap::new(), |mut acc, (id, value)| {
acc.insert(id.clone(), value.to_pair()?);
Ok::<_, serde_json::Error>(acc)
})?;
Ok(Self {
sections,
order: data.order.clone(),
})
}
}
/// Creates an unordered data set.
impl<T: ApiSectionDataEntry> From<HashMap<String, T>> for SectionConfigData<T> {
fn from(sections: HashMap<String, T>) -> Self {
Self {
sections,
order: Vec::new(),
}
}
}
/// Creates a data set ordered the same way as the iterator.
impl<T: ApiSectionDataEntry> FromIterator<(String, T)> for SectionConfigData<T> {
fn from_iter<I: IntoIterator<Item = (String, T)>>(iter: I) -> Self {
let mut sections = HashMap::new();
let mut order = Vec::new();
for (key, value) in iter {
order.push(key.clone());
sections.insert(key, value);
}
Self { sections, order }
}
}
impl<T> IntoIterator for SectionConfigData<T> {
type IntoIter = IntoIter<T>;
type Item = (String, T);
fn into_iter(self) -> IntoIter<T> {
IntoIter {
sections: self.sections,
order: self.order.into_iter(),
}
}
}
/// Iterates over the sections in their original order.
pub struct IntoIter<T> {
sections: HashMap<String, T>,
order: std::vec::IntoIter<String>,
}
impl<T> Iterator for IntoIter<T> {
type Item = (String, T);
fn next(&mut self) -> Option<Self::Item> {
loop {
let id = self.order.next()?;
if let Some(data) = self.sections.remove(&id) {
return Some((id, data));
}
}
}
}
impl<'a, T> IntoIterator for &'a SectionConfigData<T> {
type IntoIter = Iter<'a, T>;
type Item = (&'a str, &'a T);
fn into_iter(self) -> Iter<'a, T> {
Iter {
sections: &self.sections,
order: self.order.iter(),
}
}
}
/// Iterates over the sections in their original order.
pub struct Iter<'a, T> {
sections: &'a HashMap<String, T>,
order: std::slice::Iter<'a, String>,
}
impl<'a, T> Iterator for Iter<'a, T> {
type Item = (&'a str, &'a T);
fn next(&mut self) -> Option<Self::Item> {
loop {
let id = self.order.next()?;
if let Some(data) = self.sections.get(id) {
return Some((id, data));
}
}
}
}
#[cfg(test)]
mod test {
use std::borrow::Cow;
use std::collections::HashMap;
use std::sync::OnceLock;
use proxmox_schema::{ApiStringFormat, EnumEntry, ObjectSchema, Schema, StringSchema};
use crate::{SectionConfig, SectionConfigPlugin};
use super::{ApiSectionDataEntry, SectionConfigData};
enum Ty {
A,
B,
}
struct Entry {
ty: Ty,
id: String,
value: String,
}
impl serde::Serialize for Entry {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::ser::Serializer,
{
use serde::ser::SerializeStruct;
let mut s = serializer.serialize_struct("Entry", 3)?;
s.serialize_field(
"ty",
match self.ty {
Ty::A => "a",
Ty::B => "b",
},
)?;
s.serialize_field("id", &self.id)?;
s.serialize_field("value", &self.value)?;
s.end()
}
}
impl<'de> serde::Deserialize<'de> for Entry {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
use serde::de::Error;
let mut data = HashMap::<Cow<str>, String>::deserialize(deserializer)?;
Ok(Entry {
ty: match data
.remove("ty")
.ok_or_else(|| D::Error::custom("missing 'ty'"))?
.as_ref()
{
"a" => Ty::A,
"b" => Ty::B,
other => return Err(D::Error::custom(format!("bad type '{other}'"))),
},
id: data
.remove("id")
.ok_or_else(|| D::Error::custom("missing 'id'"))?,
value: data
.remove("value")
.ok_or_else(|| D::Error::custom("missing 'value'"))?,
})
}
}
const TYPE_SCHEMA: Schema = StringSchema::new("Type.")
.format(&ApiStringFormat::Enum(&[
EnumEntry {
value: "a",
description: "A",
},
EnumEntry {
value: "b",
description: "B",
},
]))
.schema();
const PROPERTIES: ObjectSchema = ObjectSchema::new(
"Stuff",
&[
("id", false, &StringSchema::new("Some id.").schema()),
("ty", false, &TYPE_SCHEMA),
("value", false, &StringSchema::new("Some value.").schema()),
],
);
const ID_SCHEMA: Schema = StringSchema::new("ID schema.").min_length(3).schema();
impl ApiSectionDataEntry for Entry {
const INTERNALLY_TAGGED: Option<&'static str> = Some("ty");
const SECION_CONFIG_USES_TYPE_KEY: bool = true;
fn section_config() -> &'static SectionConfig {
static SC: OnceLock<SectionConfig> = OnceLock::new();
SC.get_or_init(|| {
let mut config = SectionConfig::new(&ID_SCHEMA).with_type_key("ty");
config.register_plugin(SectionConfigPlugin::new(
"a".to_string(),
Some("id".to_string()),
&PROPERTIES,
));
config.register_plugin(SectionConfigPlugin::new(
"b".to_string(),
Some("id".to_string()),
&PROPERTIES,
));
config
})
}
fn section_type(&self) -> &'static str {
match self.ty {
Ty::A => "a",
Ty::B => "a",
}
}
}
#[test]
fn test_type_key() {
let filename = "sync.cfg";
let raw = "\
a: first\n\
\tvalue 1\n\
\n\
b: second\n\
\tvalue 2\n\
";
let parsed = Entry::section_config()
.parse(filename, raw)
.expect("failed to parse");
let res: SectionConfigData<Entry> = parsed.try_into().expect("failed to convert");
let written = Entry::write_section_config(filename, &res)
.expect("failed to write out section config");
assert_eq!(written, raw);
}
}