Add corresponding API operation ID to CLI docs JSON#1399
Conversation
ahl
left a comment
There was a problem hiding this comment.
Two quick suggestions. Let's check in via chat tomorrow.
| let mut runner = CommandBuilder::default(); | ||
| for op in CliCommand::iter() { | ||
| let Some(path) = xxx(op) else { continue }; | ||
| runner.add_cmd(path, GeneratedCmd(op)); |
There was a problem hiding this comment.
I think an alternative approach would be to add a field to GeneratedCmd and then upcast when generating the docs--which is effectively what the clap function must be doing
There was a problem hiding this comment.
When we build the docs JSON, we start from the clap Command tree, which doesn't know about GeneratedCmd. In order to get to the relevant GeneratedCmd, we need a way of mapping one to the other. In the version of this PR I have that doesn't use unstable-ext (diff below), I use the path string from the Command to look up the matching CliCommand. It's fine, but it's using the paths to back into something we get more or less for free with the extension. Did you have something else in mind?
Example diff without unstable-ext
diff --git a/cli/src/cli_builder.rs b/cli/src/cli_builder.rs
index 86c7b9591d..9210c39e06 100644
--- a/cli/src/cli_builder.rs
+++ b/cli/src/cli_builder.rs
@@ -110,7 +110,7 @@
let mut parser = OxideCli::command().name("oxide").subcommand_required(true);
let mut runner = CommandBuilder::default();
for op in CliCommand::iter() {
- let Some(path) = xxx(op) else { continue };
+ let Some(path) = op.cli_path() else { continue };
runner.add_cmd(path, GeneratedCmd(op));
let cmd = Cli::<OxideOverride>::get_command(op);
@@ -393,8 +393,11 @@
}
}
-fn xxx<'a>(command: CliCommand) -> Option<&'a str> {
- match command {
+impl CliCommand {
+ /// The space-delimited subcommand path for this operation in the CLI,
+ /// or `None` if it is not exposed as a generated subcommand.
+ pub(crate) fn cli_path(self) -> Option<&'static str> {
+ match self {
CliCommand::InstanceList => Some("instance list"),
CliCommand::InstanceCreate => Some("instance create"),
CliCommand::InstanceView => Some("instance view"),
@@ -822,6 +825,7 @@
| CliCommand::SystemMetric
| CliCommand::UserBuiltinList
| CliCommand::UserBuiltinView => None,
+ }
}
}
diff --git a/cli/src/cmd_docs.rs b/cli/src/cmd_docs.rs
index f5b9a5f9a8..b677413db4 100644
--- a/cli/src/cmd_docs.rs
+++ b/cli/src/cmd_docs.rs
@@ -4,7 +4,10 @@
// Copyright 2024 Oxide Computer Company
+use std::collections::HashMap;
+
use crate::context::Context;
+use crate::generated_cli::CliCommand;
use crate::{println_nopipe, RunnableCmd};
use super::cmd_version::built_info;
@@ -50,17 +53,19 @@
about: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
long_about: Option<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ operation_id: Option<&'static str>,
#[serde(skip_serializing_if = "Vec::is_empty")]
args: Vec<JsonArg>,
#[serde(skip_serializing_if = "Vec::is_empty")]
subcommands: Vec<JsonDoc>,
}
-fn to_json(cmd: &Command) -> JsonDoc {
+fn to_json(cmd: &Command, op_ids: &HashMap<&str, &'static str>, path: &[&str]) -> JsonDoc {
let mut subcommands = cmd
.get_subcommands()
- .filter(|cmd| cmd.get_name() != "help")
- .map(to_json)
+ .filter(|sub| sub.get_name() != "help")
+ .map(|sub| to_json(sub, op_ids, &[path, &[sub.get_name()]].concat()))
.collect::<Vec<_>>();
subcommands.sort_unstable_by(|a, b| a.name.cmp(&b.name));
let mut args = cmd
@@ -89,12 +94,14 @@
} else {
None
};
+ let operation_id = op_ids.get(path.join(" ").as_str()).copied();
JsonDoc {
name,
version,
about: cmd.get_about().map(ToString::to_string),
long_about: cmd.get_long_about().map(ToString::to_string),
+ operation_id,
args,
subcommands,
}
@@ -103,10 +110,13 @@
#[async_trait]
impl RunnableCmd for CmdDocs {
async fn run(&self, _ctx: &Context) -> Result<()> {
+ let op_ids: HashMap<&str, &'static str> = CliCommand::iter()
+ .filter_map(|op| Some((op.cli_path()?, op.operation_id())))
+ .collect();
let cli = crate::make_cli();
let mut app = cli.command_take();
app.build();
- let json_doc = to_json(&app);
+ let json_doc = to_json(&app, &op_ids, &[]);
let pretty_json = serde_json::to_string_pretty(&json_doc)?;
println_nopipe!("{}", pretty_json);
Ok(())|
Also please update copyright years. |
a49fd7b to
f998856
Compare
Closes #875
Was messing with linking to API endpoints corresponding to forms in the console (oxidecomputer/console#3153) and realized it would be nice to just link to the API endpoint and then let the API doc link you to the CLI if you want. Turns out I made an issue for this two years ago.
This can absolutely be done without the
unstable-extapproach if you'd rather not, it's just neater with it — feels like the way it wants to be done. The alternative is using the command path to go find the correspondingCliCommandfor a given clapCommand.unstable-extseems relatively stable to me. It hasn't been mentioned in the release notes since August 2024. If they ever messed with it, it would be really easy to deal with.