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, Debug, Default)]
148#[repr(transparent)]
149pub struct Remappings(Vec<RemappingEntry>);
150
151impl Deref for Remappings {
152	type Target = Vec<RemappingEntry>;
153
154	fn deref(&self) -> &Self::Target {
155		&self.0
156	}
157}
158
159impl DerefMut for Remappings {
160	fn deref_mut(&mut self) -> &mut Self::Target {
161		&mut self.0
162	}
163}
164
165impl Remappings {
166	/// Adds an entry to the [`Remappings`] table.
167	/// # Errors
168	/// - [`Error::AlreadyRemapped`] if entry already exists
169	pub fn add(&mut self, key: impl Into<ConstString>, remap_to: impl Into<ConstString>) -> Result<()> {
170		let key = key.into();
171		for (original, remapped) in &self.0 {
172			if original == &key {
173				return Err(Error::AlreadyRemapped {
174					key,
175					remapped: remapped.to_owned(),
176				});
177			}
178		}
179		self.0.push((key, remap_to.into()));
180		Ok(())
181	}
182
183	/// Adds an entry to the [`Remappings`] table.
184	/// Already existing values will be overwritten.
185	pub fn overwrite(&mut self, key: &str, remapped: impl Into<ConstString>) {
186		for (original, old_value) in &mut self.0 {
187			if original.as_ref() == key {
188				// replace value
189				*old_value = remapped.into();
190				return;
191			}
192		}
193		// create if not existent
194		self.0.push((key.into(), remapped.into()));
195	}
196
197	/// Returns the remapped value for `key`, if there is a remapping, otherwise `None`.
198	#[must_use]
199	pub fn find(&self, key: &str) -> Option<ConstString> {
200		for (original, remapped) in &self.0 {
201			if original.as_ref() == key {
202				// is the shortcut '{=}' used?
203				return if remapped.as_ref() == "{=}" {
204					Some((String::from("{") + key + "}").into())
205				} else {
206					Some(remapped.clone())
207				};
208			}
209		}
210		None
211	}
212
213	/// Returns the remapped value for `key` if there is one, otherwise the original `key`.
214	#[must_use]
215	pub fn remap(&self, name: &str) -> ConstString {
216		for (original, remapped) in &self.0 {
217			if original.as_ref() == name {
218				// is the shortcut '{=}' used?
219				return if remapped.as_ref() == "{=}" {
220					name.into()
221				} else {
222					remapped.clone()
223				};
224			}
225		}
226		name.into()
227	}
228
229	/// Optimize for size
230	pub fn shrink(&mut self) {
231		self.0.shrink_to_fit();
232	}
233}
234// endregion:	--- remappings
235
236#[cfg(test)]
237mod tests {
238	use super::*;
239
240	// check, that the auto traits are available
241	const fn is_normal<T: Sized + Send + Sync>() {}
242
243	#[test]
244	const fn normal_types() {
245		is_normal::<Remappings>();
246		is_normal::<RemappingEntry>();
247	}
248}