forked from Proxmox/proxmox
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:
parent
88af4ca549
commit
61d2eb891e
@ -15,6 +15,13 @@ pub trait ApiSectionDataEntry: Sized {
|
|||||||
/// property.
|
/// property.
|
||||||
const INTERNALLY_TAGGED: Option<&'static str> = None;
|
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.
|
/// Get the `SectionConfig` configuration for this enum.
|
||||||
fn section_config() -> &'static SectionConfig;
|
fn section_config() -> &'static SectionConfig;
|
||||||
|
|
||||||
@ -26,7 +33,9 @@ pub trait ApiSectionDataEntry: Sized {
|
|||||||
where
|
where
|
||||||
Self: serde::de::DeserializeOwned,
|
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 {
|
match &mut value {
|
||||||
Value::Object(obj) => {
|
Value::Object(obj) => {
|
||||||
obj.insert(tag.to_string(), ty.into());
|
obj.insert(tag.to_string(), ty.into());
|
||||||
@ -51,7 +60,11 @@ pub trait ApiSectionDataEntry: Sized {
|
|||||||
where
|
where
|
||||||
Self: Serialize,
|
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)`.
|
/// Turn this entry into a pair of `(type, value)`.
|
||||||
@ -61,7 +74,11 @@ pub trait ApiSectionDataEntry: Sized {
|
|||||||
where
|
where
|
||||||
Self: Serialize,
|
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()`.
|
/// Provided. Shortcut for `Self::section_config().parse(filename, data)?.try_into()`.
|
||||||
@ -90,14 +107,23 @@ pub trait ApiSectionDataEntry: Sized {
|
|||||||
/// the type being the key.
|
/// the type being the key.
|
||||||
///
|
///
|
||||||
/// Otherwise this will fail.
|
/// 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;
|
use serde::ser::Error;
|
||||||
|
|
||||||
match (value, tag) {
|
match (value, tag) {
|
||||||
(Value::Object(mut obj), Some(tag)) => {
|
(Value::Object(mut obj), Some(tag)) => {
|
||||||
let id = obj
|
let id = if strip_tag {
|
||||||
.remove(tag)
|
obj.remove(tag)
|
||||||
.ok_or_else(|| Error::custom(format!("tag {tag:?} missing in object")))?;
|
.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 {
|
match id {
|
||||||
Value::String(id) => Ok((id, Value::Object(obj))),
|
Value::String(id) => Ok((id, Value::Object(obj))),
|
||||||
_ => Err(Error::custom(format!(
|
_ => 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user