dif_presentation_exchange/
input_evaluation.rs

1use crate::InputDescriptor;
2use jsonpath_lib as jsonpath;
3use jsonschema::JSONSchema;
4
5#[derive(Debug, Clone, PartialEq)]
6pub enum FieldQueryResult {
7    Some { value: serde_json::Value, path: String },
8    None,
9    Invalid,
10}
11
12impl FieldQueryResult {
13    pub fn is_valid(&self) -> bool {
14        !self.is_invalid()
15    }
16
17    pub fn is_invalid(&self) -> bool {
18        *self == FieldQueryResult::Invalid
19    }
20}
21
22/// Input Evaluation as described in section [8. Input
23/// Evaluation](https://identity.foundation/presentation-exchange/spec/v2.0.0/#input-evaluation) of the DIF
24/// Presentation Exchange specification.
25pub fn evaluate_input(input_descriptor: &InputDescriptor, value: &serde_json::Value) -> bool {
26    let selector = &mut jsonpath::selector(value);
27
28    input_descriptor
29        .constraints()
30        .fields()
31        .as_ref()
32        .map(|fields| {
33            let results: Vec<FieldQueryResult> = fields
34                .iter()
35                .map(|field| {
36                    let filter = field
37                        .filter()
38                        .as_ref()
39                        .map(JSONSchema::compile)
40                        .transpose()
41                        .ok()
42                        .flatten();
43
44                    // For each JSONPath expression in the `path` array (incrementing from the 0-index),
45                    // evaluate the JSONPath expression against the candidate input and repeat the following
46                    // subsequence on the result.
47                    field
48                        .path()
49                        .iter()
50                        // Repeat until a Field Query Result is found, or the path array elements are exhausted:
51                        .find_map(|path| {
52                            // If the result returned no JSONPath match, skip to the next path array element.
53                            // Else, evaluate the first JSONPath match (candidate) as follows:
54                            selector(path).ok().and_then(|values| {
55                                values.into_iter().find_map(|result| {
56                                    // If the fields object has no `filter`, or if candidate validates against
57                                    // the JSON Schema descriptor specified in `filter`, then:
58                                    filter
59                                        .as_ref()
60                                        .map(|filter| filter.is_valid(result))
61                                        .unwrap_or(true)
62                                        // set Field Query Result to be candidate
63                                        .then(|| FieldQueryResult::Some {
64                                            value: result.to_owned(),
65                                            path: path.to_owned(),
66                                        })
67                                    // Else, skip to the next `path` array element.
68                                })
69                            })
70                        })
71                        // If no value is located for any of the specified `path` queries, and the fields
72                        // object DOES NOT contain the `optional` property or it is set to `false`, reject the
73                        // field as invalid. If no value is located for any of the specified `path` queries and
74                        // the fields object DOES contain the `optional` property set to the value `true`,
75                        // treat the field as valid and proceed to the next fields object.
76                        .or_else(|| field.optional().and_then(|opt| opt.then(|| FieldQueryResult::None)))
77                        .unwrap_or(FieldQueryResult::Invalid)
78                })
79                .collect();
80            results.iter().all(FieldQueryResult::is_valid)
81        })
82        .unwrap_or(false)
83}
84
85#[cfg(test)]
86mod tests {
87    use super::*;
88    use crate::{
89        presentation_definition::{Constraints, Field},
90        InputDescriptor,
91    };
92    use serde::de::DeserializeOwned;
93    use std::{fs::File, path::Path};
94
95    fn json_example<T>(path: &str) -> T
96    where
97        T: DeserializeOwned,
98    {
99        let file_path = Path::new(path);
100        let file = File::open(file_path).expect("file does not exist");
101        serde_json::from_reader::<_, T>(file).expect("could not parse json")
102    }
103
104    fn input_descriptor(constraints: Constraints) -> InputDescriptor {
105        InputDescriptor {
106            id: "test_input_descriptor".to_string(),
107            name: None,
108            purpose: None,
109            format: None,
110            constraints,
111            schema: None,
112        }
113    }
114
115    #[test]
116    fn test_constraints() {
117        let credential = json_example::<serde_json::Value>("../oid4vp/tests/examples/credentials/jwt_vc.json");
118
119        // Has NO fields.
120        assert!(!evaluate_input(&input_descriptor(Constraints::default()), &credential));
121
122        // Has ONE VALID field.
123        assert!(evaluate_input(
124            &input_descriptor(Constraints {
125                fields: Some(vec![Field {
126                    path: vec!["$.vc.type".to_string()],
127                    ..Default::default()
128                }]),
129                ..Default::default()
130            }),
131            &credential
132        ));
133
134        // // Has ONE INVALID field.
135        assert!(!evaluate_input(
136            &input_descriptor(Constraints {
137                fields: Some(vec![Field {
138                    path: vec!["$.vc.foo".to_string()],
139                    ..Default::default()
140                }]),
141                ..Default::default()
142            }),
143            &credential
144        ));
145
146        // First field is INVALID.
147        assert!(!evaluate_input(
148            &input_descriptor(Constraints {
149                fields: Some(vec![
150                    Field {
151                        path: vec!["$.vc.foo".to_string()],
152                        ..Default::default()
153                    },
154                    Field {
155                        path: vec!["$.vc.type".to_string()],
156                        ..Default::default()
157                    },
158                ]),
159                ..Default::default()
160            }),
161            &credential
162        ));
163
164        // Second field is INVALID.
165        assert!(!evaluate_input(
166            &input_descriptor(Constraints {
167                fields: Some(vec![
168                    Field {
169                        path: vec!["$.vc.type".to_string()],
170                        ..Default::default()
171                    },
172                    Field {
173                        path: vec!["$.vc.foo".to_string()],
174                        ..Default::default()
175                    },
176                ]),
177                ..Default::default()
178            }),
179            &credential
180        ));
181
182        // Second field is INVALID but optional.
183        assert!(evaluate_input(
184            &input_descriptor(Constraints {
185                fields: Some(vec![
186                    Field {
187                        path: vec!["$.vc.type".to_string()],
188                        ..Default::default()
189                    },
190                    Field {
191                        path: vec!["$.vc.foo".to_string()],
192                        optional: Some(true),
193                        ..Default::default()
194                    },
195                ]),
196                ..Default::default()
197            }),
198            &credential
199        ));
200    }
201
202    #[test]
203    fn test_field() {
204        let credential = json_example::<serde_json::Value>("../oid4vp/tests/examples/credentials/jwt_vc.json");
205
206        // Has NO path.
207        assert!(!evaluate_input(
208            &input_descriptor(Constraints {
209                fields: Some(vec![Field::default()]),
210                ..Default::default()
211            }),
212            &credential
213        ));
214
215        // Has ONE path.
216        assert!(evaluate_input(
217            &input_descriptor(Constraints {
218                fields: Some(vec![Field {
219                    path: vec!["$.vc.type".to_string()],
220                    ..Default::default()
221                }]),
222                ..Default::default()
223            }),
224            &credential
225        ));
226
227        // Has TWO paths. First is NO match, second is a match without filter.
228        assert!(evaluate_input(
229            &input_descriptor(Constraints {
230                fields: Some(vec![Field {
231                    path: vec!["$.vc.foo".to_string(), "$.vc.type".to_string()],
232                    ..Default::default()
233                }]),
234                ..Default::default()
235            }),
236            &credential
237        ));
238
239        // Has TWO paths. First is a match, with filter.
240        assert!(evaluate_input(
241            &input_descriptor(Constraints {
242                fields: Some(vec![Field {
243                    path: vec!["$.vc.type".to_string(), "$.vc.foo".to_string()],
244                    filter: Some(serde_json::json!({
245                        "type": "array",
246                        "contains": {
247                            "const": "IDCredential"
248                        }
249                    })),
250                    ..Default::default()
251                }]),
252                ..Default::default()
253            }),
254            &credential
255        ));
256
257        // Has ONE paths. With non-matching filter.
258        assert!(!evaluate_input(
259            &input_descriptor(Constraints {
260                fields: Some(vec![Field {
261                    path: vec!["$.vc.type".to_string()],
262                    filter: Some(serde_json::json!({
263                        "type": "array",
264                        "contains": {
265                            "const": "Foo"
266                        }
267                    })),
268                    ..Default::default()
269                }]),
270                ..Default::default()
271            }),
272            &credential
273        ));
274
275        // Has ONE path. With non-matching filter. Is optional
276        assert!(evaluate_input(
277            &input_descriptor(Constraints {
278                fields: Some(vec![Field {
279                    path: vec!["$.vc.type".to_string()],
280                    filter: Some(serde_json::json!({
281                        "type": "array",
282                        "contains": {
283                            "const": "Foo"
284                        }
285                    })),
286                    optional: Some(true),
287                    ..Default::default()
288                }]),
289                ..Default::default()
290            }),
291            &credential
292        ));
293
294        // Has ONE path, which does not exist. Is optional
295        assert!(evaluate_input(
296            &input_descriptor(Constraints {
297                fields: Some(vec![Field {
298                    path: vec!["$.vc.foo".to_string()],
299                    optional: Some(true),
300                    ..Default::default()
301                }]),
302                ..Default::default()
303            }),
304            &credential
305        ));
306
307        // Has ONE path, which does not exist. Is NOT optional (explicitly).
308        assert!(!evaluate_input(
309            &input_descriptor(Constraints {
310                fields: Some(vec![Field {
311                    path: vec!["$.vc.foo".to_string()],
312                    optional: Some(false),
313                    ..Default::default()
314                }]),
315                ..Default::default()
316            }),
317            &credential
318        ));
319    }
320}