gitlab 0.1900.1

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::*;

const BOUNDARY: &str = "RustGitLabBoundary-9a005164cd1db162d51154c6";
const CONTENT_TYPE: &str =
    "multipart/form-data; boundary=RustGitLabBoundary-9a005164cd1db162d51154c6";

/// Import a project from an exported archive file.
#[derive(Debug, Builder, Clone)]
#[builder(setter(strip_option))]
pub struct ImportProject<'a> {
    /// The raw bytes of the project export archive.
    #[builder(setter(into))]
    file: Cow<'a, [u8]>,

    /// The path for the new project, including namespace (`namespace/project-name`).
    #[builder(setter(into))]
    path: Cow<'a, str>,

    /// Name of the project to import. Defaults to the path of the project if not provided.
    #[builder(setter(into), default)]
    name: Option<Cow<'a, str>>,

    /// Namespace to import the project into. Defaults to the current user's namespace.
    #[builder(setter(into), default)]
    namespace: Option<NameOrId<'a>>,

    /// Whether to overwrite the project.
    #[builder(default)]
    overwrite: Option<bool>,
    // TODO: add `overwrite_params` support, a sub-object of per-entity overwrite flags. (#129)
}

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

    fn multipart_body(&self) -> Vec<u8> {
        let mut m = MultipartBuilder::new(BOUNDARY);

        m.file_field("file", "archive.tar.gz", &self.file);
        m.text_field("path", &self.path);
        m.text_field_opt("name", self.name.as_deref());
        if let Some(namespace) = &self.namespace {
            match namespace {
                NameOrId::Id(id) => m.text_field("namespace_id", *id),
                NameOrId::Name(path) => m.text_field("namespace_path", path),
            }
        }
        m.text_field_opt("overwrite", self.overwrite);

        m.finish()
    }
}

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

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

    fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
        Ok(Some((CONTENT_TYPE, self.multipart_body())))
    }
}

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

    use crate::api::projects::import::{ImportProject, ImportProjectBuilderError};
    use crate::api::{self, MultipartBuilder, Query};
    use crate::test::client::{ExpectedUrl, SingleTestClient};

    use super::{BOUNDARY, CONTENT_TYPE};

    // Binary test data: gzip magic bytes followed by arbitrary non-UTF-8 content.
    const TEST_FILE: &[u8] = &[0x1f, 0x8b, 0x08, 0x00, 0xde, 0xad, 0xbe, 0xef];
    const TEST_PATH: &str = "my-group/my-project";

    fn make_body(extra_fields: &[(&str, &str)]) -> Vec<u8> {
        let mut m = MultipartBuilder::new(BOUNDARY);
        m.file_field("file", "archive.tar.gz", TEST_FILE);
        m.text_field("path", TEST_PATH);
        for (name, value) in extra_fields {
            m.text_field(name, *value);
        }
        m.finish()
    }

    fn make_endpoint(extra_fields: &[(&str, &str)]) -> ExpectedUrl {
        ExpectedUrl::builder()
            .method(Method::POST)
            .endpoint("projects/import")
            .content_type(CONTENT_TYPE)
            .body_bytes(make_body(extra_fields))
            .build()
            .unwrap()
    }

    fn base_builder() -> crate::api::projects::import::ImportProjectBuilder<'static> {
        let mut b = ImportProject::builder();
        b.file(TEST_FILE).path(TEST_PATH);
        b
    }

    #[test]
    fn file_is_necessary() {
        let err = ImportProject::builder()
            .path(TEST_PATH)
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, ImportProjectBuilderError, "file");
    }

    #[test]
    fn path_is_necessary() {
        let err = ImportProject::builder()
            .file(TEST_FILE)
            .build()
            .unwrap_err();
        crate::test::assert_missing_field!(err, ImportProjectBuilderError, "path");
    }

    #[test]
    fn all_required_fields_are_sufficient() {
        base_builder().build().unwrap();
    }

    #[test]
    fn endpoint() {
        let endpoint = make_endpoint(&[]);
        let client = SingleTestClient::new_raw(endpoint, "");
        api::ignore(base_builder().build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_name() {
        let endpoint = make_endpoint(&[("name", "My Project")]);
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(base_builder().name("My Project").build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_namespace_id() {
        let endpoint = make_endpoint(&[("namespace_id", "5")]);
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(base_builder().namespace(5).build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_namespace_path() {
        let endpoint = make_endpoint(&[("namespace_path", "my-group")]);
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(base_builder().namespace("my-group").build().unwrap())
            .query(&client)
            .unwrap();
    }

    #[test]
    fn endpoint_overwrite() {
        let endpoint = make_endpoint(&[("overwrite", "true")]);
        let client = SingleTestClient::new_raw(endpoint, "");

        api::ignore(base_builder().overwrite(true).build().unwrap())
            .query(&client)
            .unwrap();
    }
}