use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::nuts_auth::holder::Holder;
pub const JWT_VC: &str = "jwt_vc";
pub const JWT_VP: &str = "jwt_vp";
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum Mismatch {
#[error("the definition has no input descriptor {descriptor:?}")]
UnknownDescriptor {
descriptor: String,
},
#[error("no credential answers input descriptor {descriptor:?}")]
Unanswered {
descriptor: String,
},
#[error("submission requirement {index} is not met")]
Requirement {
index: usize,
},
#[error("submission requirement {index} is not a valid requirement")]
InvalidRequirement {
index: usize,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct PresentationDefinition {
pub id: String,
pub input_descriptors: Vec<InputDescriptor>,
#[serde(default)]
pub submission_requirements: Option<Vec<SubmissionRequirement>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct InputDescriptor {
pub id: String,
#[serde(default)]
pub group: Vec<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct SubmissionRequirement {
pub rule: String,
pub count: Option<u64>,
pub min: Option<u64>,
pub max: Option<u64>,
pub from: Option<String>,
pub from_nested: Option<Vec<SubmissionRequirement>>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct PresentationSubmission {
pub id: String,
pub definition_id: String,
pub descriptor_map: Vec<DescriptorMapping>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct DescriptorMapping {
pub id: String,
pub format: String,
pub path: String,
}
pub fn submission(
definition: &PresentationDefinition,
holder: &Holder,
id: String,
) -> Result<PresentationSubmission, Mismatch> {
let known: BTreeSet<&str> = definition
.input_descriptors
.iter()
.map(|descriptor| descriptor.id.as_str())
.collect();
let mut answered = BTreeSet::new();
for credential in holder.credentials() {
if !known.contains(credential.descriptor()) {
return Err(Mismatch::UnknownDescriptor {
descriptor: credential.descriptor().to_owned(),
});
}
answered.insert(credential.descriptor());
}
match &definition.submission_requirements {
None => {
if let Some(missing) = definition
.input_descriptors
.iter()
.find(|descriptor| !answered.contains(descriptor.id.as_str()))
{
return Err(Mismatch::Unanswered {
descriptor: missing.id.clone(),
});
}
}
Some(requirements) => {
for (index, requirement) in requirements.iter().enumerate() {
match met(requirement, definition, &answered) {
Some(true) => {}
Some(false) => return Err(Mismatch::Requirement { index }),
None => return Err(Mismatch::InvalidRequirement { index }),
}
}
}
}
let descriptor_map = holder
.credentials()
.iter()
.enumerate()
.map(|(position, credential)| DescriptorMapping {
id: credential.descriptor().to_owned(),
format: JWT_VC.to_owned(),
path: format!("$.verifiableCredential[{position}]"),
})
.collect();
Ok(PresentationSubmission {
id,
definition_id: definition.id.clone(),
descriptor_map,
})
}
fn met(
requirement: &SubmissionRequirement,
definition: &PresentationDefinition,
answered: &BTreeSet<&str>,
) -> Option<bool> {
let (total, satisfied) = match (&requirement.from, &requirement.from_nested) {
(Some(group), None) => {
let members: Vec<&str> = definition
.input_descriptors
.iter()
.filter(|descriptor| descriptor.group.iter().any(|name| name == group))
.map(|descriptor| descriptor.id.as_str())
.collect();
let satisfied = members.iter().filter(|id| answered.contains(*id)).count();
(members.len(), satisfied)
}
(None, Some(nested)) => {
let mut satisfied = 0_usize;
for inner in nested {
if met(inner, definition, answered)? {
satisfied = satisfied.saturating_add(1);
}
}
(nested.len(), satisfied)
}
_ => return None,
};
let satisfied = u64::try_from(satisfied).ok()?;
match requirement.rule.as_str() {
"all" => Some(u64::try_from(total).ok()? == satisfied),
"pick" => Some(
requirement.count.is_none_or(|count| satisfied == count)
&& requirement.min.is_none_or(|min| satisfied >= min)
&& requirement.max.is_none_or(|max| satisfied <= max),
),
_ => None,
}
}