use std::collections::BTreeMap;
use serde::{Deserialize, Serialize};
use crate::statement::check_constraints;
use crate::{
DTGCredential, DTGCredentialError, DTGCredentialType, IssuerScope, ObjectKind,
check_predicate_iri,
};
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum PredicateStatus {
Draft,
Candidate,
Standard,
Deprecated,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AdditionalMember {
pub required: bool,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub schema: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase", deny_unknown_fields)]
pub struct AcceptListEntry {
pub status: PredicateStatus,
pub object_kind: Vec<ObjectKind>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub object_schema: Option<String>,
pub task_context_required: bool,
pub minimum_issuer_scope: Option<IssuerScope>,
pub additional_members: BTreeMap<String, AdditionalMember>,
pub superseded_by: Option<String>,
}
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)]
#[serde(rename_all = "camelCase")]
pub struct RegistryAcceptList {
#[serde(rename = "$schema")]
pub schema: String,
pub namespace: String,
pub commit: String,
pub generated_at: String,
pub predicates: BTreeMap<String, AcceptListEntry>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct PredicateAcceptList {
predicates: BTreeMap<String, Option<AcceptListEntry>>,
}
impl PredicateAcceptList {
pub fn from_iris<I, S>(iris: I) -> Result<Self, DTGCredentialError>
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut predicates = BTreeMap::new();
for iri in iris {
let iri = iri.into();
check_predicate_iri(&iri)?;
predicates.insert(iri, None);
}
Ok(PredicateAcceptList { predicates })
}
pub fn from_registry(
list: &RegistryAcceptList,
statuses: &[PredicateStatus],
) -> Result<Self, DTGCredentialError> {
let mut predicates = BTreeMap::new();
for (iri, entry) in &list.predicates {
if statuses.contains(&entry.status) {
check_predicate_iri(iri)?;
predicates.insert(iri.clone(), Some(entry.clone()));
}
}
Ok(PredicateAcceptList { predicates })
}
pub fn from_registry_json(
json: &str,
statuses: &[PredicateStatus],
) -> Result<Self, DTGCredentialError> {
let list: RegistryAcceptList = serde_json::from_str(json)
.map_err(|e| DTGCredentialError::MalformedAcceptList(e.to_string()))?;
Self::from_registry(&list, statuses)
}
pub fn contains(&self, predicate: &str) -> bool {
self.predicates.contains_key(predicate)
}
pub fn iris(&self) -> impl Iterator<Item = &str> {
self.predicates.keys().map(String::as_str)
}
pub fn entry(&self, predicate: &str) -> Option<&AcceptListEntry> {
self.predicates.get(predicate).and_then(Option::as_ref)
}
pub fn len(&self) -> usize {
self.predicates.len()
}
pub fn is_empty(&self) -> bool {
self.predicates.is_empty()
}
pub fn accept<'a>(&self, vsc: &'a DTGCredential) -> Result<&'a str, DTGCredentialError> {
let (true, Some(statement)) =
(vsc.type_() == DTGCredentialType::Statement, vsc.statement())
else {
return Err(DTGCredentialError::WrongCredentialType {
expected: DTGCredentialType::Statement.to_string(),
got: vsc.type_().to_string(),
});
};
vsc.validate()?;
let Some(entry) = self.predicates.get(&statement.predicate) else {
return Err(DTGCredentialError::PredicateNotAccepted(
statement.predicate.clone(),
));
};
if let Some(entry) = entry {
check_constraints(
&entry.object_kind,
entry.task_context_required,
entry.minimum_issuer_scope,
vsc.credential(),
statement,
)?;
for (member, definition) in &entry.additional_members {
let present = match member.as_str() {
"witnessContext" => statement.witness_context.is_some(),
other => statement.extra.contains_key(other),
};
if definition.required && !present {
return Err(DTGCredentialError::ProfileViolation(format!(
"`{}` requires `credentialSubject.{member}`",
statement.predicate
)));
}
}
}
Ok(&statement.predicate)
}
}