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>
This commit is contained in:
Wolfgang Bumiller 2024-08-13 14:09:07 +02:00
parent 88af4ca549
commit 61d2eb891e

View File

@ -15,6 +15,13 @@ pub trait ApiSectionDataEntry: Sized {
/// 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;
@ -26,7 +33,9 @@ pub trait ApiSectionDataEntry: Sized {
where
Self: serde::de::DeserializeOwned,
{
if let Some(tag) = Self::INTERNALLY_TAGGED {
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());
@ -51,7 +60,11 @@ pub trait ApiSectionDataEntry: Sized {
where
Self: Serialize,
{
to_pair(serde_json::to_value(self)?, Self::INTERNALLY_TAGGED)
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)`.
@ -61,7 +74,11 @@ pub trait ApiSectionDataEntry: Sized {
where
Self: Serialize,
{
to_pair(serde_json::to_value(self)?, Self::INTERNALLY_TAGGED)
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()`.
@ -90,14 +107,23 @@ pub trait ApiSectionDataEntry: Sized {
/// the type being the key.
///
/// Otherwise this will fail.
fn to_pair(value: Value, tag: Option<&'static str>) -> Result<(String, Value), serde_json::Error> {
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 = obj
.remove(tag)
.ok_or_else(|| Error::custom(format!("tag {tag:?} missing in object")))?;
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!(
@ -287,3 +313,153 @@ impl<'a, T> Iterator for Iter<'a, T> {
}
}
}
#[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);
}
}