1use std::{borrow::Cow, collections::BTreeMap, hash::Hash};
2
3use super::{Context, InternationalString, RelatedResource};
4use crate::{
5 syntax::{
6 non_empty_value_or_array, not_null, value_or_array, IdOr, IdentifiedObject,
7 IdentifiedTypedObject, MaybeIdentifiedTypedObject, NonEmptyObject, NonEmptyVec,
8 RequiredContextList, RequiredTypeSet, TypedObject,
9 },
10 v2::data_model,
11 Identified, MaybeIdentified, Typed,
12};
13use iref::{Uri, UriBuf};
14use rdf_types::VocabularyMut;
15use serde::{Deserialize, Serialize};
16use ssi_claims_core::{ClaimsValidity, DateTimeProvider, ValidateClaims};
17use ssi_core::Lexical;
18use ssi_json_ld::{JsonLdError, JsonLdNodeObject, JsonLdObject, JsonLdTypes, Loader};
19use ssi_rdf::{Interpretation, LdEnvironment, LinkedDataResource, LinkedDataSubject};
20use xsd_types::DateTimeStamp;
21
22pub use crate::v1::syntax::{CredentialType, JsonCredentialTypes, VERIFIABLE_CREDENTIAL_TYPE};
23
24pub type JsonCredential<S = NonEmptyObject> = SpecializedJsonCredential<S>;
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
36#[serde(bound(
37 serialize = "Subject: Serialize, Issuer: Serialize, Status: Serialize, Evidence: Serialize, Schema: Serialize, RefreshService: Serialize, TermsOfUse: Serialize, ExtraProperties: Serialize",
38 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>"
39))]
40pub struct SpecializedJsonCredential<
41 Subject = NonEmptyObject,
42 RequiredContext = (),
43 RequiredType = (),
44 Issuer = IdOr<IdentifiedObject>,
45 Status = MaybeIdentifiedTypedObject,
46 Evidence = MaybeIdentifiedTypedObject,
47 Schema = IdentifiedTypedObject,
48 RefreshService = TypedObject,
49 TermsOfUse = MaybeIdentifiedTypedObject,
50 ExtraProperties = BTreeMap<String, json_syntax::Value>,
51> {
52 #[serde(rename = "@context")]
54 pub context: Context<RequiredContext>,
55
56 #[serde(
58 default,
59 deserialize_with = "not_null",
60 skip_serializing_if = "Option::is_none"
61 )]
62 pub id: Option<UriBuf>,
63
64 #[serde(default, skip_serializing_if = "Option::is_none")]
66 pub name: Option<InternationalString>,
67
68 #[serde(default, skip_serializing_if = "Option::is_none")]
70 pub description: Option<InternationalString>,
71
72 #[serde(rename = "type")]
74 pub types: JsonCredentialTypes<RequiredType>,
75
76 #[serde(rename = "credentialSubject")]
78 #[serde(with = "non_empty_value_or_array")]
79 pub credential_subjects: NonEmptyVec<Subject>,
80
81 pub issuer: Issuer,
83
84 #[serde(rename = "validFrom")]
86 #[serde(default, skip_serializing_if = "Option::is_none")]
87 pub valid_from: Option<Lexical<xsd_types::DateTimeStamp>>,
88
89 #[serde(rename = "validUntil")]
91 #[serde(default, skip_serializing_if = "Option::is_none")]
92 pub valid_until: Option<Lexical<xsd_types::DateTimeStamp>>,
93
94 #[serde(rename = "credentialStatus")]
96 #[serde(
97 with = "value_or_array",
98 default,
99 skip_serializing_if = "Vec::is_empty"
100 )]
101 pub credential_status: Vec<Status>,
102
103 #[serde(rename = "termsOfUse")]
105 #[serde(
106 with = "value_or_array",
107 default,
108 skip_serializing_if = "Vec::is_empty"
109 )]
110 pub terms_of_use: Vec<TermsOfUse>,
111
112 #[serde(
114 with = "value_or_array",
115 default,
116 skip_serializing_if = "Vec::is_empty"
117 )]
118 pub evidence: Vec<Evidence>,
119
120 #[serde(rename = "credentialSchema")]
121 #[serde(
122 with = "value_or_array",
123 default,
124 skip_serializing_if = "Vec::is_empty"
125 )]
126 pub credential_schema: Vec<Schema>,
127
128 #[serde(rename = "refreshService")]
129 #[serde(
130 with = "value_or_array",
131 default,
132 skip_serializing_if = "Vec::is_empty"
133 )]
134 pub refresh_services: Vec<RefreshService>,
135
136 #[serde(flatten)]
137 pub extra_properties: ExtraProperties,
138}
139
140impl<
141 Subject,
142 RequiredContext,
143 RequiredType,
144 Issuer,
145 Status,
146 Evidence,
147 Schema,
148 RefreshService,
149 TermsOfUse,
150 ExtraProperties,
151 >
152 SpecializedJsonCredential<
153 Subject,
154 RequiredContext,
155 RequiredType,
156 Issuer,
157 Status,
158 Evidence,
159 Schema,
160 RefreshService,
161 TermsOfUse,
162 ExtraProperties,
163 >
164where
165 RequiredContext: RequiredContextList,
166 RequiredType: RequiredTypeSet,
167 ExtraProperties: Default,
168{
169 pub fn new(
171 id: Option<UriBuf>,
172 issuer: Issuer,
173 credential_subjects: NonEmptyVec<Subject>,
174 ) -> Self {
175 Self {
176 context: Context::default(),
177 id,
178 types: JsonCredentialTypes::default(),
179 name: None,
180 description: None,
181 issuer,
182 credential_subjects,
183 valid_from: None,
184 valid_until: None,
185 credential_status: Vec::new(),
186 terms_of_use: Vec::new(),
187 evidence: Vec::new(),
188 credential_schema: Vec::new(),
189 refresh_services: Vec::new(),
190 extra_properties: ExtraProperties::default(),
191 }
192 }
193}
194
195impl<
196 Subject,
197 RequiredContext,
198 RequiredType,
199 Issuer,
200 Status,
201 Evidence,
202 Schema,
203 RefreshService,
204 TermsOfUse,
205 ExtraProperties,
206 > JsonLdObject
207 for SpecializedJsonCredential<
208 Subject,
209 RequiredContext,
210 RequiredType,
211 Issuer,
212 Status,
213 Evidence,
214 Schema,
215 RefreshService,
216 TermsOfUse,
217 ExtraProperties,
218 >
219{
220 fn json_ld_context(&self) -> Option<Cow<ssi_json_ld::syntax::Context>> {
221 Some(Cow::Borrowed(self.context.as_ref()))
222 }
223}
224
225impl<
226 Subject,
227 RequiredContext,
228 RequiredType,
229 Issuer,
230 Status,
231 Evidence,
232 Schema,
233 RefreshService,
234 TermsOfUse,
235 ExtraProperties,
236 > JsonLdNodeObject
237 for SpecializedJsonCredential<
238 Subject,
239 RequiredContext,
240 RequiredType,
241 Issuer,
242 Status,
243 Evidence,
244 Schema,
245 RefreshService,
246 TermsOfUse,
247 ExtraProperties,
248 >
249{
250 fn json_ld_type(&self) -> JsonLdTypes {
251 self.types.to_json_ld_types()
252 }
253}
254
255impl<
256 Subject,
257 RequiredContext,
258 RequiredType,
259 Issuer: Identified,
260 Status: MaybeIdentified + Typed,
261 Evidence: MaybeIdentified + Typed,
262 Schema: Identified + Typed,
263 RefreshService: Typed,
264 TermsOfUse: MaybeIdentified + Typed,
265 ExtraProperties,
266 E,
267 P,
268 > ValidateClaims<E, P>
269 for SpecializedJsonCredential<
270 Subject,
271 RequiredContext,
272 RequiredType,
273 Issuer,
274 Status,
275 Evidence,
276 Schema,
277 RefreshService,
278 TermsOfUse,
279 ExtraProperties,
280 >
281where
282 E: DateTimeProvider,
283{
284 fn validate_claims(&self, env: &E, _proof: &P) -> ClaimsValidity {
285 crate::v2::Credential::validate_credential(self, env)
286 }
287}
288
289impl<
290 Subject,
291 RequiredContext,
292 RequiredType,
293 Issuer,
294 Status,
295 Evidence,
296 Schema,
297 RefreshService,
298 TermsOfUse,
299 ExtraProperties,
300 > crate::MaybeIdentified
301 for SpecializedJsonCredential<
302 Subject,
303 RequiredContext,
304 RequiredType,
305 Issuer,
306 Status,
307 Evidence,
308 Schema,
309 RefreshService,
310 TermsOfUse,
311 ExtraProperties,
312 >
313{
314 fn id(&self) -> Option<&Uri> {
315 self.id.as_deref()
316 }
317}
318
319impl<
320 Subject,
321 RequiredContext,
322 RequiredType,
323 Issuer: Identified,
324 Status: MaybeIdentified + Typed,
325 Evidence: MaybeIdentified + Typed,
326 Schema: Identified + Typed,
327 RefreshService: Typed,
328 TermsOfUse: MaybeIdentified + Typed,
329 ExtraProperties,
330 > crate::v2::Credential
331 for SpecializedJsonCredential<
332 Subject,
333 RequiredContext,
334 RequiredType,
335 Issuer,
336 Status,
337 Evidence,
338 Schema,
339 RefreshService,
340 TermsOfUse,
341 ExtraProperties,
342 >
343{
344 type Subject = Subject;
345 type Issuer = Issuer;
346 type Status = Status;
347 type RefreshService = RefreshService;
348 type TermsOfUse = TermsOfUse;
349 type Evidence = Evidence;
350 type Schema = Schema;
351 type RelatedResource = RelatedResource;
352
353 fn additional_types(&self) -> &[String] {
354 self.types.additional_types()
355 }
356
357 fn name(&self) -> Option<impl data_model::AnyInternationalString> {
358 self.name.as_ref()
359 }
360
361 fn description(&self) -> Option<impl data_model::AnyInternationalString> {
362 self.description.as_ref()
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 valid_from(&self) -> Option<DateTimeStamp> {
374 self.valid_from.as_ref().map(Lexical::to_value)
375 }
376
377 fn valid_until(&self) -> Option<DateTimeStamp> {
378 self.valid_until.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/ns/credentials/v2",
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}