Add temporary CSV parsing import command

This commit is contained in:
Matteo Settenvini 2022-07-28 10:28:02 +02:00
parent 4c76734032
commit f201329441
Signed by: matteo
GPG key ID: 8576CC1AD97D42DF
5 changed files with 224 additions and 246 deletions

50
src/api_client.rs Normal file
View file

@ -0,0 +1,50 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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<Self> {
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::<Vec<_>>()
)
})?;
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,
})
}
}

View file

@ -1,51 +1,20 @@
use serde::Deserialize;
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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 <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!(<server> "NextCloud server to connect to")),
)
.subcommand(
Command::new("import")
.about("Import the given URLs into NextCloud's cookbook")
.arg(server_arg.clone())
.arg(arg!(<url> ... "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 <days> "")
.value_parser(clap::builder::RangedU64ValueParser::<u32>::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::<Vec<Recipe>>().await?); // TODO
}
Some(("schedule-csv", sub_matches)) => {
let csv_file = sub_matches
.get_one::<PathBuf>("csv_file")
.expect("<csv_file> is a mandatory parameter, it cannot be missing");
let calendar_name = sub_matches
.get_one::<String>("calendar_name")
.expect("<calendar_name> 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::<CsvRecord>().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<Utc>,
date_modified: DateTime<Utc>,
fn parse_args() -> ArgMatches {
let server_arg = arg!(-s --server <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!(<server> "NextCloud server to connect to")),
)
.subcommand(
Command::new("import")
.about("Import the given URLs into NextCloud's cookbook")
.arg(server_arg.clone())
.arg(arg!(<url> ... "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 <days> "")
.value_parser(clap::builder::RangedU64ValueParser::<u32>::new().range(1..))
.required(false)
.default_value("7"))
)
.subcommand(
Command::new("schedule-csv")
.about("TEMPORARY WIP FUNCTION, UNSTABLE")
.arg(server_arg.clone())
.arg(arg!(<calendar_name> ""))
.arg(arg!(<csv_file> "").value_parser(clap::value_parser!(PathBuf)))
)
.get_matches()
}
fn get_api_client(sub_matches: &ArgMatches, configuration: &Config) -> anyhow::Result<ApiClient> {
@ -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<Self> {
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::<Vec<_>>()
)
})?;
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,
})
}
}

18
src/recipe.rs Normal file
View file

@ -0,0 +1,18 @@
// SPDX-FileCopyrightText: 2022 Matteo Settenvini <matteo.settenvini@montecristosoftware.eu>
// 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<Utc>,
date_modified: DateTime<Utc>,
}