use crate::grammar::{self, Origin, Producer};
use crate::key::Key;
use crate::profile::AppProfile;
use crate::slug::chunk_slug;
#[derive(Debug, Clone)]
pub struct V1Context {
origin: Origin,
producer: Producer,
}
impl V1Context {
pub fn for_producer(profile: &'static AppProfile, name: &str) -> Self {
Self::with_origin(Origin::Host(profile.host_id().clone()), name)
}
pub fn with_origin(origin: Origin, name: &str) -> Self {
let producer = Producer::new(name).unwrap_or_else(|_| {
let slug = chunk_slug(name);
Producer::parse_chunk(&slug)
.or_else(|_| Producer::new("sensor"))
.expect("fallback producer name is valid")
});
Self { origin, producer }
}
pub fn with_instance(mut self, instance: u32) -> Self {
if let Ok(p) = Producer::with_instance(self.producer.name(), instance) {
self.producer = p;
}
self
}
pub fn origin(&self) -> &Origin {
&self.origin
}
pub fn producer(&self) -> &Producer {
&self.producer
}
pub fn telemetry_prefix(&self) -> Key {
Key::from_canonical(format!(
"{}/{}/{}/{}",
grammar::VERSION_CHUNK,
self.origin.chunk(),
grammar::CLASS_TELEMETRY,
self.producer.chunk()
))
}
pub fn state_key(&self, subject: &[&str]) -> Key {
for c in subject {
assert!(
*c != grammar::SUBJECT_ALIVE,
"`alive` is a reserved liveliness leaf (RFC 03 §3); use alive_key()"
);
}
self.build_key(grammar::CLASS_STATE, subject)
}
fn build_key(&self, class_or_plane: &str, subject: &[&str]) -> Key {
debug_assert!(!subject.is_empty());
let mut key = String::with_capacity(
8 + self.origin.chunk().len()
+ class_or_plane.len()
+ self.producer.name().len()
+ subject.iter().map(|c| c.len() + 1).sum::<usize>()
+ 8,
);
key.push_str(grammar::VERSION_CHUNK);
key.push('/');
key.push_str(self.origin.chunk());
key.push('/');
key.push_str(class_or_plane);
key.push('/');
self.producer.push_chunk(&mut key);
for c in subject {
key.push('/');
if grammar::is_valid_plain_chunk(c) {
key.push_str(c);
} else {
key.push_str(&chunk_slug(c));
}
}
Key::from_canonical(key)
}
pub fn health_key(&self) -> Key {
self.state_key(&["health"])
}
pub fn errors_key(&self) -> Key {
self.state_key(&["errors"])
}
pub fn sensor_info_key(&self) -> Key {
self.state_key(&["sensor"])
}
pub fn evidence_self_key(&self) -> Key {
self.state_key(&["evidence", "self"])
}
pub fn evidence_device_key(&self, device: &str) -> Key {
self.state_key(&["evidence", "device", device])
}
pub fn alive_key(&self) -> Key {
grammar::alive_key(&self.origin, Some(&self.producer)).expect("producer context is valid")
}
pub fn device_alive_key(&self, device: &str) -> Key {
let device = chunk_slug(device);
grammar::device_alive_key(&self.origin, &self.producer, &device)
.expect("slugged device chunk is valid")
}
pub fn rpc_key(&self, procedure: &[&str]) -> Key {
grammar::rpc_key(&self.origin, Some(&self.producer), procedure)
.expect("registry procedure chunks are valid")
}
pub fn media_video_key(&self, stream: &str, codec: &str, tier: &str) -> Key {
self.build_key(grammar::PLANE_MEDIA, &[stream, "video", codec, tier])
}
pub fn media_key(&self, stream: &[&str]) -> Key {
self.build_key(grammar::PLANE_MEDIA, stream)
}
pub fn blob_prefix(&self, tier: grammar::BlobTier) -> Key {
Key::from_canonical(format!(
"{}/{}/{}/{}",
grammar::VERSION_CHUNK,
self.origin.chunk(),
grammar::PLANE_BLOB,
tier.chunk()
))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::origin::HostId;
fn ctx() -> V1Context {
V1Context::with_origin(
Origin::Host(HostId::parse("h-3fa9c2d41b7e").unwrap()),
"sysinfo",
)
}
#[test]
fn key_shapes() {
let c = ctx();
assert_eq!(c.telemetry_prefix(), "v1/h-3fa9c2d41b7e/telemetry/sysinfo");
assert_eq!(c.health_key(), "v1/h-3fa9c2d41b7e/state/sysinfo/health");
assert_eq!(
c.evidence_self_key(),
"v1/h-3fa9c2d41b7e/state/sysinfo/evidence/self"
);
assert_eq!(c.alive_key(), "v1/h-3fa9c2d41b7e/state/sysinfo/alive");
assert_eq!(
c.device_alive_key("router01"),
"v1/h-3fa9c2d41b7e/state/sysinfo/device/router01/alive"
);
assert_ne!(
c.device_alive_key("Router01"),
c.device_alive_key("router01")
);
assert_eq!(
c.rpc_key(&["introspect"]),
"v1/h-3fa9c2d41b7e/@rpc/sysinfo/introspect"
);
assert_eq!(
c.media_video_key("cam0", "h264", "high"),
"v1/h-3fa9c2d41b7e/@media/sysinfo/cam0/video/h264/high"
);
}
#[test]
fn keys_are_base_relative() {
let c = ctx();
for key in [
c.telemetry_prefix(),
c.health_key(),
c.alive_key(),
c.rpc_key(&["introspect"]),
c.media_key(&["cam0", "preview", "jpeg"]),
c.blob_prefix(grammar::BlobTier::Store),
] {
assert!(
key.starts_with("v1/"),
"an application key must start at the version chunk: {key}"
);
}
}
#[test]
fn the_base_composes_back_on_for_the_wire_view() {
let c = ctx();
assert_eq!(
grammar::with_base("acme", c.telemetry_prefix()),
"acme/v1/h-3fa9c2d41b7e/telemetry/sysinfo"
);
assert_eq!(
grammar::with_base("acme/fleet-a", c.telemetry_prefix()),
"acme/fleet-a/v1/h-3fa9c2d41b7e/telemetry/sysinfo"
);
let wire = grammar::with_base("acme/fleet-a", c.telemetry_prefix());
assert_eq!(
grammar::strip_base("acme/fleet-a", &wire),
Some(c.telemetry_prefix().as_str())
);
}
#[test]
fn for_producer_uses_profile_origin() {
static PROFILE: AppProfile = AppProfile::new("zenkey-ctx-test", "ctx-test-salt");
let a = V1Context::for_producer(&PROFILE, "sysinfo");
let b = V1Context::for_producer(&PROFILE, "netlink");
assert_eq!(a.origin(), b.origin());
assert!(a.health_key().starts_with("v1/h-"));
}
}