use std::collections::HashMap;
use std::fmt;
use crate::error::{GraphError, GraphResult};
use crate::frame::FilterFrame;
use crate::port::{InputPort, OutputPort, PortId, PortType};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub struct NodeId(pub u64);
impl fmt::Display for NodeId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Node({})", self.0)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NodeType {
Source,
Filter,
Sink,
}
impl fmt::Display for NodeType {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Source => write!(f, "Source"),
Self::Filter => write!(f, "Filter"),
Self::Sink => write!(f, "Sink"),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum NodeState {
#[default]
Idle,
Processing,
Done,
Error,
}
impl fmt::Display for NodeState {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Idle => write!(f, "Idle"),
Self::Processing => write!(f, "Processing"),
Self::Done => write!(f, "Done"),
Self::Error => write!(f, "Error"),
}
}
}
impl NodeState {
#[must_use]
#[allow(clippy::match_same_arms)]
pub fn can_transition_to(&self, new_state: Self) -> bool {
match (self, new_state) {
(Self::Idle, Self::Processing | Self::Done | Self::Error) => true,
(Self::Processing, Self::Idle | Self::Done | Self::Error) => true,
(Self::Done | Self::Error, Self::Idle) => true,
(a, b) if *a == b => true,
_ => false,
}
}
}
#[derive(Clone, Debug, Default)]
pub struct NodeConfig {
pub name: String,
pub node_type: Option<NodeType>,
pub options: HashMap<String, ConfigValue>,
}
impl NodeConfig {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
node_type: None,
options: HashMap::new(),
}
}
#[must_use]
pub fn with_type(mut self, node_type: NodeType) -> Self {
self.node_type = Some(node_type);
self
}
#[must_use]
pub fn with_option(mut self, key: impl Into<String>, value: ConfigValue) -> Self {
self.options.insert(key.into(), value);
self
}
#[must_use]
pub fn get_option(&self, key: &str) -> Option<&ConfigValue> {
self.options.get(key)
}
#[must_use]
pub fn get_int(&self, key: &str) -> Option<i64> {
self.options.get(key).and_then(ConfigValue::as_int)
}
#[must_use]
pub fn get_float(&self, key: &str) -> Option<f64> {
self.options.get(key).and_then(ConfigValue::as_float)
}
#[must_use]
pub fn get_string(&self, key: &str) -> Option<&str> {
self.options.get(key).and_then(ConfigValue::as_string)
}
#[must_use]
pub fn get_bool(&self, key: &str) -> Option<bool> {
self.options.get(key).and_then(ConfigValue::as_bool)
}
}
#[derive(Clone, Debug, PartialEq)]
pub enum ConfigValue {
Int(i64),
Float(f64),
String(String),
Bool(bool),
}
impl ConfigValue {
#[must_use]
pub fn as_int(&self) -> Option<i64> {
match self {
Self::Int(v) => Some(*v),
_ => None,
}
}
#[must_use]
pub fn as_float(&self) -> Option<f64> {
match self {
Self::Float(v) => Some(*v),
Self::Int(v) => Some(*v as f64),
_ => None,
}
}
#[must_use]
pub fn as_string(&self) -> Option<&str> {
match self {
Self::String(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn as_bool(&self) -> Option<bool> {
match self {
Self::Bool(v) => Some(*v),
_ => None,
}
}
}
impl From<i64> for ConfigValue {
fn from(v: i64) -> Self {
Self::Int(v)
}
}
impl From<f64> for ConfigValue {
fn from(v: f64) -> Self {
Self::Float(v)
}
}
impl From<String> for ConfigValue {
fn from(v: String) -> Self {
Self::String(v)
}
}
impl From<&str> for ConfigValue {
fn from(v: &str) -> Self {
Self::String(v.to_string())
}
}
impl From<bool> for ConfigValue {
fn from(v: bool) -> Self {
Self::Bool(v)
}
}
pub trait Node: Send + Sync {
fn id(&self) -> NodeId;
fn name(&self) -> &str;
fn node_type(&self) -> NodeType;
fn state(&self) -> NodeState;
fn set_state(&mut self, state: NodeState) -> GraphResult<()>;
fn inputs(&self) -> &[InputPort];
fn outputs(&self) -> &[OutputPort];
fn initialize(&mut self) -> GraphResult<()> {
Ok(())
}
fn process(&mut self, input: Option<FilterFrame>) -> GraphResult<Option<FilterFrame>>;
fn flush(&mut self) -> GraphResult<Vec<FilterFrame>> {
Ok(Vec::new())
}
fn reset(&mut self) -> GraphResult<()> {
self.set_state(NodeState::Idle)
}
fn input_port(&self, id: PortId) -> Option<&InputPort> {
self.inputs().iter().find(|p| p.id == id)
}
fn output_port(&self, id: PortId) -> Option<&OutputPort> {
self.outputs().iter().find(|p| p.id == id)
}
fn accepts_input(&self, port_type: PortType) -> bool {
self.inputs().iter().any(|p| p.port_type == port_type)
}
fn produces_output(&self, port_type: PortType) -> bool {
self.outputs().iter().any(|p| p.port_type == port_type)
}
}
#[allow(dead_code)]
pub struct NodeRuntime {
node: Box<dyn Node>,
input_buffers: HashMap<PortId, Vec<FilterFrame>>,
output_buffers: HashMap<PortId, Vec<FilterFrame>>,
frames_processed: u64,
}
impl NodeRuntime {
pub fn new(node: Box<dyn Node>) -> Self {
let input_buffers = node.inputs().iter().map(|p| (p.id, Vec::new())).collect();
let output_buffers = node.outputs().iter().map(|p| (p.id, Vec::new())).collect();
Self {
node,
input_buffers,
output_buffers,
frames_processed: 0,
}
}
#[must_use]
pub fn node(&self) -> &dyn Node {
self.node.as_ref()
}
pub fn node_mut(&mut self) -> &mut dyn Node {
self.node.as_mut()
}
pub fn push_input(&mut self, port: PortId, frame: FilterFrame) -> GraphResult<()> {
self.input_buffers
.get_mut(&port)
.ok_or(GraphError::PortNotFound {
node: self.node.id(),
port,
})?
.push(frame);
Ok(())
}
pub fn pop_output(&mut self, port: PortId) -> GraphResult<Option<FilterFrame>> {
let buffer = self
.output_buffers
.get_mut(&port)
.ok_or(GraphError::PortNotFound {
node: self.node.id(),
port,
})?;
Ok(if buffer.is_empty() {
None
} else {
Some(buffer.remove(0))
})
}
pub fn process(&mut self) -> GraphResult<()> {
let input = self.input_buffers.values_mut().find_map(|buf| {
if buf.is_empty() {
None
} else {
Some(buf.remove(0))
}
});
if let Some(output) = self.node.process(input)? {
if let Some(buf) = self.output_buffers.values_mut().next() {
buf.push(output);
}
self.frames_processed += 1;
}
Ok(())
}
#[must_use]
pub fn frames_processed(&self) -> u64 {
self.frames_processed
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_node_id_display() {
let id = NodeId(42);
assert_eq!(format!("{id}"), "Node(42)");
}
#[test]
fn test_node_type_display() {
assert_eq!(format!("{}", NodeType::Source), "Source");
assert_eq!(format!("{}", NodeType::Filter), "Filter");
assert_eq!(format!("{}", NodeType::Sink), "Sink");
}
#[test]
fn test_node_state_transitions() {
let state = NodeState::Idle;
assert!(state.can_transition_to(NodeState::Processing));
assert!(state.can_transition_to(NodeState::Done));
assert!(state.can_transition_to(NodeState::Error));
let state = NodeState::Processing;
assert!(state.can_transition_to(NodeState::Idle));
assert!(state.can_transition_to(NodeState::Done));
let state = NodeState::Done;
assert!(state.can_transition_to(NodeState::Idle));
assert!(!state.can_transition_to(NodeState::Processing));
}
#[test]
fn test_node_config() {
let config = NodeConfig::new("test")
.with_type(NodeType::Filter)
.with_option("quality", ConfigValue::Int(80))
.with_option("name", ConfigValue::String("test".into()));
assert_eq!(config.name, "test");
assert_eq!(config.node_type, Some(NodeType::Filter));
assert_eq!(config.get_int("quality"), Some(80));
assert_eq!(config.get_string("name"), Some("test"));
}
#[test]
fn test_config_value_conversions() {
let int_val = ConfigValue::Int(42);
assert_eq!(int_val.as_int(), Some(42));
assert_eq!(int_val.as_float(), Some(42.0));
assert_eq!(int_val.as_string(), None);
let float_val = ConfigValue::Float(3.14);
assert_eq!(float_val.as_float(), Some(3.14));
assert_eq!(float_val.as_int(), None);
let str_val = ConfigValue::String("hello".into());
assert_eq!(str_val.as_string(), Some("hello"));
let bool_val = ConfigValue::Bool(true);
assert_eq!(bool_val.as_bool(), Some(true));
}
#[test]
fn test_config_value_from() {
let _: ConfigValue = 42i64.into();
let _: ConfigValue = 3.14f64.into();
let _: ConfigValue = "test".into();
let _: ConfigValue = String::from("test").into();
let _: ConfigValue = true.into();
}
}