summaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
authorPietro Albini <pietro@pietroalbini.org>2019-01-22 16:53:19 +0100
committerPietro Albini <pietro@pietroalbini.org>2019-01-22 23:22:42 +0100
commit260a5c06d9fb856dc0f65d97ba83d76bd4aaa945 (patch)
tree590f2b82efbb70fabae592df804b0980c73a4595 /src
parent9cec8b78f83aac75a2c503d46c8c2e914a27fb15 (diff)
add static api and fix website data
Diffstat (limited to 'src')
-rw-r--r--src/main.rs9
-rw-r--r--src/schema.rs54
-rw-r--r--src/static_api.rs93
-rw-r--r--src/validate.rs14
4 files changed, 167 insertions, 3 deletions
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<String>,
#[serde(default)]
children: Vec<String>,
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<HashSet<&'a str>, 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<String>,
email: Option<String>,
repo: Option<String>,
discord_invite: Option<String>,
discord_name: Option<String>,
+ #[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<DiscordInvite> {
+ 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<Generator<'a>, 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<T: serde::Serialize>(&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<String>) {
});
}
+/// Ensure `subteam-of` points to an existing team
+fn validate_subteam_of(data: &Data, errors: &mut Vec<String>) {
+ 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<String>) {
wrapper(data.teams(), errors, |team, errors| {