Skip to main content

identity_kit/sd_jwt_vc/
path.rs

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