use std::str;
use derive_builder::Builder;
use log::warn;
use crate::api::common::{self, NameOrId};
use crate::api::endpoint_prelude::*;
use crate::api::ParamValue;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Encoding {
Text,
Base64,
}
impl Default for Encoding {
fn default() -> Self {
Encoding::Text
}
}
impl Encoding {
fn is_binary_safe(self) -> bool {
match self {
Encoding::Text => false,
Encoding::Base64 => true,
}
}
fn encode<'a>(self, as_string: Option<&'a str>, content: &'a [u8]) -> Cow<'a, str> {
match self {
Encoding::Text => {
if let Some(string) = as_string {
string.into()
} else {
panic!("attempting to encode non-utf8 content using text!");
}
},
Encoding::Base64 => base64::encode(content).into(),
}
}
fn as_str(self) -> &'static str {
match self {
Encoding::Text => "text",
Encoding::Base64 => "base64",
}
}
}
impl ParamValue<'static> for Encoding {
fn as_value(&self) -> Cow<'static, str> {
self.as_str().into()
}
}
#[derive(Debug, Builder)]
#[builder(setter(strip_option))]
pub struct CreateFile<'a> {
#[builder(setter(into))]
project: NameOrId<'a>,
#[builder(setter(into))]
file_path: Cow<'a, str>,
#[builder(setter(into))]
branch: Cow<'a, str>,
#[builder(setter(into))]
content: Cow<'a, [u8]>,
#[builder(setter(into))]
commit_message: Cow<'a, str>,
#[builder(setter(into), default)]
start_branch: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
encoding: Option<Encoding>,
#[builder(setter(into), default)]
author_email: Option<Cow<'a, str>>,
#[builder(setter(into), default)]
author_name: Option<Cow<'a, str>>,
}
impl<'a> CreateFile<'a> {
pub fn builder() -> CreateFileBuilder<'a> {
CreateFileBuilder::default()
}
}
const SAFE_ENCODING: Encoding = Encoding::Base64;
impl<'a> Endpoint for CreateFile<'a> {
fn method(&self) -> Method {
Method::POST
}
fn endpoint(&self) -> Cow<'static, str> {
format!(
"projects/{}/repository/files/{}",
self.project,
common::path_escaped(&self.file_path),
)
.into()
}
fn body(&self) -> Result<Option<(&'static str, Vec<u8>)>, BodyError> {
let mut params = FormParams::default();
params
.push("branch", &self.branch)
.push("commit_message", &self.commit_message)
.push_opt("start_branch", self.start_branch.as_ref())
.push_opt("author_email", self.author_email.as_ref())
.push_opt("author_name", self.author_name.as_ref());
let content = str::from_utf8(&self.content);
let needs_encoding = content.is_err();
let encoding = self.encoding.unwrap_or_default();
let actual_encoding = if needs_encoding && !encoding.is_binary_safe() {
warn!(
"forcing the encoding to {} due to utf-8 unsafe content",
SAFE_ENCODING.as_str(),
);
SAFE_ENCODING
} else {
encoding
};
params.push(
"content",
actual_encoding.encode(content.ok(), &self.content),
);
self.encoding
.map(|_| actual_encoding)
.or_else(|| {
if actual_encoding != Encoding::default() {
Some(actual_encoding)
} else {
None
}
})
.map(|value| params.push("encoding", value));
params.into_body()
}
}
#[cfg(test)]
mod tests {
use http::Method;
use crate::api::projects::repository::files::{CreateFile, Encoding};
use crate::api::{self, Query};
use crate::test::client::{ExpectedUrl, SingleTestClient};
#[test]
fn encoding_default() {
assert_eq!(Encoding::default(), Encoding::Text);
}
#[test]
fn encoding_is_binary_safe() {
let items = &[(Encoding::Text, false), (Encoding::Base64, true)];
for (i, s) in items {
assert_eq!(i.is_binary_safe(), *s);
}
}
#[test]
fn encoding_encode_text() {
let encoding = Encoding::Text;
assert_eq!(encoding.encode(Some("foo"), b"foo"), "foo");
}
#[test]
#[should_panic = "attempting to encode non-utf8 content using text!"]
fn encoding_encode_text_bad() {
let encoding = Encoding::Text;
encoding.encode(None, b"\xff");
}
#[test]
fn encoding_encode_base64() {
let encoding = Encoding::Base64;
assert_eq!(encoding.encode(None, b"foo"), "Zm9v");
}
#[test]
fn encoding_as_str() {
let items = &[(Encoding::Text, "text"), (Encoding::Base64, "base64")];
for (i, s) in items {
assert_eq!(i.as_str(), *s);
}
}
#[test]
fn all_parameters_are_needed() {
let err = CreateFile::builder().build().unwrap_err();
assert_eq!(err, "`project` must be initialized");
}
#[test]
fn project_is_required() {
let err = CreateFile::builder()
.file_path("new/file")
.branch("master")
.commit_message("commit message")
.content(&b"contents"[..])
.build()
.unwrap_err();
assert_eq!(err, "`project` must be initialized");
}
#[test]
fn file_path_is_required() {
let err = CreateFile::builder()
.project(1)
.branch("master")
.commit_message("commit message")
.content(&b"contents"[..])
.build()
.unwrap_err();
assert_eq!(err, "`file_path` must be initialized");
}
#[test]
fn branch_is_required() {
let err = CreateFile::builder()
.project(1)
.file_path("new/file")
.commit_message("commit message")
.content(&b"contents"[..])
.build()
.unwrap_err();
assert_eq!(err, "`branch` must be initialized");
}
#[test]
fn commit_message_is_required() {
let err = CreateFile::builder()
.project(1)
.file_path("new/file")
.branch("master")
.content(&b"contents"[..])
.build()
.unwrap_err();
assert_eq!(err, "`commit_message` must be initialized");
}
#[test]
fn content_is_required() {
let err = CreateFile::builder()
.project(1)
.file_path("new/file")
.branch("master")
.commit_message("commit message")
.build()
.unwrap_err();
assert_eq!(err, "`content` must be initialized");
}
#[test]
fn sufficient_parameters() {
CreateFile::builder()
.project(1)
.file_path("new/file")
.branch("master")
.commit_message("commit message")
.content(&b"contents"[..])
.build()
.unwrap();
}
#[test]
fn endpoint() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"branch=branch",
"&commit_message=commit+message",
"&content=file+contents",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateFile::builder()
.project("simple/project")
.file_path("path/to/file")
.branch("branch")
.content(&b"file contents"[..])
.commit_message("commit message")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_start_branch() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"branch=branch",
"&commit_message=commit+message",
"&start_branch=master",
"&content=file+contents",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateFile::builder()
.project("simple/project")
.file_path("path/to/file")
.branch("branch")
.content(&b"file contents"[..])
.commit_message("commit message")
.start_branch("master")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_encoding() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"branch=branch",
"&commit_message=commit+message",
"&content=ZmlsZSBjb250ZW50cw%3D%3D",
"&encoding=base64",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateFile::builder()
.project("simple/project")
.file_path("path/to/file")
.branch("branch")
.content(&b"file contents"[..])
.commit_message("commit message")
.encoding(Encoding::Base64)
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_encoding_upgrade() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"branch=branch",
"&commit_message=commit+message",
"&content=%2Fw%3D%3D",
"&encoding=base64",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateFile::builder()
.project("simple/project")
.file_path("path/to/file")
.branch("branch")
.content(&b"\xff"[..])
.commit_message("commit message")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_author_email() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"branch=branch",
"&commit_message=commit+message",
"&author_email=author%40email.invalid",
"&content=file+contents",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateFile::builder()
.project("simple/project")
.file_path("path/to/file")
.branch("branch")
.content(&b"file contents"[..])
.commit_message("commit message")
.author_email("author@email.invalid")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
#[test]
fn endpoint_author_name() {
let endpoint = ExpectedUrl::builder()
.method(Method::POST)
.endpoint("projects/simple%2Fproject/repository/files/path%2Fto%2Ffile")
.content_type("application/x-www-form-urlencoded")
.body_str(concat!(
"branch=branch",
"&commit_message=commit+message",
"&author_name=Arthur+Developer",
"&content=file+contents",
))
.build()
.unwrap();
let client = SingleTestClient::new_raw(endpoint, "");
let endpoint = CreateFile::builder()
.project("simple/project")
.file_path("path/to/file")
.branch("branch")
.content(&b"file contents"[..])
.commit_message("commit message")
.author_name("Arthur Developer")
.build()
.unwrap();
api::ignore(endpoint).query(&client).unwrap();
}
}