use serde_json::{Map, Value};
use crate::error::{DetectionCandidate, FaceError};
pub const ITEMS_CANDIDATES: &[&str] = &["items", "results", "data", "hits", "matches", "scopes"];
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub struct ItemsOptions {
pub candidates: Vec<String>,
}
impl Default for ItemsOptions {
fn default() -> Self {
Self {
candidates: ITEMS_CANDIDATES
.iter()
.map(|candidate| (*candidate).to_string())
.collect(),
}
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ItemsDetection {
pub items: Vec<Value>,
pub items_path: String,
pub meta: Option<Map<String, Value>>,
}
pub fn detect_items(value: Value) -> Result<ItemsDetection, FaceError> {
detect_items_with_options(value, &ItemsOptions::default())
}
pub fn detect_items_with_options(
value: Value,
options: &ItemsOptions,
) -> Result<ItemsDetection, FaceError> {
match value {
Value::Array(items) => Ok(ItemsDetection {
items,
items_path: ".".to_string(),
meta: None,
}),
Value::Object(mut map) => detect_from_object(&mut map, options),
other => Err(FaceError::InputParse {
format: crate::InputFormat::Json,
message: format!(
"input is a {} scalar; clustering needs a JSON array or an object containing one",
value_kind(&other),
),
}),
}
}
fn detect_from_object(
map: &mut Map<String, Value>,
options: &ItemsOptions,
) -> Result<ItemsDetection, FaceError> {
let array_fields: Vec<String> = map
.iter()
.filter_map(|(k, v)| v.as_array().map(|_| k.clone()))
.collect();
let chosen = match array_fields.len() {
0 => {
return Err(FaceError::InputParse {
format: crate::InputFormat::Json,
message: "input object has no array-valued field; \
clustering needs an array of items"
.to_string(),
});
}
1 => array_fields.into_iter().next().expect("len == 1"),
_ => pick_named_or_ambiguous(&array_fields, &options.candidates)?,
};
let items = map
.remove(&chosen)
.and_then(|v| match v {
Value::Array(a) => Some(a),
_ => None,
})
.expect("array_fields was filtered by `as_array().is_some()`");
let mut meta = Map::new();
for (k, v) in map.iter() {
if !v.is_array() {
meta.insert(k.clone(), v.clone());
}
}
let meta = if meta.is_empty() { None } else { Some(meta) };
Ok(ItemsDetection {
items,
items_path: format!(".{chosen}"),
meta,
})
}
fn pick_named_or_ambiguous(
array_fields: &[String],
candidates: &[String],
) -> Result<String, FaceError> {
for candidate in candidates {
if array_fields.iter().any(|f| f == candidate) {
return Ok(candidate.clone());
}
}
let candidates = array_fields
.iter()
.map(|f| DetectionCandidate::new(format!(".{f}"), 0))
.collect();
Err(FaceError::AmbiguousDetection {
candidates,
numeric_fields: Vec::new(),
})
}
fn value_kind(v: &Value) -> &'static str {
match v {
Value::Null => "null",
Value::Bool(_) => "boolean",
Value::Number(_) => "number",
Value::String(_) => "string",
Value::Array(_) => "array",
Value::Object(_) => "object",
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn detects_bare_array_as_identity() {
let det = detect_items(json!([1, 2, 3])).unwrap();
assert_eq!(det.items_path, ".");
assert_eq!(det.items.len(), 3);
assert!(det.meta.is_none());
}
#[test]
fn detects_single_array_field() {
let det = detect_items(json!({
"matches": [{"a": 1}, {"a": 2}],
"took_ms": 12,
}))
.unwrap();
assert_eq!(det.items_path, ".matches");
assert_eq!(det.items.len(), 2);
let meta = det.meta.unwrap();
assert_eq!(meta.get("took_ms"), Some(&json!(12)));
}
#[test]
fn detects_named_items_array() {
let det = detect_items(json!({"items": [1, 2, 3]})).unwrap();
assert_eq!(det.items_path, ".items");
assert_eq!(det.items.len(), 3);
}
#[test]
fn prefers_named_candidate_over_other_arrays() {
let det = detect_items(json!({
"extras": [9, 9],
"results": [1, 2],
}))
.unwrap();
assert_eq!(det.items_path, ".results");
assert!(det.meta.is_none());
}
#[test]
fn ambiguous_when_multiple_unnamed() {
let err = detect_items(json!({
"alpha": [1, 2],
"beta": [3, 4],
}))
.unwrap_err();
match err {
FaceError::AmbiguousDetection { candidates, .. } => {
assert_eq!(candidates.len(), 2);
assert!(candidates.iter().any(|c| c.field == ".alpha"));
assert!(candidates.iter().any(|c| c.field == ".beta"));
}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn errors_when_object_has_no_array_field() {
let err = detect_items(json!({"a": 1, "b": "x"})).unwrap_err();
match err {
FaceError::InputParse { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn errors_on_scalar_input() {
let err = detect_items(json!(42)).unwrap_err();
match err {
FaceError::InputParse { .. } => {}
other => panic!("unexpected error: {other:?}"),
}
}
#[test]
fn meta_preserves_nested_objects() {
let det = detect_items(json!({
"items": [1],
"query": {"q": "hello", "limit": 10},
"took_ms": 5,
}))
.unwrap();
let meta = det.meta.unwrap();
assert_eq!(meta.get("query"), Some(&json!({"q": "hello", "limit": 10})));
assert_eq!(meta.get("took_ms"), Some(&json!(5)));
}
}