use std::collections::BTreeSet;
use std::sync::OnceLock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TypeClassification {
Core,
KnownExtension,
UnknownExtension,
ReservedExperimental,
Malformed,
}
#[derive(Debug, Clone, Default)]
pub struct ExtensionRegistry {
advertised: BTreeSet<String>,
}
impl ExtensionRegistry {
#[must_use]
pub fn new() -> Self {
Self::default()
}
pub fn from_names<I, S>(names: I) -> Result<Self, String>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut reg = Self::new();
for name in names {
reg.register(name.into())?;
}
Ok(reg)
}
pub fn register(&mut self, name: String) -> Result<(), String> {
if !is_extension_name(&name) {
return Err(name);
}
self.advertised.insert(name);
Ok(())
}
#[must_use]
pub fn is_advertised(&self, name: &str) -> bool {
self.advertised.contains(name)
}
#[must_use]
pub fn len(&self) -> usize {
self.advertised.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.advertised.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &str> {
self.advertised.iter().map(String::as_str)
}
#[must_use]
pub fn classify(&self, type_name: &str) -> TypeClassification {
if type_name.is_empty() {
return TypeClassification::Malformed;
}
if type_name.starts_with("x-") {
return TypeClassification::ReservedExperimental;
}
if is_core_type(type_name) {
return TypeClassification::Core;
}
if is_extension_name(type_name) {
if self.is_advertised(type_name) {
TypeClassification::KnownExtension
} else {
TypeClassification::UnknownExtension
}
} else {
TypeClassification::Malformed
}
}
}
#[must_use]
pub fn classify_type(type_name: &str) -> TypeClassification {
ExtensionRegistry::new().classify(type_name)
}
#[must_use]
pub fn is_extension_name(name: &str) -> bool {
name.strip_prefix("arcpx.").map_or_else(
|| looks_like_reverse_dns(name) && is_dotted_versioned(name, 3),
|rest| is_dotted_versioned(rest, 2),
)
}
#[must_use]
pub fn is_core_type(type_name: &str) -> bool {
core_type_set().contains(type_name)
}
fn core_type_set() -> &'static BTreeSet<&'static str> {
static SET: OnceLock<BTreeSet<&'static str>> = OnceLock::new();
SET.get_or_init(|| {
[
"session.open",
"session.challenge",
"session.authenticate",
"session.accepted",
"session.unauthenticated",
"session.rejected",
"session.refresh",
"session.evicted",
"session.close",
"ping",
"pong",
"ack",
"nack",
"cancel",
"cancel.accepted",
"cancel.refused",
"interrupt",
"resume",
"backpressure",
"checkpoint.create",
"checkpoint.restore",
"tool.invoke",
"tool.result",
"tool.error",
"job.accepted",
"job.started",
"job.progress",
"job.heartbeat",
"job.checkpoint",
"job.completed",
"job.failed",
"job.cancelled",
"job.schedule",
"workflow.start",
"workflow.complete",
"agent.delegate",
"agent.handoff",
"stream.open",
"stream.chunk",
"stream.close",
"stream.error",
"permission.request",
"permission.grant",
"permission.deny",
"lease.granted",
"lease.extended",
"lease.revoked",
"lease.refresh",
"subscribe",
"subscribe.accepted",
"subscribe.event",
"unsubscribe",
"subscribe.closed",
"artifact.put",
"artifact.fetch",
"artifact.ref",
"artifact.release",
"event.emit",
"log",
"metric",
"trace.span",
]
.into_iter()
.collect()
})
}
fn is_dotted_versioned(s: &str, min_segments: usize) -> bool {
let segments: Vec<&str> = s.split('.').collect();
if segments.len() < min_segments {
return false;
}
if !segments.iter().all(|seg| {
!seg.is_empty()
&& seg
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_')
}) {
return false;
}
let last = segments[segments.len() - 1];
let Some(version_body) = last.strip_prefix('v') else {
return false;
};
!version_body.is_empty() && version_body.chars().all(|c| c.is_ascii_digit())
}
fn looks_like_reverse_dns(name: &str) -> bool {
let mut parts = name.split('.');
let Some(first) = parts.next() else {
return false;
};
if first.is_empty()
|| first.len() < 2
|| first.len() > 8
|| !first.chars().all(|c| c.is_ascii_lowercase())
{
return false;
}
true
}
#[cfg(test)]
#[allow(
clippy::expect_used,
clippy::unwrap_used,
clippy::panic,
clippy::missing_panics_doc
)]
mod tests {
use super::*;
#[test]
fn arcpx_namespace_is_valid_extension() {
assert!(is_extension_name("arcpx.example.v1"));
assert!(is_extension_name("arcpx.acme-corp.workflow.v2"));
}
#[test]
fn reverse_dns_namespace_is_valid_extension() {
assert!(is_extension_name("com.acme.workflow.v2"));
assert!(is_extension_name("io.example.metric.v1"));
}
#[test]
fn invalid_namespaces_are_rejected() {
assert!(!is_extension_name(""));
assert!(!is_extension_name("ping")); assert!(!is_extension_name("arcpx.foo")); assert!(!is_extension_name("arcpx.foo.v")); assert!(!is_extension_name("arcpx.foo.bar.v1.x")); assert!(!is_extension_name("1.2.3")); assert!(!is_extension_name("com.acme.workflow")); assert!(!is_extension_name("com.acme.workflow.v")); assert!(!is_extension_name("com.acme.workflow.vfoo")); }
#[test]
fn classify_recognises_core_types() {
assert_eq!(classify_type("session.open"), TypeClassification::Core);
assert_eq!(classify_type("job.progress"), TypeClassification::Core);
assert_eq!(classify_type("metric"), TypeClassification::Core);
}
#[test]
fn classify_advertised_extension() {
let mut reg = ExtensionRegistry::new();
reg.register("arcpx.example.v1".into()).expect("valid");
assert_eq!(
reg.classify("arcpx.example.v1"),
TypeClassification::KnownExtension,
);
}
#[test]
fn classify_unadvertised_extension() {
let reg = ExtensionRegistry::new();
assert_eq!(
reg.classify("arcpx.example.v1"),
TypeClassification::UnknownExtension,
);
}
#[test]
fn classify_experimental_prefix() {
assert_eq!(
classify_type("x-flaky-thing"),
TypeClassification::ReservedExperimental,
);
}
#[test]
fn classify_malformed_input() {
assert_eq!(classify_type(""), TypeClassification::Malformed);
assert_eq!(
classify_type("not.a.real.type"),
TypeClassification::Malformed
);
}
#[test]
fn registry_rejects_malformed_extension() {
let mut reg = ExtensionRegistry::new();
let err = reg
.register("not-an-extension".into())
.expect_err("must reject");
assert_eq!(err, "not-an-extension");
assert!(reg.is_empty());
}
#[test]
fn registry_from_names_propagates_error() {
let result = ExtensionRegistry::from_names(["arcpx.foo.v1", "broken"]);
assert_eq!(result.unwrap_err(), "broken");
}
}