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}