interprex 6.0.0

Provider-neutral models and async interfaces for development platforms
Documentation
use async_trait::async_trait;
use interprex::{
    AppliedRequiredCheck, AppliedRequiredCheckState, AppliedSourceRequirements,
    AppliedSourceRequirementsProvider, BranchUpdateRequirement, CommitRange, ModelError,
    ProviderAppId, ProviderError, Repository, Result, SourceCodeConfigurationProvider,
};
use serde::{Deserialize, Serialize, de::DeserializeOwned};
use std::fmt::Debug;

fn repository() -> Repository {
    Repository::new("civitas-forge", "interprex").expect("repository")
}

fn required_check(name: &str, app: Option<&str>) -> AppliedRequiredCheck {
    AppliedRequiredCheck::new(
        name,
        app.map(|id| ProviderAppId::new(id).expect("application id")),
        AppliedRequiredCheckState::Satisfied,
    )
    .expect("required check")
}

#[test]
fn applied_requirements_round_trip_the_exact_subject_and_provider_answers() {
    let requirements = AppliedSourceRequirements::new(
        repository(),
        "main",
        CommitRange {
            base_sha: "base-2".to_owned(),
            head_sha: "head-3".to_owned(),
        },
        2,
        BranchUpdateRequirement::Required,
        vec![
            required_check("quality", Some("15368")),
            AppliedRequiredCheck::new("legacy-status", None, AppliedRequiredCheckState::Pending)
                .expect("required check"),
        ],
    )
    .expect("requirements");

    let value = serde_json::to_value(&requirements).expect("serialize requirements");
    assert_eq!(
        value["repository"],
        serde_json::json!({"owner": "civitas-forge", "name": "interprex"})
    );
    assert_eq!(value["target_branch"], "main");
    assert_eq!(value["commit_range"]["base_sha"], "base-2");
    assert_eq!(value["commit_range"]["head_sha"], "head-3");
    assert_eq!(value["required_approvals"], 2);
    assert_eq!(value["branch_update"], "required");
    assert_eq!(value["required_checks"][0]["provider_application"], "15368");
    assert_eq!(value["required_checks"][1]["state"], "pending");
    assert_eq!(
        serde_json::from_value::<AppliedSourceRequirements>(value)
            .expect("deserialize requirements"),
        requirements
    );
}

#[test]
fn applied_requirements_reject_empty_exact_revision_fields() {
    for (target_branch, base_sha, head_sha, field) in [
        ("", "base", "head", "target branch"),
        ("main", "", "head", "base sha"),
        ("main", "base", "", "head sha"),
    ] {
        assert_eq!(
            AppliedSourceRequirements::new(
                repository(),
                target_branch,
                CommitRange {
                    base_sha: base_sha.to_owned(),
                    head_sha: head_sha.to_owned(),
                },
                0,
                BranchUpdateRequirement::NotRequired,
                vec![],
            ),
            Err(ModelError::Empty { field })
        );
    }
}

#[test]
fn required_checks_reject_empty_names_and_duplicate_identities() {
    assert_eq!(
        AppliedRequiredCheck::new("", None, AppliedRequiredCheckState::Missing),
        Err(ModelError::Empty {
            field: "required check name"
        })
    );

    let duplicate = required_check("quality", Some("15368"));
    assert!(matches!(
        AppliedSourceRequirements::new(
            repository(),
            "main",
            CommitRange {
                base_sha: "base".to_owned(),
                head_sha: "head".to_owned(),
            },
            1,
            BranchUpdateRequirement::Required,
            vec![duplicate.clone(), duplicate],
        ),
        Err(ModelError::DuplicateRequiredCheck { .. })
    ));
}

#[test]
fn the_same_check_name_from_distinct_provider_applications_is_not_a_duplicate() {
    let requirements = AppliedSourceRequirements::new(
        repository(),
        "main",
        CommitRange {
            base_sha: "base".to_owned(),
            head_sha: "head".to_owned(),
        },
        0,
        BranchUpdateRequirement::NotRequired,
        vec![
            required_check("quality", Some("15368")),
            required_check("quality", Some("20480")),
        ],
    )
    .expect("distinct native requirements");

    assert_eq!(requirements.required_checks().len(), 2);
}

#[test]
fn deserialization_revalidates_exact_subjects_and_check_identities() {
    let invalid = serde_json::json!({
        "repository": {"owner": "civitas-forge", "name": "interprex"},
        "target_branch": "main",
        "commit_range": {"base_sha": "base", "head_sha": ""},
        "required_approvals": 1,
        "branch_update": "required",
        "required_checks": [
            {"name": "quality", "provider_application": "15368", "state": "missing"}
        ]
    });
    assert!(serde_json::from_value::<AppliedSourceRequirements>(invalid).is_err());
}

#[test]
fn every_required_check_state_has_a_stable_serialized_name() {
    for (state, name) in [
        (AppliedRequiredCheckState::Missing, "missing"),
        (AppliedRequiredCheckState::Pending, "pending"),
        (AppliedRequiredCheckState::Satisfied, "satisfied"),
        (AppliedRequiredCheckState::Failed, "failed"),
    ] {
        let check = AppliedRequiredCheck::new("quality", None, state).expect("required check");
        assert_eq!(
            serde_json::to_value(check).expect("serialize check")["state"],
            name
        );
    }
}

fn accepts_object_safe_applied_provider(_: &dyn AppliedSourceRequirementsProvider) {}

fn accepts_configuration_provider<P>(_: &P)
where
    P: SourceCodeConfigurationProvider,
    P::Ruleset: Clone + Debug + DeserializeOwned + Serialize + Send + Sync + 'static,
{
}

struct InterfaceProof;

#[async_trait]
impl AppliedSourceRequirementsProvider for InterfaceProof {
    async fn applied_requirements(
        &self,
        _repository: &Repository,
        _target_branch: &str,
        _commit_range: &CommitRange,
    ) -> Result<AppliedSourceRequirements> {
        Err(ProviderError::Unsupported {
            provider: "proof",
            operation: "read applied source requirements",
        })
    }
}

#[derive(Clone, Debug, Deserialize, Serialize)]
struct ProofRuleset;

#[async_trait]
impl SourceCodeConfigurationProvider for InterfaceProof {
    type Ruleset = ProofRuleset;

    async fn read_rulesets(&self, _repository: &Repository) -> Result<Vec<Self::Ruleset>> {
        Err(ProviderError::Unsupported {
            provider: "proof",
            operation: "read source rulesets",
        })
    }

    async fn apply_ruleset(
        &self,
        _repository: &Repository,
        _ruleset: &Self::Ruleset,
    ) -> Result<Self::Ruleset> {
        Err(ProviderError::Unsupported {
            provider: "proof",
            operation: "apply source ruleset",
        })
    }
}

#[test]
fn applied_provider_is_object_safe_and_configuration_retains_its_ruleset_type() {
    let provider = InterfaceProof;
    accepts_object_safe_applied_provider(&provider);
    accepts_configuration_provider(&provider);
}