ssi_vc/v2/syntax/
credential.rs

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
24/// JSON Credential, without required context nor type.
25///
26/// If you care about required context and/or type, or want to customize other
27/// aspects of the credential, use the [`SpecializedJsonCredential`] type
28/// directly.
29pub type JsonCredential<S = NonEmptyObject> = SpecializedJsonCredential<S>;
30
31/// Specialized JSON Credential with custom types for each component.
32///
33/// If you don't care about the type of each component, you can use the
34/// [`JsonCredential`] type alias instead.
35#[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    /// JSON-LD context.
53    #[serde(rename = "@context")]
54    pub context: Context<RequiredContext>,
55
56    /// Credential identifier.
57    #[serde(
58        default,
59        deserialize_with = "not_null",
60        skip_serializing_if = "Option::is_none"
61    )]
62    pub id: Option<UriBuf>,
63
64    /// Name.
65    #[serde(default, skip_serializing_if = "Option::is_none")]
66    pub name: Option<InternationalString>,
67
68    /// Credential description.
69    #[serde(default, skip_serializing_if = "Option::is_none")]
70    pub description: Option<InternationalString>,
71
72    /// Credential type.
73    #[serde(rename = "type")]
74    pub types: JsonCredentialTypes<RequiredType>,
75
76    /// Credential subjects.
77    #[serde(rename = "credentialSubject")]
78    #[serde(with = "non_empty_value_or_array")]
79    pub credential_subjects: NonEmptyVec<Subject>,
80
81    /// Issuer.
82    pub issuer: Issuer,
83
84    /// Issuance date.
85    #[serde(rename = "validFrom")]
86    #[serde(default, skip_serializing_if = "Option::is_none")]
87    pub valid_from: Option<Lexical<xsd_types::DateTimeStamp>>,
88
89    /// Expiration date.
90    #[serde(rename = "validUntil")]
91    #[serde(default, skip_serializing_if = "Option::is_none")]
92    pub valid_until: Option<Lexical<xsd_types::DateTimeStamp>>,
93
94    /// Credential status.
95    #[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    /// Terms of use.
104    #[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    /// Evidence.
113    #[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    /// Creates a new credential.
170    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}