From c649fbf88ceac08635b08a18cb605e359d5c6968 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sat, 30 Jul 2022 01:02:54 +0200 Subject: [PATCH 1/8] Collect relevant ingredients for grocery list --- Cargo.lock | 34 +++++++++++ Cargo.toml | 6 ++ src/commands/groceries.rs | 107 +++++++++++++++++++++++++++++++++++ src/commands/mod.rs | 1 + src/commands/schedule_csv.rs | 11 +--- src/constants.rs | 8 ++- src/main.rs | 24 +++++++- 7 files changed, 178 insertions(+), 13 deletions(-) create mode 100644 src/commands/groceries.rs diff --git a/Cargo.lock b/Cargo.lock index 6bb7b82..ab7d06e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,15 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aho-corasick" +version = "0.7.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +dependencies = [ + "memchr", +] + [[package]] name = "android_system_properties" version = "0.1.4" @@ -171,7 +180,9 @@ dependencies = [ "futures", "iana-time-zone", "ics", + "markdown-gen", "minicaldav", + "regex", "reqwest", "rusty-hook", "serde", @@ -704,6 +715,12 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "markdown-gen" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8034621d7f1258317ca1dfb9205e3925d27ee4aa2a46620a09c567daf0310562" + [[package]] name = "matches" version = "0.1.9" @@ -1015,12 +1032,29 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c4eb3267174b8c6c2f654116623910a0fef09c4753f8dd83db29c48a0df988b" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + [[package]] name = "regex-automata" version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +[[package]] +name = "regex-syntax" +version = "0.6.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" + [[package]] name = "remove_dir_all" version = "0.5.3" diff --git a/Cargo.toml b/Cargo.toml index 06b7af8..798dedc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,9 +45,15 @@ version = "0.3" [dependencies.ics] version = "0.5" +[dependencies.markdown-gen] +version = "1.2" + [dependencies.minicaldav] version = "0.2" +[dependencies.regex] +version = "1.6" + [dependencies.reqwest] version = "0.11" features = ["json", "blocking"] diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs new file mode 100644 index 0000000..7d4068f --- /dev/null +++ b/src/commands/groceries.rs @@ -0,0 +1,107 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + crate::api_client::ApiClient, + crate::constants, + crate::recipe::{Ingredient, Recipe}, + anyhow::{anyhow, Result}, + chrono::{Duration, Local, NaiveDateTime}, + regex::Regex, + std::collections::HashSet, +}; + +pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Result<()> { + let ids = map_events_to_recipe_ids(api_client, calendar_name, days)?; + let ingredients = get_ingredients(api_client, ids).await?; + + todo!("Recipe ingredients: {:?}", ingredients) + + // let ingredients = merge_ingredients(ingredients); + // let md = prepare_grocery_list(&ingredients); + // save_grocery_list(api_client, "", md).await?; +} + +fn map_events_to_recipe_ids( + api_client: &ApiClient, + calendar_name: &str, + days: u32, +) -> Result> { + // minicaldav is quite hamfisted in retrieving events (it gets everything, every time). + // Consider crafting an XML request and use something else for ical parsing. + + let calendars = api_client + .get_calendars() + .map_err(|err| convert_caldav_err(err))?; + let calendar = calendars + .into_iter() + .filter(|c| c.name() == calendar_name) + .next() + .ok_or(anyhow!( + "Unable to find calendar '{}' on server", + calendar_name + ))?; + + let all_events = api_client + .get_events(&calendar) + .map_err(|err| convert_caldav_err(err))? + .0; + + let relevant_events = all_events.into_iter().filter(|ev| { + ev.get("PRODID") + .map(|p| p == constants::CALENDAR_PROVIDER) + .unwrap_or(false) + && { + let start_time = NaiveDateTime::parse_from_str( + ev.property("DTSTART").unwrap().value(), + "%Y%m%dT%H%M%S", + ) + .unwrap_or(NaiveDateTime::from_timestamp(0, 0)); + let now = Local::now().naive_local(); + start_time >= now && start_time < now + Duration::days(days as i64) + } + }); + + let recipe_id_regex: Regex = Regex::new(r"cookbook@(\d+)").unwrap(); + let recipe_ids = relevant_events + .flat_map(|event| { + event + .property("DESCRIPTION") + .iter() + .flat_map(|descr| recipe_id_regex.captures(descr.value())) + .flat_map(|c| c.get(1)) + .flat_map(|m| m.as_str().parse::()) + .collect::>() + }) + .collect::>(); + + Ok(recipe_ids) +} + +async fn get_ingredients( + api_client: &ApiClient, + recipe_ids: RecipeIds, +) -> Result> +where + RecipeIds: IntoIterator, +{ + let ingredients = recipe_ids.into_iter().map(|id: usize| async move { + // TODO code duplicated with schedule_csv::get_all_recipes + let recipe_url = format!("apps/cookbook/api/recipes/{id}"); + let response = api_client + .rest() + .get(api_client.base_url().join(&recipe_url).unwrap()) + .send() + .await + .expect(&format!("Cannot fetch recipe with id {}", id)); + + response.json::().await.map(|r| r.ingredients) + }); + + let ingredients = futures::future::try_join_all(ingredients).await?; + Ok(ingredients.into_iter().flatten().collect()) +} + +fn convert_caldav_err(err: minicaldav::Error) -> anyhow::Error { + anyhow!(format!("{:?}", err)) +} diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 32611c5..042df50 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -1,6 +1,7 @@ // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod groceries; pub mod import; pub mod init; pub mod schedule; diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 40769fa..1595129 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -110,15 +110,8 @@ async fn publish_events<'a, EventsIter>( where EventsIter: Iterator, { - let calendar_prototype: ics::ICalendar = ics::ICalendar::new( - "2.0", - format!( - "-//IDN {}//{} {}//EN", - constants::VENDOR, - env!("CARGO_PKG_NAME"), - env!("CARGO_PKG_VERSION") - ), - ); + let calendar_prototype: ics::ICalendar = + ics::ICalendar::new("2.0", constants::CALENDAR_PROVIDER); let dav_base = api_client .rest() diff --git a/src/constants.rs b/src/constants.rs index 4c99f75..7ea9183 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -2,4 +2,10 @@ // SPDX-License-Identifier: AGPL-3.0-or-later pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); -pub const VENDOR: &str = "montecristosoftware.eu"; +pub const CALENDAR_PROVIDER: &str = concat!( + "-//IDN montecristosoftware.eu//", + env!("CARGO_PKG_NAME"), + " ", + env!("CARGO_PKG_VERSION"), + "//EN" +); diff --git a/src/main.rs b/src/main.rs index 69aa993..cc0b3fd 100644 --- a/src/main.rs +++ b/src/main.rs @@ -41,10 +41,20 @@ fn setup_args() -> ArgMatches { .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), ) .subcommand( - Command::new("schedule") - .about("") + Command::new("groceries") + .about("Create a grocery list from scheduled calendar events") .arg(server_arg.clone()) - .arg(arg!(-d --days "") + .arg(arg!( "")) + .arg(arg!(-d --days "Number of days in the future to consider") + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) + .required(false) + .default_value("7")) + ) + .subcommand( + Command::new("schedule") + .about("TODO; not implemented yet (pick automatically recipes and create calendar events for them)") + .arg(server_arg.clone()) + .arg(arg!(-d --days "Number of days to schedule") .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) .required(false) .default_value("7")) @@ -77,6 +87,14 @@ async fn parse_args(args: &ArgMatches) -> Result<()> { .map(|s| s.as_str()); commands::import::with(&api_client, urls).await } + Some(("groceries", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + let calendar_name = sub_matches + .get_one::("calendar_name") + .expect(" is a mandatory parameter, it cannot be missing"); + let days = sub_matches.get_one::("days").unwrap(); + commands::groceries::with(&api_client, calendar_name.as_str(), *days).await + } Some(("schedule", sub_matches)) => { let api_client = get_api_client(&sub_matches, &configuration)?; commands::schedule::with(&api_client).await From ed5a6d2306bfe6e19f9ea6c6e597e3db98e17f41 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sat, 30 Jul 2022 17:33:29 +0200 Subject: [PATCH 2/8] Collect ingredients from recipes for grocery list --- .vscode/launch.json | 2 +- Cargo.lock | 199 +++++++++----------------------------- Cargo.toml | 13 +-- src/api_client.rs | 117 +++++++++++++++++----- src/commands/groceries.rs | 69 ++++--------- src/constants.rs | 2 + src/event.rs | 5 +- 7 files changed, 168 insertions(+), 239 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2d64fc4..1230984 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -18,7 +18,7 @@ "kind": "bin" } }, - "args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"], + "args": ["groceries", "Cucina"], "cwd": "${workspaceFolder}" }, { diff --git a/Cargo.lock b/Cargo.lock index ab7d06e..0d4da34 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,12 +2,6 @@ # It is not intended for manual editing. version = 3 -[[package]] -name = "adler" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" - [[package]] name = "aho-corasick" version = "0.7.18" @@ -117,12 +111,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "chunked_transfer" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff857943da45f546682664a79488be82e69e43c1a7a2307679ab9afb3a66d2e" - [[package]] name = "ci_info" version = "0.10.2" @@ -179,9 +167,10 @@ dependencies = [ "directories", "futures", "iana-time-zone", + "icalendar", "ics", + "libxml", "markdown-gen", - "minicaldav", "regex", "reqwest", "rusty-hook", @@ -191,7 +180,6 @@ dependencies = [ "strum_macros", "tokio", "toml", - "ureq", "webbrowser", ] @@ -211,15 +199,6 @@ version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" -[[package]] -name = "crc32fast" -version = "1.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" -dependencies = [ - "cfg-if", -] - [[package]] name = "csv" version = "1.1.6" @@ -325,16 +304,6 @@ dependencies = [ "instant", ] -[[package]] -name = "flate2" -version = "1.0.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82b0f4c27ad9f8bfd1f3208d882da2b09c301bc1c828fd3a00d0216d2fbbff6" -dependencies = [ - "crc32fast", - "miniz_oxide", -] - [[package]] name = "fnv" version = "1.0.7" @@ -605,6 +574,17 @@ dependencies = [ "winapi", ] +[[package]] +name = "icalendar" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf4c8ac7e9b7eefe850f6380c68ef5fd1300858c1ef65d121d602a9bab63c2f4" +dependencies = [ + "chrono", + "nom", + "uuid", +] + [[package]] name = "ics" version = "0.5.7" @@ -706,6 +686,17 @@ version = "0.2.126" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349d5a591cd28b49e1d1037471617a32ddcda5731b99419008085f72d5a53836" +[[package]] +name = "libxml" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "687f5a78939052c5d02865c0fe3ea2ce2acdca875f7f81db82f7aef256dd97ac" +dependencies = [ + "libc", + "pkg-config", + "vcpkg", +] + [[package]] name = "log" version = "0.4.17" @@ -740,26 +731,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" [[package]] -name = "minicaldav" -version = "0.2.0" +name = "minimal-lexical" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb263a7d12c40d5f200dda93b3665b9ae714d4fe64a6467938c92d974a579edb" -dependencies = [ - "base64", - "log", - "ureq", - "url", - "xmltree", -] - -[[package]] -name = "miniz_oxide" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f5c75688da582b8ffc1f1799e9db273f32133c49e048f614d22ec3256773ccc" -dependencies = [ - "adler", -] +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" [[package]] name = "mio" @@ -853,6 +828,16 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab250442c86f1850815b5d268639dff018c0627022bc1940eb2d642ca1ce12f0" +[[package]] +name = "nom" +version = "7.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8903e5a29a317527874d0402f867152a3d21c908bb0b933e416c65e301d4c36" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "num-integer" version = "0.1.45" @@ -1101,33 +1086,6 @@ dependencies = [ "winreg", ] -[[package]] -name = "ring" -version = "0.16.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" -dependencies = [ - "cc", - "libc", - "once_cell", - "spin", - "untrusted", - "web-sys", - "winapi", -] - -[[package]] -name = "rustls" -version = "0.20.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5aab8ee6c7097ed6057f43c187a62418d0c05a4bd5f18b3571db50ee0f9ce033" -dependencies = [ - "log", - "ring", - "sct", - "webpki", -] - [[package]] name = "rustversion" version = "1.0.8" @@ -1171,16 +1129,6 @@ dependencies = [ "windows-sys", ] -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "security-framework" version = "2.6.1" @@ -1273,12 +1221,6 @@ dependencies = [ "strum_macros", ] -[[package]] -name = "spin" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" - [[package]] name = "strsim" version = "0.10.0" @@ -1514,30 +1456,6 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" -[[package]] -name = "untrusted" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" - -[[package]] -name = "ureq" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b97acb4c28a254fd7a4aeec976c46a7fa404eac4d7c134b30c75144846d7cb8f" -dependencies = [ - "base64", - "chunked_transfer", - "encoding_rs", - "flate2", - "log", - "once_cell", - "rustls", - "url", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "2.2.2" @@ -1550,6 +1468,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "uuid" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc5cf98d8186244414c848017f0e2676b3fcb46807f6668a97dfe67359a3c4b7" +dependencies = [ + "getrandom", +] + [[package]] name = "vcpkg" version = "0.2.15" @@ -1679,25 +1606,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - -[[package]] -name = "webpki-roots" -version = "0.22.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1c760f0d366a6c24a02ed7816e23e691f5d92291f94d15e836006fd11b04daf" -dependencies = [ - "webpki", -] - [[package]] name = "widestring" version = "0.5.1" @@ -1786,18 +1694,3 @@ checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" dependencies = [ "winapi", ] - -[[package]] -name = "xml-rs" -version = "0.8.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3" - -[[package]] -name = "xmltree" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7d8a75eaf6557bb84a65ace8609883db44a29951042ada9b393151532e41fcb" -dependencies = [ - "xml-rs", -] diff --git a/Cargo.toml b/Cargo.toml index 798dedc..9e33d76 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -42,15 +42,19 @@ version = "4.0" [dependencies.futures] version = "0.3" +[dependencies.icalendar] +version = "0.13.0" +features = ["parser"] + [dependencies.ics] version = "0.5" +[dependencies.libxml] +version = "0.3" + [dependencies.markdown-gen] version = "1.2" -[dependencies.minicaldav] -version = "0.2" - [dependencies.regex] version = "1.6" @@ -78,8 +82,5 @@ version = "0.5" version = "1" features = ["rt-multi-thread", "net", "macros"] -[dependencies.ureq] -version = "2.5" - [dependencies.webbrowser] version = "0.7" diff --git a/src/api_client.rs b/src/api_client.rs index 33d7c43..59827b1 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -3,16 +3,15 @@ use { crate::config::Config, crate::constants, anyhow::anyhow, anyhow::Result, - base64::write::EncoderWriter as Base64Encoder, reqwest::Url, std::io::Write, + base64::write::EncoderWriter as Base64Encoder, chrono::DateTime, icalendar::CalendarComponent, + reqwest::Url, std::io::Write, std::ops::Range, }; pub struct ApiClient { rest: reqwest::Client, - agent: ureq::Agent, base_url: Url, caldav_base_url: Url, username: String, - password: String, } impl ApiClient { @@ -59,9 +58,7 @@ impl ApiClient { base_url, caldav_base_url, username: server.login_name.clone(), - password: server.password.clone(), rest: rest_client, - agent: ureq::Agent::new(), }) } @@ -77,27 +74,97 @@ impl ApiClient { &self.username } - pub fn get_calendars( + pub async fn get_events( &self, - ) -> core::result::Result, minicaldav::Error> { - minicaldav::get_calendars( - self.agent.clone(), - self.username(), - &self.password, - &self.caldav_base_url, - ) - } - - pub fn get_events( - &self, - calendar: &minicaldav::Calendar, - ) -> core::result::Result<(Vec, Vec), minicaldav::Error> + calendar_name: &str, + date_range: Range>, + ) -> Result> + where + Tz: chrono::TimeZone, + Tz::Offset: std::fmt::Display, { - minicaldav::get_events( - self.agent.clone(), - self.username(), - &self.password, - calendar, - ) + let report_method = reqwest::Method::from_bytes(b"REPORT").unwrap(); + let events_xml = self + .rest() + .request( + report_method, + // TODO extract into helper method + self.caldav_base_url.join(&format!( + "calendars/{}/{}", + self.username(), + calendar_name.to_lowercase().replace(" ", "-") + ))?, + ) + .header("Prefer", "return-minimal") + .header("Content-Type", "application/xml; charset=utf-8") + .header("Depth", 1) + .body(format!( + " + + + + + + + {} + + + + + + + + + + ", + constants::CALENDAR_PROVIDER, + date_range + .start + .naive_utc() + .format(constants::ICAL_UTCTIME_FMT) + .to_string(), + date_range + .end + .naive_utc() + .format(constants::ICAL_UTCTIME_FMT) + .to_string() + )) + .send() + .await? + .text() + .await?; + + let xml_doc = libxml::parser::Parser::default().parse_string(events_xml.as_bytes())?; + let mut xpath_ctx = libxml::xpath::Context::new(&xml_doc).unwrap(); + xpath_ctx + .register_namespace("cal", "urn:ietf:params:xml:ns:caldav") + .unwrap(); + let calendars_txt = xpath_ctx + .findnodes("//cal:calendar-data/text()", None) + .unwrap() + .into_iter() + .map(|n| icalendar::parser::unfold(&n.get_content())) + .collect::>(); + + let calendars = calendars_txt + .iter() + .map(|cal| icalendar::parser::read_calendar(cal).unwrap()) + .collect::>(); + let events = calendars + .into_iter() + .flat_map(|cal| { + cal.components + .into_iter() + .filter(|comp| comp.name == "VEVENT") + }) + .map(|comp| { + let event: CalendarComponent = comp.into(); + match event { + CalendarComponent::Event(e) => e, + _ => unreachable!(), + } + }); + + Ok(events.collect::>()) } } diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index 7d4068f..430ee62 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -3,16 +3,17 @@ use { crate::api_client::ApiClient, - crate::constants, crate::recipe::{Ingredient, Recipe}, - anyhow::{anyhow, Result}, - chrono::{Duration, Local, NaiveDateTime}, + anyhow::Result, + chrono::{Duration, Local}, + icalendar::Component, regex::Regex, std::collections::HashSet, + std::ops::Range, }; pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Result<()> { - let ids = map_events_to_recipe_ids(api_client, calendar_name, days)?; + let ids = map_events_to_recipe_ids(api_client, calendar_name, days).await?; let ingredients = get_ingredients(api_client, ids).await?; todo!("Recipe ingredients: {:?}", ingredients) @@ -22,57 +23,25 @@ pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Res // save_grocery_list(api_client, "", md).await?; } -fn map_events_to_recipe_ids( +async fn map_events_to_recipe_ids( api_client: &ApiClient, calendar_name: &str, days: u32, ) -> Result> { - // minicaldav is quite hamfisted in retrieving events (it gets everything, every time). - // Consider crafting an XML request and use something else for ical parsing. + let date_range = Range { + start: Local::now(), + end: Local::now() + Duration::days(days as i64), + }; - let calendars = api_client - .get_calendars() - .map_err(|err| convert_caldav_err(err))?; - let calendar = calendars - .into_iter() - .filter(|c| c.name() == calendar_name) - .next() - .ok_or(anyhow!( - "Unable to find calendar '{}' on server", - calendar_name - ))?; - - let all_events = api_client - .get_events(&calendar) - .map_err(|err| convert_caldav_err(err))? - .0; - - let relevant_events = all_events.into_iter().filter(|ev| { - ev.get("PRODID") - .map(|p| p == constants::CALENDAR_PROVIDER) - .unwrap_or(false) - && { - let start_time = NaiveDateTime::parse_from_str( - ev.property("DTSTART").unwrap().value(), - "%Y%m%dT%H%M%S", - ) - .unwrap_or(NaiveDateTime::from_timestamp(0, 0)); - let now = Local::now().naive_local(); - start_time >= now && start_time < now + Duration::days(days as i64) - } - }); + let all_events = api_client.get_events(calendar_name, date_range).await?; let recipe_id_regex: Regex = Regex::new(r"cookbook@(\d+)").unwrap(); - let recipe_ids = relevant_events - .flat_map(|event| { - event - .property("DESCRIPTION") - .iter() - .flat_map(|descr| recipe_id_regex.captures(descr.value())) - .flat_map(|c| c.get(1)) - .flat_map(|m| m.as_str().parse::()) - .collect::>() - }) + let recipe_ids = all_events + .iter() + .flat_map(|event| event.property_value("DESCRIPTION")) + .flat_map(|descr| recipe_id_regex.captures(descr)) + .flat_map(|c| c.get(1)) + .flat_map(|m| m.as_str().parse::()) .collect::>(); Ok(recipe_ids) @@ -101,7 +70,3 @@ where let ingredients = futures::future::try_join_all(ingredients).await?; Ok(ingredients.into_iter().flatten().collect()) } - -fn convert_caldav_err(err: minicaldav::Error) -> anyhow::Error { - anyhow!(format!("{:?}", err)) -} diff --git a/src/constants.rs b/src/constants.rs index 7ea9183..dce91a4 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -9,3 +9,5 @@ pub const CALENDAR_PROVIDER: &str = concat!( env!("CARGO_PKG_VERSION"), "//EN" ); +pub const ICAL_LOCALTIME_FMT: &str = "%Y%m%dT%H%M%S"; +pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ"; diff --git a/src/event.rs b/src/event.rs index 03c8f50..bc16bca 100644 --- a/src/event.rs +++ b/src/event.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use { + crate::constants, crate::recipe::Recipe, chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}, ics::escape_text, @@ -92,9 +93,9 @@ impl<'a> From for CalEvent<'a> { } fn dt_fmt(datetime: &DateTime) -> String { - datetime.format("%Y%m%dT%H%M%S").to_string() + datetime.format(constants::ICAL_LOCALTIME_FMT).to_string() } fn dt_utc_fmt(datetime: &DateTime) -> String { - datetime.format("%Y%m%dT%H%M%SZ").to_string() + datetime.format(constants::ICAL_UTCTIME_FMT).to_string() } From 1510eb4f3df89972c46dc2f713e97cb18241257b Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sat, 30 Jul 2022 23:30:08 +0200 Subject: [PATCH 3/8] Use only the icalendar crate instead of ics --- .vscode/launch.json | 20 ++++++++- Cargo.lock | 37 ----------------- Cargo.toml | 9 ----- src/commands/groceries.rs | 32 +++++++++++++-- src/commands/schedule_csv.rs | 32 +++++++++++---- src/constants.rs | 1 - src/event.rs | 78 +++++++++++++----------------------- src/helpers.rs | 32 +++++++++++++++ src/main.rs | 1 + src/recipe.rs | 8 ++-- 10 files changed, 136 insertions(+), 114 deletions(-) create mode 100644 src/helpers.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index 1230984..e880500 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,7 +6,7 @@ { "type": "lldb", "request": "launch", - "name": "Debug executable 'cook'", + "name": "Debug executable 'cook' -> groceries", "cargo": { "args": [ "build", @@ -21,6 +21,24 @@ "args": ["groceries", "Cucina"], "cwd": "${workspaceFolder}" }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'cook' -> schedule-csv", + "cargo": { + "args": [ + "build", + "--bin=cook", + "--package=cooking-schedule" + ], + "filter": { + "name": "cook", + "kind": "bin" + } + }, + "args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"], + "cwd": "${workspaceFolder}" + }, { "type": "lldb", "request": "launch", diff --git a/Cargo.lock b/Cargo.lock index 0d4da34..e6f93c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -11,15 +11,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "android_system_properties" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7ed72e1635e121ca3e79420540282af22da58be50de153d36f81ddc6b83aa9e" -dependencies = [ - "libc", -] - [[package]] name = "anyhow" version = "1.0.58" @@ -166,11 +157,8 @@ dependencies = [ "csv", "directories", "futures", - "iana-time-zone", "icalendar", - "ics", "libxml", - "markdown-gen", "regex", "reqwest", "rusty-hook", @@ -561,19 +549,6 @@ dependencies = [ "tokio-native-tls", ] -[[package]] -name = "iana-time-zone" -version = "0.1.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00c0d80ad9ca8d30ca648bf6cb1e3e3326d75071b76dbe143dd4a9cedcd58975" -dependencies = [ - "android_system_properties", - "core-foundation", - "js-sys", - "wasm-bindgen", - "winapi", -] - [[package]] name = "icalendar" version = "0.13.0" @@ -585,12 +560,6 @@ dependencies = [ "uuid", ] -[[package]] -name = "ics" -version = "0.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b891481ef6353e3b97118d4650469e379a39e4373a66908c12f99763182826b1" - [[package]] name = "ident_case" version = "1.0.1" @@ -706,12 +675,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "markdown-gen" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8034621d7f1258317ca1dfb9205e3925d27ee4aa2a46620a09c567daf0310562" - [[package]] name = "matches" version = "0.1.9" diff --git a/Cargo.toml b/Cargo.toml index 9e33d76..7ebd68b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,9 +26,6 @@ version = "0.13" version = "0.4" features = ["serde"] -[dependencies.iana-time-zone] -version = "0.1" - [dependencies.csv] version = "1.1" @@ -46,15 +43,9 @@ version = "0.3" version = "0.13.0" features = ["parser"] -[dependencies.ics] -version = "0.5" - [dependencies.libxml] version = "0.3" -[dependencies.markdown-gen] -version = "1.2" - [dependencies.regex] version = "1.6" diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index 430ee62..b887971 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -15,12 +15,13 @@ use { pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Result<()> { let ids = map_events_to_recipe_ids(api_client, calendar_name, days).await?; let ingredients = get_ingredients(api_client, ids).await?; + let ingredients = merge_ingredients(ingredients); + let md = prepare_grocery_list(&ingredients)?; - todo!("Recipe ingredients: {:?}", ingredients) + println!("{}", md); - // let ingredients = merge_ingredients(ingredients); - // let md = prepare_grocery_list(&ingredients); // save_grocery_list(api_client, "", md).await?; + Ok(()) } async fn map_events_to_recipe_ids( @@ -70,3 +71,28 @@ where let ingredients = futures::future::try_join_all(ingredients).await?; Ok(ingredients.into_iter().flatten().collect()) } + +fn merge_ingredients(mut ingredients: Vec) -> Vec { + ingredients.sort(); + + // TODO actual merging + + ingredients +} + +fn prepare_grocery_list(ingredients: &Vec) -> Result { + let mut out = String::new(); + use std::fmt::Write; + + writeln!( + out, + "# Grocery list - {}", + chrono::Local::now().format("%Y-%m-%d").to_string() + )?; + for ingredient in ingredients { + let ingredient = ingredient.0.as_str(); + writeln!(out, "* {}", ingredient)?; + } + + Ok(out) +} diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 1595129..f63ae7a 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -4,9 +4,9 @@ use { crate::api_client::ApiClient, crate::commands::import, - crate::constants, crate::event::{Event, Meal}, crate::recipe, + crate::{constants, helpers}, anyhow::{bail, Result}, chrono::naive::NaiveDate, futures::future::try_join_all, @@ -110,9 +110,6 @@ async fn publish_events<'a, EventsIter>( where EventsIter: Iterator, { - let calendar_prototype: ics::ICalendar = - ics::ICalendar::new("2.0", constants::CALENDAR_PROVIDER); - let dav_base = api_client .rest() .head(api_client.base_url().join("/.well-known/caldav")?) @@ -125,18 +122,37 @@ where calendar.to_lowercase().as_str().replace(" ", "-") ))?; - let calendar_prototype = &calendar_prototype; let calendar_url = &calendar_url; let update_requests = events.map(|ev| async move { let url = calendar_url.join(&format!("{}.ics", ev.uid)).unwrap(); - let mut cal = calendar_prototype.clone(); - cal.add_event(ev.into()); + let alarm_text_repr = format!( + "BEGIN:VALARM\nACTION:DISPLAY\nTRIGGER:-PT15M\nDESCRIPTION:{}\nEND:VALARM", + &helpers::ical_escape_text(&ev.recipe.name) + ); + + let cal = icalendar::Calendar::new() + .push::(ev.into()) + .done(); + + let cal_as_string = (&cal.to_string()) + .replacen( + // need to hack around inability to set PRODID in icalendar::Calendar + "PRODID:ICALENDAR-RS", + &format!("PRODID:{}", constants::CALENDAR_PROVIDER), + 1, + ) + .replacen( + // need to hack around inability to set VALARM in icalendar::Event + "END:VEVENT", + &format!("{}\nEND:VEVENT", alarm_text_repr), + 1, + ); api_client .rest() .put(url) .header("Content-Type", "text/calendar; charset=utf-8") - .body(cal.to_string()) + .body(cal_as_string) .send() .await }); diff --git a/src/constants.rs b/src/constants.rs index dce91a4..d1e38e0 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -9,5 +9,4 @@ pub const CALENDAR_PROVIDER: &str = concat!( env!("CARGO_PKG_VERSION"), "//EN" ); -pub const ICAL_LOCALTIME_FMT: &str = "%Y%m%dT%H%M%S"; pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ"; diff --git a/src/event.rs b/src/event.rs index bc16bca..8a35e20 100644 --- a/src/event.rs +++ b/src/event.rs @@ -2,12 +2,9 @@ // SPDX-License-Identifier: AGPL-3.0-or-later use { - crate::constants, crate::recipe::Recipe, - chrono::{DateTime, Datelike, Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone, Utc}, - ics::escape_text, - ics::properties as calprop, - ics::Event as CalEvent, + chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc}, + icalendar::Event as CalEvent, std::rc::Rc, }; @@ -47,55 +44,34 @@ impl Event { } } -impl<'a> From for CalEvent<'a> { +impl From for CalEvent { fn from(ev: Event) -> Self { - let start_time = Local - .from_local_datetime(&(*&ev.ends_at - ev.recipe.total_time())) - .unwrap(); - let end_time = Local.from_local_datetime(&ev.ends_at).unwrap(); - let timezone = iana_time_zone::get_timezone().unwrap(); + use icalendar::Component; - let mut event = ics::Event::new(ev.uid.clone(), dt_utc_fmt(&Utc::now())); - event.push(calprop::Summary::new(escape_text(ev.recipe.name.clone()))); - event.push(calprop::Description::new(format!( - "cookbook@{}", - ev.recipe.id - ))); - event.push(calprop::Location::new(escape_text(ev.recipe.url.clone()))); - - let mut dtstart = calprop::DtStart::new(dt_fmt(&start_time)); - dtstart.append(ics::parameters!("TZID" => timezone.clone())); - event.push(dtstart); - - let mut dtend = calprop::DtEnd::new(dt_fmt(&end_time)); - dtend.append(ics::parameters!("TZID" => timezone)); - event.push(dtend); - - // TODO make configurable yearly repetition, for now this suits my personal uses const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; - event.push(calprop::RRule::new(format!( - "FREQ=YEARLY;BYDAY={weekday};BYWEEKNO={weekno}", - weekday = DAY_NAMES - .get(start_time.weekday().num_days_from_monday() as usize) - .unwrap(), - weekno = start_time.iso_week().week(), - ))); + let start_time = ev.ends_at - ev.recipe.total_time(); - let mut trigger = calprop::Trigger::new("-PT15M"); - trigger.append(ics::parameters!("RELATED" => "START")); - let alarm = ics::Alarm::display( - trigger, - calprop::Description::new(escape_text(ev.recipe.name.clone())), - ); - event.add_alarm(alarm); - event + let cal_event = CalEvent::new() + .uid(&ev.uid) + .summary(&ev.recipe.name) + .description(&format!("cookbook@{}", ev.recipe.id)) + .location(&ev.recipe.url) + .timestamp(Utc::now()) + .starts(start_time) + .ends(ev.ends_at) + .add_property( + // TODO make configurable yearly repetition, for now this suits my personal uses + "RRULE", + &format!( + "FREQ=YEARLY;BYDAY={weekday};BYWEEKNO={weekno}", + weekday = DAY_NAMES + .get(start_time.weekday().num_days_from_monday() as usize) + .unwrap(), + weekno = start_time.iso_week().week(), + ), + ) + .done(); + + cal_event } } - -fn dt_fmt(datetime: &DateTime) -> String { - datetime.format(constants::ICAL_LOCALTIME_FMT).to_string() -} - -fn dt_utc_fmt(datetime: &DateTime) -> String { - datetime.format(constants::ICAL_UTCTIME_FMT).to_string() -} diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 0000000..1b86316 --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,32 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +pub fn ical_escape_text(text: &str) -> String { + /* https://www.kanzaki.com/docs/ical/text.html + + The "TEXT" property values may also contain special characters that + are used to signify delimiters, such as a COMMA character for lists of + values or a SEMICOLON character for structured values. In order to + support the inclusion of these special characters in "TEXT" property + values, they MUST be escaped with a BACKSLASH character. A BACKSLASH + character (US-ASCII decimal 92) in a "TEXT" property value MUST be + escaped with another BACKSLASH character. A COMMA character in a "TEXT" + property value MUST be escaped with a BACKSLASH character + (US-ASCII decimal 92). A SEMICOLON character in a "TEXT" property + value MUST be escaped with a BACKSLASH character (US-ASCII decimal + 92). However, a COLON character in a "TEXT" property value SHALL + NOT be escaped with a BACKSLASH character. + */ + + let mut out = Vec::::with_capacity(text.len()); + for c in text.as_bytes() { + match c { + b'\\' => out.extend_from_slice(&[b'\\', b'\\']), + b',' => out.extend_from_slice(&[b'\\', b',']), + b';' => out.extend_from_slice(&[b'\\', b';']), + _ => out.push(*c), + } + } + + unsafe { String::from_utf8_unchecked(out) } +} diff --git a/src/main.rs b/src/main.rs index cc0b3fd..deaa747 100644 --- a/src/main.rs +++ b/src/main.rs @@ -6,6 +6,7 @@ mod commands; mod config; mod constants; mod event; +mod helpers; mod recipe; use { diff --git a/src/recipe.rs b/src/recipe.rs index 7dfd1df..ba7a504 100644 --- a/src/recipe.rs +++ b/src/recipe.rs @@ -60,17 +60,17 @@ pub struct Recipe { //pub nutrition: Nutrition, } -#[derive(Deserialize, Debug)] +#[derive(Deserialize, Debug, Ord, Eq, PartialEq, PartialOrd)] #[serde(rename_all = "camelCase")] -pub struct Ingredient(String); +pub struct Ingredient(pub String); #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct Tool(String); +pub struct Tool(pub String); #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] -pub struct Instruction(String); +pub struct Instruction(pub String); #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] From e1b6017f061498df76f517de6cd34d703da4698b Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Sun, 31 Jul 2022 13:41:43 +0200 Subject: [PATCH 4/8] Save back markdown file to server --- src/commands/groceries.rs | 73 +++++++++++++++++++++++++++++++++++---- src/main.rs | 6 ++-- 2 files changed, 70 insertions(+), 9 deletions(-) diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index b887971..7ddbd56 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -1,26 +1,32 @@ +use reqwest::Method; + // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later use { crate::api_client::ApiClient, crate::recipe::{Ingredient, Recipe}, - anyhow::Result, + anyhow::{anyhow, Result}, chrono::{Duration, Local}, icalendar::Component, regex::Regex, + reqwest::StatusCode, std::collections::HashSet, std::ops::Range, }; -pub async fn with(api_client: &ApiClient, calendar_name: &str, days: u32) -> Result<()> { +pub async fn with( + api_client: &ApiClient, + calendar_name: &str, + location: &str, + days: u32, +) -> Result<()> { let ids = map_events_to_recipe_ids(api_client, calendar_name, days).await?; let ingredients = get_ingredients(api_client, ids).await?; let ingredients = merge_ingredients(ingredients); let md = prepare_grocery_list(&ingredients)?; - - println!("{}", md); - - // save_grocery_list(api_client, "", md).await?; + // TODO filename is hardcoded for now + save_grocery_list(api_client, location, &md).await?; Ok(()) } @@ -89,10 +95,63 @@ fn prepare_grocery_list(ingredients: &Vec) -> Result { "# Grocery list - {}", chrono::Local::now().format("%Y-%m-%d").to_string() )?; + writeln!(out)?; // leave an empty line for ingredient in ingredients { let ingredient = ingredient.0.as_str(); - writeln!(out, "* {}", ingredient)?; + writeln!(out, "- [ ] {}", ingredient)?; } Ok(out) } + +async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &str) -> Result<()> { + let dav_base_url = api_client + .base_url() + .join(&format!("remote.php/dav/files/{}/", api_client.username()))?; + + let filename_components = filename.split('/').collect::>(); + filename_components + .iter() + .take(filename_components.len() - 1) + .fold(Ok(dav_base_url.clone()), |url, dir| { + url.map(|u| u.join(&format!("{dir}/")).unwrap()) + .and_then(|url| { + futures::executor::block_on(async { + let response = api_client + .rest() + .request(Method::from_bytes(b"MKCOL").unwrap(), url.clone()) + .send() + .await; + + match response.map(|r| r.status()) { + Ok(StatusCode::OK) + | Ok(StatusCode::METHOD_NOT_ALLOWED /* already exists */) => Ok(url), + Ok(status) => Err(anyhow!( + "Could not create WebDAV collection {}, server responded with {}", + &url, + status + )), + Err(e) => Err(anyhow!(e)), + } + }) + }) + })?; + + let file_url = dav_base_url.join(filename).unwrap(); + let response = api_client + .rest() + .put(file_url.clone()) + .header("Content-Type", "text/markdown; charset=utf-8") + .body(contents.to_owned()) + .send() + .await?; + + match response.status() { + StatusCode::CREATED | StatusCode::NO_CONTENT => Ok(()), + status => Err(anyhow!( + "Cannot save grocery list at {}, server responded with status {}", + file_url, + status + )), + } +} diff --git a/src/main.rs b/src/main.rs index deaa747..d9ec382 100644 --- a/src/main.rs +++ b/src/main.rs @@ -45,7 +45,8 @@ fn setup_args() -> ArgMatches { Command::new("groceries") .about("Create a grocery list from scheduled calendar events") .arg(server_arg.clone()) - .arg(arg!( "")) + .arg(arg!( "The name of the calendar to read from for scheduled recipes")) + .arg(arg!( "The relative path for the Markdown file that will be saved on the server. Recommended ending it in '.md'.")) .arg(arg!(-d --days "Number of days in the future to consider") .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) .required(false) @@ -94,7 +95,8 @@ async fn parse_args(args: &ArgMatches) -> Result<()> { .get_one::("calendar_name") .expect(" is a mandatory parameter, it cannot be missing"); let days = sub_matches.get_one::("days").unwrap(); - commands::groceries::with(&api_client, calendar_name.as_str(), *days).await + let filename = sub_matches.get_one::("filename").unwrap(); + commands::groceries::with(&api_client, calendar_name.as_str(), filename, *days).await } Some(("schedule", sub_matches)) => { let api_client = get_api_client(&sub_matches, &configuration)?; From 7f48c6cfa4b7ce6f64c69977781762e993f0d205 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Mon, 1 Aug 2022 00:20:52 +0200 Subject: [PATCH 5/8] Add deny checks --- .gitlab-ci.yml | 48 ++++++----- .rusty-hook.toml | 2 +- Cargo.lock | 110 ++++++++++++------------ Cargo.toml | 3 +- Dockerfile | 10 ++- deny.toml | 214 +++++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 310 insertions(+), 77 deletions(-) create mode 100644 deny.toml diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 7ba9af6..61516aa 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -3,6 +3,7 @@ stages: - build + - check - test - deploy @@ -12,25 +13,6 @@ stages: - job: docker:build optional: true -.cargo_test_template: - extends: .with_rust_image - stage: test - artifacts: - reports: - coverage_report: - coverage_format: cobertura - path: cobertura.xml - junit: junit.xml - variables: - RUST_BACKTRACE: 1 - CARGO_COMMON_ARGS: --workspace --no-default-features - script: - - mkdir -p .git/hooks # for cargo-husky - - cargo tarpaulin ${CARGO_COMMON_ARGS} --locked -o Xml - - cargo test ${CARGO_COMMON_ARGS} -- -Z unstable-options --format json | tee test-results.json - - cargo2junit < test-results.json > junit.xml - # - cargo bench ${CARGO_COMMON_ARGS} # DISABLED UNTIL WE HAVE BENCH TESTS - docker:build: stage: build variables: @@ -49,8 +31,34 @@ docker:build: - Dockerfile allow_failure: true +cargo:check: + extends: .with_rust_image + stage: check + variables: + CARGO_COMMON_ARGS: --workspace --no-default-features + script: + - reuse lint + - cargo fmt -- --check + - cargo deny check + cargo:test: - extends: .cargo_test_template + extends: .with_rust_image + stage: test + artifacts: + reports: + coverage_report: + coverage_format: cobertura + path: cobertura.xml + junit: junit.xml + variables: + RUST_BACKTRACE: 1 + CARGO_COMMON_ARGS: --workspace --no-default-features + script: + - mkdir -p .git/hooks # for rusty-hook + - cargo tarpaulin ${CARGO_COMMON_ARGS} --locked -o Xml + - cargo test ${CARGO_COMMON_ARGS} -- -Z unstable-options --format json | tee test-results.json + - cargo2junit < test-results.json > junit.xml + # - cargo bench ${CARGO_COMMON_ARGS} # DISABLED UNTIL WE HAVE BENCH TESTS cargo:doc: # pages: in the future extends: .with_rust_image diff --git a/.rusty-hook.toml b/.rusty-hook.toml index a9e2c47..cdca349 100644 --- a/.rusty-hook.toml +++ b/.rusty-hook.toml @@ -2,5 +2,5 @@ # SPDX-License-Identifier: CC0-1.0 [hooks] -pre-commit = "reuse lint && cargo fmt -- --check && cargo test" +pre-commit = "reuse lint && cargo deny check && cargo fmt -- --check && cargo test" pre-push = "cargo check" \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index e6f93c7..d03e3bd 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -66,9 +66,9 @@ checksum = "37ccbd214614c6783386c1af30caf03192f17891059cecc394b4fb119e363de3" [[package]] name = "bytes" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8" +checksum = "ec8a7b6a70fde80372154c65702f00a0f56f3e1c36abbc6c440484be248856db" [[package]] name = "cc" @@ -113,9 +113,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.8" +version = "3.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190814073e85d238f31ff738fcb0bf6910cedeb73376c87cd69291028966fd83" +checksum = "a3dbbb6653e7c55cc8595ad3e1f7be8f32aba4eb7ff7f0fd1163d4f3d137c0a9" dependencies = [ "atty", "bitflags", @@ -285,9 +285,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.7.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fcf0cee53519c866c09b5de1f6c56ff9d647101f81c1964fa632e148896cdf" +checksum = "a7a407cfaa3385c4ae6b23e84623d48c2798d06e3e6a1878f7f59f17b3f86499" dependencies = [ "instant", ] @@ -459,9 +459,9 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "heck" @@ -514,9 +514,9 @@ checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" [[package]] name = "hyper" -version = "0.14.19" +version = "0.14.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42dc3c131584288d375f2d07f822b0cb012d8c6fb899a5b9fdb3cb7eb9b6004f" +checksum = "02c929dc5c39e335a03c405292728118860721b10190d98c2a0f0efd5baafbac" dependencies = [ "bytes", "futures-channel", @@ -636,9 +636,9 @@ checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" [[package]] name = "js-sys" -version = "0.3.58" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3fac17f7123a73ca62df411b1bf727ccc805daa070338fda671c86dac1bdc27" +checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" dependencies = [ "wasm-bindgen", ] @@ -853,15 +853,15 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.12.0" +version = "1.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7709cef83f0c1f58f666e746a08b21e0085f7440fa6a29cc194d68aac97a4225" +checksum = "18a6dbe30758c9f83eb00cbea4ac95966305f5a7772f3f42ebfc7fc7eddbd8e1" [[package]] name = "openssl" -version = "0.10.40" +version = "0.10.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fb81a6430ac911acb25fe5ac8f1d2af1b4ea8a4fdfda0f1ee4292af2e2d8eb0e" +checksum = "618febf65336490dfcf20b73f885f5651a0c89c64c2d4a8c3662585a70bf5bd0" dependencies = [ "bitflags", "cfg-if", @@ -891,9 +891,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.74" +version = "0.9.75" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "835363342df5fba8354c5b453325b110ffd54044e588c539cf2f20a8014e4cb1" +checksum = "e5f9bd0c2710541a3cda73d6f9ac4f1b240de4ae261065d309dbe73d9dceb42f" dependencies = [ "autocfg", "cc", @@ -904,9 +904,9 @@ dependencies = [ [[package]] name = "os_str_bytes" -version = "6.1.0" +version = "6.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21326818e99cfe6ce1e524c2a805c189a99b5ae555a35d19f9a284b427d86afa" +checksum = "648001efe5d5c0102d8cea768e348da85d90af8ba91f0bea908f157951493cd4" [[package]] name = "percent-encoding" @@ -944,9 +944,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.40" +version = "1.0.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd96a1e8ed2596c337f8eae5f24924ec83f5ad5ab21ea8e455d3566c69fbcaf7" +checksum = "c278e965f1d8cf32d6e0e96de3d3e79712178ae67986d9cf9151f51e95aac89b" dependencies = [ "unicode-ident", ] @@ -962,9 +962,9 @@ dependencies = [ [[package]] name = "redox_syscall" -version = "0.2.13" +version = "0.2.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62f25bc4c7e55e0b0b7a1d43fb893f4fa1361d0abe38b9ce4f323c2adfe6ef42" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" dependencies = [ "bitflags", ] @@ -1117,18 +1117,18 @@ dependencies = [ [[package]] name = "serde" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1" +checksum = "fc855a42c7967b7c369eb5860f7164ef1f6f81c20c7cc1141f2a604e18723b03" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.137" +version = "1.0.140" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be" +checksum = "6f2122636b9fe3b81f1cb25099fcf2d3f542cdb1d45940d56c713158884a05da" dependencies = [ "proc-macro2", "quote", @@ -1160,9 +1160,12 @@ dependencies = [ [[package]] name = "slab" -version = "0.4.6" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb703cfe953bccee95685111adeedb76fabe4e97549a58d16f03ea7b9367bb32" +checksum = "4614a76b2a8be0058caa9dbbaf66d988527d86d003c11a94fbd335d7661edcef" +dependencies = [ + "autocfg", +] [[package]] name = "socket2" @@ -1300,10 +1303,11 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.19.2" +version = "1.20.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "7a8325f63a7d4774dd041e363b2409ed1c5cbbd0f867795e661df066b2b0a581" dependencies = [ + "autocfg", "bytes", "libc", "memchr", @@ -1368,9 +1372,9 @@ checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.35" +version = "0.1.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a400e31aa60b9d44a52a8ee0343b5b18566b03a8321e0d321f695cf56e940160" +checksum = "2fce9567bd60a67d08a16488756721ba392f24f29006402881e43b19aac64307" dependencies = [ "cfg-if", "pin-project-lite", @@ -1379,9 +1383,9 @@ dependencies = [ [[package]] name = "tracing-core" -version = "0.1.28" +version = "0.1.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b7358be39f2f274f322d2aaed611acc57f382e8eb1e5b48cb9ae30933495ce7" +checksum = "5aeea4303076558a00714b823f9ad67d58a3bbda1df83d8827d21193156e22f7" dependencies = [ "once_cell", ] @@ -1400,9 +1404,9 @@ checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992" [[package]] name = "unicode-ident" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bd2fe26506023ed7b5e1e315add59d6f584c621d037f9368fea9cfb988f368c" +checksum = "15c61ba63f9235225a22310255a29b806b907c9b8c964bcbd0a2c70f3f2deea7" [[package]] name = "unicode-normalization" @@ -1481,9 +1485,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c53b543413a17a202f4be280a7e5c62a1c69345f5de525ee64f8cfdbc954994" +checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -1491,13 +1495,13 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5491a68ab4500fa6b4d726bd67408630c3dbe9c4fe7bda16d5c82a1fd8c7340a" +checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" dependencies = [ "bumpalo", - "lazy_static", "log", + "once_cell", "proc-macro2", "quote", "syn", @@ -1506,9 +1510,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.31" +version = "0.4.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "de9a9cec1733468a8c657e57fa2413d2ae2c0129b95e87c5b72b8ace4d13f31f" +checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad" dependencies = [ "cfg-if", "js-sys", @@ -1518,9 +1522,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c441e177922bc58f1e12c022624b6216378e5febc2f0533e41ba443d505b80aa" +checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1528,9 +1532,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d94ac45fcf608c1f45ef53e748d35660f168490c10b23704c7779ab8f5c3048" +checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" dependencies = [ "proc-macro2", "quote", @@ -1541,15 +1545,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.81" +version = "0.2.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a89911bd99e5f3659ec4acf9c4d93b0a90fe4a2a11f15328472058edc5261be" +checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" [[package]] name = "web-sys" -version = "0.3.58" +version = "0.3.59" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fed94beee57daf8dd7d51f2b15dc2bcde92d7a72304cdf662a4371008b71b90" +checksum = "ed055ab27f941423197eb86b2035720b1a3ce40504df082cac2ecc6ed73335a1" dependencies = [ "js-sys", "wasm-bindgen", diff --git a/Cargo.toml b/Cargo.toml index 7ebd68b..7a7aafe 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,8 @@ version = "0.13" [dependencies.chrono] version = "0.4" -features = ["serde"] +default-features = false +features = ["alloc", "serde"] [dependencies.csv] version = "1.1" diff --git a/Dockerfile b/Dockerfile index 91148de..7faef57 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ # SPDX-FileCopyrightText: 2021 Matteo Settenvini # SPDX-License-Identifier: CC0-1.0 -FROM docker.io/rust:latest@sha256:490d4448c29f5407ee6eaf62f081a82b2741b4416c8e20aef7c01b68776802c2 +FROM docker.io/rust:latest@sha256:4f1d43c216d995c2f734d7c682bc3e2abe3e110961cda4ae0743ce3944e673a2 ENV DEBIAN_FRONTEND noninteractive @@ -9,11 +9,17 @@ RUN apt update && \ apt install -y \ libssl-dev \ pkg-config \ + python3-pip \ && \ rm -rf /var/lib/apt/lists/* -RUN cargo install -f -- \ +RUN pip3 install reuse + +RUN rustup component add rustfmt + +RUN cargo install --locked -f -- \ cargo-tarpaulin \ + cargo-deny \ cargo2junit \ && \ rm -rf "${HOME}/.cargo/registry" "${HOME}/.cargo/git" diff --git a/deny.toml b/deny.toml new file mode 100644 index 0000000..4a23271 --- /dev/null +++ b/deny.toml @@ -0,0 +1,214 @@ +# SPDX-FileCopyrightText: 2021 Matteo Settenvini +# SPDX-License-Identifier: CC0-1.0 + +# This template contains all of the possible sections and their default values + +# Note that all fields that take a lint level have these possible values: +# * deny - An error will be produced and the check will fail +# * warn - A warning will be produced, but the check will not fail +# * allow - No warning or error will be produced, though in some cases a note +# will be + +# The values provided in this template are the default values that will be used +# when any section or field is not specified in your own configuration + +# If 1 or more target triples (and optionally, target_features) are specified, +# only the specified targets will be checked when running `cargo deny check`. +# This means, if a particular package is only ever used as a target specific +# dependency, such as, for example, the `nix` crate only being used via the +# `target_family = "unix"` configuration, that only having windows targets in +# this list would mean the nix crate, as well as any of its exclusive +# dependencies not shared by any other crates, would be ignored, as the target +# list here is effectively saying which targets you are building for. +targets = [ + # The triple can be any string, but only the target triples built in to + # rustc (as of 1.40) can be checked against actual config expressions + #{ triple = "x86_64-unknown-linux-musl" }, + # You can also specify which target_features you promise are enabled for a + # particular target. target_features are currently not validated against + # the actual valid features supported by the target architecture. + #{ triple = "wasm32-unknown-unknown", features = ["atomics"] }, +] + +# This section is considered when running `cargo deny check advisories` +# More documentation for the advisories section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/advisories/cfg.html +[advisories] +# The path where the advisory database is cloned/fetched into +db-path = "~/.cargo/advisory-db" +# The url(s) of the advisory databases to use +db-urls = ["https://github.com/rustsec/advisory-db"] +# The lint level for security vulnerabilities +vulnerability = "deny" +# The lint level for unmaintained crates +unmaintained = "warn" +# The lint level for crates that have been yanked from their source registry +yanked = "warn" +# The lint level for crates with security notices. +notice = "warn" +# A list of advisory IDs to ignore. Note that ignored advisories will still +# output a note when they are encountered. +ignore = [ + #"RUSTSEC-0000-0000", + "RUSTSEC-2020-0071", +] +# Threshold for security vulnerabilities, any vulnerability with a CVSS score +# lower than the range specified will be ignored. Note that ignored advisories +# will still output a note when they are encountered. +# * None - CVSS Score 0.0 +# * Low - CVSS Score 0.1 - 3.9 +# * Medium - CVSS Score 4.0 - 6.9 +# * High - CVSS Score 7.0 - 8.9 +# * Critical - CVSS Score 9.0 - 10.0 +#severity-threshold = + +# If this is true, then cargo deny will use the git executable to fetch advisory database. +# If this is false, then it uses a built-in git library. +# Setting this to true can be helpful if you have special authentication requirements that cargo-deny does not support. +# See Git Authentication for more information about setting up git authentication. +#git-fetch-with-cli = true + +# This section is considered when running `cargo deny check licenses` +# More documentation for the licenses section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/licenses/cfg.html +[licenses] +# The lint level for crates which do not have a detectable license +unlicensed = "deny" +# List of explicitly allowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +allow = [ + #"MIT", + #"Apache-2.0", + #"Apache-2.0 WITH LLVM-exception", +] +# List of explicitly disallowed licenses +# See https://spdx.org/licenses/ for list of possible licenses +# [possible values: any SPDX 3.11 short identifier (+ optional exception)]. +deny = [ + #"Nokia", +] +# Lint level for licenses considered copyleft +copyleft = "allow" +# Blanket approval or denial for OSI-approved or FSF Free/Libre licenses +# * both - The license will be approved if it is both OSI-approved *AND* FSF +# * either - The license will be approved if it is either OSI-approved *OR* FSF +# * osi-only - The license will be approved if is OSI-approved *AND NOT* FSF +# * fsf-only - The license will be approved if is FSF *AND NOT* OSI-approved +# * neither - This predicate is ignored and the default lint level is used +allow-osi-fsf-free = "both" +# Lint level used when no other predicates are matched +# 1. License isn't in the allow or deny lists +# 2. License isn't copyleft +# 3. License isn't OSI/FSF, or allow-osi-fsf-free = "neither" +default = "deny" +# The confidence threshold for detecting a license from license text. +# The higher the value, the more closely the license text must be to the +# canonical license text of a valid SPDX license file. +# [possible values: any between 0.0 and 1.0]. +confidence-threshold = 0.8 +# Allow 1 or more licenses on a per-crate basis, so that particular licenses +# aren't accepted for every possible crate as with the normal allow list +exceptions = [ + # Each entry is the crate and version constraint, and its specific allow + # list + #{ allow = ["Zlib"], name = "adler32", version = "*" }, + { allow = ["Unicode-DFS-2016"], name = "unicode-ident", version = "*" }, +] + +# Some crates don't have (easily) machine readable licensing information, +# adding a clarification entry for it allows you to manually specify the +# licensing information +#[[licenses.clarify]] +# The name of the crate the clarification applies to +#name = "ring" +# The optional version constraint for the crate +#version = "*" +# The SPDX expression for the license requirements of the crate +#expression = "MIT AND ISC AND OpenSSL" +# One or more files in the crate's source used as the "source of truth" for +# the license expression. If the contents match, the clarification will be used +# when running the license check, otherwise the clarification will be ignored +# and the crate will be checked normally, which may produce warnings or errors +# depending on the rest of your configuration +#license-files = [ + # Each entry is a crate relative path, and the (opaque) hash of its contents + #{ path = "LICENSE", hash = 0xbd0eed23 } +#] + +[licenses.private] +# If true, ignores workspace crates that aren't published, or are only +# published to private registries. +# To see how to mark a crate as unpublished (to the official registry), +# visit https://doc.rust-lang.org/cargo/reference/manifest.html#the-publish-field. +ignore = false +# One or more private registries that you might publish crates to, if a crate +# is only published to private registries, and ignore is true, the crate will +# not have its license(s) checked +registries = [ + #"https://sekretz.com/registry +] + +# This section is considered when running `cargo deny check bans`. +# More documentation about the 'bans' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/bans/cfg.html +[bans] +# Lint level for when multiple versions of the same crate are detected +multiple-versions = "warn" +# Lint level for when a crate version requirement is `*` +wildcards = "allow" +# The graph highlighting used when creating dotgraphs for crates +# with multiple versions +# * lowest-version - The path to the lowest versioned duplicate is highlighted +# * simplest-path - The path to the version with the fewest edges is highlighted +# * all - Both lowest-version and simplest-path are used +highlight = "all" +# List of crates that are allowed. Use with care! +allow = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# List of crates to deny +deny = [ + # Each entry the name of a crate and a version range. If version is + # not specified, all versions will be matched. + #{ name = "ansi_term", version = "=0.11.0" }, + # + # Wrapper crates can optionally be specified to allow the crate when it + # is a direct dependency of the otherwise banned crate + #{ name = "ansi_term", version = "=0.11.0", wrappers = [] }, +] +# Certain crates/versions that will be skipped when doing duplicate detection. +skip = [ + #{ name = "ansi_term", version = "=0.11.0" }, +] +# Similarly to `skip` allows you to skip certain crates during duplicate +# detection. Unlike skip, it also includes the entire tree of transitive +# dependencies starting at the specified crate, up to a certain depth, which is +# by default infinite +skip-tree = [ + #{ name = "ansi_term", version = "=0.11.0", depth = 20 }, +] + +# This section is considered when running `cargo deny check sources`. +# More documentation about the 'sources' section can be found here: +# https://embarkstudios.github.io/cargo-deny/checks/sources/cfg.html +[sources] +# Lint level for what to happen when a crate from a crate registry that is not +# in the allow list is encountered +unknown-registry = "warn" +# Lint level for what to happen when a crate from a git repository that is not +# in the allow list is encountered +unknown-git = "warn" +# List of URLs for allowed crate registries. Defaults to the crates.io index +# if not specified. If it is specified but empty, no registries are allowed. +allow-registry = ["https://github.com/rust-lang/crates.io-index"] +# List of URLs for allowed Git repositories +allow-git = [] + +[sources.allow-org] +# 1 or more github.com organizations to allow git sources for +#github = [""] +# 1 or more gitlab.com organizations to allow git sources for +#gitlab = [""] +# 1 or more bitbucket.org organizations to allow git sources for +#bitbucket = [""] From 9ccec9878a9b2da8a78646a3d6ccd6c195ea107f Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Mon, 1 Aug 2022 22:54:59 +0200 Subject: [PATCH 6/8] Add some logging --- Cargo.lock | 21 +++++++++++++++++++++ Cargo.toml | 6 ++++++ src/commands/groceries.rs | 8 ++++++-- src/commands/import.rs | 1 + src/commands/schedule_csv.rs | 9 +++++++++ src/main.rs | 2 ++ 6 files changed, 45 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d03e3bd..8e4874c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -156,9 +156,11 @@ dependencies = [ "clap", "csv", "directories", + "env_logger", "futures", "icalendar", "libxml", + "log", "regex", "reqwest", "rusty-hook", @@ -273,6 +275,19 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "env_logger" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b2cf0344971ee6c64c31be0d530793fba457d322dfec2810c453d0ef228f9c3" +dependencies = [ + "atty", + "humantime", + "log", + "regex", + "termcolor", +] + [[package]] name = "envmnt" version = "0.8.4" @@ -512,6 +527,12 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.20" diff --git a/Cargo.toml b/Cargo.toml index 7a7aafe..94401d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -37,6 +37,9 @@ features = ["cargo"] [dependencies.directories] version = "4.0" +[dependencies.env_logger] +version = "0.9" + [dependencies.futures] version = "0.3" @@ -47,6 +50,9 @@ features = ["parser"] [dependencies.libxml] version = "0.3" +[dependencies.log] +version = "0.4" + [dependencies.regex] version = "1.6" diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index 7ddbd56..bb70118 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -25,7 +25,7 @@ pub async fn with( let ingredients = get_ingredients(api_client, ids).await?; let ingredients = merge_ingredients(ingredients); let md = prepare_grocery_list(&ingredients)?; - // TODO filename is hardcoded for now + log::debug!("Saving the following grocery list:\n\n{}", &md); save_grocery_list(api_client, location, &md).await?; Ok(()) } @@ -71,7 +71,10 @@ where .await .expect(&format!("Cannot fetch recipe with id {}", id)); - response.json::().await.map(|r| r.ingredients) + response.json::().await.map(|r| { + log::info!("Retrieved ingredients for '{}'", r.name); + r.ingredients + }) }); let ingredients = futures::future::try_join_all(ingredients).await?; @@ -138,6 +141,7 @@ async fn save_grocery_list(api_client: &ApiClient, filename: &str, contents: &st })?; let file_url = dav_base_url.join(filename).unwrap(); + log::info!("Saving grocery list to {}", &file_url); let response = api_client .rest() .put(file_url.clone()) diff --git a/src/commands/import.rs b/src/commands/import.rs index f7c0e82..347dbb2 100644 --- a/src/commands/import.rs +++ b/src/commands/import.rs @@ -24,6 +24,7 @@ where response.status() ); } + log::info!("Imported recipe into cookbook: {}", url.as_ref()); } Ok(()) diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index f63ae7a..b160b8d 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -31,6 +31,9 @@ pub async fn with(api_client: &ApiClient, calendar: &str, csv_file: &Path) -> Re let recipe_urls = urls_from_csv(records.iter())?; import::with(&api_client, recipe_urls.into_iter()).await?; + + // Unfortunately, Nextcloud Cookbook doesn't return an id for imported recipes, + // so we have to resort to fetch all of them to match them let recipes = get_all_recipes(&api_client).await?; let events = records @@ -70,6 +73,7 @@ where } async fn get_all_recipes(api_client: &ApiClient) -> Result>> { + log::info!("Getting list of all recipes"); let metadata = api_client .rest() .get(api_client.base_url().join("apps/cookbook/api/recipes")?) @@ -130,6 +134,11 @@ where &helpers::ical_escape_text(&ev.recipe.name) ); + log::info!( + "Saving event at {} for '{}'", + &ev.ends_at.date(), + &ev.recipe.name + ); let cal = icalendar::Calendar::new() .push::(ev.into()) .done(); diff --git a/src/main.rs b/src/main.rs index d9ec382..daa8691 100644 --- a/src/main.rs +++ b/src/main.rs @@ -19,6 +19,8 @@ use { #[tokio::main(flavor = "multi_thread")] async fn main() -> Result<()> { + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); + let args = setup_args(); parse_args(&args).await } From 04fadd3d89337ace47bdf24281e354cc6783a779 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Tue, 2 Aug 2022 00:51:11 +0200 Subject: [PATCH 7/8] Put a semaphore for connection limit as a temporary hack --- examples/example-schedule.csv | 516 +++++++++++++++++----------------- src/api_client.rs | 4 +- src/commands/schedule_csv.rs | 14 +- 3 files changed, 272 insertions(+), 262 deletions(-) diff --git a/examples/example-schedule.csv b/examples/example-schedule.csv index e0ca5eb..7423889 100644 --- a/examples/example-schedule.csv +++ b/examples/example-schedule.csv @@ -1,366 +1,366 @@ "day","wday","lunch","dinner" -2022-01-01,"sabato",, -2022-01-02,"domenica",, -2022-01-03,"lunedì",, -2022-01-04,"martedì",, -2022-01-05,"mercoledì",, -2022-01-06,"giovedì",, -2022-01-07,"venerdì",, -2022-01-08,"sabato",, -2022-01-09,"domenica",, -2022-01-10,"lunedì",, -2022-01-11,"martedì",, -2022-01-12,"mercoledì",, -2022-01-13,"giovedì",, -2022-01-14,"venerdì",, -2022-01-15,"sabato",, -2022-01-16,"domenica",, -2022-01-17,"lunedì",, -2022-01-18,"martedì",, -2022-01-19,"mercoledì",, -2022-01-20,"giovedì",, -2022-01-21,"venerdì",, -2022-01-22,"sabato",, -2022-01-23,"domenica",, -2022-01-24,"lunedì",, -2022-01-25,"martedì",, -2022-01-26,"mercoledì",, -2022-01-27,"giovedì",, -2022-01-28,"venerdì",, -2022-01-29,"sabato",, -2022-01-30,"domenica",, -2022-01-31,"lunedì",, -2022-02-01,"martedì",, -2022-02-02,"mercoledì",, -2022-02-03,"giovedì",, -2022-02-04,"venerdì",, -2022-02-05,"sabato",, -2022-02-06,"domenica",, -2022-02-07,"lunedì",, -2022-02-08,"martedì",, -2022-02-09,"mercoledì",, -2022-02-10,"giovedì",, -2022-02-11,"venerdì",, -2022-02-12,"sabato",, -2022-02-13,"domenica",, -2022-02-14,"lunedì",, -2022-02-15,"martedì",, -2022-02-16,"mercoledì",, -2022-02-17,"giovedì",, -2022-02-18,"venerdì",, -2022-02-19,"sabato",, -2022-02-20,"domenica",, -2022-02-21,"lunedì",, -2022-02-22,"martedì",, -2022-02-23,"mercoledì",, -2022-02-24,"giovedì",, -2022-02-25,"venerdì",, -2022-02-26,"sabato",, -2022-02-27,"domenica",, -2022-02-28,"lunedì",, -2022-03-01,"martedì",, -2022-03-02,"mercoledì",, -2022-03-03,"giovedì",, -2022-03-04,"venerdì",, -2022-03-05,"sabato",, -2022-03-06,"domenica",, -2022-03-07,"lunedì",, -2022-03-08,"martedì",, -2022-03-09,"mercoledì",, -2022-03-10,"giovedì",, -2022-03-11,"venerdì",, -2022-03-12,"sabato",, -2022-03-13,"domenica",, -2022-03-14,"lunedì",, -2022-03-15,"martedì",, -2022-03-16,"mercoledì",, -2022-03-17,"giovedì",, -2022-03-18,"venerdì",, -2022-03-19,"sabato",, -2022-03-20,"domenica",, -2022-03-21,"lunedì",, -2022-03-22,"martedì",, -2022-03-23,"mercoledì",, -2022-03-24,"giovedì",, -2022-03-25,"venerdì",, -2022-03-26,"sabato",, -2022-03-27,"domenica",, -2022-03-28,"lunedì",, -2022-03-29,"martedì",, -2022-03-30,"mercoledì",, -2022-03-31,"giovedì",, -2022-04-01,"venerdì",, -2022-04-02,"sabato",, -2022-04-03,"domenica",, -2022-04-04,"lunedì",, -2022-04-05,"martedì",, -2022-04-06,"mercoledì",, -2022-04-07,"giovedì",, -2022-04-08,"venerdì",, -2022-04-09,"sabato",, -2022-04-10,"domenica",, -2022-04-11,"lunedì",, -2022-04-12,"martedì",, -2022-04-13,"mercoledì",, -2022-04-14,"giovedì",, -2022-04-15,"venerdì",, -2022-04-16,"sabato",, -2022-04-17,"domenica",, -2022-04-18,"lunedì",, -2022-04-19,"martedì",, -2022-04-20,"mercoledì",, -2022-04-21,"giovedì",, -2022-04-22,"venerdì",, -2022-04-23,"sabato",, -2022-04-24,"domenica",, -2022-04-25,"lunedì",, -2022-04-26,"martedì",, -2022-04-27,"mercoledì",, -2022-04-28,"giovedì",, -2022-04-29,"venerdì",, -2022-04-30,"sabato",, -2022-05-01,"domenica",, -2022-05-02,"lunedì",, -2022-05-03,"martedì",, -2022-05-04,"mercoledì",, -2022-05-05,"giovedì",, -2022-05-06,"venerdì",, -2022-05-07,"sabato",, -2022-05-08,"domenica",, -2022-05-09,"lunedì",, -2022-05-10,"martedì",, -2022-05-11,"mercoledì",, -2022-05-12,"giovedì",, -2022-05-13,"venerdì",, -2022-05-14,"sabato",, -2022-05-15,"domenica",, -2022-05-16,"lunedì",, -2022-05-17,"martedì",, -2022-05-18,"mercoledì",, -2022-05-19,"giovedì",, -2022-05-20,"venerdì",, -2022-05-21,"sabato",, -2022-05-22,"domenica",, -2022-05-23,"lunedì",, -2022-05-24,"martedì",, -2022-05-25,"mercoledì",, -2022-05-26,"giovedì",, -2022-05-27,"venerdì",, -2022-05-28,"sabato",, -2022-05-29,"domenica",, -2022-05-30,"lunedì",, -2022-05-31,"martedì",, -2022-06-01,"mercoledì",, -2022-06-02,"giovedì",, -2022-06-03,"venerdì",, -2022-06-04,"sabato",, -2022-06-05,"domenica",, -2022-06-06,"lunedì",, -2022-06-07,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" -2022-06-08,"mercoledì",, -2022-06-09,"giovedì",, -2022-06-10,"venerdì",, -2022-06-11,"sabato",, -2022-06-12,"domenica",, -2022-06-13,"lunedì",, -2022-06-14,"martedì",, -2022-06-15,"mercoledì",, -2022-06-16,"giovedì",, -2022-06-17,"venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" -2022-06-18,"sabato",, -2022-06-19,"domenica",, -2022-06-20,"lunedì",, -2022-06-21,"martedì",, -2022-06-22,"mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" -2022-06-23,"giovedì",, -2022-06-24,"venerdì",, -2022-06-25,"sabato",, -2022-06-26,"domenica",, -2022-06-27,"lunedì",, -2022-06-28,"martedì",, -2022-06-29,"mercoledì",, -2022-06-30,"giovedì",, -2022-07-01,"venerdì",, -2022-07-02,"sabato",, -2022-07-03,"domenica",, -2022-07-04,"lunedì",, -2022-07-05,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" -2022-07-06,"mercoledì",, -2022-07-07,"giovedì",, -2022-07-08,"venerdì",, -2022-07-09,"sabato",, -2022-07-10,"domenica",, -2022-07-11,"lunedì",, -2022-07-12,"martedì",, -2022-07-13,"mercoledì",, -2022-07-14,"giovedì",, -2022-07-15,"venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" -2022-07-16,"sabato",, -2022-07-17,"domenica",, -2022-07-18,"lunedì",, -2022-07-19,"martedì",, -2022-07-20,"mercoledì",, -2022-07-21,"giovedì",, -2022-07-22,"venerdì",, -2022-07-23,"sabato",, -2022-07-24,"domenica",, -2022-07-25,"lunedì",, -2022-07-26,"martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" -2022-07-27,"mercoledì",, -2022-07-28,"giovedì",, -2022-07-29,"venerdì",, -2022-07-30,"sabato",, -2022-07-31,"domenica",, +"2022-01-01","sabato",, +"2022-01-02","domenica",, +"2022-01-03","lunedì",, +"2022-01-04","martedì",, +"2022-01-05","mercoledì",, +"2022-01-06","giovedì",, +"2022-01-07","venerdì",, +"2022-01-08","sabato",, +"2022-01-09","domenica","https://ricette.giallozafferano.it/Pollo-alla-cacciatora.html","https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html" +"2022-01-10","lunedì",, +"2022-01-11","martedì",, +"2022-01-12","mercoledì",, +"2022-01-13","giovedì",, +"2022-01-14","venerdì",,"https://ricette.giallozafferano.it/Polpette-al-sugo.html" +"2022-01-15","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html","https://ricette.giallozafferano.it/Risotto-radicchio-e-pancetta.html" +"2022-01-16","domenica",, +"2022-01-17","lunedì",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html" +"2022-01-18","martedì",, +"2022-01-19","mercoledì",, +"2022-01-20","giovedì",, +"2022-01-21","venerdì",, +"2022-01-22","sabato","https://ricette.giallozafferano.it/Gnocchi-alla-sorrentina.html", +"2022-01-23","domenica",, +"2022-01-24","lunedì",, +"2022-01-25","martedì",, +"2022-01-26","mercoledì",, +"2022-01-27","giovedì",, +"2022-01-28","venerdì",, +"2022-01-29","sabato",,"https://ricette.giallozafferano.it/Costine-al-forno.html" +"2022-01-30","domenica",, +"2022-01-31","lunedì",, +"2022-02-01","martedì",, +"2022-02-02","mercoledì",, +"2022-02-03","giovedì",, +"2022-02-04","venerdì",, +"2022-02-05","sabato",,"https://ricette.giallozafferano.it/Chili-con-carne.html" +"2022-02-06","domenica",, +"2022-02-07","lunedì",, +"2022-02-08","martedì",, +"2022-02-09","mercoledì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html" +"2022-02-10","giovedì",, +"2022-02-11","venerdì",, +"2022-02-12","sabato",, +"2022-02-13","domenica",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html" +"2022-02-14","lunedì",, +"2022-02-15","martedì",, +"2022-02-16","mercoledì",,"https://ricette.giallozafferano.it/Risotto-radicchio-e-pancetta.html" +"2022-02-17","giovedì",, +"2022-02-18","venerdì",, +"2022-02-19","sabato",,"https://ricette.giallozafferano.it/Polpette-al-sugo.html" +"2022-02-20","domenica",, +"2022-02-21","lunedì",, +"2022-02-22","martedì",, +"2022-02-23","mercoledì",, +"2022-02-24","giovedì",,"https://ricette.giallozafferano.it/Orata-al-forno.html" +"2022-02-25","venerdì",,"https://ricette.giallozafferano.it/Torta-rustica.html" +"2022-02-26","sabato","https://ricette.giallozafferano.it/Gnudi.html", +"2022-02-27","domenica",, +"2022-02-28","lunedì",, +"2022-03-01","martedì",, +"2022-03-02","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html" +"2022-03-03","giovedì",, +"2022-03-04","venerdì",, +"2022-03-05","sabato",, +"2022-03-06","domenica","https://ricette.giallozafferano.it/Torta-Pasqualina.html", +"2022-03-07","lunedì",, +"2022-03-08","martedì",,"https://ricette.giallozafferano.it/Strozzapreti-cacio-cozze-e-pepe.html" +"2022-03-09","mercoledì",,"https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html" +"2022-03-10","giovedì",, +"2022-03-11","venerdì",, +"2022-03-12","sabato",,"https://ricette.giallozafferano.it/Costine-al-forno.html" +"2022-03-13","domenica",, +"2022-03-14","lunedì",, +"2022-03-15","martedì",, +"2022-03-16","mercoledì",, +"2022-03-17","giovedì",,"https://ricette.giallozafferano.it/Filetti-di-salmone-su-julienne-di-cavoletti-di-bruxelles-porri-e-carote.html" +"2022-03-18","venerdì",,"https://ricette.giallozafferano.it/Torta-rustica.html" +"2022-03-19","sabato",, +"2022-03-20","domenica",, +"2022-03-21","lunedì",, +"2022-03-22","martedì",, +"2022-03-23","mercoledì",, +"2022-03-24","giovedì",, +"2022-03-25","venerdì",, +"2022-03-26","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html", +"2022-03-27","domenica",, +"2022-03-28","lunedì",, +"2022-03-29","martedì",, +"2022-03-30","mercoledì",, +"2022-03-31","giovedì",, +"2022-04-01","venerdì",, +"2022-04-02","sabato",, +"2022-04-03","domenica",, +"2022-04-04","lunedì",, +"2022-04-05","martedì",, +"2022-04-06","mercoledì",, +"2022-04-07","giovedì",, +"2022-04-08","venerdì",, +"2022-04-09","sabato",, +"2022-04-10","domenica",, +"2022-04-11","lunedì",, +"2022-04-12","martedì",, +"2022-04-13","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html" +"2022-04-14","giovedì",, +"2022-04-15","venerdì",, +"2022-04-16","sabato",,"https://ricette.giallozafferano.it/Couscous-alla-marocchina.html" +"2022-04-17","domenica",, +"2022-04-18","lunedì",, +"2022-04-19","martedì",, +"2022-04-20","mercoledì",, +"2022-04-21","giovedì",, +"2022-04-22","venerdì",,"https://ricette.giallozafferano.it/Risotto-agli-asparagi-e-scampi.html" +"2022-04-23","sabato",, +"2022-04-24","domenica","https://ricette.giallozafferano.it/Torta-Pasqualina.html", +"2022-04-25","lunedì",, +"2022-04-26","martedì",, +"2022-04-27","mercoledì",,"https://ricette.giallozafferano.it/Orata-al-forno.html" +"2022-04-28","giovedì",, +"2022-04-29","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html" +"2022-04-30","sabato",, +"2022-05-01","domenica",, +"2022-05-02","lunedì",, +"2022-05-03","martedì",, +"2022-05-04","mercoledì",, +"2022-05-05","giovedì",, +"2022-05-06","venerdì",, +"2022-05-07","sabato",, +"2022-05-08","domenica",, +"2022-05-09","lunedì",, +"2022-05-10","martedì",, +"2022-05-11","mercoledì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html" +"2022-05-12","giovedì",, +"2022-05-13","venerdì",, +"2022-05-14","sabato",, +"2022-05-15","domenica",, +"2022-05-16","lunedì",, +"2022-05-17","martedì",, +"2022-05-18","mercoledì",,"https://ricette.giallozafferano.it/Risotto-agli-asparagi-e-scampi.html" +"2022-05-19","giovedì",, +"2022-05-20","venerdì",, +"2022-05-21","sabato",,"https://ricette.giallozafferano.it/Couscous-alla-marocchina.html" +"2022-05-22","domenica","https://ricette.giallozafferano.it/Orata-al-forno.html", +"2022-05-23","lunedì",, +"2022-05-24","martedì",, +"2022-05-25","mercoledì",, +"2022-05-26","giovedì",,"https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html" +"2022-05-27","venerdì",, +"2022-05-28","sabato",, +"2022-05-29","domenica","https://ricette.giallozafferano.it/Polpo-alla-Luciana.html", +"2022-05-30","lunedì",, +"2022-05-31","martedì",, +"2022-06-01","mercoledì",,"https://ricette.giallozafferano.it/Riso-alla-cantonese.html" +"2022-06-02","giovedì",, +"2022-06-03","venerdì",, +"2022-06-04","sabato",, +"2022-06-05","domenica",, +"2022-06-06","lunedì",, +"2022-06-07","martedì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +"2022-06-08","mercoledì",, +"2022-06-09","giovedì",, +"2022-06-10","venerdì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html" +"2022-06-11","sabato",, +"2022-06-12","domenica",,"https://ricette.giallozafferano.it/Riso-alla-cantonese.html" +"2022-06-13","lunedì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html" +"2022-06-14","martedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html" +"2022-06-15","mercoledì",,"https://ricette.giallozafferano.it/Cocotte-di-quinoa.html" +"2022-06-16","giovedì",, +"2022-06-17","venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" +"2022-06-18","sabato","https://ricette.giallozafferano.it/Insalata-di-ceci-estiva.html", +"2022-06-19","domenica",, +"2022-06-20","lunedì",, +"2022-06-21","martedì",, +"2022-06-22","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +"2022-06-23","giovedì",, +"2022-06-24","venerdì",, +"2022-06-25","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html", +"2022-06-26","domenica",, +"2022-06-27","lunedì",, +"2022-06-28","martedì",, +"2022-06-29","mercoledì",, +"2022-06-30","giovedì",, +"2022-07-01","venerdì",, +"2022-07-02","sabato",, +"2022-07-03","domenica",, +"2022-07-04","lunedì",, +"2022-07-05","martedì",,"https://ricette.giallozafferano.it/Insalata-di-ceci-estiva.html" +"2022-07-06","mercoledì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html" +"2022-07-07","giovedì",, +"2022-07-08","venerdì",, +"2022-07-09","sabato",, +"2022-07-10","domenica",, +"2022-07-11","lunedì",, +"2022-07-12","martedì",, +"2022-07-13","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +"2022-07-14","giovedì",, +"2022-07-15","venerdì",,"https://ricette.giallozafferano.it/Salmorejo.html" +"2022-07-16","sabato",, +"2022-07-17","domenica",, +"2022-07-18","lunedì",, +"2022-07-19","martedì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html" +"2022-07-20","mercoledì",, +"2022-07-21","giovedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html" +"2022-07-22","venerdì",, +"2022-07-23","sabato",,"https://ricette.giallozafferano.it/Cocotte-di-quinoa.html" +"2022-07-24","domenica",, +"2022-07-25","lunedì",, +"2022-07-26","martedì",, +"2022-07-27","mercoledì",, +"2022-07-28","giovedì",, +"2022-07-29","venerdì",,"https://ricette.giallozafferano.it/Orata-al-forno.html" +"2022-07-30","sabato",, +"2022-07-31","domenica",, "2022-08-01","lunedì",,"https://ricette.giallozafferano.it/Riso-freddo-con-tonno-zucchine-e-limone.html" "2022-08-02","martedì",, -"2022-08-03","mercoledì",, -"2022-08-04","giovedì",, +"2022-08-03","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +"2022-08-04","giovedì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html" "2022-08-05","venerdì",, "2022-08-06","sabato",, "2022-08-07","domenica",, "2022-08-08","lunedì",, -"2022-08-09","martedì",, +"2022-08-09","martedì",,"https://ricette.giallozafferano.it/Insalata-di-pasta-Mediterranea.html" "2022-08-10","mercoledì",, -"2022-08-11","giovedì",, +"2022-08-11","giovedì",,"https://ricette.giallozafferano.it/Verdure-gratinate-al-forno.html" "2022-08-12","venerdì",, "2022-08-13","sabato",, "2022-08-14","domenica",, -"2022-08-15","lunedì",, +"2022-08-15","lunedì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html" "2022-08-16","martedì",, -"2022-08-17","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" +"2022-08-17","mercoledì",,"https://ricette.giallozafferano.it/Cocotte-di-quinoa.html" "2022-08-18","giovedì",, "2022-08-19","venerdì",, "2022-08-20","sabato",,"https://ricette.giallozafferano.it/Salmorejo.html" "2022-08-21","domenica",, "2022-08-22","lunedì",, "2022-08-23","martedì",, -"2022-08-24","mercoledì",, +"2022-08-24","mercoledì",,"https://ricette.giallozafferano.it/Pasta-fredda-con-pesto-senz-aglio.html" "2022-08-25","giovedì",, "2022-08-26","venerdì",, -"2022-08-27","sabato",, +"2022-08-27","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html", "2022-08-28","domenica",, "2022-08-29","lunedì",, "2022-08-30","martedì",, "2022-08-31","mercoledì",, "2022-09-01","giovedì",, -"2022-09-02","venerdì",, +"2022-09-02","venerdì",,"https://ricette.giallozafferano.it/Petto-di-pollo-ai-peperoni.html" "2022-09-03","sabato",, "2022-09-04","domenica",, "2022-09-05","lunedì",, "2022-09-06","martedì",, "2022-09-07","mercoledì",, -"2022-09-08","giovedì",, +"2022-09-08","giovedì",,"https://ricette.giallozafferano.it/Pasta-con-le-melanzane.html" "2022-09-09","venerdì",, "2022-09-10","sabato",, -"2022-09-11","domenica",, -"2022-09-12","lunedì",, +"2022-09-11","domenica","https://ricette.giallozafferano.it/Polpo-alla-Luciana.html", +"2022-09-12","lunedì",,"https://ricette.giallozafferano.it/Pennette-con-speck-e-zucchine.html" "2022-09-13","martedì",, -"2022-09-14","mercoledì",, +"2022-09-14","mercoledì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html" "2022-09-15","giovedì",, -"2022-09-16","venerdì",, +"2022-09-16","venerdì",,"https://ricette.giallozafferano.it/Cordon-bleu-di-melanzane.html" "2022-09-17","sabato",, "2022-09-18","domenica",, "2022-09-19","lunedì",, "2022-09-20","martedì",, -"2022-09-21","mercoledì",, -"2022-09-22","giovedì",, +"2022-09-21","mercoledì",,"https://ricette.giallozafferano.it/Strozzapreti-cacio-cozze-e-pepe.html" +"2022-09-22","giovedì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html" "2022-09-23","venerdì",, -"2022-09-24","sabato",, -"2022-09-25","domenica",, -"2022-09-26","lunedì",, +"2022-09-24","sabato","https://ricette.giallozafferano.it/Gnudi.html", +"2022-09-25","domenica","https://ricette.giallozafferano.it/Pollo-alla-cacciatora.html", +"2022-09-26","lunedì",,"https://ricette.giallozafferano.it/Mezze-maniche-al-tonno.html" "2022-09-27","martedì",, -"2022-09-28","mercoledì",, +"2022-09-28","mercoledì",,"https://ricette.giallozafferano.it/Orata-al-forno.html" "2022-09-29","giovedì",, "2022-09-30","venerdì",, "2022-10-01","sabato",, -"2022-10-02","domenica",, +"2022-10-02","domenica","https://ricette.giallozafferano.it/Gnocchi-alla-sorrentina.html", "2022-10-03","lunedì",, -"2022-10-04","martedì",, +"2022-10-04","martedì",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html" "2022-10-05","mercoledì",, -"2022-10-06","giovedì",, -"2022-10-07","venerdì",, -"2022-10-08","sabato",, +"2022-10-06","giovedì",,"https://ricette.giallozafferano.it/Saltimbocca-alla-Romana.html" +"2022-10-07","venerdì",,"https://ricette.giallozafferano.it/Risotto-ai-funghi.html" +"2022-10-08","sabato","https://ricette.giallozafferano.it/Torta-Pasqualina.html", "2022-10-09","domenica",, -"2022-10-10","lunedì",, +"2022-10-10","lunedì",,"https://ricette.giallozafferano.it/Pennette-con-speck-e-zucchine.html" "2022-10-11","martedì",, "2022-10-12","mercoledì",, "2022-10-13","giovedì",, -"2022-10-14","venerdì",, +"2022-10-14","venerdì",,"https://ricette.giallozafferano.it/Polpette-al-sugo.html" "2022-10-15","sabato",, "2022-10-16","domenica",, "2022-10-17","lunedì",, "2022-10-18","martedì",, -"2022-10-19","mercoledì",, +"2022-10-19","mercoledì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html" "2022-10-20","giovedì",, "2022-10-21","venerdì",, -"2022-10-22","sabato",, +"2022-10-22","sabato","https://ricette.giallozafferano.it/Gnudi.html", "2022-10-23","domenica",, "2022-10-24","lunedì",, "2022-10-25","martedì",, "2022-10-26","mercoledì",, "2022-10-27","giovedì",, -"2022-10-28","venerdì",, -"2022-10-29","sabato",, +"2022-10-28","venerdì",,"https://ricette.giallozafferano.it/Vellutata-di-zucca-e-carote.html" +"2022-10-29","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html", "2022-10-30","domenica",, "2022-10-31","lunedì",, "2022-11-01","martedì",, "2022-11-02","mercoledì",, "2022-11-03","giovedì",, "2022-11-04","venerdì",, -"2022-11-05","sabato",, +"2022-11-05","sabato","https://ricette.giallozafferano.it/Spatzle-di-spinaci.html", "2022-11-06","domenica",, "2022-11-07","lunedì",, "2022-11-08","martedì",, "2022-11-09","mercoledì",, "2022-11-10","giovedì",, -"2022-11-11","venerdì",, -"2022-11-12","sabato",, +"2022-11-11","venerdì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html" +"2022-11-12","sabato",,"https://ricette.giallozafferano.it/Chili-con-carne.html" "2022-11-13","domenica",, "2022-11-14","lunedì",, "2022-11-15","martedì",, "2022-11-16","mercoledì",, "2022-11-17","giovedì",, -"2022-11-18","venerdì",, +"2022-11-18","venerdì",,"https://ricette.giallozafferano.it/Torta-rustica.html" "2022-11-19","sabato",, "2022-11-20","domenica",, "2022-11-21","lunedì",, "2022-11-22","martedì",, "2022-11-23","mercoledì",, "2022-11-24","giovedì",, -"2022-11-25","venerdì",, -"2022-11-26","sabato",, +"2022-11-25","venerdì",,"https://ricette.giallozafferano.it/Vellutata-di-zucca-e-carote.html" +"2022-11-26","sabato","https://ricette.giallozafferano.it/Orata-al-forno.html", "2022-11-27","domenica",, "2022-11-28","lunedì",, -"2022-11-29","martedì",, +"2022-11-29","martedì",,"https://ricette.giallozafferano.it/Scaloppine-ai-funghi.html" "2022-11-30","mercoledì",, "2022-12-01","giovedì",, "2022-12-02","venerdì",, "2022-12-03","sabato",, "2022-12-04","domenica",, -"2022-12-05","lunedì",, +"2022-12-05","lunedì",,"https://ricette.giallozafferano.it/Spatzle-di-spinaci.html" "2022-12-06","martedì",, "2022-12-07","mercoledì",, "2022-12-08","giovedì",, "2022-12-09","venerdì",, -"2022-12-10","sabato",, +"2022-12-10","sabato","https://ricette.giallozafferano.it/Gnudi.html", "2022-12-11","domenica",, "2022-12-12","lunedì",, "2022-12-13","martedì",, -"2022-12-14","mercoledì",, -"2022-12-15","giovedì",, +"2022-12-14","mercoledì",,"https://ricette.giallozafferano.it/Risotto-ai-funghi.html" +"2022-12-15","giovedì",,"https://ricette.giallozafferano.it/Vellutata-di-porri-con-crostini-saporiti.html" "2022-12-16","venerdì",, "2022-12-17","sabato",, -"2022-12-18","domenica",, +"2022-12-18","domenica","https://ricette.giallozafferano.it/Polpette-al-sugo.html", "2022-12-19","lunedì",, "2022-12-20","martedì",, "2022-12-21","mercoledì",, "2022-12-22","giovedì",, "2022-12-23","venerdì",, "2022-12-24","sabato",, -"2022-12-25","domenica",, +"2022-12-25","domenica","https://ricette.giallozafferano.it/Filetto-in-crosta.html", "2022-12-26","lunedì",, "2022-12-27","martedì",, -"2022-12-28","mercoledì",, +"2022-12-28","mercoledì",,"https://ricette.giallozafferano.it/Orata-al-forno.html" "2022-12-29","giovedì",, "2022-12-30","venerdì",, -"2022-12-31","sabato",, +"2022-12-31","sabato",, \ No newline at end of file diff --git a/src/api_client.rs b/src/api_client.rs index 59827b1..960503d 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -4,11 +4,12 @@ use { crate::config::Config, crate::constants, anyhow::anyhow, anyhow::Result, base64::write::EncoderWriter as Base64Encoder, chrono::DateTime, icalendar::CalendarComponent, - reqwest::Url, std::io::Write, std::ops::Range, + reqwest::Url, std::io::Write, std::ops::Range, std::sync::Arc, tokio::sync::Semaphore, }; pub struct ApiClient { rest: reqwest::Client, + pub(crate) rest_semaphore: Arc, // TODO: wrap in dereferentiable struct base_url: Url, caldav_base_url: Url, username: String, @@ -59,6 +60,7 @@ impl ApiClient { caldav_base_url, username: server.login_name.clone(), rest: rest_client, + rest_semaphore: Arc::new(Semaphore::new(5)), }) } diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index b160b8d..69298cb 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -97,6 +97,7 @@ async fn get_all_recipes(api_client: &ApiClient) -> Result().await.map(|r| Rc::new(r)) }); @@ -134,7 +135,7 @@ where &helpers::ical_escape_text(&ev.recipe.name) ); - log::info!( + let info_message = format!( "Saving event at {} for '{}'", &ev.ends_at.date(), &ev.recipe.name @@ -157,13 +158,20 @@ where 1, ); - api_client + // TODO: wrap this + let _ = api_client.rest_semaphore.acquire().await.unwrap(); + log::info!("{}", info_message); + let response = api_client .rest() .put(url) .header("Content-Type", "text/calendar; charset=utf-8") .body(cal_as_string) .send() - .await + .await; + + // TODO: magic numbers are bad... + std::thread::sleep(std::time::Duration::from_millis(300)); + response }); let responses = try_join_all(update_requests).await?; From 76968d59778d1088864b9c13d6f939226a2006f1 Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Tue, 2 Aug 2022 15:41:55 +0200 Subject: [PATCH 8/8] Add purge command, simplify event RRULE until clients are fixed --- .vscode/launch.json | 32 +++++++++++++++++- src/api_client.rs | 4 +++ src/commands/groceries.rs | 4 +-- src/commands/mod.rs | 1 + src/commands/purge.rs | 63 ++++++++++++++++++++++++++++++++++++ src/commands/schedule_csv.rs | 11 ++----- src/constants.rs | 2 -- src/event.rs | 10 ++++-- src/main.rs | 13 ++++++++ 9 files changed, 124 insertions(+), 16 deletions(-) create mode 100644 src/commands/purge.rs diff --git a/.vscode/launch.json b/.vscode/launch.json index e880500..205502b 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -19,7 +19,10 @@ } }, "args": ["groceries", "Cucina"], - "cwd": "${workspaceFolder}" + "env": { + "RUST_LOG": "debug", + }, + "cwd": "${workspaceFolder}", }, { "type": "lldb", @@ -37,6 +40,30 @@ } }, "args": ["schedule-csv", "Cucina", "examples/example-schedule.csv"], + "env": { + "RUST_LOG": "debug", + }, + "cwd": "${workspaceFolder}" + }, + { + "type": "lldb", + "request": "launch", + "name": "Debug executable 'cook' -> purge", + "cargo": { + "args": [ + "build", + "--bin=cook", + "--package=cooking-schedule" + ], + "filter": { + "name": "cook", + "kind": "bin" + } + }, + "args": ["purge", "Cucina"], + "env": { + "RUST_LOG": "debug", + }, "cwd": "${workspaceFolder}" }, { @@ -56,6 +83,9 @@ } }, "args": [], + "env": { + "RUST_LOG": "debug", + }, "cwd": "${workspaceFolder}" } ] diff --git a/src/api_client.rs b/src/api_client.rs index 960503d..210d054 100644 --- a/src/api_client.rs +++ b/src/api_client.rs @@ -72,6 +72,10 @@ impl ApiClient { &self.base_url } + pub fn caldav_base_url(&self) -> &Url { + &self.caldav_base_url + } + pub fn username(&self) -> &str { &self.username } diff --git a/src/commands/groceries.rs b/src/commands/groceries.rs index bb70118..ee52dd9 100644 --- a/src/commands/groceries.rs +++ b/src/commands/groceries.rs @@ -1,5 +1,3 @@ -use reqwest::Method; - // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later @@ -10,7 +8,7 @@ use { chrono::{Duration, Local}, icalendar::Component, regex::Regex, - reqwest::StatusCode, + reqwest::{Method, StatusCode}, std::collections::HashSet, std::ops::Range, }; diff --git a/src/commands/mod.rs b/src/commands/mod.rs index 042df50..7c29889 100644 --- a/src/commands/mod.rs +++ b/src/commands/mod.rs @@ -4,5 +4,6 @@ pub mod groceries; pub mod import; pub mod init; +pub mod purge; pub mod schedule; pub mod schedule_csv; diff --git a/src/commands/purge.rs b/src/commands/purge.rs new file mode 100644 index 0000000..099e353 --- /dev/null +++ b/src/commands/purge.rs @@ -0,0 +1,63 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use {crate::api_client::ApiClient, crate::constants, anyhow::Result, reqwest::Method}; + +pub async fn with(api_client: &ApiClient, calendar_name: &str) -> Result<()> { + let report_method = Method::from_bytes(b"REPORT")?; + let events_xml = api_client + .rest() + .request( + report_method, + // TODO extract into helper method + api_client.caldav_base_url().join(&format!( + "calendars/{}/{}/", + api_client.username(), + calendar_name.to_lowercase().replace(" ", "-") + ))?, + ) + .header("Prefer", "return-minimal") + .header("Content-Type", "application/xml; charset=utf-8") + .header("Depth", 1) + .body(format!( + " + + + + + + + {} + + + + + ", + constants::CALENDAR_PROVIDER, + )) + .send() + .await? + .text() + .await?; + + let xml_doc = libxml::parser::Parser::default().parse_string(events_xml.as_bytes())?; + let mut xpath_ctx = libxml::xpath::Context::new(&xml_doc).unwrap(); + xpath_ctx.register_namespace("d", "DAV:").unwrap(); + let events_to_purge = xpath_ctx + .findnodes("//d:response/d:href/text()", None) + .unwrap() + .into_iter() + .map(|n| icalendar::parser::unfold(&n.get_content())) + .collect::>(); + + for url in events_to_purge { + api_client + .rest() + .delete(api_client.base_url().join(&url)?) + .send() + .await?; + log::debug!("Purged {}", &url); + } + + Ok(()) +} diff --git a/src/commands/schedule_csv.rs b/src/commands/schedule_csv.rs index 69298cb..19728cd 100644 --- a/src/commands/schedule_csv.rs +++ b/src/commands/schedule_csv.rs @@ -115,13 +115,7 @@ async fn publish_events<'a, EventsIter>( where EventsIter: Iterator, { - let dav_base = api_client - .rest() - .head(api_client.base_url().join("/.well-known/caldav")?) - .send() - .await?; - - let calendar_url = dav_base.url().join(&format!( + let calendar_url = api_client.caldav_base_url().join(&format!( "calendars/{}/{}/", &api_client.username(), calendar.to_lowercase().as_str().replace(" ", "-") @@ -160,7 +154,6 @@ where // TODO: wrap this let _ = api_client.rest_semaphore.acquire().await.unwrap(); - log::info!("{}", info_message); let response = api_client .rest() .put(url) @@ -169,6 +162,8 @@ where .send() .await; + log::info!("{}", info_message); + // TODO: magic numbers are bad... std::thread::sleep(std::time::Duration::from_millis(300)); response diff --git a/src/constants.rs b/src/constants.rs index d1e38e0..f8c65f5 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -5,8 +5,6 @@ pub const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PK pub const CALENDAR_PROVIDER: &str = concat!( "-//IDN montecristosoftware.eu//", env!("CARGO_PKG_NAME"), - " ", - env!("CARGO_PKG_VERSION"), "//EN" ); pub const ICAL_UTCTIME_FMT: &str = "%Y%m%dT%H%M%SZ"; diff --git a/src/event.rs b/src/event.rs index 8a35e20..f0dd326 100644 --- a/src/event.rs +++ b/src/event.rs @@ -3,7 +3,7 @@ use { crate::recipe::Recipe, - chrono::{Datelike, NaiveDate, NaiveDateTime, NaiveTime, Utc}, + chrono::{/*Datelike, */ NaiveDate, NaiveDateTime, NaiveTime, Utc}, icalendar::Event as CalEvent, std::rc::Rc, }; @@ -48,7 +48,7 @@ impl From for CalEvent { fn from(ev: Event) -> Self { use icalendar::Component; - const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; + // const DAY_NAMES: [&str; 7] = ["MO", "TU", "WE", "TH", "FR", "SA", "SU"]; let start_time = ev.ends_at - ev.recipe.total_time(); let cal_event = CalEvent::new() @@ -59,6 +59,7 @@ impl From for CalEvent { .timestamp(Utc::now()) .starts(start_time) .ends(ev.ends_at) + /* FIXME .add_property( // TODO make configurable yearly repetition, for now this suits my personal uses "RRULE", @@ -69,6 +70,11 @@ impl From for CalEvent { .unwrap(), weekno = start_time.iso_week().week(), ), + )*/ + .add_property( + // TODO make configurable yearly repetition, for now this suits my personal uses + "RRULE", + "FREQ=YEARLY", ) .done(); diff --git a/src/main.rs b/src/main.rs index daa8691..77b195b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -70,6 +70,12 @@ fn setup_args() -> ArgMatches { .arg(arg!( "")) .arg(arg!( "").value_parser(clap::value_parser!(PathBuf))) ) + .subcommand( + Command::new("purge") + .about("Removes all events created by this application from a given calendar") + .arg(server_arg.clone()) + .arg(arg!( "")) + ) .get_matches() } @@ -114,6 +120,13 @@ async fn parse_args(args: &ArgMatches) -> Result<()> { .expect(" is a mandatory parameter, it cannot be missing"); commands::schedule_csv::with(&api_client, calendar_name.as_str(), &csv_file).await } + Some(("purge", sub_matches)) => { + let api_client = get_api_client(&sub_matches, &configuration)?; + let calendar_name = sub_matches + .get_one::("calendar_name") + .expect(" is a mandatory parameter, it cannot be missing"); + commands::purge::with(&api_client, calendar_name.as_str()).await + } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), } }