use serde_json::Value;
use vta_sdk::client::VtaClient;
use vta_sdk::protocols::join_requests::JoinRequestAttribute;
use vta_sdk::protocols::join_requests::manifest::v0_2;
use crate::errors::OpenVTCError;
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Asked {
pub claim_type: String,
pub required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
}
#[must_use]
pub fn asked(manifest: &v0_2::Response) -> Vec<Asked> {
manifest
.requested_attributes
.iter()
.map(|r| Asked {
claim_type: r.type_.to_string(),
required: r.required,
purpose: r.purpose.as_ref().map(|p| p.to_string()),
})
.collect()
}
#[derive(Clone, Debug, PartialEq)]
pub struct AnswerPreview {
pub preview_id: String,
pub claims: Vec<(String, Option<Value>)>,
pub missing_required: Vec<String>,
}
pub async fn preview(
client: &VtaClient,
context_id: &str,
persona_did: &str,
community_did: &str,
asked: &[Asked],
) -> Result<AnswerPreview, OpenVTCError> {
let requested: Vec<String> = asked.iter().map(|a| a.claim_type.clone()).collect();
let response = client
.persona_disclosure_preview(
context_id,
persona_did,
community_did,
requested,
Some("joining the community"),
None,
)
.await
.map_err(|e| OpenVTCError::Vta(format!("could not preview the answers: {e}")))?;
preview_from_response(&response, asked)
}
pub fn preview_from_response(
response: &Value,
asked: &[Asked],
) -> Result<AnswerPreview, OpenVTCError> {
let preview_id = response
.get("previewId")
.and_then(Value::as_str)
.ok_or_else(|| OpenVTCError::Vta("the preview carried no previewId".into()))?
.to_string();
let claims: Vec<(String, Option<Value>)> = response
.get("claims")
.and_then(Value::as_array)
.map(|cs| {
cs.iter()
.filter_map(|c| {
let t = c.get("type")?.as_str()?.to_string();
Some((t, c.get("value").cloned()))
})
.collect()
})
.unwrap_or_default();
let missing_required = asked
.iter()
.filter(|a| a.required)
.filter(|a| {
!claims
.iter()
.any(|(t, v)| *t == a.claim_type && v.is_some())
})
.map(|a| a.claim_type.clone())
.collect();
Ok(AnswerPreview {
preview_id,
claims,
missing_required,
})
}
pub async fn present(
client: &VtaClient,
context_id: &str,
preview_id: &str,
) -> Result<Vec<JoinRequestAttribute>, OpenVTCError> {
let response = client
.persona_disclosure_present(context_id, preview_id, None, None)
.await
.map_err(|e| OpenVTCError::Vta(format!("could not release the answers: {e}")))?;
let artifact = response
.get("artifact")
.and_then(Value::as_str)
.ok_or_else(|| OpenVTCError::Vta("the disclosure carried no artifact".into()))?;
attributes_from_artifact(artifact)
}
pub fn attributes_from_artifact(artifact: &str) -> Result<Vec<JoinRequestAttribute>, OpenVTCError> {
let card: Value = serde_json::from_str(artifact)
.map_err(|e| OpenVTCError::Vta(format!("the disclosure artifact is not JSON: {e}")))?;
let Some(claims) = card.get("claims").and_then(Value::as_object) else {
return Ok(Vec::new());
};
let mut ordered: Vec<(&String, &Value)> = claims.iter().collect();
ordered.sort_by(|a, b| a.0.cmp(b.0));
Ok(ordered
.into_iter()
.filter_map(|(_, c)| {
Some(JoinRequestAttribute {
claim_type: c.get("type")?.as_str()?.to_string(),
value: c.get("value")?.clone(),
})
})
.collect())
}
#[must_use]
pub fn shown_by(
asked: &[Asked],
resolved: &[crate::persona::profile::ResolvedClaim],
) -> Vec<(String, Option<Value>)> {
asked
.iter()
.map(|a| {
let value = resolved
.iter()
.find(|c| c.claim_type == a.claim_type && !c.stale)
.and_then(|c| c.value.clone());
(a.claim_type.clone(), value)
})
.collect()
}
#[must_use]
pub fn unanswered_required(asked: &[Asked], shown: &[(String, Option<Value>)]) -> Vec<String> {
asked
.iter()
.filter(|a| a.required)
.filter(|a| !shown.iter().any(|(t, v)| *t == a.claim_type && v.is_some()))
.map(|a| a.claim_type.clone())
.collect()
}
#[must_use]
pub fn preview_matches(preview: &AnswerPreview, approved: &[(String, Value)]) -> bool {
let sent: Vec<(&str, &Value)> = preview
.claims
.iter()
.filter_map(|(t, v)| v.as_ref().map(|v| (t.as_str(), v)))
.collect();
sent.len() == approved.len()
&& approved
.iter()
.all(|(t, v)| sent.iter().any(|(st, sv)| *st == t && *sv == v))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn ask(t: &str, required: bool) -> Asked {
Asked {
claim_type: t.into(),
required,
purpose: None,
}
}
#[test]
fn a_required_type_the_face_does_not_show_is_named_before_anything_leaves() {
let asked = [ask("name.display", true), ask("address.country", false)];
let p = preview_from_response(
&json!({ "previewId": "p1", "claims": [ { "type": "address.country", "value": "SG" } ] }),
&asked,
)
.unwrap();
assert_eq!(p.missing_required, vec!["name.display".to_string()]);
let ok = preview_from_response(
&json!({ "previewId": "p1", "claims": [ { "type": "name.display", "value": "Ada" } ] }),
&asked,
)
.unwrap();
assert!(ok.missing_required.is_empty());
}
#[test]
fn a_withheld_value_does_not_answer_a_required_type() {
let p = preview_from_response(
&json!({ "previewId": "p1", "claims": [ { "type": "name.display" } ] }),
&[ask("name.display", true)],
)
.unwrap();
assert_eq!(p.missing_required, vec!["name.display".to_string()]);
}
#[test]
fn the_artifact_becomes_attributes_in_order_without_provenance() {
let card = json!({
"type": ["VerifiableDataStructure", "RelationshipCard"],
"claims": {
"0001": { "type": "address.country", "value": "SG",
"provenance": { "kind": "selfAsserted" } },
"0000": { "type": "name.display", "value": "Ada" },
"0002": { "type": "person.ageOver", "predicate": { "gte": 18 } },
},
"unsigned": true,
})
.to_string();
let attrs = attributes_from_artifact(&card).unwrap();
assert_eq!(
serde_json::to_value(&attrs).unwrap(),
json!([
{ "type": "name.display", "value": "Ada" },
{ "type": "address.country", "value": "SG" },
])
);
}
#[test]
fn a_preview_that_changed_since_approval_does_not_match() {
let approved = vec![("name.display".to_string(), json!("Ada"))];
let same = AnswerPreview {
preview_id: "p".into(),
claims: vec![("name.display".into(), Some(json!("Ada")))],
missing_required: vec![],
};
assert!(preview_matches(&same, &approved));
let edited = AnswerPreview {
claims: vec![("name.display".into(), Some(json!("Ada King")))],
..same.clone()
};
assert!(!preview_matches(&edited, &approved));
let more = AnswerPreview {
claims: vec![
("name.display".into(), Some(json!("Ada"))),
("address.country".into(), Some(json!("SG"))),
],
..same
};
assert!(
!preview_matches(&more, &approved),
"one more value than approved"
);
}
}