use serde::{Deserialize, Serialize};
pub const CLAIM_PATH: &str = "/v1/jobs/claim";
pub const JOBS_PATH: &str = "/v1/jobs";
pub fn result_path(id: &str) -> String {
format!("{JOBS_PATH}/{id}/result")
}
pub fn retry_path(id: &str) -> String {
format!("{JOBS_PATH}/{id}/retry")
}
pub const KIND_MERGE: &str = "merge";
pub const KIND_EVALUATE: &str = "evaluate";
pub const STATE_QUEUED: &str = "queued";
pub const STATE_LEASED: &str = "leased";
pub const STATE_DONE: &str = "done";
pub const STATE_FAILED: &str = "failed";
pub const STATES: [&str; 4] = [STATE_QUEUED, STATE_LEASED, STATE_DONE, STATE_FAILED];
pub const MAX_WAIT_SECONDS: u64 = 30;
pub const MIN_LEASE_SECONDS: u64 = 30;
pub const MAX_LEASE_SECONDS: u64 = 600;
fn default_lease_seconds() -> u64 {
120
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaimRequest {
pub kinds: Vec<String>,
#[serde(default)]
pub wait_seconds: u64,
#[serde(default = "default_lease_seconds")]
pub lease_seconds: u64,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub claude_cli: Option<ClaudeCliReport>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaudeCliReport {
pub checked_at: String,
pub available: bool,
pub logged_in: bool,
#[serde(default)]
pub error: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ClaimResponse {
pub job: Option<Job>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct Job {
pub id: String,
pub kind: String,
pub lease_id: String,
pub lease_expires_at: String,
pub attempt: u32,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merge: Option<MergeInput>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluate: Option<EvaluateInput>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeInput {
pub project_key: String,
pub file_path: String,
pub stored: MergeSide,
pub incoming: MergeSide,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeSide {
pub sha256: String,
pub content: String,
pub source_env: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvaluateInput {
pub evaluation_id: String,
pub projects: Vec<String>,
pub contradictions: bool,
#[serde(default)]
pub files: Vec<EvaluateFile>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvaluateFile {
pub project_key: String,
pub file_path: String,
pub content: String,
pub updated_at: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvaluateResult {
pub findings: Vec<crate::evaluations::Finding>,
pub details: serde_json::Value,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResultRequest {
pub lease_id: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub merge: Option<MergeResult>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evaluate: Option<EvaluateResult>,
}
impl ResultRequest {
pub fn members(&self) -> usize {
[
self.merge.is_some(),
self.evaluate.is_some(),
self.error.is_some(),
]
.into_iter()
.filter(|m| *m)
.count()
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct MergeResult {
pub content: String,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ResultResponse {
pub id: String,
pub state: String,
pub applied: bool,
pub follow_up: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobSummary {
pub id: String,
pub kind: String,
pub state: String,
pub project_key: String,
pub file_path: String,
pub attempt: u32,
pub created_at: String,
pub updated_at: String,
pub error: Option<String>,
pub follow_up: Option<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct JobList {
pub jobs: Vec<JobSummary>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_claim_defaults_to_no_wait_and_a_two_minute_lease() {
let claim: ClaimRequest = serde_json::from_str(r#"{"kinds":["merge"]}"#).unwrap();
assert_eq!((claim.wait_seconds, claim.lease_seconds), (0, 120));
assert_eq!(claim.claude_cli, None);
}
#[test]
fn no_job_is_null() {
assert_eq!(
serde_json::to_string(&ClaimResponse::default()).unwrap(),
r#"{"job":null}"#
);
}
#[test]
fn a_result_sends_only_the_member_it_has() {
let ok = ResultRequest {
lease_id: "lse_a".into(),
merge: Some(MergeResult {
content: "merged".into(),
}),
error: None,
evaluate: None,
};
assert_eq!(
serde_json::to_string(&ok).unwrap(),
r#"{"lease_id":"lse_a","merge":{"content":"merged"}}"#
);
let failed = ResultRequest {
lease_id: "lse_a".into(),
merge: None,
error: Some("claude merge timed out after 45s".into()),
evaluate: None,
};
assert_eq!(
serde_json::to_string(&failed).unwrap(),
r#"{"lease_id":"lse_a","error":"claude merge timed out after 45s"}"#
);
}
#[test]
fn paths() {
assert_eq!(result_path("job_a"), "/v1/jobs/job_a/result");
assert_eq!(retry_path("job_a"), "/v1/jobs/job_a/retry");
}
}