use serde::{Deserialize, Serialize};
use super::{
DomainError,
code_repository::{CodeQueryKind, CodeRetrievalHit},
error::required_text,
retrieval::FreshnessPolicy,
};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetCreateRequest {
pub alias: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub default_ref_policy_json: String,
}
impl CodeRepositorySetCreateRequest {
pub fn new(
alias: impl Into<String>,
description: Option<String>,
default_ref_policy_json: Option<String>,
) -> Result<Self, DomainError> {
Ok(Self {
alias: required_text("set_alias", alias)?,
description: optional_text("description", description)?,
default_ref_policy_json: default_ref_policy_json
.filter(|value| !value.trim().is_empty())
.unwrap_or_else(|| "{\"default_ref\":\"HEAD\"}".to_owned()),
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySet {
pub set_id: String,
pub alias: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub default_ref_policy_json: String,
pub created_at_ms: u64,
pub updated_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetAddMemberRequest {
pub set_alias: String,
pub repository_alias: String,
pub ref_selector: String,
pub path_filters: Vec<String>,
pub language_filters: Vec<String>,
pub priority: i32,
}
impl CodeRepositorySetAddMemberRequest {
pub fn new(
set_alias: impl Into<String>,
repository_alias: impl Into<String>,
ref_selector: impl Into<String>,
path_filters: Vec<String>,
language_filters: Vec<String>,
priority: i32,
) -> Result<Self, DomainError> {
Ok(Self {
set_alias: required_text("set_alias", set_alias)?,
repository_alias: required_text("repository_alias", repository_alias)?,
ref_selector: required_text("ref_selector", ref_selector)?,
path_filters: normalize_filter_list("path_filter", path_filters)?,
language_filters: normalize_filter_list("language_filter", language_filters)?,
priority,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetRemoveMemberRequest {
pub set_alias: String,
pub repository_alias: String,
}
impl CodeRepositorySetRemoveMemberRequest {
pub fn new(
set_alias: impl Into<String>,
repository_alias: impl Into<String>,
) -> Result<Self, DomainError> {
Ok(Self {
set_alias: required_text("set_alias", set_alias)?,
repository_alias: required_text("repository_alias", repository_alias)?,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetMember {
pub set_id: String,
pub repository_id: String,
pub repository_alias: String,
pub ref_selector: String,
pub resolved_commit_sha: String,
pub source_scope: String,
pub path_filters: Vec<String>,
pub language_filters: Vec<String>,
pub priority: i32,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetMemberStatus {
pub member: CodeRepositorySetMember,
pub tree_hash: String,
#[serde(default, skip_serializing)]
pub indexed_path_filters: Vec<String>,
#[serde(default, skip_serializing)]
pub indexed_language_filters: Vec<String>,
pub freshness_state: String,
pub stale: bool,
pub indexed_file_count: usize,
pub symbol_count: usize,
pub reference_count: usize,
pub chunk_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub degraded_reason: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetOverlayStatus {
pub state: String,
pub stale: bool,
pub edge_count: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub refreshed_at_ms: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub degraded_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetStatus {
pub repository_set: CodeRepositorySet,
pub members: Vec<CodeRepositorySetMemberStatus>,
pub overlay: CodeRepositorySetOverlayStatus,
pub freshness_state: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub degraded_reason: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetQueryRequest {
pub set_alias: String,
pub query: String,
pub code_query_kind: CodeQueryKind,
pub limit: usize,
pub freshness_policy: FreshnessPolicy,
pub path_filters: Vec<String>,
pub language_filters: Vec<String>,
#[serde(default)]
pub exclude_generated: bool,
}
impl CodeRepositorySetQueryRequest {
pub fn new(
set_alias: impl Into<String>,
query: impl Into<String>,
code_query_kind: CodeQueryKind,
limit: usize,
freshness_policy: FreshnessPolicy,
path_filters: Vec<String>,
language_filters: Vec<String>,
) -> Result<Self, DomainError> {
let limit = match limit {
1..=50 => limit,
0 => return Err(DomainError::invalid("limit", "must be greater than zero")),
_ => return Err(DomainError::invalid("limit", "must be 50 or less")),
};
Ok(Self {
set_alias: required_text("set_alias", set_alias)?,
query: required_text("query", query)?,
code_query_kind,
limit,
freshness_policy,
path_filters: normalize_filter_list("path_filter", path_filters)?,
language_filters: normalize_filter_list("language_filter", language_filters)?,
exclude_generated: false,
})
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositoryCrossEdge {
pub edge_id: String,
pub set_id: String,
pub from_source_scope: String,
pub from_repository_id: String,
pub from_record_kind: String,
pub from_record_id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_source_scope: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_repository_id: Option<String>,
pub to_record_kind: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub to_record_id: Option<String>,
pub edge_kind: String,
pub resolution_state: String,
pub confidence_basis_points: u16,
pub confidence_tier: String,
pub evidence_json: String,
pub created_at_ms: u64,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct CodeRepositorySetQueryHit {
pub member: CodeRepositorySetMember,
pub hit: CodeRetrievalHit,
pub overlay_evidence: Vec<CodeRepositoryCrossEdge>,
pub score: f64,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetRefreshSummary {
pub set_id: String,
pub alias: String,
pub edge_count: usize,
pub resolved_edge_count: usize,
pub ambiguous_edge_count: usize,
pub unresolved_edge_count: usize,
pub refreshed_at_ms: u64,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum CodeRepositorySetRefreshTaskState {
Queued,
Running,
Succeeded,
Retrying,
DeadLetter,
}
impl CodeRepositorySetRefreshTaskState {
pub const fn as_str(self) -> &'static str {
match self {
Self::Queued => "queued",
Self::Running => "running",
Self::Succeeded => "succeeded",
Self::Retrying => "retrying",
Self::DeadLetter => "dead_letter",
}
}
pub fn parse(value: &str) -> Result<Self, DomainError> {
match value {
"queued" => Ok(Self::Queued),
"running" => Ok(Self::Running),
"succeeded" => Ok(Self::Succeeded),
"retrying" => Ok(Self::Retrying),
"dead_letter" => Ok(Self::DeadLetter),
_ => Err(DomainError::invalid(
"repository_set_refresh_task_state",
"unknown repository set refresh task state",
)),
}
}
pub const fn is_unfinished(self) -> bool {
matches!(self, Self::Queued | Self::Running | Self::Retrying)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct CodeRepositorySetRefreshTaskRecord {
pub task_id: String,
pub set_id: String,
pub set_alias: String,
pub state: CodeRepositorySetRefreshTaskState,
#[serde(skip_serializing_if = "Option::is_none")]
pub lease_owner: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lease_expires_at_ms: Option<u64>,
pub attempt_count: u32,
pub next_retry_at_ms: u64,
pub input_fingerprint: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_error_kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub last_error_message: Option<String>,
pub created_at_ms: u64,
pub updated_at_ms: u64,
}
fn optional_text(
field: &'static str,
value: Option<String>,
) -> Result<Option<String>, DomainError> {
value.map(|value| required_text(field, value)).transpose()
}
fn normalize_filter_list(
field: &'static str,
values: Vec<String>,
) -> Result<Vec<String>, DomainError> {
let mut normalized = Vec::new();
for value in values {
let value = required_text(field, value)?;
if !normalized.contains(&value) {
normalized.push(value);
}
}
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn repository_set_requests_normalize_inputs_and_defaults() {
let create = CodeRepositorySetCreateRequest::new(
" workspace ",
Some(" grouped repos ".to_owned()),
None,
)
.expect("create request should validate");
assert_eq!(create.alias, "workspace");
assert_eq!(create.description.as_deref(), Some("grouped repos"));
assert_eq!(create.default_ref_policy_json, "{\"default_ref\":\"HEAD\"}");
let add = CodeRepositorySetAddMemberRequest::new(
"workspace",
"core",
"HEAD",
vec![" src ".to_owned(), "src".to_owned()],
vec![" rust ".to_owned(), "rust".to_owned()],
10,
)
.expect("member request should validate");
assert_eq!(add.path_filters, ["src"]);
assert_eq!(add.language_filters, ["rust"]);
let remove = CodeRepositorySetRemoveMemberRequest::new(" workspace ", " core ")
.expect("remove request should validate");
assert_eq!(remove.set_alias, "workspace");
assert_eq!(remove.repository_alias, "core");
let query = CodeRepositorySetQueryRequest::new(
"workspace",
"RetryPolicy",
CodeQueryKind::Definition,
50,
FreshnessPolicy::WaitUntilFresh,
vec!["src".to_owned(), "src".to_owned()],
Vec::new(),
)
.expect("query request should validate");
assert_eq!(query.limit, 50);
assert_eq!(query.path_filters, ["src"]);
assert_eq!(query.freshness_policy, FreshnessPolicy::WaitUntilFresh);
}
#[test]
fn repository_set_requests_reject_invalid_boundaries() {
assert!(
CodeRepositorySetCreateRequest::new(" ", None, None)
.expect_err("blank alias should fail")
.to_string()
.contains("set_alias")
);
assert!(
CodeRepositorySetAddMemberRequest::new(
"workspace",
"core",
" ",
Vec::new(),
Vec::new(),
0
)
.expect_err("blank ref should fail")
.to_string()
.contains("ref_selector")
);
assert!(
CodeRepositorySetQueryRequest::new(
"workspace",
"query",
CodeQueryKind::Hybrid,
0,
FreshnessPolicy::AllowStale,
Vec::new(),
Vec::new(),
)
.expect_err("zero limit should fail")
.to_string()
.contains("greater than zero")
);
assert!(
CodeRepositorySetQueryRequest::new(
"workspace",
"query",
CodeQueryKind::Hybrid,
51,
FreshnessPolicy::AllowStale,
Vec::new(),
Vec::new(),
)
.expect_err("oversized limit should fail")
.to_string()
.contains("50 or less")
);
assert!(
CodeRepositorySetRemoveMemberRequest::new("workspace", " ")
.expect_err("blank repository alias should fail")
.to_string()
.contains("repository_alias")
);
}
#[test]
fn repository_set_refresh_task_states_have_stable_wire_values() {
for (state, wire, unfinished) in [
(CodeRepositorySetRefreshTaskState::Queued, "queued", true),
(CodeRepositorySetRefreshTaskState::Running, "running", true),
(
CodeRepositorySetRefreshTaskState::Succeeded,
"succeeded",
false,
),
(
CodeRepositorySetRefreshTaskState::Retrying,
"retrying",
true,
),
(
CodeRepositorySetRefreshTaskState::DeadLetter,
"dead_letter",
false,
),
] {
assert_eq!(state.as_str(), wire);
assert_eq!(
CodeRepositorySetRefreshTaskState::parse(wire).expect("wire state should parse"),
state
);
assert_eq!(state.is_unfinished(), unfinished);
}
assert!(
CodeRepositorySetRefreshTaskState::parse("mystery")
.expect_err("unknown state should fail")
.to_string()
.contains("unknown repository set refresh task state")
);
}
}