From 260a5c06d9fb856dc0f65d97ba83d76bd4aaa945 Mon Sep 17 00:00:00 2001 From: Pietro Albini Date: Tue, 22 Jan 2019 16:53:19 +0100 Subject: add static api and fix website data --- src/main.rs | 9 ++++++ src/schema.rs | 54 ++++++++++++++++++++++++++++++-- src/static_api.rs | 93 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/validate.rs | 14 ++++++++- 4 files changed, 167 insertions(+), 3 deletions(-) create mode 100644 src/static_api.rs (limited to 'src') diff --git a/src/main.rs b/src/main.rs index 540c038..fb8f16c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2,9 +2,11 @@ mod data; mod schema; mod sync; mod validate; +mod static_api; use crate::data::Data; use failure::{err_msg, Error}; +use std::path::PathBuf; use structopt::StructOpt; #[derive(structopt::StructOpt)] @@ -14,6 +16,8 @@ enum Cli { Check, #[structopt(name = "sync", help = "synchronize the configuration")] Sync, + #[structopt(name = "static-api", help = "generate the static API")] + StaticApi { dest: String }, #[structopt(name = "dump-team", help = "print the members of a team")] DumpTeam { name: String }, #[structopt(name = "dump-list", help = "print all the emails in a list")] @@ -41,6 +45,11 @@ fn run() -> Result<(), Error> { Cli::Sync => { sync::lists::run(&data)?; } + Cli::StaticApi { ref dest } => { + let dest = PathBuf::from(dest); + let generator = crate::static_api::Generator::new(&dest, &data)?; + generator.generate()?; + } Cli::DumpTeam { ref name } => { let team = data.team(name).ok_or_else(|| err_msg("unknown team"))?; diff --git a/src/schema.rs b/src/schema.rs index f527827..bbd577c 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -48,7 +48,6 @@ pub(crate) struct Person { } impl Person { - #[allow(unused)] pub(crate) fn name(&self) -> &str { &self.name } @@ -88,11 +87,12 @@ impl Person { } #[derive(serde_derive::Deserialize, Debug)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] pub(crate) struct Team { name: String, #[serde(default = "default_false")] wg: bool, + subteam_of: Option, #[serde(default)] children: Vec, people: TeamPeople, @@ -110,10 +110,18 @@ impl Team { self.wg } + pub(crate) fn subteam_of(&self) -> Option<&str> { + self.subteam_of.as_ref().map(|s| s.as_str()) + } + pub(crate) fn leads(&self) -> HashSet<&str> { self.people.leads.iter().map(|s| s.as_str()).collect() } + pub(crate) fn website_data(&self) -> Option<&WebsiteData> { + self.website.as_ref() + } + pub(crate) fn members<'a>(&'a self, data: &'a Data) -> Result, Error> { let mut members: HashSet<_> = self.people.members.iter().map(|s| s.as_str()).collect(); for subteam in &self.children { @@ -196,15 +204,57 @@ struct TeamPeople { include_wg_leads: bool, } +pub(crate) struct DiscordInvite<'a> { + pub(crate) url: &'a str, + pub(crate) channel: &'a str, +} + #[derive(serde_derive::Deserialize, Debug)] #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub(crate) struct WebsiteData { name: String, description: String, + page: Option, email: Option, repo: Option, discord_invite: Option, discord_name: Option, + #[serde(default)] + weight: i64, +} + +impl WebsiteData { + pub(crate) fn name(&self) -> &str { + &self.name + } + + pub(crate) fn description(&self) -> &str { + &self.description + } + + pub(crate) fn weight(&self) -> i64 { + self.weight + } + + pub(crate) fn page(&self) -> Option<&str> { + self.page.as_ref().map(|s| s.as_str()) + } + + pub(crate) fn email(&self) -> Option<&str> { + self.email.as_ref().map(|s| s.as_str()) + } + + pub(crate) fn repo(&self) -> Option<&str> { + self.repo.as_ref().map(|s| s.as_str()) + } + + pub(crate) fn discord(&self) -> Option { + if let (Some(url), Some(channel)) = (&self.discord_invite, &self.discord_name) { + Some(DiscordInvite { url: url.as_ref(), channel: channel.as_ref() }) + } else { + None + } + } } #[derive(serde_derive::Deserialize, Debug)] diff --git a/src/static_api.rs b/src/static_api.rs new file mode 100644 index 0000000..1beea23 --- /dev/null +++ b/src/static_api.rs @@ -0,0 +1,93 @@ +use crate::data::Data; +use rust_team_data::v1; +use failure::Error; +use log::info; +use std::path::Path; +use indexmap::IndexMap; + +pub(crate) struct Generator<'a> { + dest: &'a Path, + data: &'a Data, +} + +impl<'a> Generator<'a> { + pub(crate) fn new(dest: &'a Path, data: &'a Data) -> Result, Error> { + if dest.is_dir() { + std::fs::remove_dir_all(&dest)?; + } + std::fs::create_dir_all(&dest)?; + + Ok(Generator { dest, data }) + } + + pub(crate) fn generate(&self) -> Result<(), Error> { + self.generate_teams()?; + Ok(()) + } + + fn generate_teams(&self) -> Result<(), Error> { + let mut teams = IndexMap::new(); + + for team in self.data.teams() { + let leads = team.leads(); + let mut members = Vec::new(); + for github_name in &team.members(&self.data)? { + if let Some(person) = self.data.person(github_name) { + members.push(v1::TeamMember { + name: person.name().into(), + github: (*github_name).into(), + is_lead: leads.contains(github_name), + }); + } + } + members.sort_by_key(|member| member.github.to_lowercase()); + members.sort_by_key(|member| !member.is_lead); + + let team_data = v1::Team { + name: team.name().into(), + kind: if team.is_wg() { + v1::TeamKind::WorkingGroup + } else { + v1::TeamKind::Team + }, + subteam_of: team.subteam_of().map(|st| st.into()), + members, + website_data: team.website_data().map(|ws| v1::TeamWebsite { + name: ws.name().into(), + description: ws.description().into(), + page: ws.page().unwrap_or(team.name()).into(), + email: ws.email().map(|e| e.into()), + repo: ws.repo().map(|e| e.into()), + discord: ws.discord().map(|i| v1::DiscordInvite { + channel: i.channel.into(), + url: i.url.into(), + }), + weight: ws.weight(), + }), + }; + + self.add(&format!("v1/teams/{}.json", team.name()), &team_data)?; + teams.insert(team.name().into(), team_data); + } + + teams.sort_keys(); + self.add( + "v1/teams.json", + &v1::Teams { teams }, + )?; + Ok(()) + } + + fn add(&self, path: &str, obj: &T) -> Result<(), Error> { + info!("writing API object {}...", path); + let dest = self.dest.join(path); + if let Some(parent) = dest.parent() { + if !parent.exists() { + std::fs::create_dir_all(parent)?; + } + } + let json = serde_json::to_string_pretty(obj)?; + std::fs::write(&dest, json.as_bytes())?; + Ok(()) + } +} diff --git a/src/validate.rs b/src/validate.rs index c9c66a5..8b3208e 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,6 +1,6 @@ use crate::data::Data; -use failure::{bail, Error}; use crate::schema::Email; +use failure::{bail, ensure, Error}; use regex::Regex; use std::collections::HashSet; @@ -8,6 +8,7 @@ pub(crate) fn validate(data: &Data) -> Result<(), Error> { let mut errors = Vec::new(); validate_wg_names(data, &mut errors); + validate_subteam_of(data, &mut errors); validate_team_leads(data, &mut errors); validate_team_members(data, &mut errors); validate_inactive_members(data, &mut errors); @@ -44,6 +45,17 @@ fn validate_wg_names(data: &Data, errors: &mut Vec) { }); } +/// Ensure `subteam-of` points to an existing team +fn validate_subteam_of(data: &Data, errors: &mut Vec) { + let team_names: HashSet<_> = data.teams().map(|t| t.name()).collect(); + wrapper(data.teams(), errors, |team, _| { + if let Some(subteam_of) = team.subteam_of() { + ensure!(team_names.contains(subteam_of), "team `{}` doesn't exist", subteam_of); + } + Ok(()) + }); +} + /// Ensure team leaders are part of the teams they lead fn validate_team_leads(data: &Data, errors: &mut Vec) { wrapper(data.teams(), errors, |team, errors| { -- cgit v1.2.3