1use std::collections::BTreeMap;
4
5use heck::ToKebabCase;
6use openapiv3::OpenAPI;
7use proc_macro2::TokenStream;
8use quote::{format_ident, quote, ToTokens};
9use typify::{Type, TypeEnumVariant, TypeSpaceImpl, TypeStructPropInfo};
10
11use crate::{
12 method::{OperationParameterKind, OperationParameterType, OperationResponseStatus},
13 to_schema::ToSchema,
14 util::{sanitize, Case},
15 validate_openapi, Generator, Result,
16};
17
18struct CliOperation {
19 cli_fn: TokenStream,
20 execute_fn: TokenStream,
21 execute_trait: TokenStream,
22}
23
24impl Generator {
25 pub fn cli(&mut self, spec: &OpenAPI, crate_name: &str) -> Result<TokenStream> {
27 validate_openapi(spec)?;
28
29 let schemas = spec.components.iter().flat_map(|components| {
31 components
32 .schemas
33 .iter()
34 .map(|(name, ref_or_schema)| (name.clone(), ref_or_schema.to_schema()))
35 });
36
37 self.type_space.add_ref_types(schemas)?;
38
39 let raw_methods = spec
40 .paths
41 .iter()
42 .flat_map(|(path, ref_or_item)| {
43 let item = ref_or_item.as_item().unwrap();
45 item.iter().map(move |(method, operation)| {
46 (path.as_str(), method, operation, &item.parameters)
47 })
48 })
49 .map(|(path, method, operation, path_parameters)| {
50 self.process_operation(operation, &spec.components, path, method, path_parameters)
51 })
52 .collect::<Result<Vec<_>>>()?;
53
54 let methods = raw_methods
55 .iter()
56 .map(|method| self.cli_method(method))
57 .collect::<Vec<_>>();
58
59 let cli_ops = methods.iter().map(|op| &op.cli_fn);
60 let execute_ops = methods.iter().map(|op| &op.execute_fn);
61 let trait_ops = methods.iter().map(|op| &op.execute_trait);
62
63 let cli_fns = raw_methods
64 .iter()
65 .map(|method| format_ident!("cli_{}", sanitize(&method.operation_id, Case::Snake)))
66 .collect::<Vec<_>>();
67 let execute_fns = raw_methods
68 .iter()
69 .map(|method| format_ident!("execute_{}", sanitize(&method.operation_id, Case::Snake)))
70 .collect::<Vec<_>>();
71
72 let cli_variants = raw_methods
73 .iter()
74 .map(|method| format_ident!("{}", sanitize(&method.operation_id, Case::Pascal)))
75 .collect::<Vec<_>>();
76
77 let crate_path = syn::TypePath {
78 qself: None,
79 path: syn::parse_str(crate_name).unwrap(),
80 };
81
82 let cli_bounds: Vec<_> = self
83 .settings
84 .extra_cli_bounds
85 .iter()
86 .map(|b| syn::parse_str::<syn::Path>(b).unwrap().into_token_stream())
87 .collect();
88
89 let code = quote! {
90 use #crate_path::*;
91 use anyhow::Context as _;
92
93 pub struct Cli<T: CliConfig> {
94 client: Client,
95 config: T,
96 }
97 impl<T: CliConfig> Cli<T> {
98 pub fn new(
99 client: Client,
100 config: T,
101 ) -> Self {
102 Self { client, config }
103 }
104
105 pub fn get_command(cmd: CliCommand) -> ::clap::Command {
106 match cmd {
107 #(
108 CliCommand::#cli_variants => Self::#cli_fns(),
109 )*
110 }
111 }
112
113 #(#cli_ops)*
114
115 pub async fn execute(
116 &self,
117 cmd: CliCommand,
118 matches: &::clap::ArgMatches,
119 ) -> anyhow::Result<()> {
120 match cmd {
121 #(
122 CliCommand::#cli_variants => {
123 self.#execute_fns(matches).await
125 }
126 )*
127 }
128 }
129
130 #(#execute_ops)*
131 }
132
133 pub trait CliConfig {
134 fn success_item<T>(&self, value: &ResponseValue<T>)
135 where
136 T: #(#cli_bounds+)* schemars::JsonSchema + serde::Serialize + std::fmt::Debug;
137 fn success_no_item(&self, value: &ResponseValue<()>);
138 fn error<T>(&self, value: &Error<T>)
139 where
140 T: #(#cli_bounds+)* schemars::JsonSchema + serde::Serialize + std::fmt::Debug;
141
142 fn list_start<T>(&self)
143 where
144 T: #(#cli_bounds+)* schemars::JsonSchema + serde::Serialize + std::fmt::Debug;
145 fn list_item<T>(&self, value: &T)
146 where
147 T: #(#cli_bounds+)* schemars::JsonSchema + serde::Serialize + std::fmt::Debug;
148 fn list_end_success<T>(&self)
149 where
150 T: #(#cli_bounds+)* schemars::JsonSchema + serde::Serialize + std::fmt::Debug;
151 fn list_end_error<T>(&self, value: &Error<T>)
152 where
153 T: #(#cli_bounds+)* schemars::JsonSchema + serde::Serialize + std::fmt::Debug;
154
155 #(#trait_ops)*
156 }
157
158 #[derive(Copy, Clone, Debug)]
159 pub enum CliCommand {
160 #(#cli_variants,)*
161 }
162
163 impl CliCommand {
164 pub fn iter() -> impl Iterator<Item = CliCommand> {
165 vec![
166 #(
167 CliCommand::#cli_variants,
168 )*
169 ].into_iter()
170 }
171 }
172
173 };
174
175 Ok(code)
176 }
177
178 fn cli_method(&mut self, method: &crate::method::OperationMethod) -> CliOperation {
179 let CliArg {
180 parser: parser_args,
181 consumer: consumer_args,
182 } = self.cli_method_args(method);
183
184 let about = method.summary.as_ref().map(|summary| {
185 quote! {
186 .about(#summary)
187 }
188 });
189
190 let long_about = method.description.as_ref().map(|description| {
191 quote! {
192 .long_about(#description)
193 }
194 });
195
196 let fn_name = format_ident!("cli_{}", &method.operation_id);
197
198 let cli_fn = quote! {
199 pub fn #fn_name() -> ::clap::Command
200 {
201 ::clap::Command::new("")
202 #parser_args
203 #about
204 #long_about
205 }
206 };
207
208 let fn_name = format_ident!("execute_{}", &method.operation_id);
209 let op_name = format_ident!("{}", &method.operation_id);
210
211 let (_, success_kind) =
212 self.extract_responses(method, OperationResponseStatus::is_success_or_default);
213 let (_, error_kind) =
214 self.extract_responses(method, OperationResponseStatus::is_error_or_default);
215
216 let execute_and_output = match method.dropshot_paginated {
217 None => {
219 let success_output = match success_kind {
220 crate::method::OperationResponseKind::Type(_) => {
221 quote! {
222 {
223 self.config.success_item(&r);
224 Ok(())
225 }
226 }
227 }
228 crate::method::OperationResponseKind::None => {
229 quote! {
230 {
231 self.config.success_no_item(&r);
232 Ok(())
233 }
234 }
235 }
236 crate::method::OperationResponseKind::Raw
237 | crate::method::OperationResponseKind::Upgrade => {
238 quote! {
239 {
240 todo!()
241 }
242 }
243 }
244 };
245
246 let error_output = match error_kind {
247 crate::method::OperationResponseKind::Type(_)
248 | crate::method::OperationResponseKind::None => {
249 quote! {
250 {
251 self.config.error(&r);
252 Err(anyhow::Error::new(r))
253 }
254 }
255 }
256 crate::method::OperationResponseKind::Raw
257 | crate::method::OperationResponseKind::Upgrade => {
258 quote! {
259 {
260 todo!()
261 }
262 }
263 }
264 };
265
266 quote! {
267 let result = request.send().await;
268
269 match result {
270 Ok(r) => #success_output
271 Err(r) => #error_output
272 }
273 }
274 }
275
276 Some(_) => {
278 let success_type = match success_kind {
279 crate::method::OperationResponseKind::Type(type_id) => {
280 self.type_space.get_type(&type_id).unwrap().ident()
281 }
282 crate::method::OperationResponseKind::None => quote! { () },
283 crate::method::OperationResponseKind::Raw => todo!(),
284 crate::method::OperationResponseKind::Upgrade => todo!(),
285 };
286 let error_output = match error_kind {
287 crate::method::OperationResponseKind::Type(_)
288 | crate::method::OperationResponseKind::None => {
289 quote! {
290 {
291 self.config.list_end_error(&r);
292 return Err(anyhow::Error::new(r))
293 }
294 }
295 }
296 crate::method::OperationResponseKind::Raw
297 | crate::method::OperationResponseKind::Upgrade => {
298 quote! {
299 {
300 todo!()
301 }
302 }
303 }
304 };
305 quote! {
306 self.config.list_start::<#success_type>();
307
308 let mut stream = futures::StreamExt::take(
313 request.stream(),
314 matches
315 .get_one::<std::num::NonZeroU32>("limit")
316 .map_or(usize::MAX, |x| x.get() as usize));
317
318 loop {
319 match futures::TryStreamExt::try_next(&mut stream).await {
320 Err(r) => #error_output
321 Ok(None) => {
322 self.config.list_end_success::<#success_type>();
323 return Ok(());
324 }
325 Ok(Some(value)) => {
326 self.config.list_item(&value);
327 }
328 }
329 }
330 }
331 }
332 };
333
334 let execute_fn = quote! {
335 pub async fn #fn_name(&self, matches: &::clap::ArgMatches)
336 -> anyhow::Result<()>
337 {
338 let mut request = self.client.#op_name();
339 #consumer_args
340
341 self.config.#fn_name(matches, &mut request)?;
343
344 #execute_and_output
345 }
346 };
347
348 let struct_name = sanitize(&method.operation_id, Case::Pascal);
350 let struct_ident = format_ident!("{}", struct_name);
351
352 let execute_trait = quote! {
353 fn #fn_name(
354 &self,
355 matches: &::clap::ArgMatches,
356 request: &mut builder :: #struct_ident,
357 ) -> anyhow::Result<()> {
358 Ok(())
359 }
360 };
361
362 CliOperation {
363 cli_fn,
364 execute_fn,
365 execute_trait,
366 }
367 }
368
369 fn cli_method_args(&self, method: &crate::method::OperationMethod) -> CliArg {
370 let mut args = CliOperationArgs::default();
371
372 let first_page_required_set = method
373 .dropshot_paginated
374 .as_ref()
375 .map(|d| &d.first_page_params);
376
377 for param in &method.params {
378 let innately_required = match ¶m.kind {
379 OperationParameterKind::Body(_) => continue,
381
382 OperationParameterKind::Path => true,
383 OperationParameterKind::Query(required) => *required,
384 OperationParameterKind::Header(required) => *required,
385 };
386
387 if method.dropshot_paginated.is_some() && param.name.as_str() == "page_token" {
389 continue;
390 }
391
392 let first_page_required = first_page_required_set
393 .map_or(false, |required| required.contains(¶m.api_name));
394
395 let volitionality = if innately_required || first_page_required {
396 Volitionality::Required
397 } else {
398 Volitionality::Optional
399 };
400
401 let OperationParameterType::Type(arg_type_id) = ¶m.typ else {
402 unreachable!("query and path parameters must be typed")
403 };
404 let arg_type = self.type_space.get_type(arg_type_id).unwrap();
405
406 let arg_name = param.name.to_kebab_case();
407
408 assert!(!args.has_arg(&arg_name));
410
411 let parser = clap_arg(&arg_name, volitionality, ¶m.description, &arg_type);
412
413 let arg_fn_name = sanitize(¶m.name, Case::Snake);
414 let arg_fn = format_ident!("{}", arg_fn_name);
415 let OperationParameterType::Type(arg_type_id) = ¶m.typ else {
416 panic!()
417 };
418 let arg_type = self.type_space.get_type(arg_type_id).unwrap();
419 let arg_type_name = arg_type.ident();
420
421 let consumer = quote! {
422 if let Some(value) =
423 matches.get_one::<#arg_type_name>(#arg_name)
424 {
425 request = request.#arg_fn(value.clone());
428 }
429 };
430
431 args.add_arg(arg_name, CliArg { parser, consumer })
432 }
433
434 let maybe_body_type_id = method
435 .params
436 .iter()
437 .find(|param| matches!(¶m.kind, OperationParameterKind::Body(_)))
438 .and_then(|param| match ¶m.typ {
439 OperationParameterType::RawBody => None,
443
444 OperationParameterType::Type(body_type_id) => Some(body_type_id),
445 });
446
447 if let Some(body_type_id) = maybe_body_type_id {
448 args.body_present();
449 let body_type = self.type_space.get_type(body_type_id).unwrap();
450 let details = body_type.details();
451
452 match details {
453 typify::TypeDetails::Struct(struct_info) => {
454 for prop_info in struct_info.properties_info() {
455 self.cli_method_body_arg(&mut args, prop_info)
456 }
457 }
458
459 _ => {
460 args.body_required()
463 }
464 }
465 }
466
467 let parser_args = args.args.values().map(|CliArg { parser, .. }| parser);
468
469 let body_json_args = (match args.body {
471 CliBodyArg::None => None,
472 CliBodyArg::Required => Some(true),
473 CliBodyArg::Optional => Some(false),
474 })
475 .map(|required| {
476 let help = "Path to a file that contains the full json body.";
477
478 quote! {
479 .arg(
480 ::clap::Arg::new("json-body")
481 .long("json-body")
482 .value_name("JSON-FILE")
483 .required(#required)
486 .value_parser(::clap::value_parser!(std::path::PathBuf))
487 .help(#help)
488 )
489 .arg(
490 ::clap::Arg::new("json-body-template")
491 .long("json-body-template")
492 .action(::clap::ArgAction::SetTrue)
493 .help("XXX")
494 )
495 }
496 });
497
498 let parser = quote! {
499 #(
500 .arg(#parser_args)
501 )*
502 #body_json_args
503 };
504
505 let consumer_args = args.args.values().map(|CliArg { consumer, .. }| consumer);
506
507 let body_json_consumer = maybe_body_type_id.map(|body_type_id| {
508 let body_type = self.type_space.get_type(body_type_id).unwrap();
509 let body_type_ident = body_type.ident();
510 quote! {
511 if let Some(value) =
512 matches.get_one::<std::path::PathBuf>("json-body")
513 {
514 let body_txt = std::fs::read_to_string(value).with_context(|| format!("failed to read {}", value.display()))?;
515 let body_value =
516 serde_json::from_str::<#body_type_ident>(
517 &body_txt,
518 )
519 .with_context(|| format!("failed to parse {}", value.display()))?;
520 request = request.body(body_value);
521 }
522 }
523 });
524
525 let consumer = quote! {
526 #(
527 #consumer_args
528 )*
529 #body_json_consumer
530 };
531
532 CliArg { parser, consumer }
533 }
534
535 fn cli_method_body_arg(&self, args: &mut CliOperationArgs, prop_info: TypeStructPropInfo<'_>) {
536 let TypeStructPropInfo {
537 name,
538 description,
539 required,
540 type_id,
541 } = prop_info;
542
543 let prop_type = self.type_space.get_type(&type_id).unwrap();
544
545 let maybe_inner_type =
554 if let typify::TypeDetails::Option(inner_type_id) = prop_type.details() {
555 let inner_type = self.type_space.get_type(&inner_type_id).unwrap();
556 Some(inner_type)
557 } else {
558 None
559 };
560
561 let prop_type = if let Some(inner_type) = maybe_inner_type {
562 inner_type
563 } else {
564 prop_type
565 };
566
567 let scalar = prop_type.has_impl(TypeSpaceImpl::FromStr);
568
569 let prop_name = name.to_kebab_case();
570 if scalar && !args.has_arg(&prop_name) {
571 let volitionality = if required {
572 Volitionality::RequiredIfNoBody
573 } else {
574 Volitionality::Optional
575 };
576 let parser = clap_arg(
577 &prop_name,
578 volitionality,
579 &description.map(str::to_string),
580 &prop_type,
581 );
582
583 let prop_fn = format_ident!("{}", sanitize(name, Case::Snake));
584 let prop_type_ident = prop_type.ident();
585 let consumer = quote! {
586 if let Some(value) =
587 matches.get_one::<#prop_type_ident>(
588 #prop_name,
589 )
590 {
591 request = request.body_map(|body| {
594 body.#prop_fn(value.clone())
595 })
596 }
597 };
598 args.add_arg(prop_name, CliArg { parser, consumer })
599 } else if required {
600 args.body_required()
601 }
602
603 }
614}
615
616enum Volitionality {
617 Optional,
618 Required,
619 RequiredIfNoBody,
620}
621
622fn clap_arg(
623 arg_name: &str,
624 volitionality: Volitionality,
625 description: &Option<String>,
626 arg_type: &Type,
627) -> TokenStream {
628 let help = description.as_ref().map(|description| {
629 quote! {
630 .help(#description)
631 }
632 });
633 let arg_type_name = arg_type.ident();
634
635 let maybe_enum_parser = if let typify::TypeDetails::Enum(e) = arg_type.details() {
641 let maybe_var_names = e
642 .variants()
643 .map(|(var_name, var_details)| {
644 if let TypeEnumVariant::Simple = var_details {
645 Some(format_ident!("{}", var_name))
646 } else {
647 None
648 }
649 })
650 .collect::<Option<Vec<_>>>();
651
652 maybe_var_names.map(|var_names| {
653 quote! {
654 ::clap::builder::TypedValueParser::map(
655 ::clap::builder::PossibleValuesParser::new([
656 #( #arg_type_name :: #var_names.to_string(), )*
657 ]),
658 |s| #arg_type_name :: try_from(s).unwrap()
659 )
660 }
661 })
662 } else {
663 None
664 };
665
666 let value_parser = if let Some(enum_parser) = maybe_enum_parser {
667 enum_parser
668 } else {
669 quote! {
673 ::clap::value_parser!(#arg_type_name)
674 }
675 };
676
677 let required = match volitionality {
678 Volitionality::Optional => quote! { .required(false) },
679 Volitionality::Required => quote! { .required(true) },
680 Volitionality::RequiredIfNoBody => {
681 quote! { .required_unless_present("json-body") }
682 }
683 };
684
685 quote! {
686 ::clap::Arg::new(#arg_name)
687 .long(#arg_name)
688 .value_parser(#value_parser)
689 #required
690 #help
691 }
692}
693
694#[derive(Debug)]
695struct CliArg {
696 parser: TokenStream,
698
699 consumer: TokenStream,
701}
702
703#[derive(Debug, Default, PartialEq, Eq)]
704enum CliBodyArg {
705 #[default]
706 None,
707 Required,
708 Optional,
709}
710
711#[derive(Default, Debug)]
712struct CliOperationArgs {
713 args: BTreeMap<String, CliArg>,
714 body: CliBodyArg,
715}
716
717impl CliOperationArgs {
718 fn has_arg(&self, name: &String) -> bool {
719 self.args.contains_key(name)
720 }
721 fn add_arg(&mut self, name: String, arg: CliArg) {
722 self.args.insert(name, arg);
723 }
724
725 fn body_present(&mut self) {
726 assert_eq!(self.body, CliBodyArg::None);
727 self.body = CliBodyArg::Optional;
728 }
729
730 fn body_required(&mut self) {
731 assert!(self.body == CliBodyArg::Optional || self.body == CliBodyArg::Required);
732 self.body = CliBodyArg::Required;
733 }
734}