use axum::{
Json, Router,
extract::{Path as AxumPath, Query, State},
http::HeaderMap,
response::{IntoResponse, Response},
routing::{get, post},
};
use serde::Deserialize;
use crate::{
api::{ApiError, InterfaceKind, RequestContext},
domain::{
CodeFeatureFlagRequest, CodeImpactRequest, CodeIndexMode, CodeIndexRequest,
CodeRepositorySelector, CodeRetrievalRequest, SoftwareGlobalRequest,
},
};
use super::{WebState, api_error_response};
pub(super) fn routes() -> Router<WebState> {
Router::new()
.route(
"/api/v1/code/repositories/{alias}/index",
post(code_repository_index),
)
.route(
"/api/v1/code/repositories/{alias}/scope/preview",
post(code_repository_scope_preview),
)
.route(
"/api/v1/code/repositories/{alias}/query",
post(code_repository_query),
)
.route(
"/api/v1/code/repositories/{alias}/feature-flags",
post(code_repository_feature_flags),
)
.route(
"/api/v1/code/repositories/{alias}/impact",
post(code_repository_impact),
)
.route(
"/api/v1/code/repositories/{alias}/report",
get(code_repository_report),
)
.route(
"/api/v1/code/repositories/{alias}/software",
post(code_repository_software),
)
.route(
"/api/v1/code/repositories/{alias}/status",
get(code_repository_status),
)
}
async fn code_repository_index(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
Json(mut request): Json<CodeIndexRequest>,
) -> Response {
if let Some(error) = normalize_selector(&mut request.repository) {
return api_error_response(error);
}
if let Some(error) = path_alias_error(&alias, &request.repository) {
return api_error_response(error);
}
if request.mode != CodeIndexMode::Full {
return api_error_response(ApiError::invalid_argument(
"remote code repository index API accepts only full index mode",
));
}
match state
.service
.start_code_repository_index(request, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_scope_preview(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
Json(mut request): Json<CodeIndexRequest>,
) -> Response {
if let Some(error) = normalize_selector(&mut request.repository) {
return api_error_response(error);
}
if let Some(error) = path_alias_error(&alias, &request.repository) {
return api_error_response(error);
}
match state
.service
.preview_code_repository_scope(request, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_query(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
Json(mut request): Json<CodeRetrievalRequest>,
) -> Response {
if let Some(error) = normalize_query_request(&mut request) {
return api_error_response(error);
}
if let Some(error) = path_alias_error(&alias, &request.repository) {
return api_error_response(error);
}
match state
.service
.query_code_repository(request, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_feature_flags(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
Json(mut request): Json<CodeFeatureFlagRequest>,
) -> Response {
if let Some(error) = normalize_feature_flag_request(&mut request) {
return api_error_response(error);
}
if let Some(error) = path_alias_error(&alias, &request.repository) {
return api_error_response(error);
}
match state
.service
.query_code_repository_feature_flags(request, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_impact(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
Json(mut request): Json<CodeImpactRequest>,
) -> Response {
if let Some(error) = normalize_impact_request(&mut request) {
return api_error_response(error);
}
if let Some(error) = path_alias_error(&alias, &request.repository) {
return api_error_response(error);
}
match state
.service
.impact_code_repository(request, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_report(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
) -> Response {
let selector = match CodeRepositorySelector::new(alias, "HEAD", Vec::new(), Vec::new()) {
Ok(selector) => selector,
Err(error) => return api_error_response(ApiError::invalid_argument(error.to_string())),
};
match state
.service
.code_repository_report(selector, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_software(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
headers: HeaderMap,
Json(mut request): Json<SoftwareGlobalRequest>,
) -> Response {
if let Some(error) = normalize_software_request(&mut request) {
return api_error_response(error);
}
if let Some(error) = path_alias_error(&alias, &request.repository) {
return api_error_response(error);
}
match state
.service
.software_global_projection(request, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
async fn code_repository_status(
State(state): State<WebState>,
AxumPath(alias): AxumPath<String>,
Query(query): Query<CodeRepositoryStatusQuery>,
headers: HeaderMap,
) -> Response {
let selector = match CodeRepositorySelector::new(
alias,
query.ref_selector.unwrap_or_else(|| "HEAD".to_owned()),
Vec::new(),
Vec::new(),
) {
Ok(selector) => selector,
Err(error) => return api_error_response(ApiError::invalid_argument(error.to_string())),
};
match state
.service
.code_repository_status(selector, api_context(&headers))
.await
{
Ok(response) => Json(response).into_response(),
Err(error) => api_error_response(error),
}
}
#[derive(Debug, Deserialize)]
struct CodeRepositoryStatusQuery {
#[serde(rename = "ref")]
ref_selector: Option<String>,
}
fn api_context(headers: &HeaderMap) -> RequestContext {
let generated = RequestContext::for_interface(InterfaceKind::Api);
RequestContext::with_ids(
InterfaceKind::Api,
header_text(headers, "x-relay-request-id").unwrap_or(generated.request_id),
header_text(headers, "x-relay-trace-id").unwrap_or(generated.trace_id),
)
}
fn header_text(headers: &HeaderMap, name: &'static str) -> Option<String> {
headers
.get(name)
.and_then(|value| value.to_str().ok())
.map(str::trim)
.filter(|value| !value.is_empty())
.map(ToOwned::to_owned)
}
fn normalize_query_request(request: &mut CodeRetrievalRequest) -> Option<ApiError> {
if let Some(error) = normalize_selector(&mut request.repository) {
return Some(error);
}
let exclude_generated = request.exclude_generated;
match CodeRetrievalRequest::new(
std::mem::take(&mut request.query),
request.repository.clone(),
request.code_query_kind,
request.limit,
request.freshness_policy,
) {
Ok(mut validated) => {
validated.exclude_generated = exclude_generated;
*request = validated;
None
}
Err(error) => Some(ApiError::invalid_argument(error.to_string())),
}
}
fn normalize_feature_flag_request(request: &mut CodeFeatureFlagRequest) -> Option<ApiError> {
if let Some(error) = normalize_selector(&mut request.repository) {
return Some(error);
}
match CodeFeatureFlagRequest::new(
request.query.take(),
request.repository.clone(),
request.limit,
request.freshness_policy,
) {
Ok(validated) => {
*request = validated;
None
}
Err(error) => Some(ApiError::invalid_argument(error.to_string())),
}
}
fn normalize_impact_request(request: &mut CodeImpactRequest) -> Option<ApiError> {
if let Some(error) = normalize_selector(&mut request.repository) {
return Some(error);
}
match CodeImpactRequest::new(
request.repository.clone(),
std::mem::take(&mut request.base_ref),
std::mem::take(&mut request.head_ref),
request.limit,
) {
Ok(validated) => {
*request = validated;
None
}
Err(error) => Some(ApiError::invalid_argument(error.to_string())),
}
}
fn normalize_software_request(request: &mut SoftwareGlobalRequest) -> Option<ApiError> {
if let Some(error) = normalize_selector(&mut request.repository) {
return Some(error);
}
match SoftwareGlobalRequest::new(
request.repository.clone(),
request.kind,
request.freshness_policy,
request.limit,
) {
Ok(validated) => {
*request = validated;
None
}
Err(error) => Some(ApiError::invalid_argument(error.to_string())),
}
}
fn normalize_selector(selector: &mut CodeRepositorySelector) -> Option<ApiError> {
match CodeRepositorySelector::new(
std::mem::take(&mut selector.repository),
std::mem::take(&mut selector.ref_selector),
std::mem::take(&mut selector.path_filters),
std::mem::take(&mut selector.language_filters),
) {
Ok(validated) => {
*selector = validated;
None
}
Err(error) => Some(ApiError::invalid_argument(error.to_string())),
}
}
fn path_alias_error(path_alias: &str, selector: &CodeRepositorySelector) -> Option<ApiError> {
if selector.repository == path_alias {
return None;
}
Some(ApiError::invalid_argument(format!(
"path alias '{path_alias}' must match request repository '{}'",
selector.repository
)))
}