use serde_json::{Map, Value as JsonValue, json};
pub fn extract_vp_claims(vp: &JsonValue) -> JsonValue {
let vp_obj = match vp.as_object() {
Some(o) => o,
None => return empty_claims(),
};
let holder = vp_obj
.get("holder")
.and_then(|h| match h {
JsonValue::String(s) => Some(JsonValue::String(s.clone())),
JsonValue::Object(o) => o.get("id").cloned(),
_ => None,
})
.unwrap_or(JsonValue::Null);
let credentials = vp_obj
.get("verifiableCredential")
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(extract_credential)
.collect::<Vec<_>>()
})
.unwrap_or_default();
let mut m = Map::new();
m.insert("holder".into(), holder);
m.insert("credentials".into(), JsonValue::Array(credentials));
JsonValue::Object(m)
}
fn empty_claims() -> JsonValue {
let mut m = Map::new();
m.insert("holder".into(), JsonValue::Null);
m.insert("credentials".into(), JsonValue::Array(Vec::new()));
JsonValue::Object(m)
}
fn extract_credential(vc: &JsonValue) -> Option<JsonValue> {
let vc_obj = vc.as_object()?;
let mut out = Map::new();
if let Some(issuer) = vc_obj.get("issuer") {
out.insert("issuer".into(), issuer.clone());
} else {
out.insert("issuer".into(), JsonValue::Null);
}
out.insert(
"type".into(),
vc_obj.get("type").cloned().unwrap_or_else(|| json!([])),
);
if let Some(date) = vc_obj.get("issuanceDate") {
out.insert("issuanceDate".into(), date.clone());
}
if let Some(date) = vc_obj.get("validFrom") {
out.insert("validFrom".into(), date.clone());
}
out.insert(
"credentialSubject".into(),
vc_obj
.get("credentialSubject")
.cloned()
.unwrap_or(JsonValue::Null),
);
Some(JsonValue::Object(out))
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn empty_vp_yields_empty_claims() {
let claims = extract_vp_claims(&JsonValue::Null);
assert_eq!(claims, json!({ "holder": null, "credentials": [] }));
let claims = extract_vp_claims(&json!("opaque jwt string"));
assert_eq!(claims, json!({ "holder": null, "credentials": [] }));
}
#[test]
fn holder_is_extracted_from_string_or_object() {
let s = extract_vp_claims(&json!({ "holder": "did:key:zX" }));
assert_eq!(s["holder"], json!("did:key:zX"));
let o = extract_vp_claims(&json!({ "holder": { "id": "did:key:zX", "name": "h" }}));
assert_eq!(o["holder"], json!("did:key:zX"));
}
#[test]
fn credentials_are_projected_in_order() {
let vp = json!({
"holder": "did:key:zHolder",
"verifiableCredential": [
{
"issuer": "did:key:zIssuerA",
"type": ["VerifiableCredential", "EmailCredential"],
"issuanceDate": "2026-01-01T00:00:00Z",
"credentialSubject": { "email": "a@example.com" }
},
{
"issuer": { "id": "did:webvh:peer.example" },
"type": ["VerifiableCredential", "ProofOfHumanity"],
"validFrom": "2026-03-01T00:00:00Z",
"credentialSubject": { "level": "L1" }
}
]
});
let claims = extract_vp_claims(&vp);
let creds = claims["credentials"].as_array().unwrap();
assert_eq!(creds.len(), 2);
assert_eq!(creds[0]["issuer"], json!("did:key:zIssuerA"));
assert_eq!(creds[0]["type"][1], "EmailCredential");
assert_eq!(creds[0]["credentialSubject"]["email"], "a@example.com");
assert_eq!(creds[0]["issuanceDate"], "2026-01-01T00:00:00Z");
assert_eq!(creds[1]["issuer"]["id"], "did:webvh:peer.example");
assert_eq!(creds[1]["validFrom"], "2026-03-01T00:00:00Z");
assert!(creds[1].get("issuanceDate").is_none());
}
#[test]
fn credentials_array_is_optional() {
let vp = json!({ "holder": "did:key:zX" });
let claims = extract_vp_claims(&vp);
assert_eq!(claims["holder"], "did:key:zX");
assert_eq!(claims["credentials"], json!([]));
}
#[test]
fn jwt_encoded_vcs_in_array_are_skipped() {
let vp = json!({
"verifiableCredential": [
"eyJhbGciOi...",
{ "issuer": "did:key:zY", "credentialSubject": {} }
]
});
let claims = extract_vp_claims(&vp);
let creds = claims["credentials"].as_array().unwrap();
assert_eq!(creds.len(), 1);
assert_eq!(creds[0]["issuer"], "did:key:zY");
}
}