1use serde_json::{Map, Value};
8
9use crate::error::{DetectionCandidate, FaceError};
10
11pub const ITEMS_CANDIDATES: &[&str] = &["items", "results", "data", "hits", "matches", "scopes"];
16
17#[derive(Debug, Clone, PartialEq, Eq)]
19#[non_exhaustive]
20pub struct ItemsOptions {
21 pub candidates: Vec<String>,
24}
25
26impl Default for ItemsOptions {
27 fn default() -> Self {
28 Self {
29 candidates: ITEMS_CANDIDATES
30 .iter()
31 .map(|candidate| (*candidate).to_string())
32 .collect(),
33 }
34 }
35}
36
37#[derive(Debug, Clone, PartialEq)]
39#[non_exhaustive]
40pub struct ItemsDetection {
41 pub items: Vec<Value>,
43 pub items_path: String,
46 pub meta: Option<Map<String, Value>>,
50}
51
52pub fn detect_items(value: Value) -> Result<ItemsDetection, FaceError> {
72 detect_items_with_options(value, &ItemsOptions::default())
73}
74
75pub fn detect_items_with_options(
77 value: Value,
78 options: &ItemsOptions,
79) -> Result<ItemsDetection, FaceError> {
80 match value {
81 Value::Array(items) => Ok(ItemsDetection {
82 items,
83 items_path: ".".to_string(),
84 meta: None,
85 }),
86 Value::Object(mut map) => detect_from_object(&mut map, options),
87 other => Err(FaceError::InputParse {
88 format: crate::InputFormat::Json,
89 message: format!(
90 "input is a {} scalar; clustering needs a JSON array or an object containing one",
91 value_kind(&other),
92 ),
93 }),
94 }
95}
96
97fn detect_from_object(
99 map: &mut Map<String, Value>,
100 options: &ItemsOptions,
101) -> Result<ItemsDetection, FaceError> {
102 let array_fields: Vec<String> = map
104 .iter()
105 .filter_map(|(k, v)| v.as_array().map(|_| k.clone()))
106 .collect();
107
108 let chosen = match array_fields.len() {
109 0 => {
110 return Err(FaceError::InputParse {
111 format: crate::InputFormat::Json,
112 message: "input object has no array-valued field; \
113 clustering needs an array of items"
114 .to_string(),
115 });
116 }
117 1 => array_fields.into_iter().next().expect("len == 1"),
118 _ => pick_named_or_ambiguous(&array_fields, &options.candidates)?,
119 };
120
121 let items = map
123 .remove(&chosen)
124 .and_then(|v| match v {
125 Value::Array(a) => Some(a),
126 _ => None,
127 })
128 .expect("array_fields was filtered by `as_array().is_some()`");
129
130 let mut meta = Map::new();
131 for (k, v) in map.iter() {
132 if !v.is_array() {
133 meta.insert(k.clone(), v.clone());
134 }
135 }
136 let meta = if meta.is_empty() { None } else { Some(meta) };
140
141 Ok(ItemsDetection {
142 items,
143 items_path: format!(".{chosen}"),
144 meta,
145 })
146}
147
148fn pick_named_or_ambiguous(
151 array_fields: &[String],
152 candidates: &[String],
153) -> Result<String, FaceError> {
154 for candidate in candidates {
155 if array_fields.iter().any(|f| f == candidate) {
156 return Ok(candidate.clone());
157 }
158 }
159
160 let candidates = array_fields
161 .iter()
162 .map(|f| DetectionCandidate::new(format!(".{f}"), 0))
163 .collect();
164 Err(FaceError::AmbiguousDetection {
165 candidates,
166 numeric_fields: Vec::new(),
167 })
168}
169
170fn value_kind(v: &Value) -> &'static str {
172 match v {
173 Value::Null => "null",
174 Value::Bool(_) => "boolean",
175 Value::Number(_) => "number",
176 Value::String(_) => "string",
177 Value::Array(_) => "array",
178 Value::Object(_) => "object",
179 }
180}
181
182#[cfg(test)]
183mod tests {
184 use super::*;
185 use serde_json::json;
186
187 #[test]
188 fn detects_bare_array_as_identity() {
189 let det = detect_items(json!([1, 2, 3])).unwrap();
190 assert_eq!(det.items_path, ".");
191 assert_eq!(det.items.len(), 3);
192 assert!(det.meta.is_none());
193 }
194
195 #[test]
196 fn detects_single_array_field() {
197 let det = detect_items(json!({
198 "matches": [{"a": 1}, {"a": 2}],
199 "took_ms": 12,
200 }))
201 .unwrap();
202 assert_eq!(det.items_path, ".matches");
203 assert_eq!(det.items.len(), 2);
204 let meta = det.meta.unwrap();
205 assert_eq!(meta.get("took_ms"), Some(&json!(12)));
206 }
207
208 #[test]
209 fn detects_named_items_array() {
210 let det = detect_items(json!({"items": [1, 2, 3]})).unwrap();
211 assert_eq!(det.items_path, ".items");
212 assert_eq!(det.items.len(), 3);
213 }
214
215 #[test]
216 fn prefers_named_candidate_over_other_arrays() {
217 let det = detect_items(json!({
218 "extras": [9, 9],
219 "results": [1, 2],
220 }))
221 .unwrap();
222 assert_eq!(det.items_path, ".results");
223 assert!(det.meta.is_none());
226 }
227
228 #[test]
229 fn ambiguous_when_multiple_unnamed() {
230 let err = detect_items(json!({
231 "alpha": [1, 2],
232 "beta": [3, 4],
233 }))
234 .unwrap_err();
235 match err {
236 FaceError::AmbiguousDetection { candidates, .. } => {
237 assert_eq!(candidates.len(), 2);
238 assert!(candidates.iter().any(|c| c.field == ".alpha"));
239 assert!(candidates.iter().any(|c| c.field == ".beta"));
240 }
241 other => panic!("unexpected error: {other:?}"),
242 }
243 }
244
245 #[test]
246 fn errors_when_object_has_no_array_field() {
247 let err = detect_items(json!({"a": 1, "b": "x"})).unwrap_err();
248 match err {
249 FaceError::InputParse { .. } => {}
250 other => panic!("unexpected error: {other:?}"),
251 }
252 }
253
254 #[test]
255 fn errors_on_scalar_input() {
256 let err = detect_items(json!(42)).unwrap_err();
257 match err {
258 FaceError::InputParse { .. } => {}
259 other => panic!("unexpected error: {other:?}"),
260 }
261 }
262
263 #[test]
264 fn meta_preserves_nested_objects() {
265 let det = detect_items(json!({
266 "items": [1],
267 "query": {"q": "hello", "limit": 10},
268 "took_ms": 5,
269 }))
270 .unwrap();
271 let meta = det.meta.unwrap();
272 assert_eq!(meta.get("query"), Some(&json!({"q": "hello", "limit": 10})));
273 assert_eq!(meta.get("took_ms"), Some(&json!(5)));
274 }
275}