Final Release of Version 1.0
This commit is contained in:
parent
d44ece3649
commit
9a471f3158
34 changed files with 3129 additions and 567 deletions
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "bread-shared"
|
||||
version = "0.1.0"
|
||||
version = "1.0.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
|
|
|
|||
|
|
@ -1,32 +1,71 @@
|
|||
//! Shared types for the Bread automation fabric.
|
||||
//!
|
||||
//! This crate defines the canonical event types ([`RawEvent`], [`BreadEvent`])
|
||||
//! and the [`AdapterSource`] enum that both the daemon (`breadd`) and CLI
|
||||
//! (`bread-cli`) depend on. Keeping these types in a separate crate guarantees
|
||||
//! that adapters, the state engine, IPC clients, and the Lua bindings all
|
||||
//! agree on a single wire format.
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Identifies which adapter produced an event.
|
||||
///
|
||||
/// The state engine uses this to choose a normalization strategy and the
|
||||
/// IPC layer surfaces it so subscribers can filter by origin.
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize, Eq, PartialEq, Hash)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum AdapterSource {
|
||||
/// The Hyprland compositor IPC socket.
|
||||
Hyprland,
|
||||
/// The Linux udev / netlink subsystem.
|
||||
Udev,
|
||||
/// Power management (sysfs / UPower).
|
||||
Power,
|
||||
/// Network state (rtnetlink / NetworkManager).
|
||||
Network,
|
||||
/// Internal events synthesized by the daemon itself
|
||||
/// (e.g. `bread.profile.activated`, `bread.state.changed.*`).
|
||||
System,
|
||||
}
|
||||
|
||||
/// An unnormalized event as emitted by an adapter.
|
||||
///
|
||||
/// Raw events carry the adapter's native payload verbatim. The
|
||||
/// [`EventNormalizer`](../breadd/core/normalizer/struct.EventNormalizer.html)
|
||||
/// in `breadd` transforms `RawEvent` into one or more [`BreadEvent`]s with
|
||||
/// a semantic name and structured data.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct RawEvent {
|
||||
/// Which adapter produced this event.
|
||||
pub source: AdapterSource,
|
||||
/// Adapter-specific event kind (e.g. `"workspace"`, `"add"`, `"battery"`).
|
||||
pub kind: String,
|
||||
/// Adapter-specific JSON payload — not stable across versions.
|
||||
pub payload: serde_json::Value,
|
||||
/// Unix epoch milliseconds when the event was observed.
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// A normalized event ready for dispatch to Lua subscribers and IPC consumers.
|
||||
///
|
||||
/// `BreadEvent` is the public, stable contract: event names use a dotted
|
||||
/// namespace (e.g. `bread.device.dock.connected`) and the `data` payload
|
||||
/// follows a documented shape per event family. See `Documentation.md` for
|
||||
/// the full event catalogue.
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct BreadEvent {
|
||||
/// Dotted event name, e.g. `bread.workspace.changed`.
|
||||
pub event: String,
|
||||
/// Unix epoch milliseconds when the originating signal was observed.
|
||||
pub timestamp: u64,
|
||||
/// The adapter that produced the underlying raw event.
|
||||
pub source: AdapterSource,
|
||||
/// Structured event data. The shape depends on the event family.
|
||||
pub data: serde_json::Value,
|
||||
}
|
||||
|
||||
impl BreadEvent {
|
||||
/// Construct a new event with `timestamp` set to the current wall-clock.
|
||||
pub fn new(event: impl Into<String>, source: AdapterSource, data: serde_json::Value) -> Self {
|
||||
Self {
|
||||
event: event.into(),
|
||||
|
|
@ -37,9 +76,136 @@ impl BreadEvent {
|
|||
}
|
||||
}
|
||||
|
||||
/// Current Unix epoch in milliseconds.
|
||||
///
|
||||
/// Falls back to `0` if the system clock is before the epoch, which keeps
|
||||
/// callers infallible. Used for `BreadEvent::timestamp` and replay cutoffs.
|
||||
pub fn now_unix_ms() -> u64 {
|
||||
std::time::SystemTime::now()
|
||||
.duration_since(std::time::UNIX_EPOCH)
|
||||
.unwrap_or_default()
|
||||
.as_millis() as u64
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_json::json;
|
||||
|
||||
#[test]
|
||||
fn adapter_source_serializes_as_snake_case() {
|
||||
assert_eq!(
|
||||
serde_json::to_string(&AdapterSource::Hyprland).unwrap(),
|
||||
"\"hyprland\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&AdapterSource::Udev).unwrap(),
|
||||
"\"udev\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&AdapterSource::Power).unwrap(),
|
||||
"\"power\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&AdapterSource::Network).unwrap(),
|
||||
"\"network\""
|
||||
);
|
||||
assert_eq!(
|
||||
serde_json::to_string(&AdapterSource::System).unwrap(),
|
||||
"\"system\""
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_source_round_trips_through_json() {
|
||||
for source in [
|
||||
AdapterSource::Hyprland,
|
||||
AdapterSource::Udev,
|
||||
AdapterSource::Power,
|
||||
AdapterSource::Network,
|
||||
AdapterSource::System,
|
||||
] {
|
||||
let s = serde_json::to_string(&source).unwrap();
|
||||
let back: AdapterSource = serde_json::from_str(&s).unwrap();
|
||||
assert_eq!(source, back);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_source_rejects_unknown_variant() {
|
||||
let result: Result<AdapterSource, _> = serde_json::from_str("\"floppy\"");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bread_event_new_sets_current_timestamp() {
|
||||
let before = now_unix_ms();
|
||||
let event = BreadEvent::new("bread.test", AdapterSource::System, json!({}));
|
||||
let after = now_unix_ms();
|
||||
|
||||
assert!(event.timestamp >= before);
|
||||
assert!(event.timestamp <= after);
|
||||
assert_eq!(event.event, "bread.test");
|
||||
assert_eq!(event.source, AdapterSource::System);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bread_event_new_accepts_owned_and_borrowed_names() {
|
||||
let owned = BreadEvent::new(String::from("bread.a"), AdapterSource::System, json!(null));
|
||||
let borrowed = BreadEvent::new("bread.b", AdapterSource::System, json!(null));
|
||||
assert_eq!(owned.event, "bread.a");
|
||||
assert_eq!(borrowed.event, "bread.b");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn bread_event_round_trips_through_json() {
|
||||
let original = BreadEvent {
|
||||
event: "bread.device.connected".to_string(),
|
||||
timestamp: 1_700_000_000_000,
|
||||
source: AdapterSource::Udev,
|
||||
data: json!({ "id": "usb-1-1.4", "name": "Logitech" }),
|
||||
};
|
||||
let raw = serde_json::to_string(&original).unwrap();
|
||||
let decoded: BreadEvent = serde_json::from_str(&raw).unwrap();
|
||||
|
||||
assert_eq!(decoded.event, original.event);
|
||||
assert_eq!(decoded.timestamp, original.timestamp);
|
||||
assert_eq!(decoded.source, original.source);
|
||||
assert_eq!(decoded.data, original.data);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn raw_event_round_trips_through_json() {
|
||||
let original = RawEvent {
|
||||
source: AdapterSource::Hyprland,
|
||||
kind: "workspace".to_string(),
|
||||
payload: json!({ "data": "2" }),
|
||||
timestamp: 42,
|
||||
};
|
||||
let raw = serde_json::to_string(&original).unwrap();
|
||||
let decoded: RawEvent = serde_json::from_str(&raw).unwrap();
|
||||
|
||||
assert_eq!(decoded.kind, original.kind);
|
||||
assert_eq!(decoded.timestamp, original.timestamp);
|
||||
assert_eq!(decoded.source, original.source);
|
||||
assert_eq!(decoded.payload, original.payload);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn now_unix_ms_is_monotonically_non_decreasing_across_calls() {
|
||||
let a = now_unix_ms();
|
||||
let b = now_unix_ms();
|
||||
assert!(b >= a, "now_unix_ms went backwards: {a} -> {b}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_source_is_hashable_and_eq() {
|
||||
use std::collections::HashSet;
|
||||
let mut set = HashSet::new();
|
||||
set.insert(AdapterSource::Hyprland);
|
||||
set.insert(AdapterSource::Hyprland);
|
||||
set.insert(AdapterSource::Udev);
|
||||
assert_eq!(set.len(), 2);
|
||||
assert!(set.contains(&AdapterSource::Hyprland));
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue