hugr_cli/
describe.rs

1//! Describe the contents of HUGR packages.
2use crate::hugr_io::HugrInputArgs;
3use anyhow::Result;
4use clap::Parser;
5use clio::Output;
6use hugr::NodeIndex;
7use hugr::envelope::ReadError;
8use hugr::envelope::description::{ExtensionDesc, ModuleDesc, PackageDesc};
9use hugr::extension::Version;
10use hugr::package::Package;
11use std::io::Write;
12use tabled::Tabled;
13use tabled::derive::display;
14
15/// Describe the contents of a serialized HUGR package.
16#[derive(Parser, Debug)]
17#[clap(version = "1.0", long_about = None)]
18#[clap(about = "Describe the contents of a HUGR envelope.")]
19#[group(id = "hugr")]
20#[non_exhaustive]
21pub struct DescribeArgs {
22    /// Hugr input.
23    #[command(flatten)]
24    pub input_args: HugrInputArgs,
25    /// Enumerate packaged extensions
26    #[arg(long, default_value = "false", help_heading = "Filter")]
27    pub packaged_extensions: bool,
28
29    #[command(flatten)]
30    /// Configure module description
31    pub module_args: ModuleArgs,
32
33    #[arg(long, default_value = "false", help_heading = "JSON")]
34    /// Output in json format
35    pub json: bool,
36
37    #[arg(long, default_value = "false", help_heading = "JSON")]
38    /// Output JSON schema for the description format.
39    /// Can't be combined with --json.
40    pub json_schema: bool,
41
42    /// Output file. Use '-' for stdout.
43    #[clap(short, long, value_parser, default_value = "-")]
44    pub output: Output,
45}
46
47/// Arguments for reading a HUGR input.
48#[derive(Debug, clap::Args)]
49pub struct ModuleArgs {
50    #[arg(long, default_value = "false", help_heading = "Filter")]
51    /// Don't display resolved extensions used by the module.
52    pub no_resolved_extensions: bool,
53
54    #[arg(long, default_value = "false", help_heading = "Filter")]
55    /// Display public symbols in the module.
56    pub public_symbols: bool,
57
58    #[arg(long, default_value = "false", help_heading = "Filter")]
59    /// Display claimed extensions set by generator in module metadata.
60    pub generator_claimed_extensions: bool,
61}
62impl ModuleArgs {
63    fn filter_module(&self, module: &mut ModuleDesc) {
64        if self.no_resolved_extensions {
65            module.used_extensions_resolved = None;
66        }
67        if !self.public_symbols {
68            module.public_symbols = None;
69        }
70        if !self.generator_claimed_extensions {
71            module.used_extensions_generator = None;
72        }
73    }
74}
75impl DescribeArgs {
76    /// Load and describe the HUGR package.
77    pub fn run_describe(&mut self) -> Result<()> {
78        if self.json_schema {
79            let schema = schemars::schema_for!(PackageDescriptionJson);
80            let schema_json = serde_json::to_string_pretty(&schema)?;
81            writeln!(self.output, "{schema_json}")?;
82            return Ok(());
83        }
84        let (mut desc, res) = match self.input_args.get_described_package() {
85            Ok((desc, pkg)) => (desc, Ok(pkg)),
86            Err(crate::CliError::ReadEnvelope(ReadError::Payload {
87                source,
88                partial_description,
89            })) => (partial_description, Err(source)), // keep error for later
90            Err(e) => return Err(e.into()),
91        };
92
93        // clear fields that have not been requested
94        for module in desc.modules.iter_mut().flatten() {
95            self.module_args.filter_module(module);
96        }
97
98        let res = res.map_err(anyhow::Error::from);
99        if self.json {
100            if !self.packaged_extensions {
101                desc.packaged_extensions.clear();
102            }
103            self.output_json(desc, &res)?;
104        } else {
105            self.print_description(desc)?;
106        }
107
108        // bubble up any errors
109        res.map(|_| ())
110    }
111
112    fn print_description(&mut self, desc: PackageDesc) -> Result<()> {
113        let header = desc.header();
114        let n_modules = desc.n_modules();
115        let n_extensions = desc.n_packaged_extensions();
116        let module_str = if n_modules == 1 { "module" } else { "modules" };
117        let extension_str = if n_extensions == 1 {
118            "extension"
119        } else {
120            "extensions"
121        };
122        writeln!(
123            self.output,
124            "{header}\nPackage contains {n_modules} {module_str} and {n_extensions} {extension_str}",
125        )?;
126        let summaries: Vec<ModuleSummary> = desc
127            .modules
128            .iter()
129            .map(|m| m.as_ref().map(Into::into).unwrap_or_default())
130            .collect();
131        let summary_table = tabled::Table::builder(summaries).index().build();
132        writeln!(self.output, "{summary_table}")?;
133
134        for (i, module) in desc.modules.into_iter().enumerate() {
135            writeln!(self.output, "\nModule {i}:")?;
136            if let Some(module) = module {
137                self.display_module(module)?;
138            }
139        }
140        if self.packaged_extensions {
141            writeln!(self.output, "Packaged extensions:")?;
142            let ext_rows: Vec<ExtensionRow> = desc
143                .packaged_extensions
144                .into_iter()
145                .flatten()
146                .map(Into::into)
147                .collect();
148            let ext_table = tabled::Table::new(ext_rows);
149            writeln!(self.output, "{ext_table}")?;
150        }
151        Ok(())
152    }
153
154    fn output_json(&mut self, package_desc: PackageDesc, res: &Result<Package>) -> Result<()> {
155        let err_str = res.as_ref().err().map(|e| format!("{e:?}"));
156        let json_desc = PackageDescriptionJson {
157            package_desc,
158            error: err_str,
159        };
160        serde_json::to_writer_pretty(&mut self.output, &json_desc)?;
161        Ok(())
162    }
163
164    fn display_module(&mut self, desc: ModuleDesc) -> Result<()> {
165        if let Some(exts) = desc.used_extensions_resolved {
166            let ext_rows: Vec<ExtensionRow> = exts.into_iter().map(Into::into).collect();
167            let ext_table = tabled::Table::new(ext_rows);
168            writeln!(self.output, "Resolved extensions:\n{ext_table}")?;
169        }
170
171        if let Some(syms) = desc.public_symbols {
172            let sym_table = tabled::Table::new(syms.into_iter().map(|s| SymbolRow { symbol: s }));
173            writeln!(self.output, "Public symbols:\n{sym_table}")?;
174        }
175
176        if let Some(exts) = desc.used_extensions_generator {
177            let ext_rows: Vec<ExtensionRow> = exts.into_iter().map(Into::into).collect();
178            let ext_table = tabled::Table::new(ext_rows);
179            writeln!(self.output, "Generator claimed extensions:\n{ext_table}")?;
180        }
181
182        Ok(())
183    }
184}
185
186#[derive(serde::Serialize, schemars::JsonSchema)]
187struct PackageDescriptionJson {
188    #[serde(flatten)]
189    package_desc: PackageDesc,
190    #[serde(skip_serializing_if = "Option::is_none")]
191    error: Option<String>,
192}
193
194#[derive(Tabled)]
195struct ExtensionRow {
196    name: String,
197    version: Version,
198}
199
200#[derive(Tabled)]
201struct SymbolRow {
202    #[tabled(rename = "Symbol")]
203    symbol: String,
204}
205
206impl From<ExtensionDesc> for ExtensionRow {
207    fn from(desc: ExtensionDesc) -> Self {
208        Self {
209            name: desc.name,
210            version: desc.version,
211        }
212    }
213}
214
215#[derive(Tabled, Default)]
216struct ModuleSummary {
217    #[tabled(display("display::option", "n/a"))]
218    num_nodes: Option<usize>,
219    #[tabled(display("display::option", "n/a"))]
220    entrypoint_node: Option<usize>,
221    #[tabled(display("display::option", "n/a"))]
222    entrypoint_op: Option<String>,
223    #[tabled(display("display::option", "n/a"))]
224    generator: Option<String>,
225}
226
227impl From<&ModuleDesc> for ModuleSummary {
228    fn from(desc: &ModuleDesc) -> Self {
229        let (entrypoint_node, entrypoint_op) = if let Some(ep) = &desc.entrypoint {
230            (
231                Some(ep.node.index()),
232                Some(hugr::envelope::description::op_string(&ep.optype)),
233            )
234        } else {
235            (None, None)
236        };
237        Self {
238            num_nodes: desc.num_nodes,
239            entrypoint_node,
240            entrypoint_op,
241            generator: desc.generator.clone(),
242        }
243    }
244}