diff options
author | Pietro Albini <pietro@pietroalbini.org> | 2018-11-02 21:27:01 +0100 |
---|---|---|
committer | Pietro Albini <pietro@pietroalbini.org> | 2018-11-04 21:28:45 +0100 |
commit | 104979dd1c51fcdd7fd9f9c4c7c8290890d1675d (patch) | |
tree | 27adc4830a28e124ab10636a16a47083ad211b96 /src | |
parent | 2e0e7821670c242249d39c3d84ecef063c11b3cc (diff) |
move code around
Diffstat (limited to 'src')
-rw-r--r-- | src/main.rs | 260 | ||||
-rw-r--r-- | src/sync/lists.rs | 247 | ||||
-rw-r--r-- | src/sync/mod.rs | 1 |
3 files changed, 262 insertions, 246 deletions
diff --git a/src/main.rs b/src/main.rs index 49846da..c8aace9 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,60 +1,6 @@ -use std::cell::RefCell; -use std::collections::{HashMap, HashSet}; -use std::env; -use std::fs; -use std::str; +mod sync; -use curl::easy::{Easy, Form}; -use failure::{bail, format_err, Error, ResultExt}; - -#[derive(serde_derive::Deserialize)] -struct Mailmap { - lists: Vec<List>, -} - -#[derive(serde_derive::Deserialize)] -struct List { - address: String, - access_level: String, - members: Vec<String>, -} - -mod api { - #[derive(serde_derive::Deserialize)] - pub struct ListResponse { - pub items: Vec<List>, - pub paging: Paging, - } - - #[derive(serde_derive::Deserialize)] - pub struct List { - pub access_level: String, - pub address: String, - pub members_count: u64, - } - - #[derive(serde_derive::Deserialize)] - pub struct Paging { - pub first: String, - pub last: String, - pub next: String, - pub previous: String, - } - - #[derive(serde_derive::Deserialize)] - pub struct MembersResponse { - pub items: Vec<Member>, - pub paging: Paging, - } - - #[derive(serde_derive::Deserialize)] - pub struct Member { - pub address: String, - } -} - -#[derive(serde_derive::Deserialize)] -struct Empty {} +use failure::Error; fn main() { env_logger::init(); @@ -68,202 +14,24 @@ fn main() { } fn run() -> Result<(), Error> { - let mailmap = fs::read_to_string("mailmap.toml") - .with_context(|_| "failed to read `mailmap.toml`")?; - - let mailmap: Mailmap = toml::from_str(&mailmap) - .with_context(|_| "failed to deserialize toml mailmap")?; - - let mut lists = Vec::new(); - let mut response = get::<api::ListResponse>("/lists/pages")?; - while response.items.len() > 0 { - lists.extend(response.items); - response = get::<api::ListResponse>(&response.paging.next)?; - } - - let mut addr2list = HashMap::new(); - for list in mailmap.lists.iter() { - if addr2list.insert(&list.address, list).is_some() { - bail!("duplicate address: {}", list.address); - } + let args = std::env::args().skip(1).collect::<Vec<_>>(); + if args.len() != 1 { + usage(); } - for prev_list in lists { - let address = &prev_list.address; - match addr2list.remove(address) { - Some(new_list) => { - sync(&prev_list, &new_list) - .with_context(|_| format!("failed to sync {}", address))? - } - None => { - del(&prev_list) - .with_context(|_| format!("failed to delete {}", address))? - } + match args[0].as_str() { + "sync" => { + sync::lists::run()?; } - } - - for (_, list) in addr2list.iter() { - create(list) - .with_context(|_| format!("failed to create {}", list.address))?; + _ => usage(), } Ok(()) } -fn create(new: &List) -> Result<(), Error> { - let mut form = Form::new(); - form.part("address").contents(new.address.as_bytes()).add()?; - form.part("access_level").contents(new.access_level.as_bytes()).add()?; - post::<Empty>("/lists", form)?; - - add_members(&new.address, &new.members)?; - Ok(()) -} - -fn sync(prev: &api::List, new: &List) -> Result<(), Error> { - assert_eq!(prev.address, new.address); - let url = format!("/lists/{}", prev.address); - if prev.access_level != new.access_level { - let mut form = Form::new(); - form.part("access_level").contents(new.access_level.as_bytes()).add()?; - put::<Empty>(&url, form)?; - } - - let url = format!("{}/members/pages", url); - let mut prev_members = HashSet::new(); - let mut response = get::<api::MembersResponse>(&url)?; - while response.items.len() > 0 { - prev_members.extend(response.items.into_iter().map(|member| member.address)); - response = get::<api::MembersResponse>(&response.paging.next)?; - } - - let mut to_add = Vec::new(); - for member in new.members.iter() { - if !prev_members.remove(member) { - to_add.push(member.clone()); - } - } - - if to_add.len() > 0 { - add_members(&new.address, &to_add)?; - } - for member in prev_members { - delete::<Empty>(&format!("/lists/{}/members/{}", new.address, member))?; - } - - Ok(()) -} - -fn add_members(address: &str, members: &[String]) -> Result<(), Error> { - let url = format!("/lists/{}/members.json", address); - let data = serde_json::to_string(members)?; - let mut form = Form::new(); - form.part("members").contents(data.as_bytes()).add()?; - post::<Empty>(&url, form)?; - Ok(()) -} - -fn del(prev: &api::List) -> Result<(), Error> { - delete::<Empty>(&format!("/lists/{}", prev.address))?; - Ok(()) -} - -fn get<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, Error> { - execute(url, Method::Get) -} - -fn post<T: for<'de> serde::Deserialize<'de>>( - url: &str, - form: Form, -) -> Result<T, Error> { - execute(url, Method::Post(form)) -} - -fn put<T: for<'de> serde::Deserialize<'de>>( - url: &str, - form: Form, -) -> Result<T, Error> { - execute(url, Method::Put(form)) -} - -fn delete<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, Error> { - execute(url, Method::Delete) -} - -enum Method { - Get, - Delete, - Post(Form), - Put(Form), -} - -fn execute<T: for<'de> serde::Deserialize<'de>>( - url: &str, - method: Method, -) -> Result<T, Error> { - thread_local!(static HANDLE: RefCell<Easy> = RefCell::new(Easy::new())); - let password = env::var("MAILGUN_API_TOKEN") - .map_err(|_| format_err!("must set $MAILGUN_API_TOKEN"))?; - let result = HANDLE.with(|handle| { - let mut handle = handle.borrow_mut(); - handle.reset(); - let url = if url.starts_with("https://") { - url.to_string() - } else { - format!("https://api.mailgun.net/v3{}", url) - }; - handle.url(&url)?; - match method { - Method::Get => { - log::debug!("GET {}", url); - handle.get(true)?; - } - Method::Delete => { - log::debug!("DELETE {}", url); - handle.custom_request("DELETE")?; - } - Method::Post(form) => { - log::debug!("POST {}", url); - handle.httppost(form)?; - } - Method::Put(form) => { - log::debug!("PUT {}", url); - handle.httppost(form)?; - handle.custom_request("PUT")?; - } - } - handle.username("api")?; - handle.password(&password)?; - handle.useragent("rust-lang/rust membership update")?; - // handle.verbose(true)?; - let mut result = Vec::new(); - let mut headers = Vec::new(); - { - let mut transfer = handle.transfer(); - transfer.write_function(|data| { - result.extend_from_slice(data); - Ok(data.len()) - })?; - transfer.header_function(|header| { - if let Ok(s) = str::from_utf8(header) { - headers.push(s.to_string()); - } - true - })?; - transfer.perform()?; - } - - let result = String::from_utf8(result) - .map_err(|_| format_err!("response was invalid utf-8"))?; - - log::trace!("headers: {:#?}", headers); - log::trace!("json: {}", result); - let code = handle.response_code()?; - if code != 200 { - bail!("failed to get a 200 code, got {}\n\n{}", code, result) - } - Ok(serde_json::from_str(&result) - .with_context(|_| "failed to parse json response")?) - }); - Ok(result.with_context(|_| format!("failed to send request to {}", url))?) +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/sync/lists.rs b/src/sync/lists.rs new file mode 100644 index 0000000..d5c6710 --- /dev/null +++ b/src/sync/lists.rs @@ -0,0 +1,247 @@ +use curl::easy::{Easy, Form}; +use failure::{bail, format_err, Error, ResultExt}; +use std::cell::RefCell; +use std::collections::{HashMap, HashSet}; +use std::env; +use std::fs; +use std::str; + +#[derive(serde_derive::Deserialize)] +struct Mailmap { + lists: Vec<List>, +} + +#[derive(serde_derive::Deserialize)] +struct List { + address: String, + access_level: String, + members: Vec<String>, +} + +mod api { + #[derive(serde_derive::Deserialize)] + pub struct ListResponse { + pub items: Vec<List>, + pub paging: Paging, + } + + #[derive(serde_derive::Deserialize)] + pub struct List { + pub access_level: String, + pub address: String, + pub members_count: u64, + } + + #[derive(serde_derive::Deserialize)] + pub struct Paging { + pub first: String, + pub last: String, + pub next: String, + pub previous: String, + } + + #[derive(serde_derive::Deserialize)] + pub struct MembersResponse { + pub items: Vec<Member>, + pub paging: Paging, + } + + #[derive(serde_derive::Deserialize)] + pub struct Member { + pub address: String, + } +} + +#[derive(serde_derive::Deserialize)] +struct Empty {} + +pub(crate) fn run() -> Result<(), Error> { + let mailmap = + fs::read_to_string("mailmap.toml").with_context(|_| "failed to read `mailmap.toml`")?; + + let mailmap: Mailmap = + toml::from_str(&mailmap).with_context(|_| "failed to deserialize toml mailmap")?; + + let mut lists = Vec::new(); + let mut response = get::<api::ListResponse>("/lists/pages")?; + while response.items.len() > 0 { + lists.extend(response.items); + response = get::<api::ListResponse>(&response.paging.next)?; + } + + let mut addr2list = HashMap::new(); + for list in mailmap.lists.iter() { + if addr2list.insert(&list.address, list).is_some() { + bail!("duplicate address: {}", list.address); + } + } + + for prev_list in lists { + let address = &prev_list.address; + match addr2list.remove(address) { + Some(new_list) => sync(&prev_list, &new_list) + .with_context(|_| format!("failed to sync {}", address))?, + None => del(&prev_list).with_context(|_| format!("failed to delete {}", address))?, + } + } + + for (_, list) in addr2list.iter() { + create(list).with_context(|_| format!("failed to create {}", list.address))?; + } + + Ok(()) +} + +fn create(new: &List) -> Result<(), Error> { + let mut form = Form::new(); + form.part("address") + .contents(new.address.as_bytes()) + .add()?; + form.part("access_level") + .contents(new.access_level.as_bytes()) + .add()?; + post::<Empty>("/lists", form)?; + + add_members(&new.address, &new.members)?; + Ok(()) +} + +fn sync(prev: &api::List, new: &List) -> Result<(), Error> { + assert_eq!(prev.address, new.address); + let url = format!("/lists/{}", prev.address); + if prev.access_level != new.access_level { + let mut form = Form::new(); + form.part("access_level") + .contents(new.access_level.as_bytes()) + .add()?; + put::<Empty>(&url, form)?; + } + + let url = format!("{}/members/pages", url); + let mut prev_members = HashSet::new(); + let mut response = get::<api::MembersResponse>(&url)?; + while response.items.len() > 0 { + prev_members.extend(response.items.into_iter().map(|member| member.address)); + response = get::<api::MembersResponse>(&response.paging.next)?; + } + + let mut to_add = Vec::new(); + for member in new.members.iter() { + if !prev_members.remove(member) { + to_add.push(member.clone()); + } + } + + if to_add.len() > 0 { + add_members(&new.address, &to_add)?; + } + for member in prev_members { + delete::<Empty>(&format!("/lists/{}/members/{}", new.address, member))?; + } + + Ok(()) +} + +fn add_members(address: &str, members: &[String]) -> Result<(), Error> { + let url = format!("/lists/{}/members.json", address); + let data = serde_json::to_string(members)?; + let mut form = Form::new(); + form.part("members").contents(data.as_bytes()).add()?; + post::<Empty>(&url, form)?; + Ok(()) +} + +fn del(prev: &api::List) -> Result<(), Error> { + delete::<Empty>(&format!("/lists/{}", prev.address))?; + Ok(()) +} + +fn get<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, Error> { + execute(url, Method::Get) +} + +fn post<T: for<'de> serde::Deserialize<'de>>(url: &str, form: Form) -> Result<T, Error> { + execute(url, Method::Post(form)) +} + +fn put<T: for<'de> serde::Deserialize<'de>>(url: &str, form: Form) -> Result<T, Error> { + execute(url, Method::Put(form)) +} + +fn delete<T: for<'de> serde::Deserialize<'de>>(url: &str) -> Result<T, Error> { + execute(url, Method::Delete) +} + +enum Method { + Get, + Delete, + Post(Form), + Put(Form), +} + +fn execute<T: for<'de> serde::Deserialize<'de>>(url: &str, method: Method) -> Result<T, Error> { + thread_local!(static HANDLE: RefCell<Easy> = RefCell::new(Easy::new())); + let password = + env::var("MAILGUN_API_TOKEN").map_err(|_| format_err!("must set $MAILGUN_API_TOKEN"))?; + let result = HANDLE.with(|handle| { + let mut handle = handle.borrow_mut(); + handle.reset(); + let url = if url.starts_with("https://") { + url.to_string() + } else { + format!("https://api.mailgun.net/v3{}", url) + }; + handle.url(&url)?; + match method { + Method::Get => { + log::debug!("GET {}", url); + handle.get(true)?; + } + Method::Delete => { + log::debug!("DELETE {}", url); + handle.custom_request("DELETE")?; + } + Method::Post(form) => { + log::debug!("POST {}", url); + handle.httppost(form)?; + } + Method::Put(form) => { + log::debug!("PUT {}", url); + handle.httppost(form)?; + handle.custom_request("PUT")?; + } + } + handle.username("api")?; + handle.password(&password)?; + handle.useragent("rust-lang/rust membership update")?; + // handle.verbose(true)?; + let mut result = Vec::new(); + let mut headers = Vec::new(); + { + let mut transfer = handle.transfer(); + transfer.write_function(|data| { + result.extend_from_slice(data); + Ok(data.len()) + })?; + transfer.header_function(|header| { + if let Ok(s) = str::from_utf8(header) { + headers.push(s.to_string()); + } + true + })?; + transfer.perform()?; + } + + let result = + String::from_utf8(result).map_err(|_| format_err!("response was invalid utf-8"))?; + + log::trace!("headers: {:#?}", headers); + log::trace!("json: {}", result); + let code = handle.response_code()?; + if code != 200 { + bail!("failed to get a 200 code, got {}\n\n{}", code, result) + } + Ok(serde_json::from_str(&result).with_context(|_| "failed to parse json response")?) + }); + Ok(result.with_context(|_| format!("failed to send request to {}", url))?) +} diff --git a/src/sync/mod.rs b/src/sync/mod.rs new file mode 100644 index 0000000..ccefbed --- /dev/null +++ b/src/sync/mod.rs @@ -0,0 +1 @@ +pub(crate) mod lists; |