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};
6use tokio::sync::mpsc;
7
8use crate::{
9 AuthRequirement, CommandMeta, Credential, CredentialResolver, Middleware, OutputSchema, Result,
10 SchemaInfo, Tier, middleware::ValueMap, output::NextAction,
11};
12
13#[derive(Clone, Debug)]
18pub struct StreamSender(pub(crate) mpsc::Sender<Value>);
19
20impl StreamSender {
21 pub async fn send(&self, event: Value) {
23 drop(self.0.send(event).await);
24 }
25}
26
27pub type CommandFuture = Pin<Box<dyn Future<Output = Result<CommandResult>> + Send>>;
29pub type CommandHandler = Arc<dyn Fn(CommandContext) -> CommandFuture + Send + Sync>;
31
32pub type StreamingCommandFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
34pub type StreamingCommandHandler =
36 Arc<dyn Fn(CommandContext, StreamSender) -> StreamingCommandFuture + Send + Sync>;
37
38#[derive(Clone, Debug, PartialEq)]
44pub struct CommandResult {
45 pub data: Value,
47 pub metadata: CommandResultMetadata,
49}
50
51impl CommandResult {
52 #[must_use]
54 pub fn new(data: Value) -> Self {
55 Self {
56 data,
57 metadata: CommandResultMetadata::default(),
58 }
59 }
60
61 #[must_use]
63 pub fn with_next_actions(mut self, actions: Vec<NextAction>) -> Self {
64 self.metadata.next_actions = actions;
65 self
66 }
67}
68
69impl From<Value> for CommandResult {
70 fn from(data: Value) -> Self {
71 Self::new(data)
72 }
73}
74
75#[non_exhaustive]
77#[derive(Clone, Debug, Default, Eq, PartialEq)]
78pub struct CommandResultMetadata {
79 pub next_actions: Vec<NextAction>,
81}
82
83#[derive(Clone, Debug)]
93#[non_exhaustive]
94pub struct CommandContext {
95 pub credential: CredentialResolver,
97 pub args: ValueMap,
99 pub user_args: ValueMap,
101 pub command_path: String,
103 pub middleware: Middleware,
105 pub raw_matches: Arc<ArgMatches>,
107}
108
109impl CommandContext {
110 pub fn typed_args<T: clap::FromArgMatches>(&self) -> Result<T> {
119 T::from_arg_matches(self.raw_matches.as_ref())
120 .map_err(|e| crate::CliCoreError::Message(format!("argument parse error: {e}")))
121 }
122
123 pub async fn credential(&self) -> Result<Credential> {
133 self.credential.resolve().await
134 }
135
136 pub async fn try_credential(&self) -> Result<Option<Credential>> {
145 self.credential.try_resolve().await
146 }
147
148 pub async fn credential_with_scopes(&self, extra: &[String]) -> Result<Credential> {
170 self.credential.resolve_with_scopes(extra).await
171 }
172}
173
174#[derive(Clone, Debug, Default)]
179pub struct CommandSpec {
180 pub name: String,
182 pub short: String,
184 pub long: Option<String>,
186 pub aliases: Vec<String>,
188 pub hidden: bool,
190 pub system: Option<String>,
192 pub default_fields: Option<String>,
194 pub auth: AuthRequirement,
201 pub auth_provider: Option<String>,
203 pub tier: Option<Tier>,
205 pub mutates: bool,
207 pub auth_metadata: BTreeMap<String, String>,
209 pub args: Vec<Arg>,
211 pub output_schema: Option<SchemaInfo>,
213}
214
215impl CommandSpec {
216 #[must_use]
218 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
219 Self {
220 name: name.into(),
221 short: short.into(),
222 ..Self::default()
223 }
224 }
225
226 #[must_use]
232 pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
233 let placeholder = Command::new("__placeholder");
234 let augmented = T::augment_args(placeholder);
235 let args: Vec<Arg> = augmented
236 .get_arguments()
237 .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
238 .cloned()
239 .collect();
240 Self {
241 name: name.into(),
242 short: short.into(),
243 args,
244 ..Self::default()
245 }
246 }
247
248 #[must_use]
250 pub fn with_long(mut self, long: impl Into<String>) -> Self {
251 self.long = Some(long.into());
252 self
253 }
254
255 #[must_use]
257 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
258 self.aliases.push(alias.into());
259 self
260 }
261
262 #[must_use]
264 pub fn hidden(mut self, hidden: bool) -> Self {
265 self.hidden = hidden;
266 self
267 }
268
269 #[must_use]
271 pub fn with_system(mut self, system: impl Into<String>) -> Self {
272 self.system = Some(system.into());
273 self
274 }
275
276 #[must_use]
278 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
279 self.default_fields = Some(default_fields.into());
280 self
281 }
282
283 #[must_use]
285 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
286 self.auth_provider = Some(provider.into());
287 self
288 }
289
290 #[must_use]
296 pub fn no_auth(mut self, no_auth: bool) -> Self {
297 self.auth = if no_auth {
298 AuthRequirement::None
299 } else {
300 AuthRequirement::Required
301 };
302 self
303 }
304
305 #[must_use]
307 pub fn auth(mut self, requirement: AuthRequirement) -> Self {
308 self.auth = requirement;
309 self
310 }
311
312 #[must_use]
319 pub fn auth_optional(mut self) -> Self {
320 self.auth = AuthRequirement::Optional;
321 self
322 }
323
324 #[must_use]
326 pub fn with_tier(mut self, tier: Tier) -> Self {
327 self.tier = Some(tier);
328 self
329 }
330
331 #[must_use]
333 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
334 self.auth_metadata.insert(key.into(), value.into());
335 self
336 }
337
338 #[must_use]
346 pub fn with_scopes(mut self, scopes: &[impl AsRef<str>]) -> Self {
347 let joined = scopes
348 .iter()
349 .map(AsRef::as_ref)
350 .collect::<Vec<_>>()
351 .join(" ");
352 if joined.is_empty() {
355 self.auth_metadata.remove("scopes");
356 } else {
357 self.auth_metadata.insert("scopes".to_owned(), joined);
358 }
359 self
360 }
361
362 #[must_use]
364 pub fn with_arg(mut self, arg: Arg) -> Self {
365 self.args.push(arg);
366 self
367 }
368
369 #[must_use]
371 pub fn with_flag(self, flag: Arg) -> Self {
372 self.with_arg(flag)
373 }
374
375 #[must_use]
377 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
378 self.output_schema = Some(SchemaInfo {
379 command: String::new(),
380 fields: crate::output::fields_for::<T>(),
381 schema: None,
382 });
383 self
384 }
385
386 #[must_use]
388 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
389 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
390 self
391 }
392
393 #[must_use]
395 pub fn mutates(mut self, mutates: bool) -> Self {
396 self.mutates = mutates;
397 self
398 }
399
400 #[must_use]
402 pub fn metadata(&self) -> CommandMeta {
403 let mut auth_metadata = self.auth_metadata.clone();
404 if let Some(provider) = &self.auth_provider
405 && !provider.is_empty()
406 {
407 auth_metadata.insert("provider".to_owned(), provider.clone());
408 }
409 if let Some(tier) = self.tier
410 && !auth_metadata.contains_key("tier")
411 {
412 auth_metadata.insert("tier".to_owned(), tier.to_string());
413 }
414 let scopes = auth_metadata
415 .get("scopes")
416 .map(|scopes| {
417 scopes
418 .split_whitespace()
419 .map(str::to_owned)
420 .collect::<Vec<_>>()
421 })
422 .unwrap_or_default();
423
424 CommandMeta {
425 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
426 auth_metadata,
427 scopes,
428 }
429 }
430
431 #[must_use]
433 pub fn clap_command(&self) -> Command {
434 let mut command = Command::new(self.name.clone()).about(self.short.clone());
435 if let Some(long) = &self.long
436 && !long.is_empty()
437 {
438 command = command.long_about(long.clone());
439 }
440 for alias in &self.aliases {
441 command = command.alias(alias.clone());
442 }
443 if self.hidden {
444 command = command.hide(true);
445 }
446 for arg in &self.args {
447 command = command.arg(arg.clone());
448 }
449 command
450 }
451}
452
453#[derive(Clone, Debug, Default)]
458pub struct GroupSpec {
459 pub name: String,
461 pub short: String,
463 pub long: Option<String>,
465 pub aliases: Vec<String>,
467 pub hidden: bool,
469 pub commands: Vec<CommandSpec>,
471 pub groups: Vec<GroupSpec>,
473}
474
475impl GroupSpec {
476 #[must_use]
478 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
479 Self {
480 name: name.into(),
481 short: short.into(),
482 ..Self::default()
483 }
484 }
485
486 #[must_use]
488 pub fn with_long(mut self, long: impl Into<String>) -> Self {
489 self.long = Some(long.into());
490 self
491 }
492
493 #[must_use]
495 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
496 self.aliases.push(alias.into());
497 self
498 }
499
500 #[must_use]
502 pub fn hidden(mut self, hidden: bool) -> Self {
503 self.hidden = hidden;
504 self
505 }
506
507 #[must_use]
509 pub fn with_command(mut self, command: CommandSpec) -> Self {
510 self.commands.push(command);
511 self
512 }
513
514 #[must_use]
516 pub fn with_group(mut self, group: GroupSpec) -> Self {
517 self.groups.push(group);
518 self
519 }
520
521 #[must_use]
523 pub fn clap_command(&self) -> Command {
524 let mut command = Command::new(self.name.clone()).about(self.short.clone());
525 if let Some(long) = &self.long
526 && !long.is_empty()
527 {
528 command = command.long_about(long.clone());
529 }
530 for alias in &self.aliases {
531 command = command.alias(alias.clone());
532 }
533 if self.hidden {
534 command = command.hide(true);
535 }
536 for group in &self.groups {
537 command = command.subcommand(group.clap_command());
538 }
539 for child in &self.commands {
540 command = command.subcommand(child.clap_command());
541 }
542 command
543 }
544}
545
546#[derive(Clone)]
555pub struct RuntimeCommandSpec {
556 pub spec: CommandSpec,
558 pub handler: CommandHandler,
560 pub streaming_handler: Option<StreamingCommandHandler>,
563}
564
565impl std::fmt::Debug for RuntimeCommandSpec {
566 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
567 formatter
568 .debug_struct("RuntimeCommandSpec")
569 .field("spec", &self.spec)
570 .field("is_streaming", &self.streaming_handler.is_some())
571 .finish_non_exhaustive()
572 }
573}
574
575impl RuntimeCommandSpec {
576 #[must_use]
583 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
584 where
585 F: Fn(CredentialResolver, ValueMap) -> Fut + Send + Sync + 'static,
586 Fut: Future<Output = Result<Output>> + Send + 'static,
587 Output: Into<CommandResult> + Send + 'static,
588 {
589 Self {
590 spec,
591 streaming_handler: None,
592 handler: Arc::new(move |context| {
593 let future = handler(context.credential, context.args);
594 Box::pin(async move { future.await.map(Into::into) })
595 }),
596 }
597 }
598
599 #[must_use]
601 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
602 where
603 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
604 Fut: Future<Output = Result<Output>> + Send + 'static,
605 Output: Into<CommandResult> + Send + 'static,
606 {
607 Self {
608 spec,
609 streaming_handler: None,
610 handler: Arc::new(move |context| {
611 let future = handler(context);
612 Box::pin(async move { future.await.map(Into::into) })
613 }),
614 }
615 }
616
617 #[must_use]
623 pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
624 where
625 F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
626 Fut: Future<Output = Result<()>> + Send + 'static,
627 {
628 let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
629 let future = handler(context, sender);
630 Box::pin(future)
631 });
632 Self {
633 spec,
634 streaming_handler: Some(streaming),
635 handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
636 }
637 }
638
639 #[must_use]
649 pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
650 where
651 T: clap::FromArgMatches + Send + 'static,
652 F: Fn(CredentialResolver, T) -> Fut + Send + Sync + 'static,
653 Fut: Future<Output = Result<Output>> + Send + 'static,
654 Output: Into<CommandResult> + Send + 'static,
655 {
656 let handler = Arc::new(handler);
657 Self {
658 spec,
659 handler: Arc::new(move |context| {
660 let credential = context.credential.clone();
661 let parsed = T::from_arg_matches(context.raw_matches.as_ref());
662 let handler = handler.clone();
663 Box::pin(async move {
664 let args = parsed.map_err(|e| {
665 crate::CliCoreError::Message(format!("argument parse error: {e}"))
666 })?;
667 handler(credential, args).await.map(Into::into)
668 })
669 }),
670 streaming_handler: None,
671 }
672 }
673}
674
675#[derive(Clone, Debug, Default)]
677pub struct RuntimeGroupSpec {
678 pub group: GroupSpec,
680 pub commands: Vec<RuntimeCommandSpec>,
682 pub groups: Vec<RuntimeGroupSpec>,
684}
685
686impl RuntimeGroupSpec {
687 #[must_use]
689 pub fn new(group: GroupSpec) -> Self {
690 Self {
691 group,
692 ..Self::default()
693 }
694 }
695
696 #[must_use]
698 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
699 self.commands.push(command);
700 self
701 }
702
703 #[must_use]
705 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
706 self.groups.push(group);
707 self
708 }
709
710 #[must_use]
712 pub fn clap_command(&self) -> Command {
713 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
714 if let Some(long) = &self.group.long
715 && !long.is_empty()
716 {
717 command = command.long_about(long.clone());
718 }
719 for alias in &self.group.aliases {
720 command = command.alias(alias.clone());
721 }
722 if self.group.hidden {
723 command = command.hide(true);
724 }
725 for group in &self.groups {
726 command = command.subcommand(group.clap_command());
727 }
728 for child in &self.commands {
729 command = command.subcommand(child.spec.clap_command());
730 }
731 command
732 }
733
734 pub(crate) fn register_commands(
735 &self,
736 prefix: &mut Vec<String>,
737 out: &mut BTreeMap<String, RuntimeCommandSpec>,
738 ) {
739 prefix.push(self.group.name.clone());
740 for group in &self.groups {
741 group.register_commands(prefix, out);
742 }
743 for command in &self.commands {
744 prefix.push(command.spec.name.clone());
745 out.insert(prefix.join(":"), command.clone());
746 prefix.pop();
747 }
748 prefix.pop();
749 }
750}
751
752#[must_use]
754pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
755 let mut parts = Vec::new();
756 let mut current = matches;
757 while let Some((name, submatches)) = current.subcommand() {
758 if name != root_name {
759 parts.push(name.to_owned());
760 }
761 current = submatches;
762 }
763 parts.join(":")
764}
765
766#[must_use]
770pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
771 if parts.is_empty() {
772 return String::new();
773 }
774 if parts.len() > 1 {
775 return parts[1..]
776 .iter()
777 .map(AsRef::as_ref)
778 .collect::<Vec<_>>()
779 .join(":");
780 }
781 path_annotation
782 .filter(|annotation| !annotation.is_empty())
783 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
784}
785
786#[must_use]
788pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
789 let mut current = matches;
790 while let Some((_, submatches)) = current.subcommand() {
791 current = submatches;
792 }
793 current
794}
795
796#[must_use]
801pub fn command_args_from_matches(
802 matches: &ArgMatches,
803 spec: &CommandSpec,
804 changed_only: bool,
805) -> ValueMap {
806 let mut args = ValueMap::new();
807 for arg in &spec.args {
808 let id = arg.get_id().to_string();
809 let changed = matches
810 .value_source(&id)
811 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
812 if changed_only && !changed {
813 continue;
814 }
815 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
816 args.insert(id, value);
817 }
818 }
819 args
820}
821
822fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
823 matches.value_source(id)?;
824
825 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
826 && let Some(value) = matches.get_one::<bool>(id)
827 {
828 return Some(Value::Bool(*value));
829 }
830
831 if let Some(value) = typed_arg_value_from_matches(matches, id) {
832 return Some(value);
833 }
834
835 if let Some(values) = matches.get_raw(id) {
836 let rendered = values
837 .map(|value| value.to_string_lossy().into_owned())
838 .collect::<Vec<_>>();
839 return match rendered.as_slice() {
840 [] => None,
841 [single] => Some(Value::String(single.clone())),
842 _ => Some(Value::Array(
843 rendered.into_iter().map(Value::String).collect(),
844 )),
845 };
846 }
847
848 if let Some(value) = matches.get_one::<String>(id) {
849 return Some(Value::String(value.clone()));
850 }
851 if let Some(value) = matches.get_one::<usize>(id) {
852 return Some(serde_json::json!(value));
853 }
854 if let Some(value) = matches.get_one::<u64>(id) {
855 return Some(serde_json::json!(value));
856 }
857 if let Some(value) = matches.get_one::<i64>(id) {
858 return Some(serde_json::json!(value));
859 }
860 None
861}
862
863fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
864 typed_values::<bool>(matches, id, Value::Bool)
865 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
866 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
867 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
868 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
869 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
870 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
871 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
872 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
873 .or_else(|| {
874 typed_values::<usize>(matches, id, |value| {
875 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
876 })
877 })
878 .or_else(|| {
879 typed_values::<f64>(matches, id, |value| {
880 Number::from_f64(value).map_or(Value::Null, Value::Number)
881 })
882 })
883 .or_else(|| {
884 typed_values::<f32>(matches, id, |value| {
885 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
886 })
887 })
888 .or_else(|| typed_values::<String>(matches, id, Value::String))
889}
890
891fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
892where
893 T: Clone + Send + Sync + 'static,
894{
895 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
896 return None;
897 };
898 let values = values.cloned().map(to_value).collect::<Vec<_>>();
899 match values.as_slice() {
900 [] => None,
901 [single] => Some(single.clone()),
902 _ => Some(Value::Array(values)),
903 }
904}