1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
//! 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",
);
}
}