#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CliArg {
pub field: &'static str,
pub short: Option<char>,
pub position: Option<usize>,
pub long: Option<&'static str>,
}
impl CliArg {
pub const fn new(field: &'static str) -> Self {
Self {
field,
short: None,
position: None,
long: None,
}
}
pub const fn short(mut self, short: char) -> Self {
self.short = Some(short);
self
}
pub const fn at(mut self, position: usize) -> Self {
self.position = Some(position);
self
}
pub const fn long(mut self, long: &'static str) -> Self {
self.long = Some(long);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CliExample {
pub intent: &'static str,
pub command: &'static str,
}
impl CliExample {
pub const fn new(intent: &'static str, command: &'static str) -> Self {
Self { intent, command }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct CliRoute {
pub path: &'static [&'static str],
pub verb: &'static str,
pub args: &'static [CliArg],
pub examples: &'static [CliExample],
}
impl CliRoute {
pub const fn new(path: &'static [&'static str], verb: &'static str) -> Self {
Self {
path,
verb,
args: &[],
examples: &[],
}
}
pub const fn with_args(mut self, args: &'static [CliArg]) -> Self {
self.args = args;
self
}
pub const fn with_examples(mut self, examples: &'static [CliExample]) -> Self {
self.examples = examples;
self
}
pub fn spelling(&self) -> String {
let mut parts = self.path.to_vec();
parts.push(self.verb);
parts.join(" ")
}
pub fn arg(&self, field: &str) -> Option<&CliArg> {
self.args.iter().find(|arg| arg.field == field)
}
}
pub fn derived_route(wire_name: &str, http_path: &str) -> Option<(Vec<String>, String)> {
if http_path.starts_with("/test/") {
return None;
}
let verb = wire_name.split('_').next()?.to_string();
let mut nouns: Vec<String> = http_path
.trim_matches('/')
.split('/')
.skip(1)
.filter(|segment| !segment.is_empty() && !segment.starts_with('{'))
.map(|segment| segment.replace('_', "-"))
.collect();
if nouns
.last()
.is_some_and(|last| last.replace('-', "_") == verb)
{
nouns.pop();
}
if nouns.is_empty() {
return None;
}
Some((nouns, verb.replace('_', "-")))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn the_rest_path_supplies_the_hierarchy_a_flat_name_hides() {
assert_eq!(
derived_route("list_agent_versions", "/v1/agents/{agent_id}/versions"),
Some((vec!["agents".into(), "versions".into()], "list".into()))
);
assert_eq!(
derived_route(
"list_session_participants",
"/v1/sessions/{id}/participants"
),
Some((
vec!["sessions".into(), "participants".into()],
"list".into()
))
);
}
#[test]
fn an_action_endpoint_does_not_repeat_its_verb() {
assert_eq!(
derived_route("archive_session", "/v1/sessions/{id}/archive"),
Some((vec!["sessions".into()], "archive".into()))
);
}
#[test]
fn a_multi_word_verb_is_kebab() {
assert_eq!(
derived_route(
"set_default_agent_version",
"/v1/agents/{id}/versions/default"
),
Some((
vec!["agents".into(), "versions".into(), "default".into()],
"set".into()
))
);
}
#[test]
fn a_pathless_command_derives_nothing() {
assert_eq!(derived_route("health_check", "/health"), None);
}
#[test]
fn nouns_are_kebab_even_when_the_path_is_not() {
assert_eq!(
derived_route("list_plugin_marketplaces", "/v1/plugin_marketplaces"),
Some((vec!["plugin-marketplaces".into()], "list".into()))
);
}
#[test]
fn a_fixture_route_is_not_derived() {
assert_eq!(
derived_route("test_transport_conflict", "/test/transport-conflict"),
None
);
}
}