Skip to main content

identity_kit/vc/
path.rs

1//! Claim paths (SD-JWT VC §5.6.1, the claims path pointer of OID4VP §7): where a claim sits in
2//! a JSON payload.
3
4use core::fmt;
5
6use serde::{Deserialize, Serialize};
7use serde_json::Value;
8
9/// One step of a [`ClaimPath`].
10#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
11#[serde(untagged)]
12pub enum PathComponent {
13    /// The property with this name of the selected object(s).
14    Key(String),
15    /// The element at this index of the selected array(s).
16    Index(usize),
17    /// Every element of the selected array(s), `null` in JSON.
18    All,
19}
20
21impl From<&str> for PathComponent {
22    fn from(key: &str) -> Self {
23        Self::Key(key.to_owned())
24    }
25}
26
27impl From<String> for PathComponent {
28    fn from(key: String) -> Self {
29        Self::Key(key)
30    }
31}
32
33impl From<usize> for PathComponent {
34    fn from(index: usize) -> Self {
35        Self::Index(index)
36    }
37}
38
39impl From<()> for PathComponent {
40    fn from((): ()) -> Self {
41        Self::All
42    }
43}
44
45impl fmt::Display for PathComponent {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        match self {
48            Self::Key(key) => write!(f, "{key:?}"),
49            Self::Index(index) => write!(f, "{index}"),
50            Self::All => f.write_str("null"),
51        }
52    }
53}
54
55/// A path into a JSON payload: a non-empty list of property names, array indexes and `null`
56/// (every element).
57///
58/// SD-JWT VC §5.6.1 uses it as the `path` of claim metadata, where it names the claim as if
59/// everything were disclosed; OID4VP §7 defines the same construct as the claims path pointer
60/// of DCQL `claims[].path`, for every JSON-based credential format (`jwt_vc_json` and
61/// `dc+sd-jwt` alike). ISO mdoc paths, which OID4VP §7 defines differently, are out of scope.
62///
63/// # Examples
64///
65/// ```
66/// use identity_kit::vc::ClaimPath;
67/// use serde_json::json;
68///
69/// // SD-JWT VC §5.6.1.1 and OID4VP §7.
70/// let credential = json!({
71///   "name": "Arthur Dent",
72///   "address": { "street_address": "42 Market Street", "city": "Milliways" },
73///   "degrees": [
74///     { "type": "Bachelor of Science", "university": "University of Betelgeuse" },
75///     { "type": "Master of Science", "university": "University of Betelgeuse" }
76///   ],
77///   "nationalities": ["British", "Betelgeusian"]
78/// });
79/// let path: ClaimPath = serde_json::from_value(json!(["degrees", null, "type"]))?;
80/// assert_eq!(
81///     path.select(&credential)?,
82///     vec![&json!("Bachelor of Science"), &json!("Master of Science")]
83/// );
84/// let street = ClaimPath::new(["address", "street_address"]);
85/// assert_eq!(street.select(&credential)?, vec![&json!("42 Market Street")]);
86/// assert!(ClaimPath::new(["name", "first"]).select(&credential).is_err());
87/// # Ok::<(), Box<dyn std::error::Error>>(())
88/// ```
89#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(transparent)]
91pub struct ClaimPath(Vec<PathComponent>);
92
93impl ClaimPath {
94    /// The path through `components`: `&str` names, `usize` indexes and `()` for every
95    /// element.
96    pub fn new<I, C>(components: I) -> Self
97    where
98        I: IntoIterator<Item = C>,
99        C: Into<PathComponent>,
100    {
101        Self(components.into_iter().map(Into::into).collect())
102    }
103
104    /// The empty path: the payload itself.
105    pub fn root() -> Self {
106        Self(Vec::new())
107    }
108
109    /// The components.
110    pub fn components(&self) -> &[PathComponent] {
111        &self.0
112    }
113
114    /// Whether the path has no components.
115    pub fn is_empty(&self) -> bool {
116        self.0.is_empty()
117    }
118
119    /// Whether every component names one element: no `null`.
120    pub fn is_concrete(&self) -> bool {
121        !self.0.contains(&PathComponent::All)
122    }
123
124    /// Whether this path, which may contain `null` components, covers the concrete `path`.
125    pub fn matches(&self, path: &ClaimPath) -> bool {
126        self.0.len() == path.0.len()
127            && self.0.iter().zip(&path.0).all(|(pattern, component)| {
128                *pattern == PathComponent::All && matches!(component, PathComponent::Index(_))
129                    || pattern == component
130            })
131    }
132
133    /// Whether this path, which may contain `null` components, covers the concrete `path` or
134    /// one of its ancestors.
135    pub fn is_prefix_of(&self, path: &ClaimPath) -> bool {
136        self.0.len() <= path.0.len()
137            && self.0.iter().zip(&path.0).all(|(pattern, component)| {
138                *pattern == PathComponent::All && matches!(component, PathComponent::Index(_))
139                    || pattern == component
140            })
141    }
142
143    /// This path with `component` appended.
144    #[must_use]
145    pub fn join(&self, component: impl Into<PathComponent>) -> Self {
146        let mut components = self.0.clone();
147        components.push(component.into());
148        Self(components)
149    }
150
151    /// The values the path selects in `root`, per the processing rules of SD-JWT VC §5.6.1.2:
152    /// empty when a name or an index does not exist.
153    ///
154    /// # Errors
155    ///
156    /// [`ClaimPathError`] when a component meets a value of the wrong kind: a name against a
157    /// non-object, an index or `null` against a non-array.
158    pub fn select<'a>(&self, root: &'a Value) -> Result<Vec<&'a Value>, ClaimPathError> {
159        let mut selected = vec![root];
160        for (depth, component) in self.0.iter().enumerate() {
161            let mut next = Vec::new();
162            for value in selected {
163                match (component, value) {
164                    (PathComponent::Key(key), Value::Object(object)) => {
165                        next.extend(object.get(key));
166                    }
167                    (PathComponent::Index(index), Value::Array(array)) => {
168                        next.extend(array.get(*index));
169                    }
170                    (PathComponent::All, Value::Array(array)) => next.extend(array),
171                    (PathComponent::Key(_), _) => {
172                        return Err(ClaimPathError {
173                            path: self.clone(),
174                            depth,
175                            expected: "an object",
176                        });
177                    }
178                    _ => {
179                        return Err(ClaimPathError {
180                            path: self.clone(),
181                            depth,
182                            expected: "an array",
183                        });
184                    }
185                }
186            }
187            selected = next;
188        }
189        Ok(selected)
190    }
191
192    /// The concrete paths (every `null` resolved to the indexes it selects) at which the path
193    /// selects a value in `root`, in document order.
194    ///
195    /// # Errors
196    ///
197    /// As [`select`](Self::select).
198    pub fn resolve(&self, root: &Value) -> Result<Vec<ClaimPath>, ClaimPathError> {
199        let mut selected = vec![(ClaimPath(Vec::new()), root)];
200        for (depth, component) in self.0.iter().enumerate() {
201            let mut next = Vec::new();
202            for (path, value) in selected {
203                match (component, value) {
204                    (PathComponent::Key(key), Value::Object(object)) => {
205                        if let Some(value) = object.get(key) {
206                            next.push((path.join(key.as_str()), value));
207                        }
208                    }
209                    (PathComponent::Index(index), Value::Array(array)) => {
210                        if let Some(value) = array.get(*index) {
211                            next.push((path.join(*index), value));
212                        }
213                    }
214                    (PathComponent::All, Value::Array(array)) => {
215                        next.extend(
216                            array
217                                .iter()
218                                .enumerate()
219                                .map(|(index, value)| (path.join(index), value)),
220                        );
221                    }
222                    (PathComponent::Key(_), _) => {
223                        return Err(ClaimPathError {
224                            path: self.clone(),
225                            depth,
226                            expected: "an object",
227                        });
228                    }
229                    _ => {
230                        return Err(ClaimPathError {
231                            path: self.clone(),
232                            depth,
233                            expected: "an array",
234                        });
235                    }
236                }
237            }
238            selected = next;
239        }
240        Ok(selected.into_iter().map(|(path, _)| path).collect())
241    }
242}
243
244impl<C: Into<PathComponent>> FromIterator<C> for ClaimPath {
245    fn from_iter<I: IntoIterator<Item = C>>(iter: I) -> Self {
246        Self::new(iter)
247    }
248}
249
250impl<C: Into<PathComponent>, const N: usize> From<[C; N]> for ClaimPath {
251    fn from(components: [C; N]) -> Self {
252        Self::new(components)
253    }
254}
255
256impl From<Vec<PathComponent>> for ClaimPath {
257    fn from(components: Vec<PathComponent>) -> Self {
258        Self(components)
259    }
260}
261
262impl fmt::Display for ClaimPath {
263    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
264        f.write_str("[")?;
265        for (i, component) in self.0.iter().enumerate() {
266            if i > 0 {
267                f.write_str(", ")?;
268            }
269            write!(f, "{component}")?;
270        }
271        f.write_str("]")
272    }
273}
274
275/// A [`ClaimPath`](crate::vc::ClaimPath) from components of mixed kinds: string
276/// literals for names, integers for indexes, `()` for every element.
277///
278/// # Examples
279///
280/// ```
281/// use identity_kit::claim_path;
282/// use identity_kit::vc::{ClaimPath, PathComponent};
283///
284/// let path = claim_path!["degrees", (), "type"];
285/// assert_eq!(
286///     path,
287///     ClaimPath::from(vec![PathComponent::from("degrees"), PathComponent::All, PathComponent::from("type")])
288/// );
289/// assert_eq!(claim_path!["nationalities", 1].to_string(), r#"["nationalities", 1]"#);
290/// ```
291#[macro_export]
292macro_rules! claim_path {
293    ($($component:expr),* $(,)?) => {
294        $crate::vc::ClaimPath::from(vec![$($crate::vc::PathComponent::from($component)),*])
295    };
296}
297
298/// A [`ClaimPath`] component met a value it cannot select from.
299#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
300#[error("claim path {path}: component {depth} expects {expected}")]
301pub struct ClaimPathError {
302    /// The path.
303    pub path: ClaimPath,
304    /// The position of the offending component.
305    pub depth: usize,
306    /// What the component needs: `an object` or `an array`.
307    pub expected: &'static str,
308}