face-core 0.1.0

Core grouping, clustering, and paging primitives for the face CLI.
Documentation
//! Integration tests for items-array detection (§4.2 of `docs/design.md`).
//!
//! The `parse` pipeline exercises this indirectly, but the candidate
//! priority order and ambiguity error matrix are easier to pin against
//! the public detection function directly.
//!
//! Public surface under test (locked by the developer brief):
//!
//! ```ignore
//! pub struct ItemsDetection {
//!     pub items: Vec<serde_json::Value>,
//!     pub items_path: String,
//!     pub meta: Option<serde_json::Map<String, serde_json::Value>>,
//! }
//! pub fn detect_items(value: serde_json::Value) -> Result<ItemsDetection, FaceError>;
//! ```

use face_core::FaceError;
use face_core::input::items::{ItemsDetection, detect_items};

mod simple_shapes {
    //! Unambiguous shapes — array, single array field, named field.

    use super::*;

    #[test]
    fn top_level_array() {
        let v = serde_json::json!([1, 2, 3]);
        let det: ItemsDetection = detect_items(v).expect("top-level array detects");
        assert_eq!(det.items.len(), 3);
        assert_eq!(det.items[0], serde_json::json!(1));
        assert_eq!(
            det.items_path, ".",
            "top-level array's items_path is the identity path `.`",
        );
        assert!(
            det.meta.is_none(),
            "top-level array carries no meta scalars; got {:?}",
            det.meta,
        );
    }

    #[test]
    fn object_one_array_field() {
        let v = serde_json::json!({"results": [1, 2, 3]});
        let det = detect_items(v).expect("single array field detects");
        assert_eq!(det.items.len(), 3);
        assert_eq!(det.items_path, ".results");
    }

    #[test]
    fn object_with_named_items_among_unnamed() {
        // `items` is in the canonical candidate list; `extras` is not.
        // Named candidates are preferred over arbitrary array fields.
        let v = serde_json::json!({"items": [1], "extras": [2]});
        let det = detect_items(v).expect("named candidate beats unnamed sibling");
        assert_eq!(det.items, vec![serde_json::json!(1)]);
        assert_eq!(det.items_path, ".items");
    }
}

mod named_priority {
    //! §4.2 candidate priority: items > results > data > hits > matches > scopes.
    //!
    //! When more than one named candidate appears, the earliest in this
    //! list wins.

    use super::*;

    #[test]
    fn items_beats_results() {
        let v = serde_json::json!({"items": [1], "results": [2]});
        let det = detect_items(v).expect("items beats results");
        assert_eq!(det.items_path, ".items");
        assert_eq!(det.items, vec![serde_json::json!(1)]);
    }

    #[test]
    fn results_beats_data() {
        let v = serde_json::json!({"results": [1], "data": [2]});
        let det = detect_items(v).expect("results beats data");
        assert_eq!(det.items_path, ".results");
        assert_eq!(det.items, vec![serde_json::json!(1)]);
    }

    #[test]
    fn last_candidate_picked_when_first_absent() {
        // When only `scopes` (the last candidate) plus an arbitrary
        // unnamed array are present, the named candidate still wins.
        let v = serde_json::json!({"scopes": [1], "anything": [2]});
        let det = detect_items(v).expect("named scopes beats unnamed sibling");
        assert_eq!(det.items_path, ".scopes");
        assert_eq!(det.items, vec![serde_json::json!(1)]);
    }
}

mod ambiguous {
    //! §4.2: when multiple unnamed array fields exist, detection errors
    //! with the candidates so the user can pick one explicitly.

    use super::*;

    #[test]
    fn multiple_unnamed_arrays_errors() {
        let v = serde_json::json!({"foo": [1], "bar": [2]});
        let err = detect_items(v).expect_err("multiple unnamed arrays must error");
        match &err {
            FaceError::AmbiguousDetection { candidates, .. } => {
                let fields: Vec<&str> = candidates.iter().map(|c| c.field.as_str()).collect();
                assert!(
                    fields.iter().any(|f| *f == "foo" || *f == ".foo"),
                    "candidates must include foo (or .foo); got {fields:?}",
                );
                assert!(
                    fields.iter().any(|f| *f == "bar" || *f == ".bar"),
                    "candidates must include bar (or .bar); got {fields:?}",
                );
                // Don't pin cardinality — slice 4 may or may not populate
                // it from the array length.
            }
            other => panic!("expected FaceError::AmbiguousDetection, got {other:?} ({other})",),
        }
    }
}

mod scalar_inputs {
    //! Scalar root values cannot host items — they error with
    //! [`FaceError::InputParse`].

    use super::*;

    #[test]
    fn scalar_string_errors() {
        let v = serde_json::json!("hello");
        let err = detect_items(v).expect_err("scalar string cannot host items");
        match &err {
            FaceError::InputParse { .. } => {}
            other => panic!(
                "expected FaceError::InputParse for scalar string root, got {other:?} ({other})",
            ),
        }
    }

    #[test]
    fn scalar_number_errors() {
        let v = serde_json::json!(42);
        let err = detect_items(v).expect_err("scalar number cannot host items");
        match &err {
            FaceError::InputParse { .. } => {}
            other => panic!(
                "expected FaceError::InputParse for scalar number root, got {other:?} ({other})",
            ),
        }
    }
}