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,
11 middleware::ValueMap,
12 output::{NextAction, TableColumn},
13};
14
15#[derive(Clone, Debug)]
20pub struct StreamSender(pub(crate) mpsc::Sender<Value>);
21
22impl StreamSender {
23 pub async fn send(&self, event: Value) {
25 drop(self.0.send(event).await);
26 }
27}
28
29pub type CommandFuture = Pin<Box<dyn Future<Output = Result<CommandResult>> + Send>>;
31pub type CommandHandler = Arc<dyn Fn(CommandContext) -> CommandFuture + Send + Sync>;
33
34pub type StreamingCommandFuture = Pin<Box<dyn Future<Output = Result<()>> + Send>>;
36pub type StreamingCommandHandler =
38 Arc<dyn Fn(CommandContext, StreamSender) -> StreamingCommandFuture + Send + Sync>;
39
40#[derive(Clone, Debug, PartialEq)]
46pub struct CommandResult {
47 pub data: Value,
49 pub metadata: CommandResultMetadata,
51}
52
53impl CommandResult {
54 #[must_use]
56 pub fn new(data: Value) -> Self {
57 Self {
58 data,
59 metadata: CommandResultMetadata::default(),
60 }
61 }
62
63 #[must_use]
65 pub fn with_next_actions(mut self, actions: Vec<NextAction>) -> Self {
66 self.metadata.next_actions = actions;
67 self
68 }
69}
70
71impl From<Value> for CommandResult {
72 fn from(data: Value) -> Self {
73 Self::new(data)
74 }
75}
76
77#[non_exhaustive]
79#[derive(Clone, Debug, Default, Eq, PartialEq)]
80pub struct CommandResultMetadata {
81 pub next_actions: Vec<NextAction>,
83}
84
85#[derive(Clone, Debug)]
95#[non_exhaustive]
96pub struct CommandContext {
97 pub credential: CredentialResolver,
99 pub args: ValueMap,
101 pub user_args: ValueMap,
103 pub command_path: String,
105 pub middleware: Middleware,
107 pub raw_matches: Arc<ArgMatches>,
109}
110
111impl CommandContext {
112 #[must_use]
126 pub fn config(&self) -> &crate::config::ConfigFile {
127 &self.middleware.config
128 }
129
130 pub fn typed_args<T: clap::FromArgMatches>(&self) -> Result<T> {
139 T::from_arg_matches(self.raw_matches.as_ref())
140 .map_err(|e| crate::CliCoreError::Message(format!("argument parse error: {e}")))
141 }
142
143 pub async fn credential(&self) -> Result<Credential> {
153 self.credential.resolve().await
154 }
155
156 pub async fn try_credential(&self) -> Result<Option<Credential>> {
165 self.credential.try_resolve().await
166 }
167
168 pub async fn credential_with_scopes(&self, extra: &[String]) -> Result<Credential> {
190 self.credential.resolve_with_scopes(extra).await
191 }
192}
193
194#[derive(Clone, Debug, Default)]
199pub struct CommandSpec {
200 pub name: String,
202 pub short: String,
204 pub long: Option<String>,
206 pub aliases: Vec<String>,
208 pub hidden: bool,
210 pub system: Option<String>,
212 pub default_fields: Option<String>,
214 pub auth: AuthRequirement,
221 pub auth_provider: Option<String>,
223 pub tier: Option<Tier>,
225 pub mutates: bool,
227 pub auth_metadata: BTreeMap<String, String>,
229 pub args: Vec<Arg>,
231 pub output_schema: Option<SchemaInfo>,
233 pub view_columns: Vec<TableColumn>,
239 pub view_id: Option<String>,
246}
247
248impl CommandSpec {
249 #[must_use]
251 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
252 Self {
253 name: name.into(),
254 short: short.into(),
255 ..Self::default()
256 }
257 }
258
259 #[must_use]
265 pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
266 let placeholder = Command::new("__placeholder");
267 let augmented = T::augment_args(placeholder);
268 let args: Vec<Arg> = augmented
269 .get_arguments()
270 .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
271 .cloned()
272 .collect();
273 Self {
274 name: name.into(),
275 short: short.into(),
276 args,
277 ..Self::default()
278 }
279 }
280
281 #[must_use]
283 pub fn with_long(mut self, long: impl Into<String>) -> Self {
284 self.long = Some(long.into());
285 self
286 }
287
288 #[must_use]
290 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
291 self.aliases.push(alias.into());
292 self
293 }
294
295 #[must_use]
297 pub fn hidden(mut self, hidden: bool) -> Self {
298 self.hidden = hidden;
299 self
300 }
301
302 #[must_use]
304 pub fn with_system(mut self, system: impl Into<String>) -> Self {
305 self.system = Some(system.into());
306 self
307 }
308
309 #[must_use]
311 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
312 self.default_fields = Some(default_fields.into());
313 self
314 }
315
316 #[must_use]
325 pub fn with_view(mut self, columns: impl Into<Vec<TableColumn>>) -> Self {
326 self.view_columns = columns.into();
327 self
328 }
329
330 #[must_use]
337 pub fn with_view_id(mut self, id: impl Into<String>) -> Self {
338 self.view_id = Some(id.into());
339 self
340 }
341
342 #[must_use]
344 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
345 self.auth_provider = Some(provider.into());
346 self
347 }
348
349 #[must_use]
355 pub fn no_auth(mut self, no_auth: bool) -> Self {
356 self.auth = if no_auth {
357 AuthRequirement::None
358 } else {
359 AuthRequirement::Required
360 };
361 self
362 }
363
364 #[must_use]
366 pub fn auth(mut self, requirement: AuthRequirement) -> Self {
367 self.auth = requirement;
368 self
369 }
370
371 #[must_use]
378 pub fn auth_optional(mut self) -> Self {
379 self.auth = AuthRequirement::Optional;
380 self
381 }
382
383 #[must_use]
385 pub fn with_tier(mut self, tier: Tier) -> Self {
386 self.tier = Some(tier);
387 self
388 }
389
390 #[must_use]
392 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
393 self.auth_metadata.insert(key.into(), value.into());
394 self
395 }
396
397 #[must_use]
405 pub fn with_scopes(mut self, scopes: &[impl AsRef<str>]) -> Self {
406 let joined = scopes
407 .iter()
408 .map(AsRef::as_ref)
409 .collect::<Vec<_>>()
410 .join(" ");
411 if joined.is_empty() {
414 self.auth_metadata.remove("scopes");
415 } else {
416 self.auth_metadata.insert("scopes".to_owned(), joined);
417 }
418 self
419 }
420
421 #[must_use]
423 pub fn with_arg(mut self, arg: Arg) -> Self {
424 self.args.push(arg);
425 self
426 }
427
428 #[must_use]
430 pub fn with_flag(self, flag: Arg) -> Self {
431 self.with_arg(flag)
432 }
433
434 #[must_use]
436 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
437 self.output_schema = Some(SchemaInfo {
438 command: String::new(),
439 fields: crate::output::fields_for::<T>(),
440 schema: None,
441 });
442 self
443 }
444
445 #[must_use]
447 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
448 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
449 self
450 }
451
452 #[must_use]
454 pub fn mutates(mut self, mutates: bool) -> Self {
455 self.mutates = mutates;
456 self
457 }
458
459 #[must_use]
461 pub fn metadata(&self) -> CommandMeta {
462 let mut auth_metadata = self.auth_metadata.clone();
463 if let Some(provider) = &self.auth_provider
464 && !provider.is_empty()
465 {
466 auth_metadata.insert("provider".to_owned(), provider.clone());
467 }
468 if let Some(tier) = self.tier
469 && !auth_metadata.contains_key("tier")
470 {
471 auth_metadata.insert("tier".to_owned(), tier.to_string());
472 }
473 let scopes = auth_metadata
474 .get("scopes")
475 .map(|scopes| {
476 scopes
477 .split_whitespace()
478 .map(str::to_owned)
479 .collect::<Vec<_>>()
480 })
481 .unwrap_or_default();
482
483 CommandMeta {
484 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
485 auth_metadata,
486 scopes,
487 }
488 }
489
490 #[must_use]
492 pub fn clap_command(&self) -> Command {
493 let mut command = Command::new(self.name.clone()).about(self.short.clone());
494 if let Some(long) = &self.long
495 && !long.is_empty()
496 {
497 command = command.long_about(long.clone());
498 }
499 for alias in &self.aliases {
500 command = command.alias(alias.clone());
501 }
502 if self.hidden {
503 command = command.hide(true);
504 }
505 for arg in &self.args {
506 command = command.arg(arg.clone());
507 }
508 command
509 }
510}
511
512#[derive(Clone, Debug, Default)]
517pub struct GroupSpec {
518 pub name: String,
520 pub short: String,
522 pub long: Option<String>,
524 pub aliases: Vec<String>,
526 pub hidden: bool,
528 pub commands: Vec<CommandSpec>,
530 pub groups: Vec<GroupSpec>,
532}
533
534impl GroupSpec {
535 #[must_use]
537 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
538 Self {
539 name: name.into(),
540 short: short.into(),
541 ..Self::default()
542 }
543 }
544
545 #[must_use]
547 pub fn with_long(mut self, long: impl Into<String>) -> Self {
548 self.long = Some(long.into());
549 self
550 }
551
552 #[must_use]
554 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
555 self.aliases.push(alias.into());
556 self
557 }
558
559 #[must_use]
561 pub fn hidden(mut self, hidden: bool) -> Self {
562 self.hidden = hidden;
563 self
564 }
565
566 #[must_use]
568 pub fn with_command(mut self, command: CommandSpec) -> Self {
569 self.commands.push(command);
570 self
571 }
572
573 #[must_use]
575 pub fn with_group(mut self, group: GroupSpec) -> Self {
576 self.groups.push(group);
577 self
578 }
579
580 #[must_use]
582 pub fn clap_command(&self) -> Command {
583 let mut command = Command::new(self.name.clone()).about(self.short.clone());
584 if let Some(long) = &self.long
585 && !long.is_empty()
586 {
587 command = command.long_about(long.clone());
588 }
589 for alias in &self.aliases {
590 command = command.alias(alias.clone());
591 }
592 if self.hidden {
593 command = command.hide(true);
594 }
595 for group in &self.groups {
596 command = command.subcommand(group.clap_command());
597 }
598 for child in &self.commands {
599 command = command.subcommand(child.clap_command());
600 }
601 command
602 }
603}
604
605#[derive(Clone)]
614pub struct RuntimeCommandSpec {
615 pub spec: CommandSpec,
617 pub handler: CommandHandler,
619 pub streaming_handler: Option<StreamingCommandHandler>,
622}
623
624impl std::fmt::Debug for RuntimeCommandSpec {
625 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
626 formatter
627 .debug_struct("RuntimeCommandSpec")
628 .field("spec", &self.spec)
629 .field("is_streaming", &self.streaming_handler.is_some())
630 .finish_non_exhaustive()
631 }
632}
633
634impl RuntimeCommandSpec {
635 #[must_use]
642 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
643 where
644 F: Fn(CredentialResolver, ValueMap) -> Fut + Send + Sync + 'static,
645 Fut: Future<Output = Result<Output>> + Send + 'static,
646 Output: Into<CommandResult> + Send + 'static,
647 {
648 Self {
649 spec,
650 streaming_handler: None,
651 handler: Arc::new(move |context| {
652 let future = handler(context.credential, context.args);
653 Box::pin(async move { future.await.map(Into::into) })
654 }),
655 }
656 }
657
658 #[must_use]
660 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
661 where
662 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
663 Fut: Future<Output = Result<Output>> + Send + 'static,
664 Output: Into<CommandResult> + Send + 'static,
665 {
666 Self {
667 spec,
668 streaming_handler: None,
669 handler: Arc::new(move |context| {
670 let future = handler(context);
671 Box::pin(async move { future.await.map(Into::into) })
672 }),
673 }
674 }
675
676 #[must_use]
682 pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
683 where
684 F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
685 Fut: Future<Output = Result<()>> + Send + 'static,
686 {
687 let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
688 let future = handler(context, sender);
689 Box::pin(future)
690 });
691 Self {
692 spec,
693 streaming_handler: Some(streaming),
694 handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
695 }
696 }
697
698 #[must_use]
708 pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
709 where
710 T: clap::FromArgMatches + Send + 'static,
711 F: Fn(CredentialResolver, T) -> Fut + Send + Sync + 'static,
712 Fut: Future<Output = Result<Output>> + Send + 'static,
713 Output: Into<CommandResult> + Send + 'static,
714 {
715 let handler = Arc::new(handler);
716 Self {
717 spec,
718 handler: Arc::new(move |context| {
719 let credential = context.credential.clone();
720 let parsed = T::from_arg_matches(context.raw_matches.as_ref());
721 let handler = handler.clone();
722 Box::pin(async move {
723 let args = parsed.map_err(|e| {
724 crate::CliCoreError::Message(format!("argument parse error: {e}"))
725 })?;
726 handler(credential, args).await.map(Into::into)
727 })
728 }),
729 streaming_handler: None,
730 }
731 }
732}
733
734#[derive(Clone, Debug, Default)]
736pub struct RuntimeGroupSpec {
737 pub group: GroupSpec,
739 pub commands: Vec<RuntimeCommandSpec>,
741 pub groups: Vec<RuntimeGroupSpec>,
743}
744
745impl RuntimeGroupSpec {
746 #[must_use]
748 pub fn new(group: GroupSpec) -> Self {
749 Self {
750 group,
751 ..Self::default()
752 }
753 }
754
755 #[must_use]
757 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
758 self.commands.push(command);
759 self
760 }
761
762 #[must_use]
764 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
765 self.groups.push(group);
766 self
767 }
768
769 #[must_use]
771 pub fn clap_command(&self) -> Command {
772 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
773 if let Some(long) = &self.group.long
774 && !long.is_empty()
775 {
776 command = command.long_about(long.clone());
777 }
778 for alias in &self.group.aliases {
779 command = command.alias(alias.clone());
780 }
781 if self.group.hidden {
782 command = command.hide(true);
783 }
784 for group in &self.groups {
785 command = command.subcommand(group.clap_command());
786 }
787 for child in &self.commands {
788 command = command.subcommand(child.spec.clap_command());
789 }
790 command
791 }
792
793 pub(crate) fn register_commands(
794 &self,
795 prefix: &mut Vec<String>,
796 out: &mut BTreeMap<String, RuntimeCommandSpec>,
797 ) {
798 prefix.push(self.group.name.clone());
799 for group in &self.groups {
800 group.register_commands(prefix, out);
801 }
802 for command in &self.commands {
803 prefix.push(command.spec.name.clone());
804 out.insert(prefix.join(":"), command.clone());
805 prefix.pop();
806 }
807 prefix.pop();
808 }
809}
810
811#[must_use]
813pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
814 let mut parts = Vec::new();
815 let mut current = matches;
816 while let Some((name, submatches)) = current.subcommand() {
817 if name != root_name {
818 parts.push(name.to_owned());
819 }
820 current = submatches;
821 }
822 parts.join(":")
823}
824
825#[must_use]
829pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
830 if parts.is_empty() {
831 return String::new();
832 }
833 if parts.len() > 1 {
834 return parts[1..]
835 .iter()
836 .map(AsRef::as_ref)
837 .collect::<Vec<_>>()
838 .join(":");
839 }
840 path_annotation
841 .filter(|annotation| !annotation.is_empty())
842 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
843}
844
845#[must_use]
847pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
848 let mut current = matches;
849 while let Some((_, submatches)) = current.subcommand() {
850 current = submatches;
851 }
852 current
853}
854
855#[must_use]
860pub fn command_args_from_matches(
861 matches: &ArgMatches,
862 spec: &CommandSpec,
863 changed_only: bool,
864) -> ValueMap {
865 let mut args = ValueMap::new();
866 for arg in &spec.args {
867 let id = arg.get_id().to_string();
868 let changed = matches
869 .value_source(&id)
870 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
871 if changed_only && !changed {
872 continue;
873 }
874 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
875 args.insert(id, value);
876 }
877 }
878 args
879}
880
881fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
882 matches.value_source(id)?;
883
884 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
885 && let Some(value) = matches.get_one::<bool>(id)
886 {
887 return Some(Value::Bool(*value));
888 }
889
890 if let Some(value) = typed_arg_value_from_matches(matches, id) {
891 return Some(value);
892 }
893
894 if let Some(values) = matches.get_raw(id) {
895 let rendered = values
896 .map(|value| value.to_string_lossy().into_owned())
897 .collect::<Vec<_>>();
898 return match rendered.as_slice() {
899 [] => None,
900 [single] => Some(Value::String(single.clone())),
901 _ => Some(Value::Array(
902 rendered.into_iter().map(Value::String).collect(),
903 )),
904 };
905 }
906
907 if let Some(value) = matches.get_one::<String>(id) {
908 return Some(Value::String(value.clone()));
909 }
910 if let Some(value) = matches.get_one::<usize>(id) {
911 return Some(serde_json::json!(value));
912 }
913 if let Some(value) = matches.get_one::<u64>(id) {
914 return Some(serde_json::json!(value));
915 }
916 if let Some(value) = matches.get_one::<i64>(id) {
917 return Some(serde_json::json!(value));
918 }
919 None
920}
921
922fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
923 typed_values::<bool>(matches, id, Value::Bool)
924 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
925 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
926 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
927 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
928 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
929 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
930 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
931 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
932 .or_else(|| {
933 typed_values::<usize>(matches, id, |value| {
934 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
935 })
936 })
937 .or_else(|| {
938 typed_values::<f64>(matches, id, |value| {
939 Number::from_f64(value).map_or(Value::Null, Value::Number)
940 })
941 })
942 .or_else(|| {
943 typed_values::<f32>(matches, id, |value| {
944 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
945 })
946 })
947 .or_else(|| typed_values::<String>(matches, id, Value::String))
948}
949
950fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
951where
952 T: Clone + Send + Sync + 'static,
953{
954 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
955 return None;
956 };
957 let values = values.cloned().map(to_value).collect::<Vec<_>>();
958 match values.as_slice() {
959 [] => None,
960 [single] => Some(single.clone()),
961 _ => Some(Value::Array(values)),
962 }
963}