cli

Command-line interface for Radroots
git clone https://radroots.dev/git/cli.git
Log | Files | Refs | README | LICENSE

paths.rs (9518B)


      1 use std::path::{Path, PathBuf};
      2 
      3 use radroots_runtime_paths::{
      4     DEFAULT_CONFIG_FILE_NAME, RadrootsPathOverrides, RadrootsPathProfile, RadrootsRuntimeNamespace,
      5     default_shared_identity_path,
      6 };
      7 
      8 use crate::runtime::{
      9     RuntimeError,
     10     config::{EnvFileValues, Environment},
     11 };
     12 
     13 const CLI_DEFAULT_PROFILE: &str = "interactive_user";
     14 const CLI_REPO_LOCAL_PROFILE: &str = "repo_local";
     15 const CLI_APP_NAMESPACE_VALUE: &str = "cli";
     16 const SHARED_ACCOUNTS_NAMESPACE_VALUE: &str = "accounts";
     17 const SHARED_IDENTITIES_NAMESPACE_VALUE: &str = "identities";
     18 
     19 pub(crate) const CLI_ALLOWED_PROFILES: &[&str] = &[CLI_DEFAULT_PROFILE, CLI_REPO_LOCAL_PROFILE];
     20 pub(crate) const ENV_CLI_PATHS_PROFILE: &str = "RADROOTS_CLI_PATHS_PROFILE";
     21 pub(crate) const ENV_CLI_PATHS_REPO_LOCAL_ROOT: &str = "RADROOTS_CLI_PATHS_REPO_LOCAL_ROOT";
     22 
     23 #[derive(Debug, Clone, PartialEq, Eq)]
     24 pub struct PathsConfig {
     25     pub profile: String,
     26     pub profile_source: String,
     27     pub allowed_profiles: Vec<String>,
     28     pub root_source: String,
     29     pub repo_local_root: Option<PathBuf>,
     30     pub repo_local_root_source: Option<String>,
     31     pub subordinate_path_override_source: String,
     32     pub app_namespace: String,
     33     pub shared_accounts_namespace: String,
     34     pub shared_identities_namespace: String,
     35     pub app_config_path: PathBuf,
     36     pub workspace_config_path: Option<PathBuf>,
     37     pub app_data_root: PathBuf,
     38     pub app_logs_root: PathBuf,
     39     pub shared_accounts_data_root: PathBuf,
     40     pub shared_accounts_secrets_root: PathBuf,
     41     pub default_identity_path: PathBuf,
     42 }
     43 
     44 pub(crate) fn resolve_paths(
     45     env: &dyn Environment,
     46     env_file: &EnvFileValues,
     47 ) -> Result<PathsConfig, RuntimeError> {
     48     let current_dir = env.current_dir()?;
     49     let resolver = env.path_resolver();
     50     let (profile, profile_label, profile_source) = resolve_cli_path_profile(env, env_file)?;
     51     let override_selection =
     52         resolve_cli_path_overrides(current_dir.as_path(), env, env_file, profile)?;
     53     let workspace_config_path = resolve_workspace_config_path(profile, &override_selection)?;
     54     let resolved = resolver
     55         .resolve(profile, &override_selection.overrides)
     56         .map_err(|err| RuntimeError::Config(format!("resolve Radroots path roots: {err}")))?;
     57     let app_namespace = RadrootsRuntimeNamespace::app(CLI_APP_NAMESPACE_VALUE)
     58         .map_err(|err| RuntimeError::Config(format!("resolve cli namespace: {err}")))?;
     59     let shared_accounts_namespace =
     60         RadrootsRuntimeNamespace::shared(SHARED_ACCOUNTS_NAMESPACE_VALUE).map_err(|err| {
     61             RuntimeError::Config(format!("resolve shared accounts namespace: {err}"))
     62         })?;
     63     let shared_identity_namespace =
     64         RadrootsRuntimeNamespace::shared(SHARED_IDENTITIES_NAMESPACE_VALUE).map_err(|err| {
     65             RuntimeError::Config(format!("resolve shared identities namespace: {err}"))
     66         })?;
     67     let app_paths = resolved.namespaced(&app_namespace);
     68     let shared_accounts_paths = resolved.namespaced(&shared_accounts_namespace);
     69     let default_identity_path =
     70         default_shared_identity_path(&resolver, profile, &override_selection.overrides)
     71             .map_err(|err| RuntimeError::Config(format!("resolve shared identity path: {err}")))?;
     72 
     73     Ok(PathsConfig {
     74         profile: profile_label.to_owned(),
     75         profile_source,
     76         allowed_profiles: CLI_ALLOWED_PROFILES
     77             .iter()
     78             .map(|value| (*value).to_owned())
     79             .collect(),
     80         root_source: path_root_source(profile).to_owned(),
     81         repo_local_root: override_selection.repo_local_root,
     82         repo_local_root_source: override_selection.repo_local_root_source,
     83         subordinate_path_override_source: "runtime_config".to_owned(),
     84         app_namespace: app_namespace.relative_path().display().to_string(),
     85         shared_accounts_namespace: shared_accounts_namespace
     86             .relative_path()
     87             .display()
     88             .to_string(),
     89         shared_identities_namespace: shared_identity_namespace
     90             .relative_path()
     91             .display()
     92             .to_string(),
     93         app_config_path: app_paths.config.join(DEFAULT_CONFIG_FILE_NAME),
     94         workspace_config_path,
     95         app_data_root: app_paths.data,
     96         app_logs_root: app_paths.logs,
     97         shared_accounts_data_root: shared_accounts_paths.data,
     98         shared_accounts_secrets_root: shared_accounts_paths.secrets,
     99         default_identity_path,
    100     })
    101 }
    102 
    103 fn resolve_cli_path_profile(
    104     env: &dyn Environment,
    105     env_file: &EnvFileValues,
    106 ) -> Result<(RadrootsPathProfile, &'static str, String), RuntimeError> {
    107     match path_env_value_entry(env, env_file, &[ENV_CLI_PATHS_PROFILE]) {
    108         Some(entry) => parse_cli_path_profile(entry.key.as_str(), entry.value.as_str())
    109             .map(|(profile, label)| (profile, label, entry.source_label())),
    110         None => Ok((
    111             RadrootsPathProfile::InteractiveUser,
    112             CLI_DEFAULT_PROFILE,
    113             "default".to_owned(),
    114         )),
    115     }
    116 }
    117 
    118 fn parse_cli_path_profile(
    119     key: &str,
    120     value: &str,
    121 ) -> Result<(RadrootsPathProfile, &'static str), RuntimeError> {
    122     match value.trim().to_ascii_lowercase().as_str() {
    123         CLI_DEFAULT_PROFILE => Ok((RadrootsPathProfile::InteractiveUser, CLI_DEFAULT_PROFILE)),
    124         CLI_REPO_LOCAL_PROFILE => Ok((RadrootsPathProfile::RepoLocal, CLI_REPO_LOCAL_PROFILE)),
    125         other => Err(RuntimeError::Config(format!(
    126             "{key} must be `interactive_user` or `repo_local`, got `{other}`"
    127         ))),
    128     }
    129 }
    130 
    131 struct CliPathOverrideSelection {
    132     overrides: RadrootsPathOverrides,
    133     repo_local_root: Option<PathBuf>,
    134     repo_local_root_source: Option<String>,
    135 }
    136 
    137 fn resolve_cli_path_overrides(
    138     current_dir: &Path,
    139     env: &dyn Environment,
    140     env_file: &EnvFileValues,
    141     profile: RadrootsPathProfile,
    142 ) -> Result<CliPathOverrideSelection, RuntimeError> {
    143     match profile {
    144         RadrootsPathProfile::InteractiveUser => Ok(CliPathOverrideSelection {
    145             overrides: RadrootsPathOverrides::default(),
    146             repo_local_root: None,
    147             repo_local_root_source: None,
    148         }),
    149         RadrootsPathProfile::RepoLocal => {
    150             let Some(entry) = path_env_value_entry(env, env_file, &[ENV_CLI_PATHS_REPO_LOCAL_ROOT])
    151             else {
    152                 return Err(RuntimeError::Config(format!(
    153                     "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be set when {ENV_CLI_PATHS_PROFILE}=repo_local"
    154                 )));
    155             };
    156             if entry.value.trim().is_empty() {
    157                 return Err(RuntimeError::Config(format!(
    158                     "{} must not be empty when {ENV_CLI_PATHS_PROFILE}=repo_local",
    159                     entry.key
    160                 )));
    161             }
    162             let repo_local_root = normalize_explicit_path_root(current_dir, entry.value.as_str());
    163             Ok(CliPathOverrideSelection {
    164                 overrides: RadrootsPathOverrides::repo_local(repo_local_root.as_path()),
    165                 repo_local_root: Some(repo_local_root),
    166                 repo_local_root_source: Some(entry.source_label()),
    167             })
    168         }
    169         _ => Err(RuntimeError::Config(
    170             "cli only supports interactive_user and repo_local path profiles".to_owned(),
    171         )),
    172     }
    173 }
    174 
    175 fn path_root_source(profile: RadrootsPathProfile) -> &'static str {
    176     match profile {
    177         RadrootsPathProfile::InteractiveUser => "host_defaults",
    178         RadrootsPathProfile::RepoLocal => "repo_local_root",
    179         RadrootsPathProfile::ServiceHost => "service_host_defaults",
    180         RadrootsPathProfile::MobileNative => "mobile_native_defaults",
    181     }
    182 }
    183 
    184 fn normalize_explicit_path_root(current_dir: &Path, value: &str) -> PathBuf {
    185     let root = PathBuf::from(value.trim());
    186     if root.is_absolute() {
    187         root
    188     } else {
    189         current_dir.join(root)
    190     }
    191 }
    192 
    193 fn resolve_workspace_config_path(
    194     profile: RadrootsPathProfile,
    195     override_selection: &CliPathOverrideSelection,
    196 ) -> Result<Option<PathBuf>, RuntimeError> {
    197     match profile {
    198         RadrootsPathProfile::InteractiveUser => Ok(None),
    199         RadrootsPathProfile::RepoLocal => override_selection
    200             .repo_local_root
    201             .as_ref()
    202             .map(|root| Some(root.join(DEFAULT_CONFIG_FILE_NAME)))
    203             .ok_or_else(|| {
    204                 RuntimeError::Config(format!(
    205                     "{ENV_CLI_PATHS_REPO_LOCAL_ROOT} must be resolved when {ENV_CLI_PATHS_PROFILE}=repo_local"
    206                 ))
    207             }),
    208         _ => Err(RuntimeError::Config(
    209             "cli only supports interactive_user and repo_local path profiles".to_owned(),
    210         )),
    211     }
    212 }
    213 
    214 struct PathEnvValueEntry {
    215     key: String,
    216     value: String,
    217     source_kind: &'static str,
    218 }
    219 
    220 impl PathEnvValueEntry {
    221     fn source_label(&self) -> String {
    222         format!("{}:{}", self.source_kind, self.key)
    223     }
    224 }
    225 
    226 fn path_env_value_entry(
    227     env: &dyn Environment,
    228     env_file: &EnvFileValues,
    229     keys: &[&str],
    230 ) -> Option<PathEnvValueEntry> {
    231     keys.iter()
    232         .find_map(|key| {
    233             env.var(key).map(|value| PathEnvValueEntry {
    234                 key: (*key).to_owned(),
    235                 value,
    236                 source_kind: "process_env",
    237             })
    238         })
    239         .or_else(|| {
    240             keys.iter().find_map(|key| {
    241                 env_file.get(key).map(|value| PathEnvValueEntry {
    242                     key: (*key).to_owned(),
    243                     value: value.to_owned(),
    244                     source_kind: "env_file",
    245                 })
    246             })
    247         })
    248 }