Skip to main content

hopper_manager/
summary.rs

1//! Tabular summaries over a `ProgramManifest`.
2//!
3//! Each function returns a `String` containing the rendered report. The
4//! manager crate is deliberately formatter-only: all truth comes from the
5//! manifest. The CLI calls these and prints the returned strings; other
6//! tools can embed the same output in different UIs.
7
8use core::fmt::Write;
9
10use hopper_schema::{InstructionDescriptor, ProgramManifest};
11
12/// Render the same default overview that `ProgramManifest`'s `Display`
13/// implementation produces.
14pub fn program_summary(manifest: &ProgramManifest) -> String {
15    format!("{}", manifest)
16}
17
18/// Render the `manager layouts` report: one block per layout with its
19/// discriminator, version, fingerprint, and field table.
20pub fn layouts_report(manifest: &ProgramManifest) -> String {
21    let mut out = String::new();
22    let _ = writeln!(out, "=== Layouts ({}) ===", manifest.layouts.len());
23    let _ = writeln!(out);
24    for layout in manifest.layouts.iter() {
25        let _ = writeln!(
26            out,
27            "{} v{}  disc={}  size={}  id={}",
28            layout.name,
29            layout.version,
30            layout.disc,
31            layout.total_size,
32            hex8(&layout.layout_id)
33        );
34        if layout.field_count == 0 {
35            let _ = writeln!(out, "  (no fields)");
36        } else {
37            let _ = writeln!(
38                out,
39                "  {:>4}  {:>20}  {:>6}  {:>6}  {:>12}",
40                "#", "Field", "Off", "Size", "Type"
41            );
42            for (i, f) in layout.fields.iter().take(layout.field_count).enumerate() {
43                let _ = writeln!(
44                    out,
45                    "  {:>4}  {:>20}  {:>6}  {:>6}  {:>12}",
46                    i, f.name, f.offset, f.size, f.canonical_type
47                );
48            }
49        }
50        let _ = writeln!(out);
51    }
52    out
53}
54
55/// Render the `manager fingerprints` report: wire + semantic fingerprints
56/// for every layout in the manifest.
57pub fn fingerprints_report(manifest: &ProgramManifest) -> String {
58    let mut out = String::new();
59    let _ = writeln!(
60        out,
61        "=== Layout Fingerprints ({}) ===",
62        manifest.layouts.len()
63    );
64    let _ = writeln!(out);
65    for layout in manifest.layouts.iter() {
66        let _ = writeln!(out, "{} v{}", layout.name, layout.version);
67        let _ = writeln!(out, "  disc        : {}", layout.disc);
68        let _ = writeln!(out, "  total_size  : {}", layout.total_size);
69        let _ = writeln!(out, "  layout_id   : {}", hex8(&layout.layout_id));
70        let _ = writeln!(out);
71    }
72    out
73}
74
75/// Render the `manager policies` report.
76pub fn policies_report(manifest: &ProgramManifest) -> String {
77    let mut out = String::new();
78    let _ = writeln!(out, "=== Policy Packs ({}) ===", manifest.policies.len());
79    let _ = writeln!(out);
80    if manifest.policies.is_empty() {
81        let _ = writeln!(out, "(no policy packs declared)");
82        return out;
83    }
84    for policy in manifest.policies.iter() {
85        let _ = writeln!(out, "- {}", policy.name);
86        if !policy.capabilities.is_empty() {
87            let _ = writeln!(out, "    capabilities : {}", policy.capabilities.join(", "));
88        }
89        if !policy.requirements.is_empty() {
90            let _ = writeln!(out, "    requirements : {}", policy.requirements.join(", "));
91        }
92        if !policy.invariants.is_empty() {
93            let _ = writeln!(out, "    invariants   : {}", policy.invariants.join(", "));
94        }
95        if !policy.receipt_profile.is_empty() {
96            let _ = writeln!(out, "    receipt      : {}", policy.receipt_profile);
97        }
98        let _ = writeln!(out);
99    }
100    out
101}
102
103/// Render the `manager events` report.
104pub fn events_report(manifest: &ProgramManifest) -> String {
105    let mut out = String::new();
106    let _ = writeln!(out, "=== Events ({}) ===", manifest.events.len());
107    let _ = writeln!(out);
108    if manifest.events.is_empty() {
109        let _ = writeln!(out, "(no events declared)");
110        return out;
111    }
112    for ev in manifest.events.iter() {
113        let _ = writeln!(out, "{} (tag={})", ev.name, ev.tag);
114        if ev.fields.is_empty() {
115            let _ = writeln!(out, "  (no fields)");
116        } else {
117            for f in ev.fields.iter() {
118                let _ = writeln!(
119                    out,
120                    "  {:<20}  {:>12}  offset={}  size={}",
121                    f.name, f.canonical_type, f.offset, f.size
122                );
123            }
124        }
125        let _ = writeln!(out);
126    }
127    out
128}
129
130/// Render the `manager instruction <tag|name>` report for a single
131/// instruction.
132///
133/// Accepts either the instruction tag (as a decimal string) or the name.
134/// Returns `Err(String)` if the instruction isn't in the manifest.
135pub fn instruction_report(manifest: &ProgramManifest, selector: &str) -> Result<String, String> {
136    let instr = resolve_instruction(manifest, selector).ok_or_else(|| {
137        format!(
138            "no instruction matches '{}' (known: {})",
139            selector,
140            known_instruction_names(manifest)
141        )
142    })?;
143
144    let mut out = String::new();
145    let _ = writeln!(
146        out,
147        "=== Instruction: {} (tag {}) ===",
148        instr.name, instr.tag
149    );
150    if instr.receipt_expected {
151        let _ = writeln!(out, "  receipt: expected");
152    }
153    if !instr.policy_pack.is_empty() {
154        let _ = writeln!(out, "  policy : {}", instr.policy_pack);
155    }
156
157    if !instr.args.is_empty() {
158        let _ = writeln!(out);
159        let _ = writeln!(out, "  Arguments:");
160        for a in instr.args.iter() {
161            let _ = writeln!(
162                out,
163                "    {:<16}  {:>12}  ({} bytes)",
164                a.name, a.canonical_type, a.size
165            );
166        }
167    }
168
169    if !instr.accounts.is_empty() {
170        let _ = writeln!(out);
171        let _ = writeln!(out, "  Accounts:");
172        for (i, acct) in instr.accounts.iter().enumerate() {
173            let mut flags = Vec::with_capacity(3);
174            if acct.signer {
175                flags.push("signer");
176            }
177            if acct.writable {
178                flags.push("mut");
179            } else {
180                flags.push("read");
181            }
182            if !acct.layout_ref.is_empty() {
183                flags.push("typed");
184            }
185            let layout_ref = if acct.layout_ref.is_empty() {
186                String::new()
187            } else {
188                format!("-> {}", acct.layout_ref)
189            };
190            let _ = writeln!(
191                out,
192                "    [{}] {:<20} {} {}",
193                i,
194                acct.name,
195                flags.join(","),
196                layout_ref,
197            );
198        }
199    }
200    Ok(out)
201}
202
203fn resolve_instruction<'a>(
204    manifest: &'a ProgramManifest,
205    selector: &str,
206) -> Option<&'a InstructionDescriptor> {
207    if let Ok(tag) = selector.parse::<u8>() {
208        if let Some(ix) = manifest.find_instruction(tag) {
209            return Some(ix);
210        }
211    }
212    manifest.instructions.iter().find(|ix| ix.name == selector)
213}
214
215fn known_instruction_names(manifest: &ProgramManifest) -> String {
216    let names: Vec<&str> = manifest.instructions.iter().map(|i| i.name).collect();
217    if names.is_empty() {
218        String::from("<none>")
219    } else {
220        names.join(", ")
221    }
222}
223
224fn hex8(bytes: &[u8; 8]) -> String {
225    let mut out = String::with_capacity(16);
226    for b in bytes {
227        let _ = write!(out, "{:02x}", b);
228    }
229    out
230}
231
232#[cfg(test)]
233mod tests {
234    use super::*;
235
236    // A trivial manifest keeps tests self-contained and fast.
237    fn empty_manifest() -> ProgramManifest {
238        ProgramManifest {
239            name: "test",
240            version: "0.1.0",
241            description: "",
242            layouts: &[],
243            layout_metadata: &[],
244            instructions: &[],
245            events: &[],
246            policies: &[],
247            compatibility_pairs: &[],
248            tooling_hints: &[],
249            contexts: &[],
250        }
251    }
252
253    #[test]
254    fn empty_policies_reports_zero() {
255        let m = empty_manifest();
256        let s = policies_report(&m);
257        assert!(s.contains("(no policy packs declared)"));
258    }
259
260    #[test]
261    fn empty_events_reports_zero() {
262        let m = empty_manifest();
263        let s = events_report(&m);
264        assert!(s.contains("(no events declared)"));
265    }
266
267    #[test]
268    fn instruction_report_missing_gives_error() {
269        let m = empty_manifest();
270        let err = instruction_report(&m, "deposit").unwrap_err();
271        assert!(err.contains("deposit"));
272    }
273}