fhttp-core 2.1.0

core library for the fhttp tool
Documentation
use std::collections::HashMap;
use std::env::{self, VarError};
use std::iter::Iterator;
use std::path::{Path, PathBuf};
use std::str::FromStr;

use anyhow::{anyhow, Context, Result};
use promptly::prompt;
use serde::{Deserialize, Serialize};

pub use profile_variable::ProfileVariable;

use crate::path_utils::RelativePath;
use crate::{Config, ResponseStore};

mod profile_variable;

pub struct Profiles;

impl Profiles {
    pub fn parse<P: AsRef<Path>>(path: P) -> Result<HashMap<String, Profile>> {
        let path = path.as_ref();
        let content = std::fs::read_to_string(path)
            .with_context(|| format!("Error opening file {}", path.to_str().unwrap()))?;
        let profiles = serde_json::from_str::<HashMap<String, _Profile>>(&content)
            .with_context(|| format!("error reading profile from {}", path.to_str().unwrap()))?;
        let ret = profiles
            .into_iter()
            .map(|(key, value)| {
                let profile = Profile::new(path, value.variables);
                (key, profile)
            })
            .collect::<HashMap<String, Profile>>();

        Ok(ret)
    }
}

#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
struct _Profile {
    pub variables: HashMap<String, ProfileVariable>,
}

#[derive(Debug, Eq, PartialEq, Clone, Serialize, Deserialize)]
pub struct Profile {
    source_path: PathBuf,
    variables: HashMap<String, ProfileVariable>,
}

impl Profile {
    pub fn empty<T: Into<PathBuf>>(source_path: T) -> Self {
        Profile {
            source_path: source_path.into(),
            variables: HashMap::new(),
        }
    }

    pub fn new<T: Into<PathBuf>>(
        source_path: T,
        variables: HashMap<String, ProfileVariable>,
    ) -> Self {
        Profile {
            source_path: source_path.into(),
            variables,
        }
    }

    pub fn defined_through_request<K: Into<String>>(&self, key: K) -> Option<PathBuf> {
        let key = key.into();

        match self.variables.contains_key(&key) {
            true => match self.variables.get(&key) {
                Some(ProfileVariable::Request { request }) => {
                    Some(PathBuf::from_str(request).unwrap())
                }
                _ => None,
            },
            false => None,
        }
    }

    pub fn get<'a, K: Into<&'a str>>(
        &self,
        key: K,
        config: &Config,
        response_store: &ResponseStore,
        default: Option<&'a str>,
        for_dependency: bool,
    ) -> Result<String> {
        let key = key.into();

        match self.variables.get(key) {
            Some(ProfileVariable::Request { request }) => {
                Ok(response_store.get(&self.get_dependency_path(request)?))
            }
            Some(var) => var.get(config, for_dependency),
            None => get_from_environment(key, config, default),
        }
    }

    pub fn source_path(&self) -> &Path {
        &self.source_path
    }

    pub fn variables(&self) -> Vec<&ProfileVariable> {
        self.variables.values().collect()
    }

    pub fn override_with(&mut self, other: Profile) {
        for (key, value) in other.variables {
            self.variables.insert(key, value);
        }
    }
}

impl AsRef<Path> for Profile {
    fn as_ref(&self) -> &Path {
        &self.source_path
    }
}

fn get_from_environment(key: &str, config: &Config, default: Option<&str>) -> Result<String> {
    match env::var(key) {
        Ok(value) => Ok(value),
        Err(VarError::NotUnicode(_)) => {
            Err(anyhow!("environment variable {} is not unicode!", key))
        }
        Err(VarError::NotPresent) => match default {
            Some(default) => Ok(default.to_owned()),
            None => match config.prompt_missing_env_vars() {
                true => {
                    let value = prompt::<String, _>(&key).unwrap();
                    env::set_var(key, &value);
                    Ok(value)
                }
                false => Err(anyhow!(format!("missing environment variable {}", key))),
            },
        },
    }
}

#[cfg(test)]
mod test {
    use std::env;

    use maplit::hashmap;

    use crate::profiles::ProfileVariable;
    use crate::test_utils::root;

    use super::*;

    #[test]
    fn should_load_profiles() -> Result<()> {
        let path = root().join("resources/test/profiles/profile1.json");
        let profiles = Profiles::parse(path)?;
        assert_eq!(
            profiles,
            hashmap! {
                "development".into() => Profile {
                    source_path: root().join("resources/test/profiles/profile1.json").path_buf(),
                    variables: hashmap!{},
                },
                "testing".into() => Profile {
                    source_path: root().join("resources/test/profiles/profile1.json").path_buf(),
                    variables: hashmap!{
                        "var1".into() => ProfileVariable::StringValue("value1".into())
                    },
                }
            }
        );

        Ok(())
    }

    #[test]
    fn get_should_get_variables() -> Result<()> {
        let profile = Profile {
            source_path: env::current_dir().unwrap(),
            variables: hashmap! {
                "a".into() => ProfileVariable::StringValue("b".into())
            },
        };

        assert_eq!(
            profile.get("a", &Config::default(), &ResponseStore::new(), None, true)?,
            String::from("b")
        );

        Ok(())
    }

    #[test]
    fn get_should_default_to_env_vars() -> Result<()> {
        env::set_var("a", "A");

        let profile = Profile {
            source_path: env::current_dir().unwrap(),
            variables: HashMap::new(),
        };

        assert_eq!(
            profile.get("a", &Config::default(), &ResponseStore::new(), None, true)?,
            String::from("A")
        );

        Ok(())
    }

    #[test]
    fn override_with_should_merge() -> Result<()> {
        let config = Config::default();
        let response_store = ResponseStore::new();

        let mut default = Profile::new(
            env::current_dir().unwrap(),
            hashmap! {
                String::from("a") => ProfileVariable::StringValue(String::from("A")),
                String::from("b") => ProfileVariable::StringValue(String::from("B"))
            },
        );
        let local = Profile::new(
            env::current_dir().unwrap(),
            hashmap! {
                String::from("b") => ProfileVariable::StringValue(String::from("BBB")),
                String::from("c") => ProfileVariable::StringValue(String::from("CCC")),
            },
        );

        default.override_with(local);
        assert_eq!(default.get("a", &config, &response_store, None, true)?, "A");
        assert_eq!(
            default.get("b", &config, &response_store, None, true)?,
            "BBB"
        );
        assert_eq!(
            default.get("c", &config, &response_store, None, true)?,
            "CCC"
        );

        Ok(())
    }
}