use super::{IndexError, PackageIndex, PackageMeta, VersionMeta};
use std::collections::{HashMap, HashSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SnapChannel {
Stable,
Candidate,
Beta,
Edge,
}
impl SnapChannel {
pub fn risk(&self) -> &'static str {
match self {
Self::Stable => "stable",
Self::Candidate => "candidate",
Self::Beta => "beta",
Self::Edge => "edge",
}
}
pub fn name(&self) -> &'static str {
self.risk()
}
pub fn all() -> &'static [SnapChannel] {
&[Self::Stable, Self::Candidate, Self::Beta, Self::Edge]
}
pub fn stable() -> &'static [SnapChannel] {
&[Self::Stable]
}
pub fn dev() -> &'static [SnapChannel] {
&[Self::Candidate, Self::Beta, Self::Edge]
}
pub fn release() -> &'static [SnapChannel] {
&[Self::Stable, Self::Candidate]
}
}
pub struct Snap {
channels: Vec<SnapChannel>,
}
impl Snap {
const API_BASE: &'static str = "https://api.snapcraft.io/v2/snaps";
pub fn all() -> Self {
Self {
channels: SnapChannel::all().to_vec(),
}
}
pub fn stable() -> Self {
Self {
channels: SnapChannel::stable().to_vec(),
}
}
pub fn dev() -> Self {
Self {
channels: SnapChannel::dev().to_vec(),
}
}
pub fn release() -> Self {
Self {
channels: SnapChannel::release().to_vec(),
}
}
pub fn with_channels(channels: &[SnapChannel]) -> Self {
Self {
channels: channels.to_vec(),
}
}
fn fetch_snap_info(name: &str) -> Result<serde_json::Value, IndexError> {
let url = format!("{}/info/{}", Self::API_BASE, name);
let response: serde_json::Value = ureq::get(&url)
.set("Snap-Device-Series", "16")
.call()
.map_err(|_| IndexError::NotFound(name.to_string()))?
.into_json()?;
Ok(response)
}
fn get_channel_version(
channel_map: &[serde_json::Value],
channel: SnapChannel,
) -> Option<(String, Option<String>)> {
channel_map
.iter()
.find(|ch| ch["channel"]["risk"].as_str() == Some(channel.risk()))
.map(|ch| {
(
ch["version"].as_str().unwrap_or("unknown").to_string(),
ch["released-at"].as_str().map(String::from),
)
})
}
}
impl PackageIndex for Snap {
fn ecosystem(&self) -> &'static str {
"snap"
}
fn display_name(&self) -> &'static str {
"Snap (Snapcraft)"
}
fn fetch(&self, name: &str) -> Result<PackageMeta, IndexError> {
let response = Self::fetch_snap_info(name)?;
let snap = &response["snap"];
let channel_map = response["channel-map"].as_array();
let (version, channel_name) = channel_map
.and_then(|channels| {
for ch in &self.channels {
if let Some((ver, _)) = Self::get_channel_version(channels, *ch) {
return Some((ver, ch.name()));
}
}
None
})
.unwrap_or_else(|| {
(
snap["version"].as_str().unwrap_or("unknown").to_string(),
"unknown",
)
});
let keywords: Vec<String> = snap["categories"]
.as_array()
.map(|arr| {
arr.iter()
.filter_map(|c| c["name"].as_str().map(String::from))
.collect()
})
.unwrap_or_default();
let maintainers: Vec<String> = snap["publisher"]["display-name"]
.as_str()
.or(snap["publisher"]["username"].as_str())
.map(|p| vec![p.to_string()])
.unwrap_or_default();
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String(channel_name.to_string()),
);
Ok(PackageMeta {
name: snap["name"].as_str().unwrap_or(name).to_string(),
version,
description: snap["summary"]
.as_str()
.or(snap["description"].as_str())
.map(String::from),
homepage: snap["website"].as_str().map(String::from),
repository: snap["contact"].as_str().and_then(|c| {
if c.contains("github.com") || c.contains("gitlab.com") {
Some(c.to_string())
} else {
None
}
}),
license: snap["license"].as_str().map(String::from),
binaries: Vec::new(),
keywords,
maintainers,
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra,
})
}
fn fetch_versions(&self, name: &str) -> Result<Vec<VersionMeta>, IndexError> {
let response = Self::fetch_snap_info(name)?;
let channels = response["channel-map"]
.as_array()
.ok_or_else(|| IndexError::NotFound(name.to_string()))?;
let channel_risks: HashSet<_> = self.channels.iter().map(|c| c.risk()).collect();
let mut seen = HashSet::new();
let versions: Vec<VersionMeta> = channels
.iter()
.filter(|ch| {
ch["channel"]["risk"]
.as_str()
.map(|r| channel_risks.contains(r))
.unwrap_or(false)
})
.filter_map(|ch| {
let version = ch["version"].as_str()?;
let risk = ch["channel"]["risk"].as_str().unwrap_or("unknown");
let key = format!("{}-{}", version, risk);
if seen.insert(key) {
Some(VersionMeta {
version: format!("{} ({})", version, risk),
released: ch["released-at"].as_str().map(String::from),
yanked: false,
})
} else {
None
}
})
.collect();
if versions.is_empty() {
return Err(IndexError::NotFound(name.to_string()));
}
Ok(versions)
}
fn search(&self, query: &str) -> Result<Vec<PackageMeta>, IndexError> {
let url = format!("{}/find?q={}", Self::API_BASE, query);
let response: serde_json::Value = ureq::get(&url)
.set("Snap-Device-Series", "16")
.call()?
.into_json()?;
let results = response["results"]
.as_array()
.ok_or_else(|| IndexError::Parse("Invalid search response".into()))?;
Ok(results
.iter()
.filter_map(|result| {
let snap = &result["snap"];
let mut extra = HashMap::new();
extra.insert(
"source_repo".to_string(),
serde_json::Value::String("stable".to_string()),
);
Some(PackageMeta {
name: snap["name"].as_str()?.to_string(),
version: result["version"].as_str().unwrap_or("unknown").to_string(),
description: snap["summary"].as_str().map(String::from),
homepage: snap["website"].as_str().map(String::from),
repository: None,
license: snap["license"].as_str().map(String::from),
binaries: Vec::new(),
keywords: Vec::new(),
maintainers: Vec::new(),
published: None,
downloads: None,
archive_url: None,
checksum: None,
extra,
})
})
.collect())
}
}