1use std::collections::HashMap;
21
22#[derive(Debug, Clone, PartialEq)]
24pub enum IpldPathValue {
25 String(String),
27 Number(f64),
29 Array(Vec<IpldPathValue>),
31 Object(HashMap<String, IpldPathValue>),
33 Null,
35}
36
37impl IpldPathValue {
38 pub fn as_str(&self) -> Option<&str> {
40 match self {
41 IpldPathValue::String(s) => Some(s.as_str()),
42 _ => None,
43 }
44 }
45
46 pub fn as_number(&self) -> Option<f64> {
48 match self {
49 IpldPathValue::Number(n) => Some(*n),
50 _ => None,
51 }
52 }
53}
54
55#[derive(Debug, thiserror::Error)]
57pub enum PathError {
58 #[error("Invalid path: {0}")]
59 InvalidPath(String),
60 #[error("Path segment not found: {0}")]
61 NotFound(String),
62 #[error("Type mismatch at {0}: expected {1}")]
63 TypeMismatch(String, String),
64 #[error("Index out of bounds: {0}")]
65 IndexOutOfBounds(usize),
66 #[error("Deserialization error: {0}")]
67 Deserialization(String),
68}
69
70pub struct IpldPathResolver;
74
75impl IpldPathResolver {
76 pub fn resolve_rule_path(block_data: &[u8], path: &str) -> Result<IpldPathValue, PathError> {
92 let segments = Self::parse_path(path);
93
94 if segments.len() < 2 {
97 return Err(PathError::InvalidPath(format!(
98 "Rule path must start with /rule/<cid>/…; got: {}",
99 path
100 )));
101 }
102 if segments[0] != "rule" {
103 return Err(PathError::InvalidPath(format!(
104 "Expected 'rule' as first segment, got '{}'",
105 segments[0]
106 )));
107 }
108
109 let root: serde_json::Value = serde_json::from_slice(block_data)
110 .map_err(|e| PathError::Deserialization(e.to_string()))?;
111
112 let traversal = &segments[2..];
114 Self::traverse(&root, traversal)
115 }
116
117 pub fn resolve_fact_path(block_data: &[u8], path: &str) -> Result<IpldPathValue, PathError> {
125 let segments = Self::parse_path(path);
126
127 if segments.len() < 2 {
128 return Err(PathError::InvalidPath(format!(
129 "Fact path must start with /fact/<cid>/…; got: {}",
130 path
131 )));
132 }
133 if segments[0] != "fact" {
134 return Err(PathError::InvalidPath(format!(
135 "Expected 'fact' as first segment, got '{}'",
136 segments[0]
137 )));
138 }
139
140 let root: serde_json::Value = serde_json::from_slice(block_data)
141 .map_err(|e| PathError::Deserialization(e.to_string()))?;
142
143 let traversal = &segments[2..];
144 Self::traverse(&root, traversal)
145 }
146
147 pub fn parse_path(path: &str) -> Vec<String> {
152 path.split('/')
153 .filter(|s| !s.is_empty())
154 .map(|s| s.to_string())
155 .collect()
156 }
157
158 fn traverse(
162 current: &serde_json::Value,
163 segments: &[String],
164 ) -> Result<IpldPathValue, PathError> {
165 if segments.is_empty() {
166 return Self::json_to_ipld(current);
167 }
168
169 let seg = &segments[0];
170 let rest = &segments[1..];
171
172 match current {
173 serde_json::Value::Object(map) => {
174 let child = map.get(seg.as_str()).ok_or_else(|| {
175 PathError::NotFound(format!("Key '{}' not found in object", seg))
176 })?;
177 Self::traverse(child, rest)
178 }
179 serde_json::Value::Array(arr) => {
180 let idx: usize = seg.parse().map_err(|_| {
181 PathError::TypeMismatch(
182 seg.clone(),
183 "numeric index for array traversal".to_string(),
184 )
185 })?;
186 let child = arr.get(idx).ok_or(PathError::IndexOutOfBounds(idx))?;
187 Self::traverse(child, rest)
188 }
189 other => Err(PathError::TypeMismatch(
190 seg.clone(),
191 format!(
192 "object or array (cannot descend into {})",
193 Self::json_type_name(other)
194 ),
195 )),
196 }
197 }
198
199 fn json_to_ipld(value: &serde_json::Value) -> Result<IpldPathValue, PathError> {
201 match value {
202 serde_json::Value::Null => Ok(IpldPathValue::Null),
203 serde_json::Value::Bool(b) => Ok(IpldPathValue::Number(if *b { 1.0 } else { 0.0 })),
204 serde_json::Value::Number(n) => {
205 let f = n.as_f64().ok_or_else(|| {
206 PathError::Deserialization(format!("Cannot convert number {} to f64", n))
207 })?;
208 Ok(IpldPathValue::Number(f))
209 }
210 serde_json::Value::String(s) => Ok(IpldPathValue::String(s.clone())),
211 serde_json::Value::Array(arr) => {
212 let items = arr
213 .iter()
214 .map(Self::json_to_ipld)
215 .collect::<Result<Vec<_>, _>>()?;
216 Ok(IpldPathValue::Array(items))
217 }
218 serde_json::Value::Object(map) => {
219 let kv = map
220 .iter()
221 .map(|(k, v)| Self::json_to_ipld(v).map(|ipld| (k.clone(), ipld)))
222 .collect::<Result<HashMap<_, _>, _>>()?;
223 Ok(IpldPathValue::Object(kv))
224 }
225 }
226 }
227
228 fn json_type_name(v: &serde_json::Value) -> &'static str {
230 match v {
231 serde_json::Value::Null => "null",
232 serde_json::Value::Bool(_) => "bool",
233 serde_json::Value::Number(_) => "number",
234 serde_json::Value::String(_) => "string",
235 serde_json::Value::Array(_) => "array",
236 serde_json::Value::Object(_) => "object",
237 }
238 }
239}
240
241#[cfg(test)]
244mod tests {
245 use super::*;
246 use crate::ipld_codec::{
247 fact_to_block, predicate_to_fact_ipld, rule_to_block, rule_to_rule_ipld,
248 };
249 use crate::ir::{Constant, Predicate, Rule, Term};
250
251 fn grandparent_rule() -> Rule {
253 let head = Predicate::new(
254 "grandparent".to_string(),
255 vec![Term::Var("X".to_string()), Term::Var("Z".to_string())],
256 );
257 let body = vec![
258 Predicate::new(
259 "parent".to_string(),
260 vec![Term::Var("X".to_string()), Term::Var("Y".to_string())],
261 ),
262 Predicate::new(
263 "parent".to_string(),
264 vec![Term::Var("Y".to_string()), Term::Var("Z".to_string())],
265 ),
266 ];
267 Rule::new(head, body)
268 }
269
270 fn rule_bytes(rule: &Rule) -> Vec<u8> {
272 let ipld = rule_to_rule_ipld(rule).expect("rule_to_rule_ipld");
273 let block = rule_to_block(&ipld).expect("rule_to_block");
274 block.data().to_vec()
275 }
276
277 fn fact_bytes(pred: &Predicate) -> Vec<u8> {
279 let ipld = predicate_to_fact_ipld(pred).expect("predicate_to_fact_ipld");
280 let block = fact_to_block(&ipld).expect("fact_to_block");
281 block.data().to_vec()
282 }
283
284 #[test]
285 fn test_resolve_head_functor() {
286 let rule = grandparent_rule();
287 let data = rule_bytes(&rule);
288 let cid = "bafktest000";
289
290 let val =
291 IpldPathResolver::resolve_rule_path(&data, &format!("/rule/{}/head/functor", cid))
292 .expect("resolve head/functor");
293
294 assert_eq!(val, IpldPathValue::String("grandparent".to_string()));
295 }
296
297 #[test]
298 fn test_resolve_head_arg_by_index() {
299 let rule = grandparent_rule();
300 let data = rule_bytes(&rule);
301 let cid = "bafktest001";
302
303 let val0 =
305 IpldPathResolver::resolve_rule_path(&data, &format!("/rule/{}/head/args/0/name", cid))
306 .expect("head/args/0/name");
307
308 assert_eq!(val0, IpldPathValue::String("X".to_string()));
309
310 let val1 =
311 IpldPathResolver::resolve_rule_path(&data, &format!("/rule/{}/head/args/1/name", cid))
312 .expect("head/args/1/name");
313
314 assert_eq!(val1, IpldPathValue::String("Z".to_string()));
315 }
316
317 #[test]
318 fn test_resolve_body_goal() {
319 let rule = grandparent_rule();
320 let data = rule_bytes(&rule);
321 let cid = "bafktest002";
322
323 let val =
325 IpldPathResolver::resolve_rule_path(&data, &format!("/rule/{}/body/0/functor", cid))
326 .expect("body/0/functor");
327
328 assert_eq!(val, IpldPathValue::String("parent".to_string()));
329
330 let val2 =
332 IpldPathResolver::resolve_rule_path(&data, &format!("/rule/{}/body/1/functor", cid))
333 .expect("body/1/functor");
334
335 assert_eq!(val2, IpldPathValue::String("parent".to_string()));
336 }
337
338 #[test]
339 fn test_invalid_path_error_wrong_prefix() {
340 let rule = grandparent_rule();
341 let data = rule_bytes(&rule);
342
343 let err = IpldPathResolver::resolve_rule_path(&data, "/fact/bafk/head").unwrap_err();
344 assert!(
345 matches!(err, PathError::InvalidPath(_)),
346 "Expected InvalidPath, got {:?}",
347 err
348 );
349 }
350
351 #[test]
352 fn test_invalid_path_too_short() {
353 let rule = grandparent_rule();
354 let data = rule_bytes(&rule);
355
356 let err = IpldPathResolver::resolve_rule_path(&data, "/rule").unwrap_err();
357 assert!(
358 matches!(err, PathError::InvalidPath(_)),
359 "Expected InvalidPath for short path, got {:?}",
360 err
361 );
362 }
363
364 #[test]
365 fn test_index_out_of_bounds() {
366 let rule = grandparent_rule();
367 let data = rule_bytes(&rule);
368 let cid = "bafktest003";
369
370 let err =
372 IpldPathResolver::resolve_rule_path(&data, &format!("/rule/{}/head/args/99", cid))
373 .unwrap_err();
374
375 assert!(
376 matches!(err, PathError::IndexOutOfBounds(99)),
377 "Expected IndexOutOfBounds(99), got {:?}",
378 err
379 );
380 }
381
382 #[test]
383 fn test_key_not_found() {
384 let rule = grandparent_rule();
385 let data = rule_bytes(&rule);
386 let cid = "bafktest004";
387
388 let err = IpldPathResolver::resolve_rule_path(
389 &data,
390 &format!("/rule/{}/head/nonexistent_key", cid),
391 )
392 .unwrap_err();
393
394 assert!(
395 matches!(err, PathError::NotFound(_)),
396 "Expected NotFound, got {:?}",
397 err
398 );
399 }
400
401 #[test]
402 fn test_fact_path_predicate() {
403 let pred = Predicate::new(
404 "parent".to_string(),
405 vec![
406 Term::Const(Constant::String("alice".to_string())),
407 Term::Const(Constant::String("bob".to_string())),
408 ],
409 );
410 let data = fact_bytes(&pred);
411 let cid = "bafktest005";
412
413 let val = IpldPathResolver::resolve_fact_path(&data, &format!("/fact/{}/predicate", cid))
414 .expect("fact/predicate");
415
416 assert_eq!(val, IpldPathValue::String("parent".to_string()));
417 }
418
419 #[test]
420 fn test_fact_path_arg_by_index() {
421 let pred = Predicate::new(
422 "likes".to_string(),
423 vec![
424 Term::Const(Constant::String("alice".to_string())),
425 Term::Const(Constant::String("chocolate".to_string())),
426 ],
427 );
428 let data = fact_bytes(&pred);
429 let cid = "bafktest006";
430
431 let val =
433 IpldPathResolver::resolve_fact_path(&data, &format!("/fact/{}/args/1/value", cid))
434 .expect("fact/args/1/value");
435
436 assert_eq!(val, IpldPathValue::String("chocolate".to_string()));
437 }
438
439 #[test]
440 fn test_parse_path_strips_leading_slash() {
441 let segments = IpldPathResolver::parse_path("/rule/bafk/head/args/0");
442 assert_eq!(segments, vec!["rule", "bafk", "head", "args", "0"]);
443 }
444
445 #[test]
446 fn test_parse_path_no_leading_slash() {
447 let segments = IpldPathResolver::parse_path("rule/bafk/head");
448 assert_eq!(segments, vec!["rule", "bafk", "head"]);
449 }
450
451 #[test]
452 fn test_rule_body_arg_resolution() {
453 let rule = grandparent_rule();
454 let data = rule_bytes(&rule);
455 let cid = "bafktest007";
456
457 let val = IpldPathResolver::resolve_rule_path(
459 &data,
460 &format!("/rule/{}/body/0/args/0/name", cid),
461 )
462 .expect("body/0/args/0/name");
463
464 assert_eq!(val, IpldPathValue::String("X".to_string()));
465 }
466}