gitlab-butler-lib 0.9.1

Support library for [`gitlab-butler`](https://crates.io/crates/gitlab-butler) Gitlab cli to automate the boring stuff.
Documentation
use url::percent_encoding::{utf8_percent_encode, PATH_SEGMENT_ENCODE_SET};

use anyhow::Result;
use log::{debug, error};

use crate::client::Client;
use crate::entities::{CreateIssue, Issue, IssueState, Project, ProjectMilestone};

impl Issue {
    pub fn create(client: &Client, issue: &CreateIssue) -> Result<Issue> {
        //! `POST /projects/:id/issues`
        let api_path = {
            let project_name_or_id =
                utf8_percent_encode(&issue.project_id, PATH_SEGMENT_ENCODE_SET).to_string();
            format!("api/v4/projects/{}/issues", project_name_or_id)
        };
        let response = client.post_json(&api_path, &issue)?;
        let de = &mut serde_json::Deserializer::from_reader(response);
        let result: Result<Issue, _> = serde_path_to_error::deserialize(de);
        match result {
            Ok(issue) => Ok(issue),
            Err(err) => {
                dbg!(&api_path);
                dbg!(err.path().to_string());
                dbg!(client.get(&api_path)?.text()?);
                Err(err.into_inner().into())
            }
        }
    }
    pub fn issue_from_branch(
        client: &Client,
        project: &str,
        branch: &str,
    ) -> Option<Result<Issue>> {
        let prefix = branch.split('-').next();
        let issue_iid = prefix.and_then(|iid| iid.parse::<usize>().ok());
        issue_iid.map(|issue_iid| Issue::single(&client, &project, issue_iid))
    }
    pub fn single(client: &Client, project: &str, issue_iid: usize) -> Result<Issue> {
        //! `GET /projects/:id/issues/:issue_iid`
        let api_path = {
            let project_name_or_id =
                utf8_percent_encode(project, PATH_SEGMENT_ENCODE_SET).to_string();
            format!(
                "api/v4/projects/{}/issues/{}",
                project_name_or_id, issue_iid
            )
        };
        let response = client.get(&api_path)?;
        let de = &mut serde_json::Deserializer::from_reader(response);
        let result: Result<Issue, _> = serde_path_to_error::deserialize(de);
        match result {
            Ok(issue) => Ok(issue),
            Err(err) => {
                error!("Path: {}", err.path().to_string());
                error!("Body: {:?}", client.get(&api_path)?.bytes()?);
                Err(err.into_inner().into())
            }
        }
    }
    pub fn list(
        client: &Client,
        project: &Option<String>,
        state: &IssueState,
        labels: &Option<String>,
        milestone: &Option<String>,
        limit: usize,
    ) -> Result<Vec<Issue>> {
        let prefix = if let Some(aproject) = project {
            let project_name_or_id =
                utf8_percent_encode(aproject, PATH_SEGMENT_ENCODE_SET).to_string();
            format!("api/v4/projects/{}", project_name_or_id)
        } else {
            "api/v4".to_string()
        };
        let scope = {
            if project.is_some() {
                "all"
            } else {
                "assigned_to_me" // avoid 500 error for too broad queries
            }
        };
        let mut api_path = format!("{}/issues?state={}&scope={}", prefix, state.name(), scope);
        if let Some(labels) = labels {
            api_path = format!("{}&labels={}", api_path, labels);
        }
        if let Some(milestone) = milestone {
            api_path = format!("{}&milestone={}", api_path, milestone);
        }
        debug!("Ready to query {:?}", api_path);
        let mut all_issues: Vec<Issue> = vec![];
        for issues in client.get_paginated(&api_path) {
            let issues = issues?;
            let de = &mut serde_json::Deserializer::from_reader(issues);
            let result: Result<Vec<Issue>, _> = serde_path_to_error::deserialize(de);
            let issues = match result {
                Ok(issues) => issues,
                Err(err) => {
                    dbg!(&api_path);
                    dbg!(err.path().to_string());
                    dbg!(client.get(&api_path)?.text()?);
                    return Err(err.into_inner().into());
                }
            };
            all_issues.extend(issues);
            if all_issues.len() >= limit {
                break;
            }
        }
        Ok(all_issues)
    }
}

impl ProjectMilestone {
    pub fn in_project_by_title(
        client: &Client,
        project: &str,
        title: &str,
    ) -> Result<Option<ProjectMilestone>> {
        // The API returns only a single item or nothing.
        ProjectMilestone::_in_project_by_title(client, project, Some(title)).map(|mut m| m.pop())
    }
    pub fn in_project(client: &Client, project: &str) -> Result<Vec<ProjectMilestone>> {
        // The API returns only a single item or nothing.
        ProjectMilestone::_in_project_by_title(client, project, None)
    }
    fn _in_project_by_title(
        client: &Client,
        project: &str,
        title: Option<&str>,
    ) -> Result<Vec<ProjectMilestone>> {
        let prefix = {
            let project_name_or_id =
                utf8_percent_encode(project, PATH_SEGMENT_ENCODE_SET).to_string();
            format!("api/v4/projects/{}", project_name_or_id)
        };
        let mut api_path = format!(
            "{}/milestones?state=active&include_parent_milestones=true",
            prefix
        );
        if let Some(title) = title {
            api_path = format!("{}&title={}", api_path, title);
        }
        debug!("Ready to query {:?}", api_path);
        let response = client.get(&api_path)?;
        let de = &mut serde_json::Deserializer::from_reader(response);
        let result: Result<Vec<ProjectMilestone>, _> = serde_path_to_error::deserialize(de);
        let milestones = match result {
            Ok(milestones) => milestones,
            Err(err) => {
                dbg!(&api_path);
                dbg!(err.path().to_string());
                dbg!(client.get(&api_path)?.text()?);
                return Err(err.into_inner().into());
            }
        };
        Ok(milestones)
    }
}

impl Project {
    pub fn list(client: &Client, search: &Option<String>, limit: usize) -> Result<Vec<Project>> {
        let mut api_path = "api/v4/projects?simple=true&oder_by=id".to_string();
        if let Some(search) = search {
            api_path = format!("{}&search={}", api_path, search);
        }
        debug!("Ready to query {:?}", api_path);
        let mut all_projects = vec![];
        for projects in client.get_paginated(&api_path) {
            let response = projects?;
            let de = &mut serde_json::Deserializer::from_reader(response);
            let result: Result<Vec<Project>, _> = serde_path_to_error::deserialize(de);
            let projects = match result {
                Ok(projects) => projects,
                Err(err) => {
                    dbg!(&api_path);
                    dbg!(err.path().to_string());
                    return Err(err.into_inner().into());
                }
            };
            all_projects.extend(projects);
            if all_projects.len() >= limit {
                break;
            }
        }
        Ok(all_projects)
    }
    pub fn single(client: &Client, name_or_id: &str) -> Result<Project> {
        let api_path = {
            let name_or_id = utf8_percent_encode(name_or_id, PATH_SEGMENT_ENCODE_SET).to_string();
            format!("api/v4/projects/{}", name_or_id)
        };
        let response = client.get(&api_path)?;
        let de = &mut serde_json::Deserializer::from_reader(response);
        let result: Result<Project, _> = serde_path_to_error::deserialize(de);
        match result {
            Ok(project) => Ok(project),
            Err(err) => {
                dbg!(&api_path);
                dbg!(err.path().to_string());
                dbg!(client.get(&api_path)?.text()?);
                Err(err.into_inner().into())
            }
        }
    }
}