packc/cli/
inspect_lock.rs1#![forbid(unsafe_code)]
2
3use std::path::{Path, PathBuf};
4
5use anyhow::{Context, Result};
6use clap::Args;
7use greentic_pack::pack_lock::read_pack_lock;
8use serde::Serialize;
9use serde_json::Value;
10
11#[derive(Debug, Args)]
12pub struct InspectLockArgs {
13 #[arg(long = "in", value_name = "DIR", default_value = ".")]
15 pub input: PathBuf,
16
17 #[arg(long = "lock", value_name = "FILE")]
19 pub lock: Option<PathBuf>,
20}
21
22pub fn handle(args: InspectLockArgs) -> Result<()> {
23 let pack_dir = args
24 .input
25 .canonicalize()
26 .with_context(|| format!("failed to resolve pack dir {}", args.input.display()))?;
27 let lock_path = resolve_lock_path(&pack_dir, args.lock.as_deref());
28 let lock = read_pack_lock(&lock_path)
29 .with_context(|| format!("failed to read {}", lock_path.display()))?;
30
31 let json = to_sorted_json(&lock)?;
32 println!("{json}");
33 Ok(())
34}
35
36fn resolve_lock_path(pack_dir: &Path, override_path: Option<&Path>) -> PathBuf {
37 match override_path {
38 Some(path) if path.is_absolute() => path.to_path_buf(),
39 Some(path) => pack_dir.join(path),
40 None => pack_dir.join("pack.lock.cbor"),
41 }
42}
43
44fn to_sorted_json<T: Serialize>(value: &T) -> Result<String> {
45 let value = serde_json::to_value(value).context("encode lock to json")?;
46 let sorted = sort_json(value);
47 serde_json::to_string_pretty(&sorted).context("serialize json")
48}
49
50fn sort_json(value: Value) -> Value {
51 match value {
52 Value::Object(map) => {
53 let mut entries: Vec<(String, Value)> = map.into_iter().collect();
54 entries.sort_by(|a, b| a.0.cmp(&b.0));
55 let mut sorted = serde_json::Map::new();
56 for (key, value) in entries {
57 sorted.insert(key, sort_json(value));
58 }
59 Value::Object(sorted)
60 }
61 Value::Array(values) => Value::Array(values.into_iter().map(sort_json).collect()),
62 other => other,
63 }
64}
65
66#[cfg(test)]
67mod tests {
68 use super::*;
69 use serde_json::json;
70 use tempfile::tempdir;
71
72 #[test]
73 fn resolve_lock_path_defaults_under_pack_dir() {
74 let dir = tempdir().expect("tempdir");
75 assert_eq!(
76 resolve_lock_path(dir.path(), None),
77 dir.path().join("pack.lock.cbor")
78 );
79 }
80
81 #[test]
82 fn resolve_lock_path_joins_relative_override() {
83 let dir = tempdir().expect("tempdir");
84 assert_eq!(
85 resolve_lock_path(dir.path(), Some(Path::new("nested/pack.lock.cbor"))),
86 dir.path().join("nested/pack.lock.cbor")
87 );
88 }
89
90 #[test]
91 fn resolve_lock_path_keeps_absolute_override() {
92 let path = PathBuf::from("/tmp/pack.lock.cbor");
93 assert_eq!(
94 resolve_lock_path(Path::new("/repo"), Some(path.as_path())),
95 path
96 );
97 }
98
99 #[test]
100 fn to_sorted_json_sorts_nested_object_keys() {
101 let json = to_sorted_json(&json!({
102 "z": 1,
103 "a": { "d": true, "b": false },
104 "m": [ { "y": 2, "x": 1 } ]
105 }))
106 .expect("serialize");
107
108 let a_pos = json.find("\"a\"").expect("a");
109 let m_pos = json.find("\"m\"").expect("m");
110 let z_pos = json.find("\"z\"").expect("z");
111 assert!(
112 a_pos < m_pos && m_pos < z_pos,
113 "top-level keys should be sorted"
114 );
115
116 let b_pos = json.find("\"b\"").expect("b");
117 let d_pos = json.find("\"d\"").expect("d");
118 assert!(b_pos < d_pos, "nested keys should be sorted");
119 }
120}