Add temporary CSV parsing import command
This commit is contained in:
parent
4c76734032
commit
f201329441
5 changed files with 224 additions and 246 deletions
50
src/api_client.rs
Normal file
50
src/api_client.rs
Normal 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,
|
||||
})
|
||||
}
|
||||
}
|
175
src/main.rs
175
src/main.rs
|
@ -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
18
src/recipe.rs
Normal 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>,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue