ssi_ldp/
proof.rs

1use std::collections::HashMap as Map;
2use std::{convert::TryFrom, str::FromStr};
3
4use chrono::prelude::*;
5
6use crate::dataintegrity::DataIntegrityCryptoSuite;
7
8use super::*;
9
10use serde::{Deserialize, Serialize};
11use serde_json::Value;
12use ssi_dids::did_resolve::DIDResolver;
13use ssi_dids::VerificationRelationship as ProofPurpose;
14use ssi_json_ld::{json_to_dataset, parse_ld_context, ContextLoader};
15
16const RDF_TYPE: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#type");
17const RDF_NIL: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#nil");
18const RDF_FIRST: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#first");
19const RDF_REST: Iri<'static> = iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#rest");
20
21#[macro_export]
22macro_rules! assert_local {
23    ($cond:expr) => {
24        if !$cond {
25            return false;
26        }
27    };
28}
29
30#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
31#[serde(rename_all = "camelCase")]
32// TODO use enum to separate betwen JWS and LD proofs?
33// TODO create generics type to allow users to provide their own proof suite that implements ProofSuite
34pub struct Proof {
35    #[serde(rename = "@context")]
36    // TODO: use consistent types for context
37    #[serde(default, skip_serializing_if = "Value::is_null")]
38    pub context: Value,
39    #[serde(rename = "type")]
40    pub type_: ProofSuiteType,
41    #[serde(skip_serializing_if = "Option::is_none")]
42    pub proof_purpose: Option<ProofPurpose>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub proof_value: Option<String>,
45    #[serde(skip_serializing_if = "Option::is_none")]
46    pub challenge: Option<String>,
47    #[serde(skip_serializing_if = "Option::is_none")]
48    pub creator: Option<String>,
49    #[serde(skip_serializing_if = "Option::is_none")]
50    // Note: ld-proofs specifies verificationMethod as a "set of parameters",
51    // but all examples use a single string.
52    pub verification_method: Option<String>,
53    #[serde(skip_serializing_if = "Option::is_none")]
54    pub created: Option<DateTime<Utc>>, // ISO 8601
55    #[serde(skip_serializing_if = "Option::is_none")]
56    pub domain: Option<String>,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub nonce: Option<String>,
59    #[serde(skip_serializing_if = "Option::is_none")]
60    pub jws: Option<String>,
61    #[serde(skip_serializing_if = "Option::is_none")]
62    pub cryptosuite: Option<dataintegrity::DataIntegrityCryptoSuite>,
63    #[serde(skip_serializing_if = "Option::is_none")]
64    #[serde(flatten)]
65    pub property_set: Option<Map<String, Value>>,
66}
67
68impl Proof {
69    pub fn new(type_: ProofSuiteType) -> Self {
70        Self {
71            type_,
72            context: Value::default(),
73            proof_purpose: None,
74            proof_value: None,
75            challenge: None,
76            creator: None,
77            verification_method: None,
78            created: None,
79            domain: None,
80            nonce: None,
81            jws: None,
82            property_set: None,
83            cryptosuite: None,
84        }
85    }
86
87    pub fn with_options(self, options: &LinkedDataProofOptions) -> Self {
88        Self {
89            proof_purpose: options.proof_purpose.clone(),
90            verification_method: options
91                .verification_method
92                .clone()
93                .map(|uri| uri.to_string()),
94            domain: options.domain.clone(),
95            challenge: options.challenge.clone(),
96            created: Some(options.created.unwrap_or_else(now_ns)),
97            ..self
98        }
99    }
100
101    pub fn with_properties(self, properties: Option<Map<String, Value>>) -> Self {
102        Self {
103            property_set: properties,
104            ..self
105        }
106    }
107
108    /// Check that a proof matches the given options.
109    #[allow(clippy::ptr_arg)]
110    pub fn matches_options(&self, options: &LinkedDataProofOptions) -> bool {
111        if let Some(ref verification_method) = options.verification_method {
112            assert_local!(
113                self.verification_method.as_ref() == Some(&verification_method.to_string())
114            );
115        }
116        if let Some(created) = self.created {
117            assert_local!(options.created.unwrap_or_else(now_ns) >= created);
118        } else {
119            return false;
120        }
121        if let Some(ref challenge) = options.challenge {
122            assert_local!(self.challenge.as_ref() == Some(challenge));
123        }
124        if let Some(ref domain) = options.domain {
125            assert_local!(self.domain.as_ref() == Some(domain));
126        }
127        if let Some(ref proof_purpose) = options.proof_purpose {
128            assert_local!(self.proof_purpose.as_ref() == Some(proof_purpose));
129        }
130        if let Some(ref type_) = options.type_ {
131            assert_local!(&self.type_ == type_);
132        }
133        true
134    }
135
136    /// Check that a proof's verification method belongs to the given set.
137    pub fn matches_vms(&self, allowed_vms: &[String]) -> bool {
138        if let Some(vm) = self.verification_method.as_ref() {
139            assert_local!(allowed_vms.contains(vm));
140        }
141        true
142    }
143
144    /// Check that a proof matches the given options and allowed verification methods.
145    ///
146    /// Equivalent to [Self::matches_options] and [Self::matches_vm].
147    #[allow(clippy::ptr_arg)]
148    pub fn matches(&self, options: &LinkedDataProofOptions, allowed_vms: &Vec<String>) -> bool {
149        self.matches_options(options) && self.matches_vms(allowed_vms)
150    }
151
152    pub async fn verify(
153        &self,
154        document: &(dyn LinkedDataDocument + Sync),
155        resolver: &dyn DIDResolver,
156        context_loader: &mut ContextLoader,
157    ) -> VerificationResult {
158        LinkedDataProofs::verify(self, document, resolver, context_loader)
159            .await
160            .into()
161    }
162}
163
164#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
165#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
166impl LinkedDataDocument for Proof {
167    fn get_contexts(&self) -> Result<Option<String>, Error> {
168        Ok(None)
169    }
170
171    async fn to_dataset_for_signing(
172        &self,
173        parent: Option<&(dyn LinkedDataDocument + Sync)>,
174        context_loader: &mut ContextLoader,
175    ) -> Result<DataSet, Error> {
176        let mut copy = self.clone();
177        copy.jws = None;
178        copy.proof_value = None;
179        let json = json_syntax::to_value_with(copy, Default::default).unwrap();
180        let dataset = json_to_dataset(
181            json,
182            context_loader,
183            parent
184                .map(LinkedDataDocument::get_contexts)
185                .transpose()?
186                .flatten()
187                .as_deref()
188                .map(parse_ld_context)
189                .transpose()?,
190        )
191        .await?;
192
193        verify_proof_consistency(self, &dataset)?;
194        Ok(dataset)
195    }
196
197    fn to_value(&self) -> Result<Value, Error> {
198        Ok(serde_json::to_value(self)?)
199    }
200}
201
202// https://w3c-ccg.github.io/vc-http-api/#/Verifier/verifyCredential
203#[derive(Debug, Serialize, Deserialize, Clone)]
204#[serde(rename_all = "camelCase")]
205#[serde(deny_unknown_fields)]
206/// Options for specifying how the LinkedDataProof is created.
207/// Reference: vc-http-api
208pub struct LinkedDataProofOptions {
209    #[serde(skip_serializing_if = "Option::is_none")]
210    #[serde(rename = "type")]
211    /// The type of the proof. Default is an appropriate proof type corresponding to the verification method.
212    pub type_: Option<ProofSuiteType>,
213    #[serde(skip_serializing_if = "Option::is_none")]
214    /// The URI of the verificationMethod used for the proof. If omitted a default
215    /// assertionMethod will be used.
216    pub verification_method: Option<URI>,
217    #[serde(skip_serializing_if = "Option::is_none")]
218    /// The purpose of the proof. If omitted "assertionMethod" will be used.
219    pub proof_purpose: Option<ProofPurpose>,
220    #[serde(skip_serializing_if = "Option::is_none")]
221    /// The date of the proof. If omitted system time will be used.
222    pub created: Option<DateTime<Utc>>,
223    #[serde(skip_serializing_if = "Option::is_none")]
224    /// The challenge of the proof.
225    pub challenge: Option<String>,
226    #[serde(skip_serializing_if = "Option::is_none")]
227    /// The domain of the proof.
228    pub domain: Option<String>,
229    #[serde(skip_serializing_if = "Option::is_none")]
230    /// Checks to perform
231    pub checks: Option<Vec<Check>>,
232    /// Metadata for EthereumEip712Signature2021 (not standard in vc-http-api)
233    #[serde(skip_serializing_if = "Option::is_none")]
234    #[cfg(feature = "eip")]
235    pub eip712_domain: Option<crate::eip712::ProofInfo>,
236    #[cfg(not(feature = "eip"))]
237    pub eip712_domain: Option<()>,
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub cryptosuite: Option<DataIntegrityCryptoSuite>,
240}
241
242impl Default for LinkedDataProofOptions {
243    fn default() -> Self {
244        Self {
245            verification_method: None,
246            proof_purpose: Some(ProofPurpose::default()),
247            created: Some(crate::now_ns()),
248            challenge: None,
249            domain: None,
250            checks: Some(vec![Check::Proof]),
251            eip712_domain: None,
252            type_: None,
253            cryptosuite: None,
254        }
255    }
256}
257
258#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
259#[serde(try_from = "String")]
260#[serde(rename_all = "camelCase")]
261#[non_exhaustive]
262pub enum Check {
263    Proof,
264    #[serde(rename = "JWS")]
265    JWS,
266    Status,
267}
268
269impl FromStr for Check {
270    type Err = Error;
271    fn from_str(purpose: &str) -> Result<Self, Self::Err> {
272        match purpose {
273            "proof" => Ok(Self::Proof),
274            "JWS" => Ok(Self::JWS),
275            "credentialStatus" => Ok(Self::Status),
276            _ => Err(Error::UnsupportedCheck),
277        }
278    }
279}
280
281impl TryFrom<String> for Check {
282    type Error = Error;
283    fn try_from(purpose: String) -> Result<Self, Self::Error> {
284        Self::from_str(&purpose)
285    }
286}
287
288impl From<Check> for String {
289    fn from(check: Check) -> String {
290        match check {
291            Check::Proof => "proof".to_string(),
292            Check::JWS => "JWS".to_string(),
293            Check::Status => "credentialStatus".to_string(),
294        }
295    }
296}
297
298#[derive(thiserror::Error, Debug)]
299pub enum ProofInconsistency {
300    /// RDF statement object does not match value.
301    #[error(
302        "RDF statement object does not match value. Predicate: {0}. Expected: {1}. Actual: {2}"
303    )]
304    ObjectMismatch(IriBuf, String, String),
305
306    /// Missing RDF statement object.
307    #[error("Missing RDF statement object. Predicate: {0}. Expected value: {1}")]
308    ExpectedObject(IriBuf, String),
309
310    /// Unexpected RDF statement object.
311    #[error("Unexpected RDF statement object. Predicate: {0}. Value: {1}")]
312    UnexpectedObject(IriBuf, String),
313
314    /// List item mismatch.
315    #[error("List item mismatch. Value in RDF: {0}. Value in JSON: {1}")]
316    ListItemMismatch(String, String),
317
318    /// Invalid RDF list.
319    #[error("Invalid list")]
320    InvalidList,
321
322    /// Unexpected end of list.
323    #[error("Unexpected end of list")]
324    UnexpectedEndOfList,
325
326    /// Expected end of list.
327    #[error("Expected end of list")]
328    ExpectedEndOfList,
329
330    /// Missing RDF type.
331    #[error("Missing type")]
332    MissingType,
333
334    /// Invalid RDF type value.
335    #[error("Invalid type value")]
336    InvalidType,
337
338    /// Missing associated context.
339    #[error("Missing associated context: {0}")]
340    MissingAssociatedContext(IriBuf),
341
342    /// Unexpected RDF triple.
343    #[error("Unexpected triple {0:?}")]
344    UnexpectedTriple(Box<rdf_types::Triple>),
345}
346
347/// Verify alignment of proof options in JSON with RDF terms
348fn verify_proof_consistency(
349    proof: &Proof,
350    dataset: &DataSet,
351) -> Result<(), Box<ProofInconsistency>> {
352    let mut dataset = dataset.clone();
353    let graph_ref = dataset.default_graph_mut();
354
355    let type_triple = graph_ref
356        .take_match::<rdf_types::Subject, _, rdf_types::Object>(rdf_types::Triple(
357            None,
358            Some(&RDF_TYPE),
359            None,
360        ))
361        .ok_or(ProofInconsistency::MissingType)?;
362
363    let type_iri = type_triple
364        .object()
365        .as_iri()
366        .ok_or(ProofInconsistency::InvalidType)?;
367
368    if !proof
369        .type_
370        .associated_contexts()
371        .contains(&type_iri.as_str())
372    {
373        return Err(Box::new(ProofInconsistency::MissingAssociatedContext(
374            type_iri.clone(),
375        )));
376    }
377
378    let proof_id = type_triple.subject();
379
380    graph_ref.take_object_and_assert_eq_iri(
381        proof_id,
382        iri!("https://w3id.org/security#proofPurpose"),
383        proof.proof_purpose.as_ref().map(|pp| pp.to_iri()),
384    )?;
385
386    // FIXME: this only works under the (false) assumption that
387    // `verificationMethod` is always an IRI.
388    // See: https://www.w3.org/TR/did-core/#did-document-properties
389    graph_ref.take_object_and_assert_eq_iri(
390        proof_id,
391        iri!("https://w3id.org/security#verificationMethod"),
392        proof
393            .verification_method
394            .as_ref()
395            .map(|m| Iri::new(m).unwrap()),
396    )?;
397
398    graph_ref.take_object_and_assert_eq_iri_or_str(
399        proof_id,
400        iri!("https://w3id.org/security#challenge"),
401        proof.challenge.as_deref(),
402    )?;
403
404    graph_ref.take_object_and_assert_eq_iri_or_str(
405        proof_id,
406        iri!("https://w3id.org/security#domain"),
407        proof.domain.as_deref(),
408    )?;
409
410    graph_ref.take_object_and_assert_eq_date(
411        proof_id,
412        iri!("http://purl.org/dc/terms/created"),
413        proof.created.as_ref(),
414    )?;
415
416    graph_ref.take_object_and_assert_eq_json(
417        proof_id,
418        iri!("https://w3id.org/security#publicKeyJwk"),
419        proof
420            .property_set
421            .as_ref()
422            .and_then(|cc| cc.get("publicKeyJwk")),
423    )?;
424
425    graph_ref.take_object_and_assert_eq_multibase(
426        proof_id,
427        iri!("https://w3id.org/security#publicKeyMultibase"),
428        proof
429            .property_set
430            .as_ref()
431            .and_then(|cc| cc.get("publicKeyMultibase"))
432            .and_then(|cap| cap.as_str()),
433    )?;
434
435    graph_ref.take_object_and_assert_eq_iri_or_str(
436        proof_id,
437        iri!("https://w3id.org/security#cryptosuite"),
438        proof
439            .cryptosuite
440            .clone()
441            .map(|cc| cc.to_string())
442            .as_deref(),
443    )?;
444
445    graph_ref.take_object_and_assert_eq_iri(
446        proof_id,
447        iri!("https://w3id.org/security#capability"),
448        proof
449            .property_set
450            .as_ref()
451            .and_then(|cc| cc.get("capability"))
452            .and_then(|cap| cap.as_str())
453            .map(|cap| Iri::new(cap).unwrap()),
454    )?;
455
456    graph_ref.take_object_and_assert_eq_list(
457        proof_id,
458        iri!("https://w3id.org/security#capabilityChain"),
459        proof
460            .property_set
461            .as_ref()
462            .and_then(|cc| cc.get("capabilityChain"))
463            .map(|cc| cc.as_array().unwrap().iter()),
464        |item, expected| match expected.as_str() {
465            Some(e) => match (item, Iri::new(e)) {
466                (rdf_types::Term::Iri(iri), Ok(expected)) => *iri == expected,
467                _ => false,
468            },
469            None => false,
470        },
471    )?;
472
473    // Disallow additional unexpected statements
474    if let Some(rdf_types::Triple(s, p, o)) = graph_ref.triples().next() {
475        return Err(Box::new(ProofInconsistency::UnexpectedTriple(Box::new(
476            rdf_types::Triple(s.clone(), p.clone(), o.clone()),
477        ))));
478    }
479
480    Ok(())
481}
482
483/// RDF graph extension adding utility methods on proof graphs.
484trait ProofGraph:
485    grdf::Graph<Subject = rdf_types::Subject, Predicate = IriBuf, Object = rdf_types::Object>
486    + for<'a> grdf::GraphTake<rdf_types::Subject, Iri<'a>, rdf_types::Object>
487{
488    /// Take any statement of the form `s p o` for the given `s` and `p`
489    /// and call `object_predicate(Some(o))`.
490    /// If no statement has the form `s p o`, call `object_predicate(None)`.
491    fn take_object_and_assert<E>(
492        &mut self,
493        s: &Self::Subject,
494        p: Iri,
495        object_predicate: impl FnOnce(&mut Self, Option<Self::Object>) -> Result<(), E>,
496    ) -> Result<(), E> {
497        match self.take_match(rdf_types::Triple(Some(s), Some(&p), None)) {
498            Some(rdf_types::Triple(_, _, o)) => object_predicate(self, Some(o)),
499            None => object_predicate(self, None),
500        }
501    }
502
503    /// When `expected_o` is `Some(iri)`.
504    /// take any statement of the form `s p o` for the given `s` and `p`
505    /// and checks that `o` is equal to `iri` according to `eq`.
506    /// When `expected_o` is `None`,
507    /// checks that no statement has the form `s p o`.
508    fn take_object_and_assert_eq<V: ToString>(
509        &mut self,
510        s: &Self::Subject,
511        p: Iri,
512        expected_o: Option<V>,
513        eq: impl FnOnce(&Self::Object, &V) -> bool,
514    ) -> Result<(), Box<ProofInconsistency>> {
515        self.take_object_and_assert(s, p, |_, o| match (o, expected_o) {
516            (Some(o), Some(expected)) => {
517                if eq(&o, &expected) {
518                    Ok(())
519                } else {
520                    Err(Box::new(ProofInconsistency::ObjectMismatch(
521                        p.to_owned(),
522                        expected.to_string(),
523                        o.to_string(),
524                    )))
525                }
526            }
527            (None, None) => Ok(()),
528            (None, Some(expected_iri)) => Err(Box::new(ProofInconsistency::ExpectedObject(
529                p.to_owned(),
530                expected_iri.to_string(),
531            ))),
532            (Some(o), None) => Err(Box::new(ProofInconsistency::UnexpectedObject(
533                p.to_owned(),
534                o.to_string(),
535            ))),
536        })
537    }
538
539    /// When `expected_o` is `Some(json)`.
540    /// take any statement of the form `s p o` for the given `s` and `p`
541    /// and checks that `o` is equal to the JSON array value `json`.
542    /// When `expected_o` is `None`,
543    /// checks that no statement has the form `s p o`.
544    fn take_object_and_assert_eq_list<I: Iterator>(
545        &mut self,
546        s: &Self::Subject,
547        p: Iri,
548        expected_o: Option<I>,
549        eq: impl Fn(&Self::Object, &I::Item) -> bool,
550    ) -> Result<(), Box<ProofInconsistency>>
551    where
552        I::Item: ToString,
553    {
554        fn format_list<I: Iterator>(list: I) -> String
555        where
556            I::Item: ToString,
557        {
558            let mut expected_string = "[".to_string();
559
560            for (i, item) in list.enumerate() {
561                if i > 0 {
562                    expected_string.push(',');
563                }
564
565                expected_string.push_str(&item.to_string());
566            }
567
568            expected_string.push(']');
569            expected_string
570        }
571
572        self.take_object_and_assert(s, p, |this, o| match (o, expected_o) {
573            (Some(o), Some(expected)) => {
574                let mut head = match o {
575                    rdf_types::Term::Iri(i) if i == RDF_NIL => None,
576                    rdf_types::Term::Iri(i) => Some(rdf_types::Subject::Iri(i)),
577                    rdf_types::Term::Blank(b) => Some(rdf_types::Subject::Blank(b)),
578                    rdf_types::Term::Literal(l) => {
579                        return Err(Box::new(ProofInconsistency::ObjectMismatch(
580                            p.to_owned(),
581                            l.to_string(),
582                            format_list(expected),
583                        )))
584                    }
585                };
586
587                for expected_item in expected {
588                    match head.take() {
589                        Some(id) => {
590                            match this.take_match(rdf_types::Triple(
591                                Some(&id),
592                                Some(&RDF_FIRST),
593                                None,
594                            )) {
595                                Some(rdf_types::Triple(_, _, first)) => {
596                                    if !eq(&first, &expected_item) {
597                                        return Err(Box::new(
598                                            ProofInconsistency::ListItemMismatch(
599                                                first.to_string(),
600                                                expected_item.to_string(),
601                                            ),
602                                        ));
603                                    }
604
605                                    match this.take_match(rdf_types::Triple(
606                                        Some(&id),
607                                        Some(&RDF_REST),
608                                        None,
609                                    )) {
610                                        Some(rdf_types::Triple(_, _, rest)) => {
611                                            head = match rest {
612                                                rdf_types::Term::Iri(i) if i == RDF_NIL => None,
613                                                rdf_types::Term::Iri(i) => {
614                                                    Some(rdf_types::Subject::Iri(i))
615                                                }
616                                                rdf_types::Term::Blank(b) => {
617                                                    Some(rdf_types::Subject::Blank(b))
618                                                }
619                                                rdf_types::Term::Literal(_) => {
620                                                    return Err(Box::new(
621                                                        ProofInconsistency::InvalidList,
622                                                    ))
623                                                }
624                                            };
625                                        }
626                                        None => {
627                                            return Err(Box::new(ProofInconsistency::InvalidList))
628                                        }
629                                    }
630                                }
631                                None => return Err(Box::new(ProofInconsistency::InvalidList)),
632                            }
633                        }
634                        None => return Err(Box::new(ProofInconsistency::UnexpectedEndOfList)),
635                    }
636                }
637
638                if head.is_some() {
639                    return Err(Box::new(ProofInconsistency::ExpectedEndOfList));
640                }
641
642                Ok(())
643            }
644            (None, None) => Ok(()),
645            (None, Some(expected)) => Err(Box::new(ProofInconsistency::ExpectedObject(
646                p.to_owned(),
647                format_list(expected),
648            ))),
649            (Some(o), None) => Err(Box::new(ProofInconsistency::UnexpectedObject(
650                p.to_owned(),
651                o.to_string(),
652            ))),
653        })
654    }
655
656    /// When `expected_o` is `Some(iri)`.
657    /// take any statement of the form `s p o` for the given `s` and `p`
658    /// and checks that `o` is equal to `iri`.
659    /// When `expected_o` is `None`,
660    /// checks that no statement has the form `s p o`.
661    fn take_object_and_assert_eq_iri(
662        &mut self,
663        s: &Self::Subject,
664        p: Iri,
665        expected_o: Option<Iri>,
666    ) -> Result<(), Box<ProofInconsistency>>;
667
668    /// When `expected_o` is `Some(str)`.
669    /// take any statement of the form `s p o` for the given `s` and `p`
670    /// and checks that `o` is an IRI or string literal equal to `str`.
671    /// When `expected_o` is `None`,
672    /// checks that no statement has the form `s p o`.
673    fn take_object_and_assert_eq_iri_or_str(
674        &mut self,
675        s: &Self::Subject,
676        p: Iri,
677        expected_o: Option<&str>,
678    ) -> Result<(), Box<ProofInconsistency>>;
679
680    /// When `expected_o` is `Some(date)`.
681    /// take any statement of the form `s p o` for the given `s` and `p`
682    /// and checks that `o` is equal to `date`.
683    /// When `expected_o` is `None`,
684    /// checks that no statement has the form `s p o`.
685    fn take_object_and_assert_eq_date(
686        &mut self,
687        s: &Self::Subject,
688        p: Iri,
689        expected_o: Option<&DateTime<Utc>>,
690    ) -> Result<(), Box<ProofInconsistency>>;
691
692    /// When `expected_o` is `Some(str)`.
693    /// take any statement of the form `s p o` for the given `s` and `p`
694    /// and checks that `o` is equal to the multibase-encoded string `str`.
695    /// When `expected_o` is `None`,
696    /// checks that no statement has the form `s p o`.
697    fn take_object_and_assert_eq_multibase(
698        &mut self,
699        s: &Self::Subject,
700        p: Iri,
701        expected_o: Option<&str>,
702    ) -> Result<(), Box<ProofInconsistency>>;
703
704    /// When `expected_o` is `Some(json)`.
705    /// take any statement of the form `s p o` for the given `s` and `p`
706    /// and checks that `o` is equal to the JSON value `json`.
707    /// When `expected_o` is `None`,
708    /// checks that no statement has the form `s p o`.
709    fn take_object_and_assert_eq_json(
710        &mut self,
711        s: &Self::Subject,
712        p: Iri,
713        expected_o: Option<&serde_json::Value>,
714    ) -> Result<(), Box<ProofInconsistency>>;
715}
716
717impl ProofGraph for grdf::HashGraph<rdf_types::Subject, IriBuf, rdf_types::Object> {
718    fn take_object_and_assert_eq_iri(
719        &mut self,
720        s: &Self::Subject,
721        p: Iri,
722        expected_o: Option<Iri>,
723    ) -> Result<(), Box<ProofInconsistency>> {
724        self.take_object_and_assert_eq(s, p, expected_o, |o, expected_iri| match o {
725            rdf_types::Object::Iri(iri) => iri == expected_iri,
726            _ => false,
727        })
728    }
729
730    fn take_object_and_assert_eq_iri_or_str(
731        &mut self,
732        s: &Self::Subject,
733        p: Iri,
734        expected_o: Option<&str>,
735    ) -> Result<(), Box<ProofInconsistency>> {
736        self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
737            rdf_types::Object::Iri(iri) => iri.as_str() == *expected,
738            rdf_types::Object::Literal(rdf_types::Literal::String(s)) => s.as_str() == *expected,
739            _ => false,
740        })
741    }
742
743    fn take_object_and_assert_eq_date(
744        &mut self,
745        s: &Self::Subject,
746        p: Iri,
747        expected_o: Option<&DateTime<Utc>>,
748    ) -> Result<(), Box<ProofInconsistency>> {
749        self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
750            rdf_types::Object::Literal(rdf_types::Literal::TypedString(date, ty))
751                if *ty == iri!("http://www.w3.org/2001/XMLSchema#dateTime") =>
752            {
753                match DateTime::parse_from_rfc3339(date.as_str()) {
754                    Ok(date) => date == **expected,
755                    _ => false,
756                }
757            }
758            _ => false,
759        })
760    }
761
762    fn take_object_and_assert_eq_multibase(
763        &mut self,
764        s: &Self::Subject,
765        p: Iri,
766        expected_o: Option<&str>,
767    ) -> Result<(), Box<ProofInconsistency>> {
768        self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
769            rdf_types::Object::Literal(rdf_types::Literal::TypedString(s, ty)) => {
770                *ty == iri!("https://w3id.org/security#multibase") && s.as_str() == *expected
771            }
772            _ => false,
773        })
774    }
775
776    fn take_object_and_assert_eq_json(
777        &mut self,
778        s: &Self::Subject,
779        p: Iri,
780        expected_o: Option<&serde_json::Value>,
781    ) -> Result<(), Box<ProofInconsistency>> {
782        self.take_object_and_assert_eq(s, p, expected_o, |o, expected| match o {
783            rdf_types::Object::Literal(rdf_types::Literal::TypedString(json, ty))
784                if *ty == iri!("http://www.w3.org/1999/02/22-rdf-syntax-ns#JSON") =>
785            {
786                match serde_json::from_str::<serde_json::Value>(json) {
787                    Ok(json) => json == **expected,
788                    _ => false,
789                }
790            }
791            _ => false,
792        })
793    }
794}