ssi_vc/v1/syntax/
credential.rs

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
37/// JSON Credential, without required context nor type.
38///
39/// If you care about required context and/or type, or want to customize other
40/// aspects of the credential, use the [`SpecializedJsonCredential`] type
41/// directly.
42pub type JsonCredential<S = json_syntax::Object> = SpecializedJsonCredential<S>;
43
44/// Specialized JSON Credential with custom types for each component.
45///
46/// If you don't care about the type of each component, you can use the
47/// [`JsonCredential`] type alias instead.
48#[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    /// JSON-LD context.
66    #[serde(rename = "@context")]
67    pub context: Context<RequiredContext>,
68
69    /// Credential identifier.
70    #[serde(
71        default,
72        deserialize_with = "not_null",
73        skip_serializing_if = "Option::is_none"
74    )]
75    pub id: Option<UriBuf>,
76
77    /// Credential type.
78    #[serde(rename = "type")]
79    pub types: JsonCredentialTypes<RequiredType>,
80
81    /// Credential subjects.
82    #[serde(rename = "credentialSubject")]
83    #[serde(with = "non_empty_value_or_array")]
84    pub credential_subjects: NonEmptyVec<Subject>,
85
86    /// Issuer.
87    pub issuer: Issuer,
88
89    /// Issuance date.
90    ///
91    /// This property is required for validation.
92    #[serde(rename = "issuanceDate")]
93    pub issuance_date: Option<Lexical<xsd_types::DateTime>>,
94
95    /// Expiration date.
96    #[serde(rename = "expirationDate")]
97    #[serde(default, skip_serializing_if = "Option::is_none")]
98    pub expiration_date: Option<Lexical<xsd_types::DateTime>>,
99
100    /// Credential status.
101    #[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    /// Terms of use.
110    #[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    /// Evidence.
119    #[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    /// Creates a new credential.
176    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}