use std::{borrow::Cow, ffi::OsStr, fmt::Write as _};
use schemars::{SchemaGenerator, generate::SchemaSettings};
use serde_json::{Map, Value};
mod invocation;
use crate::{
Error,
cli::{
command::{Command, Key},
help,
protocol::{HandlerSchemaSource, HandlerSchemas},
},
};
#[derive(Clone, Copy, Debug)]
struct Entry {
key: Key,
schemas: fn(&mut SchemaGenerator) -> HandlerSchemas,
}
#[doc(hidden)]
#[derive(Debug, Default)]
pub struct Registry {
entries: Vec<Entry>,
}
impl Registry {
#[must_use]
#[doc(hidden)]
pub const fn new() -> Self {
Self { entries: Vec::new() }
}
#[doc(hidden)]
pub fn register(
&mut self,
command: &'static Command<'static>,
schemas: fn(&mut SchemaGenerator) -> HandlerSchemas,
) {
self.entries.push(Entry { key: command.key, schemas });
}
fn handler(
&self,
command: &Command<'_>,
generator: &mut SchemaGenerator,
) -> Option<HandlerSchemas> {
self.entries
.iter()
.find(|entry| entry.key == command.key)
.map(|entry| (entry.schemas)(generator))
}
}
#[doc(hidden)]
pub trait SchemaCommand {
fn register_schema_commands(command: &'static Command<'static>, registry: &mut Registry);
}
#[doc(hidden)]
pub trait SchemaSubcommands {
fn register_schema_subcommands(
commands: &'static [&'static Command<'static>],
registry: &mut Registry,
);
}
#[doc(hidden)]
pub fn register_handler<T>(command: &'static Command<'static>, registry: &mut Registry)
where
T: HandlerSchemaSource,
{
registry.register(command, T::handler_schemas);
}
pub(crate) fn pseudo_command(
root: &'static Command<'static>,
argv: &[&OsStr],
registry: &Registry,
) -> Option<Error> {
if root.subcommands.is_empty() {
return None;
}
let (first, segments) = argv.split_first()?;
if first.as_encoded_bytes() != b"schema" {
return None;
}
if segments
.iter()
.any(|segment| *segment == OsStr::new("-h") || *segment == OsStr::new("--help"))
{
return Some(Error::DisplayHelp { help: help::render_schema(root) });
}
let (segments, full) = match segments.split_last() {
Some((last, command_path)) if *last == OsStr::new("--full") => (command_path, true),
_ => (segments, false),
};
let mut command = root;
let mut path = vec![root];
for segment in segments {
let bytes = segment.as_encoded_bytes();
let Some(child) = command.subcommands.iter().copied().find(|child| {
child.name.as_bytes() == bytes
|| child.aliases.iter().any(|alias| alias.as_bytes() == bytes)
}) else {
return Some(Error::UnknownCommand { token: bytes.to_vec() });
};
command = child;
path.push(child);
}
Some(display_schema(&path, registry, full))
}
pub(crate) fn display_schema(path: &[&Command<'_>], registry: &Registry, full: bool) -> Error {
let command = path.last().copied().expect("schema discovery always has a root command");
let effective_full = full || command.subcommands.is_empty();
let schema = if effective_full {
full_command_schema(path, registry, &[])
} else {
concise_command_schema(path)
};
let mut rendered =
serde_json::to_string_pretty(&schema).expect("schema document must serialize");
rendered.push('\n');
Error::DisplaySchema { schema: rendered }
}
fn concise_command_schema(path: &[&Command<'_>]) -> Value {
let command = path.last().copied().expect("schema discovery always has a root command");
let mut schema = serde_json::to_value(invocation::invocation_schema_for_path(path))
.expect("invocation schema must serialize");
let object = schema.as_object_mut().expect("invocation schemas are objects");
let mut definitions = Map::new();
bundle_subcommands(object, &mut definitions, command, &[], |child, _| {
let mut stub = Map::new();
invocation::add_command_header(&mut stub, child);
Value::Object(stub)
});
if !definitions.is_empty() {
object.insert("$defs".to_owned(), Value::Object(definitions));
}
schema
}
pub(super) fn doc_summary(documentation: &str) -> Option<&str> {
documentation.split("\n\n").map(str::trim).find(|paragraph| !paragraph.is_empty())
}
fn full_command_schema(path: &[&Command<'_>], registry: &Registry, location: &[String]) -> Value {
let command = path.last().copied().expect("schema discovery always has a root command");
let schema = serde_json::to_value(invocation::invocation_schema_for_path(path))
.expect("invocation schema must serialize");
complete_command_schema(schema, command, registry, location)
}
fn complete_command_schema(
mut schema: Value,
command: &Command<'_>,
registry: &Registry,
location: &[String],
) -> Value {
let object = schema.as_object_mut().expect("invocation schemas are objects");
if !location.is_empty() {
object.remove("$schema");
}
let mut definitions = Map::new();
let mut generator = schema_generator(location);
if let Some(handler) = registry.handler(command, &mut generator) {
definitions.insert("result".to_owned(), schema_value(handler.result));
definitions.insert("error".to_owned(), schema_value(handler.error));
let types = generator.take_definitions(false);
if !types.is_empty() {
definitions.insert("types".to_owned(), definitions_schema(types));
}
}
bundle_subcommands(object, &mut definitions, command, location, |child, child_location| {
let child_schema = serde_json::to_value(invocation::local_invocation_schema(child))
.expect("invocation schema must serialize");
complete_command_schema(child_schema, child, registry, child_location)
});
if !definitions.is_empty() {
object.insert("$defs".to_owned(), Value::Object(definitions));
}
schema
}
fn bundle_subcommands(
object: &mut Map<String, Value>,
definitions: &mut Map<String, Value>,
command: &Command<'_>,
location: &[String],
mut project: impl FnMut(&Command<'_>, &[String]) -> Value,
) {
if command.subcommands.is_empty() {
return;
}
let mut commands = Map::new();
let properties = object
.entry("properties".to_owned())
.or_insert_with(|| Value::Object(Map::new()))
.as_object_mut()
.expect("invocation properties must be an object");
for &child in command.subcommands {
assert!(
!properties.contains_key(child.name),
"schema subcommand name must not collide with an invocation property",
);
let child_location = child_location(location, child.name);
properties.insert(child.name.to_owned(), reference_schema(&child_location));
commands.insert(child.name.to_owned(), project(child, &child_location));
}
if command.subcommands.len() == 1 {
append_required(object, command.subcommands[0].name);
} else {
object.insert(
"oneOf".to_owned(),
Value::Array(
command.subcommands.iter().map(|child| required_schema(child.name)).collect(),
),
);
}
definitions.insert("commands".to_owned(), definitions_schema(commands));
}
fn child_location(location: &[String], child: &str) -> Vec<String> {
let mut child_location = location.to_vec();
child_location.extend([
"$defs".to_owned(),
"commands".to_owned(),
"$defs".to_owned(),
child.to_owned(),
]);
child_location
}
fn reference_schema(location: &[String]) -> Value {
let pointer =
location.iter().map(|segment| pointer_token(segment)).collect::<Vec<_>>().join("/");
let mut schema = Map::new();
schema.insert("$ref".to_owned(), Value::String(format!("#/{pointer}")));
Value::Object(schema)
}
fn append_required(object: &mut Map<String, Value>, property: &str) {
let required = object
.entry("required".to_owned())
.or_insert_with(|| Value::Array(Vec::new()))
.as_array_mut()
.expect("invocation required must be an array");
required.push(Value::String(property.to_owned()));
}
fn required_schema(property: &str) -> Value {
let mut schema = Map::new();
schema.insert("required".to_owned(), Value::Array(vec![Value::String(property.to_owned())]));
Value::Object(schema)
}
fn schema_generator(location: &[String]) -> SchemaGenerator {
SchemaSettings::draft2020_12()
.with(|settings| {
settings.meta_schema = None;
settings.definitions_path = Cow::Owned(definitions_path(location));
})
.into_generator()
}
fn definitions_path(location: &[String]) -> String {
let mut segments = location.iter().map(|segment| pointer_token(segment)).collect::<Vec<_>>();
segments.extend(["$defs".to_owned(), "types".to_owned(), "$defs".to_owned()]);
format!("/{}", segments.join("/"))
}
fn pointer_token(token: &str) -> String {
let escaped = token.replace('~', "~0").replace('/', "~1");
let mut encoded = String::with_capacity(escaped.len());
for byte in escaped.bytes() {
if byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'.' | b'_' | b'~' | b'$') {
encoded.push(char::from(byte));
} else {
write!(&mut encoded, "%{byte:02X}").expect("writing to String cannot fail");
}
}
encoded
}
fn definitions_schema(definitions: Map<String, Value>) -> Value {
let mut schema = Map::new();
schema.insert("$defs".to_owned(), Value::Object(definitions));
Value::Object(schema)
}
fn schema_value(schema: schemars::Schema) -> Value {
schema.to_value()
}