gitlab 0.1810.0

Gitlab API client.
Documentation
// Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or
// http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your
// option. This file may not be copied, modified, or distributed
// except according to those terms.

use derive_builder::Builder;

use crate::api::common::NameOrId;
use crate::api::endpoint_prelude::*;
use crate::api::ParamValue;

/// A label priority.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LabelPriority {
    /// The priority of the label.
    Value(u64),
    /// Remove the priority from a label.
    Null,
}

impl LabelPriority {
    fn as_str(self) -> Cow<'static, str> {
        match self {
            LabelPriority::Null => "null".into(),
            LabelPriority::Value(priority) => format!("{priority}").into(),
        }
    }
}

impl From<u64> for LabelPriority {
    fn from(prio: u64) -> Self {
        Self::Value(prio)
    }
}

impl ParamValue<'static> for LabelPriority {
    fn as_value(&self) -> Cow<'static, str> {
        self.as_str()
    }
}

/// The color of a label.
#[derive(Debug, Clone, Copy)]
#[non_exhaustive]
pub enum GitlabDefaultColor {
    /// Gitlab default color: Magenta Pink
    MagentaPink,
    /// Gitlab default color: Crimson
    Crimson,
    /// Gitlab default color: Rose Red
    RoseRed,
    /// Gitlab default color: Dark Coral
    DarkCoral,
    /// Gitlab default color: Carrot Orange
    CarrotOrange,
    /// Gitlab default color: Titanium Yellow
    TitaniumYellow,
    /// Gitlab default color: Green Cyan
    GreenCyan,
    /// Gitlab default color: Dark Sea Green
    DarkSeaGreen,
    /// Gitlab default color: Blue Gray
    BlueGray,
    /// Gitlab default color: Lavender
    Lavender,
    /// Gitlab default color: Dark Violet
    DarkViolet,
    /// Gitlab default color: Deep Violet
    DeepViolet,
    /// Gitlab default color: Charcoal Grey
    CharcoalGrey,
    /// Gitlab default color: Gray
    Gray,
}

impl GitlabDefaultColor {
    /// The string representation of a label color.
    fn to_label_color(self) -> LabelColor {
        match self {
            Self::MagentaPink => LabelColor::rgb(0xcc, 0x33, 0x8b),
            Self::Crimson => LabelColor::rgb(0xdc, 0x14, 0x3c),
            Self::RoseRed => LabelColor::rgb(0xc2, 0x1e, 0x56),
            Self::DarkCoral => LabelColor::rgb(0xcd, 0x5b, 0x45),
            Self::CarrotOrange => LabelColor::rgb(0xed, 0x91, 0x21),
            Self::TitaniumYellow => LabelColor::rgb(0xee, 0xe6, 0x00),
            Self::GreenCyan => LabelColor::rgb(0x00, 0x99, 0x66),
            Self::DarkSeaGreen => LabelColor::rgb(0x8f, 0xbc, 0x8f),
            Self::BlueGray => LabelColor::rgb(0x66, 0x99, 0xcc),
            Self::Lavender => LabelColor::rgb(0xe6, 0xe6, 0xfa),
            Self::DarkViolet => LabelColor::rgb(0x94, 0x00, 0xd3),
            Self::DeepViolet => LabelColor::rgb(0x33, 0x00, 0x66),
            Self::CharcoalGrey => LabelColor::rgb(0x36, 0x45, 0x4f),
            Self::Gray => LabelColor::rgb(0x80, 0x80, 0x80),
        }
    }
}

// FIXME: When removing the `DEPRECATED` `impl From` instances, remove this type and make
// `LabelColor` `impl Copy`.
#[derive(Debug, Clone)]
enum RgbOrString {
    Rgb(u8, u8, u8),
    String(String),
}

/// The color of a label.
///
/// Crates such as `css-named-colors` may be used to get an RGB tuple for CSS-named colors.
#[derive(Debug, Clone)]
pub struct LabelColor {
    color: RgbOrString,
}

impl LabelColor {
    /// Create an RGB label color.
    fn rgb(red: u8, green: u8, blue: u8) -> Self {
        let color = RgbOrString::Rgb(red, green, blue);

        Self {
            color,
        }
    }

    fn named<N>(name: N) -> Self
    where
        N: Into<String>,
    {
        let color = RgbOrString::String(name.into());

        Self {
            color,
        }
    }

    /// The string representation of a label color.
    fn as_str(&self) -> String {
        match &self.color {
            RgbOrString::Rgb(r, g, b) => format!("#{r:02x}{g:02x}{b:02x}"),
            RgbOrString::String(string) => string.clone(),
        }
    }
}

impl From<GitlabDefaultColor> for LabelColor {
    fn from(gdc: GitlabDefaultColor) -> Self {
        gdc.to_label_color()
    }
}

impl From<(u8, u8, u8)> for LabelColor {
    fn from((r, g, b): (u8, u8, u8)) -> Self {
        Self::rgb(r, g, b)
    }
}

/// DEPRECATED!
///
/// Crates such as `css-named-colors` may be used to get an RGB tuple for CSS-named colors.
impl From<&str> for LabelColor {
    fn from(s: &str) -> Self {
        Self::named(s)
    }
}

/// DEPRECATED!
///
/// Crates such as `css-named-colors` may be used to get an RGB tuple for CSS-named colors.
impl From<String> for LabelColor {
    fn from(s: String) -> Self {
        Self::named(s)
    }
}

/// DEPRECATED!
///
/// Crates such as `css-named-colors` may be used to get an RGB tuple for CSS-named colors.
impl From<Cow<'_, str>> for LabelColor {
    fn from(s: Cow<str>) -> Self {
        Self::named(s)
    }
}

impl ParamValue<'static> for &LabelColor {
    fn as_value(&self) -> Cow<'static, str> {
        self.as_str().into()
    }
}

/// Create a label within a project.
#[derive(Debug, Builder, Clone)]
#[builder(setter(strip_option))]
pub struct CreateLabel<'a> {
    /// The project to create a label within.
    #[builder(setter(into))]
    project: NameOrId<'a>,
    /// The name of the label.
    #[builder(setter(into))]
    name: Cow<'a, str>,
    /// The color of the label.
    #[builder(setter(into))]
    color: LabelColor,

    /// The description of the label.
    #[builder(setter(into), default)]
    description: Option<Cow<'a, str>>,
    /// The priority of the label.
    #[builder(setter(into), default)]
    priority: Option<LabelPriority>,
}

impl<'a> CreateLabel<'a> {
    /// Create a builder for the endpoint.
    pub fn builder() -> CreateLabelBuilder<'a> {
        CreateLabelBuilder::default()
    }
}

impl Endpoint for CreateLabel<'_> {
    fn method(&self) -> Method {
        Method::POST
    }

    fn endpoint(&self) -> Cow<'static, str> {
        format!("projects/{}/labels", self.project).into()
    }

    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
        let mut params = FormParams::default();

        params
            .push("name", &self.name)
            .push("color", &self.color)
            .push_opt("description", self.description.as_ref())
            .push_opt("priority", self.priority);

        params.into_body()
    }
}

#[cfg(test)]
mod tests {
    use http::Method;

    use crate::api::projects::labels::{
        CreateLabel, CreateLabelBuilderError, GitlabDefaultColor, LabelColor, LabelPriority,
    };
    use crate::api::{self, Query};
    use crate::test::client::{ExpectedUrl, SingleTestClient};

    #[test]
    fn label_priority_as_str() {
        let items = &[
            (LabelPriority::Null, "null"),
            (LabelPriority::Value(100), "100"),
        ];

        for (i, s) in items {
            assert_eq!(i.as_str(), *s);
        }
    }

    #[test]
    fn label_color_as_str() {
        let items = &[
            (GitlabDefaultColor::MagentaPink.into(), "#cc338b"),
            (GitlabDefaultColor::Crimson.into(), "#dc143c"),
            (GitlabDefaultColor::RoseRed.into(), "#c21e56"),
            (GitlabDefaultColor::DarkCoral.into(), "#cd5b45"),
            (GitlabDefaultColor::CarrotOrange.into(), "#ed9121"),
            (GitlabDefaultColor::TitaniumYellow.into(), "#eee600"),
            (GitlabDefaultColor::GreenCyan.into(), "#009966"),
            (GitlabDefaultColor::DarkSeaGreen.into(), "#8fbc8f"),
            (GitlabDefaultColor::BlueGray.into(), "#6699cc"),
            (GitlabDefaultColor::Lavender.into(), "#e6e6fa"),
            (GitlabDefaultColor::DarkViolet.into(), "#9400d3"),
            (GitlabDefaultColor::DeepViolet.into(), "#330066"),
            (GitlabDefaultColor::CharcoalGrey.into(), "#36454f"),
            (GitlabDefaultColor::Gray.into(), "#808080"),
            (LabelColor::rgb(0xf1, 0x00, 0xfe), "#f100fe"),
            (LabelColor::named("blue"), "blue"),
        ];

        for (i, s) in items {
            assert_eq!(i.as_str(), *s);
        }
    }

    #[test]
    fn project_name_and_color_are_necessary() {
        let err = CreateLabel::builder().build().unwrap_err();
        crate::test::assert_missing_field!(err, CreateLabelBuilderError, "project");
    }

    #[test]
    fn project_is_necessary() {
        let err = CreateLabel::builder()
            .name("label")
            .color((0xf1, 0x00, 0xfe))
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, CreateLabelBuilderError, "project");
    }

    #[test]
    fn name_is_necessary() {
        let err = CreateLabel::builder()
            .project(1)
            .color((0xf1, 0x00, 0xfe))
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, CreateLabelBuilderError, "name");
    }

    #[test]
    fn color_is_necessary() {
        let err = CreateLabel::builder()
            .project(1)
            .name("label")
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, CreateLabelBuilderError, "color");
    }

    #[test]
    fn project_name_and_color_are_sufficient() {
        CreateLabel::builder()
            .project(1)
            .name("label")
            .color((0xf1, 0x00, 0xfe))
            .build()
            .unwrap();
    }

    #[test]
    fn endpoint() {
        let endpoint = ExpectedUrl::builder()
            .method(Method::POST)
            .endpoint("projects/simple%2Fproject/labels")
            .content_type("application/x-www-form-urlencoded")
            .body_str(concat!("name=label", "&color=%23ffffff"))
            .build()
            .unwrap();
        let client = SingleTestClient::new_raw(endpoint, "");

        let endpoint = CreateLabel::builder()
            .project("simple/project")
            .name("label")
            .color((0xff, 0xff, 0xff))
            .build()
            .unwrap();
        api::ignore(endpoint).query(&client).unwrap();
    }

    #[test]
    fn endpoint_description() {
        let endpoint = ExpectedUrl::builder()
            .method(Method::POST)
            .endpoint("projects/simple%2Fproject/labels")
            .content_type("application/x-www-form-urlencoded")
            .body_str(concat!(
                "name=label",
                "&color=%23ffffff",
                "&description=description",
            ))
            .build()
            .unwrap();
        let client = SingleTestClient::new_raw(endpoint, "");

        let endpoint = CreateLabel::builder()
            .project("simple/project")
            .name("label")
            .color((0xff, 0xff, 0xff))
            .description("description")
            .build()
            .unwrap();
        api::ignore(endpoint).query(&client).unwrap();
    }

    #[test]
    fn endpoint_priority() {
        let endpoint = ExpectedUrl::builder()
            .method(Method::POST)
            .endpoint("projects/simple%2Fproject/labels")
            .content_type("application/x-www-form-urlencoded")
            .body_str(concat!("name=label", "&color=%23808080", "&priority=1"))
            .build()
            .unwrap();
        let client = SingleTestClient::new_raw(endpoint, "");

        let endpoint = CreateLabel::builder()
            .project("simple/project")
            .name("label")
            .color(GitlabDefaultColor::Gray)
            .priority(1)
            .build()
            .unwrap();
        api::ignore(endpoint).query(&client).unwrap();
    }
}