summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPietro Albini <pietro@pietroalbini.org>2018-11-25 18:43:11 +0100
committerPietro Albini <pietro@pietroalbini.org>2018-11-25 18:43:11 +0100
commit17618b4912eb46f41154c515157bedd80b7caf3d (patch)
tree6e29b5bd83a7aa526b6301f142d6d08e6ff5ccfb /src
parent484ad8557dd22a5e221191997d55e731c93168a0 (diff)
add basic team structure handling
Diffstat (limited to 'src')
-rw-r--r--src/data.rs66
-rw-r--r--src/main.rs55
-rw-r--r--src/schema.rs65
-rw-r--r--src/validate.rs79
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));
+ }
+}