use std::fmt;
use std::ops::Deref;
use zenoh_keyexpr::{OwnedKeyExpr, keyexpr};
use crate::grammar::KeyError;
use crate::slug::chunk_slug;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Key(OwnedKeyExpr);
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Selector(OwnedKeyExpr);
impl Key {
pub(crate) fn from_canonical(s: String) -> Self {
let ke = OwnedKeyExpr::try_from(s).expect("builder output is a canonical keyexpr");
assert!(
!is_wild(&ke),
"a Key is concrete (RFC 08 §1.2): {ke} carries a wildcard — build a Selector"
);
Key(ke)
}
}
fn is_wild(ke: &keyexpr) -> bool {
ke.as_str().contains('*')
}
impl Selector {
pub(crate) fn from_canonical(s: String) -> Self {
Selector(OwnedKeyExpr::try_from(s).expect("builder output is a canonical keyexpr"))
}
}
#[doc(hidden)]
pub mod __private {
use super::{Key, Selector};
#[must_use]
pub fn key_from_canonical(s: String) -> Key {
Key::from_canonical(s)
}
#[must_use]
pub fn selector_from_canonical(s: String) -> Selector {
Selector::from_canonical(s)
}
}
macro_rules! keyexpr_newtype {
($ty:ident) => {
impl $ty {
pub fn as_keyexpr(&self) -> &keyexpr {
&self.0
}
pub fn as_str(&self) -> &str {
self.0.as_str()
}
}
impl Deref for $ty {
type Target = keyexpr;
fn deref(&self) -> &keyexpr {
&self.0
}
}
impl fmt::Display for $ty {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}
impl AsRef<str> for $ty {
fn as_ref(&self) -> &str {
self.0.as_str()
}
}
impl From<$ty> for OwnedKeyExpr {
fn from(k: $ty) -> OwnedKeyExpr {
k.0
}
}
impl From<$ty> for String {
fn from(k: $ty) -> String {
k.0.to_string()
}
}
impl PartialEq<str> for $ty {
fn eq(&self, other: &str) -> bool {
self.as_str() == other
}
}
impl PartialEq<&str> for $ty {
fn eq(&self, other: &&str) -> bool {
self.as_str() == *other
}
}
impl PartialEq<String> for $ty {
fn eq(&self, other: &String) -> bool {
self.as_str() == other
}
}
impl PartialEq<$ty> for str {
fn eq(&self, other: &$ty) -> bool {
self == other.as_str()
}
}
impl PartialEq<$ty> for &str {
fn eq(&self, other: &$ty) -> bool {
*self == other.as_str()
}
}
};
}
keyexpr_newtype!(Key);
keyexpr_newtype!(Selector);
impl From<Key> for Selector {
fn from(k: Key) -> Selector {
Selector(k.0)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct Chunk(String);
impl Chunk {
#[must_use]
pub fn slug(value: impl AsRef<str>) -> Chunk {
Chunk(chunk_slug(value.as_ref()))
}
pub fn parse(value: &str) -> Result<Chunk, KeyError> {
if crate::grammar::is_valid_plain_chunk(value) {
Ok(Chunk(value.to_string()))
} else {
Err(KeyError::InvalidPlainChunk(value.to_string()))
}
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Deref for Chunk {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
impl fmt::Display for Chunk {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl AsRef<str> for Chunk {
fn as_ref(&self) -> &str {
&self.0
}
}
impl From<&str> for Chunk {
fn from(v: &str) -> Chunk {
Chunk::slug(v)
}
}
impl From<String> for Chunk {
fn from(v: String) -> Chunk {
Chunk::slug(&v)
}
}
impl From<&crate::origin::HostId> for Chunk {
fn from(id: &crate::origin::HostId) -> Chunk {
Chunk(id.as_str().to_string())
}
}
impl PartialEq<str> for Chunk {
fn eq(&self, other: &str) -> bool {
self.0 == other
}
}
impl PartialEq<&str> for Chunk {
fn eq(&self, other: &&str) -> bool {
self.0 == *other
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::grammar::{self, Class, Origin, Producer};
use crate::origin::HostId;
fn host() -> Origin {
Origin::Host(HostId::parse("h-3fa9c2d41b7e").unwrap())
}
#[test]
fn grammar_output_is_already_canonical() {
let producer = Producer::new("netring").unwrap();
let built = [
grammar::data_key(
&host(),
Class::Telemetry,
Some(&producer),
&["flow", "red", "p95_ms"],
)
.unwrap(),
grammar::rpc_key(&host(), Some(&producer), &["capture_disk", "set"]).unwrap(),
grammar::alive_key(&host(), Some(&producer)).unwrap(),
grammar::data_key(&Origin::catalog(), Class::State, None, &["entity", "abc"]).unwrap(),
];
for s in built {
let ke = OwnedKeyExpr::autocanonize(s.to_string()).unwrap();
assert_eq!(ke.as_str(), s.as_str(), "canonization rewrote {s}");
let key = Key::from_canonical(s.to_string());
assert_eq!(key, s.as_str());
}
}
#[test]
fn key_moves_into_owned_keyexpr() {
let key = Key::from_canonical("v1/h-3fa9c2d41b7e/state/netring/health".to_string());
let ke: OwnedKeyExpr = key.clone().into();
assert_eq!(ke.as_str(), key.as_str());
let sel: Selector = key.into();
assert_eq!(sel, "v1/h-3fa9c2d41b7e/state/netring/health");
}
#[test]
fn selector_intersects_via_deref() {
let sel = Selector::from_canonical("v1/*/telemetry/**".to_string());
let key = Key::from_canonical("v1/h-3fa9c2d41b7e/telemetry/netring/flow".to_string());
assert!(sel.intersects(&key));
}
#[test]
fn chunk_slug_and_parse() {
assert_eq!(Chunk::slug("p95_ms"), "p95_ms");
let dirty = Chunk::slug("Röuter 1/ETH0");
assert!(crate::grammar::is_valid_plain_chunk(dirty.as_str()));
assert!(Chunk::parse("p95_ms").is_ok());
assert!(Chunk::parse("Not A Chunk").is_err());
assert!(Chunk::parse("").is_err());
}
#[test]
fn a_host_id_converts_to_a_chunk_verbatim() {
let id = HostId::parse("h-3fa9c2d41b7e").unwrap();
let chunk = Chunk::from(&id);
assert_eq!(chunk, "h-3fa9c2d41b7e");
assert!(crate::grammar::is_valid_plain_chunk(chunk.as_str()));
}
#[test]
#[should_panic(expected = "a Key is concrete")]
fn a_wildcard_is_not_a_key() {
let _ = crate::__private::key_from_canonical("v1/*/state/**".to_string());
}
#[test]
fn the_same_wildcard_is_a_selector() {
let sel = crate::__private::selector_from_canonical("v1/*/state/**".to_string());
assert_eq!(sel, "v1/*/state/**");
}
#[test]
fn every_wildcard_shape_is_refused() {
for wild in [
"v1/*/state/netring/health",
"v1/h-3fa9c2d41b7e/state/*/health",
"v1/h-3fa9c2d41b7e/state/netring/**",
"v1/h-3fa9c2d41b7e/**/health",
] {
let attempt =
std::panic::catch_unwind(|| crate::__private::key_from_canonical(wild.to_string()));
assert!(attempt.is_err(), "{wild} must not become a Key");
assert_eq!(
crate::__private::selector_from_canonical(wild.to_string()),
wild
);
}
}
}