#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct ModelId {
server: String,
name: String,
}
impl ModelId {
pub const GATEWAY: &'static str = "gateway";
pub fn new(
server: impl Into<String>,
name: impl Into<String>,
) -> std::result::Result<ModelId, ModelIdError> {
let server = server.into();
let name = name.into();
Self::validate("server", &server)?;
Self::validate("name", &name)?;
Ok(Self { server, name })
}
pub fn gateway(name: impl Into<String>) -> std::result::Result<ModelId, ModelIdError> {
Self::new(Self::GATEWAY, name)
}
pub(crate) fn from_validated(server: impl Into<String>, name: impl Into<String>) -> ModelId {
ModelId {
server: server.into(),
name: name.into(),
}
}
pub(crate) const PICKER_SEPARATOR: char = '\u{001e}';
fn validate(field: &'static str, value: &str) -> std::result::Result<(), ModelIdError> {
if value.is_empty() {
return Err(ModelIdError {
field,
reason: "must not be empty",
});
}
if value
.chars()
.any(|c| c.is_control() || c == Self::PICKER_SEPARATOR)
{
return Err(ModelIdError {
field,
reason: "must not contain a control character",
});
}
Ok(())
}
#[must_use]
pub fn server(&self) -> &str {
&self.server
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid model id: {field} {reason}")]
#[non_exhaustive]
pub struct ModelIdError {
field: &'static str,
reason: &'static str,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum ModelCatalogError {
#[error("duplicate model identity in catalog: {server}/{name}")]
#[non_exhaustive]
DuplicateId {
server: String,
name: String,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rejects_c0_c1_and_picker_separator_controls() {
assert!(ModelId::new(ModelId::GATEWAY, "a\u{001e}b").is_err());
assert!(ModelId::new(ModelId::GATEWAY, "a\u{0085}b").is_err());
assert!(ModelId::new(ModelId::GATEWAY, "a\u{007f}b").is_err());
assert!(ModelId::new("srv\u{0000}", "name").is_err());
assert!(ModelId::new(ModelId::GATEWAY, "café-模型").is_ok());
}
}