pks-session 0.1.0

Session declaration and lifecycle composition for PocketStation
use pks_capture::{SourceKind, StableSourceId};

use crate::SessionError;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ProcessId(u32);

impl ProcessId {
    pub const fn new(process_id: u32) -> Self {
        Self(process_id)
    }

    pub const fn get(self) -> u32 {
        self.0
    }
}

#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct DeviceId(String);

impl DeviceId {
    pub fn new(device_id: impl Into<String>) -> Self {
        Self(device_id.into())
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ApplicationSelector {
    BundleId(String),
    ProcessId(ProcessId),
    ProcessInstance {
        process_id: ProcessId,
        stable_id: StableSourceId,
    },
    StableId(StableSourceId),
    Name(String),
}

impl ApplicationSelector {
    pub fn bundle_id(bundle_id: impl Into<String>) -> Self {
        Self::BundleId(bundle_id.into())
    }

    pub const fn process_id(process_id: ProcessId) -> Self {
        Self::ProcessId(process_id)
    }

    pub fn process_instance(process_id: ProcessId, stable_id: StableSourceId) -> Self {
        Self::ProcessInstance {
            process_id,
            stable_id,
        }
    }

    pub fn stable_id(source_id: StableSourceId) -> Self {
        Self::StableId(source_id)
    }

    pub fn name(name: impl Into<String>) -> Self {
        Self::Name(name.into())
    }

    pub(crate) fn validate(&self) -> Result<(), SessionError> {
        match self {
            Self::BundleId(bundle_id) if bundle_id.trim().is_empty() => {
                Err(SessionError::InvalidSelector {
                    reason: "application bundle id cannot be empty".to_owned(),
                })
            }
            Self::ProcessId(process_id) if process_id.get() == 0 => {
                Err(SessionError::InvalidSelector {
                    reason: "application process id must be non-zero".to_owned(),
                })
            }
            Self::ProcessInstance { process_id, .. } if process_id.get() == 0 => {
                Err(SessionError::InvalidSelector {
                    reason: "application process instance id must be non-zero".to_owned(),
                })
            }
            Self::ProcessInstance { stable_id, .. } | Self::StableId(stable_id)
                if stable_id.kind != SourceKind::Application =>
            {
                Err(SessionError::InvalidSelector {
                    reason: "application stable id must identify an application".to_owned(),
                })
            }
            Self::ProcessInstance { stable_id, .. } | Self::StableId(stable_id)
                if stable_id.stable_key.trim().is_empty() =>
            {
                Err(SessionError::InvalidSelector {
                    reason: "application stable id cannot be empty".to_owned(),
                })
            }
            Self::Name(name) if name.trim().is_empty() => Err(SessionError::InvalidSelector {
                reason: "application name cannot be empty".to_owned(),
            }),
            _ => Ok(()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeviceSelector {
    Default,
    Id(DeviceId),
}

impl DeviceSelector {
    pub const fn default() -> Self {
        Self::Default
    }

    pub fn id(device_id: DeviceId) -> Self {
        Self::Id(device_id)
    }

    pub(crate) fn validate(&self) -> Result<(), SessionError> {
        match self {
            Self::Id(device_id) if device_id.as_str().trim().is_empty() => {
                Err(SessionError::InvalidSelector {
                    reason: "microphone device id cannot be empty".to_owned(),
                })
            }
            _ => Ok(()),
        }
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Source {
    Application(ApplicationSelector),
    Microphone(DeviceSelector),
}

impl Source {
    pub fn application(selector: ApplicationSelector) -> Self {
        Self::Application(selector)
    }

    pub fn microphone(selector: DeviceSelector) -> Self {
        Self::Microphone(selector)
    }

    pub const fn microphone_default() -> Self {
        Self::Microphone(DeviceSelector::default())
    }

    pub(crate) fn validate(&self) -> Result<(), SessionError> {
        match self {
            Self::Application(selector) => selector.validate(),
            Self::Microphone(selector) => selector.validate(),
        }
    }
}

#[cfg(test)]
mod tests {
    use pks_capture::{SourceKind, StableSourceId};
    use pks_frame::Platform;

    use super::{ApplicationSelector, ProcessId};
    use crate::SessionError;

    #[test]
    fn given_zero_pid_when_process_instance_validated_then_selector_is_rejected() {
        let selector = ApplicationSelector::process_instance(
            ProcessId::new(0),
            StableSourceId::new(
                Platform::Windows,
                SourceKind::Application,
                "wasapi:pid:0:creation-100ns:133801234567890000",
            ),
        );

        assert!(matches!(
            selector.validate(),
            Err(SessionError::InvalidSelector { .. })
        ));
    }

    #[test]
    fn given_non_application_identity_when_process_instance_validated_then_selector_is_rejected() {
        let selector = ApplicationSelector::process_instance(
            ProcessId::new(42),
            StableSourceId::new(
                Platform::Windows,
                SourceKind::InputDevice,
                "wasapi:pid:42:creation-100ns:133801234567890000",
            ),
        );

        assert!(matches!(
            selector.validate(),
            Err(SessionError::InvalidSelector { .. })
        ));
    }

    #[test]
    fn given_empty_stable_key_when_process_instance_validated_then_selector_is_rejected() {
        let selector = ApplicationSelector::process_instance(
            ProcessId::new(42),
            StableSourceId::new(Platform::Windows, SourceKind::Application, " "),
        );

        assert!(matches!(
            selector.validate(),
            Err(SessionError::InvalidSelector { .. })
        ));
    }
}