Skip to main content

ryra_core/
metadata.rs

1//! Per-install record. Written at `ryra add` time, read by every command
2//! that needs to know how a service was set up. Mirrors the data that used
3//! to live in `# Service-*` quadlet header comments.
4
5use crate::capability::Capability;
6use crate::error::{Error, Result};
7use crate::paths::metadata_path;
8use crate::registry::service_def::AuthKind;
9
10/// Per-install record persisted to `~/.local/share/services/<name>/metadata.toml`.
11///
12/// Exposure isn't stored — it's derived from `url` at read time
13/// (absent = Loopback, `.internal` = Internal, `.ts.net` = Tailscale,
14/// otherwise Public). One source of truth for "where does this
15/// service live."
16#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
17pub struct Metadata {
18    pub registry: String,
19    #[serde(default, skip_serializing_if = "Option::is_none")]
20    pub url: Option<String>,
21    /// Auth kind: `oidc` if `--auth` was used, otherwise absent.
22    #[serde(default, skip_serializing_if = "Option::is_none")]
23    pub auth: Option<AuthKind>,
24    /// Capabilities the service provides — snapshotted from
25    /// `service.toml` at install time so [`crate::list_installed`] can
26    /// answer "is there an installed reverse proxy / OIDC provider /
27    /// SMTP relay / metrics scraper?" without re-reading the registry.
28    #[serde(default, skip_serializing_if = "Vec::is_empty")]
29    pub provides: Vec<Capability>,
30    /// True if `--backup` was passed at `ryra add` time. Drives
31    /// whether `ryra backup run` picks this install up.
32    ///
33    /// Default `false` so an existing install (written by a ryra
34    /// version that pre-dates the backup feature) reads back as
35    /// not-enabled rather than as malformed.
36    #[serde(default, skip_serializing_if = "is_false")]
37    pub backup_enabled: bool,
38}
39
40fn is_false(b: &bool) -> bool {
41    !b
42}
43
44/// Load metadata.toml for an installed service. Returns `None` if the
45/// file doesn't exist (service not installed via this ryra version, or
46/// uninstalled), `Err` if it exists but can't be parsed.
47pub fn load_metadata(service_name: &str) -> Result<Option<Metadata>> {
48    let path = metadata_path(service_name)?;
49    if !path.exists() {
50        return Ok(None);
51    }
52    let content = std::fs::read_to_string(&path).map_err(|source| Error::FileRead {
53        path: path.clone(),
54        source,
55    })?;
56    let meta: Metadata = toml::from_str(&content).map_err(|source| Error::TomlParse {
57        path: path.clone(),
58        source,
59    })?;
60    Ok(Some(meta))
61}
62
63#[cfg(test)]
64mod tests {
65    use super::*;
66
67    #[test]
68    fn backup_enabled_defaults_false_on_legacy_metadata() {
69        // Pre-feature metadata files have no `backup_enabled` key.
70        let toml_src = r#"
71registry = "bundled"
72"#;
73        let meta: Metadata = toml::from_str(toml_src).expect("parse");
74        assert!(!meta.backup_enabled);
75    }
76
77    #[test]
78    fn backup_enabled_round_trips() {
79        let meta = Metadata {
80            registry: "bundled".into(),
81            url: None,
82            auth: None,
83            provides: vec![],
84            backup_enabled: true,
85        };
86        let text = toml::to_string(&meta).expect("serialize");
87        assert!(
88            text.contains("backup_enabled = true"),
89            "serialized form: {text}"
90        );
91        let parsed: Metadata = toml::from_str(&text).expect("parse");
92        assert!(parsed.backup_enabled);
93    }
94
95    #[test]
96    fn backup_enabled_false_is_omitted_from_serialization() {
97        // Reduce visual noise for the common case (every existing
98        // service today): when off, the field shouldn't appear at all.
99        let meta = Metadata {
100            registry: "bundled".into(),
101            url: None,
102            auth: None,
103            provides: vec![],
104            backup_enabled: false,
105        };
106        let text = toml::to_string(&meta).expect("serialize");
107        assert!(!text.contains("backup_enabled"), "got: {text}");
108    }
109}