use std::borrow::Cow;
#[derive(PartialEq, Eq, Clone, Debug)]
pub struct EntityKind {
value: Cow<'static, str>,
name_start: usize,
version_start: usize,
}
impl EntityKind {
pub fn parse_parts(s: &str) -> Result<(usize, usize), EntityKindParseError> {
const fn is_valid_kind_char(c: char) -> bool {
match c {
'a'..='z' | 'A'..='Z' | '0'..='9' => true,
_ => false,
}
}
let (rest, version) = match s.rsplit_once(".v") {
Some(x) => x,
None => return Err(EntityKindParseError::MissingVersion),
};
let (ns, name) = match rest.rsplit_once('/') {
Some(x) => x,
None => return Err(EntityKindParseError::MissingNamespace),
};
if ns.is_empty() {
return Err(EntityKindParseError::MissingNamespace);
}
if let Some(c) = ns.chars().find(|c| !is_valid_kind_char(*c)) {
return Err(EntityKindParseError::InvalidNamespaceCharacter(c));
}
if name.is_empty() {
return Err(EntityKindParseError::MissingName);
}
if let Some(c) = name.chars().find(|c| !is_valid_kind_char(*c)) {
return Err(EntityKindParseError::InvalidNameCharacter(c));
}
let len = s.len();
let version_start = len - version.len();
let name_start = version_start - 2 - name.len();
Ok((name_start, version_start))
}
pub fn parse(s: impl Into<String>) -> Result<Self, EntityKindParseError> {
let s = s.into();
let (name_start, version_start) = Self::parse_parts(&s)?;
Ok(Self {
value: Cow::Owned(s),
name_start,
version_start,
})
}
pub fn namespace(&self) -> &str {
&self.value[..self.name_start - 1]
}
pub fn name(&self) -> &str {
&self.value[self.name_start..self.version_start - 2]
}
pub fn version(&self) -> &str {
&self.value[self.version_start..]
}
}
impl serde::Serialize for EntityKind {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.value)
}
}
impl<'de> serde::Deserialize<'de> for EntityKind {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
EntityKind::parse(s).map_err(serde::de::Error::custom)
}
}
impl schemars::JsonSchema for EntityKind {
fn schema_name() -> String {
"EntityKind".to_string()
}
fn json_schema(_gen: &mut schemars::gen::SchemaGenerator) -> schemars::schema::Schema {
schemars::schema::Schema::Object(schemars::schema::SchemaObject {
instance_type: Some(schemars::schema::InstanceType::String.into()),
..Default::default()
})
}
}
impl std::str::FromStr for EntityKind {
type Err = EntityKindParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
EntityKind::parse(s)
}
}
#[derive(Clone, Debug)]
pub enum EntityKindParseError {
MissingVersion,
MissingNamespace,
MissingName,
InvalidNamespaceCharacter(char),
InvalidNameCharacter(char),
InvalidVersion,
}
impl std::fmt::Display for EntityKindParseError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EntityKindParseError::MissingVersion => write!(f, "missing version"),
EntityKindParseError::MissingNamespace => write!(f, "missing namespace"),
EntityKindParseError::MissingName => write!(f, "missing name"),
EntityKindParseError::InvalidNamespaceCharacter(c) => {
write!(f, "invalid character in namespace: '{c}'")
}
EntityKindParseError::InvalidNameCharacter(c) => {
write!(f, "invalid character in name: '{}'", c)
}
EntityKindParseError::InvalidVersion => write!(f, "invalid version"),
}
}
}
impl std::error::Error for EntityKindParseError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_entity_kind() {
let e = EntityKind::parse("a/b.v1").unwrap();
assert_eq!(e.namespace(), "a");
assert_eq!(e.name(), "b");
assert_eq!(e.version(), "1");
}
}