use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Manifest {
#[serde(default = "default_version")]
pub version: String,
#[serde(default)]
pub discovery: DiscoveryConfig,
#[serde(default)]
pub policy: PolicyConfig,
pub services: Vec<ServiceEntry>,
}
impl Manifest {
pub fn load(path: &Path) -> Result<Self> {
let text = std::fs::read_to_string(path)
.with_context(|| format!("cannot read manifest: {}", path.display()))?;
let manifest: Self = serde_yaml::from_str(&text)
.with_context(|| format!("cannot parse manifest: {}", path.display()))?;
Ok(manifest)
}
pub fn effective_discovery_paths(&self) -> Vec<String> {
if self.discovery.paths.is_empty() {
DEFAULT_DISCOVERY_PATHS
.iter()
.map(|s| s.to_string())
.collect()
} else {
self.discovery.paths.clone()
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct PolicyConfig {
#[serde(default)]
pub require_fields: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct DiscoveryConfig {
#[serde(default)]
pub paths: Vec<String>,
#[serde(default = "default_markers")]
pub markers: Vec<String>,
#[serde(default)]
pub ignore: Vec<String>,
}
pub const DEFAULT_DISCOVERY_PATHS: &[&str] =
&["services/*", "microservices/*", "apps/*", "packages/*"];
fn default_markers() -> Vec<String> {
default_markers_pub()
}
pub fn default_markers_pub() -> Vec<String> {
[
"Cargo.toml",
"Dockerfile",
"go.mod",
"package.json",
"pyproject.toml",
"requirements.txt",
]
.iter()
.map(|s| s.to_string())
.collect()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ServiceEntry {
pub name: String,
pub language: Option<String>,
pub platform: Option<String>,
pub url: Option<String>,
pub role: Option<String>,
pub team: Option<String>,
pub oncall: Option<String>,
pub submodule: Option<String>,
pub path: Option<String>,
pub docs: Option<String>,
pub ci: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub depends_on: Vec<String>,
}
impl ServiceEntry {
pub fn declared_path(&self) -> Option<&str> {
self.path.as_deref().or(self.submodule.as_deref())
}
}
fn default_version() -> String {
"1".to_string()
}
pub fn find_default(root: &Path) -> PathBuf {
for name in &["svccat.yaml", "svccat.yml", "services.yaml", "services.yml"] {
let p = root.join(name);
if p.exists() {
return p;
}
}
root.join("services.yaml")
}