databoard/
remappings.rs

1// Copyright © 2025 Stephan Kunz
2//! Implements [`databoard`][`Remappings`] and helper functions handling the remapping rules.
3
4use super::error::{Error, Result};
5use crate::ConstString;
6use alloc::{borrow::ToOwned, string::String, vec::Vec};
7use core::ops::{Deref, DerefMut};
8
9// region:		--- helpers
10/// Returns `true` if a key is a valid databoard key, otherwise `false`
11#[must_use]
12fn is_valid_db_key(key: &str) -> bool {
13	!key.contains('"') && !key.contains('\'') && !key.contains(':') && !key.contains('{') && !key.contains('}')
14}
15
16/// Returns `true` if a key is not a board pointer but a constant assignment , otherwise `false`
17#[must_use]
18pub fn is_const_assignment(key: &str) -> bool {
19	!key.starts_with('{') && !key.ends_with('}') || key.contains('"') || key.contains(':') || key.contains('\'')
20}
21
22/// Checks whether the given key is a pointer into a [`Databoard`](crate::databoard).
23#[must_use]
24pub fn is_board_pointer(key: &str) -> bool {
25	key.starts_with('{') && key.ends_with('}') && is_valid_db_key(&key[1..key.len() - 1])
26}
27
28/// Returns Some(literal) of the [`Databoard`](crate::databoard) pointer if it is one, otherwise `None`.
29#[must_use]
30pub fn strip_board_pointer(key: &str) -> Option<&str> {
31	if is_board_pointer(key) {
32		Some(&key[1..key.len() - 1])
33	} else {
34		None
35	}
36}
37
38/// Returns the literal of the [`Databoard`](crate::databoard) pointer if it is one.
39/// # Errors
40/// - if is not a [`Databoard`](crate::databoard) pointer, the error contains the unchanged key.
41pub fn check_board_pointer(key: &str) -> core::result::Result<&str, &str> {
42	if is_board_pointer(key) {
43		Ok(&key[1..key.len() - 1])
44	} else {
45		Err(key)
46	}
47}
48
49/// Returns the literal of the current/local [`Databoard`](crate::databoard) key if it is one.
50/// # Errors
51/// - if is not a current/local [`Databoard`](crate::databoard) `key`, the error contains the unchanged `key`.
52pub fn check_local_key(key: &str) -> core::result::Result<&str, &str> {
53	if key.starts_with('_') && is_valid_db_key(&key[1..]) {
54		Ok(&key[1..])
55	} else {
56		Err(key)
57	}
58}
59
60/// Checks whether the given key is a pointer into current/local [`Databoard`](crate::databoard).
61#[must_use]
62pub fn is_local_pointer(key: &str) -> bool {
63	key.starts_with("{_") && key.ends_with('}') && is_valid_db_key(&key[2..key.len() - 1])
64}
65
66/// Returns Some(literal) of the current/local [`Databoard`](crate::databoard) pointer if it is one, otherwise `None`.
67/// The leading `_` is removed from the literal.
68#[must_use]
69pub fn strip_local_pointer(key: &str) -> Option<&str> {
70	if is_local_pointer(key) {
71		Some(&key[2..key.len() - 1])
72	} else {
73		None
74	}
75}
76
77/// Returns the literal of the current/local [`Databoard`](crate::databoard) pointer if it is one.
78/// # Errors
79/// - if is not a current/local [`Databoard`](crate::databoard) pointer, the error contains the unchanged key.
80pub fn check_local_pointer(key: &str) -> core::result::Result<&str, &str> {
81	if is_local_pointer(key) {
82		Ok(&key[2..key.len() - 1])
83	} else {
84		Err(key)
85	}
86}
87
88/// Returns the literal of the top level [`Databoard`](crate::databoard) key if it is one.
89/// # Errors
90/// - if is not a top level [`Databoard`](crate::databoard) `key`, the error contains the unchanged `key`.
91pub fn check_top_level_key(key: &str) -> core::result::Result<&str, &str> {
92	if key.starts_with('@') && is_valid_db_key(&key[1..]) {
93		Ok(&key[1..])
94	} else {
95		Err(key)
96	}
97}
98
99/// Checks whether the given key is a pointer into top level [`Databoard`](crate::databoard).
100#[must_use]
101pub fn is_top_level_pointer(key: &str) -> bool {
102	key.starts_with("{@") && key.ends_with('}') && is_valid_db_key(&key[2..key.len() - 1])
103}
104
105/// Returns Some(literal) of the top level [`Databoard`](crate::databoard) pointer if it is one, otherwise `None`.
106/// The leading `@` is removed from the literal.
107#[must_use]
108pub fn strip_top_level_pointer(key: &str) -> Option<&str> {
109	if is_top_level_pointer(key) {
110		Some(&key[2..key.len() - 1])
111	} else {
112		None
113	}
114}
115
116/// Returns the literal of the top level [`Databoard`](crate::databoard) pointer if it is one.
117/// # Errors
118/// - if is not a top level [`Databoard`](crate::databoard) pointer, the error contains the unchanged pointer.
119pub fn check_top_level_pointer(key: &str) -> core::result::Result<&str, &str> {
120	if is_top_level_pointer(key) {
121		Ok(&key[2..key.len() - 1])
122	} else {
123		Err(key)
124	}
125}
126// endregion:	--- helpers
127
128// region:		--- remappings
129/// An immutable remapping entry.
130type RemappingEntry = (ConstString, ConstString);
131
132/// A mutable remapping list.
133///
134/// The following rules between `key`and `value` are valid:
135/// - `key`and `value` are literals.
136/// - The `key`s may not start with the characters `@` and `_`, these are reserved.
137/// - The `key`s may not contain any of the following characters: [`:`, `"`, `'`].
138/// - A `value` **NOT** wrapped in brackets is a constant,
139///   or a `value` containing any of the following characters: [`:`, `"`, `'`]
140///   is a constant assignment e.g. `literal` or `{x: 1, y: 2}` or `{"value"}`.
141///   It does not access a [`Databoard`](crate::databoard).
142///   It is helpful in combination with types that implement the trait [`FromStr`](core::str::FromStr) to create a distinct value.
143/// - A `value` wrapped in brackets is a `remapped_key` to a parent [`Databoard`](crate::databoard), e.g. `{remapped_key}`.
144///  - A `remapped_key` starting with `@` is a redirection to the top level [`Databoard`](crate::databoard), e.g. `{@remapped_key}`.
145///  - A `remapped_key` starting with `_` is a restriction to the current level [`Databoard`](crate::databoard), e.g. `{_remapped_key}`.
146/// - The `value` `{=}` is a shortcut for the redirection with the same name as in `key`, e.g. `{=}`.
147#[derive(Clone, Default)]
148#[repr(transparent)]
149pub struct Remappings(Vec<RemappingEntry>);
150
151impl core::fmt::Debug for Remappings {
152	fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
153		write!(f, "Remappings {{ ")?;
154		write!(f, "{:?}", &self.0)?;
155		write!(f, " }}")
156	}
157}
158
159impl Deref for Remappings {
160	type Target = Vec<RemappingEntry>;
161
162	fn deref(&self) -> &Self::Target {
163		&self.0
164	}
165}
166
167impl DerefMut for Remappings {
168	fn deref_mut(&mut self) -> &mut Self::Target {
169		&mut self.0
170	}
171}
172
173impl Remappings {
174	/// Adds an entry to the [`Remappings`] table.
175	/// # Errors
176	/// - [`Error::AlreadyRemapped`] if entry already exists
177	pub fn add(&mut self, key: impl Into<ConstString>, remap_to: impl Into<ConstString>) -> Result<()> {
178		let key = key.into();
179		for (original, remapped) in &self.0 {
180			if original == &key {
181				return Err(Error::AlreadyRemapped {
182					key,
183					remapped: remapped.to_owned(),
184				});
185			}
186		}
187		self.0.push((key, remap_to.into()));
188		Ok(())
189	}
190
191	/// Adds an entry to the [`Remappings`] table.
192	/// Already existing values will be overwritten.
193	pub fn overwrite(&mut self, key: &str, remapped: impl Into<ConstString>) {
194		for (original, old_value) in &mut self.0 {
195			if original.as_ref() == key {
196				// replace value
197				*old_value = remapped.into();
198				return;
199			}
200		}
201		// create if not existent
202		self.0.push((key.into(), remapped.into()));
203	}
204
205	/// Returns the remapped value for `key`, if there is a remapping, otherwise `None`.
206	#[must_use]
207	pub fn find(&self, key: &str) -> Option<ConstString> {
208		for (original, remapped) in &self.0 {
209			if original.as_ref() == key {
210				// is the shortcut '{=}' used?
211				return if remapped.as_ref() == "{=}" {
212					Some((String::from("{") + key + "}").into())
213				} else {
214					Some(remapped.clone())
215				};
216			}
217		}
218		None
219	}
220
221	/// Returns the remapped value for `key` if there is one, otherwise the original `key`.
222	#[must_use]
223	pub fn remap(&self, name: &str) -> ConstString {
224		for (original, remapped) in &self.0 {
225			if original.as_ref() == name {
226				// is the shortcut '{=}' used?
227				return if remapped.as_ref() == "{=}" {
228					name.into()
229				} else {
230					remapped.clone()
231				};
232			}
233		}
234		name.into()
235	}
236
237	/// Optimize for size
238	pub fn shrink(&mut self) {
239		self.0.shrink_to_fit();
240	}
241}
242// endregion:	--- remappings
243
244#[cfg(test)]
245mod tests {
246	use super::*;
247
248	// check, that the auto traits are available
249	const fn is_normal<T: Sized + Send + Sync>() {}
250
251	#[test]
252	const fn normal_types() {
253		is_normal::<Remappings>();
254		is_normal::<RemappingEntry>();
255	}
256}