databoard 0.4.0

Provides a hierarchical key-value-store
Documentation
// Copyright © 2025 Stephan Kunz
//! Implements [`databoard`][`RemappingList`] and helper functions handling the remapping rules.

use core::{
	ops::{Deref, DerefMut},
	str::FromStr,
};

use alloc::{
	string::{String, ToString},
	vec::Vec,
};

use crate::{Arc, ConstString, RemappingTarget};

use super::error::Error;

/// An immutable remapping entry.
type RemappingEntry = (ConstString, RemappingTarget);

/// A mutable remapping list.
///
/// The following rules between `key`and `value` are valid:
/// - `key`and `value` are literals.
/// - The `key`s may not start with the characters `@` and `_`, these are reserved.
/// - The `key`s may not contain any of the following characters: [`:`, `"`, `'`].
/// - A `value` **NOT** wrapped in brackets is a constant,
///   or a `value` containing any of the following characters: [`:`, `"`, `'`]
///   is a constant assignment e.g. `literal` or `{x: 1, y: 2}` or `{"value"}`.
///   It does not access a [`Databoard`](crate::databoard).
///   It is helpful in combination with types that implement the trait [`FromStr`](core::str::FromStr) to create a distinct value.
/// - A `value` wrapped in brackets is a `remapped_key` to a parent [`Databoard`](crate::databoard), e.g. `{remapped_key}`.
///  - A `remapped_key` starting with `@` is a redirection to the top level [`Databoard`](crate::databoard), e.g. `{@remapped_key}`.
///  - A `remapped_key` starting with `_` is a restriction to the current level [`Databoard`](crate::databoard), e.g. `{_remapped_key}`.
/// - The `value` `{=}` is a shortcut for the redirection with the same name as in `key`, e.g. `{=}`.
#[derive(Clone, Default)]
#[repr(transparent)]
pub struct RemappingList(Vec<RemappingEntry>);

impl core::fmt::Debug for RemappingList {
	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
		write!(f, "Remappings {{ ")?;
		write!(f, "{:?}", &self.0)?;
		write!(f, " }}")
	}
}

impl Deref for RemappingList {
	type Target = Vec<RemappingEntry>;

	fn deref(&self) -> &Self::Target {
		&self.0
	}
}

impl DerefMut for RemappingList {
	fn deref_mut(&mut self) -> &mut Self::Target {
		&mut self.0
	}
}

impl RemappingList {
	/// Adds an entry to the [`Remappings`] table.
	/// # Errors
	/// - [`Error::AlreadyRemapped`] if entry already exists
	/// - [`Error::CreateRemapping`] if target is not a valid [`RemappingTarget`]
	pub fn add(&mut self, key: ConstString, target: ConstString) -> Result<(), Error> {
		let remap_to = if target.as_ref() == "{=}" {
			(String::from("{") + &key + "}").into()
		} else {
			target
		};
		for (original, remapped) in &self.0 {
			if original == &key {
				return Err(Error::AlreadyRemapped {
					name: key,
					remapped: remapped.to_string().into(),
				});
			}
		}
		let target = RemappingTarget::from_str(&remap_to)?;
		self.0.push((key, target));
		Ok(())
	}

	pub(crate) fn add_target(&mut self, entry: RemappingEntry) -> Result<(), Error> {
		for (original, _remapped) in &self.0 {
			if original == &entry.0 {
				return Err(Error::AlreadyRemapped {
					name: entry.0,
					remapped: "todo!()".into(),
				});
			}
		}

		self.0.push(entry);
		Ok(())
	}

	/// Adds an entry to the [`Remappings`] table.
	/// Already existing values will be overwritten.
	/// # Errors
	/// - [`Error::CreateRemapping`] if target is not a valid [`RemappingTarget`]
	pub fn overwrite(&mut self, key: ConstString, target: ConstString) -> Result<(), Error> {
		let remap_to = if target.as_ref() == "{=}" {
			(String::from("{") + &key + "}").into()
		} else {
			target
		};
		let target = RemappingTarget::from_str(&remap_to)?;
		for (original, old_value) in &mut self.0 {
			if original == &key {
				// replace value
				*old_value = target;
				return Ok(());
			}
		}
		// create if not existent
		self.0.push((key, target));
		Ok(())
	}

	/// Returns the remapped value for `key`.
	#[must_use]
	pub fn find(&self, key: &str) -> RemappingTarget {
		for (original, remapped) in &self.0 {
			if original.as_ref() == key {
				return remapped.clone();
			}
		}
		RemappingTarget::None(key.into())
	}

	/// Optimize for size
	pub fn shrink(&mut self) {
		self.0.shrink_to_fit();
	}

	pub(crate) fn into_inner(self) -> Vec<RemappingEntry> {
		self.0
	}

	pub(crate) fn find_origin(&self, name: &str) -> Option<Arc<str>> {
		for (source_name, target) in &self.0 {
			match target {
				RemappingTarget::BoardPointer(target_name)
				| RemappingTarget::LocalPointer(target_name)
				| RemappingTarget::RootPointer(target_name) => {
					if name == target_name.as_ref() {
						return Some(source_name.clone());
					}
				}
				RemappingTarget::StringAssignment(_) | RemappingTarget::None(_) => {
					//@TODO: what needs to be done here?
				}
			}
		}
		None
	}
}

#[cfg(test)]
mod tests {
	#![allow(clippy::unwrap_used)]

	use super::*;

	// check, that the auto traits are available
	const fn is_normal<T: Sized + Send + Sync>() {}

	#[test]
	const fn normal_types() {
		is_normal::<RemappingList>();
		is_normal::<RemappingEntry>();
	}

	#[test]
	fn find_origin_local_pointer() {
		let mut r = RemappingList::default();
		r.push(("alias".into(), RemappingTarget::LocalPointer("real".into())));
		assert_eq!(r.find_origin("real").unwrap().as_ref(), "alias");
	}

	#[test]
	fn find_origin_root_pointer() {
		let mut r = RemappingList::default();
		r.push(("alias".into(), RemappingTarget::RootPointer("root_key".into())));
		assert_eq!(r.find_origin("root_key").unwrap().as_ref(), "alias");
	}

	#[test]
	fn find_origin_skips_string_assignment() {
		let mut r = RemappingList::default();
		r.push(("alias".into(), RemappingTarget::StringAssignment("constant".into())));
		assert!(r.find_origin("constant").is_none());
	}

	#[test]
	fn find_origin_skips_none() {
		let mut r = RemappingList::default();
		r.push(("alias".into(), RemappingTarget::None("whatever".into())));
		assert!(r.find_origin("whatever").is_none());
	}

	#[test]
	fn find_origin_local_pointer_no_match() {
		let mut r = RemappingList::default();
		r.push(("alias".into(), RemappingTarget::LocalPointer("real".into())));
		assert!(r.find_origin("other").is_none());
	}

	#[test]
	fn add_target_iterates_non_matching_entries() {
		let mut list = RemappingList::default();
		list.push(("existing".into(), RemappingTarget::BoardPointer("other".into())));
		// add_target with a different key must iterate past the existing entry
		// (exercising the false branch / closing `}` at the end of the loop body)
		list.add_target(("new_key".into(), RemappingTarget::BoardPointer("target".into())))
			.unwrap();
		assert_eq!(list.len(), 2);
	}
}