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)]
96pub struct SourceMetadata {
97 pub name: String,
99
100 pub path: Option<PathBuf>,
102
103 pub url: Option<String>,
105
106 pub env_var: Option<String>,
108
109 pub priority: i32,
111
112 pub optional: bool,
114
115 pub labels: Vec<String>,
117}
118
119impl SourceMetadata {
120 #[must_use]
122 pub fn new(name: impl Into<String>) -> Self {
123 Self {
124 name: name.into(),
125 path: None,
126 url: None,
127 env_var: None,
128 priority: 0,
129 optional: false,
130 labels: Vec::new(),
131 }
132 }
133
134 #[must_use]
136 pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
137 self.path = Some(path.into());
138 self
139 }
140
141 #[must_use]
143 pub fn with_url(mut self, url: impl Into<String>) -> Self {
144 self.url = Some(url.into());
145 self
146 }
147
148 #[must_use]
150 pub fn with_env_var(mut self, env_var: impl Into<String>) -> Self {
151 self.env_var = Some(env_var.into());
152 self
153 }
154
155 #[must_use]
157 pub const fn with_priority(mut self, priority: i32) -> Self {
158 self.priority = priority;
159 self
160 }
161
162 #[must_use]
164 pub const fn with_optional(mut self, optional: bool) -> Self {
165 self.optional = optional;
166 self
167 }
168
169 #[must_use]
171 pub fn with_label(mut self, label: impl Into<String>) -> Self {
172 self.labels.push(label.into());
173 self
174 }
175
176 #[must_use]
178 pub fn display_id(&self) -> String {
179 self.path
180 .as_ref()
181 .map(|path| path.display().to_string())
182 .or_else(|| self.url.clone())
183 .or_else(|| self.env_var.clone())
184 .map_or_else(
185 || self.name.clone(),
186 |target| format!("{}:{target}", self.name),
187 )
188 }
189}
190
191impl Default for SourceMetadata {
192 fn default() -> Self {
193 Self::new("unnamed")
194 }
195}
196
197pub trait Source: Send + Sync + 'static {
238 fn kind(&self) -> SourceKind;
240
241 fn metadata(&self) -> SourceMetadata;
243
244 fn load_raw(&self) -> Result<RawContent>;
250
251 fn detect_format(&self) -> Option<Format>;
255
256 fn validate(&self) -> Result<()> {
264 Ok(())
265 }
266
267 #[must_use]
271 fn is_required(&self) -> bool {
272 !self.metadata().optional
273 }
274
275 #[must_use]
279 fn is_optional(&self) -> bool {
280 self.metadata().optional
281 }
282
283 #[must_use]
285 fn display_name(&self) -> String {
286 let meta = self.metadata();
287 meta.display_id()
288 }
289
290 #[must_use]
292 fn cache_key(&self) -> String {
293 format!("{}::{}", self.kind(), self.metadata().display_id())
294 }
295}
296
297#[cfg(test)]
298mod tests {
299 use super::*;
300
301 #[test]
302 fn test_source_kind_as_str() {
303 assert_eq!(SourceKind::File.as_str(), "file");
304 assert_eq!(SourceKind::Env.as_str(), "env");
305 assert_eq!(SourceKind::Remote.as_str(), "remote");
306 }
307
308 #[test]
309 fn test_source_kind_is_async() {
310 assert!(!SourceKind::File.is_async());
311 assert!(SourceKind::Remote.is_async());
312 }
313
314 #[test]
315 fn test_source_kind_display() {
316 assert_eq!(format!("{}", SourceKind::File), "file");
317 }
318
319 #[test]
320 fn test_source_metadata_new() {
321 let meta = SourceMetadata::new("test");
322 assert_eq!(meta.name, "test");
323 assert!(meta.path.is_none());
324 assert_eq!(meta.priority, 0);
325 }
326
327 #[test]
328 fn test_source_metadata_builders() {
329 let meta = SourceMetadata::new("test")
330 .with_path("/etc/config.toml")
331 .with_priority(10)
332 .with_optional(true)
333 .with_label("production");
334
335 assert_eq!(meta.path.unwrap().to_str(), Some("/etc/config.toml"));
336 assert_eq!(meta.priority, 10);
337 assert!(meta.optional);
338 assert!(meta.labels.contains(&"production".to_string()));
339 }
340
341 #[test]
342 fn test_source_metadata_display_id() {
343 let meta = SourceMetadata::new("test").with_path("/config.toml");
344 assert_eq!(meta.display_id(), "test:/config.toml");
345
346 let meta = SourceMetadata::new("test").with_url("https://example.com/config");
347 assert_eq!(meta.display_id(), "test:https://example.com/config");
348 }
349
350 #[test]
351 fn test_source_metadata_serialization() {
352 let meta = SourceMetadata::new("test").with_priority(5);
353 let json = serde_json::to_string(&meta).unwrap();
354 let decoded: SourceMetadata = serde_json::from_str(&json).unwrap();
355 assert_eq!(meta, decoded);
356 }
357
358 #[test]
359 fn test_source_kind_serialization() {
360 let kind = SourceKind::File;
361 let json = serde_json::to_string(&kind).unwrap();
362 assert_eq!(json, "\"file\"");
363 }
364}