use crate::{Did, DidResult, Proof};
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct VerifiableCredential {
#[serde(rename = "@context")]
pub context: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type")]
pub credential_type: Vec<String>,
pub issuer: CredentialIssuerInfo,
#[serde(skip_serializing_if = "Option::is_none")]
pub issuance_date: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub valid_from: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub expiration_date: Option<DateTime<Utc>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub valid_until: Option<DateTime<Utc>>,
pub credential_subject: CredentialSubjectContainer,
#[serde(skip_serializing_if = "Option::is_none")]
pub proof: Option<ProofContainer>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credential_status: Option<CredentialStatus>,
#[serde(skip_serializing_if = "Option::is_none")]
pub terms_of_use: Option<Vec<TermsOfUse>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub evidence: Option<Vec<Evidence>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credential_schema: Option<Vec<CredentialSchema>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_service: Option<RefreshService>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CredentialIssuerInfo {
Did(Did),
Object {
id: Did,
#[serde(skip_serializing_if = "Option::is_none")]
name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
description: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
image: Option<String>,
},
}
impl CredentialIssuerInfo {
pub fn did(&self) -> &Did {
match self {
CredentialIssuerInfo::Did(did) => did,
CredentialIssuerInfo::Object { id, .. } => id,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CredentialSubjectContainer {
Single(CredentialSubject),
Multiple(Vec<CredentialSubject>),
}
impl CredentialSubjectContainer {
pub fn subjects(&self) -> Vec<&CredentialSubject> {
match self {
CredentialSubjectContainer::Single(s) => vec![s],
CredentialSubjectContainer::Multiple(v) => v.iter().collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CredentialSubject {
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(flatten)]
pub claims: HashMap<String, serde_json::Value>,
}
impl CredentialSubject {
pub fn new(id: Option<&str>) -> Self {
Self {
id: id.map(String::from),
claims: HashMap::new(),
}
}
pub fn with_claim(mut self, key: &str, value: impl Into<serde_json::Value>) -> Self {
self.claims.insert(key.to_string(), value.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum ProofContainer {
Single(Box<Proof>),
Multiple(Vec<Proof>),
}
impl ProofContainer {
pub fn proofs(&self) -> Vec<&Proof> {
match self {
ProofContainer::Single(p) => vec![p],
ProofContainer::Multiple(v) => v.iter().collect(),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CredentialStatus {
pub id: String,
#[serde(rename = "type")]
pub status_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_purpose: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_list_index: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub status_list_credential: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct TermsOfUse {
#[serde(rename = "type")]
pub terms_type: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Evidence {
#[serde(rename = "type")]
pub evidence_type: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(flatten)]
pub extra: HashMap<String, serde_json::Value>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CredentialSchema {
pub id: String,
#[serde(rename = "type")]
pub schema_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct RefreshService {
pub id: String,
#[serde(rename = "type")]
pub service_type: String,
}
impl VerifiableCredential {
pub fn new(issuer: Did, subject: CredentialSubject, credential_types: Vec<String>) -> Self {
let mut types = vec!["VerifiableCredential".to_string()];
types.extend(credential_types);
let now = Utc::now();
Self {
context: vec!["https://www.w3.org/ns/credentials/v2".to_string()],
id: Some(format!("urn:uuid:{}", uuid::Uuid::new_v4())),
credential_type: types,
issuer: CredentialIssuerInfo::Did(issuer),
issuance_date: Some(now),
valid_from: Some(now),
expiration_date: None,
valid_until: None,
credential_subject: CredentialSubjectContainer::Single(subject),
proof: None,
credential_status: None,
terms_of_use: None,
evidence: None,
credential_schema: None,
refresh_service: None,
}
}
pub fn with_expiration(mut self, expires: DateTime<Utc>) -> Self {
self.expiration_date = Some(expires);
self.valid_until = Some(expires);
self
}
pub fn with_context(mut self, context: &str) -> Self {
self.context.push(context.to_string());
self
}
pub fn is_expired(&self) -> bool {
let now = Utc::now();
if let Some(exp) = self.expiration_date {
return now > exp;
}
if let Some(until) = self.valid_until {
return now > until;
}
false
}
pub fn is_not_yet_valid(&self) -> bool {
let now = Utc::now();
if let Some(from) = self.valid_from {
return now < from;
}
if let Some(issued) = self.issuance_date {
return now < issued;
}
false
}
pub fn to_json(&self) -> DidResult<String> {
serde_json::to_string_pretty(self)
.map_err(|e| crate::DidError::SerializationError(e.to_string()))
}
pub fn from_json(json: &str) -> DidResult<Self> {
serde_json::from_str(json).map_err(|e| crate::DidError::SerializationError(e.to_string()))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_credential() {
let issuer = Did::new_key_ed25519(&[0u8; 32]).unwrap();
let subject = CredentialSubject::new(Some("did:example:alice"))
.with_claim("name", "Alice")
.with_claim("age", 30);
let vc = VerifiableCredential::new(
issuer.clone(),
subject,
vec!["ExampleCredential".to_string()],
);
assert!(vc
.credential_type
.contains(&"VerifiableCredential".to_string()));
assert!(vc
.credential_type
.contains(&"ExampleCredential".to_string()));
assert!(!vc.is_expired());
}
#[test]
fn test_credential_serialization() {
let issuer = Did::new_key_ed25519(&[0u8; 32]).unwrap();
let subject = CredentialSubject::new(Some("did:example:bob"))
.with_claim("degree", "Bachelor of Science");
let vc = VerifiableCredential::new(
issuer,
subject,
vec!["UniversityDegreeCredential".to_string()],
);
let json = vc.to_json().unwrap();
let parsed = VerifiableCredential::from_json(&json).unwrap();
assert_eq!(vc.id, parsed.id);
assert_eq!(vc.credential_type, parsed.credential_type);
}
#[test]
fn test_credential_expiration() {
let issuer = Did::new_key_ed25519(&[0u8; 32]).unwrap();
let subject = CredentialSubject::new(None);
let expired = VerifiableCredential::new(issuer.clone(), subject.clone(), vec![])
.with_expiration(Utc::now() - chrono::Duration::hours(1));
let valid = VerifiableCredential::new(issuer, subject, vec![])
.with_expiration(Utc::now() + chrono::Duration::hours(1));
assert!(expired.is_expired());
assert!(!valid.is_expired());
}
}