interprex 1.0.0

Provider-neutral models and async interfaces for development platforms
Documentation
use std::{fmt, str::FromStr};

use async_trait::async_trait;
use secrecy::SecretString;
use serde::{Deserialize, Serialize};

use crate::{ModelError, Result, error::segment};

#[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)]
pub struct Repository {
    owner: String,
    name: String,
}

impl Repository {
    pub fn new(
        owner: impl Into<String>,
        name: impl Into<String>,
    ) -> std::result::Result<Self, ModelError> {
        Ok(Self {
            owner: segment(owner, "owner")?,
            name: segment(name, "repository name")?,
        })
    }

    #[must_use]
    pub fn owner(&self) -> &str {
        &self.owner
    }

    #[must_use]
    pub fn name(&self) -> &str {
        &self.name
    }
}

impl fmt::Display for Repository {
    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(formatter, "{}/{}", self.owner, self.name)
    }
}

impl FromStr for Repository {
    type Err = ModelError;

    fn from_str(value: &str) -> std::result::Result<Self, Self::Err> {
        let (owner, name) = value.split_once('/').ok_or(ModelError::InvalidRepository)?;
        if name.contains('/') {
            return Err(ModelError::InvalidRepository);
        }
        Self::new(owner, name)
    }
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RepositoryFacts {
    pub repository: Repository,
    pub default_branch: String,
    pub private: bool,
    pub archived: bool,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RepositorySettings {
    pub allow_squash_merge: bool,
    pub allow_merge_commit: bool,
    pub allow_rebase_merge: bool,
    pub delete_branch_on_merge: bool,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct RequiredCheck {
    pub context: String,
    pub integration_id: Option<u64>,
}

#[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)]
pub struct Ruleset {
    pub id: Option<u64>,
    pub name: String,
    pub active: bool,
    pub target_branch_patterns: Vec<String>,
    pub required_checks: Vec<RequiredCheck>,
    pub required_approvals: u8,
}

#[async_trait]
pub trait CodeHostingProvider: Send + Sync {
    async fn repository(&self, repository: &Repository) -> Result<RepositoryFacts>;
    async fn settings(&self, repository: &Repository) -> Result<RepositorySettings>;
    async fn apply_settings(
        &self,
        repository: &Repository,
        settings: &RepositorySettings,
    ) -> Result<RepositorySettings>;
    async fn rulesets(&self, repository: &Repository) -> Result<Vec<Ruleset>>;
    async fn upsert_ruleset(&self, repository: &Repository, ruleset: &Ruleset) -> Result<Ruleset>;
    async fn put_secret(
        &self,
        repository: &Repository,
        name: &str,
        value: SecretString,
    ) -> Result<()>;
}

#[cfg(test)]
mod tests {
    use super::Repository;
    use crate::ModelError;

    #[test]
    fn repository_round_trips_through_its_canonical_address() {
        let repository: Repository = "civitas-forge/interprex".parse().expect("valid repository");
        assert_eq!(repository.to_string(), "civitas-forge/interprex");
    }

    #[test]
    fn repository_rejects_extra_path_segments() {
        assert_eq!(
            "civitas-forge/interprex/extra".parse::<Repository>(),
            Err(ModelError::InvalidRepository)
        );
    }
}