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);
macro_rules! keyexpr_newtype {
($ty:ident) => {
impl $ty {
#[doc(hidden)]
pub fn from_canonical(s: String) -> Self {
Self(OwnedKeyExpr::try_from(s).expect("builder output is a canonical keyexpr"))
}
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 {
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()))
}
}
#[doc(hidden)]
pub fn from_valid(value: &str) -> Chunk {
debug_assert!(
crate::grammar::is_valid_plain_chunk(value),
"from_valid on an illegal chunk: {value:?}"
);
Chunk(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 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());
}
}