//! Swarm event subjects: the names the controller publishes on and hives //! subscribe to. //! //! Same reason [`crate::status`] exists rather than a `const` on each side — //! **the ends must agree, and a literal repeated across crates is an agreement //! nothing checks.** Here there are three of them: the swarm controller //! publishes, a hive subscribes, and the auth-callout responder derives the //! subject the controller is *permitted* to publish to. A copied literal in the //! third would produce the worst failure of the set: a grant that looks right, //! a publish that is refused, and — because a NATS denial reaches the client as //! a timeout rather than an error — no message saying so. //! //! Unconditional, with no feature gate and no NATS types, for the same reason //! the bucket *name* in [`crate::status`] is not gated: the responder names //! this subject without ever publishing to it, and speaks neither `jetstream` //! nor `kv`. A gate here would make that consumer choose between a stack it //! does not use and a copied literal. //! //! # Why a per-hive subject rather than one shared one //! //! The callout policy refuses any extra hive subject with no `{hive}` in it, //! because such a template expands to the same subject for every hive and so //! grants each of them the others'. A per-hive event subject satisfies that by //! construction. It is also what makes the event addressable: the controller //! decides *which* hives need to know, rather than every hive filtering a //! shared firehose. /// The root of the swarm event namespace. /// /// `$SWARM` rather than a bare name: the `$` prefix is NATS' convention for /// system-ish subjects and keeps these clear of anything an application might /// choose for itself. pub const SUBJECT_ROOT: &str = "$SWARM.events"; /// The leaf naming the *knowledge repository changed* event. /// /// Semantic, not transport-shaped: it says what happened, not that a forge /// webhook arrived. The controller interprets a delivery and decides this is /// what it means; a hive that receives it does not need to know a forge exists. const KNOWLEDGE_LEAF: &str = "knowledge"; /// The wildcard standing for "any hive", for a grant that must cover all of /// them. /// /// Exported so the one caller that needs it — the callout responder, building /// the controller's publish grant — can pass it to [`knowledge`] instead of /// assembling a wildcard subject itself. That is the whole point: the grant and /// the published subject come out of **the same function**, so they cannot /// drift into disagreement the way two literals would. pub const ANY_HIVE: &str = "*"; /// The subject carrying *the knowledge repository changed* for `hive`. /// /// Pass [`ANY_HIVE`] to get the wildcard form used by a grant. #[must_use] pub fn knowledge(hive: &str) -> String { format!("{SUBJECT_ROOT}.{hive}.{KNOWLEDGE_LEAF}") } #[cfg(test)] mod tests { use super::{ANY_HIVE, SUBJECT_ROOT, knowledge}; /// The concrete and wildcard forms must differ in exactly the hive token. /// /// Written as a structural comparison rather than by asserting two /// literals, because two literals is the failure this module exists to /// prevent: a test that spells the expected subject out by hand passes /// happily when both it and the code are wrong in the same way. #[test] fn the_grant_form_and_the_published_form_differ_only_in_the_hive() { let concrete = knowledge("alpha"); let wildcard = knowledge(ANY_HIVE); assert_eq!( concrete.replacen("alpha", ANY_HIVE, 1), wildcard, "substituting the hive token must turn one form into the other" ); } /// A NATS wildcard matches one token, so the hive must occupy exactly one. /// A hive name with a dot in it would silently widen the grant. #[test] fn the_hive_occupies_exactly_one_subject_token() { let root_tokens = SUBJECT_ROOT.split('.').count(); assert_eq!( knowledge("alpha").split('.').count(), root_tokens + 2, "root + hive + leaf; anything else means the hive is not one token" ); } /// The event is addressed per hive — one hive's subject must never be /// another's. Cheap, and it is the property the callout policy relies on. #[test] fn two_hives_get_different_subjects() { assert_ne!(knowledge("alpha"), knowledge("beta")); } }