Skip to main content

ucifer/
document.rs

1/* Copyright © 2025 CZ.NIC z.s.p.o. (http://www.nic.cz/)
2 *
3 * This file is part of the ucifer library
4 *
5 * Ucifer is free software: you can redistribute it and/or modify it under
6 * the terms of the GNU General Public License as published by the Free
7 * Software Foundation, either version 3 of the License, or (at your option)
8 * any later version.
9 *
10 * Ucifer is distributed in the hope that it will be useful, but WITHOUT ANY
11 * WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12 * FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
13 * details.
14 *
15 * For more information, see /LICENSE.txt
16 */
17
18//! Represents model of the configuration in a structured manner.
19//!
20//! This model can be then used to perform lookup in.
21
22pub mod export;
23pub mod import;
24pub mod option;
25
26pub use option::ConfigOption;
27
28use {
29	crate::{document::export::DocumentExport, syntax::DirectiveIter},
30	alloc::{
31		boxed::Box,
32		collections::{
33			BTreeMap,
34			btree_map::{Range, RangeMut},
35		},
36		string::String,
37	},
38	core::{
39		any::type_name,
40		fmt::{Debug, Display},
41		hash::{BuildHasher, Hash},
42		ops::Bound,
43		str::FromStr,
44	},
45	derive_more::{Display, Error, From},
46	hashbrown::{HashMap, HashTable, hash_map::EntryRef},
47	import::Importer,
48	lasso::{Rodeo, Spur},
49};
50
51/// A configuration file
52///
53/// This contains sections indexed by type and ordinal and map for the keys.
54#[derive(Debug, Default)]
55pub struct Document {
56	/// Map of all sections
57	sections: BTreeMap<SectionKey, SectionRecord>,
58	/// Map of name to section key
59	names: HashTable<(u64, SectionKey)>,
60	/// Hash state for names
61	hash_state: hashbrown::DefaultHashBuilder,
62	/// Interned type names
63	types: Rodeo,
64}
65
66impl PartialEq for Document {
67	fn eq(&self, other: &Self) -> bool {
68		// We compare only sections as other stuff is implementation detail
69		self.sections == other.sections
70	}
71}
72
73impl Document {
74	/// Delete all entries in [`Document`], but keeping the allocations
75	pub fn clear(&mut self) {
76		self.sections.clear();
77		self.names.clear();
78		self.types.clear();
79	}
80
81	/// Insert a section, replacing the previous one if one with matching name
82	/// existed
83	///
84	/// If no name is provided, new section is always allocated.
85	///
86	/// **TIP:** If you need to pass [`None`] as `name`, try `None::<&str>`.
87	///
88	/// ```
89	/// # use ucifer::document::*;
90	/// let mut doc = Document::default();
91	/// doc.insert("foo", Some("bar"), Section::default());
92	/// ```
93	pub fn insert(
94		&mut self,
95		type_: &str,
96		name: Option<impl AsRef<str> + Into<Box<str>>>,
97		value: Section,
98	) -> (SectionKey, Option<Section>) {
99		let type_key = TypeSymbol(self.types.get_or_intern(type_));
100
101		let maybe_conflicting_key = name
102			.as_ref()
103			.and_then(|name| self.section_key_of_name(name.as_ref()))
104			.copied();
105
106		let key = maybe_conflicting_key.unwrap_or_else(|| SectionKey {
107			type_key,
108			index: self.free_index(type_key),
109		});
110
111		let previous_section = match self.sections.entry(key) {
112			alloc::collections::btree_map::Entry::Vacant(e) => {
113				if let Some(name) = &name {
114					let hash = self.hash_state.hash_one(name.as_ref());
115					self.names.insert_unique(hash, (hash, key), |(h, _)| *h);
116				}
117
118				e.insert(SectionRecord {
119					name: name.map(Into::into),
120					section: value,
121				});
122				None
123			}
124			alloc::collections::btree_map::Entry::Occupied(e) => {
125				Some(core::mem::replace(&mut e.into_mut().section, value))
126			}
127		};
128
129		(key, previous_section)
130	}
131
132	/// Insert a section in the file of specific type, merging two sections of
133	/// same type, if found.
134	///
135	/// # Errors
136	/// When same-named section exists but type differs
137	pub fn merge_or_insert(
138		&mut self,
139		type_: &str,
140		name: Option<impl AsRef<str> + Into<Box<str>>>,
141		value: Section,
142	) -> Result<SectionKey, MergeTypeMismatch> {
143		let maybe_key = name
144			.as_ref()
145			.and_then(|name| self.section_key_of_name(name.as_ref()));
146
147		// First, let's check if there is already a section so we can merge them
148		let Some(&key) = maybe_key else {
149			let (key, _) = self.insert(type_, name, value);
150			return Ok(key);
151		};
152
153		let type_key = TypeSymbol(self.types.get_or_intern(type_));
154		if type_key != key.type_key {
155			return Err(MergeTypeMismatch {
156				stored: key.type_key,
157				inserting: type_key,
158			});
159		}
160
161		let stored = self.get_mut(&key).unwrap_or_else(|| {
162			unreachable!(
163				"state consistency violation: we got name to a section which doesn't exist"
164			)
165		});
166
167		stored.merge(value);
168
169		Ok(key)
170	}
171
172	/// Lookup all sections matching a type
173	///
174	/// If no sections of such type are found, returns empty iterator.
175	///
176	/// ```
177	/// # use ucifer::document::*;
178	/// let mut doc = Document::default();
179	/// doc.insert("type", None::<&str>, Section::default());
180	/// doc.insert("type", Some("named"), Section::default());
181	///
182	/// for (key, section) in doc.lookup_by_type("type") {
183	///     // ...
184	/// }
185	/// ```
186	pub fn lookup_by_type<'this>(&'this self, type_: &str) -> LookupRange<'this> {
187		let Some(type_key) = self.type_key(type_) else {
188			return LookupRange::default();
189		};
190
191		LookupRange {
192			range: self.sections.range(lookup_range_by_type(type_key)),
193		}
194	}
195
196	/// Lookup all sections matching a type and get mutable references to them
197	///
198	/// If no sections of such type are found, returns empty iterator.
199	///
200	/// Refer to [`Self::lookup_by_type`]
201	///
202	/// ```
203	/// # use assert2::{assert, let_assert};
204	/// # use ucifer::document::*;
205	/// let mut doc = Document::default();
206	/// doc.insert("type", None::<&str>, Section::default());
207	/// doc.insert("type", Some("ほげら"), Section::default());
208	///
209	/// for (_key, section) in doc.lookup_by_type_mut("type") {
210	///     section.insert("křeček", ConfigOption::from("karel"));
211	/// }
212	///
213	/// # let_assert!(Some(section) = doc.lookup_by_type_name("type", "ほげら"));
214	/// # assert!(section.get("křeček") == Some(&ConfigOption::from("karel")));
215	/// ```
216	pub fn lookup_by_type_mut<'this>(&'this mut self, type_: &str) -> LookupRangeMut<'this> {
217		let Some(type_key) = self.type_key(type_) else {
218			return LookupRangeMut::default();
219		};
220
221		LookupRangeMut {
222			range: self.sections.range_mut(lookup_range_by_type(type_key)),
223		}
224	}
225
226	/// Lookup a section by it's name
227	///
228	/// ```
229	/// # use ucifer::document::*;
230	/// let mut doc = Document::default();
231	/// doc.insert("type", Some("name"), Section::default());
232	///
233	/// let lookup = doc.lookup_by_name("name");
234	/// # assert2::assert!(lookup.is_some())
235	/// ```
236	#[must_use]
237	pub fn lookup_by_name(&self, name: &str) -> Option<&Section> {
238		self.get(self.section_key_of_name(name)?)
239	}
240
241	/// Lookup a section by it's name and get a mutable refernece to
242	/// it
243	///
244	/// Refer to [`Self::lookup_by_name`]
245	///
246	/// ```
247	/// # use ucifer::document::*;
248	/// let mut doc = Document::default();
249	/// let mut section = Section::default();
250	/// section.insert("křeček", ConfigOption::from("karel"));
251	/// doc.insert("type", Some("name"), section);
252	///
253	/// if let Some(section) = doc.lookup_by_name_mut("name") {
254	///     # let removed =
255	///     section.remove("křeček");
256	///     # assert2::assert!(removed.is_some());
257	/// }
258	/// # else {
259	/// #     panic!("expected section");
260	/// # }
261	/// ```
262	pub fn lookup_by_name_mut(&mut self, name: &str) -> Option<&mut Section> {
263		let &key = self.section_key_of_name(name)?;
264		self.get_mut(&key)
265	}
266
267	/// Lookup a section by it's type *and* name
268	///
269	/// ```
270	/// # use assert2::assert;
271	/// # use ucifer::document::*;
272	/// let mut doc = Document::default();
273	/// doc.insert("kitty", Some("maria"), Section::default());
274	/// doc.insert("box", Some("dynAny"), Section::default());
275	///
276	/// let lookup = doc.lookup_by_type_name("box", "maria");
277	/// assert!(lookup.is_none());
278	///
279	/// let lookup = doc.lookup_by_type_name("kitty", "maria");
280	/// assert!(lookup.is_some());
281	/// ```
282	#[must_use]
283	pub fn lookup_by_type_name(&self, ty: &str, name: &str) -> Option<&Section> {
284		let key = self.section_key_of_name(name)?;
285		let type_key = self.type_key(ty)?;
286		(key.type_key == type_key).then(|| self.get(key)).flatten()
287	}
288
289	/// Lookup a section by it's type *and* name and get mutable reference to it
290	///
291	/// Refer to [`Self::lookup_by_type_name`]
292	/// ```
293	/// # use ucifer::document::*;
294	/// let mut doc = Document::default();
295	/// let mut section = Section::default();
296	/// section.insert("křeček", ConfigOption::from("karel"));
297	/// doc.insert("type", Some("name"), section);
298	///
299	/// if let Some(section) = doc.lookup_by_type_name_mut("type", "name") {
300	///     # let removed =
301	///     section.remove("křeček");
302	///     # assert2::assert!(removed.is_some());
303	/// }
304	/// # else {
305	/// #     panic!("expected section");
306	/// # }
307	/// ```
308	#[must_use]
309	pub fn lookup_by_type_name_mut(&mut self, ty: &str, name: &str) -> Option<&mut Section> {
310		let key = *self.section_key_of_name(name)?;
311		let type_key = self.type_key(ty)?;
312		(key.type_key == type_key)
313			.then(|| self.get_mut(&key))
314			.flatten()
315	}
316
317	/// Get reference to section by specified key
318	///
319	/// ```
320	/// # use ucifer::document::*;
321	/// # let mut document = Document::default();
322	/// # document.insert("mlem", None::<&str>, Section::default());
323	/// let lookup = document.lookup_by_type("mlem");
324	/// for (key, section) in lookup {
325	///     let got = document.get(key);
326	///     assert_eq!(got, Some(section));
327	/// }
328	/// ```
329	#[must_use]
330	pub fn get(&self, key: &SectionKey) -> Option<&Section> {
331		self.sections.get(key).map(|r| &r.section)
332	}
333
334	/// Get mutable reference to section by specified key
335	///
336	/// ```
337	/// # use ucifer::document::*;
338	/// # let mut document = Document::default();
339	/// # document.insert("mlem", None::<&str>, Section::default());
340	/// let mut lookup = document.lookup_by_type_mut("mlem");
341	/// let (&key, _) = lookup.next().unwrap();
342	/// document.get_mut(&key);
343	/// ```
344	#[must_use]
345	pub fn get_mut(&mut self, key: &SectionKey) -> Option<&mut Section> {
346		self.sections.get_mut(key).map(|r| &mut r.section)
347	}
348
349	/// Remove section by specified key, returning it
350	///
351	/// If it wasn't present, returns [`None`]
352	///
353	/// ```
354	/// # use assert2::assert;
355	/// # use ucifer::document::*;
356	/// let mut document = Document::default();
357	/// document.insert("mlem", Some("section"), Section::default());
358	///
359	/// let mut lookup = document.lookup_by_type("section");
360	/// if let Some((&key, _)) = lookup.next() {
361	///     document.remove(key);
362	///     assert!(document.lookup_by_type("section").next().is_none());
363	/// }
364	///
365	/// assert!(document.lookup_by_name("mlem").is_none());
366	/// ```
367	pub fn remove(&mut self, key: SectionKey) -> Option<Section> {
368		let record = self.sections.remove(&key)?;
369
370		if let Some(name) = record.name {
371			let hash = self.hash_state.hash_one(&name[..]);
372			let maybe_entry = self.names.find_entry(hash, |(got_hash, got_key)| {
373				got_hash == &hash && got_key == &key
374			});
375
376			if let Ok(entry) = maybe_entry {
377				entry.remove();
378			}
379		}
380
381		Some(record.section)
382	}
383
384	/// Get name of a section by it's specified key
385	#[must_use]
386	pub fn section_name(&self, key: SectionKey) -> Option<&str> {
387		self.sections.get(&key)?.name.as_deref()
388	}
389
390	/// Get section type key from the interner. Returns [`None`] if not found.
391	#[must_use]
392	pub fn type_key(&self, name: &str) -> Option<TypeSymbol> {
393		self.types.get(name).map(TypeSymbol)
394	}
395
396	/// Get section type name by it's key
397	#[must_use]
398	pub fn type_name(&self, key: TypeSymbol) -> Option<&str> {
399		self.types.try_resolve(&key.0)
400	}
401
402	/// Export entire document as iterator of directives
403	pub fn export(&self) -> DocumentExport<'_> {
404		DocumentExport::from_document(self)
405	}
406
407	/// Create importer to import directives to
408	pub const fn importer(&mut self) -> Importer<'_> {
409		Importer::new(self)
410	}
411
412	/// Last free index of type hash
413	fn free_index(&self, type_key: TypeSymbol) -> usize {
414		self.sections
415			.range(lookup_range_by_type(type_key))
416			.last()
417			.map_or(0, |(key, _)| key.index + 1)
418	}
419
420	/// Find section key of name
421	fn section_key_of_name(&self, name: &str) -> Option<&SectionKey> {
422		let (hash, eq) = section_name_lookup(&self.hash_state, name, &self.sections);
423		let (_, key) = self.names.find(hash, eq)?;
424
425		Some(key)
426	}
427}
428
429/// Convenience implementation for serializing document to it's string
430/// representation
431impl Display for Document {
432	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
433		self.export()
434			.try_for_each(|directive| Display::fmt(&directive, f))
435	}
436}
437
438/// Convenience implementation for reading entire document from string
439impl FromStr for Document {
440	type Err = FromStrError;
441
442	fn from_str(s: &str) -> Result<Self, Self::Err> {
443		let mut document = Self::default();
444		let mut importer = document.importer();
445
446		for result in DirectiveIter::new(s) {
447			importer.import(result?)?;
448		}
449
450		Ok(document)
451	}
452}
453
454/// Error returned by [`Document`] as [`FromStr`]
455#[derive(Clone, Debug, Display, Error, From, PartialEq, Eq)]
456pub enum FromStrError {
457	/// Syntax Error
458	///
459	/// Document couldn't be parsed into directives
460	Syntax(crate::syntax::ParseError),
461	/// Import Error
462	///
463	/// Document is syntactically correct but directives are arranged in invalid
464	/// way
465	Import(import::ImportError),
466}
467
468/// There was a type mismatch when attempting to merge two sections
469#[derive(Clone, Copy, Display, Debug, PartialEq, Eq, Error)]
470#[display("merge type mismatch, stored: {stored:?} but we are inserting {inserting:?}")]
471pub struct MergeTypeMismatch {
472	/// Type of section present in the document
473	pub stored: TypeSymbol,
474	/// Type of section attempted to be inserted
475	pub inserting: TypeSymbol,
476}
477
478/// Configuration section
479///
480/// Delimited by `config` in the doc.
481#[derive(Debug, Default, PartialEq, Eq)]
482pub struct Section {
483	/// Configuration options
484	options: HashMap<String, ConfigOption>,
485}
486
487impl Section {
488	/// Get option / list reference in section by key
489	#[inline]
490	#[must_use]
491	pub fn get(&self, key: &str) -> Option<&ConfigOption> {
492		self.options.get(key)
493	}
494
495	/// Get option / list mutable reference in section by key
496	#[inline]
497	#[must_use]
498	pub fn get_mut(&mut self, key: &str) -> Option<&mut ConfigOption> {
499		self.options.get_mut(key)
500	}
501
502	/// Insert option / list into the section
503	///
504	/// If there was an option in the section with this key already present,
505	/// inserts it in the option as list.
506	pub fn insert(&mut self, key: &str, option: ConfigOption) {
507		match self.options.entry_ref(key) {
508			EntryRef::Occupied(entry) => {
509				entry.replace_entry_with(|_, current| {
510					Some(match (current, option) {
511						(_, new @ ConfigOption::Scalar(_)) => new,
512						(ConfigOption::Scalar(current), ConfigOption::List(mut items)) => {
513							items.insert(0, current);
514							ConfigOption::List(items)
515						}
516						(ConfigOption::List(mut current), ConfigOption::List(new)) => {
517							current.extend_from_slice(&new);
518							ConfigOption::List(current)
519						}
520					})
521				});
522			}
523			EntryRef::Vacant(entry) => {
524				entry.insert(option);
525			}
526		}
527	}
528
529	/// Merge from another section
530	///
531	/// Performs [`Self::insert`] on every entry
532	pub fn merge(&mut self, other: Self) {
533		for (key, value) in other.options {
534			self.insert(&key, value);
535		}
536	}
537
538	/// Remove entry from section
539	///
540	/// If it wasn't present, returns [`None`]
541	pub fn remove(&mut self, key: &str) -> Option<ConfigOption> {
542		self.options.remove(key)
543	}
544
545	/// Create borrowing iterator from section
546	#[inline]
547	#[must_use]
548	pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
549		<&Self as IntoIterator>::into_iter(self)
550	}
551}
552
553impl IntoIterator for Section {
554	type Item = (String, ConfigOption);
555	type IntoIter = hashbrown::hash_map::IntoIter<String, ConfigOption>;
556
557	fn into_iter(self) -> Self::IntoIter {
558		self.options.into_iter()
559	}
560}
561
562impl<'a> IntoIterator for &'a Section {
563	type Item = (&'a String, &'a ConfigOption);
564	type IntoIter = hashbrown::hash_map::Iter<'a, String, ConfigOption>;
565
566	fn into_iter(self) -> Self::IntoIter {
567		self.options.iter()
568	}
569}
570
571impl<K: AsRef<str>> Extend<(K, ConfigOption)> for Section {
572	fn extend<T: IntoIterator<Item = (K, ConfigOption)>>(&mut self, iter: T) {
573		for (key, value) in iter {
574			self.insert(key.as_ref(), value);
575		}
576	}
577}
578
579/// Key to a section. As sections are stored in [`BTreeMap`] this contains a
580/// hash of section type name and its index.
581#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
582pub struct SectionKey {
583	/// Section type key
584	type_key: TypeSymbol,
585	/// Section index unique for the type
586	index: usize,
587}
588
589impl Debug for SectionKey {
590	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
591		f.debug_struct(type_name::<Self>())
592			.field("type_key", &format_args!("{:#x?}", self.type_key))
593			.field("index", &self.index)
594			.finish()
595	}
596}
597
598/// Section record in the map
599///
600/// As this may contain a name and we can get mutable references to sections
601/// from the document, meddling with seciton's name will cause consistency
602/// violation, so private type is made instead of putting name into [`Section`].
603#[derive(Debug, Default, PartialEq, Eq)]
604struct SectionRecord {
605	/// Maybe name of the section
606	name: Option<Box<str>>,
607	/// Section itself
608	section: Section,
609}
610
611/// Symbolized type
612///
613/// Can be resolved into name using [`Document::type_name`]
614#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
615#[repr(transparent)]
616pub struct TypeSymbol(Spur);
617
618/// Iterator over document sub-range
619#[derive(Debug, Default)]
620#[must_use]
621pub struct LookupRange<'doc> {
622	/// Underlying range we map over
623	range: Range<'doc, SectionKey, SectionRecord>,
624}
625
626impl<'doc> Iterator for LookupRange<'doc> {
627	type Item = (&'doc SectionKey, &'doc Section);
628
629	fn next(&mut self) -> Option<Self::Item> {
630		let (key, value) = self.range.next()?;
631		Some((key, &value.section))
632	}
633}
634
635impl DoubleEndedIterator for LookupRange<'_> {
636	fn next_back(&mut self) -> Option<Self::Item> {
637		let (key, value) = self.range.next_back()?;
638		Some((key, &value.section))
639	}
640}
641
642/// Mutable iterator over document sub-range
643#[derive(Debug, Default)]
644#[must_use]
645pub struct LookupRangeMut<'doc> {
646	/// Underlying range we map over
647	range: RangeMut<'doc, SectionKey, SectionRecord>,
648}
649
650impl<'doc> Iterator for LookupRangeMut<'doc> {
651	type Item = (&'doc SectionKey, &'doc mut Section);
652
653	fn next(&mut self) -> Option<Self::Item> {
654		let (key, value) = self.range.next()?;
655		Some((key, &mut value.section))
656	}
657}
658
659impl DoubleEndedIterator for LookupRangeMut<'_> {
660	fn next_back(&mut self) -> Option<Self::Item> {
661		let (key, value) = self.range.next_back()?;
662		Some((key, &mut value.section))
663	}
664}
665
666/// Create a range for lookup of all entires of type
667const fn lookup_range_by_type(type_key: TypeSymbol) -> (Bound<SectionKey>, Bound<SectionKey>) {
668	let from = SectionKey { type_key, index: 0 };
669	let to = SectionKey {
670		type_key,
671		index: usize::MAX,
672	};
673
674	(Bound::Included(from), Bound::Included(to))
675}
676
677/// Get all stuff necessary to look up things in name table
678///
679/// Returns hash and equivalence predicate
680fn section_name_lookup(
681	hash_state: &impl BuildHasher,
682	name: &str,
683	sections: &BTreeMap<SectionKey, SectionRecord>,
684) -> (u64, impl Fn(&(u64, SectionKey)) -> bool) {
685	let hash = hash_state.hash_one(name);
686	let pred = move |(got_hash, key): &_| {
687		let name_matches =
688			|| sections.get(key).and_then(|record| record.name.as_deref()) == Some(name);
689
690		*got_hash == hash && name_matches()
691	};
692
693	(hash, pred)
694}
695
696#[cfg(test)]
697mod tests {
698	use {
699		super::*,
700		crate::syntax::Directive,
701		alloc::string::ToString,
702		assert2::{assert, let_assert},
703	};
704
705	#[test]
706	fn import_export() {
707		let source = [
708			Directive::Section {
709				type_: "kitty".into(),
710				name: Some("emily".into()),
711			},
712			Directive::Option {
713				key: "foo".into(),
714				value: Some("bar".into()),
715			},
716			Directive::Section {
717				type_: "kitty".into(),
718				name: None,
719			},
720			Directive::List {
721				key: "list".into(),
722				value: ":".into(),
723			},
724			Directive::List {
725				key: "list".into(),
726				value: "3".into(),
727			},
728		];
729
730		let mut doc = Document::default();
731		let mut importer = doc.importer();
732		source
733			.iter()
734			.cloned()
735			.try_for_each(|dir| importer.import(dir))
736			.unwrap();
737
738		let exported: alloc::vec::Vec<_> = doc.export().collect();
739		assert!(exported == source);
740	}
741
742	#[test]
743	fn document_reuse() {
744		let mut doc = Document::default();
745		let mut section = Section::default();
746		section.insert("A", ConfigOption::from("value"));
747		doc.insert("section", Some(":3"), section);
748
749		let_assert!(Some(key) = doc.lookup_by_type_name("section", ":3"));
750		assert!(key.get("A").is_some());
751
752		doc.clear();
753		assert!(doc.lookup_by_type_name("section", ":3").is_none());
754	}
755
756	#[test]
757	fn parse_fmt() {
758		let source = r#"
759			config "A" "B"
760				option "c" "d"
761				list   "e" "f"
762		"#;
763
764		let document = Document::from_str(source).unwrap();
765		let from_fmt = Document::from_str(&document.to_string()).unwrap();
766
767		assert!(document == from_fmt);
768	}
769}