Skip to main content

pixelflow_core/
source.rs

1//! Source request metadata shared by graph construction and source plugins.
2
3use std::collections::BTreeMap;
4
5use crate::{ErrorCategory, ErrorCode, PixelFlowError, Rational, Result};
6
7/// Script-provided scalar option for a source request.
8#[derive(Clone, Debug, Eq, PartialEq)]
9pub enum SourceOptionValue {
10    /// String option.
11    String(String),
12    /// Boolean option.
13    Bool(bool),
14    /// Integer option.
15    Int(i64),
16    /// Rational option.
17    Rational(Rational),
18}
19
20/// Lazy source request captured during graph construction.
21#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct SourceRequest {
23    path: String,
24    options: BTreeMap<String, SourceOptionValue>,
25}
26
27impl SourceRequest {
28    /// Creates source request for user-provided media path.
29    #[must_use]
30    pub fn new(path: impl Into<String>) -> Self {
31        Self {
32            path: path.into(),
33            options: BTreeMap::new(),
34        }
35    }
36
37    /// Returns user-provided media path.
38    #[must_use]
39    pub fn path(&self) -> &str {
40        &self.path
41    }
42
43    /// Returns source options sorted by option name.
44    #[must_use]
45    pub const fn options(&self) -> &BTreeMap<String, SourceOptionValue> {
46        &self.options
47    }
48
49    /// Adds validated source option and returns updated request.
50    pub fn try_with_option(
51        mut self,
52        name: impl Into<String>,
53        value: SourceOptionValue,
54    ) -> Result<Self> {
55        let name = name.into();
56        validate_option_name(&name)?;
57        self.options.insert(name, value);
58        Ok(self)
59    }
60
61    /// Adds option for tests and internal construction where name is known valid.
62    #[must_use]
63    pub fn with_option(mut self, name: impl Into<String>, value: SourceOptionValue) -> Self {
64        let name = name.into();
65        debug_assert!(is_option_name(&name));
66        self.options.insert(name, value);
67        self
68    }
69}
70
71fn validate_option_name(name: &str) -> Result<()> {
72    if is_option_name(name) {
73        return Ok(());
74    }
75
76    Err(PixelFlowError::new(
77        ErrorCategory::Source,
78        ErrorCode::new("source.invalid_option"),
79        format!("invalid source option name '{name}'"),
80    ))
81}
82
83fn is_option_name(name: &str) -> bool {
84    let mut bytes = name.bytes();
85
86    matches!(bytes.next(), Some(first) if first.is_ascii_alphabetic() || first == b'_')
87        && bytes.all(|byte| byte.is_ascii_alphanumeric() || byte == b'_')
88}
89
90#[cfg(test)]
91mod tests {
92    use crate::{ErrorCategory, ErrorCode, Rational};
93
94    use super::{SourceOptionValue, SourceRequest};
95
96    #[test]
97    fn source_request_rejects_invalid_option_name() {
98        let error = SourceRequest::new("input.mkv")
99            .try_with_option(
100                "bad-name",
101                SourceOptionValue::Rational(Rational {
102                    numerator: 30_000,
103                    denominator: 1_001,
104                }),
105            )
106            .expect_err("invalid option name should fail");
107
108        assert_eq!(error.category(), ErrorCategory::Source);
109        assert_eq!(error.code(), ErrorCode::new("source.invalid_option"));
110    }
111}