Skip to main content

agentics_contracts/validation/
github.rs

1//! Cross-field GitHub provenance validation for challenge review records.
2
3use agentics_domain::models::github::GithubPullRequestNumber;
4use agentics_domain::models::urls::{GithubPullRequestUrl, GithubRepoRemote};
5use agentics_error::{Result, ServiceError};
6
7/// Validated GitHub pull-request provenance tuple.
8#[derive(Debug, Clone)]
9pub struct GithubPullRequestRef {
10    repo_url: GithubRepoRemote,
11    pr_url: GithubPullRequestUrl,
12    pr_number: GithubPullRequestNumber,
13}
14
15impl GithubPullRequestRef {
16    /// Validate that repository URL, PR URL, and PR number describe the same PR.
17    pub fn try_new(
18        repo_url: GithubRepoRemote,
19        pr_url: GithubPullRequestUrl,
20        pr_number: GithubPullRequestNumber,
21    ) -> Result<Self> {
22        let pr_repo_key = pr_url
23            .repository_key()
24            .map_err(|e| ServiceError::Validation(e.to_string()))?;
25        if repo_url.repository_key() != &pr_repo_key {
26            return Err(ServiceError::Validation(format!(
27                "pr_url repository `{pr_repo_key}` must match repo_url repository `{}`",
28                repo_url.repository_key()
29            )));
30        }
31        let pr_url_number = pr_url
32            .number()
33            .map_err(|e| ServiceError::Validation(e.to_string()))?;
34        if pr_number.as_str() != pr_url_number {
35            return Err(ServiceError::Validation(format!(
36                "pr_url pull request number `{pr_url_number}` must match pr_number `{pr_number}`"
37            )));
38        }
39
40        Ok(Self {
41            repo_url,
42            pr_url,
43            pr_number,
44        })
45    }
46
47    /// Borrow the repository remote.
48    pub fn repo_url(&self) -> &GithubRepoRemote {
49        &self.repo_url
50    }
51
52    /// Borrow the pull request URL.
53    pub fn pr_url(&self) -> &GithubPullRequestUrl {
54        &self.pr_url
55    }
56
57    /// Borrow the pull request number.
58    pub fn pr_number(&self) -> &GithubPullRequestNumber {
59        &self.pr_number
60    }
61}
62
63#[cfg(test)]
64mod tests {
65    use agentics_domain::models::github::GithubPullRequestNumber;
66    use agentics_domain::models::urls::{GithubPullRequestUrl, GithubRepoRemote};
67
68    use super::GithubPullRequestRef;
69
70    #[test]
71    fn validates_matching_pull_request_reference() {
72        let reference = GithubPullRequestRef::try_new(
73            GithubRepoRemote::try_new("https://github.com/Agentics-Reifying/Agentics-Challenges")
74                .expect("repo"),
75            GithubPullRequestUrl::try_new(
76                "https://github.com/agentics-reifying/agentics-challenges/pull/42",
77            )
78            .expect("pr"),
79            GithubPullRequestNumber::try_new("42".to_string()).expect("number"),
80        )
81        .expect("reference should validate");
82
83        assert_eq!(
84            reference.repo_url().repository_key().as_str(),
85            "agentics-reifying/agentics-challenges"
86        );
87        assert_eq!(reference.pr_number().as_str(), "42");
88    }
89
90    #[test]
91    fn rejects_cross_field_mismatch() {
92        assert!(
93            GithubPullRequestRef::try_new(
94                GithubRepoRemote::try_new("https://github.com/agentics-reifying/agentics")
95                    .expect("repo"),
96                GithubPullRequestUrl::try_new(
97                    "https://github.com/agentics-reifying/agentics-challenges/pull/42",
98                )
99                .expect("pr"),
100                GithubPullRequestNumber::try_new("42".to_string()).expect("number"),
101            )
102            .is_err()
103        );
104
105        assert!(
106            GithubPullRequestRef::try_new(
107                GithubRepoRemote::try_new(
108                    "git@github.com:agentics-reifying/agentics-challenges.git",
109                )
110                .expect("repo"),
111                GithubPullRequestUrl::try_new(
112                    "https://github.com/agentics-reifying/agentics-challenges/pull/43",
113                )
114                .expect("pr"),
115                GithubPullRequestNumber::try_new("42".to_string()).expect("number"),
116            )
117            .is_err()
118        );
119    }
120}