refactor: move library into it's own crate

This commit is contained in:
Alexander Navarro 2025-05-15 11:38:00 -04:00
parent 91d702088d
commit b31502fb37
13 changed files with 138 additions and 34 deletions

101
cli/src/config.rs Normal file
View file

@ -0,0 +1,101 @@
use clap::{Parser, Subcommand, ValueEnum};
use serde::{Deserialize, Serialize};
use std::fmt;
use std::path::PathBuf;
use tracing_core::LevelFilter;
#[derive(ValueEnum, Clone, Debug, Deserialize, Serialize)]
pub enum VerbosityLevel {
Off,
Error,
Warn,
Info,
Debug,
Trace,
}
impl Default for VerbosityLevel {
fn default() -> Self {
VerbosityLevel::Error
}
}
impl fmt::Display for VerbosityLevel {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
VerbosityLevel::Off => {
write!(f, "off")
}
VerbosityLevel::Error => {
write!(f, "error")
}
VerbosityLevel::Warn => {
write!(f, "warn")
}
VerbosityLevel::Info => {
write!(f, "info")
}
VerbosityLevel::Debug => {
write!(f, "debug")
}
VerbosityLevel::Trace => {
write!(f, "trace")
}
}
}
}
impl Into<LevelFilter> for VerbosityLevel {
fn into(self) -> LevelFilter {
match self {
VerbosityLevel::Off => LevelFilter::OFF,
VerbosityLevel::Error => LevelFilter::ERROR,
VerbosityLevel::Warn => LevelFilter::WARN,
VerbosityLevel::Info => LevelFilter::INFO,
VerbosityLevel::Debug => LevelFilter::DEBUG,
VerbosityLevel::Trace => LevelFilter::TRACE,
}
}
}
#[derive(Debug, Subcommand)]
#[clap(rename_all = "snake_case")]
pub enum Command {
/// Load task into the database from [path]
LoadTasks{
/// Path to the file
path: PathBuf,
},
Query,
Run,
#[clap(skip)]
None,
}
impl Default for Command {
fn default() -> Self {
Command::None
}
}
#[derive(Debug, Parser, Serialize, Deserialize)]
pub struct Config {
#[command(subcommand)]
#[serde(skip)]
pub command: Command,
#[arg(
long,
short = 'v',
default_value_t,
global = true,
help = "Increase logging verbosity"
)]
log_level: VerbosityLevel,
}
impl Config {
pub fn log_level(&self) -> LevelFilter {
self.log_level.clone().into()
}
}

33
cli/src/error.rs Normal file
View file

@ -0,0 +1,33 @@
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
#[error("{0}")]
Exception(&'static str),
#[error("{0}")]
Runtime(String),
#[error("{0}")]
Unhandled(&'static str),
#[error(transparent)]
Sync(#[from] lib_sync_core::error::Error),
#[error(transparent)]
Sqlx(#[from] sqlx::Error),
#[error(transparent)]
Migration(#[from] sqlx::migrate::MigrateError),
#[error(transparent)]
Io(#[from] tokio::io::Error),
#[error(transparent)]
ParseJson(#[from] serde_json::Error),
#[error(transparent)]
Config(#[from] figment::Error),
}
pub type Result<T> = std::result::Result<T, Error>;

5
cli/src/lib.rs Normal file
View file

@ -0,0 +1,5 @@
pub mod config;
pub mod readwise;
mod error;
pub use error::*;

70
cli/src/main.rs Normal file
View file

@ -0,0 +1,70 @@
use clap::{CommandFactory, Parser};
use figment::{
providers::{Env, Serialized},
Figment,
};
use readwise_bulk_upload::config::{Command, Config};
use readwise_bulk_upload::readwise::DocumentPayload;
use lib_sync_core::task_manager::{TaskManager, TaskStatus};
use readwise_bulk_upload::{Error, Result};
use std::fs::File;
use directories::ProjectDirs;
use tabled::Table;
use tracing_subscriber;
#[tokio::main]
async fn main() -> Result<()> {
let cli = Config::parse();
let args: Config = Figment::new()
.merge(Serialized::defaults(&cli))
.merge(Env::prefixed("APP_"))
.extract()?;
tracing_subscriber::fmt()
.with_max_level(args.log_level())
.init();
run(&cli.command).await?;
Ok(())
}
async fn run(command: &Command) -> Result<()> {
let project_dir = ProjectDirs::from("", "", env!("CARGO_PKG_NAME"))
.ok_or(lib_sync_core::error::Error::Unhandled("Could not get standard directories"))?;
let task_manager = TaskManager::new(project_dir.data_dir()).await?;
match command {
Command::LoadTasks { path } => {
let file = File::open(path).map_err(|_| {
Error::Runtime(format!(
r#"The file "{}" could not be open"#,
path.display()
))
})?;
let documents: Vec<DocumentPayload> = serde_json::from_reader(file)?;
task_manager.load_tasks(documents).await?;
}
Command::Query => {
let tasks = task_manager.get_tasks::<DocumentPayload>(None, Some(25)).await?;
println!("{}", Table::new(tasks));
}
Command::Run => {
task_manager.run_tasks::<DocumentPayload>(|task| {
println!("{}", task.get_key());
TaskStatus::Completed
}).await?;
}
Command::None => {
Config::command().print_help()?;
}
}
Ok(())
}

41
cli/src/readwise.rs Normal file
View file

@ -0,0 +1,41 @@
use lib_sync_core::task_manager::TaskPayload;
use chrono::{DateTime, Local};
use serde::{de, Deserialize, Deserializer, Serialize};
use serde_json::Value;
use std::fmt::Display;
#[derive(Deserialize, Serialize, Debug)]
pub struct DocumentPayload {
title: String,
summary: Option<String>,
url: String,
#[serde(deserialize_with = "single_or_vec")]
tags: Vec<String>,
published_date: DateTime<Local>,
location: String,
}
impl Display for DocumentPayload {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
serde_json::to_string_pretty(self).map_err(|_| std::fmt::Error)?
)
}
}
impl TaskPayload for DocumentPayload {
fn get_key(&self) -> String {
self.url.clone()
}
}
fn single_or_vec<'de, D: Deserializer<'de>>(deserializer: D) -> Result<Vec<String>, D::Error> {
Ok(match Value::deserialize(deserializer)? {
Value::String(s) => vec![s.parse().map_err(de::Error::custom)?],
Value::Array(arr) => arr.into_iter().map(|a| a.to_string()).collect(),
Value::Null => Vec::new(),
_ => return Err(de::Error::custom("wrong type")),
})
}