use std::fmt;
use serde::de::Error as DeError;
use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
#[derive(Debug, Error)]
#[error("invalid {kind}: {value:?} ({rule})")]
pub struct IdError {
kind: &'static str,
value: String,
rule: &'static str,
}
fn is_ident(s: &str) -> bool {
let mut chars = s.chars();
chars
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '_')
&& s.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
}
macro_rules! string_id {
($(#[$doc:meta])* $name:ident, $kind:literal, $rule:literal, $check:expr) => {
$(#[$doc])*
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
#[serde(transparent)]
pub struct $name(String);
impl $name {
pub fn parse(value: &str) -> Result<Self, IdError> {
let rule_holds: fn(&str) -> bool = $check;
if rule_holds(value) {
Ok(Self(value.to_owned()))
} else {
Err(IdError { kind: $kind, value: value.to_owned(), rule: $rule })
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl std::str::FromStr for $name {
type Err = IdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(D::Error::custom)
}
}
};
}
string_id!(
CaseId,
"case id",
"non-empty, ASCII, no whitespace",
|s: &str| !s.is_empty() && s.is_ascii() && !s.contains(char::is_whitespace)
);
string_id!(
CapabilityName,
"capability name",
"ASCII identifier",
is_ident
);
string_id!(
CorpusKey,
"corpus key",
"dot-separated lowercase segments of [a-z0-9_-]",
|s: &str| {
!s.is_empty()
&& s.split('.').all(|seg| {
!seg.is_empty()
&& seg.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
})
}
);
string_id!(
AmbiguityId,
"ambiguity id",
"AMB-<number>",
|s: &str| {
s.strip_prefix("AMB-").is_some_and(|n| !n.is_empty() && n.chars().all(|c| c.is_ascii_digit()))
}
);
string_id!(
OptionTag,
"option tag",
"non-empty [a-z0-9_-]",
|s: &str| !s.is_empty() && s.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_' || c == '-')
);
string_id!(
ViewName,
"view name",
"ASCII identifier",
is_ident
);
string_id!(
RecipeName,
"recipe name",
"ASCII identifier",
is_ident
);
string_id!(
InstanceName,
"instance name",
"ASCII identifier",
is_ident
);
string_id!(
CaptureName,
"capture name",
"ASCII identifier",
is_ident
);
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct SmOperationRef {
interface: String,
operation: String,
}
impl SmOperationRef {
pub fn parse(value: &str) -> Result<Self, IdError> {
let err = |rule| IdError {
kind: "SM operation",
value: value.to_owned(),
rule,
};
let (interface, operation) = value
.split_once('.')
.ok_or_else(|| err("must be I_<INTERFACE>.<operation>"))?;
if !interface.starts_with("I_")
|| !interface
.chars()
.all(|c| c.is_ascii_uppercase() || c.is_ascii_digit() || c == '_')
{
return Err(err("interface must be I_UPPER_SNAKE"));
}
if !operation
.chars()
.next()
.is_some_and(|c| c.is_ascii_lowercase())
|| !operation
.chars()
.all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '_')
{
return Err(err("operation must be lower_snake"));
}
Ok(Self {
interface: interface.to_owned(),
operation: operation.to_owned(),
})
}
#[must_use]
pub fn interface(&self) -> &str {
&self.interface
}
#[must_use]
pub fn operation(&self) -> &str {
&self.operation
}
#[must_use]
pub fn sibling(&self, operation: &str) -> Self {
Self {
interface: self.interface.clone(),
operation: operation.to_owned(),
}
}
}
impl fmt::Display for SmOperationRef {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}.{}", self.interface, self.operation)
}
}
impl std::str::FromStr for SmOperationRef {
type Err = IdError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
Self::parse(s)
}
}
impl Serialize for SmOperationRef {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for SmOperationRef {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let s = String::deserialize(deserializer)?;
Self::parse(&s).map_err(D::Error::custom)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn sm_operation_parses_and_prints() {
let op = SmOperationRef::parse("I_EHR_SERVICE.create_ehr").unwrap();
assert_eq!(op.interface(), "I_EHR_SERVICE");
assert_eq!(op.operation(), "create_ehr");
assert_eq!(op.to_string(), "I_EHR_SERVICE.create_ehr");
assert_eq!(op.sibling("get_ehr").to_string(), "I_EHR_SERVICE.get_ehr");
}
#[test]
fn sm_operation_rejects_malformed() {
assert!(SmOperationRef::parse("EHR_SERVICE.create_ehr").is_err());
assert!(SmOperationRef::parse("I_EHR_SERVICE").is_err());
assert!(SmOperationRef::parse("I_EHR_SERVICE.CreateEhr").is_err());
}
#[test]
fn ambiguity_id_shape() {
assert!(AmbiguityId::parse("AMB-13").is_ok());
assert!(AmbiguityId::parse("AMB-").is_err());
assert!(AmbiguityId::parse("amb-1").is_err());
}
#[test]
fn corpus_key_shape() {
assert!(CorpusKey::parse("cnf.composition.minimal_event.v1").is_ok());
assert!(CorpusKey::parse("cnf.set.bp-10").is_ok());
assert!(CorpusKey::parse("Cnf.Upper").is_err());
assert!(CorpusKey::parse("cnf..double_dot").is_err());
}
}