section-config, api-macro: add SectionConfig enum support
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
This commit is contained in:
parent
51d78fdd2b
commit
4d96aa52d2
@ -22,6 +22,7 @@ syn = { workspace = true , features = [ "extra-traits" ] }
|
||||
futures.workspace = true
|
||||
serde = { workspace = true, features = [ "derive" ] }
|
||||
serde_json.workspace = true
|
||||
proxmox-section-config.workspace = true
|
||||
|
||||
[dev-dependencies.proxmox-schema]
|
||||
workspace = true
|
||||
|
@ -4,13 +4,54 @@ use anyhow::Error;
|
||||
|
||||
use proc_macro2::{Ident, Span, TokenStream};
|
||||
use quote::quote_spanned;
|
||||
use syn::spanned::Spanned;
|
||||
|
||||
use super::Schema;
|
||||
use crate::serde;
|
||||
use crate::util::{self, FieldName, JSONObject, JSONValue, Maybe};
|
||||
|
||||
/// Enums, provided they're simple enums, simply get an enum string schema attached to them.
|
||||
pub fn handle_enum(
|
||||
pub fn handle_enum(attribs: JSONObject, enum_ty: syn::ItemEnum) -> Result<TokenStream, Error> {
|
||||
let mut first_unit = None;
|
||||
let mut first_unnamed = None;
|
||||
let mut first_named = None;
|
||||
for variant in &enum_ty.variants {
|
||||
match &variant.fields {
|
||||
syn::Fields::Unit => first_unit = Some(variant.fields.span()),
|
||||
syn::Fields::Unnamed(_) => first_unnamed = Some(variant.fields.span()),
|
||||
syn::Fields::Named(_) => first_named = Some(variant.fields.span()),
|
||||
}
|
||||
}
|
||||
|
||||
if first_unit.is_some() {
|
||||
if let Some(conflict) = first_unnamed.or(first_named) {
|
||||
bail!(
|
||||
conflict,
|
||||
"enums must be either with only unit types or only newtypes"
|
||||
);
|
||||
}
|
||||
return handle_string_enum(attribs, enum_ty);
|
||||
}
|
||||
|
||||
if first_unnamed.is_some() {
|
||||
if let Some(conflict) = first_unit.or(first_named) {
|
||||
bail!(
|
||||
conflict,
|
||||
"enums must be either with only unit types or only newtypes"
|
||||
);
|
||||
}
|
||||
return handle_section_config_enum(attribs, enum_ty);
|
||||
}
|
||||
|
||||
if let Some(bad) = first_named {
|
||||
bail!(bad, "api type enums with named fields are not allowed");
|
||||
}
|
||||
|
||||
bail!(enum_ty => "api type enums must not be empty");
|
||||
}
|
||||
|
||||
/// Enums, provided they're simple enums, simply get an enum string schema attached to them.
|
||||
fn handle_string_enum(
|
||||
mut attribs: JSONObject,
|
||||
mut enum_ty: syn::ItemEnum,
|
||||
) -> Result<TokenStream, Error> {
|
||||
@ -114,3 +155,127 @@ pub fn handle_enum(
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn handle_section_config_enum(
|
||||
mut attribs: JSONObject,
|
||||
enum_ty: syn::ItemEnum,
|
||||
) -> Result<TokenStream, Error> {
|
||||
let name = &enum_ty.ident;
|
||||
|
||||
let description: syn::LitStr = match attribs.remove("description") {
|
||||
Some(desc) => desc.try_into()?,
|
||||
None => {
|
||||
let (comment, span) = util::get_doc_comments(&enum_ty.attrs)?;
|
||||
syn::LitStr::new(comment.trim(), span)
|
||||
}
|
||||
};
|
||||
|
||||
let id_schema = {
|
||||
let schema: Schema = match attribs.remove("id-schema") {
|
||||
Some(schema) => schema.try_into()?,
|
||||
None => {
|
||||
bail!(name => "missing 'id-schema' property for SectionConfig style enum")
|
||||
}
|
||||
};
|
||||
|
||||
let mut ts = TokenStream::new();
|
||||
schema.to_typed_schema(&mut ts)?;
|
||||
ts
|
||||
};
|
||||
let id_property: syn::LitStr = match attribs.remove("id-property") {
|
||||
Some(name) => name.try_into()?,
|
||||
None => bail!(name => "missing 'id-property' property for SectionConfig style enum"),
|
||||
};
|
||||
|
||||
let container_attrs = serde::ContainerAttrib::try_from(&enum_ty.attrs[..])?;
|
||||
let Some(tag) = container_attrs.tag.as_ref() else {
|
||||
bail!(name => r#"SectionConfig enum needs a `#[serde(tag = "...")]` container attribute"#);
|
||||
};
|
||||
|
||||
let mut variants = TokenStream::new();
|
||||
let mut register_sections = TokenStream::new();
|
||||
let mut to_type = TokenStream::new();
|
||||
for variant in &enum_ty.variants {
|
||||
let field = match &variant.fields {
|
||||
syn::Fields::Unnamed(field) if field.unnamed.len() == 1 => &field.unnamed[0],
|
||||
_ => bail!(variant => "SectionConfig style enum can only have newtype variants"),
|
||||
};
|
||||
|
||||
let attrs = serde::VariantAttrib::try_from(&variant.attrs[..])?;
|
||||
let variant_string = if let Some(renamed) = attrs.rename {
|
||||
renamed
|
||||
} else if let Some(rename_all) = container_attrs.rename_all {
|
||||
let name = rename_all.apply_to_variant(&variant.ident.to_string());
|
||||
syn::LitStr::new(&name, variant.ident.span())
|
||||
} else {
|
||||
let name = &variant.ident;
|
||||
syn::LitStr::new(&name.to_string(), name.span())
|
||||
};
|
||||
|
||||
let variant_ident = &variant.ident;
|
||||
let ty = &field.ty;
|
||||
variants.extend(quote_spanned! { variant.ident.span() =>
|
||||
(
|
||||
#variant_string,
|
||||
&<#ty as ::proxmox_schema::ApiType>::API_SCHEMA,
|
||||
),
|
||||
});
|
||||
register_sections.extend(quote_spanned! { variant.ident.span() =>
|
||||
this.register_plugin(::proxmox_section_config::SectionConfigPlugin::new(
|
||||
#variant_string.to_string(),
|
||||
Some(#id_property.to_string()),
|
||||
const {
|
||||
match &<#ty as ::proxmox_schema::ApiType>::API_SCHEMA {
|
||||
::proxmox_schema::Schema::Object(schema) => schema,
|
||||
::proxmox_schema::Schema::OneOf(schema) => schema,
|
||||
_ => panic!("enum requires an object schema"),
|
||||
}
|
||||
}
|
||||
));
|
||||
});
|
||||
to_type.extend(quote_spanned! { variant.ident.span() =>
|
||||
Self::#variant_ident(_) => #variant_string,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(quote_spanned! { name.span() =>
|
||||
#enum_ty
|
||||
|
||||
impl ::proxmox_schema::ApiType for #name {
|
||||
const API_SCHEMA: ::proxmox_schema::Schema =
|
||||
::proxmox_schema::OneOfSchema::new(
|
||||
#description,
|
||||
&(#tag, false, &#id_schema.schema()),
|
||||
&[#variants],
|
||||
)
|
||||
.schema();
|
||||
}
|
||||
|
||||
impl ::proxmox_section_config::typed::ApiSectionDataEntry for #name {
|
||||
const INTERNALLY_TAGGED: Option<&'static str> = Some(#tag);
|
||||
|
||||
fn section_config() -> &'static ::proxmox_section_config::SectionConfig {
|
||||
static CONFIG: ::std::sync::OnceLock<::proxmox_section_config::SectionConfig> =
|
||||
::std::sync::OnceLock::new();
|
||||
|
||||
CONFIG.get_or_init(|| {
|
||||
let id_schema = const {
|
||||
<Self as ::proxmox_schema::ApiType>::API_SCHEMA
|
||||
.unwrap_one_of_schema()
|
||||
.type_property_entry
|
||||
.2
|
||||
};
|
||||
let mut this = ::proxmox_section_config::SectionConfig::new(id_schema);
|
||||
#register_sections
|
||||
this
|
||||
})
|
||||
}
|
||||
|
||||
fn section_type(&self) -> &'static str {
|
||||
match self {
|
||||
#to_type
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
@ -128,6 +128,7 @@ impl RenameAll {
|
||||
#[derive(Default)]
|
||||
pub struct ContainerAttrib {
|
||||
pub rename_all: Option<RenameAll>,
|
||||
pub tag: Option<syn::LitStr>,
|
||||
}
|
||||
|
||||
impl TryFrom<&[syn::Attribute]> for ContainerAttrib {
|
||||
@ -147,18 +148,28 @@ impl TryFrom<&[syn::Attribute]> for ContainerAttrib {
|
||||
|
||||
for arg in args {
|
||||
if let syn::Meta::NameValue(var) = arg {
|
||||
if !var.path.is_ident("rename_all") {
|
||||
continue;
|
||||
}
|
||||
match &var.value {
|
||||
syn::Expr::Lit(lit) => {
|
||||
let rename_all = RenameAll::try_from(&lit.lit)?;
|
||||
if this.rename_all.is_some() && this.rename_all != Some(rename_all) {
|
||||
error!(var.value => "multiple conflicting 'rename_all' attributes");
|
||||
if var.path.is_ident("rename_all") {
|
||||
match &var.value {
|
||||
syn::Expr::Lit(lit) => {
|
||||
let rename_all = RenameAll::try_from(&lit.lit)?;
|
||||
if this.rename_all.is_some() && this.rename_all != Some(rename_all)
|
||||
{
|
||||
error!(var.value => "multiple conflicting 'rename_all' attributes");
|
||||
}
|
||||
this.rename_all = Some(rename_all);
|
||||
}
|
||||
this.rename_all = Some(rename_all);
|
||||
_ => error!(var.value => "invalid 'rename_all' value type"),
|
||||
}
|
||||
} else if var.path.is_ident("tag") {
|
||||
match &var.value {
|
||||
syn::Expr::Lit(syn::ExprLit {
|
||||
lit: syn::Lit::Str(lit),
|
||||
..
|
||||
}) => {
|
||||
this.tag = Some(lit.clone());
|
||||
}
|
||||
_ => error!(var.value => "invalid 'tag' value type"),
|
||||
}
|
||||
_ => error!(var.value => "invalid 'rename_all' value type"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
75
proxmox-api-macro/tests/section-config.rs
Normal file
75
proxmox-api-macro/tests/section-config.rs
Normal file
@ -0,0 +1,75 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use proxmox_api_macro::api;
|
||||
use proxmox_section_config::typed::ApiSectionDataEntry;
|
||||
|
||||
#[api]
|
||||
/// Type A.
|
||||
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct TypeA {
|
||||
/// The id.
|
||||
id: String,
|
||||
|
||||
/// Some name.
|
||||
name: String,
|
||||
}
|
||||
|
||||
#[api]
|
||||
/// Type B.
|
||||
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
pub struct TypeB {
|
||||
/// The id.
|
||||
id: String,
|
||||
|
||||
/// An age.
|
||||
age: u64,
|
||||
}
|
||||
|
||||
#[api(
|
||||
"id-property": "id",
|
||||
"id-schema": {
|
||||
type: String,
|
||||
description: "A config ID",
|
||||
max_length: 16,
|
||||
},
|
||||
)]
|
||||
#[derive(Debug, Eq, PartialEq, Deserialize, Serialize)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum Config {
|
||||
A(TypeA),
|
||||
B(TypeB),
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config() {
|
||||
let content = "\
|
||||
A: the-a\n\
|
||||
\tname The Name\n\
|
||||
\n\
|
||||
B: the-b\n\
|
||||
\tage 42\n\
|
||||
";
|
||||
|
||||
let data = Config::parse_section_config("a_test_file.cfg", content)
|
||||
.expect("failed to parse test section config");
|
||||
|
||||
assert_eq!(data.len(), 2);
|
||||
assert_eq!(
|
||||
data["the-a"],
|
||||
Config::A(TypeA {
|
||||
id: "the-a".to_string(),
|
||||
name: "The Name".to_string(),
|
||||
})
|
||||
);
|
||||
assert_eq!(
|
||||
data["the-b"],
|
||||
Config::B(TypeB {
|
||||
id: "the-b".to_string(),
|
||||
age: 42,
|
||||
})
|
||||
);
|
||||
|
||||
let raw = Config::write_section_config("a_test_output_file.cfg", &data)
|
||||
.expect("failed to write out test section config");
|
||||
assert_eq!(raw, content);
|
||||
}
|
@ -33,6 +33,8 @@ use proxmox_lang::try_block;
|
||||
use proxmox_schema::format::{dump_properties, wrap_text, ParameterDisplayStyle};
|
||||
use proxmox_schema::*;
|
||||
|
||||
pub mod typed;
|
||||
|
||||
/// Used for additional properties when the schema allows them.
|
||||
const ADDITIONAL_PROPERTY_SCHEMA: Schema = StringSchema::new("Additional property").schema();
|
||||
|
||||
|
289
proxmox-section-config/src/typed.rs
Normal file
289
proxmox-section-config/src/typed.rs
Normal file
@ -0,0 +1,289 @@
|
||||
//! 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;
|
||||
|
||||
/// 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 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)
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
|
||||
/// 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>) -> 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")))?;
|
||||
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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user