diff options
author | Pietro Albini <pietro@pietroalbini.org> | 2019-07-11 20:49:49 +0200 |
---|---|---|
committer | Pietro Albini <pietro@pietroalbini.org> | 2019-07-13 21:19:01 +0200 |
commit | 8578b7e3cdc93b583d5c25762ac36ec512c779f3 (patch) | |
tree | 78c5fde9f4f9cd6ea037a5ce0917bd1c09ace8a9 /src | |
parent | 474d58efe82228cafd02f025274eb411bbe0596a (diff) |
add github ids to the people tomls
Diffstat (limited to 'src')
-rw-r--r-- | src/github.rs | 86 | ||||
-rw-r--r-- | src/schema.rs | 7 | ||||
-rw-r--r-- | src/validate.rs | 36 |
3 files changed, 126 insertions, 3 deletions
diff --git a/src/github.rs b/src/github.rs index 9a62994..55bfc19 100644 --- a/src/github.rs +++ b/src/github.rs @@ -1,7 +1,8 @@ -use failure::{Error, ResultExt}; +use failure::{bail, Error, ResultExt}; use reqwest::header::{self, HeaderValue}; use reqwest::{Client, Method, RequestBuilder}; use std::borrow::Cow; +use std::collections::HashMap; static API_BASE: &str = "https://api.github.com/"; static TOKEN_VAR: &str = "GITHUB_TOKEN"; @@ -13,6 +14,23 @@ pub(crate) struct User { pub(crate) email: Option<String>, } +#[derive(serde::Deserialize)] +struct GraphResult<T> { + data: Option<T>, + #[serde(default)] + errors: Vec<GraphError>, +} + +#[derive(serde::Deserialize)] +struct GraphError { + message: String, +} + +#[derive(serde::Deserialize)] +struct GraphNodes<T> { + nodes: Vec<Option<T>>, +} + pub(crate) struct GitHubApi { http: Client, token: String, @@ -40,6 +58,31 @@ impl GitHubApi { )) } + fn graphql<R, V>(&self, query: &str, variables: V) -> Result<R, Error> + where + R: for<'de> serde::Deserialize<'de>, + V: serde::Serialize, + { + #[derive(serde::Serialize)] + struct Request<'a, V> { + query: &'a str, + variables: V, + } + let res: GraphResult<R> = self + .prepare(Method::POST, "graphql")? + .json(&Request { query, variables }) + .send()? + .error_for_status()? + .json()?; + if let Some(error) = res.errors.iter().next() { + bail!("graphql error: {}", error.message); + } else if let Some(data) = res.data { + Ok(data) + } else { + bail!("missing graphql data"); + } + } + pub(crate) fn user(&self, login: &str) -> Result<User, Error> { Ok(self .prepare(Method::GET, &format!("users/{}", login))? @@ -47,4 +90,45 @@ impl GitHubApi { .error_for_status()? .json()?) } + + pub(crate) fn usernames(&self, ids: &[usize]) -> Result<HashMap<usize, String>, Error> { + #[derive(serde::Deserialize)] + #[serde(rename_all = "camelCase")] + struct Usernames { + database_id: usize, + login: String, + } + #[derive(serde::Serialize)] + struct Params { + ids: Vec<String>, + } + static QUERY: &str = " + query($ids: [ID!]!) { + nodes(ids: $ids) { + ... on User { + databaseId + login + } + } + } + "; + + let mut result = HashMap::new(); + for chunk in ids.chunks(100) { + let res: GraphNodes<Usernames> = self.graphql( + QUERY, + Params { + ids: chunk.iter().map(|id| user_node_id(*id)).collect(), + }, + )?; + for node in res.nodes.into_iter().filter_map(|n| n) { + result.insert(node.database_id, node.login); + } + } + Ok(result) + } +} + +fn user_node_id(id: usize) -> String { + base64::encode(&format!("04:User{}", id)) } diff --git a/src/schema.rs b/src/schema.rs index c7a1b48..570b212 100644 --- a/src/schema.rs +++ b/src/schema.rs @@ -38,10 +38,11 @@ pub(crate) enum Email<'a> { } #[derive(serde_derive::Deserialize, Debug)] -#[serde(deny_unknown_fields)] +#[serde(deny_unknown_fields, rename_all = "kebab-case")] pub(crate) struct Person { name: String, github: String, + github_id: usize, irc: Option<String>, #[serde(default)] email: EmailField, @@ -59,6 +60,10 @@ impl Person { &self.github } + pub(crate) fn github_id(&self) -> usize { + self.github_id + } + #[allow(unused)] pub(crate) fn irc(&self) -> &str { if let Some(irc) = &self.irc { diff --git a/src/validate.rs b/src/validate.rs index 87f2354..3965a58 100644 --- a/src/validate.rs +++ b/src/validate.rs @@ -1,6 +1,8 @@ use crate::data::Data; +use crate::github::GitHubApi; use crate::schema::{Email, Permissions}; use failure::{bail, Error}; +use log::{error, warn}; use regex::Regex; use std::collections::{HashMap, HashSet}; @@ -23,6 +25,8 @@ static CHECKS: &[fn(&Data, &mut Vec<String>)] = &[ validate_team_names, ]; +static GITHUB_CHECKS: &[fn(&Data, &GitHubApi, &mut Vec<String>)] = &[validate_github_usernames]; + pub(crate) fn validate(data: &Data) -> Result<(), Error> { let mut errors = Vec::new(); @@ -30,12 +34,24 @@ pub(crate) fn validate(data: &Data) -> Result<(), Error> { check(data, &mut errors); } + match GitHubApi::new() { + Ok(github) => { + for check in GITHUB_CHECKS { + check(data, &github, &mut errors); + } + } + Err(err) => { + warn!("couldn't perform checks relying on the GitHub API, some errors will not be detected"); + warn!("cause: {}", err); + } + } + if !errors.is_empty() { errors.sort(); errors.dedup_by(|a, b| a == b); for err in &errors { - eprintln!("validation error: {}", err); + error!("validation error: {}", err); } bail!("{} validation errors found", errors.len()); @@ -363,6 +379,24 @@ fn validate_team_names(data: &Data, errors: &mut Vec<String>) { }); } +/// Ensure there are no misspelled GitHub account names +fn validate_github_usernames(data: &Data, github: &GitHubApi, errors: &mut Vec<String>) { + let people = data + .people() + .map(|p| (p.github_id(), p)) + .collect::<HashMap<_, _>>(); + match github.usernames(&people.keys().cloned().collect::<Vec<_>>()) { + Ok(res) => wrapper(res.iter(), errors, |(id, name), _| { + let original = people[id].github(); + if original != name { + bail!("user `{}` changed username to `{}`", original, name); + } + Ok(()) + }), + Err(err) => errors.push(format!("couldn't verify GitHub usernames: {}", err)), + } +} + fn wrapper<T, I, F>(iter: I, errors: &mut Vec<String>, mut func: F) where I: Iterator<Item = T>, |