ucifer 0.3.0

OpenWrt UCI Parser and Exporter
Documentation
/* Copyright © 2025 CZ.NIC z.s.p.o. (http://www.nic.cz/)
 *
 * This file is part of the ucifer library
 *
 * Ucifer is free software: you can redistribute it and/or modify it under
 * the terms of the GNU General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option)
 * any later version.
 *
 * Ucifer is distributed in the hope that it will be useful, but WITHOUT ANY
 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
 * details.
 *
 * For more information, see /LICENSE.txt
 */

//! Export [`Document`] into [`Directive`]

use {
	crate::{
		document::{ConfigOption, Document, SectionKey, SectionRecord},
		syntax::Directive,
	},
	alloc::{borrow::Cow, collections::BTreeMap, string::String, vec::Vec},
	hashbrown::HashMap,
};

/// An iterator yielding [`Directive`] of a file
#[must_use = "iterators are lazy and do nothing unless consumed"]
pub struct DocumentExport<'a> {
	/// Document to export, serves mostly for interning lookups
	doc: &'a Document,
	/// Iterator over all sections
	sections: <&'a BTreeMap<SectionKey, SectionRecord> as IntoIterator>::IntoIter,
	/// Iterator over all options in current section
	options: Option<<&'a HashMap<String, ConfigOption> as IntoIterator>::IntoIter>,
	/// Iterator over values in list of current option
	list: Option<ListExport<'a>>,
}

impl<'a> DocumentExport<'a> {
	/// Construct self from a UCI file
	pub(super) fn from_document(file: &'a Document) -> Self {
		Self {
			sections: file.sections.iter(),
			options: None,
			list: None,
			doc: file,
		}
	}

	/// Try to get next section and then set it's options iteraotr
	fn section(&mut self) -> Option<Directive<'a>> {
		let (key, value) = self.sections.next()?;
		let name = value.name.as_deref().map(Cow::Borrowed);
		let Some(type_name) = self.doc.type_name(key.type_key) else {
			unreachable!("state consistency violation: no type name of such key found in interner");
		};

		self.options = Some(value.section.options.iter());
		Some(Directive::Section {
			type_: Cow::Borrowed(type_name),
			name,
		})
	}

	/// Try to get next option
	///
	/// If option is list, sets an sub-iterator and proceeds to [`Self::list`]
	fn option(&mut self) -> Option<Directive<'a>> {
		let (key, option) = self.options.as_mut()?.next()?;
		match option {
			ConfigOption::Scalar(value) => Some(Directive::Option {
				key: Cow::Borrowed(key),
				value: Some(Cow::Borrowed(value)),
			}),
			ConfigOption::List(items) => {
				self.list = Some(ListExport {
					key,
					iter: items.iter(),
				});

				self.list()
			}
		}
	}

	/// Try to get next item of list option
	fn list(&mut self) -> Option<Directive<'a>> {
		self.list.as_mut()?.next()
	}
}

impl<'a> Iterator for DocumentExport<'a> {
	type Item = Directive<'a>;

	fn next(&mut self) -> Option<Self::Item> {
		self.list()
			.or_else(|| self.option())
			.or_else(|| self.section())
	}
}

/// Currently processed list entry
struct ListExport<'a> {
	/// Option key
	key: &'a str,
	/// Values
	iter: <&'a Vec<String> as IntoIterator>::IntoIter,
}

impl<'a> Iterator for ListExport<'a> {
	type Item = Directive<'a>;

	fn next(&mut self) -> Option<Self::Item> {
		let value = self.iter.next()?;
		Some(Directive::List {
			key: Cow::Borrowed(self.key),
			value: Cow::Borrowed(value),
		})
	}
}

#[cfg(test)]
mod tests {
	use {super::*, crate::document::Section, alloc::borrow::ToOwned};

	#[test]
	fn document_export() {
		let mut doc = Document::default();
		doc.merge_or_insert("cat", Some("emily"), {
			let mut section = Section::default();
			section.insert("foo", ConfigOption::Scalar("bar".to_owned()));
			section
		})
		.unwrap();

		let mut export = doc.export();
		assert_eq!(
			export.next(),
			Some(Directive::Section {
				type_: "cat".into(),
				name: Some("emily".into())
			})
		);

		assert_eq!(
			export.next(),
			Some(Directive::Option {
				key: "foo".into(),
				value: Some("bar".into())
			})
		);

		assert!(export.next().is_none());
	}
}