ssi_vc/syntax/
credential.rs

1use std::{borrow::Cow, hash::Hash};
2
3use iref::Uri;
4use rdf_types::VocabularyMut;
5use serde::{Deserialize, Serialize};
6use ssi_claims_core::{ClaimsValidity, DateTimeProvider, ValidateClaims};
7use ssi_json_ld::{JsonLdError, JsonLdNodeObject, JsonLdObject, JsonLdTypes, Loader};
8use ssi_rdf::{Interpretation, LdEnvironment, LinkedDataResource, LinkedDataSubject, Vocabulary};
9
10use super::{RequiredContextList, RequiredTypeSet};
11use crate::{v1, v2, MaybeIdentified};
12
13/// Any JSON credential using VCDM v1 or v2.
14///
15/// If you care about required context and/or type, use the
16/// [`AnySpecializedJsonCredential`] type directly.
17pub type AnyJsonCredential<S = json_syntax::Object> = AnySpecializedJsonCredential<S>;
18
19/// Any JSON credential using VCDM v1 or v2 with custom required contexts and
20/// types.
21///
22/// If you don't care about required context and/or type, you can use the
23/// [`AnyJsonCredential`] type alias instead.
24#[derive(Debug, Clone, Serialize, Deserialize)]
25#[serde(
26    untagged,
27    bound(
28        serialize = "S: Serialize",
29        deserialize = "S: Deserialize<'de>, C: RequiredContextList, T: RequiredTypeSet"
30    )
31)]
32pub enum AnySpecializedJsonCredential<S = json_syntax::Object, C = (), T = ()> {
33    V1(v1::syntax::SpecializedJsonCredential<S, C, T>),
34    V2(v2::syntax::SpecializedJsonCredential<S, C, T>),
35}
36
37impl<S, C, T> JsonLdObject for AnySpecializedJsonCredential<S, C, T> {
38    fn json_ld_context(&self) -> Option<Cow<ssi_json_ld::syntax::Context>> {
39        match self {
40            Self::V1(c) => c.json_ld_context(),
41            Self::V2(c) => c.json_ld_context(),
42        }
43    }
44}
45
46impl<S, C, T> JsonLdNodeObject for AnySpecializedJsonCredential<S, C, T> {
47    fn json_ld_type(&self) -> JsonLdTypes {
48        match self {
49            Self::V1(c) => c.json_ld_type(),
50            Self::V2(c) => c.json_ld_type(),
51        }
52    }
53}
54
55impl<S, C, T, E, P> ValidateClaims<E, P> for AnySpecializedJsonCredential<S, C, T>
56where
57    E: DateTimeProvider,
58{
59    fn validate_claims(&self, env: &E, proof: &P) -> ClaimsValidity {
60        match self {
61            Self::V1(c) => c.validate_claims(env, proof),
62            Self::V2(c) => c.validate_claims(env, proof),
63        }
64    }
65}
66
67impl<S, C, T> MaybeIdentified for AnySpecializedJsonCredential<S, C, T> {
68    fn id(&self) -> Option<&Uri> {
69        match self {
70            Self::V1(c) => c.id(),
71            Self::V2(c) => c.id(),
72        }
73    }
74}
75
76impl<S, C, T> ssi_json_ld::Expandable for AnySpecializedJsonCredential<S, C, T>
77where
78    S: Serialize,
79{
80    type Error = JsonLdError;
81
82    type Expanded<I: Interpretation, V: Vocabulary>
83        = ssi_json_ld::ExpandedDocument<V::Iri, V::BlankId>
84    where
85        I: Interpretation,
86        V: VocabularyMut,
87        V::Iri: LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
88        V::BlankId: LinkedDataResource<I, V> + LinkedDataSubject<I, V>;
89
90    #[allow(async_fn_in_trait)]
91    async fn expand_with<I, V>(
92        &self,
93        ld: &mut LdEnvironment<V, I>,
94        loader: &impl Loader,
95    ) -> Result<Self::Expanded<I, V>, Self::Error>
96    where
97        I: Interpretation,
98        V: VocabularyMut,
99        V::Iri: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
100        V::BlankId: Clone + Eq + Hash + LinkedDataResource<I, V> + LinkedDataSubject<I, V>,
101    {
102        let json = ssi_json_ld::CompactJsonLd(json_syntax::to_value(self).unwrap());
103        json.expand_with(ld, loader).await
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use ssi_json_ld::{json_ld, ContextLoader, Expandable};
110
111    use super::*;
112
113    #[async_std::test]
114    async fn reject_undefined_type_v2() {
115        let input: AnyJsonCredential = serde_json::from_value(serde_json::json!({
116            "@context": [
117                "https://www.w3.org/ns/credentials/v2",
118                { "@vocab": null }
119            ],
120            "type": [
121                "VerifiableCredential",
122                "ExampleTestCredential"
123            ],
124            "issuer": "did:example:issuer",
125            "credentialSubject": {
126                "id": "did:example:subject"
127            }
128        }))
129        .unwrap();
130        match input.expand(&ContextLoader::default()).await.unwrap_err() {
131            JsonLdError::Expansion(json_ld::expansion::Error::InvalidTypeValue) => (),
132            e => panic!("{:?}", e),
133        }
134    }
135
136    #[async_std::test]
137    async fn reject_undefined_type_v1() {
138        let input: AnyJsonCredential = serde_json::from_value(serde_json::json!({
139            "@context": [
140                "https://www.w3.org/2018/credentials/v1",
141                { "@vocab": null }
142            ],
143            "type": [
144                "VerifiableCredential",
145                "ExampleTestCredential"
146            ],
147            "issuer": "did:example:issuer",
148            "credentialSubject": {
149                "id": "did:example:subject"
150            }
151        }))
152        .unwrap();
153        match input.expand(&ContextLoader::default()).await.unwrap_err() {
154            JsonLdError::Expansion(json_ld::expansion::Error::InvalidTypeValue) => (),
155            e => panic!("{:?}", e),
156        }
157    }
158}