| use super::{GlobalContext, StringList, Value}; |
| use regex::Regex; |
| use serde::{Deserialize, de::Error}; |
| use std::path::PathBuf; |
| |
| /// Use with the `get` API to fetch a string that will be converted to a |
| /// `PathBuf`. Relative paths are converted to absolute paths based on the |
| /// location of the config file. |
| #[derive(Debug, Deserialize, PartialEq, Clone)] |
| #[serde(transparent)] |
| pub struct ConfigRelativePath(Value<String>); |
| |
| impl ConfigRelativePath { |
| pub fn new(path: Value<String>) -> ConfigRelativePath { |
| ConfigRelativePath(path) |
| } |
| |
| /// Returns the underlying value. |
| pub fn value(&self) -> &Value<String> { |
| &self.0 |
| } |
| |
| /// Returns the raw underlying configuration value for this key. |
| pub fn raw_value(&self) -> &str { |
| &self.0.val |
| } |
| |
| /// Resolves this configuration-relative path to an absolute path. |
| /// |
| /// This will always return an absolute path where it's relative to the |
| /// location for configuration for this value. |
| pub fn resolve_path(&self, gctx: &GlobalContext) -> PathBuf { |
| self.0.definition.root(gctx.cwd()).join(&self.0.val) |
| } |
| |
| /// Same as [`Self::resolve_path`] but will make string replacements |
| /// before resolving the path. |
| /// |
| /// `replacements` should be an [`IntoIterator`] of tuples with the "from" and "to" for the |
| /// string replacement |
| pub fn resolve_templated_path( |
| &self, |
| gctx: &GlobalContext, |
| replacements: impl IntoIterator<Item = (impl AsRef<str>, impl AsRef<str>)>, |
| ) -> Result<PathBuf, ResolveTemplateError> { |
| let mut value = self.0.val.clone(); |
| |
| for (from, to) in replacements { |
| value = value.replace(from.as_ref(), to.as_ref()); |
| } |
| |
| // Check for expected variables |
| let re = Regex::new(r"\{(.*)\}").unwrap(); |
| if let Some(caps) = re.captures(&value) { |
| return Err(ResolveTemplateError::UnexpectedVariable { |
| variable: caps[1].to_string(), |
| raw_template: self.0.val.clone(), |
| }); |
| }; |
| |
| if value.contains("{") { |
| return Err(ResolveTemplateError::UnexpectedBracket { |
| bracket_type: BracketType::Opening, |
| raw_template: self.0.val.clone(), |
| }); |
| } |
| |
| if value.contains("}") { |
| return Err(ResolveTemplateError::UnexpectedBracket { |
| bracket_type: BracketType::Closing, |
| raw_template: self.0.val.clone(), |
| }); |
| } |
| |
| Ok(self.0.definition.root(gctx.cwd()).join(&value)) |
| } |
| |
| /// Resolves this configuration-relative path to either an absolute path or |
| /// something appropriate to execute from `PATH`. |
| /// |
| /// Values which don't look like a filesystem path (don't contain `/` or |
| /// `\`) will be returned as-is, and everything else will fall through to an |
| /// absolute path. |
| pub fn resolve_program(&self, gctx: &GlobalContext) -> PathBuf { |
| gctx.string_to_path(&self.0.val, &self.0.definition) |
| } |
| } |
| |
| /// A config type that is a program to run. |
| /// |
| /// This supports a list of strings like `['/path/to/program', 'somearg']` |
| /// or a space separated string like `'/path/to/program somearg'`. |
| /// |
| /// This expects the first value to be the path to the program to run. |
| /// Subsequent values are strings of arguments to pass to the program. |
| /// |
| /// Typically you should use `ConfigRelativePath::resolve_program` on the path |
| /// to get the actual program. |
| /// |
| /// **Note**: Any usage of this type in config needs to be listed in |
| /// the `NON_MERGEABLE_LISTS` check to prevent list merging |
| /// from multiple config files. |
| #[derive(Debug, Clone, PartialEq)] |
| pub struct PathAndArgs { |
| pub path: ConfigRelativePath, |
| pub args: Vec<String>, |
| } |
| |
| impl<'de> serde::Deserialize<'de> for PathAndArgs { |
| fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
| where |
| D: serde::Deserializer<'de>, |
| { |
| let vsl = Value::<StringList>::deserialize(deserializer)?; |
| let mut strings = vsl.val.0; |
| if strings.is_empty() { |
| return Err(D::Error::invalid_length(0, &"at least one element")); |
| } |
| let first = strings.remove(0); |
| let crp = Value { |
| val: first, |
| definition: vsl.definition, |
| }; |
| Ok(PathAndArgs { |
| path: ConfigRelativePath(crp), |
| args: strings, |
| }) |
| } |
| } |
| |
| impl PathAndArgs { |
| /// Construct a `PathAndArgs` from a string. The string will be split on ascii whitespace, |
| /// with the first item being treated as a `ConfigRelativePath` to the executable, and subsequent |
| /// items as arguments. |
| pub fn from_whitespace_separated_string(p: &Value<String>) -> PathAndArgs { |
| let mut iter = p.val.split_ascii_whitespace().map(str::to_string); |
| let val = iter.next().unwrap_or_default(); |
| let args = iter.collect(); |
| let crp = Value { |
| val, |
| definition: p.definition.clone(), |
| }; |
| PathAndArgs { |
| path: ConfigRelativePath(crp), |
| args, |
| } |
| } |
| } |
| |
| #[derive(Debug)] |
| pub enum ResolveTemplateError { |
| UnexpectedVariable { |
| variable: String, |
| raw_template: String, |
| }, |
| UnexpectedBracket { |
| bracket_type: BracketType, |
| raw_template: String, |
| }, |
| } |
| |
| #[derive(Debug)] |
| pub enum BracketType { |
| Opening, |
| Closing, |
| } |