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
 */

//! Import [`Directive`]s to [`Document`]

use {
	crate::{
		document::{Document, MergeTypeMismatch, Section, SectionKey, option::ConfigOption},
		syntax::Directive,
	},
	alloc::{string::String, vec::Vec},
	core::mem::take,
	derive_more::{Display, Error, From},
	hashbrown::{DefaultHashBuilder, hash_map::EntryRef},
};

// todo: do not index all the time to section?

/// Holds state to import [`Directive`]s into a [`Document`]
pub struct Importer<'doc> {
	/// Document to import to
	doc: &'doc mut Document,
	/// Current selection key
	///
	/// If [`None`], inserting `option` or `list` will fail
	current_section: Option<SectionKey>,
}

impl<'doc> Importer<'doc> {
	/// Construct a new importer
	pub(super) const fn new(doc: &'doc mut Document) -> Self {
		Self {
			doc,
			current_section: None,
		}
	}

	/// Import by directive
	///
	/// # Errors
	/// - Returns [`ImportError::OutsideSection`] when attempted to import
	///   [`Directive::Option`] or [`Directive::List`]
	///
	/// # Example
	/// ```
	/// # use ucifer::{document::{Document, import::ImportError}, syntax::Directive};
	/// #
	/// # fn main() -> Result<(), ImportError> {
	/// let mut doc = Document::default();
	/// let mut importer = doc.importer();
	///
	/// let type_ = "type".into();
	/// importer.import(Directive::Section { type_, name: None })?;
	///
	/// let section = doc.lookup_by_type("type").next();
	/// # assert!(section.is_some());
	/// # Ok(())
	/// # }
	/// ```
	pub fn import<'d>(&mut self, directive: Directive<'d>) -> Result<(), ImportError>
	where
		'doc: 'd,
	{
		match directive {
			Directive::Package(_) => (),
			Directive::Section { type_, name } => {
				let key = self
					.doc
					.merge_or_insert(&type_, name.as_deref(), Section::default())?;

				self.current_section = Some(key);
			}
			Directive::Option { key, value } => {
				if let Some(value) = value {
					let value = value.into_owned();
					self.option_entry(&key)?.insert(ConfigOption::Scalar(value));
				}
			}
			Directive::List { key, value } => {
				let entry = self
					.option_entry(&key)?
					.or_insert(ConfigOption::List(Vec::new()));

				let value = value.into_owned();
				match entry {
					ConfigOption::List(list) => list.push(value),
					ConfigOption::Scalar(current) => {
						let current = take(current);
						*entry = ConfigOption::List(alloc::vec![current, value]);
					}
				}
			}
		}

		Ok(())
	}

	/// Get an entry for option (it being scalar or list) in current section by
	/// name
	fn option_entry<'key>(
		&mut self,
		option_key: &'key str,
	) -> Result<EntryRef<'_, 'key, String, str, ConfigOption, DefaultHashBuilder>, ImportError> {
		let section_key = self.current_section.ok_or(ImportError::OutsideSection)?;
		let section = self
			.doc
			.get_mut(&section_key)
			.expect("section has to exist as we just used its key");

		Ok(section.options.entry_ref(option_key))
	}
}

/// Error on importing
///
/// Currently describes only option outside a section
#[derive(Clone, Copy, Display, Debug, PartialEq, Eq, Error, From)]
#[non_exhaustive]
pub enum ImportError {
	/// Returned if tried to set an option entry with no section entered
	#[display("found `option` or `list` directive outside section")]
	OutsideSection,
	/// Attempted to merge two sections but their types differ
	MergeTypeMismatch(MergeTypeMismatch),
}