use std::collections::BTreeSet;
use crate::WyrdClient;
use wyrd_spec::error::WyrdError;
use wyrd_spec::ids::CardUid;
use wyrd_spec::registry::DeclareCardResponse;
use crate::cards::error::RegistryEngineError;
pub(crate) async fn complete_uploaded_cards(
client: &WyrdClient,
response: &DeclareCardResponse,
idempotency_key: &str,
) -> Result<DeclareCardResponse, RegistryEngineError> {
let card_uids = collect_uploaded_card_uids(response)?;
let mut completed = response.clone();
for uid in card_uids {
let outcome = complete_uploaded_card(client, &uid, idempotency_key).await?;
validate_completion_response(response, &uid, &outcome)?;
merge_completed_outcome(&mut completed, outcome)?;
}
Ok(completed)
}
fn collect_uploaded_card_uids(
response: &DeclareCardResponse,
) -> Result<Vec<CardUid>, RegistryEngineError> {
let mut seen = BTreeSet::new();
let mut uids = Vec::with_capacity(response.upload_plans.len());
for plan in &response.upload_plans {
let uid = plan
.card_ref
.uid
.clone()
.ok_or_else(|| WyrdError::RegistryInvalidCardSpec {
message: "server upload plan card reference has no Card UID".to_owned(),
details: serde_json::json!({}),
})?;
if seen.insert(uid.clone()) {
uids.push(uid);
}
}
Ok(uids)
}
async fn complete_uploaded_card(
client: &WyrdClient,
uid: &CardUid,
idempotency_key: &str,
) -> Result<DeclareCardResponse, RegistryEngineError> {
let path = format!("/v1/cards/{uid}/complete");
client
.http
.submit_with_idempotency_key(reqwest::Method::POST, &path, &(), idempotency_key)
.await
.map_err(Into::into)
}
fn validate_completion_response(
original: &DeclareCardResponse,
uid: &CardUid,
completed: &DeclareCardResponse,
) -> Result<(), RegistryEngineError> {
let matching = completed
.outcomes
.iter()
.filter(|outcome| outcome.card_ref.uid.as_ref() == Some(uid))
.count();
if matching != 1 || completed.outcomes.len() != 1 {
return Err(WyrdError::RegistryArtifactVerifyFailed {
message: "card completion response identity did not match the requested Card"
.to_owned(),
details: serde_json::json!({
"card_uid": uid,
"outcome_count": completed.outcomes.len(),
"matching_count": matching,
"registered_outcomes": original.outcomes.len(),
}),
}
.into());
}
Ok(())
}
fn merge_completed_outcome(
response: &mut DeclareCardResponse,
completed: DeclareCardResponse,
) -> Result<(), RegistryEngineError> {
let outcome = completed.outcomes.into_iter().next().ok_or_else(|| {
WyrdError::RegistryArtifactVerifyFailed {
message: "card completion response did not contain an outcome".to_owned(),
details: serde_json::json!({}),
}
})?;
let existing = response
.outcomes
.iter_mut()
.find(|candidate| candidate.card_ref.same_identity(&outcome.card_ref))
.ok_or_else(|| WyrdError::RegistryArtifactVerifyFailed {
message: "card completion response returned an unrelated outcome".to_owned(),
details: serde_json::json!({ "card_ref": outcome.card_ref }),
})?;
*existing = outcome;
Ok(())
}
#[cfg(test)]
mod tests {
use super::{merge_completed_outcome, validate_completion_response};
use wyrd_semver::VersionBlock;
use wyrd_spec::envelope::CardKind;
use wyrd_spec::error::WyrdError;
use wyrd_spec::ids::{CardName, CardUid, SpaceName};
use wyrd_spec::reference::CardRef;
use wyrd_spec::registry::DeclareCardResponse;
use wyrd_spec::registry::{
CardDeclarationOutcome, CardLifecycleStatus, DeclarationOutcomeKind,
};
#[test]
fn merge_preserves_composite_root_and_outcome_order() {
let first_uid = CardUid::new("018f0000-0000-7000-8000-000000000001").expect("test UID");
let second_uid = CardUid::new("018f0000-0000-7000-8000-000000000002").expect("test UID");
let card_ref = |uid: CardUid, name: &str| CardRef {
kind: CardKind::Prompt,
name: CardName::new(name).expect("test name"),
version: VersionBlock::parse("1.0.0").expect("test version"),
space: Some(SpaceName::new("default").expect("test space")),
uid: Some(uid),
};
let first = CardDeclarationOutcome {
card_ref: card_ref(first_uid, "first"),
spec_hash: "first".to_owned(),
artifact_hash: None,
status: CardLifecycleStatus::Pending,
outcome: DeclarationOutcomeKind::Declared,
card_blob_uri: None,
};
let second = CardDeclarationOutcome {
card_ref: card_ref(second_uid, "second"),
spec_hash: "second".to_owned(),
artifact_hash: None,
status: CardLifecycleStatus::Pending,
outcome: DeclarationOutcomeKind::Declared,
card_blob_uri: None,
};
let mut response = DeclareCardResponse {
root: first.card_ref.clone(),
outcomes: vec![first, second.clone()],
upload_plans: Vec::new(),
};
let completed = DeclareCardResponse {
root: second.card_ref.clone(),
outcomes: vec![CardDeclarationOutcome {
status: CardLifecycleStatus::Active,
..second
}],
upload_plans: Vec::new(),
};
merge_completed_outcome(&mut response, completed).expect("matching outcome merges");
assert_eq!(response.root, response.outcomes[0].card_ref);
assert_eq!(response.outcomes[0].card_ref.name.as_str(), "first");
assert_eq!(response.outcomes[1].status, CardLifecycleStatus::Active);
}
#[test]
fn rejects_missing_completion_outcome() {
let uid = CardUid::new("018f0000-0000-7000-8000-000000000001").expect("test UID");
let card_ref = CardRef {
kind: CardKind::Prompt,
name: CardName::new("card-1").expect("test name"),
version: VersionBlock::parse("1.0.0").expect("test version"),
space: Some(SpaceName::new("default").expect("test space")),
uid: Some(uid.clone()),
};
let response = DeclareCardResponse {
root: card_ref,
outcomes: Vec::new(),
upload_plans: Vec::new(),
};
let error: WyrdError = validate_completion_response(&response, &uid, &response)
.expect_err("missing outcome is rejected")
.into();
assert_eq!(error.code(), "WYRD_REGISTRY_507_ARTIFACT_VERIFY_FAILED");
}
#[test]
fn rejects_duplicate_or_unrelated_completion_outcomes() {
let requested = CardUid::new("018f0000-0000-7000-8000-000000000001").expect("test UID");
let unrelated = CardUid::new("018f0000-0000-7000-8000-000000000002").expect("test UID");
let response = |uid: CardUid, name: &str| CardDeclarationOutcome {
card_ref: CardRef {
kind: CardKind::Prompt,
name: CardName::new(name).expect("test name"),
version: VersionBlock::parse("1.0.0").expect("test version"),
space: Some(SpaceName::new("default").expect("test space")),
uid: Some(uid),
},
spec_hash: "sha256:spec".to_owned(),
artifact_hash: None,
status: CardLifecycleStatus::Active,
outcome: DeclarationOutcomeKind::Declared,
card_blob_uri: Some("wyrd://blob".parse().expect("test blob URI")),
};
let duplicate = DeclareCardResponse {
root: response(requested.clone(), "requested").card_ref,
outcomes: vec![
response(requested.clone(), "requested"),
response(requested.clone(), "requested"),
],
upload_plans: Vec::new(),
};
assert!(validate_completion_response(&duplicate, &requested, &duplicate).is_err());
let unrelated_response = DeclareCardResponse {
root: response(unrelated.clone(), "unrelated").card_ref,
outcomes: vec![response(unrelated, "unrelated")],
upload_plans: Vec::new(),
};
assert!(validate_completion_response(&duplicate, &requested, &unrelated_response).is_err());
}
}