use chrono::{DateTime, Utc};
use serde::Deserialize;
use super::{ListOptions, Tag};
use crate::Result;
#[serde_with::serde_as]
#[derive(Debug, Deserialize)]
struct TagsPage {
count: u32,
next: Option<url::Url>,
#[serde_as(as = "serde_with::VecSkipError<_>")]
results: Vec<TagsPageTag>,
}
#[derive(Debug, Deserialize)]
struct TagsPageTag {
name: String,
last_updated: DateTime<Utc>,
}
pub async fn fetch(source: &url::Url, options: &ListOptions<'_>) -> Result<Vec<Tag>> {
let path = match source.host_str() {
Some("docker.io") | None => source.path().trim_start_matches('/').to_owned(),
Some(org) => format!("{org}{path}", path = source.path()),
};
let (namespace, repository) = match path.split_once('/') {
Some(pair) => pair,
None => ("library", path.as_str()),
};
let client = crate::util::http::client()?;
let mut url: url::Url =
format!("https://hub.docker.com/v2/namespaces/{namespace}/repositories/{repository}/tags?page_size=100")
.parse()
.unwrap();
let mut tags = Vec::new();
let registry_host = url.host_str().map(str::to_owned);
for page_number in 0.. {
if page_number >= super::super::MAX_PAGES {
return Err(super::super::too_many_pages("Docker Hub"));
}
let response = client.get(url.as_str()).send().await?;
let page: TagsPage = serde_json::from_str(&crate::util::http::body(response).await?)?;
if tags.is_empty() {
tags.reserve(page.count as usize);
}
for item in page.results {
use crate::util::iter::FilterWhileAction;
match options.consider(&item.name, &item.last_updated) {
FilterWhileAction::Pick => tags.push(Tag::new(item.name, item.last_updated)),
FilterWhileAction::Skip => {}
FilterWhileAction::Break => return Ok(tags),
}
}
url = match page.next {
Some(next) if next.host_str().map(str::to_owned) == registry_host => next,
Some(next) => {
return Err(crate::Error::invalid_source(
next,
"the pagination tried to leave the registry host",
));
}
None => break,
};
}
Ok(tags)
}