1use std::collections::BTreeMap;
4
5use crate::{ErrorCategory, ErrorCode, PixelFlowError, Rational, Result};
6
7#[derive(Clone, Debug, Eq, PartialEq)]
9pub enum SourceOptionValue {
10 String(String),
12 Bool(bool),
14 Int(i64),
16 Rational(Rational),
18}
19
20#[derive(Clone, Debug, Eq, PartialEq)]
22pub struct SourceRequest {
23 path: String,
24 options: BTreeMap<String, SourceOptionValue>,
25}
26
27impl SourceRequest {
28 #[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 #[must_use]
39 pub fn path(&self) -> &str {
40 &self.path
41 }
42
43 #[must_use]
45 pub const fn options(&self) -> &BTreeMap<String, SourceOptionValue> {
46 &self.options
47 }
48
49 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 #[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}