use super::{render, PeopleFilter, ProfileInput, ProfilePatch, Relations};
use crate::out::Out;
use anyhow::Result;
use clap::{Args, CommandFactory, Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
use triblespace::prelude::Id;
#[derive(Parser)]
#[command(
version = crate::GIT_VERSION,
name = "relations",
about = "Authored people, groups, and identity verdicts"
)]
pub struct Cli {
#[arg(long, env = "PILE")]
pile: PathBuf,
#[arg(long, env = "TRIBLESPACE_KEY")]
key: Option<PathBuf>,
#[command(subcommand)]
command: Option<Command>,
}
#[derive(Subcommand)]
enum Command {
Add {
label: String,
#[arg(long, value_parser = parse_id_arg)]
id: Option<Id>,
#[arg(long)]
source: Vec<String>,
#[command(flatten)]
profile: NewProfileArgs,
},
Set {
person: String,
#[arg(long)]
source: Vec<String>,
#[command(flatten)]
patch: ProfilePatchArgs,
},
Reconcile {
person: String,
#[arg(long)]
base: Option<String>,
#[command(flatten)]
patch: ProfilePatchArgs,
},
List {
#[arg(long, default_value_t = 50)]
limit: usize,
#[arg(long)]
all: bool,
#[arg(long, conflicts_with = "all")]
retired: bool,
},
Show {
person: String,
},
Retire { person: String },
#[command(alias = "restore")]
Unretire { person: String },
Group {
#[command(subcommand)]
command: GroupCommand,
},
Identity {
#[command(subcommand)]
command: IdentityCommand,
},
}
#[derive(Args, Clone, Default)]
struct NewProfileArgs {
#[arg(long)]
alias: Vec<String>,
#[arg(long)]
affinity: Vec<String>,
#[arg(long)]
first_name: Option<String>,
#[arg(long)]
last_name: Option<String>,
#[arg(long)]
display_name: Option<String>,
#[arg(long)]
note: Option<String>,
#[arg(long)]
teams_user_id: Vec<String>,
#[arg(long)]
email: Vec<String>,
#[arg(long)]
phone: Vec<String>,
#[arg(long)]
company: Option<String>,
#[arg(long)]
position: Option<String>,
#[arg(long)]
profile_url: Vec<String>,
}
impl NewProfileArgs {
fn into_profile(self, label: String) -> ProfileInput {
ProfileInput {
label,
aliases: self.alias,
affinities: self.affinity,
first_name: self.first_name,
last_name: self.last_name,
display_name: self.display_name,
note: self.note,
teams_user_ids: self.teams_user_id,
emails: self.email,
phones: self.phone,
company: self.company,
position: self.position,
profile_urls: self.profile_url,
}
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq, ValueEnum)]
enum ProfileField {
Aliases,
Affinities,
FirstName,
LastName,
DisplayName,
Note,
TeamsUserIds,
Emails,
Phones,
Company,
Position,
ProfileUrls,
}
#[derive(Args, Clone, Default)]
struct ProfilePatchArgs {
#[arg(long)]
label: Option<String>,
#[arg(long)]
alias: Vec<String>,
#[arg(long)]
affinity: Vec<String>,
#[arg(long)]
first_name: Option<String>,
#[arg(long)]
last_name: Option<String>,
#[arg(long)]
display_name: Option<String>,
#[arg(long)]
note: Option<String>,
#[arg(long)]
teams_user_id: Vec<String>,
#[arg(long)]
email: Vec<String>,
#[arg(long)]
phone: Vec<String>,
#[arg(long)]
company: Option<String>,
#[arg(long)]
position: Option<String>,
#[arg(long)]
profile_url: Vec<String>,
#[arg(long, value_enum)]
clear: Vec<ProfileField>,
}
#[derive(Subcommand)]
enum GroupCommand {
Create {
name: String,
},
Add {
group: String,
person: String,
},
Remove {
group: String,
person: String,
},
Rename {
group: String,
name: String,
},
Reconcile {
group: String,
#[arg(long)]
name: Option<String>,
},
List,
Show {
group: String,
},
}
#[derive(Subcommand)]
enum IdentityCommand {
Resolve {
first: String,
second: String,
#[arg(
long,
conflicts_with = "distinct",
required_unless_present = "distinct"
)]
same: bool,
#[arg(long, conflicts_with = "same", required_unless_present = "same")]
distinct: bool,
},
List,
}
fn parse_id_arg(raw: &str) -> std::result::Result<Id, String> {
Id::from_hex(raw.trim()).ok_or_else(|| format!("invalid id '{raw}'"))
}
impl From<ProfileField> for super::ProfileField {
fn from(value: ProfileField) -> Self {
match value {
ProfileField::Aliases => Self::Aliases,
ProfileField::Affinities => Self::Affinities,
ProfileField::FirstName => Self::FirstName,
ProfileField::LastName => Self::LastName,
ProfileField::DisplayName => Self::DisplayName,
ProfileField::Note => Self::Note,
ProfileField::TeamsUserIds => Self::TeamsUserIds,
ProfileField::Emails => Self::Emails,
ProfileField::Phones => Self::Phones,
ProfileField::Company => Self::Company,
ProfileField::Position => Self::Position,
ProfileField::ProfileUrls => Self::ProfileUrls,
}
}
}
impl ProfilePatchArgs {
fn into_patch(self) -> ProfilePatch {
fn provided(values: Vec<String>) -> Option<Vec<String>> {
(!values.is_empty()).then_some(values)
}
ProfilePatch {
label: self.label,
aliases: provided(self.alias),
affinities: provided(self.affinity),
first_name: self.first_name,
last_name: self.last_name,
display_name: self.display_name,
note: self.note,
teams_user_ids: provided(self.teams_user_id),
emails: provided(self.email),
phones: provided(self.phone),
company: self.company,
position: self.position,
profile_urls: provided(self.profile_url),
clear: self.clear.into_iter().map(Into::into).collect(),
}
}
}
pub fn execute(cli: Cli, output: &mut Out<'_>) -> Result<()> {
let Some(command) = cli.command else {
return output.line(Cli::command().render_help().to_string());
};
let operations = Relations::new(cli.pile, cli.key);
match command {
Command::Add {
label,
id,
source,
profile,
} => render::added(
&operations.add(profile.into_profile(label), id, &source)?,
output,
),
Command::Set {
person,
source,
patch,
} => render::profile_updated(
&operations.set(&person, patch.into_patch(), &source)?,
output,
),
Command::Reconcile {
person,
base,
patch,
} => render::profile_reconciled(
&operations.reconcile(&person, base.as_deref(), patch.into_patch())?,
output,
),
Command::List {
limit,
all,
retired,
} => {
let filter = if all {
PeopleFilter::All
} else if retired {
PeopleFilter::Retired
} else {
PeopleFilter::Active
};
output.text(operations.list(limit, filter)?)
}
Command::Show { person } => output.text(operations.show(&person)?),
Command::Retire { person } => render::lifecycle(&operations.retire(&person)?, true, output),
Command::Unretire { person } => {
render::lifecycle(&operations.unretire(&person)?, false, output)
}
Command::Group { command } => match command {
GroupCommand::Create { name } => {
render::group_created(&operations.group_create(&name)?, output)
}
GroupCommand::Add { group, person } => {
render::group_added(&operations.group_add(&group, &person)?, output)
}
GroupCommand::Remove { group, person } => {
render::group_removed(&operations.group_remove(&group, &person)?, output)
}
GroupCommand::Rename { group, name } => {
render::group_renamed(&operations.group_rename(&group, &name)?, output)
}
GroupCommand::Reconcile { group, name } => render::group_reconciled(
&operations.group_reconcile(&group, name.as_deref())?,
output,
),
GroupCommand::List => output.text(operations.group_list()?),
GroupCommand::Show { group } => output.text(operations.group_show(&group)?),
},
Command::Identity { command } => match command {
IdentityCommand::Resolve {
first,
second,
same,
distinct: _,
} => render::identity(
&operations.identity_resolve(&first, &second, same)?,
same,
output,
),
IdentityCommand::List => output.text(operations.identity_list()?),
},
}
}
pub fn run() -> Result<()> {
let cli = Cli::parse();
if cli.command.is_none() {
Cli::command().print_help()?;
println!();
return Ok(());
}
crate::cli::with_output("relations", |output| execute(cli, output))
}