use crate::MAX_SCHEMA_NAME_BYTES;
use sha2::{Digest, Sha256};
const HEX: &[u8; 16] = b"0123456789abcdef";
#[must_use]
pub fn is_engine_reserved_schema_name(name: &str) -> bool {
name.starts_with("__icydb_")
}
#[must_use]
pub fn canonical_index_name(
entity: &str,
terms: &[&str],
unique: bool,
predicate: Option<&[u8]>,
) -> String {
let mut hash = Sha256::new();
hash.update(b"icydb:index-name:v1\0");
hash.update([u8::from(unique)]);
hash_name_part(&mut hash, entity.as_bytes());
hash.update((terms.len() as u64).to_be_bytes());
for term in terms {
hash_name_part(&mut hash, term.as_bytes());
}
hash.update([u8::from(predicate.is_some())]);
if let Some(predicate) = predicate {
hash_name_part(&mut hash, predicate);
}
let digest = hash.finalize();
let prefix = if unique { "uniq" } else { "idx" };
let mut name = format!(
"{prefix}_{}__{}",
canonical_index_name_slug(entity),
terms
.iter()
.map(|term| canonical_index_name_slug(term))
.collect::<Vec<_>>()
.join("_")
);
name.truncate(MAX_SCHEMA_NAME_BYTES - 33);
name.push('_');
for byte in &digest[..16] {
name.push(char::from(HEX[(byte >> 4) as usize]));
name.push(char::from(HEX[(byte & 15) as usize]));
}
name
}
fn hash_name_part(hash: &mut Sha256, bytes: &[u8]) {
hash.update((bytes.len() as u64).to_be_bytes());
hash.update(bytes);
}
#[must_use]
pub fn canonical_index_name_slug(value: &str) -> String {
let separated = value
.chars()
.map(|character| {
if character.is_ascii_alphanumeric() {
character
} else {
'_'
}
})
.collect::<String>();
to_snake_case(separated.as_str())
}
fn to_snake_case(value: &str) -> String {
let mut snake_case = String::new();
let mut characters = value.trim().chars().peekable();
let mut previous = None;
let mut index = 0;
while let Some(character) = characters.next() {
let next = characters.peek().copied();
if character.is_uppercase() {
let previous_is_lower_or_digit = previous.is_some_and(|candidate: char| {
candidate.is_lowercase() || candidate.is_ascii_digit()
});
let next_is_lower = next.is_some_and(char::is_lowercase);
if index != 0
&& !snake_case.ends_with('_')
&& (previous_is_lower_or_digit || next_is_lower)
{
snake_case.push('_');
}
snake_case.extend(character.to_lowercase());
} else if character == ' ' || character == '_' {
if !snake_case.ends_with('_') {
snake_case.push('_');
}
} else if character.is_alphanumeric() {
snake_case.push(character);
}
previous = Some(character);
index += 1;
}
snake_case.trim_matches('_').to_string()
}
#[cfg(test)]
mod tests;