1use std::{fmt, path::PathBuf};
2
3use bon::Builder;
4use kithara_assets::StoreOptions;
5use kithara_events::EventBus;
6use kithara_net::Headers;
7use kithara_stream::dl::Downloader;
8use tokio_util::sync::CancellationToken;
9use url::Url;
10
11#[derive(Clone, Debug, PartialEq, Eq)]
13pub enum FileSrc {
14 Remote(Url),
16 Local(PathBuf),
18}
19
20impl From<Url> for FileSrc {
21 fn from(url: Url) -> Self {
22 Self::Remote(url)
23 }
24}
25
26impl From<PathBuf> for FileSrc {
27 fn from(path: PathBuf) -> Self {
28 Self::Local(path)
29 }
30}
31
32#[derive(Clone, Builder)]
36#[builder(state_mod(vis = "pub"))]
37#[non_exhaustive]
38pub struct FileConfig {
39 pub src: FileSrc,
41 #[builder(name = events)]
43 pub bus: Option<EventBus>,
44 pub cancel: Option<CancellationToken>,
46 pub downloader: Option<Downloader>,
48 pub headers: Option<Headers>,
50 pub look_ahead_bytes: Option<u64>,
52 pub name: Option<String>,
54 #[builder(default)]
56 pub store: StoreOptions,
57 #[builder(default = kithara_events::DEFAULT_EVENT_BUS_CAPACITY)]
59 pub event_channel_capacity: usize,
60}
61
62impl fmt::Debug for FileConfig {
63 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
64 f.debug_struct("FileConfig")
65 .field("src", &self.src)
66 .field("bus", &self.bus)
67 .field("cancel", &self.cancel)
68 .field("headers", &self.headers)
69 .field("look_ahead_bytes", &self.look_ahead_bytes)
70 .field("name", &self.name)
71 .field("store", &self.store)
72 .field("event_channel_capacity", &self.event_channel_capacity)
73 .finish_non_exhaustive()
74 }
75}
76
77impl Default for FileConfig {
78 fn default() -> Self {
79 let url = Url::parse("http://localhost/audio.mp3").expect("valid default URL");
80 Self::for_src(FileSrc::Remote(url)).build()
81 }
82}
83
84impl FileConfig {
85 #[must_use]
87 pub fn new(src: FileSrc) -> Self {
88 Self::for_src(src).build()
89 }
90
91 pub fn for_src(src: FileSrc) -> FileConfigBuilder<file_config_builder::SetSrc> {
93 Self::builder().src(src)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use std::path::Path;
100
101 use kithara_test_utils::kithara;
102
103 use super::*;
104
105 fn test_src() -> FileSrc {
106 FileSrc::Remote(Url::parse("http://example.com/audio.mp3").unwrap())
107 }
108
109 #[kithara::test]
110 #[case(test_src())]
111 #[case(FileSrc::Local(PathBuf::from("/tmp/song.mp3")))]
112 fn test_file_config_new_preserves_source(#[case] src: FileSrc) {
113 let config = FileConfig::new(src.clone());
114
115 assert_eq!(config.src, src);
116 assert!(config.bus.is_none());
117 assert!(config.cancel.is_none());
118 if let FileSrc::Local(path) = &config.src {
119 assert_eq!(path, Path::new("/tmp/song.mp3"));
120 }
121 }
122
123 #[kithara::test]
124 fn test_with_store() {
125 let store = StoreOptions::default();
126 let config = FileConfig::for_src(test_src()).store(store).build();
127
128 assert!(config.bus.is_none());
129 }
130
131 fn apply_cancel(mut config: FileConfig) -> FileConfig {
132 config.cancel = Some(CancellationToken::new());
133 config
134 }
135
136 fn apply_events(mut config: FileConfig) -> FileConfig {
137 config.bus = Some(EventBus::new(32));
138 config
139 }
140
141 fn apply_headers(mut config: FileConfig) -> FileConfig {
142 let mut headers = Headers::new();
143 headers.insert("Authorization", "Bearer token123");
144 config.headers = Some(headers);
145 config
146 }
147
148 fn has_cancel(config: &FileConfig) -> bool {
149 config.cancel.is_some()
150 }
151
152 fn has_bus(config: &FileConfig) -> bool {
153 config.bus.is_some()
154 }
155
156 fn has_auth_header(config: &FileConfig) -> bool {
157 config.headers.as_ref().and_then(|h| h.get("Authorization")) == Some("Bearer token123")
158 }
159
160 #[kithara::test]
161 #[case(apply_cancel, has_cancel)]
162 #[case(apply_events, has_bus)]
163 #[case(apply_headers, has_auth_header)]
164 fn test_optional_setters_update_expected_field(
165 #[case] apply: fn(FileConfig) -> FileConfig,
166 #[case] check: fn(&FileConfig) -> bool,
167 ) {
168 let config = apply(FileConfig::new(test_src()));
169 assert!(check(&config));
170 }
171
172 #[kithara::test]
173 fn test_builder_chain() {
174 let store = StoreOptions::default();
175 let cancel = CancellationToken::new();
176 let bus = EventBus::new(32);
177
178 let config = FileConfig::for_src(test_src())
179 .store(store)
180 .cancel(cancel.clone())
181 .events(bus)
182 .build();
183
184 assert!(config.cancel.is_some());
185 assert!(config.bus.is_some());
186 }
187
188 #[kithara::test]
189 #[case("stream-a")]
190 #[case("stream-b")]
191 fn test_with_name_sets_name(#[case] name: &str) {
192 let config = FileConfig::for_src(test_src())
193 .name(name.to_string())
194 .build();
195 assert_eq!(config.name.as_deref(), Some(name));
196 }
197
198 #[kithara::test]
199 fn test_debug_impl() {
200 let config = FileConfig::new(test_src());
201 let debug_str = format!("{:?}", config);
202
203 assert!(debug_str.contains("FileConfig"));
204 }
205
206 #[kithara::test]
207 fn test_clone() {
208 let bus = EventBus::new(32);
209 let config = FileConfig::for_src(test_src()).events(bus).build();
210
211 let cloned = config.clone();
212
213 assert!(cloned.bus.is_some());
214 }
215}