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 #[must_use]
124 pub fn config(&self) -> &crate::config::ConfigFile {
125 &self.middleware.config
126 }
127
128 pub fn typed_args<T: clap::FromArgMatches>(&self) -> Result<T> {
137 T::from_arg_matches(self.raw_matches.as_ref())
138 .map_err(|e| crate::CliCoreError::Message(format!("argument parse error: {e}")))
139 }
140
141 pub async fn credential(&self) -> Result<Credential> {
151 self.credential.resolve().await
152 }
153
154 pub async fn try_credential(&self) -> Result<Option<Credential>> {
163 self.credential.try_resolve().await
164 }
165
166 pub async fn credential_with_scopes(&self, extra: &[String]) -> Result<Credential> {
188 self.credential.resolve_with_scopes(extra).await
189 }
190}
191
192#[derive(Clone, Debug, Default)]
197pub struct CommandSpec {
198 pub name: String,
200 pub short: String,
202 pub long: Option<String>,
204 pub aliases: Vec<String>,
206 pub hidden: bool,
208 pub system: Option<String>,
210 pub default_fields: Option<String>,
212 pub auth: AuthRequirement,
219 pub auth_provider: Option<String>,
221 pub tier: Option<Tier>,
223 pub mutates: bool,
225 pub auth_metadata: BTreeMap<String, String>,
227 pub args: Vec<Arg>,
229 pub output_schema: Option<SchemaInfo>,
231}
232
233impl CommandSpec {
234 #[must_use]
236 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
237 Self {
238 name: name.into(),
239 short: short.into(),
240 ..Self::default()
241 }
242 }
243
244 #[must_use]
250 pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
251 let placeholder = Command::new("__placeholder");
252 let augmented = T::augment_args(placeholder);
253 let args: Vec<Arg> = augmented
254 .get_arguments()
255 .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
256 .cloned()
257 .collect();
258 Self {
259 name: name.into(),
260 short: short.into(),
261 args,
262 ..Self::default()
263 }
264 }
265
266 #[must_use]
268 pub fn with_long(mut self, long: impl Into<String>) -> Self {
269 self.long = Some(long.into());
270 self
271 }
272
273 #[must_use]
275 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
276 self.aliases.push(alias.into());
277 self
278 }
279
280 #[must_use]
282 pub fn hidden(mut self, hidden: bool) -> Self {
283 self.hidden = hidden;
284 self
285 }
286
287 #[must_use]
289 pub fn with_system(mut self, system: impl Into<String>) -> Self {
290 self.system = Some(system.into());
291 self
292 }
293
294 #[must_use]
296 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
297 self.default_fields = Some(default_fields.into());
298 self
299 }
300
301 #[must_use]
303 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
304 self.auth_provider = Some(provider.into());
305 self
306 }
307
308 #[must_use]
314 pub fn no_auth(mut self, no_auth: bool) -> Self {
315 self.auth = if no_auth {
316 AuthRequirement::None
317 } else {
318 AuthRequirement::Required
319 };
320 self
321 }
322
323 #[must_use]
325 pub fn auth(mut self, requirement: AuthRequirement) -> Self {
326 self.auth = requirement;
327 self
328 }
329
330 #[must_use]
337 pub fn auth_optional(mut self) -> Self {
338 self.auth = AuthRequirement::Optional;
339 self
340 }
341
342 #[must_use]
344 pub fn with_tier(mut self, tier: Tier) -> Self {
345 self.tier = Some(tier);
346 self
347 }
348
349 #[must_use]
351 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
352 self.auth_metadata.insert(key.into(), value.into());
353 self
354 }
355
356 #[must_use]
364 pub fn with_scopes(mut self, scopes: &[impl AsRef<str>]) -> Self {
365 let joined = scopes
366 .iter()
367 .map(AsRef::as_ref)
368 .collect::<Vec<_>>()
369 .join(" ");
370 if joined.is_empty() {
373 self.auth_metadata.remove("scopes");
374 } else {
375 self.auth_metadata.insert("scopes".to_owned(), joined);
376 }
377 self
378 }
379
380 #[must_use]
382 pub fn with_arg(mut self, arg: Arg) -> Self {
383 self.args.push(arg);
384 self
385 }
386
387 #[must_use]
389 pub fn with_flag(self, flag: Arg) -> Self {
390 self.with_arg(flag)
391 }
392
393 #[must_use]
395 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
396 self.output_schema = Some(SchemaInfo {
397 command: String::new(),
398 fields: crate::output::fields_for::<T>(),
399 schema: None,
400 });
401 self
402 }
403
404 #[must_use]
406 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
407 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
408 self
409 }
410
411 #[must_use]
413 pub fn mutates(mut self, mutates: bool) -> Self {
414 self.mutates = mutates;
415 self
416 }
417
418 #[must_use]
420 pub fn metadata(&self) -> CommandMeta {
421 let mut auth_metadata = self.auth_metadata.clone();
422 if let Some(provider) = &self.auth_provider
423 && !provider.is_empty()
424 {
425 auth_metadata.insert("provider".to_owned(), provider.clone());
426 }
427 if let Some(tier) = self.tier
428 && !auth_metadata.contains_key("tier")
429 {
430 auth_metadata.insert("tier".to_owned(), tier.to_string());
431 }
432 let scopes = auth_metadata
433 .get("scopes")
434 .map(|scopes| {
435 scopes
436 .split_whitespace()
437 .map(str::to_owned)
438 .collect::<Vec<_>>()
439 })
440 .unwrap_or_default();
441
442 CommandMeta {
443 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
444 auth_metadata,
445 scopes,
446 }
447 }
448
449 #[must_use]
451 pub fn clap_command(&self) -> Command {
452 let mut command = Command::new(self.name.clone()).about(self.short.clone());
453 if let Some(long) = &self.long
454 && !long.is_empty()
455 {
456 command = command.long_about(long.clone());
457 }
458 for alias in &self.aliases {
459 command = command.alias(alias.clone());
460 }
461 if self.hidden {
462 command = command.hide(true);
463 }
464 for arg in &self.args {
465 command = command.arg(arg.clone());
466 }
467 command
468 }
469}
470
471#[derive(Clone, Debug, Default)]
476pub struct GroupSpec {
477 pub name: String,
479 pub short: String,
481 pub long: Option<String>,
483 pub aliases: Vec<String>,
485 pub hidden: bool,
487 pub commands: Vec<CommandSpec>,
489 pub groups: Vec<GroupSpec>,
491}
492
493impl GroupSpec {
494 #[must_use]
496 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
497 Self {
498 name: name.into(),
499 short: short.into(),
500 ..Self::default()
501 }
502 }
503
504 #[must_use]
506 pub fn with_long(mut self, long: impl Into<String>) -> Self {
507 self.long = Some(long.into());
508 self
509 }
510
511 #[must_use]
513 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
514 self.aliases.push(alias.into());
515 self
516 }
517
518 #[must_use]
520 pub fn hidden(mut self, hidden: bool) -> Self {
521 self.hidden = hidden;
522 self
523 }
524
525 #[must_use]
527 pub fn with_command(mut self, command: CommandSpec) -> Self {
528 self.commands.push(command);
529 self
530 }
531
532 #[must_use]
534 pub fn with_group(mut self, group: GroupSpec) -> Self {
535 self.groups.push(group);
536 self
537 }
538
539 #[must_use]
541 pub fn clap_command(&self) -> Command {
542 let mut command = Command::new(self.name.clone()).about(self.short.clone());
543 if let Some(long) = &self.long
544 && !long.is_empty()
545 {
546 command = command.long_about(long.clone());
547 }
548 for alias in &self.aliases {
549 command = command.alias(alias.clone());
550 }
551 if self.hidden {
552 command = command.hide(true);
553 }
554 for group in &self.groups {
555 command = command.subcommand(group.clap_command());
556 }
557 for child in &self.commands {
558 command = command.subcommand(child.clap_command());
559 }
560 command
561 }
562}
563
564#[derive(Clone)]
573pub struct RuntimeCommandSpec {
574 pub spec: CommandSpec,
576 pub handler: CommandHandler,
578 pub streaming_handler: Option<StreamingCommandHandler>,
581}
582
583impl std::fmt::Debug for RuntimeCommandSpec {
584 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585 formatter
586 .debug_struct("RuntimeCommandSpec")
587 .field("spec", &self.spec)
588 .field("is_streaming", &self.streaming_handler.is_some())
589 .finish_non_exhaustive()
590 }
591}
592
593impl RuntimeCommandSpec {
594 #[must_use]
601 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
602 where
603 F: Fn(CredentialResolver, ValueMap) -> 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.credential, context.args);
612 Box::pin(async move { future.await.map(Into::into) })
613 }),
614 }
615 }
616
617 #[must_use]
619 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
620 where
621 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
622 Fut: Future<Output = Result<Output>> + Send + 'static,
623 Output: Into<CommandResult> + Send + 'static,
624 {
625 Self {
626 spec,
627 streaming_handler: None,
628 handler: Arc::new(move |context| {
629 let future = handler(context);
630 Box::pin(async move { future.await.map(Into::into) })
631 }),
632 }
633 }
634
635 #[must_use]
641 pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
642 where
643 F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
644 Fut: Future<Output = Result<()>> + Send + 'static,
645 {
646 let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
647 let future = handler(context, sender);
648 Box::pin(future)
649 });
650 Self {
651 spec,
652 streaming_handler: Some(streaming),
653 handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
654 }
655 }
656
657 #[must_use]
667 pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
668 where
669 T: clap::FromArgMatches + Send + 'static,
670 F: Fn(CredentialResolver, T) -> Fut + Send + Sync + 'static,
671 Fut: Future<Output = Result<Output>> + Send + 'static,
672 Output: Into<CommandResult> + Send + 'static,
673 {
674 let handler = Arc::new(handler);
675 Self {
676 spec,
677 handler: Arc::new(move |context| {
678 let credential = context.credential.clone();
679 let parsed = T::from_arg_matches(context.raw_matches.as_ref());
680 let handler = handler.clone();
681 Box::pin(async move {
682 let args = parsed.map_err(|e| {
683 crate::CliCoreError::Message(format!("argument parse error: {e}"))
684 })?;
685 handler(credential, args).await.map(Into::into)
686 })
687 }),
688 streaming_handler: None,
689 }
690 }
691}
692
693#[derive(Clone, Debug, Default)]
695pub struct RuntimeGroupSpec {
696 pub group: GroupSpec,
698 pub commands: Vec<RuntimeCommandSpec>,
700 pub groups: Vec<RuntimeGroupSpec>,
702}
703
704impl RuntimeGroupSpec {
705 #[must_use]
707 pub fn new(group: GroupSpec) -> Self {
708 Self {
709 group,
710 ..Self::default()
711 }
712 }
713
714 #[must_use]
716 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
717 self.commands.push(command);
718 self
719 }
720
721 #[must_use]
723 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
724 self.groups.push(group);
725 self
726 }
727
728 #[must_use]
730 pub fn clap_command(&self) -> Command {
731 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
732 if let Some(long) = &self.group.long
733 && !long.is_empty()
734 {
735 command = command.long_about(long.clone());
736 }
737 for alias in &self.group.aliases {
738 command = command.alias(alias.clone());
739 }
740 if self.group.hidden {
741 command = command.hide(true);
742 }
743 for group in &self.groups {
744 command = command.subcommand(group.clap_command());
745 }
746 for child in &self.commands {
747 command = command.subcommand(child.spec.clap_command());
748 }
749 command
750 }
751
752 pub(crate) fn register_commands(
753 &self,
754 prefix: &mut Vec<String>,
755 out: &mut BTreeMap<String, RuntimeCommandSpec>,
756 ) {
757 prefix.push(self.group.name.clone());
758 for group in &self.groups {
759 group.register_commands(prefix, out);
760 }
761 for command in &self.commands {
762 prefix.push(command.spec.name.clone());
763 out.insert(prefix.join(":"), command.clone());
764 prefix.pop();
765 }
766 prefix.pop();
767 }
768}
769
770#[must_use]
772pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
773 let mut parts = Vec::new();
774 let mut current = matches;
775 while let Some((name, submatches)) = current.subcommand() {
776 if name != root_name {
777 parts.push(name.to_owned());
778 }
779 current = submatches;
780 }
781 parts.join(":")
782}
783
784#[must_use]
788pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
789 if parts.is_empty() {
790 return String::new();
791 }
792 if parts.len() > 1 {
793 return parts[1..]
794 .iter()
795 .map(AsRef::as_ref)
796 .collect::<Vec<_>>()
797 .join(":");
798 }
799 path_annotation
800 .filter(|annotation| !annotation.is_empty())
801 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
802}
803
804#[must_use]
806pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
807 let mut current = matches;
808 while let Some((_, submatches)) = current.subcommand() {
809 current = submatches;
810 }
811 current
812}
813
814#[must_use]
819pub fn command_args_from_matches(
820 matches: &ArgMatches,
821 spec: &CommandSpec,
822 changed_only: bool,
823) -> ValueMap {
824 let mut args = ValueMap::new();
825 for arg in &spec.args {
826 let id = arg.get_id().to_string();
827 let changed = matches
828 .value_source(&id)
829 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
830 if changed_only && !changed {
831 continue;
832 }
833 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
834 args.insert(id, value);
835 }
836 }
837 args
838}
839
840fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
841 matches.value_source(id)?;
842
843 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
844 && let Some(value) = matches.get_one::<bool>(id)
845 {
846 return Some(Value::Bool(*value));
847 }
848
849 if let Some(value) = typed_arg_value_from_matches(matches, id) {
850 return Some(value);
851 }
852
853 if let Some(values) = matches.get_raw(id) {
854 let rendered = values
855 .map(|value| value.to_string_lossy().into_owned())
856 .collect::<Vec<_>>();
857 return match rendered.as_slice() {
858 [] => None,
859 [single] => Some(Value::String(single.clone())),
860 _ => Some(Value::Array(
861 rendered.into_iter().map(Value::String).collect(),
862 )),
863 };
864 }
865
866 if let Some(value) = matches.get_one::<String>(id) {
867 return Some(Value::String(value.clone()));
868 }
869 if let Some(value) = matches.get_one::<usize>(id) {
870 return Some(serde_json::json!(value));
871 }
872 if let Some(value) = matches.get_one::<u64>(id) {
873 return Some(serde_json::json!(value));
874 }
875 if let Some(value) = matches.get_one::<i64>(id) {
876 return Some(serde_json::json!(value));
877 }
878 None
879}
880
881fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
882 typed_values::<bool>(matches, id, Value::Bool)
883 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
884 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
885 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
886 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
887 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
888 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
889 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
890 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
891 .or_else(|| {
892 typed_values::<usize>(matches, id, |value| {
893 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
894 })
895 })
896 .or_else(|| {
897 typed_values::<f64>(matches, id, |value| {
898 Number::from_f64(value).map_or(Value::Null, Value::Number)
899 })
900 })
901 .or_else(|| {
902 typed_values::<f32>(matches, id, |value| {
903 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
904 })
905 })
906 .or_else(|| typed_values::<String>(matches, id, Value::String))
907}
908
909fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
910where
911 T: Clone + Send + Sync + 'static,
912{
913 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
914 return None;
915 };
916 let values = values.cloned().map(to_value).collect::<Vec<_>>();
917 match values.as_slice() {
918 [] => None,
919 [single] => Some(single.clone()),
920 _ => Some(Value::Array(values)),
921 }
922}