From f2013294418cf55cd04db9fc34ba75754d716bdc Mon Sep 17 00:00:00 2001 From: Matteo Settenvini Date: Thu, 28 Jul 2022 10:28:02 +0200 Subject: [PATCH] Add temporary CSV parsing import command --- Cargo.lock | 217 ++++++++++++++-------------------------------- Cargo.toml | 10 ++- src/api_client.rs | 50 +++++++++++ src/main.rs | 175 ++++++++++++++++++------------------- src/recipe.rs | 18 ++++ 5 files changed, 224 insertions(+), 246 deletions(-) create mode 100644 src/api_client.rs create mode 100644 src/recipe.rs diff --git a/Cargo.lock b/Cargo.lock index 47bd39d..c1e26dc 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 = "anyhow" version = "1.0.58" @@ -43,6 +37,18 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bstr" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" +dependencies = [ + "lazy_static", + "memchr", + "regex-automata", + "serde", +] + [[package]] name = "bumpalo" version = "3.10.0" @@ -87,12 +93,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" @@ -145,10 +145,12 @@ dependencies = [ "base64", "chrono", "clap", + "csv", "directories", - "minicaldav", + "ics", "reqwest", "rusty-hook", + "rustydav", "serde", "serde_json", "tokio", @@ -173,12 +175,25 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" [[package]] -name = "crc32fast" -version = "1.3.2" +name = "csv" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +checksum = "22813a6dc45b335f9bade10bf7271dc477e81113e89eb251a0bc2a8a81c536e1" dependencies = [ - "cfg-if", + "bstr", + "csv-core", + "itoa 0.4.8", + "ryu", + "serde", +] + +[[package]] +name = "csv-core" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b2466559f260f48ad25fe6317b3c8dac77b5bdb5763ac7d9d6103530663bc90" +dependencies = [ + "memchr", ] [[package]] @@ -264,16 +279,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" @@ -421,7 +426,7 @@ checksum = "75f43d41e26995c17e71ee126451dd3941010b0514a81a9d11f3b341debc2399" dependencies = [ "bytes", "fnv", - "itoa", + "itoa 1.0.2", ] [[package]] @@ -462,7 +467,7 @@ dependencies = [ "http-body", "httparse", "httpdate", - "itoa", + "itoa 1.0.2", "pin-project-lite", "socket2", "tokio", @@ -484,6 +489,12 @@ dependencies = [ "tokio-native-tls", ] +[[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" @@ -526,6 +537,12 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "879d54834c8c76457ef4293a689b2a8c59b076067ad77b15efafbb05f92a592b" +[[package]] +name = "itoa" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" + [[package]] name = "itoa" version = "1.0.2" @@ -600,28 +617,6 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" -[[package]] -name = "minicaldav" -version = "0.2.0" -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", -] - [[package]] name = "mio" version = "0.8.4" @@ -893,6 +888,12 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -939,33 +940,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 = "rusty-hook" version = "0.11.2" @@ -978,6 +952,15 @@ dependencies = [ "toml", ] +[[package]] +name = "rustydav" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc4c86c47126ac8bfc573084610e93f4ca8726f3ae7bf6c64bd60476731b6e42" +dependencies = [ + "reqwest", +] + [[package]] name = "ryu" version = "1.0.10" @@ -1003,16 +986,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" @@ -1062,7 +1035,7 @@ version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "82c2c1fdcd807d1098552c5b9a36e425e42e9fbd7c6a37a8425f390f781f7fa7" dependencies = [ - "itoa", + "itoa 1.0.2", "ryu", "serde", ] @@ -1074,7 +1047,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" dependencies = [ "form_urlencoded", - "itoa", + "itoa 1.0.2", "ryu", "serde", ] @@ -1095,12 +1068,6 @@ dependencies = [ "winapi", ] -[[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" @@ -1314,30 +1281,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.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9399fa2f927a3d327187cbd201480cee55bee6ac5d3c77dd27f0c6814cff16d5" -dependencies = [ - "base64", - "chunked_transfer", - "encoding_rs", - "flate2", - "log", - "once_cell", - "rustls", - "url", - "webpki", - "webpki-roots", -] - [[package]] name = "url" version = "2.2.2" @@ -1479,25 +1422,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.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44d8de8415c823c8abd270ad483c6feeac771fad964890779f9a8cb24fbbc1bf" -dependencies = [ - "webpki", -] - [[package]] name = "widestring" version = "0.5.1" @@ -1586,18 +1510,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 8b130fb..8dd4aeb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,9 @@ version = "0.13" version = "0.4" features = ["serde"] +[dependencies.csv] +version = "1.1" + [dependencies.clap] version = "3.2" features = ["cargo"] @@ -33,13 +36,16 @@ features = ["cargo"] [dependencies.directories] version = "4.0" -[dependencies.minicaldav] -version = "0.2" +[dependencies.ics] +version = "0.5" [dependencies.reqwest] version = "0.11" features = ["json", "blocking"] +[dependencies.rustydav] +version = "0.1" + [dependencies.serde] version = "1.0" features = ["derive"] diff --git a/src/api_client.rs b/src/api_client.rs new file mode 100644 index 0000000..2a1be9f --- /dev/null +++ b/src/api_client.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + crate::config::Config, crate::constants, anyhow::anyhow, + base64::write::EncoderWriter as Base64Encoder, reqwest::Url, std::io::Write, +}; + +pub struct ApiClient { + pub base_url: Url, + pub client: reqwest::Client, +} + +impl ApiClient { + pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result { + let server = configuration + .credentials + .servers + .get(server_name) + .ok_or_else(|| { + anyhow!( + "Unknown server {}. Did you use '{} init' first? Known servers: {:#?}", + server_name, + env!("CARGO_BIN_NAME"), + configuration.credentials.servers.keys().collect::>() + ) + })?; + + use reqwest::header; + let mut default_headers = header::HeaderMap::new(); + let mut auth_header = b"Basic ".to_vec(); + { + let mut encoder = Base64Encoder::new(&mut auth_header, base64::STANDARD); + write!(encoder, "{}:{}", server.login_name, server.password).unwrap(); + } + let mut auth_header = header::HeaderValue::from_bytes(&auth_header)?; + auth_header.set_sensitive(true); + default_headers.insert(header::AUTHORIZATION, auth_header); + + let client = reqwest::Client::builder() + .user_agent(constants::USER_AGENT) + .default_headers(default_headers) + .build()?; + + Ok(ApiClient { + base_url: Url::parse(&server.url)?, + client: client, + }) + } +} diff --git a/src/main.rs b/src/main.rs index 01de7b6..097f3aa 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,51 +1,20 @@ -use serde::Deserialize; - // SPDX-FileCopyrightText: 2022 Matteo Settenvini // SPDX-License-Identifier: AGPL-3.0-or-later +mod api_client; mod config; mod constants; +mod recipe; use { - self::config::Config, - anyhow::anyhow, - base64::write::EncoderWriter as Base64Encoder, - chrono::{DateTime, Utc}, + crate::api_client::ApiClient, + crate::config::Config, + crate::recipe::Recipe, clap::{arg, command, ArgMatches, Command}, - reqwest::Url, - std::io::Write, + reqwest::{StatusCode, Url}, + std::path::PathBuf, }; -fn parse_args() -> ArgMatches { - let server_arg = arg!(-s --server "NextCloud server to connect to").required(false); - - command!() - .propagate_version(true) - .subcommand_required(true) - .arg_required_else_help(true) - .subcommand( - Command::new("init") - .about("Authenticate against the provided NextCloud server") - .arg(arg!( "NextCloud server to connect to")), - ) - .subcommand( - Command::new("import") - .about("Import the given URLs into NextCloud's cookbook") - .arg(server_arg.clone()) - .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), - ) - .subcommand( - Command::new("schedule") - .about("") - .arg(server_arg.clone()) - .arg(arg!(-d --days "") - .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) - .required(false) - .default_value("7")) - ) - .get_matches() -} - #[tokio::main(flavor = "multi_thread")] async fn main() -> anyhow::Result<()> { let args = parse_args(); @@ -79,6 +48,7 @@ async fn main() -> anyhow::Result<()> { .send() .await?; println!("{:#?}", response); // TODO + assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status())); } } Some(("schedule", sub_matches)) => { @@ -90,21 +60,89 @@ async fn main() -> anyhow::Result<()> { .await?; println!("{:#?}", recipes.json::>().await?); // TODO } + Some(("schedule-csv", sub_matches)) => { + let csv_file = sub_matches + .get_one::("csv_file") + .expect(" is a mandatory parameter, it cannot be missing"); + let calendar_name = sub_matches + .get_one::("calendar_name") + .expect(" is a mandatory parameter, it cannot be missing"); + + let mut csv = csv::Reader::from_path(csv_file)?; + + #[derive(serde::Deserialize)] + struct CsvRecord { + day: chrono::naive::NaiveDate, + lunch: String, + dinner: String, + } + + let recipe_urls = csv.deserialize::().fold( + std::collections::HashSet::new(), + |mut set, r| { + if let Ok(r) = r { + set.insert(r.lunch); + set.insert(r.dinner); + } + set + }, + ); + + let api_client = get_api_client(&sub_matches, &configuration)?; + for url in recipe_urls { + let response = api_client + .client + .post(api_client.base_url.join("apps/cookbook/import")?) + .json(&serde_json::json!({ + "url": url, + })) + .send() + .await?; + println!("{:#?}", response); // TODO + assert!([StatusCode::OK, StatusCode::CONFLICT].contains(&response.status())); + } + } _ => unreachable!("Exhausted list of subcommands and subcommand_required prevents `None`"), }; Ok(()) } -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -struct Recipe { - #[serde(rename = "recipe_id")] - recipe_id: u32, - name: String, - keywords: String, - date_created: DateTime, - date_modified: DateTime, +fn parse_args() -> ArgMatches { + let server_arg = arg!(-s --server "NextCloud server to connect to").required(false); + + command!() + .propagate_version(true) + .subcommand_required(true) + .arg_required_else_help(true) + .subcommand( + Command::new("init") + .about("Authenticate against the provided NextCloud server") + .arg(arg!( "NextCloud server to connect to")), + ) + .subcommand( + Command::new("import") + .about("Import the given URLs into NextCloud's cookbook") + .arg(server_arg.clone()) + .arg(arg!( ... "One or more URLs each pointing to page with a recipe to import in NextCloud")), + ) + .subcommand( + Command::new("schedule") + .about("") + .arg(server_arg.clone()) + .arg(arg!(-d --days "") + .value_parser(clap::builder::RangedU64ValueParser::::new().range(1..)) + .required(false) + .default_value("7")) + ) + .subcommand( + Command::new("schedule-csv") + .about("TEMPORARY WIP FUNCTION, UNSTABLE") + .arg(server_arg.clone()) + .arg(arg!( "")) + .arg(arg!( "").value_parser(clap::value_parser!(PathBuf))) + ) + .get_matches() } fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result { @@ -121,46 +159,3 @@ fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::R ApiClient::new(&server_name, &configuration) } - -struct ApiClient { - pub base_url: Url, - pub client: reqwest::Client, -} - -impl ApiClient { - pub fn new(server_name: &str, configuration: &Config) -> anyhow::Result { - let server = configuration - .credentials - .servers - .get(server_name) - .ok_or_else(|| { - anyhow!( - "Unknown server {}. Did you use '{} init' first? Known servers: {:#?}", - server_name, - env!("CARGO_BIN_NAME"), - configuration.credentials.servers.keys().collect::>() - ) - })?; - - use reqwest::header; - let mut default_headers = header::HeaderMap::new(); - let mut auth_header = b"Basic ".to_vec(); - { - let mut encoder = Base64Encoder::new(&mut auth_header, base64::STANDARD); - write!(encoder, "{}:{}", server.login_name, server.password).unwrap(); - } - let mut auth_header = header::HeaderValue::from_bytes(&auth_header)?; - auth_header.set_sensitive(true); - default_headers.insert(header::AUTHORIZATION, auth_header); - - let client = reqwest::Client::builder() - .user_agent(constants::USER_AGENT) - .default_headers(default_headers) - .build()?; - - Ok(ApiClient { - base_url: Url::parse(&server.url)?, - client: client, - }) - } -} diff --git a/src/recipe.rs b/src/recipe.rs new file mode 100644 index 0000000..ab56983 --- /dev/null +++ b/src/recipe.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2022 Matteo Settenvini +// SPDX-License-Identifier: AGPL-3.0-or-later + +use { + chrono::{DateTime, Utc}, + serde::Deserialize, +}; + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +pub struct Recipe { + #[serde(rename = "recipe_id")] + recipe_id: u32, + name: String, + keywords: String, + date_created: DateTime, + date_modified: DateTime, +}