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 CommandMeta, Credential, Middleware, OutputSchema, Result, SchemaInfo, Tier,
10 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: Option<Credential>,
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
124#[derive(Clone, Debug, Default)]
129pub struct CommandSpec {
130 pub name: String,
132 pub short: String,
134 pub long: Option<String>,
136 pub aliases: Vec<String>,
138 pub hidden: bool,
140 pub system: Option<String>,
142 pub default_fields: Option<String>,
144 pub no_auth: bool,
146 pub auth_provider: Option<String>,
148 pub tier: Option<Tier>,
150 pub mutates: bool,
152 pub auth_metadata: BTreeMap<String, String>,
154 pub args: Vec<Arg>,
156 pub output_schema: Option<SchemaInfo>,
158}
159
160impl CommandSpec {
161 #[must_use]
163 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
164 Self {
165 name: name.into(),
166 short: short.into(),
167 ..Self::default()
168 }
169 }
170
171 #[must_use]
177 pub fn from_args<T: clap::Args>(name: impl Into<String>, short: impl Into<String>) -> Self {
178 let placeholder = Command::new("__placeholder");
179 let augmented = T::augment_args(placeholder);
180 let args: Vec<Arg> = augmented
181 .get_arguments()
182 .filter(|a| !matches!(a.get_id().as_str(), "help" | "version"))
183 .cloned()
184 .collect();
185 Self {
186 name: name.into(),
187 short: short.into(),
188 args,
189 ..Self::default()
190 }
191 }
192
193 #[must_use]
195 pub fn with_long(mut self, long: impl Into<String>) -> Self {
196 self.long = Some(long.into());
197 self
198 }
199
200 #[must_use]
202 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
203 self.aliases.push(alias.into());
204 self
205 }
206
207 #[must_use]
209 pub fn hidden(mut self, hidden: bool) -> Self {
210 self.hidden = hidden;
211 self
212 }
213
214 #[must_use]
216 pub fn with_system(mut self, system: impl Into<String>) -> Self {
217 self.system = Some(system.into());
218 self
219 }
220
221 #[must_use]
223 pub fn with_default_fields(mut self, default_fields: impl Into<String>) -> Self {
224 self.default_fields = Some(default_fields.into());
225 self
226 }
227
228 #[must_use]
230 pub fn with_auth_provider(mut self, provider: impl Into<String>) -> Self {
231 self.auth_provider = Some(provider.into());
232 self
233 }
234
235 #[must_use]
237 pub fn no_auth(mut self, no_auth: bool) -> Self {
238 self.no_auth = no_auth;
239 self
240 }
241
242 #[must_use]
244 pub fn with_tier(mut self, tier: Tier) -> Self {
245 self.tier = Some(tier);
246 self
247 }
248
249 #[must_use]
251 pub fn with_auth_metadata(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
252 self.auth_metadata.insert(key.into(), value.into());
253 self
254 }
255
256 #[must_use]
258 pub fn with_arg(mut self, arg: Arg) -> Self {
259 self.args.push(arg);
260 self
261 }
262
263 #[must_use]
265 pub fn with_flag(self, flag: Arg) -> Self {
266 self.with_arg(flag)
267 }
268
269 #[must_use]
271 pub fn with_output_schema<T: OutputSchema>(mut self) -> Self {
272 self.output_schema = Some(SchemaInfo {
273 command: String::new(),
274 fields: crate::output::fields_for::<T>(),
275 schema: None,
276 });
277 self
278 }
279
280 #[must_use]
282 pub fn with_json_schema<T: JsonSchema>(mut self) -> Self {
283 self.output_schema = Some(crate::output::json_schema_info::<T>(""));
284 self
285 }
286
287 #[must_use]
289 pub fn mutates(mut self, mutates: bool) -> Self {
290 self.mutates = mutates;
291 self
292 }
293
294 #[must_use]
296 pub fn metadata(&self) -> CommandMeta {
297 let mut auth_metadata = self.auth_metadata.clone();
298 if let Some(provider) = &self.auth_provider
299 && !provider.is_empty()
300 {
301 auth_metadata.insert("provider".to_owned(), provider.clone());
302 }
303 if let Some(tier) = self.tier
304 && !auth_metadata.contains_key("tier")
305 {
306 auth_metadata.insert("tier".to_owned(), tier.to_string());
307 }
308 let scopes = auth_metadata
309 .get("scopes")
310 .map(|scopes| {
311 scopes
312 .split_whitespace()
313 .map(str::to_owned)
314 .collect::<Vec<_>>()
315 })
316 .unwrap_or_default();
317
318 CommandMeta {
319 dry_run_prompt: self.mutates || self.tier.is_some_and(Tier::is_mutating),
320 auth_metadata,
321 scopes,
322 }
323 }
324
325 #[must_use]
327 pub fn clap_command(&self) -> Command {
328 let mut command = Command::new(self.name.clone()).about(self.short.clone());
329 if let Some(long) = &self.long
330 && !long.is_empty()
331 {
332 command = command.long_about(long.clone());
333 }
334 for alias in &self.aliases {
335 command = command.alias(alias.clone());
336 }
337 if self.hidden {
338 command = command.hide(true);
339 }
340 for arg in &self.args {
341 command = command.arg(arg.clone());
342 }
343 command
344 }
345}
346
347#[derive(Clone, Debug, Default)]
352pub struct GroupSpec {
353 pub name: String,
355 pub short: String,
357 pub long: Option<String>,
359 pub aliases: Vec<String>,
361 pub hidden: bool,
363 pub commands: Vec<CommandSpec>,
365 pub groups: Vec<GroupSpec>,
367}
368
369impl GroupSpec {
370 #[must_use]
372 pub fn new(name: impl Into<String>, short: impl Into<String>) -> Self {
373 Self {
374 name: name.into(),
375 short: short.into(),
376 ..Self::default()
377 }
378 }
379
380 #[must_use]
382 pub fn with_long(mut self, long: impl Into<String>) -> Self {
383 self.long = Some(long.into());
384 self
385 }
386
387 #[must_use]
389 pub fn with_alias(mut self, alias: impl Into<String>) -> Self {
390 self.aliases.push(alias.into());
391 self
392 }
393
394 #[must_use]
396 pub fn hidden(mut self, hidden: bool) -> Self {
397 self.hidden = hidden;
398 self
399 }
400
401 #[must_use]
403 pub fn with_command(mut self, command: CommandSpec) -> Self {
404 self.commands.push(command);
405 self
406 }
407
408 #[must_use]
410 pub fn with_group(mut self, group: GroupSpec) -> Self {
411 self.groups.push(group);
412 self
413 }
414
415 #[must_use]
417 pub fn clap_command(&self) -> Command {
418 let mut command = Command::new(self.name.clone()).about(self.short.clone());
419 if let Some(long) = &self.long
420 && !long.is_empty()
421 {
422 command = command.long_about(long.clone());
423 }
424 for alias in &self.aliases {
425 command = command.alias(alias.clone());
426 }
427 if self.hidden {
428 command = command.hide(true);
429 }
430 for group in &self.groups {
431 command = command.subcommand(group.clap_command());
432 }
433 for child in &self.commands {
434 command = command.subcommand(child.clap_command());
435 }
436 command
437 }
438}
439
440#[derive(Clone)]
449pub struct RuntimeCommandSpec {
450 pub spec: CommandSpec,
452 pub handler: CommandHandler,
454 pub streaming_handler: Option<StreamingCommandHandler>,
457}
458
459impl std::fmt::Debug for RuntimeCommandSpec {
460 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
461 formatter
462 .debug_struct("RuntimeCommandSpec")
463 .field("spec", &self.spec)
464 .field("is_streaming", &self.streaming_handler.is_some())
465 .finish_non_exhaustive()
466 }
467}
468
469impl RuntimeCommandSpec {
470 #[must_use]
475 pub fn new<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
476 where
477 F: Fn(Option<Credential>, ValueMap) -> Fut + Send + Sync + 'static,
478 Fut: Future<Output = Result<Output>> + Send + 'static,
479 Output: Into<CommandResult> + Send + 'static,
480 {
481 Self {
482 spec,
483 streaming_handler: None,
484 handler: Arc::new(move |context| {
485 let future = handler(context.credential, context.args);
486 Box::pin(async move { future.await.map(Into::into) })
487 }),
488 }
489 }
490
491 #[must_use]
493 pub fn new_with_context<F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
494 where
495 F: Fn(CommandContext) -> Fut + Send + Sync + 'static,
496 Fut: Future<Output = Result<Output>> + Send + 'static,
497 Output: Into<CommandResult> + Send + 'static,
498 {
499 Self {
500 spec,
501 streaming_handler: None,
502 handler: Arc::new(move |context| {
503 let future = handler(context);
504 Box::pin(async move { future.await.map(Into::into) })
505 }),
506 }
507 }
508
509 #[must_use]
515 pub fn new_streaming<F, Fut>(spec: CommandSpec, handler: F) -> Self
516 where
517 F: Fn(CommandContext, StreamSender) -> Fut + Send + Sync + 'static,
518 Fut: Future<Output = Result<()>> + Send + 'static,
519 {
520 let streaming: StreamingCommandHandler = Arc::new(move |context, sender| {
521 let future = handler(context, sender);
522 Box::pin(future)
523 });
524 Self {
525 spec,
526 streaming_handler: Some(streaming),
527 handler: Arc::new(|_context| Box::pin(async { Ok(CommandResult::new(Value::Null)) })),
528 }
529 }
530
531 #[must_use]
541 pub fn new_typed<T, F, Fut, Output>(spec: CommandSpec, handler: F) -> Self
542 where
543 T: clap::FromArgMatches + Send + 'static,
544 F: Fn(Option<Credential>, T) -> Fut + Send + Sync + 'static,
545 Fut: Future<Output = Result<Output>> + Send + 'static,
546 Output: Into<CommandResult> + Send + 'static,
547 {
548 let handler = Arc::new(handler);
549 Self {
550 spec,
551 handler: Arc::new(move |context| {
552 let credential = context.credential.clone();
553 let parsed = T::from_arg_matches(context.raw_matches.as_ref());
554 let handler = handler.clone();
555 Box::pin(async move {
556 let args = parsed.map_err(|e| {
557 crate::CliCoreError::Message(format!("argument parse error: {e}"))
558 })?;
559 handler(credential, args).await.map(Into::into)
560 })
561 }),
562 streaming_handler: None,
563 }
564 }
565}
566
567#[derive(Clone, Debug, Default)]
569pub struct RuntimeGroupSpec {
570 pub group: GroupSpec,
572 pub commands: Vec<RuntimeCommandSpec>,
574 pub groups: Vec<RuntimeGroupSpec>,
576}
577
578impl RuntimeGroupSpec {
579 #[must_use]
581 pub fn new(group: GroupSpec) -> Self {
582 Self {
583 group,
584 ..Self::default()
585 }
586 }
587
588 #[must_use]
590 pub fn with_command(mut self, command: RuntimeCommandSpec) -> Self {
591 self.commands.push(command);
592 self
593 }
594
595 #[must_use]
597 pub fn with_group(mut self, group: RuntimeGroupSpec) -> Self {
598 self.groups.push(group);
599 self
600 }
601
602 #[must_use]
604 pub fn clap_command(&self) -> Command {
605 let mut command = Command::new(self.group.name.clone()).about(self.group.short.clone());
606 if let Some(long) = &self.group.long
607 && !long.is_empty()
608 {
609 command = command.long_about(long.clone());
610 }
611 for alias in &self.group.aliases {
612 command = command.alias(alias.clone());
613 }
614 if self.group.hidden {
615 command = command.hide(true);
616 }
617 for group in &self.groups {
618 command = command.subcommand(group.clap_command());
619 }
620 for child in &self.commands {
621 command = command.subcommand(child.spec.clap_command());
622 }
623 command
624 }
625
626 pub(crate) fn register_commands(
627 &self,
628 prefix: &mut Vec<String>,
629 out: &mut BTreeMap<String, RuntimeCommandSpec>,
630 ) {
631 prefix.push(self.group.name.clone());
632 for group in &self.groups {
633 group.register_commands(prefix, out);
634 }
635 for command in &self.commands {
636 prefix.push(command.spec.name.clone());
637 out.insert(prefix.join(":"), command.clone());
638 prefix.pop();
639 }
640 prefix.pop();
641 }
642}
643
644#[must_use]
646pub fn command_path_from_matches(root_name: &str, matches: &ArgMatches) -> String {
647 let mut parts = Vec::new();
648 let mut current = matches;
649 while let Some((name, submatches)) = current.subcommand() {
650 if name != root_name {
651 parts.push(name.to_owned());
652 }
653 current = submatches;
654 }
655 parts.join(":")
656}
657
658#[must_use]
662pub fn command_path_from_parts(parts: &[impl AsRef<str>], path_annotation: Option<&str>) -> String {
663 if parts.is_empty() {
664 return String::new();
665 }
666 if parts.len() > 1 {
667 return parts[1..]
668 .iter()
669 .map(AsRef::as_ref)
670 .collect::<Vec<_>>()
671 .join(":");
672 }
673 path_annotation
674 .filter(|annotation| !annotation.is_empty())
675 .map_or_else(|| parts[0].as_ref().to_owned(), ToOwned::to_owned)
676}
677
678#[must_use]
680pub fn leaf_matches(matches: &ArgMatches) -> &ArgMatches {
681 let mut current = matches;
682 while let Some((_, submatches)) = current.subcommand() {
683 current = submatches;
684 }
685 current
686}
687
688#[must_use]
693pub fn command_args_from_matches(
694 matches: &ArgMatches,
695 spec: &CommandSpec,
696 changed_only: bool,
697) -> ValueMap {
698 let mut args = ValueMap::new();
699 for arg in &spec.args {
700 let id = arg.get_id().to_string();
701 let changed = matches
702 .value_source(&id)
703 .is_some_and(|source| source == clap::parser::ValueSource::CommandLine);
704 if changed_only && !changed {
705 continue;
706 }
707 if let Some(value) = arg_value_from_matches(matches, arg, &id) {
708 args.insert(id, value);
709 }
710 }
711 args
712}
713
714fn arg_value_from_matches(matches: &ArgMatches, flag: &Arg, id: &str) -> Option<Value> {
715 matches.value_source(id)?;
716
717 if matches!(flag.get_action(), ArgAction::SetTrue | ArgAction::SetFalse)
718 && let Some(value) = matches.get_one::<bool>(id)
719 {
720 return Some(Value::Bool(*value));
721 }
722
723 if let Some(value) = typed_arg_value_from_matches(matches, id) {
724 return Some(value);
725 }
726
727 if let Some(values) = matches.get_raw(id) {
728 let rendered = values
729 .map(|value| value.to_string_lossy().into_owned())
730 .collect::<Vec<_>>();
731 return match rendered.as_slice() {
732 [] => None,
733 [single] => Some(Value::String(single.clone())),
734 _ => Some(Value::Array(
735 rendered.into_iter().map(Value::String).collect(),
736 )),
737 };
738 }
739
740 if let Some(value) = matches.get_one::<String>(id) {
741 return Some(Value::String(value.clone()));
742 }
743 if let Some(value) = matches.get_one::<usize>(id) {
744 return Some(serde_json::json!(value));
745 }
746 if let Some(value) = matches.get_one::<u64>(id) {
747 return Some(serde_json::json!(value));
748 }
749 if let Some(value) = matches.get_one::<i64>(id) {
750 return Some(serde_json::json!(value));
751 }
752 None
753}
754
755fn typed_arg_value_from_matches(matches: &ArgMatches, id: &str) -> Option<Value> {
756 typed_values::<bool>(matches, id, Value::Bool)
757 .or_else(|| typed_values::<i8>(matches, id, |value| Value::Number(value.into())))
758 .or_else(|| typed_values::<i16>(matches, id, |value| Value::Number(value.into())))
759 .or_else(|| typed_values::<i64>(matches, id, |value| Value::Number(value.into())))
760 .or_else(|| typed_values::<i32>(matches, id, |value| Value::Number(value.into())))
761 .or_else(|| typed_values::<u8>(matches, id, |value| Value::Number(value.into())))
762 .or_else(|| typed_values::<u16>(matches, id, |value| Value::Number(value.into())))
763 .or_else(|| typed_values::<u64>(matches, id, |value| Value::Number(value.into())))
764 .or_else(|| typed_values::<u32>(matches, id, |value| Value::Number(value.into())))
765 .or_else(|| {
766 typed_values::<usize>(matches, id, |value| {
767 u64::try_from(value).map_or(Value::Null, |value| Value::Number(value.into()))
768 })
769 })
770 .or_else(|| {
771 typed_values::<f64>(matches, id, |value| {
772 Number::from_f64(value).map_or(Value::Null, Value::Number)
773 })
774 })
775 .or_else(|| {
776 typed_values::<f32>(matches, id, |value| {
777 Number::from_f64(f64::from(value)).map_or(Value::Null, Value::Number)
778 })
779 })
780 .or_else(|| typed_values::<String>(matches, id, Value::String))
781}
782
783fn typed_values<T>(matches: &ArgMatches, id: &str, to_value: impl Fn(T) -> Value) -> Option<Value>
784where
785 T: Clone + Send + Sync + 'static,
786{
787 let Ok(Some(values)) = matches.try_get_many::<T>(id) else {
788 return None;
789 };
790 let values = values.cloned().map(to_value).collect::<Vec<_>>();
791 match values.as_slice() {
792 [] => None,
793 [single] => Some(single.clone()),
794 _ => Some(Value::Array(values)),
795 }
796}