use serde::{Deserialize, Serialize};
use std::error::Error;
use std::fmt;
#[derive(Debug)]
pub struct SourceBuildError(String);
impl fmt::Display for SourceBuildError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Source build error: {}", self.0)
}
}
impl Error for SourceBuildError {}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Branch {
#[serde(skip_serializing_if = "Option::is_none")]
display_name: Option<String>,
}
impl Branch {
#[must_use]
pub fn new(display_name: impl Into<String>) -> Self {
Self {
display_name: Some(display_name.into()),
}
}
#[must_use]
pub fn display_name(&self) -> Option<&str> {
self.display_name.as_deref()
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct GithubRepo {
#[serde(skip_serializing_if = "Option::is_none")]
owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
repo: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
is_private: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
default_branch: Option<Branch>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
branches: Vec<Branch>,
}
impl GithubRepo {
#[must_use]
pub fn new(owner: impl Into<String>, repo: impl Into<String>) -> Self {
Self {
owner: Some(owner.into()),
repo: Some(repo.into()),
..Self::default()
}
}
#[must_use]
pub fn with_is_private(mut self, is_private: bool) -> Self {
self.is_private = Some(is_private);
self
}
#[must_use]
pub fn with_default_branch(mut self, default_branch: Branch) -> Self {
self.default_branch = Some(default_branch);
self
}
#[must_use]
pub fn with_branches(mut self, branches: Vec<Branch>) -> Self {
self.branches = branches;
self
}
#[must_use]
pub fn owner(&self) -> Option<&str> {
self.owner.as_deref()
}
#[must_use]
pub fn repo(&self) -> Option<&str> {
self.repo.as_deref()
}
#[must_use]
pub fn is_private(&self) -> Option<bool> {
self.is_private
}
#[must_use]
pub fn default_branch(&self) -> Option<&Branch> {
self.default_branch.as_ref()
}
#[must_use]
pub fn branches(&self) -> &[Branch] {
&self.branches
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Source {
#[serde(skip_serializing_if = "Option::is_none")]
id: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
github_repo: Option<GithubRepo>,
}
impl Source {
#[must_use]
pub fn builder() -> SourceBuilder {
SourceBuilder::default()
}
#[must_use]
pub fn id(&self) -> Option<&str> {
self.id.as_deref()
}
#[must_use]
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
#[must_use]
pub fn github_repo(&self) -> Option<&GithubRepo> {
self.github_repo.as_ref()
}
}
#[derive(Debug, Default)]
pub struct SourceBuilder {
id: Option<String>,
name: Option<String>,
github_repo: Option<GithubRepo>,
}
impl SourceBuilder {
#[must_use]
pub fn id(mut self, id: impl Into<String>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn name(mut self, name: impl Into<String>) -> Self {
self.name = Some(name.into());
self
}
#[must_use]
pub fn github_repo(mut self, github_repo: GithubRepo) -> Self {
self.github_repo = Some(github_repo);
self
}
pub fn build(self) -> Result<Source, SourceBuildError> {
Ok(Source {
id: self.id,
name: self.name,
github_repo: self.github_repo,
})
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_source_builder_with_fields() {
let source = Source::builder()
.id("src-1")
.name("My Source")
.build()
.unwrap();
assert_eq!(source.id(), Some("src-1"));
assert_eq!(source.name(), Some("My Source"));
}
#[test]
fn test_source_builder_without_fields() {
let source = Source::builder().build().unwrap();
assert_eq!(source.id(), None);
assert_eq!(source.name(), None);
}
#[test]
fn test_source_builder_with_github_repo() {
let source = Source::builder()
.id("src-1")
.name("sources/github/example-owner/example-repo")
.github_repo(GithubRepo::new("example-owner", "example-repo").with_is_private(true))
.build()
.unwrap();
assert_eq!(
source.github_repo().and_then(GithubRepo::owner),
Some("example-owner")
);
}
#[test]
fn test_source_deserializes_real_api_shape() {
let json = r#"{
"name": "sources/github/example-owner/example-repo",
"githubRepo": {
"owner": "example-owner",
"repo": "example-repo",
"isPrivate": true,
"defaultBranch": {
"displayName": "main"
},
"branches": [
{"displayName": "main"},
{"displayName": "feature/x"}
]
},
"id": "github/example-owner/example-repo"
}"#;
let source: Source = serde_json::from_str(json).unwrap();
assert_eq!(
source.name(),
Some("sources/github/example-owner/example-repo")
);
let repo = source.github_repo().unwrap();
assert_eq!(repo.owner(), Some("example-owner"));
assert_eq!(repo.is_private(), Some(true));
assert_eq!(
repo.default_branch().and_then(Branch::display_name),
Some("main")
);
assert_eq!(repo.branches().len(), 2);
}
}