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 environment(&self) -> Result<crate::environments::Environment> {
153 let environments = self.middleware.environments.as_ref().ok_or_else(|| {
154 crate::error::CliCoreError::message("no environment system configured")
155 })?;
156 environments.resolve(&self.middleware.env)
157 }
158
159 pub fn typed_args<T: clap::FromArgMatches>(&self) -> Result<T> {
168 T::from_arg_matches(self.raw_matches.as_ref())
169 .map_err(|e| crate::CliCoreError::Message(format!("argument parse error: {e}")))
170 }
171
172 pub async fn credential(&self) -> Result<Credential> {
182 self.credential.resolve().await
183 }
184
185 pub async fn try_credential(&self) -> Result<Option<Credential>> {
194 self.credential.try_resolve().await
195 }
196
197 pub async fn credential_with_scopes(&self, extra: &[String]) -> Result<Credential> {
219 self.credential.resolve_with_scopes(extra).await
220 }
221}
222
223#[derive(Clone, Debug, Default)]
228pub struct CommandSpec {
229 pub name: String,
231 pub short: String,
233 pub long: Option<String>,
235 pub aliases: Vec<String>,
237 pub hidden: bool,
239 pub system: Option<String>,
241 pub default_fields: Option<String>,
243 pub auth: AuthRequirement,
250 pub auth_provider: Option<String>,
252 pub tier: Option<Tier>,
254 pub mutates: bool,
256 pub auth_metadata: BTreeMap<String, String>,
258 pub args: Vec<Arg>,
260 pub output_schema: Option<SchemaInfo>,
262 pub view_columns: Vec<TableColumn>,
268 pub view_id: Option<String>,
275}
276
277impl CommandSpec {
278 #[must_use]
280 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
281 Self {
282 name: name.into(),
283 short: short.into(),
284 ..Self::default()
285 }
286 }
287
288 #[must_use]
294 pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
295 let placeholder = Command::new("__placeholder");
296 let augmented = T::augment_args(placeholder);
297 let args: Vec<Arg> = augmented
298 .get_arguments()
299 .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
300 .cloned()
301 .collect();
302 Self {
303 name: name.into(),
304 short: short.into(),
305 args,
306 ..Self::default()
307 }
308 }
309
310 #[must_use]
312 pub fn with_long(mut self, long: impl Into<String>) -> Self {
313 self.long = Some(long.into());
314 self
315 }
316
317 #[must_use]
319 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
320 self.aliases.push(alias.into());
321 self
322 }
323
324 #[must_use]
326 pub fn hidden(mut self, hidden: bool) -> Self {
327 self.hidden = hidden;
328 self
329 }
330
331 #[must_use]
333 pub fn with_system(mut self, system: impl Into<String>) -> Self {
334 self.system = Some(system.into());
335 self
336 }
337
338 #[must_use]
340 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
341 self.default_fields = Some(default_fields.into());
342 self
343 }
344
345 #[must_use]
354 pub fn with_view(mut self, columns: impl Into<Vec<TableColumn>>) -> Self {
355 self.view_columns = columns.into();
356 self
357 }
358
359 #[must_use]
366 pub fn with_view_id(mut self, id: impl Into<String>) -> Self {
367 self.view_id = Some(id.into());
368 self
369 }
370
371 #[must_use]
373 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
374 self.auth_provider = Some(provider.into());
375 self
376 }
377
378 #[must_use]
384 pub fn no_auth(mut self, no_auth: bool) -> Self {
385 self.auth = if no_auth {
386 AuthRequirement::None
387 } else {
388 AuthRequirement::Required
389 };
390 self
391 }
392
393 #[must_use]
395 pub fn auth(mut self, requirement: AuthRequirement) -> Self {
396 self.auth = requirement;
397 self
398 }
399
400 #[must_use]
407 pub fn auth_optional(mut self) -> Self {
408 self.auth = AuthRequirement::Optional;
409 self
410 }
411
412 #[must_use]
414 pub fn with_tier(mut self, tier: Tier) -> Self {
415 self.tier = Some(tier);
416 self
417 }
418
419 #[must_use]
421 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
422 self.auth_metadata.insert(key.into(), value.into());
423 self
424 }
425
426 #[must_use]
434 pub fn with_scopes(mut self, scopes: &[impl AsRef<str>]) -> Self {
435 let joined = scopes
436 .iter()
437 .map(AsRef::as_ref)
438 .collect::<Vec<_>>()
439 .join(" ");
440 if joined.is_empty() {
443 self.auth_metadata.remove("scopes");
444 } else {
445 self.auth_metadata.insert("scopes".to_owned(), joined);
446 }
447 self
448 }
449
450 #[must_use]
452 pub fn with_arg(mut self, arg: Arg) -> Self {
453 self.args.push(arg);
454 self
455 }
456
457 #[must_use]
459 pub fn with_flag(self, flag: Arg) -> Self {
460 self.with_arg(flag)
461 }
462
463 #[must_use]
465 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
466 self.output_schema = Some(SchemaInfo {
467 command: String::new(),
468 fields: crate::output::fields_for::<T>(),
469 schema: None,
470 });
471 self
472 }
473
474 #[must_use]
476 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
477 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
478 self
479 }
480
481 #[must_use]
483 pub fn mutates(mut self, mutates: bool) -> Self {
484 self.mutates = mutates;
485 self
486 }
487
488 #[must_use]
490 pub fn metadata(&self) -> CommandMeta {
491 let mut auth_metadata = self.auth_metadata.clone();
492 if let Some(provider) = &self.auth_provider
493 && !provider.is_empty()
494 {
495 auth_metadata.insert("provider".to_owned(), provider.clone());
496 }
497 if let Some(tier) = self.tier
498 && !auth_metadata.contains_key("tier")
499 {
500 auth_metadata.insert("tier".to_owned(), tier.to_string());
501 }
502 let scopes = auth_metadata
503 .get("scopes")
504 .map(|scopes| {
505 scopes
506 .split_whitespace()
507 .map(str::to_owned)
508 .collect::<Vec<_>>()
509 })
510 .unwrap_or_default();
511
512 CommandMeta {
513 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
514 auth_metadata,
515 scopes,
516 }
517 }
518
519 #[must_use]
521 pub fn clap_command(&self) -> Command {
522 let mut command = Command::new(self.name.clone()).about(self.short.clone());
523 if let Some(long) = &self.long
524 && !long.is_empty()
525 {
526 command = command.long_about(long.clone());
527 }
528 for alias in &self.aliases {
529 command = command.alias(alias.clone());
530 }
531 if self.hidden {
532 command = command.hide(true);
533 }
534 for arg in &self.args {
535 command = command.arg(arg.clone());
536 }
537 command
538 }
539}
540
541#[derive(Clone, Debug, Default)]
546pub struct GroupSpec {
547 pub name: String,
549 pub short: String,
551 pub long: Option<String>,
553 pub aliases: Vec<String>,
555 pub hidden: bool,
557 pub commands: Vec<CommandSpec>,
559 pub groups: Vec<GroupSpec>,
561}
562
563impl GroupSpec {
564 #[must_use]
566 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
567 Self {
568 name: name.into(),
569 short: short.into(),
570 ..Self::default()
571 }
572 }
573
574 #[must_use]
576 pub fn with_long(mut self, long: impl Into<String>) -> Self {
577 self.long = Some(long.into());
578 self
579 }
580
581 #[must_use]
583 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
584 self.aliases.push(alias.into());
585 self
586 }
587
588 #[must_use]
590 pub fn hidden(mut self, hidden: bool) -> Self {
591 self.hidden = hidden;
592 self
593 }
594
595 #[must_use]
597 pub fn with_command(mut self, command: CommandSpec) -> Self {
598 self.commands.push(command);
599 self
600 }
601
602 #[must_use]
604 pub fn with_group(mut self, group: GroupSpec) -> Self {
605 self.groups.push(group);
606 self
607 }
608
609 #[must_use]
611 pub fn clap_command(&self) -> Command {
612 let mut command = Command::new(self.name.clone()).about(self.short.clone());
613 if let Some(long) = &self.long
614 && !long.is_empty()
615 {
616 command = command.long_about(long.clone());
617 }
618 for alias in &self.aliases {
619 command = command.alias(alias.clone());
620 }
621 if self.hidden {
622 command = command.hide(true);
623 }
624 for group in &self.groups {
625 command = command.subcommand(group.clap_command());
626 }
627 for child in &self.commands {
628 command = command.subcommand(child.clap_command());
629 }
630 command
631 }
632}
633
634#[derive(Clone)]
643pub struct RuntimeCommandSpec {
644 pub spec: CommandSpec,
646 pub handler: CommandHandler,
648 pub streaming_handler: Option<StreamingCommandHandler>,
651}
652
653impl std::fmt::Debug for RuntimeCommandSpec {
654 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
655 formatter
656 .debug_struct("RuntimeCommandSpec")
657 .field("spec", &self.spec)
658 .field("is_streaming", &self.streaming_handler.is_some())
659 .finish_non_exhaustive()
660 }
661}
662
663impl RuntimeCommandSpec {
664 #[must_use]
671 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
672 where
673 F: Fn(CredentialResolver, ValueMap) -> Fut + Send + Sync + 'static,
674 Fut: Future<Output = Result<Output>> + Send + 'static,
675 Output: Into<CommandResult> + Send + 'static,
676 {
677 Self {
678 spec,
679 streaming_handler: None,
680 handler: Arc::new(move |context| {
681 let future = handler(context.credential, context.args);
682 Box::pin(async move { future.await.map(Into::into) })
683 }),
684 }
685 }
686
687 #[must_use]
689 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
690 where
691 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
692 Fut: Future<Output = Result<Output>> + Send + 'static,
693 Output: Into<CommandResult> + Send + 'static,
694 {
695 Self {
696 spec,
697 streaming_handler: None,
698 handler: Arc::new(move |context| {
699 let future = handler(context);
700 Box::pin(async move { future.await.map(Into::into) })
701 }),
702 }
703 }
704
705 #[must_use]
711 pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
712 where
713 F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
714 Fut: Future<Output = Result<()>> + Send + 'static,
715 {
716 let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
717 let future = handler(context, sender);
718 Box::pin(future)
719 });
720 Self {
721 spec,
722 streaming_handler: Some(streaming),
723 handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
724 }
725 }
726
727 #[must_use]
737 pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
738 where
739 T: clap::FromArgMatches + Send + 'static,
740 F: Fn(CredentialResolver, T) -> Fut + Send + Sync + 'static,
741 Fut: Future<Output = Result<Output>> + Send + 'static,
742 Output: Into<CommandResult> + Send + 'static,
743 {
744 let handler = Arc::new(handler);
745 Self {
746 spec,
747 handler: Arc::new(move |context| {
748 let credential = context.credential.clone();
749 let parsed = T::from_arg_matches(context.raw_matches.as_ref());
750 let handler = handler.clone();
751 Box::pin(async move {
752 let args = parsed.map_err(|e| {
753 crate::CliCoreError::Message(format!("argument parse error: {e}"))
754 })?;
755 handler(credential, args).await.map(Into::into)
756 })
757 }),
758 streaming_handler: None,
759 }
760 }
761}
762
763#[derive(Clone, Debug, Default)]
765pub struct RuntimeGroupSpec {
766 pub group: GroupSpec,
768 pub commands: Vec<RuntimeCommandSpec>,
770 pub groups: Vec<RuntimeGroupSpec>,
772}
773
774impl RuntimeGroupSpec {
775 #[must_use]
777 pub fn new(group: GroupSpec) -> Self {
778 Self {
779 group,
780 ..Self::default()
781 }
782 }
783
784 #[must_use]
786 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
787 self.commands.push(command);
788 self
789 }
790
791 #[must_use]
793 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
794 self.groups.push(group);
795 self
796 }
797
798 #[must_use]
800 pub fn clap_command(&self) -> Command {
801 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
802 if let Some(long) = &self.group.long
803 && !long.is_empty()
804 {
805 command = command.long_about(long.clone());
806 }
807 for alias in &self.group.aliases {
808 command = command.alias(alias.clone());
809 }
810 if self.group.hidden {
811 command = command.hide(true);
812 }
813 for group in &self.groups {
814 command = command.subcommand(group.clap_command());
815 }
816 for child in &self.commands {
817 command = command.subcommand(child.spec.clap_command());
818 }
819 command
820 }
821
822 pub(crate) fn register_commands(
823 &self,
824 prefix: &mut Vec<String>,
825 out: &mut BTreeMap<String, RuntimeCommandSpec>,
826 ) {
827 prefix.push(self.group.name.clone());
828 for group in &self.groups {
829 group.register_commands(prefix, out);
830 }
831 for command in &self.commands {
832 prefix.push(command.spec.name.clone());
833 out.insert(prefix.join(":"), command.clone());
834 prefix.pop();
835 }
836 prefix.pop();
837 }
838}
839
840#[must_use]
842pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
843 let mut parts = Vec::new();
844 let mut current = matches;
845 while let Some((name, submatches)) = current.subcommand() {
846 if name != root_name {
847 parts.push(name.to_owned());
848 }
849 current = submatches;
850 }
851 parts.join(":")
852}
853
854#[must_use]
858pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
859 if parts.is_empty() {
860 return String::new();
861 }
862 if parts.len() > 1 {
863 return parts[1..]
864 .iter()
865 .map(AsRef::as_ref)
866 .collect::<Vec<_>>()
867 .join(":");
868 }
869 path_annotation
870 .filter(|annotation| !annotation.is_empty())
871 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
872}
873
874#[must_use]
876pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
877 let mut current = matches;
878 while let Some((_, submatches)) = current.subcommand() {
879 current = submatches;
880 }
881 current
882}
883
884#[must_use]
889pub fn command_args_from_matches(
890 matches: &ArgMatches,
891 spec: &CommandSpec,
892 changed_only: bool,
893) -> ValueMap {
894 let mut args = ValueMap::new();
895 for arg in &spec.args {
896 let id = arg.get_id().to_string();
897 let changed = matches
898 .value_source(&id)
899 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
900 if changed_only && !changed {
901 continue;
902 }
903 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
904 args.insert(id, value);
905 }
906 }
907 args
908}
909
910fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
911 matches.value_source(id)?;
912
913 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
914 && let Some(value) = matches.get_one::<bool>(id)
915 {
916 return Some(Value::Bool(*value));
917 }
918
919 if let Some(value) = typed_arg_value_from_matches(matches, id) {
920 return Some(value);
921 }
922
923 if let Some(values) = matches.get_raw(id) {
924 let rendered = values
925 .map(|value| value.to_string_lossy().into_owned())
926 .collect::<Vec<_>>();
927 return match rendered.as_slice() {
928 [] => None,
929 [single] => Some(Value::String(single.clone())),
930 _ => Some(Value::Array(
931 rendered.into_iter().map(Value::String).collect(),
932 )),
933 };
934 }
935
936 if let Some(value) = matches.get_one::<String>(id) {
937 return Some(Value::String(value.clone()));
938 }
939 if let Some(value) = matches.get_one::<usize>(id) {
940 return Some(serde_json::json!(value));
941 }
942 if let Some(value) = matches.get_one::<u64>(id) {
943 return Some(serde_json::json!(value));
944 }
945 if let Some(value) = matches.get_one::<i64>(id) {
946 return Some(serde_json::json!(value));
947 }
948 None
949}
950
951fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
952 typed_values::<bool>(matches, id, Value::Bool)
953 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
954 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
955 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
956 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
957 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
958 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
959 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
960 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
961 .or_else(|| {
962 typed_values::<usize>(matches, id, |value| {
963 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
964 })
965 })
966 .or_else(|| {
967 typed_values::<f64>(matches, id, |value| {
968 Number::from_f64(value).map_or(Value::Null, Value::Number)
969 })
970 })
971 .or_else(|| {
972 typed_values::<f32>(matches, id, |value| {
973 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
974 })
975 })
976 .or_else(|| typed_values::<String>(matches, id, Value::String))
977}
978
979fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
980where
981 T: Clone + Send + Sync + 'static,
982{
983 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
984 return None;
985 };
986 let values = values.cloned().map(to_value).collect::<Vec<_>>();
987 match values.as_slice() {
988 [] => None,
989 [single] => Some(single.clone()),
990 _ => Some(Value::Array(values)),
991 }
992}