use chrono::{DateTime, Utc};
use indexmap::IndexMap;
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
use serde_json::Value;
use crate::did::{Context, OneOrMany, Uri};
use super::model::{CONTEXT_V1, append_context, check_context_and_type, push_type};
use super::{CredentialError, JwtClaims, Party, Subject, TypedObject, claims};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(remote = "Self")]
pub struct Credential {
#[serde(rename = "@context")]
pub context: Context,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<Uri>,
#[serde(rename = "type")]
pub types: OneOrMany<String>,
pub issuer: Party,
#[serde(rename = "issuanceDate")]
pub issuance_date: DateTime<Utc>,
#[serde(
rename = "expirationDate",
default,
skip_serializing_if = "Option::is_none"
)]
pub expiration_date: Option<DateTime<Utc>>,
#[serde(rename = "credentialSubject")]
pub credential_subject: OneOrMany<Subject>,
#[serde(
rename = "credentialStatus",
default,
skip_serializing_if = "Option::is_none"
)]
pub credential_status: Option<TypedObject>,
#[serde(
rename = "credentialSchema",
default,
skip_serializing_if = "Option::is_none"
)]
pub credential_schema: Option<OneOrMany<TypedObject>>,
#[serde(
rename = "refreshService",
default,
skip_serializing_if = "Option::is_none"
)]
pub refresh_service: Option<OneOrMany<TypedObject>>,
#[serde(
rename = "termsOfUse",
default,
skip_serializing_if = "Option::is_none"
)]
pub terms_of_use: Option<OneOrMany<TypedObject>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub evidence: Option<OneOrMany<TypedObject>>,
#[serde(flatten)]
pub properties: IndexMap<String, Value>,
}
impl Serialize for Credential {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
Self::serialize(self, serializer)
}
}
impl<'de> Deserialize<'de> for Credential {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
let credential = Self::deserialize(deserializer)?;
credential.check().map_err(de::Error::custom)?;
Ok(credential)
}
}
impl Credential {
pub const CONTEXT: &'static str = CONTEXT_V1;
pub const TYPE: &'static str = "VerifiableCredential";
pub fn new(issuer: impl Into<Party>, issuance_date: DateTime<Utc>, subject: Subject) -> Self {
Self {
context: Context::uri(Self::CONTEXT),
id: None,
types: OneOrMany::One(Self::TYPE.to_owned()),
issuer: issuer.into(),
issuance_date,
expiration_date: None,
credential_subject: OneOrMany::One(subject),
credential_status: None,
credential_schema: None,
refresh_service: None,
terms_of_use: None,
evidence: None,
properties: IndexMap::new(),
}
}
#[must_use]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = append_context(self.context, context.into());
self
}
#[must_use]
pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
self.id = Some(id.into());
self
}
#[must_use]
pub fn with_type(mut self, credential_type: impl Into<String>) -> Self {
push_type(&mut self.types, credential_type.into());
self
}
#[must_use]
pub fn with_expiration_date(mut self, expiration_date: DateTime<Utc>) -> Self {
self.expiration_date = Some(expiration_date);
self
}
#[must_use]
pub fn with_subject(mut self, subject: Subject) -> Self {
let mut subjects =
core::mem::replace(&mut self.credential_subject, OneOrMany::Many(Vec::new()))
.into_vec();
subjects.push(subject);
self.credential_subject = OneOrMany::Many(subjects);
self
}
#[must_use]
pub fn with_status(mut self, status: TypedObject) -> Self {
self.credential_status = Some(status);
self
}
#[must_use]
pub fn with_schema(mut self, schema: TypedObject) -> Self {
self.credential_schema = Some(push(self.credential_schema.take(), schema));
self
}
#[must_use]
pub fn with_terms_of_use(mut self, terms: TypedObject) -> Self {
self.terms_of_use = Some(push(self.terms_of_use.take(), terms));
self
}
#[must_use]
pub fn with_evidence(mut self, evidence: TypedObject) -> Self {
self.evidence = Some(push(self.evidence.take(), evidence));
self
}
#[must_use]
pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
self.properties.insert(name.into(), value.into());
self
}
pub fn subject(&self) -> Option<&Subject> {
(self.credential_subject.len() == 1).then(|| self.credential_subject.first())?
}
pub fn has_type(&self, credential_type: &str) -> bool {
self.types.iter().any(|t| t == credential_type)
}
pub fn check(&self) -> Result<(), CredentialError> {
check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)?;
if self.credential_subject.is_empty() {
return Err(CredentialError::NoSubject);
}
if self
.expiration_date
.is_some_and(|until| until < self.issuance_date)
{
return Err(CredentialError::InvalidValidity {
from: "issuanceDate",
until: "expirationDate",
});
}
Ok(())
}
pub fn to_jwt_claims(&self) -> Result<JwtClaims, CredentialError> {
self.check()?;
let mut vc = claims::to_object(self);
let jti = claims::take_id(&mut vc, "id");
let iss = claims::take_id(&mut vc, "issuer");
vc.shift_remove("issuanceDate");
vc.shift_remove("expirationDate");
let sub = claims::take_subject(&mut vc)?;
Ok(JwtClaims {
iss,
sub,
aud: None,
exp: self.expiration_date.map(JwtClaims::numeric_date),
nbf: Some(JwtClaims::numeric_date(self.issuance_date)),
iat: None,
jti,
additional: IndexMap::from([("vc".to_owned(), Value::Object(vc))]),
})
}
pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
let mut vc = claims::claim_object(jwt_claims, "vc")?;
claims::merge_id(&mut vc, "id", "jti", jwt_claims.jti.as_deref())?;
claims::merge_id(&mut vc, "issuer", "iss", jwt_claims.iss.as_deref())?;
claims::merge_time(&mut vc, "issuanceDate", "nbf", jwt_claims.nbf)?;
claims::merge_time(&mut vc, "expirationDate", "exp", jwt_claims.exp)?;
claims::merge_subject(&mut vc, jwt_claims.sub.as_deref())?;
let credential: Self = serde_json::from_value(Value::Object(vc)).map_err(|e| {
CredentialError::InvalidClaim {
claim: "vc",
reason: e.to_string(),
}
})?;
Ok(credential)
}
}
pub(super) fn push<T>(list: Option<OneOrMany<T>>, item: T) -> OneOrMany<T> {
match list {
None => OneOrMany::One(item),
Some(list) => {
let mut items = list.into_vec();
items.push(item);
OneOrMany::Many(items)
}
}
}