ssi_vc/syntax/
context.rs

1use std::{borrow::Borrow, marker::PhantomData};
2
3use educe::Educe;
4use iref::{Iri, IriBuf, IriRef};
5use serde::{Deserialize, Serialize};
6use ssi_json_ld::syntax::ContextEntry;
7
8/// JSON-LD context.
9///
10/// This type represents the value of the `@context` property.
11///
12/// It is an ordered set where the first item is a URI given by the `V` type
13/// parameter implementing [`RequiredContext`], followed by a list of more
14/// required context given by the type parameter `T`, implementing
15/// [`RequiredContextList`].
16#[derive(Educe, Serialize)] // FIXME serializing a single entry as a string breaks Tezos JCS cryptosuite.
17#[educe(Debug, Clone)]
18#[serde(transparent, bound = "")]
19pub struct Context<V, T = ()>(ssi_json_ld::syntax::Context, PhantomData<(V, T)>);
20
21impl<V: RequiredContext, T: RequiredContextList> Default for Context<V, T> {
22    fn default() -> Self {
23        Self(
24            ssi_json_ld::syntax::Context::Many(
25                std::iter::once(ContextEntry::IriRef(V::CONTEXT_IRI.as_iri_ref().to_owned()))
26                    .chain(
27                        T::CONTEXT_IRIS
28                            .iter()
29                            .map(|&i| ContextEntry::IriRef(i.as_iri_ref().to_owned())),
30                    )
31                    .collect(),
32            ),
33            PhantomData,
34        )
35    }
36}
37
38impl<V, T> Context<V, T> {
39    /// Checks if this context contains the given entry.
40    pub fn contains(&self, entry: &ContextEntry) -> bool {
41        match &self.0 {
42            ssi_json_ld::syntax::Context::One(e) => e == entry,
43            ssi_json_ld::syntax::Context::Many(entries) => entries.iter().any(|e| e == entry),
44        }
45    }
46
47    /// Checks if this context contains the given IRI entry.
48    pub fn contains_iri(&self, iri: &Iri) -> bool {
49        self.contains_iri_ref(iri.as_iri_ref())
50    }
51
52    /// Checks if this context contains the given IRI reference entry.
53    pub fn contains_iri_ref(&self, iri_ref: &IriRef) -> bool {
54        match &self.0 {
55            ssi_json_ld::syntax::Context::One(ContextEntry::IriRef(i)) => i == iri_ref,
56            ssi_json_ld::syntax::Context::One(_) => false,
57            ssi_json_ld::syntax::Context::Many(entries) => entries
58                .iter()
59                .any(|e| matches!(e, ContextEntry::IriRef(i) if i == iri_ref)),
60        }
61    }
62
63    /// Returns an iterator over the context entries.
64    pub fn iter(&self) -> std::slice::Iter<ContextEntry> {
65        self.0.iter()
66    }
67
68    /// Inserts the given entry in the context.
69    ///
70    /// Appends the entry at the end unless it is already present.
71    ///
72    /// Returns `true` if the entry was not already present.
73    /// Returns `false` if the entry was already present in the context, in
74    /// which case it is not added a second time.
75    pub fn insert(&mut self, entry: ContextEntry) -> bool {
76        if self.contains(&entry) {
77            false
78        } else {
79            let mut entries = match std::mem::take(&mut self.0) {
80                ssi_json_ld::syntax::Context::One(e) => vec![e],
81                ssi_json_ld::syntax::Context::Many(entries) => entries,
82            };
83
84            entries.push(entry);
85            self.0 = ssi_json_ld::syntax::Context::Many(entries);
86            true
87        }
88    }
89}
90
91impl<'a, V, T> IntoIterator for &'a Context<V, T> {
92    type Item = &'a ContextEntry;
93    type IntoIter = std::slice::Iter<'a, ContextEntry>;
94
95    fn into_iter(self) -> Self::IntoIter {
96        self.iter()
97    }
98}
99
100impl<V, T> IntoIterator for Context<V, T> {
101    type Item = ContextEntry;
102    type IntoIter = ssi_json_ld::syntax::context::IntoIter;
103
104    fn into_iter(self) -> Self::IntoIter {
105        self.0.into_iter()
106    }
107}
108
109impl<V, T> Extend<ContextEntry> for Context<V, T> {
110    fn extend<E: IntoIterator<Item = ContextEntry>>(&mut self, iter: E) {
111        for entry in iter {
112            self.insert(entry);
113        }
114    }
115}
116
117impl<V, T> AsRef<ssi_json_ld::syntax::Context> for Context<V, T> {
118    fn as_ref(&self) -> &ssi_json_ld::syntax::Context {
119        &self.0
120    }
121}
122
123impl<V, T> Borrow<ssi_json_ld::syntax::Context> for Context<V, T> {
124    fn borrow(&self) -> &ssi_json_ld::syntax::Context {
125        &self.0
126    }
127}
128
129impl<V, T> From<Context<V, T>> for ssi_json_ld::syntax::Context {
130    fn from(value: Context<V, T>) -> Self {
131        value.0
132    }
133}
134
135/// Error that can occur while converting an arbitrary JSON-LD context into a
136/// VCDM context.
137#[derive(Debug, thiserror::Error)]
138pub enum InvalidContext {
139    #[error("unexpected context entry (expected `{0}`)")]
140    UnexpectedContext(IriBuf, ContextEntry),
141
142    #[error("missing required context entry `{0}`")]
143    MissingRequiredContext(IriBuf),
144}
145
146impl<V: RequiredContext, T: RequiredContextList> TryFrom<ssi_json_ld::syntax::Context>
147    for Context<V, T>
148{
149    type Error = InvalidContext;
150
151    fn try_from(value: ssi_json_ld::syntax::Context) -> Result<Self, Self::Error> {
152        let entries = match value {
153            ssi_json_ld::syntax::Context::One(entry) => vec![entry],
154            ssi_json_ld::syntax::Context::Many(entries) => entries,
155        };
156
157        match entries.split_first() {
158            Some((ContextEntry::IriRef(iri), rest)) if iri == V::CONTEXT_IRI => {
159                let mut expected = T::CONTEXT_IRIS.iter();
160                let mut rest = rest.iter();
161                loop {
162                    match (expected.next(), rest.next()) {
163                        (Some(e), Some(ContextEntry::IriRef(f))) if *e == f => (),
164                        (Some(e), Some(f)) => {
165                            break Err(InvalidContext::UnexpectedContext(
166                                (*e).to_owned(),
167                                f.clone(),
168                            ))
169                        }
170                        (Some(e), None) => {
171                            break Err(InvalidContext::MissingRequiredContext((*e).to_owned()))
172                        }
173                        _ => {
174                            break Ok(Self(
175                                ssi_json_ld::syntax::Context::Many(entries),
176                                PhantomData,
177                            ))
178                        }
179                    }
180                }
181            }
182            Some((other, _)) => Err(InvalidContext::UnexpectedContext(
183                V::CONTEXT_IRI.to_owned(),
184                other.clone(),
185            )),
186            None => Err(InvalidContext::MissingRequiredContext(
187                V::CONTEXT_IRI.to_owned(),
188            )),
189        }
190    }
191}
192
193impl<'de, V: RequiredContext, T: RequiredContextList> Deserialize<'de> for Context<V, T> {
194    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
195    where
196        D: serde::Deserializer<'de>,
197    {
198        let context = ssi_json_ld::syntax::Context::deserialize(deserializer)?;
199        context.try_into().map_err(serde::de::Error::custom)
200    }
201}
202
203pub trait RequiredContext {
204    const CONTEXT_IRI: &'static Iri;
205}
206
207/// Set of required contexts.
208pub trait RequiredContextList {
209    const CONTEXT_IRIS: &'static [&'static Iri];
210}
211
212impl RequiredContextList for () {
213    const CONTEXT_IRIS: &'static [&'static Iri] = &[];
214}
215
216impl<T: RequiredContext> RequiredContextList for T {
217    const CONTEXT_IRIS: &'static [&'static Iri] = &[T::CONTEXT_IRI];
218}
219
220macro_rules! required_context_tuple {
221    ($($t:ident: $n:tt),*) => {
222        impl<$($t : RequiredContext),*> RequiredContextList for ($($t),*,) {
223            const CONTEXT_IRIS: &'static [&'static Iri] = &[
224                $($t::CONTEXT_IRI),*
225            ];
226        }
227    };
228}
229
230required_context_tuple!(T0: 0);
231required_context_tuple!(T0: 0, T1: 1);
232required_context_tuple!(T0: 0, T1: 1, T2: 2);
233required_context_tuple!(T0: 0, T1: 1, T2: 2, T3: 3);