use std::path::PathBuf;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Describe {
pub name: String,
pub link_namespace: String,
pub fetch_style: FetchStyle,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub auth_realm: Option<String>,
pub protocol: u32,
}
pub const PROTOCOL: u32 = 1;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum FetchStyle {
Windowed,
Snapshot,
Sweep,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuthChallenge {
pub verification_url: String,
pub user_code: String,
pub expires_in_secs: u64,
pub handle: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum AuthPollResult {
Pending,
Done(String),
Failed(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum AuthStatus {
NotRequired,
Authenticated(String),
NotAuthenticated(String),
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Context {
pub config: String,
pub secrets_dir: PathBuf,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FetchRequest {
pub config: String,
pub secrets_dir: PathBuf,
pub out_dir: PathBuf,
pub spec: RunSpec,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub checkpoint: Option<String>,
}
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct FetchResult {
pub items: Vec<Item>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub notes: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub next_checkpoint: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub enum RunSpec {
Window {
from: String,
to: String,
},
Snapshot,
Sweep,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Item {
pub id: String,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub kind: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub version: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub content_hash: String,
pub files: Vec<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub locator: Option<String>,
#[serde(default, skip_serializing_if = "String::is_empty")]
pub meta: String,
}
pub trait SourcePlugin {
fn describe(&self) -> Describe;
fn validate_config(&self, config: &str) -> Result<(), String>;
fn config_auth_realm(&self, _config: &str) -> Result<Option<String>, String> {
Ok(None)
}
fn auth_status(&self, ctx: &Context) -> Result<AuthStatus, String>;
fn authenticate(&self, ctx: &Context) -> Result<(), String>;
fn auth_begin(&self, _ctx: &Context) -> Result<AuthChallenge, String> {
Err("interactive sign-in is not supported for this source — use the CLI".into())
}
fn auth_poll(&self, _ctx: &Context, _handle: &str) -> Result<AuthPollResult, String> {
Err("interactive sign-in is not supported for this source".into())
}
fn fetch(&self, req: &FetchRequest) -> Result<FetchResult, String>;
}
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginRequest {
Describe,
ValidateConfig { config: String },
ConfigAuthRealm { config: String },
AuthStatus { ctx: Context },
Authenticate { ctx: Context },
AuthBegin { ctx: Context },
AuthPoll { ctx: Context, handle: String },
Fetch { req: FetchRequest },
}
#[derive(Debug, Serialize, Deserialize)]
pub enum PluginResponse {
Describe(Describe),
Ok,
Realm(Option<String>),
Status(AuthStatus),
Challenge(AuthChallenge),
Poll(AuthPollResult),
Fetched(FetchResult),
Error(String),
}
pub fn tap_main(select: impl Fn(&str) -> Option<Box<dyn SourcePlugin>>) {
use std::io::Read;
let respond = |r: &PluginResponse| match crate::ronfmt::to_ron(r) {
Ok(text) => print!("{text}"),
Err(e) => {
eprintln!("serializing response: {e}");
std::process::exit(2);
}
};
let args: Vec<String> = std::env::args().collect();
let name = match args.iter().position(|a| a == "--plugin") {
Some(i) if i + 1 < args.len() => args[i + 1].clone(),
_ => {
respond(&PluginResponse::Error("usage: --plugin <name>".into()));
std::process::exit(2);
}
};
let Some(plugin) = select(&name) else {
respond(&PluginResponse::Error(format!(
"this tap does not serve a plugin named '{name}'"
)));
std::process::exit(2);
};
let mut input = String::new();
if let Err(e) = std::io::stdin().read_to_string(&mut input) {
respond(&PluginResponse::Error(format!("reading stdin: {e}")));
std::process::exit(2);
}
let request = match ron::from_str::<PluginRequest>(&input) {
Ok(r) => r,
Err(e) => {
respond(&PluginResponse::Error(format!("parsing request: {e}")));
std::process::exit(2);
}
};
let response = match request {
PluginRequest::Describe => PluginResponse::Describe(plugin.describe()),
PluginRequest::ValidateConfig { config } => match plugin.validate_config(&config) {
Ok(()) => PluginResponse::Ok,
Err(e) => PluginResponse::Error(e),
},
PluginRequest::ConfigAuthRealm { config } => match plugin.config_auth_realm(&config) {
Ok(r) => PluginResponse::Realm(r),
Err(e) => PluginResponse::Error(e),
},
PluginRequest::AuthStatus { ctx } => match plugin.auth_status(&ctx) {
Ok(s) => PluginResponse::Status(s),
Err(e) => PluginResponse::Error(e),
},
PluginRequest::Authenticate { ctx } => match plugin.authenticate(&ctx) {
Ok(()) => PluginResponse::Ok,
Err(e) => PluginResponse::Error(e),
},
PluginRequest::AuthBegin { ctx } => match plugin.auth_begin(&ctx) {
Ok(c) => PluginResponse::Challenge(c),
Err(e) => PluginResponse::Error(e),
},
PluginRequest::AuthPoll { ctx, handle } => match plugin.auth_poll(&ctx, &handle) {
Ok(p) => PluginResponse::Poll(p),
Err(e) => PluginResponse::Error(e),
},
PluginRequest::Fetch { req } => match plugin.fetch(&req) {
Ok(r) => PluginResponse::Fetched(r),
Err(e) => PluginResponse::Error(e),
},
};
respond(&response);
}