1use std::{collections::BTreeMap, future::Future, pin::Pin, sync::Arc};
2
3use clap::{Arg, ArgAction, ArgMatches, Command};
4use schemars::JsonSchema;
5use serde_json::{Number, Value};
6
7use crate::{
8 CommandMeta, Credential, Middleware, OutputSchema, Result, SchemaInfo, Tier,
9 middleware::ValueMap,
10};
11
12pub type CommandFuture = Pin<Box<dyn Future<Output = Result<CommandResult>> + Send>>;
14pub type CommandHandler = Arc<dyn Fn(CommandContext) -> CommandFuture + Send + Sync>;
16
17#[derive(Clone, Debug, PartialEq)]
23pub struct CommandResult {
24 pub data: Value,
26 pub metadata: CommandResultMetadata,
28}
29
30impl CommandResult {
31 #[must_use]
33 pub fn new(data: Value) -> Self {
34 Self {
35 data,
36 metadata: CommandResultMetadata::default(),
37 }
38 }
39}
40
41impl From<Value> for CommandResult {
42 fn from(data: Value) -> Self {
43 Self::new(data)
44 }
45}
46
47#[non_exhaustive]
49#[derive(Clone, Debug, Default, Eq, PartialEq)]
50pub struct CommandResultMetadata {}
51
52#[derive(Clone, Debug)]
58pub struct CommandContext {
59 pub credential: Option<Credential>,
61 pub args: ValueMap,
63 pub user_args: ValueMap,
65 pub command_path: String,
67 pub middleware: Middleware,
69}
70
71#[derive(Clone, Debug, Default)]
76pub struct CommandSpec {
77 pub name: String,
79 pub short: String,
81 pub long: Option<String>,
83 pub aliases: Vec<String>,
85 pub hidden: bool,
87 pub system: Option<String>,
89 pub default_fields: Option<String>,
91 pub no_auth: bool,
93 pub auth_provider: Option<String>,
95 pub tier: Option<Tier>,
97 pub mutates: bool,
99 pub auth_metadata: BTreeMap<String, String>,
101 pub args: Vec<Arg>,
103 pub output_schema: Option<SchemaInfo>,
105}
106
107impl CommandSpec {
108 #[must_use]
110 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
111 Self {
112 name: name.into(),
113 short: short.into(),
114 ..Self::default()
115 }
116 }
117
118 #[must_use]
120 pub fn with_long(mut self, long: impl Into<String>) -> Self {
121 self.long = Some(long.into());
122 self
123 }
124
125 #[must_use]
127 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
128 self.aliases.push(alias.into());
129 self
130 }
131
132 #[must_use]
134 pub fn hidden(mut self, hidden: bool) -> Self {
135 self.hidden = hidden;
136 self
137 }
138
139 #[must_use]
141 pub fn with_system(mut self, system: impl Into<String>) -> Self {
142 self.system = Some(system.into());
143 self
144 }
145
146 #[must_use]
148 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
149 self.default_fields = Some(default_fields.into());
150 self
151 }
152
153 #[must_use]
155 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
156 self.auth_provider = Some(provider.into());
157 self
158 }
159
160 #[must_use]
162 pub fn no_auth(mut self, no_auth: bool) -> Self {
163 self.no_auth = no_auth;
164 self
165 }
166
167 #[must_use]
169 pub fn with_tier(mut self, tier: Tier) -> Self {
170 self.tier = Some(tier);
171 self
172 }
173
174 #[must_use]
176 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
177 self.auth_metadata.insert(key.into(), value.into());
178 self
179 }
180
181 #[must_use]
183 pub fn with_arg(mut self, arg: Arg) -> Self {
184 self.args.push(arg);
185 self
186 }
187
188 #[must_use]
190 pub fn with_flag(self, flag: Arg) -> Self {
191 self.with_arg(flag)
192 }
193
194 #[must_use]
196 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
197 self.output_schema = Some(SchemaInfo {
198 command: String::new(),
199 fields: crate::output::fields_for::<T>(),
200 schema: None,
201 });
202 self
203 }
204
205 #[must_use]
207 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
208 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
209 self
210 }
211
212 #[must_use]
214 pub fn mutates(mut self, mutates: bool) -> Self {
215 self.mutates = mutates;
216 self
217 }
218
219 #[must_use]
221 pub fn metadata(&self) -> CommandMeta {
222 let mut auth_metadata = self.auth_metadata.clone();
223 if let Some(provider) = &self.auth_provider
224 && !provider.is_empty()
225 {
226 auth_metadata.insert("provider".to_owned(), provider.clone());
227 }
228 if let Some(tier) = self.tier
229 && !auth_metadata.contains_key("tier")
230 {
231 auth_metadata.insert("tier".to_owned(), tier.to_string());
232 }
233 let scopes = auth_metadata
234 .get("scopes")
235 .map(|scopes| {
236 scopes
237 .split_whitespace()
238 .map(str::to_owned)
239 .collect::<Vec<_>>()
240 })
241 .unwrap_or_default();
242
243 CommandMeta {
244 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
245 auth_metadata,
246 scopes,
247 }
248 }
249
250 #[must_use]
252 pub fn clap_command(&self) -> Command {
253 let mut command = Command::new(self.name.clone()).about(self.short.clone());
254 if let Some(long) = &self.long
255 && !long.is_empty()
256 {
257 command = command.long_about(long.clone());
258 }
259 for alias in &self.aliases {
260 command = command.alias(alias.clone());
261 }
262 if self.hidden {
263 command = command.hide(true);
264 }
265 for arg in &self.args {
266 command = command.arg(arg.clone());
267 }
268 command
269 }
270}
271
272#[derive(Clone, Debug, Default)]
277pub struct GroupSpec {
278 pub name: String,
280 pub short: String,
282 pub long: Option<String>,
284 pub aliases: Vec<String>,
286 pub hidden: bool,
288 pub commands: Vec<CommandSpec>,
290 pub groups: Vec<GroupSpec>,
292}
293
294impl GroupSpec {
295 #[must_use]
297 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
298 Self {
299 name: name.into(),
300 short: short.into(),
301 ..Self::default()
302 }
303 }
304
305 #[must_use]
307 pub fn with_long(mut self, long: impl Into<String>) -> Self {
308 self.long = Some(long.into());
309 self
310 }
311
312 #[must_use]
314 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
315 self.aliases.push(alias.into());
316 self
317 }
318
319 #[must_use]
321 pub fn hidden(mut self, hidden: bool) -> Self {
322 self.hidden = hidden;
323 self
324 }
325
326 #[must_use]
328 pub fn with_command(mut self, command: CommandSpec) -> Self {
329 self.commands.push(command);
330 self
331 }
332
333 #[must_use]
335 pub fn with_group(mut self, group: GroupSpec) -> Self {
336 self.groups.push(group);
337 self
338 }
339
340 #[must_use]
342 pub fn clap_command(&self) -> Command {
343 let mut command = Command::new(self.name.clone()).about(self.short.clone());
344 if let Some(long) = &self.long
345 && !long.is_empty()
346 {
347 command = command.long_about(long.clone());
348 }
349 for alias in &self.aliases {
350 command = command.alias(alias.clone());
351 }
352 if self.hidden {
353 command = command.hide(true);
354 }
355 for group in &self.groups {
356 command = command.subcommand(group.clap_command());
357 }
358 for child in &self.commands {
359 command = command.subcommand(child.clap_command());
360 }
361 command
362 }
363}
364
365#[derive(Clone)]
371pub struct RuntimeCommandSpec {
372 pub spec: CommandSpec,
374 pub handler: CommandHandler,
376}
377
378impl std::fmt::Debug for RuntimeCommandSpec {
379 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
380 formatter
381 .debug_struct("RuntimeCommandSpec")
382 .field("spec", &self.spec)
383 .finish_non_exhaustive()
384 }
385}
386
387impl RuntimeCommandSpec {
388 #[must_use]
393 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
394 where
395 F: Fn(Option<Credential>, ValueMap) -> Fut + Send + Sync + 'static,
396 Fut: Future<Output = Result<Output>> + Send + 'static,
397 Output: Into<CommandResult> + Send + 'static,
398 {
399 Self {
400 spec,
401 handler: Arc::new(move |context| {
402 let future = handler(context.credential, context.args);
403 Box::pin(async move { future.await.map(Into::into) })
404 }),
405 }
406 }
407
408 #[must_use]
410 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
411 where
412 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
413 Fut: Future<Output = Result<Output>> + Send + 'static,
414 Output: Into<CommandResult> + Send + 'static,
415 {
416 Self {
417 spec,
418 handler: Arc::new(move |context| {
419 let future = handler(context);
420 Box::pin(async move { future.await.map(Into::into) })
421 }),
422 }
423 }
424}
425
426#[derive(Clone, Debug, Default)]
428pub struct RuntimeGroupSpec {
429 pub group: GroupSpec,
431 pub commands: Vec<RuntimeCommandSpec>,
433 pub groups: Vec<RuntimeGroupSpec>,
435}
436
437impl RuntimeGroupSpec {
438 #[must_use]
440 pub fn new(group: GroupSpec) -> Self {
441 Self {
442 group,
443 ..Self::default()
444 }
445 }
446
447 #[must_use]
449 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
450 self.commands.push(command);
451 self
452 }
453
454 #[must_use]
456 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
457 self.groups.push(group);
458 self
459 }
460
461 #[must_use]
463 pub fn clap_command(&self) -> Command {
464 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
465 if let Some(long) = &self.group.long
466 && !long.is_empty()
467 {
468 command = command.long_about(long.clone());
469 }
470 for alias in &self.group.aliases {
471 command = command.alias(alias.clone());
472 }
473 if self.group.hidden {
474 command = command.hide(true);
475 }
476 for group in &self.groups {
477 command = command.subcommand(group.clap_command());
478 }
479 for child in &self.commands {
480 command = command.subcommand(child.spec.clap_command());
481 }
482 command
483 }
484
485 pub(crate) fn register_commands(
486 &self,
487 prefix: &mut Vec<String>,
488 out: &mut BTreeMap<String, RuntimeCommandSpec>,
489 ) {
490 prefix.push(self.group.name.clone());
491 for group in &self.groups {
492 group.register_commands(prefix, out);
493 }
494 for command in &self.commands {
495 prefix.push(command.spec.name.clone());
496 out.insert(prefix.join(":"), command.clone());
497 prefix.pop();
498 }
499 prefix.pop();
500 }
501}
502
503#[must_use]
505pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
506 let mut parts = Vec::new();
507 let mut current = matches;
508 while let Some((name, submatches)) = current.subcommand() {
509 if name != root_name {
510 parts.push(name.to_owned());
511 }
512 current = submatches;
513 }
514 parts.join(":")
515}
516
517#[must_use]
521pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
522 if parts.is_empty() {
523 return String::new();
524 }
525 if parts.len() > 1 {
526 return parts[1..]
527 .iter()
528 .map(AsRef::as_ref)
529 .collect::<Vec<_>>()
530 .join(":");
531 }
532 path_annotation
533 .filter(|annotation| !annotation.is_empty())
534 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
535}
536
537#[must_use]
539pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
540 let mut current = matches;
541 while let Some((_, submatches)) = current.subcommand() {
542 current = submatches;
543 }
544 current
545}
546
547#[must_use]
552pub fn command_args_from_matches(
553 matches: &ArgMatches,
554 spec: &CommandSpec,
555 changed_only: bool,
556) -> ValueMap {
557 let mut args = ValueMap::new();
558 for arg in &spec.args {
559 let id = arg.get_id().to_string();
560 let changed = matches
561 .value_source(&id)
562 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
563 if changed_only && !changed {
564 continue;
565 }
566 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
567 args.insert(id, value);
568 }
569 }
570 args
571}
572
573fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
574 matches.value_source(id)?;
575
576 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
577 && let Some(value) = matches.get_one::<bool>(id)
578 {
579 return Some(Value::Bool(*value));
580 }
581
582 if let Some(value) = typed_arg_value_from_matches(matches, id) {
583 return Some(value);
584 }
585
586 if let Some(values) = matches.get_raw(id) {
587 let rendered = values
588 .map(|value| value.to_string_lossy().into_owned())
589 .collect::<Vec<_>>();
590 return match rendered.as_slice() {
591 [] => None,
592 [single] => Some(Value::String(single.clone())),
593 _ => Some(Value::Array(
594 rendered.into_iter().map(Value::String).collect(),
595 )),
596 };
597 }
598
599 if let Some(value) = matches.get_one::<String>(id) {
600 return Some(Value::String(value.clone()));
601 }
602 if let Some(value) = matches.get_one::<usize>(id) {
603 return Some(serde_json::json!(value));
604 }
605 if let Some(value) = matches.get_one::<u64>(id) {
606 return Some(serde_json::json!(value));
607 }
608 if let Some(value) = matches.get_one::<i64>(id) {
609 return Some(serde_json::json!(value));
610 }
611 None
612}
613
614fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
615 typed_values::<bool>(matches, id, Value::Bool)
616 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
617 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
618 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
619 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
620 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
621 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
622 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
623 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
624 .or_else(|| {
625 typed_values::<usize>(matches, id, |value| {
626 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
627 })
628 })
629 .or_else(|| {
630 typed_values::<f64>(matches, id, |value| {
631 Number::from_f64(value).map_or(Value::Null, Value::Number)
632 })
633 })
634 .or_else(|| {
635 typed_values::<f32>(matches, id, |value| {
636 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
637 })
638 })
639 .or_else(|| typed_values::<String>(matches, id, Value::String))
640}
641
642fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
643where
644 T: Clone + Send + Sync + 'static,
645{
646 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
647 return None;
648 };
649 let values = values.cloned().map(to_value).collect::<Vec<_>>();
650 match values.as_slice() {
651 [] => None,
652 [single] => Some(single.clone()),
653 _ => Some(Value::Array(values)),
654 }
655}