face-core 0.1.0

Core grouping, clustering, and paging primitives for the face CLI.
Documentation
//! Integration tests for jq-like path resolution (§4.2 of `docs/design.md`).
//!
//! Pins the contract for [`face_core::path::resolve`] and
//! [`face_core::path::resolve_owned`]: dotted segments, bracketed
//! indices, error matrix, and the borrowed-vs-owned split.
//!
//! Public surface under test (locked by the developer brief):
//!
//! ```ignore
//! pub fn resolve<'v>(value: &'v serde_json::Value, path: &str)
//!     -> Result<&'v serde_json::Value, FaceError>;
//! pub fn resolve_owned(value: &serde_json::Value, path: &str)
//!     -> Result<serde_json::Value, FaceError>;
//! ```

use face_core::FaceError;
use face_core::path::{resolve, resolve_owned};
use serde_json::json;

mod dotted {
    //! Object-key navigation via dotted segments.

    use super::*;

    #[test]
    fn identity() {
        let v = json!({"a": 1});
        let got = resolve(&v, ".").expect("identity path resolves");
        assert_eq!(got, &v);
    }

    #[test]
    fn single_level() {
        let v = json!({"a": 1});
        let got = resolve(&v, ".a").expect("single-level lookup resolves");
        assert_eq!(got, &json!(1));
    }

    #[test]
    fn nested_two_levels() {
        let v = json!({"a": {"b": "x"}});
        let got = resolve(&v, ".a.b").expect("two-level lookup resolves");
        assert_eq!(got, &json!("x"));
    }

    #[test]
    fn rg_style_nested() {
        // `rg --json` emits `{"data": {"path": {"text": "..."}}}`.
        // The leading `.` is optional — both forms resolve.
        let v = json!({"data": {"path": {"text": "src/cli.rs"}}});
        let got =
            resolve(&v, "data.path.text").expect("rg-style path without leading dot resolves");
        assert_eq!(got, &json!("src/cli.rs"));
    }

    #[test]
    fn value_at_intermediate() {
        // Resolving to an intermediate object returns the object itself
        // as a borrowed `Value`.
        let v = json!({"a": {"b": 1}});
        let got = resolve(&v, ".a").expect("intermediate-level lookup resolves");
        assert_eq!(got, &json!({"b": 1}));
        assert!(got.is_object(), "intermediate must come back as the object");
    }
}

mod indices {
    //! Bracketed integer indices on arrays.

    use super::*;

    #[test]
    fn top_level_array_index() {
        // CONTRACT NOTE: the brief lists `.[1]` as the top-level array
        // syntax; if the developer required something different (e.g. a
        // key before `[`, no top-level index syntax at all), this test
        // surfaces the mismatch as a finding rather than a test bug.
        let v = json!([1, 2, 3]);
        let got = resolve(&v, ".[1]").expect("top-level array index resolves");
        assert_eq!(got, &json!(2));
    }

    #[test]
    fn nested_array_index() {
        let v = json!({"items": [10, 20, 30]});
        let got = resolve(&v, ".items[2]").expect("nested array index resolves");
        assert_eq!(got, &json!(30));
    }

    #[test]
    fn chained_array_object() {
        let v = json!({"a": [{"b": "x"}, {"b": "y"}]});
        let got = resolve(&v, ".a[1].b").expect("chained index + object resolves");
        assert_eq!(got, &json!("y"));
    }
}

mod errors {
    //! Error matrix: missing keys, out-of-range indices, type
    //! mismatches.

    use super::*;

    /// Helper: assert the result is an `Err`. The exact `FaceError`
    /// variant is left to the developer (`UnknownItemsPath`,
    /// `InputParse`, etc.); the test author's contract only pins
    /// `Result<_, FaceError>`. A more-precise assertion would cement
    /// implementation choices we don't want to lock yet.
    fn assert_path_err(result: Result<&serde_json::Value, FaceError>, label: &str) -> FaceError {
        match result {
            Ok(v) => panic!("[{label}] expected Err, got Ok({v})"),
            Err(e) => e,
        }
    }

    #[test]
    fn unknown_object_key() {
        let v = json!({"a": 1});
        let err = assert_path_err(resolve(&v, ".b"), "unknown_object_key");
        // The path string should appear somewhere in the error so users
        // can identify which lookup failed.
        let msg = err.to_string();
        assert!(
            msg.contains(".b") || msg.contains("\"b\"") || msg.contains("`b`"),
            "error should reference the failed path `.b`; got: {msg}",
        );
    }

    #[test]
    fn index_out_of_range() {
        let v = json!([1, 2]);
        let _ = assert_path_err(resolve(&v, ".[5]"), "index_out_of_range");
    }

    #[test]
    fn index_into_object_errors() {
        let v = json!({"a": 1});
        let _ = assert_path_err(resolve(&v, ".[0]"), "index_into_object_errors");
    }

    #[test]
    fn key_into_array_errors() {
        let v = json!([1, 2]);
        let _ = assert_path_err(resolve(&v, ".foo"), "key_into_array_errors");
    }
}

mod resolve_owned {
    //! [`resolve_owned`] returns an owned `Value` clone of the resolved
    //! sub-value, so the caller can drop the original and keep the
    //! resolved node.

    use super::*;

    #[test]
    fn clones_when_required() {
        // Build the input, resolve, drop the input, and verify the owned
        // value still carries the resolved data.
        let original = json!({"a": 1});
        let owned = resolve_owned(&original, ".a").expect("owned resolve succeeds");
        drop(original);
        assert_eq!(
            owned,
            json!(1),
            "resolve_owned must return a value that survives drop of the input",
        );
    }
}