use std::{
collections::VecDeque,
sync::{
mpsc::{sync_channel, Receiver, SyncSender, TryRecvError, TrySendError},
Arc,
},
};
use serde::Serialize;
use utils::sync::Mutex;
const CONFIGURATION_PORT_CAPACITY: usize = 64;
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, Serialize)]
#[serde(transparent)]
pub struct ConfigurationEventId(u64);
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(untagged)]
pub enum ConfigurationValue {
Bool(bool),
Integer(i64),
Float(f64),
Text(String),
}
impl ConfigurationValue {
pub fn as_text(&self) -> Option<&str> {
match self {
Self::Text(value) => Some(value),
_ => None,
}
}
}
impl From<&str> for ConfigurationValue {
fn from(value: &str) -> Self {
Self::Text(value.to_string())
}
}
impl From<String> for ConfigurationValue {
fn from(value: String) -> Self {
Self::Text(value)
}
}
#[derive(Clone, Debug, PartialEq, Serialize)]
#[serde(tag = "status", rename_all = "kebab-case")]
pub enum ConfigurationUpdateState {
Pending,
Set {
value: ConfigurationValue,
},
NotSet {
reason: String,
},
}
#[derive(Clone, Debug, PartialEq, Serialize)]
pub struct ConfigurationEvent {
id: ConfigurationEventId,
parameter: String,
requested: ConfigurationValue,
state: ConfigurationUpdateState,
}
impl ConfigurationEvent {
pub fn id(&self) -> ConfigurationEventId {
self.id
}
pub fn parameter(&self) -> &str {
&self.parameter
}
pub fn requested(&self) -> &ConfigurationValue {
&self.requested
}
pub fn state(&self) -> &ConfigurationUpdateState {
&self.state
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct ConfigurationUpdate {
id: ConfigurationEventId,
parameter: String,
value: ConfigurationValue,
}
impl ConfigurationUpdate {
pub fn id(&self) -> ConfigurationEventId {
self.id
}
pub fn parameter(&self) -> &str {
&self.parameter
}
pub fn value(&self) -> &ConfigurationValue {
&self.value
}
}
struct ConfigurationRoute {
prefix: String,
sender: SyncSender<ConfigurationUpdate>,
}
struct ConfigurationState {
next_event_id: u64,
routes: Vec<ConfigurationRoute>,
events: VecDeque<ConfigurationEvent>,
}
#[derive(Clone)]
pub struct Configuration {
state: Arc<Mutex<ConfigurationState>>,
}
impl Default for Configuration {
fn default() -> Self {
Self::new()
}
}
impl Configuration {
pub fn new() -> Self {
Self {
state: Arc::new(Mutex::new(ConfigurationState {
next_event_id: 1,
routes: Vec::new(),
events: VecDeque::new(),
})),
}
}
pub fn register(&self, prefix: &str) -> ConfigurationPort {
let (sender, receiver) = sync_channel(CONFIGURATION_PORT_CAPACITY);
self.state.lock().routes.push(ConfigurationRoute {
prefix: prefix.to_string(),
sender,
});
ConfigurationPort {
receiver,
state: self.state.clone(),
}
}
pub fn update(&self, parameter: impl Into<String>, value: impl Into<ConfigurationValue>) -> ConfigurationEventId {
let parameter = parameter.into();
let value = value.into();
let (id, sender) = {
let mut state = self.state.lock();
let id = ConfigurationEventId(state.next_event_id);
state.next_event_id += 1;
let sender = state
.routes
.iter()
.filter(|route| parameter.starts_with(&route.prefix))
.max_by_key(|route| route.prefix.len())
.map(|route| route.sender.clone());
state.events.push_back(ConfigurationEvent {
id,
parameter: parameter.clone(),
requested: value.clone(),
state: ConfigurationUpdateState::Pending,
});
(id, sender)
};
let update = ConfigurationUpdate { id, parameter, value };
match sender {
Some(sender) => match sender.try_send(update) {
Ok(()) => {}
Err(TrySendError::Full(_)) => self.finish_not_set(
id,
"Configuration update was not queued. The most likely cause is that the owning system has not drained its pending updates.",
),
Err(TrySendError::Disconnected(_)) => self.finish_not_set(
id,
"Configuration update was not queued. The most likely cause is that the owning system stopped receiving updates.",
),
},
None => self.finish_not_set(
id,
"Configuration parameter was not routed. The most likely cause is that no system registered its parameter prefix.",
),
}
id
}
pub fn event(&self, id: ConfigurationEventId) -> Option<ConfigurationEvent> {
self.state.lock().events.iter().find(|event| event.id == id).cloned()
}
pub fn events(&self) -> Vec<ConfigurationEvent> {
self.state.lock().events.iter().cloned().collect()
}
fn finish_not_set(&self, id: ConfigurationEventId, reason: impl Into<String>) {
finish_event(&self.state, id, ConfigurationUpdateState::NotSet { reason: reason.into() });
}
}
pub struct ConfigurationPort {
receiver: Receiver<ConfigurationUpdate>,
state: Arc<Mutex<ConfigurationState>>,
}
impl ConfigurationPort {
pub fn read(&self) -> Option<ConfigurationUpdate> {
match self.receiver.try_recv() {
Ok(update) => Some(update),
Err(TryRecvError::Empty | TryRecvError::Disconnected) => None,
}
}
pub fn set(&self, id: ConfigurationEventId, value: ConfigurationValue) {
finish_event(&self.state, id, ConfigurationUpdateState::Set { value });
}
pub fn not_set(&self, id: ConfigurationEventId, reason: impl Into<String>) {
finish_event(&self.state, id, ConfigurationUpdateState::NotSet { reason: reason.into() });
}
}
fn finish_event(state: &Arc<Mutex<ConfigurationState>>, id: ConfigurationEventId, result: ConfigurationUpdateState) {
let mut state = state.lock();
let Some(event) = state.events.iter_mut().find(|event| event.id == id) else {
return;
};
if matches!(event.state, ConfigurationUpdateState::Pending) {
event.state = result;
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn owner_reports_the_effective_value_for_a_pending_update() {
let configuration = Configuration::new();
let port = configuration.register("render.pass.");
let id = configuration.update("render.pass.bloom", "bypassed");
assert_eq!(configuration.event(id).unwrap().state(), &ConfigurationUpdateState::Pending);
let update = port.read().expect("render configuration update");
assert_eq!(update.id(), id);
assert_eq!(update.parameter(), "render.pass.bloom");
assert_eq!(update.value(), &ConfigurationValue::from("bypassed"));
port.set(id, ConfigurationValue::from("bypassed"));
assert_eq!(
configuration.event(id).unwrap().state(),
&ConfigurationUpdateState::Set {
value: ConfigurationValue::from("bypassed"),
}
);
}
#[test]
fn unrouted_update_gets_an_event_and_a_not_set_result() {
let configuration = Configuration::new();
let id = configuration.update("audio.master.gain", ConfigurationValue::Float(0.5));
assert!(matches!(
configuration.event(id).unwrap().state(),
ConfigurationUpdateState::NotSet { reason } if reason.contains("no system registered")
));
}
#[test]
fn the_longest_registered_prefix_owns_the_update() {
let configuration = Configuration::new();
let broad = configuration.register("render.");
let pass = configuration.register("render.pass.");
configuration.update("render.pass.bloom", "enabled");
assert!(broad.read().is_none());
assert!(pass.read().is_some());
}
}