use std::{collections::HashSet, fmt::Debug};
use async_trait::async_trait;
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use crate::{CommitRange, ModelError, ProviderAppId, Repository, Result};
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum BranchUpdateRequirement {
Required,
NotRequired,
}
#[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, PartialEq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum AppliedRequiredCheckState {
Missing,
Pending,
Satisfied,
Failed,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct AppliedRequiredCheckWire {
name: String,
provider_application: Option<ProviderAppId>,
state: AppliedRequiredCheckState,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(
try_from = "AppliedRequiredCheckWire",
into = "AppliedRequiredCheckWire"
)]
pub struct AppliedRequiredCheck {
name: String,
provider_application: Option<ProviderAppId>,
state: AppliedRequiredCheckState,
}
impl AppliedRequiredCheck {
pub fn new(
name: impl Into<String>,
provider_application: Option<ProviderAppId>,
state: AppliedRequiredCheckState,
) -> std::result::Result<Self, ModelError> {
let name = non_empty(name, "required check name")?;
Ok(Self {
name,
provider_application,
state,
})
}
#[must_use]
pub fn name(&self) -> &str {
&self.name
}
#[must_use]
pub const fn provider_application(&self) -> Option<&ProviderAppId> {
self.provider_application.as_ref()
}
#[must_use]
pub const fn state(&self) -> AppliedRequiredCheckState {
self.state
}
}
impl TryFrom<AppliedRequiredCheckWire> for AppliedRequiredCheck {
type Error = ModelError;
fn try_from(value: AppliedRequiredCheckWire) -> std::result::Result<Self, Self::Error> {
Self::new(value.name, value.provider_application, value.state)
}
}
impl From<AppliedRequiredCheck> for AppliedRequiredCheckWire {
fn from(value: AppliedRequiredCheck) -> Self {
Self {
name: value.name,
provider_application: value.provider_application,
state: value.state,
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
struct AppliedSourceRequirementsWire {
repository: Repository,
target_branch: String,
commit_range: CommitRange,
required_approvals: u32,
branch_update: BranchUpdateRequirement,
required_checks: Vec<AppliedRequiredCheck>,
}
#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
#[serde(
try_from = "AppliedSourceRequirementsWire",
into = "AppliedSourceRequirementsWire"
)]
pub struct AppliedSourceRequirements {
repository: Repository,
target_branch: String,
commit_range: CommitRange,
required_approvals: u32,
branch_update: BranchUpdateRequirement,
required_checks: Vec<AppliedRequiredCheck>,
}
impl AppliedSourceRequirements {
#[allow(clippy::too_many_arguments)]
pub fn new(
repository: Repository,
target_branch: impl Into<String>,
commit_range: CommitRange,
required_approvals: u32,
branch_update: BranchUpdateRequirement,
required_checks: Vec<AppliedRequiredCheck>,
) -> std::result::Result<Self, ModelError> {
let target_branch = non_empty(target_branch, "target branch")?;
non_empty(&commit_range.base_sha, "base sha")?;
non_empty(&commit_range.head_sha, "head sha")?;
let mut identities = HashSet::with_capacity(required_checks.len());
for check in &required_checks {
let identity = (check.name.clone(), check.provider_application.clone());
if !identities.insert(identity) {
return Err(ModelError::DuplicateRequiredCheck {
name: check.name.clone(),
});
}
}
Ok(Self {
repository,
target_branch,
commit_range,
required_approvals,
branch_update,
required_checks,
})
}
#[must_use]
pub const fn repository(&self) -> &Repository {
&self.repository
}
#[must_use]
pub fn target_branch(&self) -> &str {
&self.target_branch
}
#[must_use]
pub const fn commit_range(&self) -> &CommitRange {
&self.commit_range
}
#[must_use]
pub const fn required_approvals(&self) -> u32 {
self.required_approvals
}
#[must_use]
pub const fn branch_update(&self) -> BranchUpdateRequirement {
self.branch_update
}
#[must_use]
pub fn required_checks(&self) -> &[AppliedRequiredCheck] {
&self.required_checks
}
}
impl TryFrom<AppliedSourceRequirementsWire> for AppliedSourceRequirements {
type Error = ModelError;
fn try_from(value: AppliedSourceRequirementsWire) -> std::result::Result<Self, Self::Error> {
Self::new(
value.repository,
value.target_branch,
value.commit_range,
value.required_approvals,
value.branch_update,
value.required_checks,
)
}
}
impl From<AppliedSourceRequirements> for AppliedSourceRequirementsWire {
fn from(value: AppliedSourceRequirements) -> Self {
Self {
repository: value.repository,
target_branch: value.target_branch,
commit_range: value.commit_range,
required_approvals: value.required_approvals,
branch_update: value.branch_update,
required_checks: value.required_checks,
}
}
}
#[async_trait]
pub trait SourceCodeConfigurationProvider: Send + Sync {
type Ruleset: Clone + Debug + DeserializeOwned + Serialize + Send + Sync + 'static;
async fn read_rulesets(&self, repository: &Repository) -> Result<Vec<Self::Ruleset>>;
async fn apply_ruleset(
&self,
repository: &Repository,
ruleset: &Self::Ruleset,
) -> Result<Self::Ruleset>;
}
#[async_trait]
pub trait AppliedSourceRequirementsProvider: Send + Sync {
async fn applied_requirements(
&self,
repository: &Repository,
target_branch: &str,
commit_range: &CommitRange,
) -> Result<AppliedSourceRequirements>;
}
fn non_empty(
value: impl Into<String>,
field: &'static str,
) -> std::result::Result<String, ModelError> {
let value = value.into();
if value.is_empty() {
Err(ModelError::Empty { field })
} else {
Ok(value)
}
}