diff options
author | Pietro Albini <pietro@pietroalbini.org> | 2018-11-25 18:43:11 +0100 |
---|---|---|
committer | Pietro Albini <pietro@pietroalbini.org> | 2018-11-25 18:43:11 +0100 |
commit | 17618b4912eb46f41154c515157bedd80b7caf3d (patch) | |
tree | 6e29b5bd83a7aa526b6301f142d6d08e6ff5ccfb /src | |
parent | 484ad8557dd22a5e221191997d55e731c93168a0 (diff) |
add basic team structure handling
Diffstat (limited to 'src')
-rw-r--r-- | src/data.rs | 66 | ||||
-rw-r--r-- | src/main.rs | 55 | ||||
-rw-r--r-- | src/schema.rs | 65 | ||||
-rw-r--r-- | src/validate.rs | 79 |
4 files changed, 249 insertions, 16 deletions
diff --git a/src/data.rs b/src/data.rs new file mode 100644 index 0000000..2734f38 --- /dev/null +++ b/src/data.rs @@ -0,0 +1,66 @@ +use crate::schema::{Person, Team}; +use failure::{Error, ResultExt}; +use serde::Deserialize; +use std::collections::HashMap; +use std::ffi::OsStr; + +#[derive(Debug)] +pub(crate) struct Data { + people: HashMap<String, Person>, + teams: HashMap<String, Team>, +} + +impl Data { + pub(crate) fn load() -> Result<Self, Error> { + let mut data = Data { + people: HashMap::new(), + teams: HashMap::new(), + }; + + data.load_dir("people", |this, person: Person| { + this.people.insert(person.github().to_string(), person); + })?; + + data.load_dir("teams", |this, team: Team| { + this.teams.insert(team.name().to_string(), team); + })?; + + Ok(data) + } + + fn load_dir<T, F>(&mut self, dir: &str, f: F) -> Result<(), Error> + where + T: for<'de> Deserialize<'de>, + F: Fn(&mut Self, T), + { + for entry in std::fs::read_dir(dir)? { + let path = entry?.path(); + + if path.is_file() && path.extension() == Some(OsStr::new("toml")) { + let content = std::fs::read(&path) + .with_context(|_| format!("failed to read {}", path.display()))?; + let parsed: T = toml::from_slice(&content) + .with_context(|_| format!("failed to parse {}", path.display()))?; + f(self, parsed); + } + } + + Ok(()) + } + + pub(crate) fn team(&self, name: &str) -> Option<&Team> { + self.teams.get(name) + } + + pub(crate) fn teams(&self) -> impl Iterator<Item = &Team> { + self.teams.values() + } + + pub(crate) fn person(&self, name: &str) -> Option<&Person> { + self.people.get(name) + } + + pub(crate) fn people(&self) -> impl Iterator<Item = &Person> { + self.people.values() + } +} diff --git a/src/main.rs b/src/main.rs index c8aace9..4cd04d6 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,24 @@ +mod data; +mod schema; mod sync; +mod validate; -use failure::Error; +use crate::data::Data; +use failure::{Error, err_msg}; +use structopt::StructOpt; + +#[derive(structopt::StructOpt)] +#[structopt(name = "team", about = "manage the rust team members")] +enum Cli { + #[structopt(name = "check", help = "check if the configuration is correct")] + Check, + #[structopt(name = "sync", help = "synchronize the configuration")] + Sync, + #[structopt(name = "dump-team", help = "print the members of a team")] + DumpTeam { + name: String, + } +} fn main() { env_logger::init(); @@ -14,24 +32,29 @@ fn main() { } fn run() -> Result<(), Error> { - let args = std::env::args().skip(1).collect::<Vec<_>>(); - if args.len() != 1 { - usage(); - } - - match args[0].as_str() { - "sync" => { + let cli = Cli::from_args(); + match cli { + Cli::Check => { + let data = Data::load()?; + crate::validate::validate(&data)?; + } + Cli::Sync => { sync::lists::run()?; } - _ => usage(), + Cli::DumpTeam { ref name } => { + let data = Data::load()?; + let team = data.team(name).ok_or_else(|| err_msg("unknown team"))?; + + let leads = team.leads(); + for member in team.members(&data)? { + println!("{}{}", member, if leads.contains(member) { + " (lead)" + } else { + "" + }); + } + } } Ok(()) } - -fn usage() { - eprintln!("usage: {} <mode>", std::env::args().next().unwrap()); - eprintln!("available modes:"); - eprintln!("- sync: synchronize local state with the remote providers"); - std::process::exit(1); -} diff --git a/src/schema.rs b/src/schema.rs new file mode 100644 index 0000000..bab5809 --- /dev/null +++ b/src/schema.rs @@ -0,0 +1,65 @@ +use crate::data::Data; +use failure::{Error, err_msg}; +use std::collections::HashSet; + +#[derive(serde_derive::Deserialize, Debug)] +pub(crate) struct Person { + name: String, + github: String, + irc: Option<String>, +} + +impl Person { + pub(crate) fn name(&self) -> &str { + &self.name + } + + pub(crate) fn github(&self) -> &str { + &self.github + } + + pub(crate) fn irc(&self) -> &str { + if let Some(irc) = &self.irc { + irc + } else { + &self.github + } + } +} + +#[derive(serde_derive::Deserialize, Debug)] +pub(crate) struct Team { + name: String, + #[serde(default)] + children: Vec<String>, + people: TeamPeople, +} + +impl Team { + pub(crate) fn name(&self) -> &str { + &self.name + } + + pub(crate) fn leads(&self) -> HashSet<&str> { + self.people.leads.iter().map(|s| s.as_str()).collect() + } + + pub(crate) fn members<'a>(&'a self, data: &'a Data) -> Result<HashSet<&'a str>, Error> { + let mut members: HashSet<_> = self.people.members.iter().map(|s| s.as_str()).collect(); + for subteam in &self.children { + let submembers = data + .team(&subteam) + .ok_or_else(|| err_msg(format!("missing team {}", subteam)))?; + for person in submembers.members(data)? { + members.insert(person); + } + } + Ok(members) + } +} + +#[derive(serde_derive::Deserialize, Debug)] +struct TeamPeople { + leads: Vec<String>, + members: Vec<String>, +} diff --git a/src/validate.rs b/src/validate.rs new file mode 100644 index 0000000..61e0778 --- /dev/null +++ b/src/validate.rs @@ -0,0 +1,79 @@ +use crate::data::Data; +use std::collections::HashSet; +use failure::{Error, bail}; + +pub(crate) fn validate(data: &Data) -> Result<(), Error> { + let mut errors = Vec::new(); + + validate_team_leads(data, &mut errors); + validate_team_members(data, &mut errors); + validate_inactive_members(data, &mut errors); + + if !errors.is_empty() { + for err in &errors { + eprintln!("validation error: {}", err); + } + + bail!("{} validation errors found", errors.len()); + } + + Ok(()) +} + +/// Ensure team leaders are part of the teams they lead +fn validate_team_leads(data: &Data, errors: &mut Vec<String>) { + for team in data.teams() { + let members = match team.members(data) { + Ok(m) => m, + Err(err) => { + errors.push(err.to_string()); + continue; + } + }; + for lead in team.leads() { + if !members.contains(lead) { + errors.push(format!("`{}` leads team `{}`, but is not a member of it", lead, team.name())); + } + } + } +} + +/// Ensure team members are people +fn validate_team_members(data: &Data, errors: &mut Vec<String>) { + for team in data.teams() { + let members = match team.members(data) { + Ok(m) => m, + Err(err) => { + errors.push(err.to_string()); + continue; + } + }; + for member in members { + if data.person(member).is_none() { + errors.push(format!("person `{}` is member of team `{}` but doesn't exist", member, team.name())); + } + } + } +} + +/// Ensure every person is part of at least a team +fn validate_inactive_members(data: &Data, errors: &mut Vec<String>) { + let mut active_members = HashSet::new(); + for team in data.teams() { + let members = match team.members(data) { + Ok(m) => m, + Err(err) => { + errors.push(err.to_string()); + continue; + } + }; + for member in members { + active_members.insert(member); + } + } + + let all_members = data.people().map(|p| p.github()).collect::<HashSet<_>>(); + for person in all_members.difference(&active_members) { + errors.push(format!("person `{}` is not a member of any team", person)); + } +} |