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
149#[derive(Clone, Debug, Default)]
154pub struct CommandSpec {
155 pub name: String,
157 pub short: String,
159 pub long: Option<String>,
161 pub aliases: Vec<String>,
163 pub hidden: bool,
165 pub system: Option<String>,
167 pub default_fields: Option<String>,
169 pub auth: AuthRequirement,
176 pub auth_provider: Option<String>,
178 pub tier: Option<Tier>,
180 pub mutates: bool,
182 pub auth_metadata: BTreeMap<String, String>,
184 pub args: Vec<Arg>,
186 pub output_schema: Option<SchemaInfo>,
188}
189
190impl CommandSpec {
191 #[must_use]
193 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
194 Self {
195 name: name.into(),
196 short: short.into(),
197 ..Self::default()
198 }
199 }
200
201 #[must_use]
207 pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
208 let placeholder = Command::new("__placeholder");
209 let augmented = T::augment_args(placeholder);
210 let args: Vec<Arg> = augmented
211 .get_arguments()
212 .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
213 .cloned()
214 .collect();
215 Self {
216 name: name.into(),
217 short: short.into(),
218 args,
219 ..Self::default()
220 }
221 }
222
223 #[must_use]
225 pub fn with_long(mut self, long: impl Into<String>) -> Self {
226 self.long = Some(long.into());
227 self
228 }
229
230 #[must_use]
232 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
233 self.aliases.push(alias.into());
234 self
235 }
236
237 #[must_use]
239 pub fn hidden(mut self, hidden: bool) -> Self {
240 self.hidden = hidden;
241 self
242 }
243
244 #[must_use]
246 pub fn with_system(mut self, system: impl Into<String>) -> Self {
247 self.system = Some(system.into());
248 self
249 }
250
251 #[must_use]
253 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
254 self.default_fields = Some(default_fields.into());
255 self
256 }
257
258 #[must_use]
260 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
261 self.auth_provider = Some(provider.into());
262 self
263 }
264
265 #[must_use]
271 pub fn no_auth(mut self, no_auth: bool) -> Self {
272 self.auth = if no_auth {
273 AuthRequirement::None
274 } else {
275 AuthRequirement::Required
276 };
277 self
278 }
279
280 #[must_use]
282 pub fn auth(mut self, requirement: AuthRequirement) -> Self {
283 self.auth = requirement;
284 self
285 }
286
287 #[must_use]
294 pub fn auth_optional(mut self) -> Self {
295 self.auth = AuthRequirement::Optional;
296 self
297 }
298
299 #[must_use]
301 pub fn with_tier(mut self, tier: Tier) -> Self {
302 self.tier = Some(tier);
303 self
304 }
305
306 #[must_use]
308 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
309 self.auth_metadata.insert(key.into(), value.into());
310 self
311 }
312
313 #[must_use]
315 pub fn with_arg(mut self, arg: Arg) -> Self {
316 self.args.push(arg);
317 self
318 }
319
320 #[must_use]
322 pub fn with_flag(self, flag: Arg) -> Self {
323 self.with_arg(flag)
324 }
325
326 #[must_use]
328 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
329 self.output_schema = Some(SchemaInfo {
330 command: String::new(),
331 fields: crate::output::fields_for::<T>(),
332 schema: None,
333 });
334 self
335 }
336
337 #[must_use]
339 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
340 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
341 self
342 }
343
344 #[must_use]
346 pub fn mutates(mut self, mutates: bool) -> Self {
347 self.mutates = mutates;
348 self
349 }
350
351 #[must_use]
353 pub fn metadata(&self) -> CommandMeta {
354 let mut auth_metadata = self.auth_metadata.clone();
355 if let Some(provider) = &self.auth_provider
356 && !provider.is_empty()
357 {
358 auth_metadata.insert("provider".to_owned(), provider.clone());
359 }
360 if let Some(tier) = self.tier
361 && !auth_metadata.contains_key("tier")
362 {
363 auth_metadata.insert("tier".to_owned(), tier.to_string());
364 }
365 let scopes = auth_metadata
366 .get("scopes")
367 .map(|scopes| {
368 scopes
369 .split_whitespace()
370 .map(str::to_owned)
371 .collect::<Vec<_>>()
372 })
373 .unwrap_or_default();
374
375 CommandMeta {
376 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
377 auth_metadata,
378 scopes,
379 }
380 }
381
382 #[must_use]
384 pub fn clap_command(&self) -> Command {
385 let mut command = Command::new(self.name.clone()).about(self.short.clone());
386 if let Some(long) = &self.long
387 && !long.is_empty()
388 {
389 command = command.long_about(long.clone());
390 }
391 for alias in &self.aliases {
392 command = command.alias(alias.clone());
393 }
394 if self.hidden {
395 command = command.hide(true);
396 }
397 for arg in &self.args {
398 command = command.arg(arg.clone());
399 }
400 command
401 }
402}
403
404#[derive(Clone, Debug, Default)]
409pub struct GroupSpec {
410 pub name: String,
412 pub short: String,
414 pub long: Option<String>,
416 pub aliases: Vec<String>,
418 pub hidden: bool,
420 pub commands: Vec<CommandSpec>,
422 pub groups: Vec<GroupSpec>,
424}
425
426impl GroupSpec {
427 #[must_use]
429 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
430 Self {
431 name: name.into(),
432 short: short.into(),
433 ..Self::default()
434 }
435 }
436
437 #[must_use]
439 pub fn with_long(mut self, long: impl Into<String>) -> Self {
440 self.long = Some(long.into());
441 self
442 }
443
444 #[must_use]
446 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
447 self.aliases.push(alias.into());
448 self
449 }
450
451 #[must_use]
453 pub fn hidden(mut self, hidden: bool) -> Self {
454 self.hidden = hidden;
455 self
456 }
457
458 #[must_use]
460 pub fn with_command(mut self, command: CommandSpec) -> Self {
461 self.commands.push(command);
462 self
463 }
464
465 #[must_use]
467 pub fn with_group(mut self, group: GroupSpec) -> Self {
468 self.groups.push(group);
469 self
470 }
471
472 #[must_use]
474 pub fn clap_command(&self) -> Command {
475 let mut command = Command::new(self.name.clone()).about(self.short.clone());
476 if let Some(long) = &self.long
477 && !long.is_empty()
478 {
479 command = command.long_about(long.clone());
480 }
481 for alias in &self.aliases {
482 command = command.alias(alias.clone());
483 }
484 if self.hidden {
485 command = command.hide(true);
486 }
487 for group in &self.groups {
488 command = command.subcommand(group.clap_command());
489 }
490 for child in &self.commands {
491 command = command.subcommand(child.clap_command());
492 }
493 command
494 }
495}
496
497#[derive(Clone)]
506pub struct RuntimeCommandSpec {
507 pub spec: CommandSpec,
509 pub handler: CommandHandler,
511 pub streaming_handler: Option<StreamingCommandHandler>,
514}
515
516impl std::fmt::Debug for RuntimeCommandSpec {
517 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
518 formatter
519 .debug_struct("RuntimeCommandSpec")
520 .field("spec", &self.spec)
521 .field("is_streaming", &self.streaming_handler.is_some())
522 .finish_non_exhaustive()
523 }
524}
525
526impl RuntimeCommandSpec {
527 #[must_use]
534 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
535 where
536 F: Fn(CredentialResolver, ValueMap) -> Fut + Send + Sync + 'static,
537 Fut: Future<Output = Result<Output>> + Send + 'static,
538 Output: Into<CommandResult> + Send + 'static,
539 {
540 Self {
541 spec,
542 streaming_handler: None,
543 handler: Arc::new(move |context| {
544 let future = handler(context.credential, context.args);
545 Box::pin(async move { future.await.map(Into::into) })
546 }),
547 }
548 }
549
550 #[must_use]
552 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
553 where
554 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
555 Fut: Future<Output = Result<Output>> + Send + 'static,
556 Output: Into<CommandResult> + Send + 'static,
557 {
558 Self {
559 spec,
560 streaming_handler: None,
561 handler: Arc::new(move |context| {
562 let future = handler(context);
563 Box::pin(async move { future.await.map(Into::into) })
564 }),
565 }
566 }
567
568 #[must_use]
574 pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
575 where
576 F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
577 Fut: Future<Output = Result<()>> + Send + 'static,
578 {
579 let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
580 let future = handler(context, sender);
581 Box::pin(future)
582 });
583 Self {
584 spec,
585 streaming_handler: Some(streaming),
586 handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
587 }
588 }
589
590 #[must_use]
600 pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
601 where
602 T: clap::FromArgMatches + Send + 'static,
603 F: Fn(CredentialResolver, T) -> Fut + Send + Sync + 'static,
604 Fut: Future<Output = Result<Output>> + Send + 'static,
605 Output: Into<CommandResult> + Send + 'static,
606 {
607 let handler = Arc::new(handler);
608 Self {
609 spec,
610 handler: Arc::new(move |context| {
611 let credential = context.credential.clone();
612 let parsed = T::from_arg_matches(context.raw_matches.as_ref());
613 let handler = handler.clone();
614 Box::pin(async move {
615 let args = parsed.map_err(|e| {
616 crate::CliCoreError::Message(format!("argument parse error: {e}"))
617 })?;
618 handler(credential, args).await.map(Into::into)
619 })
620 }),
621 streaming_handler: None,
622 }
623 }
624}
625
626#[derive(Clone, Debug, Default)]
628pub struct RuntimeGroupSpec {
629 pub group: GroupSpec,
631 pub commands: Vec<RuntimeCommandSpec>,
633 pub groups: Vec<RuntimeGroupSpec>,
635}
636
637impl RuntimeGroupSpec {
638 #[must_use]
640 pub fn new(group: GroupSpec) -> Self {
641 Self {
642 group,
643 ..Self::default()
644 }
645 }
646
647 #[must_use]
649 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
650 self.commands.push(command);
651 self
652 }
653
654 #[must_use]
656 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
657 self.groups.push(group);
658 self
659 }
660
661 #[must_use]
663 pub fn clap_command(&self) -> Command {
664 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
665 if let Some(long) = &self.group.long
666 && !long.is_empty()
667 {
668 command = command.long_about(long.clone());
669 }
670 for alias in &self.group.aliases {
671 command = command.alias(alias.clone());
672 }
673 if self.group.hidden {
674 command = command.hide(true);
675 }
676 for group in &self.groups {
677 command = command.subcommand(group.clap_command());
678 }
679 for child in &self.commands {
680 command = command.subcommand(child.spec.clap_command());
681 }
682 command
683 }
684
685 pub(crate) fn register_commands(
686 &self,
687 prefix: &mut Vec<String>,
688 out: &mut BTreeMap<String, RuntimeCommandSpec>,
689 ) {
690 prefix.push(self.group.name.clone());
691 for group in &self.groups {
692 group.register_commands(prefix, out);
693 }
694 for command in &self.commands {
695 prefix.push(command.spec.name.clone());
696 out.insert(prefix.join(":"), command.clone());
697 prefix.pop();
698 }
699 prefix.pop();
700 }
701}
702
703#[must_use]
705pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
706 let mut parts = Vec::new();
707 let mut current = matches;
708 while let Some((name, submatches)) = current.subcommand() {
709 if name != root_name {
710 parts.push(name.to_owned());
711 }
712 current = submatches;
713 }
714 parts.join(":")
715}
716
717#[must_use]
721pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
722 if parts.is_empty() {
723 return String::new();
724 }
725 if parts.len() > 1 {
726 return parts[1..]
727 .iter()
728 .map(AsRef::as_ref)
729 .collect::<Vec<_>>()
730 .join(":");
731 }
732 path_annotation
733 .filter(|annotation| !annotation.is_empty())
734 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
735}
736
737#[must_use]
739pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
740 let mut current = matches;
741 while let Some((_, submatches)) = current.subcommand() {
742 current = submatches;
743 }
744 current
745}
746
747#[must_use]
752pub fn command_args_from_matches(
753 matches: &ArgMatches,
754 spec: &CommandSpec,
755 changed_only: bool,
756) -> ValueMap {
757 let mut args = ValueMap::new();
758 for arg in &spec.args {
759 let id = arg.get_id().to_string();
760 let changed = matches
761 .value_source(&id)
762 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
763 if changed_only && !changed {
764 continue;
765 }
766 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
767 args.insert(id, value);
768 }
769 }
770 args
771}
772
773fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
774 matches.value_source(id)?;
775
776 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
777 && let Some(value) = matches.get_one::<bool>(id)
778 {
779 return Some(Value::Bool(*value));
780 }
781
782 if let Some(value) = typed_arg_value_from_matches(matches, id) {
783 return Some(value);
784 }
785
786 if let Some(values) = matches.get_raw(id) {
787 let rendered = values
788 .map(|value| value.to_string_lossy().into_owned())
789 .collect::<Vec<_>>();
790 return match rendered.as_slice() {
791 [] => None,
792 [single] => Some(Value::String(single.clone())),
793 _ => Some(Value::Array(
794 rendered.into_iter().map(Value::String).collect(),
795 )),
796 };
797 }
798
799 if let Some(value) = matches.get_one::<String>(id) {
800 return Some(Value::String(value.clone()));
801 }
802 if let Some(value) = matches.get_one::<usize>(id) {
803 return Some(serde_json::json!(value));
804 }
805 if let Some(value) = matches.get_one::<u64>(id) {
806 return Some(serde_json::json!(value));
807 }
808 if let Some(value) = matches.get_one::<i64>(id) {
809 return Some(serde_json::json!(value));
810 }
811 None
812}
813
814fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
815 typed_values::<bool>(matches, id, Value::Bool)
816 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
817 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
818 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
819 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
820 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
821 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
822 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
823 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
824 .or_else(|| {
825 typed_values::<usize>(matches, id, |value| {
826 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
827 })
828 })
829 .or_else(|| {
830 typed_values::<f64>(matches, id, |value| {
831 Number::from_f64(value).map_or(Value::Null, Value::Number)
832 })
833 })
834 .or_else(|| {
835 typed_values::<f32>(matches, id, |value| {
836 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
837 })
838 })
839 .or_else(|| typed_values::<String>(matches, id, Value::String))
840}
841
842fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
843where
844 T: Clone + Send + Sync + 'static,
845{
846 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
847 return None;
848 };
849 let values = values.cloned().map(to_value).collect::<Vec<_>>();
850 match values.as_slice() {
851 [] => None,
852 [single] => Some(single.clone()),
853 _ => Some(Value::Array(values)),
854 }
855}