Skip to main content

packc/cli/
inspect_lock.rs

1#![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    /// Pack root directory containing pack.yaml.
14    #[arg(long = "in", value_name = "DIR", default_value = ".")]
15    pub input: PathBuf,
16
17    /// Path to pack.lock.cbor (default: pack.lock.cbor under pack root).
18    #[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}