adnv 1.5.0

CLI that tracks, checks and auto-updates infrastructure dependency versions across Terraform, Helm, Docker, Git and npm
Documentation
use super::{ListOptions, Tag};
use crate::{Error, Result};
use serde::Deserialize;

#[derive(Debug, Deserialize)]
struct GitlabTag {
    name: String,
    commit: GitlabCommit,
}

#[derive(Debug, Deserialize)]
struct GitlabCommit {
    created_at: chrono::DateTime<chrono::Utc>,
}

/// `project` is the only field of the endpoint without a default, so leaving it
/// out does not query the instance at large — it fails to build. Kept apart
/// from the request so that is covered without a GitLab to talk to.
fn tags_endpoint(source: &url::Url) -> Result<gitlab::api::projects::repository::tags::Tags<'_>> {
    use gitlab::api::projects::repository::tags;

    let path = source.path().strip_prefix('/').unwrap_or(source.path());

    tags::Tags::builder()
        .project(path)
        .build()
        .map_err(|err| Error::gitlab_boxed(err, "Could not construct the tags endpoint"))
}

pub async fn list_tags(source: &url::Url, options: &ListOptions<'_>) -> Result<Vec<Tag>> {
    use gitlab::{GitlabBuilder, api::AsyncQuery};

    let host = source
        .host_str()
        .ok_or_else(|| Error::invalid_source(source.clone(), "No host provided"))?;

    let client = GitlabBuilder::new_unauthenticated(host)
        .build_async()
        .await
        .map_err(|err| Error::gitlab(err, "Could not connect to GitLab"))?;

    let gitlab_tags: Vec<GitlabTag> = tags_endpoint(source)?
        .query_async(&client)
        .await
        .map_err(|err| Error::gitlab(err, "Could not fetch project tags"))?;

    let tags = gitlab_tags
        .into_iter()
        .map(|tag| Tag::new(tag.name, tag.commit.created_at))
        .collect();

    Ok(options.filter_tags(tags))
}

#[cfg(test)]
mod tests {
    use gitlab::api::Endpoint as _;

    use super::tags_endpoint;

    /// The endpoint used to be built with no `project` at all, which listed
    /// nothing useful. These assert the path a source URL turns into, so a
    /// source pointing at a subgroup keeps reaching its own project.
    #[test]
    fn the_endpoint_names_the_project_the_source_points_at() {
        for (source, expected) in [
            (
                "terraform://gitlab.com/vigigloo/tf-modules/k8slimitednamespace",
                "projects/vigigloo%2Ftf-modules%2Fk8slimitednamespace/repository/tags",
            ),
            (
                "https://gitlab.com/group/project",
                "projects/group%2Fproject/repository/tags",
            ),
        ] {
            let url = source.parse().unwrap();

            assert_eq!(tags_endpoint(&url).unwrap().endpoint(), expected);
        }
    }

    /// Not a test of the dependency but of what we are allowed to assume about
    /// it: were `project` to gain a default, dropping the setter would go back
    /// to querying something else instead of failing to build.
    #[test]
    fn the_project_is_the_only_field_the_endpoint_requires() {
        use gitlab::api::projects::repository::tags;

        assert!(tags::Tags::builder().build().is_err());
        assert!(
            tags::Tags::builder()
                .project("group/project")
                .build()
                .is_ok()
        );
    }
}