use async_trait::async_trait;
use crate::{GitLabClient, GitLabError, models};
use lmrc_ports::{
CiVariable, CiVariableRequest, GitProvider, PipelineRun, PortError, PortResult, Repository,
};
pub struct GitLabAdapter {
client: GitLabClient,
url: String,
}
impl GitLabAdapter {
pub fn new(url: &str, token: &str) -> PortResult<Self> {
let client = GitLabClient::new(url, token)
.map_err(|e| PortError::InvalidConfiguration(format!("Failed to create GitLab client: {}", e)))?;
Ok(Self {
client,
url: url.to_string(),
})
}
pub fn from_env() -> PortResult<Self> {
let url = std::env::var("GITLAB_URL").unwrap_or_else(|_| "https://gitlab.com".to_string());
let token = std::env::var("GITLAB_TOKEN").map_err(|_| {
PortError::InvalidConfiguration(
"GITLAB_TOKEN environment variable is required".to_string(),
)
})?;
Self::new(&url, &token)
}
}
fn convert_error(err: GitLabError) -> PortError {
match err {
GitLabError::Authentication(msg) => {
PortError::InvalidConfiguration(format!("GitLab authentication failed: {}", msg))
}
GitLabError::NotFound { resource, id } => PortError::NotFound {
resource_type: resource,
resource_id: id,
},
GitLabError::Api(msg) => PortError::OperationFailed(format!("GitLab API error: {}", msg)),
GitLabError::Http(e) => PortError::NetworkError(format!("HTTP error: {}", e)),
GitLabError::Serialization(e) => {
PortError::OperationFailed(format!("Serialization error: {}", e))
}
GitLabError::RateLimit { retry_after } => PortError::OperationFailed(format!(
"Rate limit exceeded, retry after: {:?} seconds",
retry_after
)),
GitLabError::PermissionDenied(msg) => {
PortError::OperationFailed(format!("Permission denied: {}", msg))
}
GitLabError::InvalidInput { field, message } => {
PortError::InvalidConfiguration(format!("Invalid input for {}: {}", field, message))
}
GitLabError::Timeout { seconds } => {
PortError::OperationFailed(format!("Request timed out after {} seconds", seconds))
}
GitLabError::Conflict(msg) => PortError::AlreadyExists {
resource_type: "Resource".to_string(),
resource_id: msg,
},
GitLabError::ServerError(msg) => {
PortError::OperationFailed(format!("GitLab server error: {}", msg))
}
GitLabError::Unexpected(msg) => PortError::OperationFailed(format!("Unexpected error: {}", msg)),
GitLabError::Config(msg) => PortError::InvalidConfiguration(msg),
}
}
fn convert_to_port_variable(var: models::Variable) -> CiVariable {
CiVariable {
key: var.key,
value: var.value,
protected: var.protected,
masked: var.masked,
}
}
fn convert_variable_request(request: &CiVariableRequest) -> models::VariableOptions {
models::VariableOptions::new()
.protected(request.protected)
.masked(request.masked)
}
fn convert_pipeline_status(status: models::PipelineStatus) -> lmrc_ports::PipelineStatus {
use lmrc_ports::PipelineStatus;
match status {
models::PipelineStatus::Pending => PipelineStatus::Pending,
models::PipelineStatus::Running => PipelineStatus::Running,
models::PipelineStatus::Success => PipelineStatus::Success,
models::PipelineStatus::Failed => PipelineStatus::Failed,
models::PipelineStatus::Canceled => PipelineStatus::Canceled,
models::PipelineStatus::Skipped => PipelineStatus::Skipped,
_ => PipelineStatus::Failed, }
}
#[async_trait]
impl GitProvider for GitLabAdapter {
async fn get_repository(&self, project_id: &str) -> PortResult<Repository> {
Ok(Repository {
id: project_id.to_string(),
name: project_id.split('/').next_back().unwrap_or(project_id).to_string(),
url: format!("{}/{}", self.url, project_id),
ssh_url: format!("git@{}:{}.git",
self.url.trim_start_matches("https://"),
project_id
),
default_branch: "main".to_string(), })
}
async fn create_ci_variable(
&self,
project_id: &str,
request: CiVariableRequest,
) -> PortResult<CiVariable> {
let opts = convert_variable_request(&request);
let var = self
.client
.variables(project_id)
.create(&request.key, &request.value, opts)
.await
.map_err(convert_error)?;
Ok(convert_to_port_variable(var))
}
async fn update_ci_variable(
&self,
project_id: &str,
key: &str,
request: CiVariableRequest,
) -> PortResult<CiVariable> {
let opts = convert_variable_request(&request);
let var = self
.client
.variables(project_id)
.update(key, &request.value, opts)
.await
.map_err(convert_error)?;
Ok(convert_to_port_variable(var))
}
async fn list_ci_variables(&self, project_id: &str) -> PortResult<Vec<CiVariable>> {
let vars = self
.client
.variables(project_id)
.list()
.await
.map_err(convert_error)?;
Ok(vars.into_iter().map(convert_to_port_variable).collect())
}
async fn delete_ci_variable(&self, project_id: &str, key: &str) -> PortResult<()> {
self.client
.variables(project_id)
.delete(key)
.await
.map_err(convert_error)?;
Ok(())
}
async fn trigger_pipeline(
&self,
project_id: &str,
reference: &str,
) -> PortResult<PipelineRun> {
let pipeline = self
.client
.project(project_id)
.create_pipeline()
.ref_name(reference)
.trigger()
.await
.map_err(convert_error)?;
Ok(PipelineRun {
id: pipeline.id.to_string(),
status: convert_pipeline_status(pipeline.status),
reference: pipeline.ref_name,
web_url: pipeline.web_url,
})
}
async fn get_pipeline(
&self,
project_id: &str,
pipeline_id: &str,
) -> PortResult<PipelineRun> {
let pipeline_id_num = pipeline_id.parse::<u64>().map_err(|_| {
PortError::InvalidConfiguration(format!("Invalid pipeline ID: {}", pipeline_id))
})?;
let pipeline = self
.client
.pipeline(project_id, pipeline_id_num)
.get()
.await
.map_err(convert_error)?;
Ok(PipelineRun {
id: pipeline.id.to_string(),
status: convert_pipeline_status(pipeline.status),
reference: pipeline.ref_name,
web_url: pipeline.web_url,
})
}
}