use serde::{Deserialize, Serialize};
use std::path::PathBuf;
use super::{Format, RawContent, Result};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum SourceKind {
File,
Env,
Remote,
Memory,
Custom,
}
impl SourceKind {
#[must_use]
pub const fn as_str(&self) -> &'static str {
match self {
Self::File => "file",
Self::Env => "env",
Self::Remote => "remote",
Self::Memory => "memory",
Self::Custom => "custom",
}
}
#[must_use]
pub const fn is_async(&self) -> bool {
matches!(self, Self::Remote)
}
}
impl std::fmt::Display for SourceKind {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.as_str())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SourceMetadata {
pub name: String,
pub path: Option<PathBuf>,
pub url: Option<String>,
pub env_var: Option<String>,
pub priority: i32,
pub optional: bool,
pub labels: Vec<String>,
}
impl SourceMetadata {
#[must_use]
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
path: None,
url: None,
env_var: None,
priority: 0,
optional: false,
labels: Vec::new(),
}
}
#[must_use]
pub fn with_path(mut self, path: impl Into<PathBuf>) -> Self {
self.path = Some(path.into());
self
}
#[must_use]
pub fn with_url(mut self, url: impl Into<String>) -> Self {
self.url = Some(url.into());
self
}
#[must_use]
pub fn with_env_var(mut self, env_var: impl Into<String>) -> Self {
self.env_var = Some(env_var.into());
self
}
#[must_use]
pub fn with_priority(mut self, priority: i32) -> Self {
self.priority = priority;
self
}
#[must_use]
pub fn with_optional(mut self, optional: bool) -> Self {
self.optional = optional;
self
}
#[must_use]
pub fn with_label(mut self, label: impl Into<String>) -> Self {
self.labels.push(label.into());
self
}
#[must_use]
pub fn display_id(&self) -> String {
if let Some(ref path) = self.path {
format!("{}:{}", self.name, path.display())
} else if let Some(ref url) = self.url {
format!("{}:{}", self.name, url)
} else if let Some(ref env_var) = self.env_var {
format!("{}:{}", self.name, env_var)
} else {
self.name.clone()
}
}
}
impl Default for SourceMetadata {
fn default() -> Self {
Self::new("unnamed")
}
}
pub trait Source: Send + Sync + 'static {
fn kind(&self) -> SourceKind;
fn metadata(&self) -> SourceMetadata;
fn load_raw(&self) -> Result<RawContent>;
fn detect_format(&self) -> Option<Format>;
fn validate(&self) -> Result<()> {
Ok(())
}
#[must_use]
fn is_required(&self) -> bool {
!self.metadata().optional
}
#[must_use]
fn is_optional(&self) -> bool {
self.metadata().optional
}
#[must_use]
fn display_name(&self) -> String {
let meta = self.metadata();
format!("{}:{}", self.kind(), meta.name)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_kind_as_str() {
assert_eq!(SourceKind::File.as_str(), "file");
assert_eq!(SourceKind::Env.as_str(), "env");
assert_eq!(SourceKind::Remote.as_str(), "remote");
}
#[test]
fn test_source_kind_is_async() {
assert!(!SourceKind::File.is_async());
assert!(SourceKind::Remote.is_async());
}
#[test]
fn test_source_kind_display() {
assert_eq!(format!("{}", SourceKind::File), "file");
}
#[test]
fn test_source_metadata_new() {
let meta = SourceMetadata::new("test");
assert_eq!(meta.name, "test");
assert!(meta.path.is_none());
assert_eq!(meta.priority, 0);
}
#[test]
fn test_source_metadata_builders() {
let meta = SourceMetadata::new("test")
.with_path("/etc/config.toml")
.with_priority(10)
.with_optional(true)
.with_label("production");
assert_eq!(meta.path.unwrap().to_str(), Some("/etc/config.toml"));
assert_eq!(meta.priority, 10);
assert!(meta.optional);
assert!(meta.labels.contains(&"production".to_string()));
}
#[test]
fn test_source_metadata_display_id() {
let meta = SourceMetadata::new("test").with_path("/config.toml");
assert_eq!(meta.display_id(), "test:/config.toml");
let meta = SourceMetadata::new("test").with_url("https://example.com/config");
assert_eq!(meta.display_id(), "test:https://example.com/config");
}
#[test]
fn test_source_metadata_serialization() {
let meta = SourceMetadata::new("test").with_priority(5);
let json = serde_json::to_string(&meta).unwrap();
let decoded: SourceMetadata = serde_json::from_str(&json).unwrap();
assert_eq!(meta, decoded);
}
#[test]
fn test_source_kind_serialization() {
let kind = SourceKind::File;
let json = serde_json::to_string(&kind).unwrap();
assert_eq!(json, "\"file\"");
}
}