use anyhow::Result;
use axum::http::{HeaderMap, StatusCode};
use bytes::Bytes;
use std::collections::HashMap;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub struct ForgeMetadata {
pub repo_url: Option<String>,
pub base_sha: String,
pub head_sha: String,
pub pr_number: i64,
pub pr_title: Option<String>,
pub pr_url: Option<String>,
}
pub trait ForgeProvider: Send + Sync {
fn name(&self) -> &str;
fn validate_event(&self, headers: &HeaderMap) -> Result<(), StatusCode>;
fn parse_payload(&self, body: &Bytes) -> Result<(String, ForgeMetadata), StatusCode>;
}
pub struct GitHubForge;
impl ForgeProvider for GitHubForge {
fn name(&self) -> &str {
"GitHub"
}
fn validate_event(&self, headers: &HeaderMap) -> Result<(), StatusCode> {
let event = headers
.get("x-github-event")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::BAD_REQUEST)?;
if event != "pull_request" {
return Err(StatusCode::BAD_REQUEST);
}
Ok(())
}
fn parse_payload(&self, body: &Bytes) -> Result<(String, ForgeMetadata), StatusCode> {
use serde_json::Value;
let payload: Value = serde_json::from_slice(body).map_err(|_| StatusCode::BAD_REQUEST)?;
let action = payload["action"]
.as_str()
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
let pr = &payload["pull_request"];
if pr.is_null() {
return Err(StatusCode::BAD_REQUEST);
}
let head_sha = pr["head"]["sha"]
.as_str()
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
let base_sha = pr["base"]["sha"]
.as_str()
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
let pr_number = pr["number"].as_i64().ok_or(StatusCode::BAD_REQUEST)?;
let pr_title = pr["title"].as_str().map(|s| s.to_string());
let pr_url = pr["html_url"].as_str().map(|s| s.to_string());
let repo_url = payload["repository"]["clone_url"]
.as_str()
.map(|s| s.to_string());
let metadata = ForgeMetadata {
repo_url,
base_sha,
head_sha,
pr_number,
pr_title,
pr_url,
};
Ok((action, metadata))
}
}
pub struct GitLabForge;
impl ForgeProvider for GitLabForge {
fn name(&self) -> &str {
"GitLab"
}
fn validate_event(&self, headers: &HeaderMap) -> Result<(), StatusCode> {
let event = headers
.get("x-gitlab-event")
.and_then(|v| v.to_str().ok())
.ok_or(StatusCode::BAD_REQUEST)?;
if event != "Merge Request Hook" {
return Err(StatusCode::BAD_REQUEST);
}
Ok(())
}
fn parse_payload(&self, body: &Bytes) -> Result<(String, ForgeMetadata), StatusCode> {
use serde_json::Value;
let payload: Value = serde_json::from_slice(body).map_err(|_| StatusCode::BAD_REQUEST)?;
let action = payload["object_kind"]
.as_str()
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
let attrs = &payload["object_attributes"];
if attrs.is_null() {
return Err(StatusCode::BAD_REQUEST);
}
let head_sha = attrs["last_commit"]["id"]
.as_str()
.ok_or(StatusCode::BAD_REQUEST)?
.to_string();
let base_sha = attrs["diff_refs"]["base_sha"]
.as_str()
.map(|s| s.to_string())
.unwrap_or_else(|| head_sha.clone());
let pr_number = attrs["iid"].as_i64().ok_or(StatusCode::BAD_REQUEST)?;
let pr_title = attrs["title"].as_str().map(|s| s.to_string());
let pr_url = attrs["url"].as_str().map(|s| s.to_string());
let repo_url = payload["project"]["git_http_url"]
.as_str()
.map(|s| s.to_string());
let metadata = ForgeMetadata {
repo_url,
base_sha,
head_sha,
pr_number,
pr_title,
pr_url,
};
Ok((action, metadata))
}
}
pub fn extract_repo_name_from_url(url: &str) -> String {
url.trim_end_matches('/')
.split('/')
.next_back()
.map(|s| s.trim_end_matches(".git"))
.unwrap_or("repo")
.to_string()
}
pub fn extract_repo_name_from_mr_url(url: &str) -> Option<String> {
if let Some(before_sep) = url.split("/-/").next() {
let name = before_sep
.trim_end_matches('/')
.split('/')
.next_back()?
.to_string();
Some(name)
} else {
None
}
}
pub struct ForgeRegistry {
providers: HashMap<String, Arc<dyn ForgeProvider>>,
}
impl ForgeRegistry {
pub fn new() -> Self {
let mut registry = Self {
providers: HashMap::new(),
};
registry.register("github", Arc::new(GitHubForge));
registry.register("gitlab", Arc::new(GitLabForge));
registry
}
pub fn register(&mut self, name: &str, provider: Arc<dyn ForgeProvider>) {
self.providers.insert(name.to_string(), provider);
}
pub fn get(&self, name: &str) -> Option<Arc<dyn ForgeProvider>> {
self.providers.get(name).cloned()
}
pub fn list_providers(&self) -> Vec<String> {
self.providers.keys().cloned().collect()
}
}
impl Default for ForgeRegistry {
fn default() -> Self {
Self::new()
}
}