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 }