lib

Core libraries for Radroots
git clone https://radroots.dev/git/lib.git
Log | Files | Refs | README | LICENSE

registry.rs (10146B)


      1 use std::fs;
      2 use std::path::Path;
      3 
      4 use crate::error::RadrootsRuntimeManagerError;
      5 use crate::model::{ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry};
      6 
      7 pub fn load_registry(
      8     path: impl AsRef<Path>,
      9 ) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> {
     10     load_registry_path(path.as_ref())
     11 }
     12 
     13 fn load_registry_path(
     14     path: &Path,
     15 ) -> Result<ManagedRuntimeInstanceRegistry, RadrootsRuntimeManagerError> {
     16     let raw = match fs::read_to_string(path) {
     17         Ok(raw) => raw,
     18         Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
     19             return Ok(ManagedRuntimeInstanceRegistry::default());
     20         }
     21         Err(source) => {
     22             return Err(RadrootsRuntimeManagerError::ReadRegistry {
     23                 path: path.to_path_buf(),
     24                 source,
     25             });
     26         }
     27     };
     28 
     29     toml::from_str::<ManagedRuntimeInstanceRegistry>(&raw).map_err(|source| {
     30         RadrootsRuntimeManagerError::ParseRegistry {
     31             path: path.to_path_buf(),
     32             details: source.to_string(),
     33         }
     34     })
     35 }
     36 
     37 pub fn save_registry(
     38     path: impl AsRef<Path>,
     39     registry: &ManagedRuntimeInstanceRegistry,
     40 ) -> Result<(), RadrootsRuntimeManagerError> {
     41     save_registry_path(path.as_ref(), registry)
     42 }
     43 
     44 fn save_registry_path(
     45     path: &Path,
     46     registry: &ManagedRuntimeInstanceRegistry,
     47 ) -> Result<(), RadrootsRuntimeManagerError> {
     48     save_registry_path_with(path, registry, toml::to_string_pretty)
     49 }
     50 
     51 fn save_registry_path_with(
     52     path: &Path,
     53     registry: &ManagedRuntimeInstanceRegistry,
     54     serializer: fn(&ManagedRuntimeInstanceRegistry) -> Result<String, toml::ser::Error>,
     55 ) -> Result<(), RadrootsRuntimeManagerError> {
     56     ensure_registry_parent(path)?;
     57 
     58     let raw = serializer(registry)
     59         .map_err(|err| RadrootsRuntimeManagerError::SerializeRegistry(err.to_string()))?;
     60     fs::write(path, raw).map_err(|source| RadrootsRuntimeManagerError::WriteRegistry {
     61         path: path.to_path_buf(),
     62         source,
     63     })
     64 }
     65 
     66 pub fn upsert_instance(
     67     registry: &mut ManagedRuntimeInstanceRegistry,
     68     record: ManagedRuntimeInstanceRecord,
     69 ) {
     70     if let Some(existing) = registry.instances.iter_mut().find(|existing| {
     71         existing.runtime_id == record.runtime_id && existing.instance_id == record.instance_id
     72     }) {
     73         *existing = record;
     74     } else {
     75         registry.instances.push(record);
     76         registry.instances.sort_by(|left, right| {
     77             left.runtime_id
     78                 .cmp(&right.runtime_id)
     79                 .then_with(|| left.instance_id.cmp(&right.instance_id))
     80         });
     81     }
     82 }
     83 
     84 pub fn instance<'a>(
     85     registry: &'a ManagedRuntimeInstanceRegistry,
     86     runtime_id: &str,
     87     instance_id: &str,
     88 ) -> Option<&'a ManagedRuntimeInstanceRecord> {
     89     registry
     90         .instances
     91         .iter()
     92         .find(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)
     93 }
     94 
     95 pub fn remove_instance(
     96     registry: &mut ManagedRuntimeInstanceRegistry,
     97     runtime_id: &str,
     98     instance_id: &str,
     99 ) -> Option<ManagedRuntimeInstanceRecord> {
    100     let index = registry
    101         .instances
    102         .iter()
    103         .position(|record| record.runtime_id == runtime_id && record.instance_id == instance_id)?;
    104     Some(registry.instances.remove(index))
    105 }
    106 
    107 fn ensure_registry_parent(path: &Path) -> Result<(), RadrootsRuntimeManagerError> {
    108     let Some(parent) = path.parent() else {
    109         return Ok(());
    110     };
    111     if parent.as_os_str().is_empty() {
    112         return Ok(());
    113     }
    114     fs::create_dir_all(parent).map_err(|source| RadrootsRuntimeManagerError::CreateRegistryParent {
    115         path: parent.to_path_buf(),
    116         source,
    117     })
    118 }
    119 
    120 #[cfg(test)]
    121 mod tests {
    122     use std::fs;
    123     use std::path::{Path, PathBuf};
    124 
    125     use serde::ser::Error as _;
    126     use tempfile::tempdir;
    127 
    128     use super::{
    129         ensure_registry_parent, instance, load_registry, remove_instance, save_registry,
    130         save_registry_path_with, upsert_instance,
    131     };
    132     use crate::{
    133         ManagedRuntimeInstallState, ManagedRuntimeInstanceRecord, ManagedRuntimeInstanceRegistry,
    134         RadrootsRuntimeManagerError,
    135     };
    136 
    137     fn sample_record(runtime_id: &str, instance_id: &str) -> ManagedRuntimeInstanceRecord {
    138         ManagedRuntimeInstanceRecord {
    139             runtime_id: runtime_id.to_string(),
    140             instance_id: instance_id.to_string(),
    141             management_mode: "interactive_user_managed".to_string(),
    142             install_state: ManagedRuntimeInstallState::Configured,
    143             binary_path: PathBuf::from("/tmp/radrootsd"),
    144             config_path: PathBuf::from("/tmp/config.toml"),
    145             logs_path: PathBuf::from("/tmp/logs"),
    146             run_path: PathBuf::from("/tmp/run"),
    147             installed_version: "0.1.0-alpha.2".to_string(),
    148             health_endpoint: Some("jsonrpc_status".to_string()),
    149             secret_material_ref: None,
    150             last_started_at: None,
    151             last_stopped_at: None,
    152             notes: Some("test".to_string()),
    153         }
    154     }
    155 
    156     fn assert_error_contains(err: &RadrootsRuntimeManagerError, parts: &[&str]) {
    157         let rendered = err.to_string();
    158         for part in parts {
    159             assert!(
    160                 rendered.contains(part),
    161                 "expected `{rendered}` to contain `{part}`"
    162             );
    163         }
    164     }
    165 
    166     #[test]
    167     fn load_registry_returns_default_when_file_is_missing() {
    168         let dir = tempdir().expect("tempdir");
    169         let registry = load_registry(dir.path().join("missing.toml")).expect("missing registry");
    170         assert_eq!(registry, ManagedRuntimeInstanceRegistry::default());
    171     }
    172 
    173     #[test]
    174     fn load_registry_reports_read_errors() {
    175         let dir = tempdir().expect("tempdir");
    176         let err = load_registry(dir.path()).expect_err("directory should fail");
    177         assert_error_contains(
    178             &err,
    179             &[
    180                 dir.path().to_string_lossy().as_ref(),
    181                 "read runtime instance registry",
    182             ],
    183         );
    184     }
    185 
    186     #[test]
    187     fn load_registry_reports_parse_errors() {
    188         let dir = tempdir().expect("tempdir");
    189         let path = dir.path().join("instances.toml");
    190         fs::write(&path, "not = [valid").expect("write invalid registry");
    191 
    192         let err = load_registry(&path).expect_err("invalid registry should fail");
    193         assert_error_contains(
    194             &err,
    195             &[
    196                 path.to_string_lossy().as_ref(),
    197                 "parse runtime instance registry",
    198             ],
    199         );
    200     }
    201 
    202     #[test]
    203     fn save_registry_reports_write_errors() {
    204         let dir = tempdir().expect("tempdir");
    205         let path = dir.path().join("registry-dir");
    206         fs::create_dir(&path).expect("create directory target");
    207 
    208         let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default())
    209             .expect_err("directory path should fail");
    210         assert_error_contains(
    211             &err,
    212             &[
    213                 path.to_string_lossy().as_ref(),
    214                 "write runtime instance registry",
    215             ],
    216         );
    217     }
    218 
    219     #[test]
    220     fn save_registry_reports_parent_creation_errors() {
    221         let dir = tempdir().expect("tempdir");
    222         let file_parent = dir.path().join("occupied");
    223         fs::write(&file_parent, "file").expect("occupied parent");
    224         let path = file_parent.join("instances.toml");
    225 
    226         let err = save_registry(&path, &ManagedRuntimeInstanceRegistry::default())
    227             .expect_err("file parent should fail");
    228         assert_error_contains(
    229             &err,
    230             &[
    231                 file_parent.to_string_lossy().as_ref(),
    232                 "create runtime instance registry parent",
    233             ],
    234         );
    235     }
    236 
    237     #[test]
    238     fn save_registry_reports_serializer_errors() {
    239         let dir = tempdir().expect("tempdir");
    240         let path = dir.path().join("instances.toml");
    241 
    242         let err =
    243             save_registry_path_with(&path, &ManagedRuntimeInstanceRegistry::default(), |_| {
    244                 Err(toml::ser::Error::custom(
    245                     "forced registry serializer failure",
    246                 ))
    247             })
    248             .expect_err("serializer should fail");
    249 
    250         assert_error_contains(
    251             &err,
    252             &[
    253                 "serialize runtime instance registry",
    254                 "forced registry serializer failure",
    255             ],
    256         );
    257     }
    258 
    259     #[test]
    260     fn ensure_registry_parent_accepts_parentless_relative_paths() {
    261         ensure_registry_parent(Path::new("instances.toml")).expect("relative path parentless");
    262         ensure_registry_parent(Path::new("/")).expect("root path parentless");
    263     }
    264 
    265     #[test]
    266     fn upsert_instance_replaces_existing_and_sorts_new_records() {
    267         let mut registry = ManagedRuntimeInstanceRegistry::default();
    268         upsert_instance(&mut registry, sample_record("radrootsd", "b"));
    269         upsert_instance(&mut registry, sample_record("radrootsd", "a"));
    270         upsert_instance(&mut registry, sample_record("myc", "a"));
    271 
    272         let mut replacement = sample_record("radrootsd", "b");
    273         replacement.installed_version = "0.2.0".to_string();
    274         upsert_instance(&mut registry, replacement);
    275 
    276         assert_eq!(registry.instances.len(), 3);
    277         assert_eq!(registry.instances[0].runtime_id, "myc");
    278         assert_eq!(registry.instances[1].instance_id, "a");
    279         assert_eq!(registry.instances[2].runtime_id, "radrootsd");
    280         assert_eq!(registry.instances[2].instance_id, "b");
    281         assert_eq!(registry.instances[2].installed_version, "0.2.0");
    282     }
    283 
    284     #[test]
    285     fn instance_and_remove_instance_handle_missing_and_present_rows() {
    286         let mut registry = ManagedRuntimeInstanceRegistry::default();
    287         upsert_instance(&mut registry, sample_record("radrootsd", "local"));
    288 
    289         assert!(instance(&registry, "myc", "local").is_none());
    290         assert!(remove_instance(&mut registry, "myc", "local").is_none());
    291 
    292         let removed = remove_instance(&mut registry, "radrootsd", "local").expect("remove");
    293         assert_eq!(removed.runtime_id, "radrootsd");
    294         assert!(registry.instances.is_empty());
    295     }
    296 }