Skip to content

Commit e555db5

Browse files
committed
add schema output
1 parent f2332d6 commit e555db5

File tree

7 files changed

+238
-13
lines changed

7 files changed

+238
-13
lines changed

Cargo.lock

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ pretty_assertions = "1.4.1"
9191
zstd = "0.13.2"
9292
relrc = "0.5.0"
9393
wyhash = "0.6.0"
94+
schemars = "1.0.4"
9495

9596
# These public dependencies usually require breaking changes downstream, so we
9697
# try to be as permissive as possible.

hugr-cli/Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@ thiserror.workspace = true
2828
tracing = "0.1.41"
2929
tracing-subscriber = { version = "0.3.20", features = ["fmt"] }
3030
tabled = "0.20.0"
31+
schemars = { workspace = true, features = ["derive"] }
32+
3133

3234
[lints]
3335
workspace = true

hugr-cli/src/describe.rs

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
11
//! Describe the contents of HUGR packages.
2-
use std::io::Write;
3-
42
use crate::hugr_io::HugrInputArgs;
53
use anyhow::Result;
64
use clap::Parser;
@@ -10,6 +8,7 @@ use hugr::envelope::ReadError;
108
use hugr::envelope::description::{ExtensionDesc, ModuleDesc, PackageDesc};
119
use hugr::extension::Version;
1210
use hugr::package::Package;
11+
use std::io::Write;
1312
use tabled::Tabled;
1413
use tabled::derive::display;
1514

@@ -31,10 +30,15 @@ pub struct DescribeArgs {
3130
/// Configure module description
3231
pub module_args: ModuleArgs,
3332

34-
#[arg(long, default_value = "false")]
33+
#[arg(long, default_value = "false", help_heading = "JSON")]
3534
/// Output in json format
3635
pub json: bool,
3736

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+
3842
/// Output file. Use '-' for stdout.
3943
#[clap(short, long, value_parser, default_value = "-")]
4044
pub output: Output,
@@ -71,6 +75,12 @@ impl ModuleArgs {
7175
impl DescribeArgs {
7276
/// Load and describe the HUGR package.
7377
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+
}
7484
let (mut desc, res) = match self.input_args.get_described_package() {
7585
Ok((desc, pkg)) => (desc, Ok(pkg)),
7686
Err(crate::CliError::ReadEnvelope(ReadError::Payload {
@@ -143,7 +153,7 @@ impl DescribeArgs {
143153

144154
fn output_json(&mut self, package_desc: PackageDesc, res: &Result<Package>) -> Result<()> {
145155
let err_str = res.as_ref().err().map(|e| format!("{e:?}"));
146-
let json_desc = JsonDescription {
156+
let json_desc = PackageDescriptionJson {
147157
package_desc,
148158
error: err_str,
149159
};
@@ -173,8 +183,8 @@ impl DescribeArgs {
173183
}
174184
}
175185

176-
#[derive(serde::Serialize)]
177-
struct JsonDescription {
186+
#[derive(serde::Serialize, schemars::JsonSchema)]
187+
struct PackageDescriptionJson {
178188
#[serde(flatten)]
179189
package_desc: PackageDesc,
180190
#[serde(skip_serializing_if = "Option::is_none")]

hugr-cli/tests/describe.rs

Lines changed: 165 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -274,3 +274,168 @@ fn test_describe_invalid_package_json(invalid_package: Vec<u8>, mut describe_cmd
274274
.stdout(contains("\"error\": \"Error reading a HUGR model payload")) // error included in JSON
275275
.stderr(contains("Error reading a HUGR model payload"));
276276
}
277+
278+
#[rstest]
279+
fn test_schema(mut describe_cmd: Command) {
280+
describe_cmd.arg("--json-schema");
281+
let output = describe_cmd.assert().success().get_output().stdout.clone();
282+
let schema: Value = serde_json::from_slice(&output).unwrap();
283+
let expected = json!(
284+
{
285+
"$schema": "https://json-schema.org/draft/2020-12/schema",
286+
"title": "PackageDescriptionJson",
287+
"description": "High-level description of a HUGR package.",
288+
"type": "object",
289+
"properties": {
290+
"error": {
291+
"type": [
292+
"string",
293+
"null"
294+
]
295+
},
296+
"header": {
297+
"description": "Envelope header information.",
298+
"type": "string"
299+
},
300+
"modules": {
301+
"description": "Description of the modules in the package.",
302+
"type": "array",
303+
"items": {
304+
"anyOf": [
305+
{
306+
"$ref": "#/$defs/ModuleDesc"
307+
},
308+
{
309+
"type": "null"
310+
}
311+
]
312+
}
313+
},
314+
"packaged_extensions": {
315+
"description": "Description of the extensions in the package.",
316+
"type": "array",
317+
"items": {
318+
"anyOf": [
319+
{
320+
"$ref": "#/$defs/ExtensionDesc"
321+
},
322+
{
323+
"type": "null"
324+
}
325+
]
326+
}
327+
}
328+
},
329+
"required": [
330+
"header",
331+
"modules"
332+
],
333+
"$defs": {
334+
"Entrypoint": {
335+
"description": "Description of the entrypoint of a module.",
336+
"type": "object",
337+
"properties": {
338+
"node": {
339+
"description": "Node id of the entrypoint.",
340+
"type": "integer",
341+
"format": "uint32",
342+
"minimum": 0
343+
},
344+
"optype": {
345+
"description": "Operation type of the entrypoint node.",
346+
"type": "string"
347+
}
348+
},
349+
"required": [
350+
"node",
351+
"optype"
352+
]
353+
},
354+
"ExtensionDesc": {
355+
"description": "High level description of an extension.",
356+
"type": "object",
357+
"properties": {
358+
"name": {
359+
"description": "Name of the extension.",
360+
"type": "string"
361+
},
362+
"version": {
363+
"description": "Version of the extension.",
364+
"type": "string"
365+
}
366+
},
367+
"required": [
368+
"name",
369+
"version"
370+
]
371+
},
372+
"ModuleDesc": {
373+
"description": "High-level description of a module in a HUGR package.",
374+
"type": "object",
375+
"properties": {
376+
"entrypoint": {
377+
"description": "The entrypoint node and the corresponding operation type.",
378+
"anyOf": [
379+
{
380+
"$ref": "#/$defs/Entrypoint"
381+
},
382+
{
383+
"type": "null"
384+
}
385+
]
386+
},
387+
"generator": {
388+
"description": "Generator specified in the module metadata.",
389+
"type": [
390+
"string",
391+
"null"
392+
]
393+
},
394+
"num_nodes": {
395+
"description": "Number of nodes in the module.",
396+
"type": [
397+
"integer",
398+
"null"
399+
],
400+
"format": "uint",
401+
"minimum": 0
402+
},
403+
"public_symbols": {
404+
"description": "Public symbols defined in the module.",
405+
"type": [
406+
"array",
407+
"null"
408+
],
409+
"items": {
410+
"type": "string"
411+
}
412+
},
413+
"used_extensions_generator": {
414+
"description": "Generator specified used extensions in the module metadata.",
415+
"type": [
416+
"array",
417+
"null"
418+
],
419+
"items": {
420+
"$ref": "#/$defs/ExtensionDesc"
421+
}
422+
},
423+
"used_extensions_resolved": {
424+
"description": "Extensions used in the module computed while resolving, expected to be a subset of `used_extensions_generator`.",
425+
"type": [
426+
"array",
427+
"null"
428+
],
429+
"items": {
430+
"$ref": "#/$defs/ExtensionDesc"
431+
}
432+
}
433+
}
434+
}
435+
}
436+
}
437+
438+
);
439+
440+
assert_eq!(schema, expected);
441+
}

hugr-core/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ base64.workspace = true
6565
relrc = { workspace = true, features = ["petgraph", "serde"] }
6666
smallvec = { workspace = true }
6767
tracing = "0.1.41"
68+
schemars = { workspace = true, features = ["derive"] }
6869

6970
[dev-dependencies]
7071
rstest = { workspace = true }

0 commit comments

Comments
 (0)