1use iref::{Uri, UriBuf};
2use linked_data::{LinkedDataResource, LinkedDataSubject};
3use rdf_types::VocabularyMut;
4use serde::{Deserialize, Serialize};
5use ssi_claims_core::{ClaimsValidity, DateTimeProvider, ValidateClaims};
6use ssi_core::Lexical;
7use ssi_json_ld::{JsonLdError, JsonLdNodeObject, JsonLdObject, JsonLdTypes, Loader};
8use ssi_rdf::{Interpretation, LdEnvironment};
9use std::{borrow::Cow, collections::BTreeMap, hash::Hash};
10use xsd_types::DateTime;
11
12use crate::{
13 syntax::{
14 non_empty_value_or_array, not_null, value_or_array, IdOr, IdentifiedObject,
15 IdentifiedTypedObject, MaybeIdentifiedTypedObject, NonEmptyVec, RequiredContextList,
16 RequiredType, RequiredTypeSet, TypeSerializationPolicy, Types,
17 },
18 Identified, MaybeIdentified, Typed,
19};
20
21use super::Context;
22
23pub const VERIFIABLE_CREDENTIAL_TYPE: &str = "VerifiableCredential";
24
25pub struct CredentialType;
26
27impl RequiredType for CredentialType {
28 const REQUIRED_TYPE: &'static str = VERIFIABLE_CREDENTIAL_TYPE;
29}
30
31impl TypeSerializationPolicy for CredentialType {
32 const PREFER_ARRAY: bool = true;
33}
34
35pub type JsonCredentialTypes<T = ()> = Types<CredentialType, T>;
36
37pub type JsonCredential<S = json_syntax::Object> = SpecializedJsonCredential<S>;
43
44#[derive(Debug, Clone, Serialize, Deserialize)]
49#[serde(bound(
50 serialize = "Subject: Serialize, Issuer: Serialize, Status: Serialize, Evidence: Serialize, Schema: Serialize, RefreshService: Serialize, TermsOfUse: Serialize, ExtraProperties: Serialize",
51 deserialize = "Subject: Deserialize<'de>, RequiredContext: RequiredContextList, RequiredType: RequiredTypeSet, Issuer: Deserialize<'de>, Status: Deserialize<'de>, Evidence: Deserialize<'de>, Schema: Deserialize<'de>, RefreshService: Deserialize<'de>, TermsOfUse: Deserialize<'de>, ExtraProperties: Deserialize<'de>"
52))]
53pub struct SpecializedJsonCredential<
54 Subject = json_syntax::Object,
55 RequiredContext = (),
56 RequiredType = (),
57 Issuer = IdOr<IdentifiedObject>,
58 Status = IdentifiedTypedObject,
59 Evidence = MaybeIdentifiedTypedObject,
60 Schema = IdentifiedTypedObject,
61 RefreshService = IdentifiedTypedObject,
62 TermsOfUse = MaybeIdentifiedTypedObject,
63 ExtraProperties = BTreeMap<String, json_syntax::Value>,
64> {
65 #[serde(rename = "@context")]
67 pub context: Context<RequiredContext>,
68
69 #[serde(
71 default,
72 deserialize_with = "not_null",
73 skip_serializing_if = "Option::is_none"
74 )]
75 pub id: Option<UriBuf>,
76
77 #[serde(rename = "type")]
79 pub types: JsonCredentialTypes<RequiredType>,
80
81 #[serde(rename = "credentialSubject")]
83 #[serde(with = "non_empty_value_or_array")]
84 pub credential_subjects: NonEmptyVec<Subject>,
85
86 pub issuer: Issuer,
88
89 #[serde(rename = "issuanceDate")]
93 pub issuance_date: Option<Lexical<xsd_types::DateTime>>,
94
95 #[serde(rename = "expirationDate")]
97 #[serde(default, skip_serializing_if = "Option::is_none")]
98 pub expiration_date: Option<Lexical<xsd_types::DateTime>>,
99
100 #[serde(rename = "credentialStatus")]
102 #[serde(
103 with = "value_or_array",
104 default,
105 skip_serializing_if = "Vec::is_empty"
106 )]
107 pub credential_status: Vec<Status>,
108
109 #[serde(rename = "termsOfUse")]
111 #[serde(
112 with = "value_or_array",
113 default,
114 skip_serializing_if = "Vec::is_empty"
115 )]
116 pub terms_of_use: Vec<TermsOfUse>,
117
118 #[serde(
120 with = "value_or_array",
121 default,
122 skip_serializing_if = "Vec::is_empty"
123 )]
124 pub evidence: Vec<Evidence>,
125
126 #[serde(rename = "credentialSchema")]
127 #[serde(
128 with = "value_or_array",
129 default,
130 skip_serializing_if = "Vec::is_empty"
131 )]
132 pub credential_schema: Vec<Schema>,
133
134 #[serde(rename = "refreshService")]
135 #[serde(
136 with = "value_or_array",
137 default,
138 skip_serializing_if = "Vec::is_empty"
139 )]
140 pub refresh_services: Vec<RefreshService>,
141
142 #[serde(flatten)]
143 pub additional_properties: ExtraProperties,
144}
145
146impl<
147 Subject,
148 RequiredContext,
149 RequiredType,
150 Issuer,
151 Status,
152 Evidence,
153 Schema,
154 RefreshService,
155 TermsOfUse,
156 ExtraProperties,
157 >
158 SpecializedJsonCredential<
159 Subject,
160 RequiredContext,
161 RequiredType,
162 Issuer,
163 Status,
164 Evidence,
165 Schema,
166 RefreshService,
167 TermsOfUse,
168 ExtraProperties,
169 >
170where
171 RequiredContext: RequiredContextList,
172 RequiredType: RequiredTypeSet,
173 ExtraProperties: Default,
174{
175 pub fn new(
177 id: Option<UriBuf>,
178 issuer: Issuer,
179 issuance_date: Lexical<xsd_types::DateTime>,
180 credential_subjects: NonEmptyVec<Subject>,
181 ) -> Self {
182 Self {
183 context: Context::default(),
184 id,
185 types: JsonCredentialTypes::default(),
186 issuer,
187 issuance_date: Some(issuance_date),
188 credential_subjects,
189 expiration_date: None,
190 credential_status: Vec::new(),
191 terms_of_use: Vec::new(),
192 evidence: Vec::new(),
193 credential_schema: Vec::new(),
194 refresh_services: Vec::new(),
195 additional_properties: ExtraProperties::default(),
196 }
197 }
198}
199
200impl<
201 Subject,
202 RequiredContext,
203 RequiredType,
204 Issuer,
205 Status,
206 Evidence,
207 Schema,
208 RefreshService,
209 TermsOfUse,
210 ExtraProperties,
211 > JsonLdObject
212 for SpecializedJsonCredential<
213 Subject,
214 RequiredContext,
215 RequiredType,
216 Issuer,
217 Status,
218 Evidence,
219 Schema,
220 RefreshService,
221 TermsOfUse,
222 ExtraProperties,
223 >
224{
225 fn json_ld_context(&self) -> Option<Cow<ssi_json_ld::syntax::Context>> {
226 Some(Cow::Borrowed(self.context.as_ref()))
227 }
228}
229
230impl<
231 Subject,
232 RequiredContext,
233 RequiredType,
234 Issuer,
235 Status,
236 Evidence,
237 Schema,
238 RefreshService,
239 TermsOfUse,
240 ExtraProperties,
241 > JsonLdNodeObject
242 for SpecializedJsonCredential<
243 Subject,
244 RequiredContext,
245 RequiredType,
246 Issuer,
247 Status,
248 Evidence,
249 Schema,
250 RefreshService,
251 TermsOfUse,
252 ExtraProperties,
253 >
254{
255 fn json_ld_type(&self) -> JsonLdTypes {
256 self.types.to_json_ld_types()
257 }
258}
259
260impl<
261 Subject,
262 RequiredContext,
263 RequiredType,
264 Issuer: Identified,
265 Status: Identified + Typed,
266 Evidence: MaybeIdentified + Typed,
267 Schema: Identified + Typed,
268 RefreshService: Identified + Typed,
269 TermsOfUse: MaybeIdentified + Typed,
270 ExtraProperties,
271 E,
272 P,
273 > ValidateClaims<E, P>
274 for SpecializedJsonCredential<
275 Subject,
276 RequiredContext,
277 RequiredType,
278 Issuer,
279 Status,
280 Evidence,
281 Schema,
282 RefreshService,
283 TermsOfUse,
284 ExtraProperties,
285 >
286where
287 E: DateTimeProvider,
288{
289 fn validate_claims(&self, env: &E, _proof: &P) -> ClaimsValidity {
290 crate::v1::Credential::validate_credential(self, env)
291 }
292}
293
294impl<
295 Subject,
296 RequiredContext,
297 RequiredType,
298 Issuer,
299 Status,
300 Evidence,
301 Schema,
302 RefreshService,
303 TermsOfUse,
304 ExtraProperties,
305 > crate::MaybeIdentified
306 for SpecializedJsonCredential<
307 Subject,
308 RequiredContext,
309 RequiredType,
310 Issuer,
311 Status,
312 Evidence,
313 Schema,
314 RefreshService,
315 TermsOfUse,
316 ExtraProperties,
317 >
318{
319 fn id(&self) -> Option<&Uri> {
320 self.id.as_deref()
321 }
322}
323
324impl<
325 Subject,
326 RequiredContext,
327 RequiredType,
328 Issuer: Identified,
329 Status: Identified + Typed,
330 Evidence: MaybeIdentified + Typed,
331 Schema: Identified + Typed,
332 RefreshService: Identified + Typed,
333 TermsOfUse: MaybeIdentified + Typed,
334 ExtraProperties,
335 > crate::v1::Credential
336 for SpecializedJsonCredential<
337 Subject,
338 RequiredContext,
339 RequiredType,
340 Issuer,
341 Status,
342 Evidence,
343 Schema,
344 RefreshService,
345 TermsOfUse,
346 ExtraProperties,
347 >
348{
349 type Subject = Subject;
350 type Issuer = Issuer;
351 type Status = Status;
352 type RefreshService = RefreshService;
353 type TermsOfUse = TermsOfUse;
354 type Evidence = Evidence;
355 type Schema = Schema;
356
357 fn id(&self) -> Option<&Uri> {
358 self.id.as_deref()
359 }
360
361 fn additional_types(&self) -> &[String] {
362 self.types.additional_types()
363 }
364
365 fn credential_subjects(&self) -> &[Self::Subject] {
366 &self.credential_subjects
367 }
368
369 fn issuer(&self) -> &Self::Issuer {
370 &self.issuer
371 }
372
373 fn issuance_date(&self) -> Option<DateTime> {
374 self.issuance_date.as_ref().map(Lexical::to_value)
375 }
376
377 fn expiration_date(&self) -> Option<DateTime> {
378 self.expiration_date.as_ref().map(Lexical::to_value)
379 }
380
381 fn credential_status(&self) -> &[Self::Status] {
382 &self.credential_status
383 }
384
385 fn refresh_services(&self) -> &[Self::RefreshService] {
386 &self.refresh_services
387 }
388
389 fn terms_of_use(&self) -> &[Self::TermsOfUse] {
390 &self.terms_of_use
391 }
392
393 fn evidence(&self) -> &[Self::Evidence] {
394 &self.evidence
395 }
396
397 fn credential_schemas(&self) -> &[Self::Schema] {
398 &self.credential_schema
399 }
400}
401
402impl<
403 Subject,
404 RequiredContext,
405 RequiredType,
406 Issuer,
407 Status,
408 Evidence,
409 Schema,
410 RefreshService,
411 TermsOfUse,
412 ExtraProperties,
413 > ssi_json_ld::Expandable
414 for SpecializedJsonCredential<
415 Subject,
416 RequiredContext,
417 RequiredType,
418 Issuer,
419 Status,
420 Evidence,
421 Schema,
422 RefreshService,
423 TermsOfUse,
424 ExtraProperties,
425 >
426where
427 Subject: Serialize,
428 Issuer: Serialize,
429 Status: Serialize,
430 Evidence: Serialize,
431 Schema: Serialize,
432 RefreshService: Serialize,
433 TermsOfUse: Serialize,
434 ExtraProperties: Serialize,
435{
436 type Error = JsonLdError;
437
438 type Expanded<I, V>
439 = ssi_json_ld::ExpandedDocument<V::Iri, V::BlankId>
440 where
441 I: Interpretation,
442 V: VocabularyMut,
443 V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
444 V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
445
446 async fn expand_with<I, V>(
447 &self,
448 ld: &mut LdEnvironment<V, I>,
449 loader: &impl Loader,
450 ) -> Result<Self::Expanded<I, V>, Self::Error>
451 where
452 I: Interpretation,
453 V: VocabularyMut,
454 V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
455 V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
456 {
457 let json = ssi_json_ld::CompactJsonLd(json_syntax::to_value(self).unwrap());
458 json.expand_with(ld, loader).await
459 }
460}
461
462#[cfg(test)]
463mod tests {
464 use ssi_json_ld::{json_ld, ContextLoader, Expandable};
465
466 use super::*;
467
468 #[async_std::test]
469 async fn reject_undefined_type() {
470 let input: JsonCredential = serde_json::from_value(serde_json::json!({
471 "@context": [
472 "https://www.w3.org/2018/credentials/v1",
473 { "@vocab": null }
474 ],
475 "type": [
476 "VerifiableCredential",
477 "ExampleTestCredential"
478 ],
479 "issuer": "did:example:issuer",
480 "credentialSubject": {
481 "id": "did:example:subject"
482 }
483 }))
484 .unwrap();
485 match input.expand(&ContextLoader::default()).await.unwrap_err() {
486 JsonLdError::Expansion(json_ld::expansion::Error::InvalidTypeValue) => (),
487 e => panic!("{:?}", e),
488 }
489 }
490}