bread/bread-shared/src/apps.rs

113 lines
3.5 KiB
Rust

//! The known-apps registry for sibling `bread*` application integration.
//!
//! Adding a new sibling app is a one-line edit to [`KNOWN_APPS`] — no new
//! `AdapterSource` variant, no new normalizer arm, no recompile-driven
//! exhaustiveness churn. This is what makes the sibling-app integration
//! model extensible: see `Documentation.md`'s "Namespaces" section and
//! "Integrating a bread* app" recipe.
/// Registered sibling `bread*` app ids. Each id is also the app's reserved
/// segment in the `bread.<id>.*` (events) and `bread.command.<id>.*`
/// (commands) namespaces.
pub const KNOWN_APPS: &[&str] = &[
"clip", "pad", "bar", "box", "lock", "mon", "paper", "search", "shot", "arr", "crumbs", "help",
"bakery",
];
/// Daemon-internal domains that are reserved and can never be claimed as an
/// app id, even if a future `bread*` app would otherwise want that name —
/// these are the top-level segments the normalizer and built-in event
/// families already use.
const RESERVED_DOMAINS: &[&str] = &[
"terminal",
"git",
"hyprland",
"device",
"power",
"network",
"service",
"container",
"project",
"remote",
"system",
"profile",
"notify",
"command",
"workflow",
];
/// Whether `id` is a registered sibling-app id.
pub fn is_known_app(id: &str) -> bool {
KNOWN_APPS.contains(&id)
}
/// Whether `id` is reserved for daemon-internal use and can never be
/// registered as a sibling-app id.
pub fn is_reserved_domain(id: &str) -> bool {
RESERVED_DOMAINS.contains(&id)
}
/// Whether `event` is a well-formed event name for `app` — i.e. it starts
/// with `bread.<app>.`. An app may only publish within its own namespace
/// segment; this is what the IPC boundary checks before constructing a
/// `RawEvent` tagged `AdapterSource::App(app)`.
pub fn validate_app_namespace(app: &str, event: &str) -> bool {
event.starts_with(&format!("bread.{app}."))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn known_apps_are_recognized() {
assert!(is_known_app("clip"));
assert!(is_known_app("bakery"));
}
#[test]
fn unknown_app_is_not_recognized() {
assert!(!is_known_app("notanapp"));
assert!(!is_known_app(""));
}
#[test]
fn reserved_domains_are_never_known_apps() {
for domain in RESERVED_DOMAINS {
assert!(
!is_known_app(domain),
"reserved domain '{domain}' must not double as a known app id"
);
}
}
#[test]
fn reserved_domains_are_recognized() {
assert!(is_reserved_domain("power"));
assert!(is_reserved_domain("hyprland"));
assert!(!is_reserved_domain("clip"));
}
#[test]
fn validate_app_namespace_accepts_own_namespace() {
assert!(validate_app_namespace("clip", "bread.clip.copied"));
assert!(validate_app_namespace(
"clip",
"bread.clip.stack_trace.captured"
));
}
#[test]
fn validate_app_namespace_rejects_other_namespaces() {
assert!(!validate_app_namespace("clip", "bread.pad.reminder.due"));
assert!(!validate_app_namespace("clip", "bread.power.ac.connected"));
assert!(!validate_app_namespace("clip", "bread.clipboard.copied"));
}
#[test]
fn validate_app_namespace_rejects_bare_prefix_without_trailing_dot() {
// "bread.clipx..." must not satisfy the "clip" namespace just
// because it shares a string prefix.
assert!(!validate_app_namespace("clip", "bread.clipx.copied"));
}
}