use std::any::TypeId;
use schemars::JsonSchema;
use serde::Serialize;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct CliContract {
pub(crate) discovery: DiscoveryNode,
pub(crate) extended_schema: Option<Value>,
}
impl CliContract {
#[must_use]
pub fn command_for<T>(&self) -> Option<CommandInfo>
where
T: 'static,
{
let node = self.unique_command_node::<T>()?;
Some(self.command_info(node))
}
#[must_use]
pub const fn extended_schema(&self) -> Option<&Value> {
self.extended_schema.as_ref()
}
pub fn extended_schema_for(&self, path: &[&str]) -> crate::Result<Option<&Value>> {
let node = self.discovery.resolve(path)?;
Ok(self.extended_schema_for_node(node))
}
#[must_use]
pub fn extended_schema_for_command<T>(&self) -> Option<&Value>
where
T: 'static,
{
let node = self.unique_command_node::<T>()?;
self.extended_schema_for_node(node)
}
pub fn schema(&self, request: &SchemaRequest) -> crate::Result<SchemaDocument> {
let path = request.path.iter().map(String::as_str).collect::<Vec<_>>();
let node = self.discovery.resolve(&path)?;
Ok(self.schema_document(node, request.full))
}
pub fn command(&self, path: &[&str]) -> crate::Result<CommandInfo> {
let node = self.discovery.resolve(path)?;
Ok(self.command_info(node))
}
fn command_info(&self, node: &DiscoveryNode) -> CommandInfo {
CommandInfo {
name: node.name.clone(),
path: node.path.clone(),
aliases: node.visible_aliases.clone(),
description: node.description.clone(),
usage: node.usage.clone(),
arguments: node.arguments.clone(),
options: node.options.clone(),
executable: node.executable.is_some(),
output: node.executable.as_ref().and_then(|executable| executable.output.clone()),
has_subcommands: !node.children.is_empty(),
}
}
fn schema_document(&self, node: &DiscoveryNode, full: bool) -> SchemaDocument {
let command = self.command_info(node);
let subcommands = node
.children
.iter()
.map(|child| {
if full {
SchemaSubcommand::Resolved(Box::new(self.schema_document(child, true)))
} else {
let command = self.command_info(child);
SchemaSubcommand::Summary(SchemaCommandSummary::from_command(&command))
}
})
.collect();
SchemaDocument { command, subcommands }
}
fn unique_command_node<T>(&self) -> Option<&DiscoveryNode>
where
T: 'static,
{
self.discovery.unique_command(TypeId::of::<T>())
}
fn extended_schema_for_node<'a>(&'a self, node: &'a DiscoveryNode) -> Option<&'a Value> {
node.executable
.as_ref()
.and_then(|executable| executable.extended_schema.as_ref())
.or(self.extended_schema.as_ref())
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
#[non_exhaustive]
pub struct SchemaRequest {
pub path: Vec<String>,
pub full: bool,
}
impl SchemaRequest {
#[must_use]
pub fn new<I, S>(path: I) -> Self
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
Self { path: path.into_iter().map(Into::into).collect(), full: false }
}
#[must_use]
pub const fn with_full(mut self, full: bool) -> Self {
self.full = full;
self
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[non_exhaustive]
pub struct SchemaDocument {
#[serde(flatten)]
pub command: CommandInfo,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub subcommands: Vec<SchemaSubcommand>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[serde(untagged)]
#[non_exhaustive]
pub enum SchemaSubcommand {
Summary(SchemaCommandSummary),
Resolved(Box<SchemaDocument>),
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub(crate) struct ExecutableData {
pub(crate) id: TypeId,
pub(crate) output: Option<Value>,
pub(crate) extended_schema: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[non_exhaustive]
pub struct CommandInfo {
pub name: String,
pub path: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub usage: String,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub arguments: Vec<ArgumentInfo>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub options: Vec<ArgumentInfo>,
#[serde(default, skip_serializing_if = "is_false")]
pub executable: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub output: Option<Value>,
#[serde(default, skip_serializing_if = "is_false")]
pub has_subcommands: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[non_exhaustive]
pub struct SchemaCommandSummary {
pub path: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub executable: bool,
#[serde(default, skip_serializing_if = "is_false")]
pub has_subcommands: bool,
}
impl SchemaCommandSummary {
fn from_command(command: &CommandInfo) -> Self {
Self {
path: command.path.clone(),
description: command.description.clone(),
executable: command.executable,
has_subcommands: command.has_subcommands,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, JsonSchema)]
#[non_exhaustive]
pub struct ArgumentInfo {
pub id: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub index: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub short: Option<char>,
#[serde(skip_serializing_if = "Option::is_none")]
pub long: Option<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub short_aliases: Vec<char>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub aliases: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub value_names: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub help: Option<String>,
#[serde(default, skip_serializing_if = "is_false")]
pub required: bool,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub default_values: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")]
pub possible_values: Vec<String>,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub(crate) struct DiscoveryNode {
pub(crate) name: String,
pub(crate) path: Vec<String>,
pub(crate) aliases: Vec<String>,
pub(crate) visible_aliases: Vec<String>,
pub(crate) description: Option<String>,
pub(crate) usage: String,
pub(crate) arguments: Vec<ArgumentInfo>,
pub(crate) options: Vec<ArgumentInfo>,
pub(crate) executable: Option<ExecutableData>,
pub(crate) children: Vec<Self>,
}
impl DiscoveryNode {
pub(crate) fn unique_command(&self, id: TypeId) -> Option<&Self> {
fn visit<'a>(
node: &'a DiscoveryNode,
id: TypeId,
found: &mut Option<&'a DiscoveryNode>,
ambiguous: &mut bool,
) {
if node.executable.as_ref().is_some_and(|executable| executable.id == id) {
if found.is_some() {
*ambiguous = true;
return;
}
*found = Some(node);
}
for child in &node.children {
if *ambiguous {
return;
}
visit(child, id, found, ambiguous);
}
}
let mut found = None;
let mut ambiguous = false;
visit(self, id, &mut found, &mut ambiguous);
if ambiguous { None } else { found }
}
pub(crate) fn resolve(&self, path: &[&str]) -> crate::Result<&Self> {
let mut node = self;
for segment in path {
node = node
.children
.iter()
.find(|candidate| {
candidate.name == *segment
|| candidate.aliases.iter().any(|alias| alias == *segment)
})
.ok_or_else(|| crate::Error::UnknownCommand {
path: path.iter().map(|segment| (*segment).to_owned()).collect(),
})?;
}
Ok(node)
}
}
const fn is_false(value: &bool) -> bool {
!*value
}