#![cfg_attr(
not(test),
deny(clippy::unwrap_used, clippy::panic, clippy::indexing_slicing)
)]
use std::path::Path;
use org_gdocs::cli::{Cli, Command};
use org_gdocs::config::{Config, EnvInputs, Paths};
use org_gdocs::doctor::OrgGdocsDoctor;
use org_gdocs::google::client::GoogleClient;
use org_gdocs::{Error, Result, auth, clean, envelope, open, pull, push, sexp::Sexp};
use tftio_cli_common::{
AgentModeContext, FatalCliError, JsonOutput, ProcessEnv, StandardCommand, map_standard_command,
run_cli_from,
};
fn main() {
let env = process_env();
std::process::exit(run_cli_from::<Cli, _, OrgGdocsDoctor, _, _>(
&org_gdocs::TOOL_SPEC,
&env,
std::env::args_os(),
&OrgGdocsDoctor,
metadata_command,
run,
));
}
#[allow(
clippy::disallowed_methods,
reason = "agent token / HOME read once at the process edge (REPO_INVARIANTS.md #5)"
)]
fn process_env() -> ProcessEnv {
ProcessEnv {
agent: AgentModeContext::from_tokens(
std::env::var(tftio_cli_common::AGENT_TOKEN_ENV).ok(),
std::env::var(tftio_cli_common::AGENT_TOKEN_EXPECTED_ENV).ok(),
),
home: std::env::var_os("HOME").map(std::path::PathBuf::from),
}
}
fn metadata_command(cli: &Cli) -> Option<StandardCommand> {
match &cli.command {
Command::Meta { command } => Some(map_standard_command(
command,
JsonOutput::from_flag(cli.json),
)),
_ => None,
}
}
#[allow(
clippy::unnecessary_wraps,
reason = "signature is fixed by cli-common's command runner (FnOnce(Cli) -> Result<i32, FatalCliError>)"
)]
fn run(cli: Cli) -> std::result::Result<i32, FatalCliError> {
let exit = match cli.command {
Command::Meta { .. } => unreachable!("meta commands are routed by metadata_command"),
Command::Auth => emit("auth", run_auth()),
Command::Push { file } => emit("push", run_push(&file)),
Command::Pull { file } => emit("pull", run_pull(&file)),
Command::Clean { file } => emit("clean", run_clean(&file)),
Command::Open { file } => emit("open", run_open(&file)),
};
Ok(exit)
}
fn emit(command: &str, result: Result<Sexp>) -> i32 {
match result {
Ok(value) => {
println!("{}", value.render());
0
}
Err(err) => {
println!("{}", envelope::error(command, &err.to_string()).render());
1
}
}
}
fn run_auth() -> Result<Sexp> {
let config = load_config()?;
block_on(async {
let authenticator =
auth::authenticator(&config.credentials_path, &config.token_path).await?;
auth::obtain_token(&authenticator).await
})?;
Ok(envelope::ok(
"auth",
vec![(
"token-path",
Sexp::string(config.token_path.display().to_string()),
)],
))
}
fn run_push(file: &Path) -> Result<Sexp> {
let config = load_config()?;
let content = read_file(file)?;
let title = file
.file_stem()
.and_then(|stem| stem.to_str())
.unwrap_or("Untitled")
.to_owned();
let now = chrono::Utc::now().to_rfc3339();
let outcome = block_on(async {
let authenticator =
auth::authenticator(&config.credentials_path, &config.token_path).await?;
let client = GoogleClient::new(authenticator)?;
push::push(&client, &content, &title, &now).await
})?;
write_file(file, &outcome.new_content)?;
Ok(push::envelope(&outcome))
}
fn run_pull(file: &Path) -> Result<Sexp> {
let config = load_config()?;
let content = read_file(file)?;
let now = chrono::Utc::now().to_rfc3339();
let outcome = block_on(async {
let authenticator =
auth::authenticator(&config.credentials_path, &config.token_path).await?;
let client = GoogleClient::new(authenticator)?;
pull::pull(&client, &content, &now).await
})?;
write_file(file, &outcome.new_content)?;
Ok(pull::envelope(&outcome))
}
fn run_clean(file: &Path) -> Result<Sexp> {
let content = read_file(file)?;
let outcome = clean::clean(&content)?;
write_file(file, &outcome.new_content)?;
Ok(clean::envelope(&outcome))
}
fn run_open(file: &Path) -> Result<Sexp> {
let content = read_file(file)?;
Ok(open::envelope(&open::open(&content)?))
}
fn load_config() -> Result<Config> {
let paths = Paths::resolve(&EnvInputs::from_env())?;
Config::load(&paths)
}
fn read_file(path: &Path) -> Result<String> {
std::fs::read_to_string(path).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})
}
fn write_file(path: &Path, content: &str) -> Result<()> {
std::fs::write(path, content).map_err(|source| Error::Io {
path: path.to_path_buf(),
source,
})
}
fn block_on<F: std::future::Future<Output = Result<T>>, T>(future: F) -> Result<T> {
let runtime = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.map_err(|err| Error::Auth(format!("build async runtime: {err}")))?;
runtime.block_on(future)
}