use std::path::Path;
use crate::cli::SetKey;
use crate::config::{OriginPreference, PartialConfig, config_path};
use crate::output;
use crate::validate::{validate_github_username, validate_tangled_username};
pub fn run(key: SetKey, value: String) -> Result<(), Box<dyn std::error::Error>> {
let path = config_path()?;
run_with_config_path(key, value, &path)
}
pub fn run_with_config_path(
key: SetKey,
value: String,
config_path: &Path,
) -> Result<(), Box<dyn std::error::Error>> {
let (validated_value, confirmation) = validate_for_key(&key, &value)?;
let mut partial = PartialConfig::load_from_path(config_path)?;
apply_to_partial(&mut partial, &key, validated_value);
partial.save_to_path(config_path)?;
println!("{}", output::success(&confirmation));
Ok(())
}
fn validate_for_key(
key: &SetKey,
value: &str,
) -> Result<(String, String), Box<dyn std::error::Error>> {
match key {
SetKey::GithubUser => {
let validated = validate_github_username(value)?;
let msg = format!("GitHub username set to: {validated}");
Ok((validated, msg))
}
SetKey::TangledUser => {
let validated = validate_tangled_username(value)?;
let msg = format!("Tangled username set to: {validated}");
Ok((validated, msg))
}
SetKey::Origin => {
let sanitized = crate::validate::sanitize(value)?;
let pref = OriginPreference::from_alias(&sanitized).ok_or_else(|| {
format!(
"'{sanitized}' is not a valid origin preference. \
Use 'github' (or 'gh') or 'tangled' (or 'tngl')."
)
})?;
let canonical = pref.to_string();
let msg = format!("Origin preference set to: {canonical}");
Ok((canonical, msg))
}
}
}
fn apply_to_partial(partial: &mut PartialConfig, key: &SetKey, validated: String) {
match key {
SetKey::GithubUser => {
partial.github_username = Some(validated);
}
SetKey::TangledUser => {
partial.tangled_username = Some(validated);
}
SetKey::Origin => {
partial.origin_preference = OriginPreference::from_alias(&validated);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::{Config, OriginPreference};
use tempfile::NamedTempFile;
fn set_at(key: SetKey, value: &str) -> (NamedTempFile, Result<(), Box<dyn std::error::Error>>) {
let f = NamedTempFile::new().unwrap();
let result = run_with_config_path(key, value.to_string(), f.path());
(f, result)
}
fn read_partial(f: &NamedTempFile) -> PartialConfig {
PartialConfig::load_from_path(f.path()).unwrap()
}
#[test]
fn set_gh_user_writes_github_username() {
let (f, result) = set_at(SetKey::GithubUser, "cyrusae");
result.unwrap();
let partial = read_partial(&f);
assert_eq!(partial.github_username, Some("cyrusae".to_string()));
assert_eq!(partial.tangled_username, None);
assert_eq!(partial.origin_preference, None);
}
#[test]
fn set_tngl_user_writes_tangled_username() {
let (f, result) = set_at(SetKey::TangledUser, "atdot.fyi");
result.unwrap();
let partial = read_partial(&f);
assert_eq!(partial.tangled_username, Some("atdot.fyi".to_string()));
assert_eq!(partial.github_username, None);
assert_eq!(partial.origin_preference, None);
}
#[test]
fn set_origin_github_full_word() {
let (f, result) = set_at(SetKey::Origin, "github");
result.unwrap();
let partial = read_partial(&f);
assert_eq!(partial.origin_preference, Some(OriginPreference::Github));
}
#[test]
fn set_origin_gh_alias() {
let (f, result) = set_at(SetKey::Origin, "gh");
result.unwrap();
let partial = read_partial(&f);
assert_eq!(partial.origin_preference, Some(OriginPreference::Github));
}
#[test]
fn set_origin_tangled_full_word() {
let (f, result) = set_at(SetKey::Origin, "tangled");
result.unwrap();
let partial = read_partial(&f);
assert_eq!(partial.origin_preference, Some(OriginPreference::Tangled));
}
#[test]
fn set_origin_tngl_alias() {
let (f, result) = set_at(SetKey::Origin, "tngl");
result.unwrap();
let partial = read_partial(&f);
assert_eq!(partial.origin_preference, Some(OriginPreference::Tangled));
}
#[test]
fn set_gh_user_leaves_existing_fields_untouched() {
let f = NamedTempFile::new().unwrap();
let initial = PartialConfig {
github_username: Some("old-name".to_string()),
tangled_username: Some("atdot.fyi".to_string()),
origin_preference: Some(OriginPreference::Tangled),
};
initial.save_to_path(f.path()).unwrap();
run_with_config_path(SetKey::GithubUser, "cyrusae".to_string(), f.path()).unwrap();
let updated = read_partial(&f);
assert_eq!(updated.github_username, Some("cyrusae".to_string()));
assert_eq!(updated.tangled_username, Some("atdot.fyi".to_string()));
assert_eq!(updated.origin_preference, Some(OriginPreference::Tangled));
}
#[test]
fn set_on_missing_config_creates_file_with_just_that_field() {
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("entangle").join("config.json");
run_with_config_path(SetKey::GithubUser, "cyrusae".to_string(), &path).unwrap();
let partial = PartialConfig::load_from_path(&path).unwrap();
assert_eq!(partial.github_username, Some("cyrusae".to_string()));
assert_eq!(partial.tangled_username, None);
assert_eq!(partial.origin_preference, None);
}
#[test]
fn invalid_github_username_does_not_write_config() {
let f = NamedTempFile::new().unwrap();
let result = run_with_config_path(
SetKey::GithubUser,
"-invalid-leading-hyphen".to_string(),
f.path(),
);
assert!(result.is_err());
let partial = read_partial(&f);
assert_eq!(partial.github_username, None);
}
#[test]
fn invalid_tangled_username_does_not_write_config() {
let f = NamedTempFile::new().unwrap();
let result = run_with_config_path(
SetKey::TangledUser,
"nodot".to_string(), f.path(),
);
assert!(result.is_err());
let partial = read_partial(&f);
assert_eq!(partial.tangled_username, None);
}
#[test]
fn invalid_origin_value_does_not_write_config() {
let f = NamedTempFile::new().unwrap();
let result = run_with_config_path(SetKey::Origin, "gitlab".to_string(), f.path());
assert!(result.is_err());
let partial = read_partial(&f);
assert_eq!(partial.origin_preference, None);
}
#[test]
fn dangerous_char_in_username_does_not_write_config() {
let f = NamedTempFile::new().unwrap();
let result = run_with_config_path(SetKey::GithubUser, "cyrus$ae".to_string(), f.path());
assert!(result.is_err());
let partial = read_partial(&f);
assert_eq!(partial.github_username, None);
}
#[test]
fn set_gh_user_normalises_case() {
let (f, result) = set_at(SetKey::GithubUser, "CyrusAE");
result.unwrap();
assert_eq!(
read_partial(&f).github_username,
Some("cyrusae".to_string())
);
}
#[test]
fn set_gh_user_strips_quotes() {
let (f, result) = set_at(SetKey::GithubUser, r#""cyrusae""#);
result.unwrap();
assert_eq!(
read_partial(&f).github_username,
Some("cyrusae".to_string())
);
}
#[test]
fn set_origin_normalises_case() {
let (f, result) = set_at(SetKey::Origin, "GitHub");
result.unwrap();
assert_eq!(
read_partial(&f).origin_preference,
Some(OriginPreference::Github)
);
}
#[test]
fn three_sets_produce_valid_full_config() {
let f = NamedTempFile::new().unwrap();
run_with_config_path(SetKey::GithubUser, "cyrusae".to_string(), f.path()).unwrap();
run_with_config_path(SetKey::TangledUser, "atdot.fyi".to_string(), f.path()).unwrap();
run_with_config_path(SetKey::Origin, "github".to_string(), f.path()).unwrap();
let cfg = Config::load_from_path(f.path()).unwrap();
assert_eq!(cfg.github_username, "cyrusae");
assert_eq!(cfg.tangled_username, "atdot.fyi");
assert_eq!(cfg.origin_preference, OriginPreference::Github);
}
}