adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
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 {
            // The next URL comes from the response, so keep it on the host the
            // request started from: `https_only` pins the scheme, nothing else
            // pinned where the pagination could wander.
            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)
}