#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
#[non_exhaustive]
pub struct ToolId {
server: String,
name: String,
}
impl ToolId {
pub fn new(server: impl Into<String>, name: impl Into<String>) -> Result<ToolId, ToolIdError> {
let server = server.into();
let name = name.into();
Self::validate("server", &server)?;
Self::validate("name", &name)?;
Ok(Self { server, name })
}
pub(crate) fn from_validated(server: impl Into<String>, name: impl Into<String>) -> ToolId {
ToolId {
server: server.into(),
name: name.into(),
}
}
fn validate(field: &'static str, value: &str) -> Result<(), ToolIdError> {
validate_identifier(field, value)
}
#[must_use]
pub fn server(&self) -> &str {
&self.server
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ToolIdErrorKind {
Empty,
Separator,
Control,
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[error("invalid tool {field}: {reason}")]
#[non_exhaustive]
pub struct ToolIdError {
field: &'static str,
kind: ToolIdErrorKind,
reason: &'static str,
}
impl ToolIdError {
#[must_use]
pub fn kind(&self) -> ToolIdErrorKind {
self.kind
}
#[must_use]
pub fn field(&self) -> &str {
self.field
}
pub(crate) fn reason(&self) -> &'static str {
self.reason
}
}
pub(crate) fn validate_identifier(field: &'static str, value: &str) -> Result<(), ToolIdError> {
if value.is_empty() {
return Err(ToolIdError {
field,
kind: ToolIdErrorKind::Empty,
reason: "must not be empty",
});
}
if value.contains('/') {
return Err(ToolIdError {
field,
kind: ToolIdErrorKind::Separator,
reason: "must not contain the '/' separator",
});
}
if value.bytes().any(|b| b < 0x20 || b == 0x7f) {
return Err(ToolIdError {
field,
kind: ToolIdErrorKind::Control,
reason: "must not contain a control character",
});
}
Ok(())
}