ssi_json_ld/
contexts.rs

1use iref::{Iri, IriBuf};
2use json_ld::{
3    syntax::TryFromJson, LoadError, Loader, RemoteContext, RemoteContextReference, RemoteDocument,
4};
5use json_syntax::Parse;
6use static_iref::iri;
7use std::collections::HashMap;
8use thiserror::Error;
9
10pub const CREDENTIALS_V1_CONTEXT: &Iri = iri!("https://www.w3.org/2018/credentials/v1");
11pub const CREDENTIALS_V2_CONTEXT: &Iri = iri!("https://www.w3.org/ns/credentials/v2");
12pub const CREDENTIALS_EXAMPLES_V1_CONTEXT: &Iri =
13    iri!("https://www.w3.org/2018/credentials/examples/v1");
14pub const CREDENTIALS_EXAMPLES_V2_CONTEXT: &Iri =
15    iri!("https://www.w3.org/ns/credentials/examples/v2");
16pub const ODRL_CONTEXT: &Iri = iri!("https://www.w3.org/ns/odrl.jsonld");
17pub const SECURITY_V1_CONTEXT: &Iri = iri!("https://w3id.org/security/v1");
18pub const SECURITY_V2_CONTEXT: &Iri = iri!("https://w3id.org/security/v2");
19pub const SCHEMA_ORG_CONTEXT: &Iri = iri!("https://schema.org/");
20pub const DID_V1_CONTEXT: &Iri = iri!("https://www.w3.org/ns/did/v1");
21pub const DID_V1_CONTEXT_NO_WWW: &Iri = iri!("https://w3.org/ns/did/v1");
22pub const W3ID_DID_V1_CONTEXT: &Iri = iri!("https://w3id.org/did/v1");
23pub const DID_RESOLUTION_V1_CONTEXT: &Iri = iri!("https://w3id.org/did-resolution/v1");
24pub const DIF_ESRS2020_CONTEXT: &Iri = iri!("https://identity.foundation/EcdsaSecp256k1RecoverySignature2020/lds-ecdsa-secp256k1-recovery2020-0.0.jsonld");
25#[deprecated(note = "Use W3ID_ESRS2020_V2_CONTEXT instead")]
26pub const ESRS2020_EXTRA_CONTEXT: &Iri =
27    iri!("https://demo.spruceid.com/EcdsaSecp256k1RecoverySignature2020/esrs2020-extra-0.0.jsonld");
28pub const W3ID_ESRS2020_V2_CONTEXT: &Iri =
29    iri!("https://w3id.org/security/suites/secp256k1recovery-2020/v2");
30pub const LDS_JWS2020_V1_CONTEXT: &Iri =
31    iri!("https://w3c-ccg.github.io/lds-jws2020/contexts/lds-jws2020-v1.json");
32pub const W3ID_JWS2020_V1_CONTEXT: &Iri = iri!("https://w3id.org/security/suites/jws-2020/v1");
33pub const W3ID_ED2020_V1_CONTEXT: &Iri = iri!("https://w3id.org/security/suites/ed25519-2020/v1");
34pub const W3ID_MULTIKEY_V1_CONTEXT: &Iri = iri!("https://w3id.org/security/multikey/v1");
35pub const W3ID_DATA_INTEGRITY_V1_CONTEXT: &Iri =
36    iri!("https://w3id.org/security/data-integrity/v1");
37pub const W3ID_DATA_INTEGRITY_V2_CONTEXT: &Iri =
38    iri!("https://w3id.org/security/data-integrity/v2");
39pub const BLOCKCHAIN2021_V1_CONTEXT: &Iri =
40    iri!("https://w3id.org/security/suites/blockchain-2021/v1");
41pub const CITIZENSHIP_V1_CONTEXT: &Iri = iri!("https://w3id.org/citizenship/v1");
42pub const VACCINATION_V1_CONTEXT: &Iri = iri!("https://w3id.org/vaccination/v1");
43pub const TRACEABILITY_CONTEXT: &Iri = iri!("https://w3id.org/traceability/v1");
44pub const REVOCATION_LIST_2020_V1_CONTEXT: &Iri =
45    iri!("https://w3id.org/vc-revocation-list-2020/v1");
46pub const BBS_V1_CONTEXT: &Iri = iri!("https://w3id.org/security/bbs/v1");
47pub const STATUS_LIST_2021_V1_CONTEXT: &Iri = iri!("https://w3id.org/vc/status-list/2021/v1");
48pub const EIP712SIG_V0_1_CONTEXT: &Iri =
49    iri!("https://demo.spruceid.com/ld/eip712sig-2021/v0.1.jsonld");
50pub const EIP712SIG_V1_CONTEXT: &Iri = iri!("https://w3id.org/security/suites/eip712sig-2021/v1");
51pub const PRESENTATION_SUBMISSION_V1_CONTEXT: &Iri =
52    iri!("https://identity.foundation/presentation-exchange/submission/v1");
53pub const VDL_V1_CONTEXT: &Iri = iri!("https://w3id.org/vdl/v1");
54pub const WALLET_V1_CONTEXT: &Iri = iri!("https://w3id.org/wallet/v1");
55pub const ZCAP_V1_CONTEXT: &Iri = iri!("https://w3id.org/zcap/v1");
56pub const CACAO_ZCAP_V1_CONTEXT: &Iri =
57    iri!("https://demo.didkit.dev/2022/cacao-zcap/contexts/v1.json");
58pub const JFF_VC_EDU_PLUGFEST_2022_CONTEXT: &Iri =
59    iri!("https://w3c-ccg.github.io/vc-ed/plugfest-1-2022/jff-vc-edu-plugfest-1-context.json");
60pub const DID_CONFIGURATION_V0_0_CONTEXT: &Iri =
61    iri!("https://identity.foundation/.well-known/contexts/did-configuration-v0.0.jsonld");
62pub const DID_CONFIGURATION_V1_CONTEXT: &Iri =
63    iri!("https://identity.foundation/.well-known/did-configuration/v1");
64pub const JFF_VC_EDU_PLUGFEST_2022_2_CONTEXT: &Iri =
65    iri!("https://purl.imsglobal.org/spec/ob/v3p0/context.json");
66pub const LINKED_VP_V1_CONTEXT: &Iri = iri!("https://identity.foundation/linked-vp/contexts/v1");
67
68/// Load a remote context from its static definition.
69fn load_static_context(iri: &Iri, content: &str) -> RemoteDocument {
70    RemoteDocument::new(
71        Some(iri.to_owned()),
72        Some("application/ld+json".parse().unwrap()),
73        json_syntax::Value::parse_str(content).unwrap().0,
74    )
75}
76
77lazy_static::lazy_static! {
78    pub static ref CREDENTIALS_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
79        CREDENTIALS_V1_CONTEXT,
80        ssi_contexts::CREDENTIALS_V1
81    );
82    pub static ref CREDENTIALS_V2_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
83        CREDENTIALS_V2_CONTEXT,
84        ssi_contexts::CREDENTIALS_V2
85    );
86    pub static ref CREDENTIALS_EXAMPLES_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
87        CREDENTIALS_EXAMPLES_V1_CONTEXT,
88        ssi_contexts::CREDENTIALS_EXAMPLES_V1
89    );
90    pub static ref CREDENTIALS_EXAMPLES_V2_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
91        CREDENTIALS_EXAMPLES_V2_CONTEXT,
92        ssi_contexts::CREDENTIALS_EXAMPLES_V2
93    );
94    pub static ref ODRL_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
95        ODRL_CONTEXT,
96        ssi_contexts::ODRL
97    );
98    pub static ref SCHEMA_ORG_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
99        SCHEMA_ORG_CONTEXT,
100        ssi_contexts::SCHEMA_ORG
101    );
102    pub static ref SECURITY_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
103        SECURITY_V1_CONTEXT,
104        ssi_contexts::SECURITY_V1
105    );
106    pub static ref SECURITY_V2_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
107        SECURITY_V2_CONTEXT,
108        ssi_contexts::SECURITY_V2
109    );
110    pub static ref DID_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
111        DID_V1_CONTEXT,
112        ssi_contexts::DID_V1
113    );
114    pub static ref DID_RESOLUTION_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
115        DID_RESOLUTION_V1_CONTEXT,
116        ssi_contexts::DID_RESOLUTION_V1
117    );
118    /// Deprecated in favor of W3ID_ESRS2020_V2_CONTEXT_DOCUMENT
119    pub static ref DIF_ESRS2020_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
120        #[allow(deprecated)]
121        DIF_ESRS2020_CONTEXT,
122        #[allow(deprecated)]
123        ssi_contexts::DIF_ESRS2020
124    );
125    pub static ref W3ID_ESRS2020_V2_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
126        W3ID_ESRS2020_V2_CONTEXT,
127        ssi_contexts::W3ID_ESRS2020_V2
128    );
129    /// Deprecated in favor of W3ID_ESRS2020_V2_CONTEXT_DOCUMENT
130    pub static ref ESRS2020_EXTRA_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
131        #[allow(deprecated)]
132        ESRS2020_EXTRA_CONTEXT,
133        #[allow(deprecated)]
134        ssi_contexts::ESRS2020_EXTRA
135    );
136    pub static ref LDS_JWS2020_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
137        LDS_JWS2020_V1_CONTEXT,
138        ssi_contexts::LDS_JWS2020_V1
139    );
140    pub static ref W3ID_JWS2020_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
141        W3ID_JWS2020_V1_CONTEXT,
142        ssi_contexts::W3ID_JWS2020_V1
143    );
144    pub static ref W3ID_ED2020_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
145        W3ID_ED2020_V1_CONTEXT,
146        ssi_contexts::W3ID_ED2020_V1
147    );
148    pub static ref W3ID_MULTIKEY_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
149        W3ID_MULTIKEY_V1_CONTEXT,
150        ssi_contexts::W3ID_MULTIKEY_V1
151    );
152    pub static ref W3ID_DATA_INTEGRITY_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
153        W3ID_DATA_INTEGRITY_V1_CONTEXT,
154        ssi_contexts::W3ID_DATA_INTEGRITY_V1
155    );
156    pub static ref W3ID_DATA_INTEGRITY_V2_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
157        W3ID_DATA_INTEGRITY_V2_CONTEXT,
158        ssi_contexts::W3ID_DATA_INTEGRITY_V2
159    );
160    pub static ref BLOCKCHAIN2021_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
161        BLOCKCHAIN2021_V1_CONTEXT,
162        ssi_contexts::BLOCKCHAIN2021_V1
163    );
164    pub static ref CITIZENSHIP_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
165        CITIZENSHIP_V1_CONTEXT,
166        ssi_contexts::CITIZENSHIP_V1
167    );
168    pub static ref VACCINATION_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
169        VACCINATION_V1_CONTEXT,
170        ssi_contexts::VACCINATION_V1
171    );
172    pub static ref TRACEABILITY_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
173        TRACEABILITY_CONTEXT,
174        ssi_contexts::TRACEABILITY_V1
175    );
176    pub static ref REVOCATION_LIST_2020_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
177        REVOCATION_LIST_2020_V1_CONTEXT,
178        ssi_contexts::REVOCATION_LIST_2020_V1
179    );
180    pub static ref STATUS_LIST_2021_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
181        STATUS_LIST_2021_V1_CONTEXT,
182        ssi_contexts::STATUS_LIST_2021_V1
183    );
184    pub static ref EIP712SIG_V0_1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
185        EIP712SIG_V0_1_CONTEXT,
186        ssi_contexts::EIP712SIG_V0_1
187    );
188    pub static ref BBS_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
189        BBS_V1_CONTEXT,
190        ssi_contexts::BBS_V1
191    );
192    pub static ref EIP712SIG_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
193        EIP712SIG_V1_CONTEXT,
194        ssi_contexts::EIP712SIG_V1
195    );
196    pub static ref PRESENTATION_SUBMISSION_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
197        PRESENTATION_SUBMISSION_V1_CONTEXT,
198        ssi_contexts::PRESENTATION_SUBMISSION_V1
199    );
200    pub static ref VDL_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
201        VDL_V1_CONTEXT,
202        ssi_contexts::VDL_V1
203    );
204    pub static ref WALLET_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
205        WALLET_V1_CONTEXT,
206        ssi_contexts::WALLET_V1
207    );
208    pub static ref ZCAP_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
209        ZCAP_V1_CONTEXT,
210        ssi_contexts::ZCAP_V1
211    );
212    pub static ref CACAO_ZCAP_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
213        CACAO_ZCAP_V1_CONTEXT,
214        ssi_contexts::CACAO_ZCAP_V1
215    );
216    pub static ref JFF_VC_EDU_PLUGFEST_2022_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
217        JFF_VC_EDU_PLUGFEST_2022_CONTEXT,
218        ssi_contexts::JFF_VC_EDU_PLUGFEST_2022
219    );
220    pub static ref DID_CONFIGURATION_V0_0_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
221        DID_CONFIGURATION_V0_0_CONTEXT,
222        ssi_contexts::DID_CONFIGURATION_V0_0
223    );
224    pub static ref DID_CONFIGURATION_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
225        DID_CONFIGURATION_V1_CONTEXT,
226        ssi_contexts::DID_CONFIGURATION_V1
227    );
228    pub static ref JFF_VC_EDU_PLUGFEST_2022_2_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
229        JFF_VC_EDU_PLUGFEST_2022_2_CONTEXT,
230        ssi_contexts::JFF_VC_EDU_PLUGFEST_2022_2
231    );
232    pub static ref LINKED_VP_V1_CONTEXT_DOCUMENT: RemoteDocument = load_static_context(
233        LINKED_VP_V1_CONTEXT,
234        ssi_contexts::LINKED_VP_V1
235    );
236}
237
238macro_rules! iri_match {
239    { match $input:ident { $($(#[$meta:meta])? $($id:ident)|* => $e:expr,)* _ as $default:ident => $de:expr } } => {
240        match $input {
241            $($(#[$meta])? $input if $($input == $id)||* => $e),*
242            $default => $de
243        }
244    };
245}
246
247/// Error raised when an unknown context is loaded with [`StaticLoader`] or
248/// [`ContextLoader`].
249#[derive(thiserror::Error, Debug)]
250#[error("Unknown context")]
251pub struct UnknownContext;
252
253#[derive(Clone)]
254pub struct StaticLoader;
255
256impl Loader for StaticLoader {
257    async fn load(&self, url: &Iri) -> json_ld::LoadingResult {
258        iri_match! {
259            match url {
260                CREDENTIALS_V1_CONTEXT => Ok(CREDENTIALS_V1_CONTEXT_DOCUMENT.clone()),
261                CREDENTIALS_V2_CONTEXT => Ok(CREDENTIALS_V2_CONTEXT_DOCUMENT.clone()),
262                CREDENTIALS_EXAMPLES_V1_CONTEXT => {
263                    Ok(CREDENTIALS_EXAMPLES_V1_CONTEXT_DOCUMENT.clone())
264                },
265                CREDENTIALS_EXAMPLES_V2_CONTEXT => {
266                    Ok(CREDENTIALS_EXAMPLES_V2_CONTEXT_DOCUMENT.clone())
267                },
268                ODRL_CONTEXT => Ok(ODRL_CONTEXT_DOCUMENT.clone()),
269                SECURITY_V1_CONTEXT => Ok(SECURITY_V1_CONTEXT_DOCUMENT.clone()),
270                SECURITY_V2_CONTEXT => Ok(SECURITY_V2_CONTEXT_DOCUMENT.clone()),
271                SCHEMA_ORG_CONTEXT => Ok(SCHEMA_ORG_CONTEXT_DOCUMENT.clone()),
272                DID_V1_CONTEXT | DID_V1_CONTEXT_NO_WWW | W3ID_DID_V1_CONTEXT => {
273                    Ok(DID_V1_CONTEXT_DOCUMENT.clone())
274                },
275                DID_RESOLUTION_V1_CONTEXT => Ok(DID_RESOLUTION_V1_CONTEXT_DOCUMENT.clone()),
276                #[allow(deprecated)]
277                DIF_ESRS2020_CONTEXT => Ok(DIF_ESRS2020_CONTEXT_DOCUMENT.clone()),
278                W3ID_ESRS2020_V2_CONTEXT => Ok(W3ID_ESRS2020_V2_CONTEXT_DOCUMENT.clone()),
279                #[allow(deprecated)]
280                ESRS2020_EXTRA_CONTEXT => Ok(ESRS2020_EXTRA_CONTEXT_DOCUMENT.clone()),
281                LDS_JWS2020_V1_CONTEXT => Ok(LDS_JWS2020_V1_CONTEXT_DOCUMENT.clone()),
282                W3ID_JWS2020_V1_CONTEXT => Ok(W3ID_JWS2020_V1_CONTEXT_DOCUMENT.clone()),
283                W3ID_ED2020_V1_CONTEXT => Ok(W3ID_ED2020_V1_CONTEXT_DOCUMENT.clone()),
284                W3ID_MULTIKEY_V1_CONTEXT => Ok(W3ID_MULTIKEY_V1_CONTEXT_DOCUMENT.clone()),
285                W3ID_DATA_INTEGRITY_V1_CONTEXT => Ok(W3ID_DATA_INTEGRITY_V1_CONTEXT_DOCUMENT.clone()),
286                W3ID_DATA_INTEGRITY_V2_CONTEXT => Ok(W3ID_DATA_INTEGRITY_V2_CONTEXT_DOCUMENT.clone()),
287                BLOCKCHAIN2021_V1_CONTEXT => Ok(BLOCKCHAIN2021_V1_CONTEXT_DOCUMENT.clone()),
288                CITIZENSHIP_V1_CONTEXT => Ok(CITIZENSHIP_V1_CONTEXT_DOCUMENT.clone()),
289                VACCINATION_V1_CONTEXT => Ok(VACCINATION_V1_CONTEXT_DOCUMENT.clone()),
290                TRACEABILITY_CONTEXT => Ok(TRACEABILITY_CONTEXT_DOCUMENT.clone()),
291                REVOCATION_LIST_2020_V1_CONTEXT => {
292                    Ok(REVOCATION_LIST_2020_V1_CONTEXT_DOCUMENT.clone())
293                },
294                STATUS_LIST_2021_V1_CONTEXT => Ok(STATUS_LIST_2021_V1_CONTEXT_DOCUMENT.clone()),
295                EIP712SIG_V0_1_CONTEXT => Ok(EIP712SIG_V0_1_CONTEXT_DOCUMENT.clone()),
296                BBS_V1_CONTEXT => Ok(BBS_V1_CONTEXT_DOCUMENT.clone()),
297                EIP712SIG_V1_CONTEXT => Ok(EIP712SIG_V1_CONTEXT_DOCUMENT.clone()),
298                PRESENTATION_SUBMISSION_V1_CONTEXT => {
299                    Ok(PRESENTATION_SUBMISSION_V1_CONTEXT_DOCUMENT.clone())
300                },
301                VDL_V1_CONTEXT => Ok(VDL_V1_CONTEXT_DOCUMENT.clone()),
302                WALLET_V1_CONTEXT => Ok(WALLET_V1_CONTEXT_DOCUMENT.clone()),
303                ZCAP_V1_CONTEXT => Ok(ZCAP_V1_CONTEXT_DOCUMENT.clone()),
304                CACAO_ZCAP_V1_CONTEXT => Ok(CACAO_ZCAP_V1_CONTEXT_DOCUMENT.clone()),
305                JFF_VC_EDU_PLUGFEST_2022_CONTEXT => {
306                    Ok(JFF_VC_EDU_PLUGFEST_2022_CONTEXT_DOCUMENT.clone())
307                },
308                DID_CONFIGURATION_V0_0_CONTEXT => {
309                    Ok(DID_CONFIGURATION_V0_0_CONTEXT_DOCUMENT.clone())
310                },
311                DID_CONFIGURATION_V1_CONTEXT => {
312                    Ok(DID_CONFIGURATION_V1_CONTEXT_DOCUMENT.clone())
313                },
314                JFF_VC_EDU_PLUGFEST_2022_2_CONTEXT => {
315                    Ok(JFF_VC_EDU_PLUGFEST_2022_2_CONTEXT_DOCUMENT.clone())
316                },
317                LINKED_VP_V1_CONTEXT => { Ok(LINKED_VP_V1_CONTEXT_DOCUMENT.clone()) },
318                _ as iri => Err(LoadError::new(iri.to_owned(), UnknownContext))
319            }
320        }
321    }
322}
323
324pub type ContextMap = HashMap<IriBuf, RemoteDocument>;
325
326#[derive(Clone)]
327pub struct ContextLoader {
328    // Specifies if StaticLoader is meant to be checked first.
329    static_loader: Option<StaticLoader>,
330
331    // This map holds the optional, additional context objects.  This is where any app-specific context
332    // objects would go.  The Arc<RwLock<_>> is necessary because json_ld::Loader trait unfortunately
333    // has a method that uses `&mut self`.
334    context_map: Option<ContextMap>,
335}
336
337impl std::fmt::Debug for ContextLoader {
338    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
339        f.debug_struct("ContextLoader").finish_non_exhaustive()
340    }
341}
342
343/// Error that can be raised by the [`ContextLoader::with_context_map_from`]
344/// constructor function.
345///
346/// This error is raised either if some input document is not JSON, or if it is
347/// bound to an invalid IRI.
348#[derive(Debug, Error)]
349pub enum FromContextMapError {
350    #[error(transparent)]
351    ParseError(#[from] json_ld::syntax::parse::Error),
352
353    #[error(transparent)]
354    InvalidIri(iref::InvalidIri<String>),
355}
356
357impl From<iref::InvalidIri<String>> for FromContextMapError {
358    fn from(e: iref::InvalidIri<String>) -> Self {
359        Self::InvalidIri(e)
360    }
361}
362
363impl ContextLoader {
364    /// Constructs an "empty" ContextLoader.
365    pub fn empty() -> Self {
366        Self {
367            static_loader: None,
368            context_map: None,
369        }
370    }
371    /// Using the builder pattern, the StaticLoader can be enabled so that contexts are checked
372    /// against it before being checked against context_map.
373    pub fn with_static_loader(mut self) -> Self {
374        self.static_loader = Some(StaticLoader);
375        self
376    }
377    /// Using the builder pattern, the map of additional contexts can be set.  These context objects
378    /// will be checked after StaticLoader (if it's specified).  preparsed_context_map should map
379    /// the context URLs to their JSON content.
380    pub fn with_context_map_from(
381        mut self,
382        preparsed_context_map: HashMap<String, String>,
383    ) -> Result<Self, FromContextMapError> {
384        let context_map = preparsed_context_map
385            .into_iter()
386            .map(
387                |(url, jsonld)| -> Result<(IriBuf, RemoteDocument), FromContextMapError> {
388                    let (doc, _) = json_syntax::Value::parse_str(&jsonld)?;
389                    let iri = IriBuf::new(url)?;
390                    let remote_doc = RemoteDocument::new(
391                        Some(iri.clone()),
392                        Some("application/ld+json".parse().unwrap()),
393                        doc,
394                    );
395                    Ok((iri, remote_doc))
396                },
397            )
398            .collect::<Result<HashMap<IriBuf, RemoteDocument>, FromContextMapError>>()?;
399        self.context_map = Some(context_map);
400        Ok(self)
401    }
402}
403
404/// The default ContextLoader only uses StaticLoader.
405impl std::default::Default for ContextLoader {
406    fn default() -> Self {
407        Self {
408            static_loader: Some(StaticLoader),
409            context_map: None,
410        }
411    }
412}
413
414impl Loader for ContextLoader {
415    async fn load(&self, url: &Iri) -> json_ld::LoadingResult {
416        let url = match &self.static_loader {
417            Some(static_loader) => {
418                match static_loader.load(url).await {
419                    Ok(x) => {
420                        // The url was present in `StaticLoader`.
421                        return Ok(x);
422                    }
423                    Err(_) => {
424                        // This is ok, the url just wasn't found in
425                        // `StaticLoader`. Fall through to
426                        // `self.context_map`.
427                        url
428                    }
429                }
430            }
431            None => url,
432        };
433
434        // If we fell through, then try `self.context_map`.
435        if let Some(context_map) = &self.context_map {
436            context_map
437                .get(url)
438                .cloned()
439                .ok_or_else(|| LoadError::new(url.to_owned(), UnknownContext))
440        } else {
441            Err(LoadError::new(url.to_owned(), UnknownContext))
442        }
443    }
444}
445
446#[derive(Debug, thiserror::Error)]
447pub enum ContextError {
448    #[error("Invalid JSON: {0}")]
449    InvalidJson(#[from] json_syntax::parse::Error),
450
451    #[error("Invalid JSON-LD context: {0}")]
452    InvalidContext(#[from] json_ld::syntax::context::InvalidContext),
453}
454
455/// Parse a JSON-LD context.
456pub fn parse_ld_context(content: &str) -> Result<RemoteContextReference, ContextError> {
457    let (json, _) = json_syntax::Value::parse_str(content)?;
458    let context = json_ld::syntax::Context::try_from_json(json)?;
459    Ok(RemoteContextReference::Loaded(RemoteContext::new(
460        None, None, context,
461    )))
462}
463
464#[cfg(test)]
465mod test {
466    use serde_json::json;
467
468    use super::*;
469
470    #[tokio::test]
471    async fn context_loader() {
472        let cl = ContextLoader::default().with_context_map_from([(
473            "https://w3id.org/age/v1".to_string(),
474            serde_json::to_string(&json!({
475              "@context": {
476                "@protected": "true",
477                "id": "@id",
478                "type": "@type",
479                "description": "https://schema.org/description",
480                "image": {
481                  "@id": "https://schema.org/image",
482                  "@type": "@id"
483                },
484                "name": "https://schema.org/name",
485                "overAge": {
486                  "@id": "https://w3id.org/age#overAge",
487                  "@type": "http://www.w3.org/2001/XMLSchema#positiveInteger"
488                },
489                "concealedIdToken": {
490                  "@id": "https://w3id.org/cit#concealedIdToken",
491                  "@type": "https://w3id.org/security#multibase"
492                },
493                "anchoredResource": {
494                  "@type": "@id",
495                  "@id": "https://w3id.org/security#anchoredResource"
496                },
497                "digestMultibase": {
498                  "@id": "https://w3id.org/security#digestMultibase",
499                  "@type": "https://w3id.org/security#multibase"
500                },
501                "PersonalPhotoCredential": "https://convenience.org/vocab#PersonalPhotoCredential",
502                "OverAgeTokenCredential": "https://w3id.org/age#OverAgeTokenCredential",
503                "VerifiableCredentialRefreshService2021": {
504                  "@id": "https://w3id.org/vc-refresh-service#VerifiableCredentialRefreshService2021",
505                  "@context": {
506                    "@protected": true,
507                    "url": {
508                      "@id": "https://schema.org/url",
509                      "@type": "@id"
510                    },
511                    "refreshToken": {
512                      "@id": "https://w3id.org/vc-refresh-service#refreshToken",
513                      "@type": "https://w3id.org/security#multibase"
514                    }
515                  }
516                },
517                "AgeVerificationCredential": "https://w3id.org/age#AgeVerificationCredential",
518                "AgeVerificationContainerCredential": "https://w3id.org/age#AgeVerificationContainerCredential"
519              }
520            })).unwrap())]
521            .iter()
522                .cloned()
523                .collect(),
524                ).unwrap() ;
525        cl.load_with(
526            &mut (),
527            IriBuf::new("https://w3id.org/age/v1".to_string()).unwrap(),
528        )
529        .await
530        .unwrap();
531    }
532}