1use serde_yaml::Value;
18
19use crate::error::NapError;
20
21pub struct ManifestQuery;
23
24impl ManifestQuery {
25 pub fn query(root: &Value, path: &str, manifest_id: &str) -> Result<Value, NapError> {
50 if path.is_empty() {
51 return Err(NapError::InvalidQueryPath(
52 "query path cannot be empty".to_string(),
53 ));
54 }
55
56 let segments: Vec<&str> = path.split('.').collect();
57 let mut current = root;
58
59 for (depth, segment) in segments.iter().enumerate() {
60 if segment.is_empty() {
61 return Err(NapError::InvalidQueryPath(format!(
62 "empty segment at position {depth} in path '{path}'"
63 )));
64 }
65
66 current = match current {
67 Value::Mapping(map) => {
68 let key = Value::String(segment.to_string());
69 map.get(&key).ok_or_else(|| {
70 let traversed = segments[..depth].join(".");
71 let available_keys: Vec<String> = map
72 .keys()
73 .filter_map(|k| k.as_str().map(String::from))
74 .collect();
75 tracing::debug!(
76 manifest_id = manifest_id,
77 path = path,
78 segment = segment,
79 traversed = traversed,
80 available_keys = ?available_keys,
81 "query segment not found in mapping"
82 );
83 NapError::QueryPathNotFound {
84 path: path.to_string(),
85 manifest_id: manifest_id.to_string(),
86 }
87 })?
88 }
89 Value::Sequence(seq) => {
90 let index: usize =
92 segment.parse().map_err(|_| NapError::QueryPathNotFound {
93 path: path.to_string(),
94 manifest_id: manifest_id.to_string(),
95 })?;
96 seq.get(index).ok_or_else(|| NapError::QueryPathNotFound {
97 path: path.to_string(),
98 manifest_id: manifest_id.to_string(),
99 })?
100 }
101 _ => {
102 return Err(NapError::QueryPathNotFound {
103 path: path.to_string(),
104 manifest_id: manifest_id.to_string(),
105 });
106 }
107 };
108 }
109
110 Ok(current.clone())
111 }
112
113 pub fn list_keys(root: &Value, path: &str, manifest_id: &str) -> Result<Vec<String>, NapError> {
115 let target = if path.is_empty() {
116 root.clone()
117 } else {
118 Self::query(root, path, manifest_id)?
119 };
120
121 match target {
122 Value::Mapping(map) => Ok(map
123 .keys()
124 .filter_map(|k| k.as_str().map(String::from))
125 .collect()),
126 Value::Sequence(seq) => Ok((0..seq.len()).map(|i| i.to_string()).collect()),
127 _ => Ok(vec![]),
128 }
129 }
130}
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 fn test_manifest() -> Value {
137 let yaml = r#"
138id: "nap://starwars/character/lukeskywalker"
139name: "Luke Skywalker"
140entity_type: character
141version: 17
142properties:
143 homeworld: "nap://starwars/location/tatooine"
144 species: human
145 affiliation: rebel_alliance
146representations:
147 reference_image:
148 hash: "blake3:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
149 format: png
150references:
151 appears_in:
152 - "nap://starwars/scene/cantina"
153 - "nap://starwars/scene/trench-run"
154 relationships:
155 - target: "nap://starwars/character/darthvader"
156 type: parent
157"#;
158 serde_yaml::from_str(yaml).unwrap()
159 }
160
161 #[test]
162 fn test_query_simple_property() {
163 let root = test_manifest();
164 let result = ManifestQuery::query(&root, "properties.species", "test").unwrap();
165 assert_eq!(result, Value::String("human".to_string()));
166 }
167
168 #[test]
169 fn test_query_nested_representation() {
170 let root = test_manifest();
171 let result =
172 ManifestQuery::query(&root, "representations.reference_image.hash", "test").unwrap();
173 assert_eq!(
174 result,
175 Value::String(
176 "blake3:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
177 .to_string()
178 )
179 );
180 }
181
182 #[test]
183 fn test_query_array() {
184 let root = test_manifest();
185 let result = ManifestQuery::query(&root, "references.appears_in", "test").unwrap();
186 match result {
187 Value::Sequence(seq) => assert_eq!(seq.len(), 2),
188 _ => panic!("expected sequence"),
189 }
190 }
191
192 #[test]
193 fn test_query_array_index() {
194 let root = test_manifest();
195 let result = ManifestQuery::query(&root, "references.appears_in.0", "test").unwrap();
196 assert_eq!(
197 result,
198 Value::String("nap://starwars/scene/cantina".to_string())
199 );
200 }
201
202 #[test]
203 fn test_query_not_found() {
204 let root = test_manifest();
205 let result = ManifestQuery::query(&root, "properties.nonexistent", "test");
206 assert!(result.is_err());
207 }
208
209 #[test]
210 fn test_query_empty_path() {
211 let root = test_manifest();
212 let result = ManifestQuery::query(&root, "", "test");
213 assert!(result.is_err());
214 }
215
216 #[test]
217 fn test_list_keys_root() {
218 let root = test_manifest();
219 let keys = ManifestQuery::list_keys(&root, "", "test").unwrap();
220 assert!(keys.contains(&"properties".to_string()));
221 assert!(keys.contains(&"representations".to_string()));
222 }
223
224 #[test]
225 fn test_list_keys_nested() {
226 let root = test_manifest();
227 let keys = ManifestQuery::list_keys(&root, "properties", "test").unwrap();
228 assert!(keys.contains(&"species".to_string()));
229 assert!(keys.contains(&"homeworld".to_string()));
230 }
231}