use std::fmt;
use std::num::NonZeroU32;
use khive_types::Hash32;
#[derive(Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
pub struct EmbeddingSpaceKey(String);
impl EmbeddingSpaceKey {
pub fn as_str(&self) -> &str {
&self.0
}
}
impl AsRef<str> for EmbeddingSpaceKey {
fn as_ref(&self) -> &str {
self.as_str()
}
}
impl fmt::Display for EmbeddingSpaceKey {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}
#[derive(Clone, Debug, Eq, PartialEq, Hash)]
pub struct EmbeddingSpaceIdentity {
space_key: EmbeddingSpaceKey,
protocol: String,
fingerprint: [u8; 32],
model_name: String,
dimensions: NonZeroU32,
}
#[derive(Clone, Debug, Eq, PartialEq, thiserror::Error)]
pub enum EmbeddingSpaceIdentityError {
#[error("embedding space key prefix must be non-empty ASCII alphanumeric/underscore")]
InvalidKeyPrefix,
#[error("embedding protocol must be 1..=128 bytes from [A-Za-z0-9._-]")]
InvalidProtocol,
#[error("embedding model name must be 1..=512 bytes with no surrounding whitespace")]
InvalidModelName,
#[error("embedding dimensions must be in 1..=8192, got {dimensions}")]
InvalidDimensions {
dimensions: u32,
},
#[error("embedding space key must be at most 128 bytes, got {bytes}")]
SpaceKeyTooLong {
bytes: usize,
},
}
impl EmbeddingSpaceIdentity {
pub fn new(
key_prefix: &str,
protocol: &str,
fingerprint: [u8; 32],
model_name: &str,
dimensions: u32,
) -> Result<Self, EmbeddingSpaceIdentityError> {
if key_prefix.is_empty()
|| !key_prefix
.bytes()
.all(|b| b.is_ascii_alphanumeric() || b == b'_')
{
return Err(EmbeddingSpaceIdentityError::InvalidKeyPrefix);
}
if protocol.is_empty()
|| protocol.len() > 128
|| !protocol
.bytes()
.all(|b| b.is_ascii_alphanumeric() || matches!(b, b'.' | b'_' | b'-'))
{
return Err(EmbeddingSpaceIdentityError::InvalidProtocol);
}
if model_name.trim().is_empty() || model_name.trim() != model_name || model_name.len() > 512
{
return Err(EmbeddingSpaceIdentityError::InvalidModelName);
}
if !(1..=8192).contains(&dimensions) {
return Err(EmbeddingSpaceIdentityError::InvalidDimensions { dimensions });
}
let dimension_text = dimensions.to_string();
let key_bytes = key_prefix.len() + 1 + 64 + 1 + dimension_text.len();
if key_bytes > 128 {
return Err(EmbeddingSpaceIdentityError::SpaceKeyTooLong { bytes: key_bytes });
}
let space_key = EmbeddingSpaceKey(format!(
"{key_prefix}_{}_{dimension_text}",
Hash32::from_bytes(fingerprint)
));
Ok(Self {
space_key,
protocol: protocol.to_owned(),
fingerprint,
model_name: model_name.to_owned(),
dimensions: NonZeroU32::new(dimensions).expect("validated nonzero dimensions"),
})
}
pub fn space_key(&self) -> &EmbeddingSpaceKey {
&self.space_key
}
pub fn protocol(&self) -> &str {
&self.protocol
}
pub fn fingerprint(&self) -> &[u8; 32] {
&self.fingerprint
}
pub fn model_name(&self) -> &str {
&self.model_name
}
pub fn dimensions(&self) -> NonZeroU32 {
self.dimensions
}
}