aprender_contracts_cli/json_obj.rs
1//! JSON object construction that does not go through `serde_json::json!`.
2//!
3//! `serde_json::json!` expands to `Result::unwrap` internally, and this repo
4//! bans unwrap via `.clippy.toml` disallowed-methods (GH-41). The ban fired on
5//! the `pv` command surface only after that surface moved from `main.rs` into
6//! `lib.rs` — the diagnostics were real the whole time, just charged to a bin
7//! target nothing linted.
8//!
9//! The usual fix elsewhere in this workspace is a local
10//! `#[derive(serde::Serialize)]` struct (see
11//! `aprender-qa-cli/src/main_tickets_and_parity.rs::save_tool_results_json_or_exit`).
12//! `aprender-contracts-cli` depends on `serde_json` but not on `serde` itself,
13//! so it builds the `serde_json::Map` directly instead. That is the same
14//! representation `json!` produces — identical keys, identical key ordering
15//! (`Map` is the one canonical map type either way), identical nesting — with
16//! no unwrap anywhere.
17
18use serde_json::{Map, Value};
19
20/// Build a JSON object from an ordered list of `(key, value)` pairs.
21///
22/// Drop-in replacement for `serde_json::json!({ "k": v, ... })` where every
23/// key is a literal.
24pub fn obj<I>(pairs: I) -> Value
25where
26 I: IntoIterator<Item = (&'static str, Value)>,
27{
28 Value::Object(
29 pairs
30 .into_iter()
31 .map(|(k, v)| (k.to_owned(), v))
32 .collect::<Map<String, Value>>(),
33 )
34}
35
36#[cfg(test)]
37mod tests {
38 use super::obj;
39 use serde_json::Value;
40
41 #[test]
42 fn obj_builds_the_same_shape_as_the_json_macro() {
43 let built = obj([
44 ("name", Value::from("x")),
45 ("count", Value::from(3usize)),
46 ("ok", Value::from(true)),
47 ]);
48 let parsed: Value = serde_json::from_str(r#"{"name":"x","count":3,"ok":true}"#)
49 .expect("literal is valid JSON");
50 assert_eq!(built, parsed);
51 }
52
53 #[test]
54 fn obj_nests() {
55 let built = obj([("outer", obj([("inner", Value::from(1u64))]))]);
56 assert_eq!(built["outer"]["inner"], Value::from(1u64));
57 }
58
59 #[test]
60 fn obj_with_no_pairs_is_an_empty_object() {
61 let built = obj([]);
62 assert_eq!(built, Value::Object(serde_json::Map::new()));
63 }
64}