use std::time::Duration;
use semver::VersionReq;
use tracing::warn;
use nexo_plugin_manifest::PluginManifest;
use crate::types::{ManifestSummary, PluginCategory};
pub async fn fetch_manifest(
http: &reqwest::Client,
primary: &str,
fallbacks: &[String],
timeout: Duration,
) -> Option<FetchedManifest> {
let candidates: Vec<&str> = std::iter::once(primary)
.chain(fallbacks.iter().map(String::as_str))
.collect();
for url in candidates.into_iter() {
let req = http.get(url).timeout(timeout).send();
let body = match req.await {
Ok(resp) => {
if !resp.status().is_success() {
warn!(
target: "plugin_discovery::manifest_fetcher",
url = %url,
status = %resp.status(),
"manifest fetch non-2xx; trying next fallback"
);
continue;
}
match resp.text().await {
Ok(t) => t,
Err(e) => {
warn!(
target: "plugin_discovery::manifest_fetcher",
url = %url,
error = %e,
"manifest body read failed; trying next fallback"
);
continue;
}
}
}
Err(e) => {
warn!(
target: "plugin_discovery::manifest_fetcher",
url = %url,
error = %e,
"manifest fetch transport failure; trying next fallback"
);
continue;
}
};
match PluginManifest::from_str(&body) {
Ok(manifest) => return Some(parse_into_fetched(url.to_string(), manifest)),
Err(e) => {
warn!(
target: "plugin_discovery::manifest_fetcher",
url = %url,
error = %e,
"manifest parse failed; trying next fallback"
);
continue;
}
}
}
None
}
#[derive(Debug, Clone)]
pub struct FetchedManifest {
pub source_url: String,
pub min_nexo_version: VersionReq,
pub category: PluginCategory,
pub summary: ManifestSummary,
}
fn parse_into_fetched(source_url: String, manifest: PluginManifest) -> FetchedManifest {
let category = derive_category(&manifest);
let summary = ManifestSummary {
plugin_id: manifest.plugin.id.clone(),
plugin_version: manifest.plugin.version.to_string(),
manifest_version: manifest.manifest_version,
sdk_requires: Some(manifest.plugin.min_nexo_version.to_string()),
category,
};
FetchedManifest {
source_url,
min_nexo_version: manifest.plugin.min_nexo_version.clone(),
category,
summary,
}
}
pub fn derive_category(manifest: &PluginManifest) -> PluginCategory {
if manifest.plugin.poller.is_some() {
return PluginCategory::Poller;
}
let has_pairing = !manifest.plugin.pairing.is_unset();
let has_dashboard = manifest.plugin.dashboard.is_some();
if has_pairing || has_dashboard {
return PluginCategory::Channel;
}
if manifest.plugin.http.is_some() {
return PluginCategory::Webhook;
}
PluginCategory::Tool
}
#[cfg(test)]
mod tests {
use super::*;
fn parse(toml_src: &str) -> PluginManifest {
PluginManifest::from_str(toml_src).expect("manifest parses")
}
fn base(id: &str) -> String {
format!(
r#"manifest_version = 2
[plugin]
id = "{id}"
version = "0.1.0"
name = "{id} demo"
description = "fixture"
min_nexo_version = ">=0.1"
"#
)
}
#[test]
fn derives_tool_when_no_special_sections() {
let m = parse(&base("tool_only"));
assert_eq!(derive_category(&m), PluginCategory::Tool);
}
#[test]
fn derives_poller_when_poller_section_present() {
let mut s = base("poller_plug");
s.push_str(
r#"
[plugin.poller]
kinds = ["echo"]
broker_topic_prefix = "echo"
"#,
);
let m = parse(&s);
assert_eq!(derive_category(&m), PluginCategory::Poller);
}
#[test]
fn derives_channel_when_pairing_section_present() {
let mut s = base("chan_plug");
s.push_str(
r#"
[plugin.pairing]
kind = "qr"
"#,
);
let m = parse(&s);
assert_eq!(derive_category(&m), PluginCategory::Channel);
}
#[test]
fn derives_channel_when_dashboard_present() {
let mut s = base("dash_plug");
s.push_str(
r#"
[plugin.dashboard.layout]
kind = "single"
[plugin.dashboard.auth_check]
kind = "file_presence"
path = "session.json"
"#,
);
let m = parse(&s);
assert_eq!(derive_category(&m), PluginCategory::Channel);
}
#[test]
fn derives_webhook_when_only_http() {
let mut s = base("web_plug");
s.push_str(
r#"
[plugin.http]
mount_prefix = "/webhook"
"#,
);
let m = parse(&s);
assert_eq!(derive_category(&m), PluginCategory::Webhook);
}
#[test]
fn poller_wins_over_other_sections() {
let mut s = base("multi");
s.push_str(
r#"
[plugin.poller]
kinds = ["echo"]
broker_topic_prefix = "echo"
[plugin.http]
mount_prefix = "/foo"
"#,
);
let m = parse(&s);
assert_eq!(derive_category(&m), PluginCategory::Poller);
}
}