1use chrono::{DateTime, Utc};
4use indexmap::IndexMap;
5use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
6use serde_json::Value;
7
8use crate::did::{Context, OneOrMany, Uri};
9
10use super::model::{CONTEXT_V1, append_context, check_context_and_type, push_type};
11use super::{CredentialError, JwtClaims, Party, Subject, TypedObject, claims};
12
13#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
47#[non_exhaustive]
48#[serde(remote = "Self")]
49pub struct Credential {
50 #[serde(rename = "@context")]
52 pub context: Context,
53 #[serde(default, skip_serializing_if = "Option::is_none")]
55 pub id: Option<Uri>,
56 #[serde(rename = "type")]
58 pub types: OneOrMany<String>,
59 pub issuer: Party,
61 #[serde(rename = "issuanceDate")]
63 pub issuance_date: DateTime<Utc>,
64 #[serde(
66 rename = "expirationDate",
67 default,
68 skip_serializing_if = "Option::is_none"
69 )]
70 pub expiration_date: Option<DateTime<Utc>>,
71 #[serde(rename = "credentialSubject")]
73 pub credential_subject: OneOrMany<Subject>,
74 #[serde(
76 rename = "credentialStatus",
77 default,
78 skip_serializing_if = "Option::is_none"
79 )]
80 pub credential_status: Option<TypedObject>,
81 #[serde(
83 rename = "credentialSchema",
84 default,
85 skip_serializing_if = "Option::is_none"
86 )]
87 pub credential_schema: Option<OneOrMany<TypedObject>>,
88 #[serde(
90 rename = "refreshService",
91 default,
92 skip_serializing_if = "Option::is_none"
93 )]
94 pub refresh_service: Option<OneOrMany<TypedObject>>,
95 #[serde(
97 rename = "termsOfUse",
98 default,
99 skip_serializing_if = "Option::is_none"
100 )]
101 pub terms_of_use: Option<OneOrMany<TypedObject>>,
102 #[serde(default, skip_serializing_if = "Option::is_none")]
104 pub evidence: Option<OneOrMany<TypedObject>>,
105 #[serde(flatten)]
107 pub properties: IndexMap<String, Value>,
108}
109
110impl Serialize for Credential {
111 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
112 Self::serialize(self, serializer)
113 }
114}
115
116impl<'de> Deserialize<'de> for Credential {
117 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
119 let credential = Self::deserialize(deserializer)?;
120 credential.check().map_err(de::Error::custom)?;
121 Ok(credential)
122 }
123}
124
125impl Credential {
126 pub const CONTEXT: &'static str = CONTEXT_V1;
128
129 pub const TYPE: &'static str = "VerifiableCredential";
131
132 pub fn new(issuer: impl Into<Party>, issuance_date: DateTime<Utc>, subject: Subject) -> Self {
135 Self {
136 context: Context::uri(Self::CONTEXT),
137 id: None,
138 types: OneOrMany::One(Self::TYPE.to_owned()),
139 issuer: issuer.into(),
140 issuance_date,
141 expiration_date: None,
142 credential_subject: OneOrMany::One(subject),
143 credential_status: None,
144 credential_schema: None,
145 refresh_service: None,
146 terms_of_use: None,
147 evidence: None,
148 properties: IndexMap::new(),
149 }
150 }
151
152 #[must_use]
154 pub fn with_context(mut self, context: impl Into<String>) -> Self {
155 self.context = append_context(self.context, context.into());
156 self
157 }
158
159 #[must_use]
161 pub fn with_id(mut self, id: impl Into<Uri>) -> Self {
162 self.id = Some(id.into());
163 self
164 }
165
166 #[must_use]
168 pub fn with_type(mut self, credential_type: impl Into<String>) -> Self {
169 push_type(&mut self.types, credential_type.into());
170 self
171 }
172
173 #[must_use]
175 pub fn with_expiration_date(mut self, expiration_date: DateTime<Utc>) -> Self {
176 self.expiration_date = Some(expiration_date);
177 self
178 }
179
180 #[must_use]
182 pub fn with_subject(mut self, subject: Subject) -> Self {
183 let mut subjects =
184 core::mem::replace(&mut self.credential_subject, OneOrMany::Many(Vec::new()))
185 .into_vec();
186 subjects.push(subject);
187 self.credential_subject = OneOrMany::Many(subjects);
188 self
189 }
190
191 #[must_use]
193 pub fn with_status(mut self, status: TypedObject) -> Self {
194 self.credential_status = Some(status);
195 self
196 }
197
198 #[must_use]
200 pub fn with_schema(mut self, schema: TypedObject) -> Self {
201 self.credential_schema = Some(push(self.credential_schema.take(), schema));
202 self
203 }
204
205 #[must_use]
207 pub fn with_terms_of_use(mut self, terms: TypedObject) -> Self {
208 self.terms_of_use = Some(push(self.terms_of_use.take(), terms));
209 self
210 }
211
212 #[must_use]
214 pub fn with_evidence(mut self, evidence: TypedObject) -> Self {
215 self.evidence = Some(push(self.evidence.take(), evidence));
216 self
217 }
218
219 #[must_use]
221 pub fn with_property(mut self, name: impl Into<String>, value: impl Into<Value>) -> Self {
222 self.properties.insert(name.into(), value.into());
223 self
224 }
225
226 pub fn subject(&self) -> Option<&Subject> {
228 (self.credential_subject.len() == 1).then(|| self.credential_subject.first())?
229 }
230
231 pub fn has_type(&self, credential_type: &str) -> bool {
233 self.types.iter().any(|t| t == credential_type)
234 }
235
236 pub fn check(&self) -> Result<(), CredentialError> {
244 check_context_and_type(&self.context, Self::CONTEXT, &self.types, Self::TYPE)?;
245 if self.credential_subject.is_empty() {
246 return Err(CredentialError::NoSubject);
247 }
248 if self
249 .expiration_date
250 .is_some_and(|until| until < self.issuance_date)
251 {
252 return Err(CredentialError::InvalidValidity {
253 from: "issuanceDate",
254 until: "expirationDate",
255 });
256 }
257 Ok(())
258 }
259
260 pub fn to_jwt_claims(&self) -> Result<JwtClaims, CredentialError> {
272 self.check()?;
273 let mut vc = claims::to_object(self);
274 let jti = claims::take_id(&mut vc, "id");
275 let iss = claims::take_id(&mut vc, "issuer");
276 vc.shift_remove("issuanceDate");
277 vc.shift_remove("expirationDate");
278 let sub = claims::take_subject(&mut vc)?;
279 Ok(JwtClaims {
280 iss,
281 sub,
282 aud: None,
283 exp: self.expiration_date.map(JwtClaims::numeric_date),
284 nbf: Some(JwtClaims::numeric_date(self.issuance_date)),
285 iat: None,
286 jti,
287 additional: IndexMap::from([("vc".to_owned(), Value::Object(vc))]),
288 })
289 }
290
291 pub fn from_jwt_claims(jwt_claims: &JwtClaims) -> Result<Self, CredentialError> {
301 let mut vc = claims::claim_object(jwt_claims, "vc")?;
302 claims::merge_id(&mut vc, "id", "jti", jwt_claims.jti.as_deref())?;
303 claims::merge_id(&mut vc, "issuer", "iss", jwt_claims.iss.as_deref())?;
304 claims::merge_time(&mut vc, "issuanceDate", "nbf", jwt_claims.nbf)?;
305 claims::merge_time(&mut vc, "expirationDate", "exp", jwt_claims.exp)?;
306 claims::merge_subject(&mut vc, jwt_claims.sub.as_deref())?;
307 let credential: Self = serde_json::from_value(Value::Object(vc)).map_err(|e| {
308 CredentialError::InvalidClaim {
309 claim: "vc",
310 reason: e.to_string(),
311 }
312 })?;
313 Ok(credential)
314 }
315}
316
317pub(super) fn push<T>(list: Option<OneOrMany<T>>, item: T) -> OneOrMany<T> {
319 match list {
320 None => OneOrMany::One(item),
321 Some(list) => {
322 let mut items = list.into_vec();
323 items.push(item);
324 OneOrMany::Many(items)
325 }
326 }
327}