cfgmatic_source/domain/
source.rs1use serde::{Deserialize, Serialize};
38use std::path::PathBuf;
39
40use super::{Format, RawContent, Result};
41
42#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
46#[serde(rename_all = "lowercase")]
47pub enum SourceKind {
48 File,
50
51 Env,
53
54 Remote,
56
57 Memory,
59
60 Custom,
62}
63
64impl SourceKind {
65 #[must_use]
67 pub const fn as_str(&self) -> &'static str {
68 match self {
69 Self::File => "file",
70 Self::Env => "env",
71 Self::Remote => "remote",
72 Self::Memory => "memory",
73 Self::Custom => "custom",
74 }
75 }
76
77 #[must_use]
79 pub const fn is_async(&self) -> bool {
80 matches!(self, Self::Remote)
81 }
82}
83
84impl std::fmt::Display for SourceKind {
85 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86 write!(f, "{}", self.as_str())
87 }
88}
89
90#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct SourceMetadata {
95 pub name: String,
97
98 pub path: Option<PathBuf>,
100
101 pub url: Option<String>,
103
104 pub env_var: Option<String>,
106
107 pub priority: i32,
109
110 pub optional: bool,
112
113 pub labels: Vec<String>,
115}
116
117impl SourceMetadata {
118 #[must_use]
120 pub fn new(name: impl Into<String>) -> Self {
121 Self {
122 name: name.into(),
123 path: None,
124 url: None,
125 env_var: None,
126 priority: 0,
127 optional: false,
128 labels: Vec::new(),
129 }
130 }
131
132 #[must_use]
134 pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
135 self.path = Some(path.into());
136 self
137 }
138
139 #[must_use]
141 pub fn with_url(mut self, url: impl Into<String>) -> Self {
142 self.url = Some(url.into());
143 self
144 }
145
146 #[must_use]
148 pub fn with_env_var(mut self, env_var: impl Into<String>) -> Self {
149 self.env_var = Some(env_var.into());
150 self
151 }
152
153 #[must_use]
155 pub fn with_priority(mut self, priority: i32) -> Self {
156 self.priority = priority;
157 self
158 }
159
160 #[must_use]
162 pub fn with_optional(mut self, optional: bool) -> Self {
163 self.optional = optional;
164 self
165 }
166
167 #[must_use]
169 pub fn with_label(mut self, label: impl Into<String>) -> Self {
170 self.labels.push(label.into());
171 self
172 }
173
174 #[must_use]
176 pub fn display_id(&self) -> String {
177 if let Some(ref path) = self.path {
178 format!("{}:{}", self.name, path.display())
179 } else if let Some(ref url) = self.url {
180 format!("{}:{}", self.name, url)
181 } else if let Some(ref env_var) = self.env_var {
182 format!("{}:{}", self.name, env_var)
183 } else {
184 self.name.clone()
185 }
186 }
187}
188
189impl Default for SourceMetadata {
190 fn default() -> Self {
191 Self::new("unnamed")
192 }
193}
194
195pub trait Source: Send + Sync + 'static {
235 fn kind(&self) -> SourceKind;
237
238 fn metadata(&self) -> SourceMetadata;
240
241 fn load_raw(&self) -> Result<RawContent>;
247
248 fn detect_format(&self) -> Option<Format>;
252
253 fn validate(&self) -> Result<()> {
261 Ok(())
262 }
263
264 #[must_use]
268 fn is_required(&self) -> bool {
269 !self.metadata().optional
270 }
271
272 #[must_use]
276 fn is_optional(&self) -> bool {
277 self.metadata().optional
278 }
279
280 #[must_use]
282 fn display_name(&self) -> String {
283 let meta = self.metadata();
284 format!("{}:{}", self.kind(), meta.name)
285 }
286}
287
288#[cfg(test)]
289mod tests {
290 use super::*;
291
292 #[test]
293 fn test_source_kind_as_str() {
294 assert_eq!(SourceKind::File.as_str(), "file");
295 assert_eq!(SourceKind::Env.as_str(), "env");
296 assert_eq!(SourceKind::Remote.as_str(), "remote");
297 }
298
299 #[test]
300 fn test_source_kind_is_async() {
301 assert!(!SourceKind::File.is_async());
302 assert!(SourceKind::Remote.is_async());
303 }
304
305 #[test]
306 fn test_source_kind_display() {
307 assert_eq!(format!("{}", SourceKind::File), "file");
308 }
309
310 #[test]
311 fn test_source_metadata_new() {
312 let meta = SourceMetadata::new("test");
313 assert_eq!(meta.name, "test");
314 assert!(meta.path.is_none());
315 assert_eq!(meta.priority, 0);
316 }
317
318 #[test]
319 fn test_source_metadata_builders() {
320 let meta = SourceMetadata::new("test")
321 .with_path("/etc/config.toml")
322 .with_priority(10)
323 .with_optional(true)
324 .with_label("production");
325
326 assert_eq!(meta.path.unwrap().to_str(), Some("/etc/config.toml"));
327 assert_eq!(meta.priority, 10);
328 assert!(meta.optional);
329 assert!(meta.labels.contains(&"production".to_string()));
330 }
331
332 #[test]
333 fn test_source_metadata_display_id() {
334 let meta = SourceMetadata::new("test").with_path("/config.toml");
335 assert_eq!(meta.display_id(), "test:/config.toml");
336
337 let meta = SourceMetadata::new("test").with_url("https://example.com/config");
338 assert_eq!(meta.display_id(), "test:https://example.com/config");
339 }
340
341 #[test]
342 fn test_source_metadata_serialization() {
343 let meta = SourceMetadata::new("test").with_priority(5);
344 let json = serde_json::to_string(&meta).unwrap();
345 let decoded: SourceMetadata = serde_json::from_str(&json).unwrap();
346 assert_eq!(meta, decoded);
347 }
348
349 #[test]
350 fn test_source_kind_serialization() {
351 let kind = SourceKind::File;
352 let json = serde_json::to_string(&kind).unwrap();
353 assert_eq!(json, "\"file\"");
354 }
355}