1#![deny(missing_docs)]
6
7use std::collections::{BTreeMap, HashMap, HashSet};
8
9use openapiv3::OpenAPI;
10use proc_macro2::TokenStream;
11use quote::quote;
12use serde::Deserialize;
13use thiserror::Error;
14use typify::{TypeSpace, TypeSpaceSettings};
15
16use crate::to_schema::ToSchema;
17
18pub use typify::CrateVers;
19pub use typify::TypeSpaceImpl as TypeImpl;
20pub use typify::TypeSpacePatch as TypePatch;
21pub use typify::UnknownPolicy;
22
23mod cli;
24mod httpmock;
25mod method;
26mod template;
27mod to_schema;
28mod util;
29
30#[allow(missing_docs)]
31#[derive(Error, Debug)]
32pub enum Error {
33 #[error("unexpected value type {0}: {1}")]
34 BadValue(String, serde_json::Value),
35 #[error("type error {0}")]
36 TypeError(#[from] typify::Error),
37 #[error("unexpected or unhandled format in the OpenAPI document {0}")]
38 UnexpectedFormat(String),
39 #[error("invalid operation path {0}")]
40 InvalidPath(String),
41 #[error("invalid dropshot extension use: {0}")]
42 InvalidExtension(String),
43 #[error("internal error {0}")]
44 InternalError(String),
45}
46
47#[allow(missing_docs)]
48pub type Result<T> = std::result::Result<T, Error>;
49
50pub struct Generator {
52 type_space: TypeSpace,
53 settings: GenerationSettings,
54 uses_futures: bool,
55 uses_websockets: bool,
56}
57
58#[derive(Default, Clone)]
60pub struct GenerationSettings {
61 interface: InterfaceStyle,
62 tag: TagStyle,
63 inner_type: Option<TokenStream>,
64 pre_hook: Option<TokenStream>,
65 pre_hook_async: Option<TokenStream>,
66 post_hook: Option<TokenStream>,
67 post_hook_async: Option<TokenStream>,
68 extra_derives: Vec<String>,
69
70 map_type: Option<String>,
71 unknown_crates: UnknownPolicy,
72 crates: BTreeMap<String, CrateSpec>,
73
74 patch: HashMap<String, TypePatch>,
75 replace: HashMap<String, (String, Vec<TypeImpl>)>,
76 convert: Vec<(schemars::schema::SchemaObject, String, Vec<TypeImpl>)>,
77}
78
79#[derive(Debug, Clone)]
80struct CrateSpec {
81 version: CrateVers,
82 rename: Option<String>,
83}
84
85#[derive(Clone, Deserialize, PartialEq, Eq)]
87pub enum InterfaceStyle {
88 Positional,
90 Builder,
92}
93
94impl Default for InterfaceStyle {
95 fn default() -> Self {
96 Self::Positional
97 }
98}
99
100#[derive(Clone, Deserialize)]
102pub enum TagStyle {
103 Merged,
105 Separate,
107}
108
109impl Default for TagStyle {
110 fn default() -> Self {
111 Self::Merged
112 }
113}
114
115impl GenerationSettings {
116 pub fn new() -> Self {
118 Self::default()
119 }
120
121 pub fn with_interface(&mut self, interface: InterfaceStyle) -> &mut Self {
123 self.interface = interface;
124 self
125 }
126
127 pub fn with_tag(&mut self, tag: TagStyle) -> &mut Self {
129 self.tag = tag;
130 self
131 }
132
133 pub fn with_inner_type(&mut self, inner_type: TokenStream) -> &mut Self {
135 self.inner_type = Some(inner_type);
136 self
137 }
138
139 pub fn with_pre_hook(&mut self, pre_hook: TokenStream) -> &mut Self {
141 self.pre_hook = Some(pre_hook);
142 self
143 }
144
145 pub fn with_pre_hook_async(&mut self, pre_hook: TokenStream) -> &mut Self {
147 self.pre_hook_async = Some(pre_hook);
148 self
149 }
150
151 pub fn with_post_hook(&mut self, post_hook: TokenStream) -> &mut Self {
153 self.post_hook = Some(post_hook);
154 self
155 }
156
157 pub fn with_post_hook_async(&mut self, post_hook: TokenStream) -> &mut Self {
159 self.post_hook_async = Some(post_hook);
160 self
161 }
162
163 pub fn with_derive(&mut self, derive: impl ToString) -> &mut Self {
165 self.extra_derives.push(derive.to_string());
166 self
167 }
168
169 pub fn with_patch<S: AsRef<str>>(&mut self, type_name: S, patch: &TypePatch) -> &mut Self {
172 self.patch
173 .insert(type_name.as_ref().to_string(), patch.clone());
174 self
175 }
176
177 pub fn with_replacement<TS: ToString, RS: ToString, I: Iterator<Item = TypeImpl>>(
180 &mut self,
181 type_name: TS,
182 replace_name: RS,
183 impls: I,
184 ) -> &mut Self {
185 self.replace.insert(
186 type_name.to_string(),
187 (replace_name.to_string(), impls.collect()),
188 );
189 self
190 }
191
192 pub fn with_conversion<S: ToString, I: Iterator<Item = TypeImpl>>(
195 &mut self,
196 schema: schemars::schema::SchemaObject,
197 type_name: S,
198 impls: I,
199 ) -> &mut Self {
200 self.convert
201 .push((schema, type_name.to_string(), impls.collect()));
202 self
203 }
204
205 pub fn with_unknown_crates(&mut self, policy: UnknownPolicy) -> &mut Self {
209 self.unknown_crates = policy;
210 self
211 }
212
213 pub fn with_crate<S1: ToString>(
218 &mut self,
219 crate_name: S1,
220 version: CrateVers,
221 rename: Option<&String>,
222 ) -> &mut Self {
223 self.crates.insert(
224 crate_name.to_string(),
225 CrateSpec {
226 version,
227 rename: rename.cloned(),
228 },
229 );
230 self
231 }
232
233 pub fn with_map_type<MT: ToString>(&mut self, map_type: MT) -> &mut Self {
241 self.map_type = Some(map_type.to_string());
242 self
243 }
244}
245
246impl Default for Generator {
247 fn default() -> Self {
248 Self {
249 type_space: TypeSpace::new(TypeSpaceSettings::default().with_type_mod("types")),
250 settings: Default::default(),
251 uses_futures: Default::default(),
252 uses_websockets: Default::default(),
253 }
254 }
255}
256
257impl Generator {
258 pub fn new(settings: &GenerationSettings) -> Self {
260 let mut type_settings = TypeSpaceSettings::default();
261 type_settings
262 .with_type_mod("types")
263 .with_struct_builder(settings.interface == InterfaceStyle::Builder);
264 settings.extra_derives.iter().for_each(|derive| {
265 let _ = type_settings.with_derive(derive.clone());
266 });
267
268 type_settings.with_unknown_crates(settings.unknown_crates);
270 settings
271 .crates
272 .iter()
273 .for_each(|(crate_name, CrateSpec { version, rename })| {
274 type_settings.with_crate(crate_name, version.clone(), rename.as_ref());
275 });
276
277 settings.patch.iter().for_each(|(type_name, patch)| {
279 type_settings.with_patch(type_name, patch);
280 });
281 settings
282 .replace
283 .iter()
284 .for_each(|(type_name, (replace_name, impls))| {
285 type_settings.with_replacement(type_name, replace_name, impls.iter().cloned());
286 });
287 settings
288 .convert
289 .iter()
290 .for_each(|(schema, type_name, impls)| {
291 type_settings.with_conversion(schema.clone(), type_name, impls.iter().cloned());
292 });
293
294 if let Some(map_type) = &settings.map_type {
296 type_settings.with_map_type(map_type.clone());
297 }
298
299 Self {
300 type_space: TypeSpace::new(&type_settings),
301 settings: settings.clone(),
302 uses_futures: false,
303 uses_websockets: false,
304 }
305 }
306
307 pub fn generate_tokens(&mut self, spec: &OpenAPI) -> Result<TokenStream> {
309 validate_openapi(spec)?;
310
311 let schemas = spec.components.iter().flat_map(|components| {
313 components
314 .schemas
315 .iter()
316 .map(|(name, ref_or_schema)| (name.clone(), ref_or_schema.to_schema()))
317 });
318
319 self.type_space.add_ref_types(schemas)?;
320
321 let raw_methods = spec
322 .paths
323 .iter()
324 .flat_map(|(path, ref_or_item)| {
325 let item = ref_or_item.as_item().unwrap();
327 item.iter().map(move |(method, operation)| {
328 (path.as_str(), method, operation, &item.parameters)
329 })
330 })
331 .map(|(path, method, operation, path_parameters)| {
332 self.process_operation(operation, &spec.components, path, method, path_parameters)
333 })
334 .collect::<Result<Vec<_>>>()?;
335
336 let operation_code = match (&self.settings.interface, &self.settings.tag) {
337 (InterfaceStyle::Positional, TagStyle::Merged) => self
338 .generate_tokens_positional_merged(
339 &raw_methods,
340 self.settings.inner_type.is_some(),
341 ),
342 (InterfaceStyle::Positional, TagStyle::Separate) => {
343 unimplemented!("positional arguments with separate tags are currently unsupported")
344 }
345 (InterfaceStyle::Builder, TagStyle::Merged) => self
346 .generate_tokens_builder_merged(&raw_methods, self.settings.inner_type.is_some()),
347 (InterfaceStyle::Builder, TagStyle::Separate) => {
348 let tag_info = spec
349 .tags
350 .iter()
351 .map(|tag| (&tag.name, tag))
352 .collect::<BTreeMap<_, _>>();
353 self.generate_tokens_builder_separate(
354 &raw_methods,
355 tag_info,
356 self.settings.inner_type.is_some(),
357 )
358 }
359 }?;
360
361 let error_enums = self.generate_error_enums(&raw_methods);
363
364 let types = self.type_space.to_stream();
365
366 let (inner_type, inner_fn_value) = match self.settings.inner_type.as_ref() {
367 Some(inner_type) => (inner_type.clone(), quote! { &self.inner }),
368 None => (quote! { () }, quote! { &() }),
369 };
370
371 let inner_property = self.settings.inner_type.as_ref().map(|inner| {
372 quote! {
373 pub (crate) inner: #inner,
374 }
375 });
376 let inner_parameter = self.settings.inner_type.as_ref().map(|inner| {
377 quote! {
378 inner: #inner,
379 }
380 });
381 let inner_value = self.settings.inner_type.as_ref().map(|_| {
382 quote! {
383 inner
384 }
385 });
386
387 let client_docstring = {
388 let mut s = format!("Client for {}", spec.info.title);
389
390 if let Some(ss) = &spec.info.description {
391 s.push_str("\n\n");
392 s.push_str(ss);
393 }
394 if let Some(ss) = &spec.info.terms_of_service {
395 s.push_str("\n\n");
396 s.push_str(ss);
397 }
398
399 s.push_str(&format!("\n\nVersion: {}", &spec.info.version));
400
401 s
402 };
403
404 let version_str = &spec.info.version;
405
406 let file = quote! {
411 #[allow(unused_imports)]
413 pub use progenitor_middleware_client::{
414 ByteStream,
415 ClientInfo,
416 Error,
417 ResponseValue,
418 };
419 #[allow(unused_imports)]
420 use progenitor_middleware_client::{
421 encode_path,
422 ClientHooks,
423 OperationInfo,
424 RequestBuilderExt,
425 };
426
427 #[allow(clippy::all)]
429 pub mod types {
430 #[allow(unused_imports)]
431 use super::{ByteStream, ResponseValue};
432
433 #types
434
435 #(#error_enums)*
437 }
438
439 #[derive(Clone, Debug)]
440 #[doc = #client_docstring]
441 pub struct Client {
442 pub(crate) baseurl: String,
443 pub(crate) client: reqwest_middleware::ClientWithMiddleware,
444 #inner_property
445 }
446
447 impl Client {
448 pub fn new(
454 baseurl: &str,
455 #inner_parameter
456 ) -> Self {
457 #[cfg(not(target_arch = "wasm32"))]
458 let client = {
459 let dur = std::time::Duration::from_secs(15);
460
461 let reqwest_client = reqwest::ClientBuilder::new()
462 .connect_timeout(dur)
463 .timeout(dur)
464 .build()
465 .unwrap();
466
467 reqwest_middleware::ClientBuilder::new(reqwest_client)
468 .build()
469 };
470 #[cfg(target_arch = "wasm32")]
471 let client = {
472 let reqwest_client = reqwest::ClientBuilder::new()
473 .build()
474 .unwrap();
475
476 reqwest_middleware::ClientBuilder::new(reqwest_client)
477 .build()
478 };
479
480 Self::new_with_client(baseurl, client, #inner_value)
481 }
482
483 pub fn new_with_client(
490 baseurl: &str,
491 client: reqwest_middleware::ClientWithMiddleware,
492 #inner_parameter
493 ) -> Self {
494 Self {
495 baseurl: baseurl.to_string(),
496 client,
497 #inner_value
498 }
499 }
500 }
501
502 impl ClientInfo<#inner_type> for Client {
503 fn api_version() -> &'static str {
504 #version_str
505 }
506
507 fn baseurl(&self) -> &str {
508 self.baseurl.as_str()
509 }
510
511 fn client(&self) -> &reqwest_middleware::ClientWithMiddleware {
512 &self.client
513 }
514
515 fn inner(&self) -> &#inner_type {
516 #inner_fn_value
517 }
518 }
519
520 impl ClientHooks<#inner_type> for &Client {}
521
522 #operation_code
523 };
524
525 Ok(file)
526 }
527
528 fn generate_tokens_positional_merged(
529 &mut self,
530 input_methods: &[method::OperationMethod],
531 has_inner: bool,
532 ) -> Result<TokenStream> {
533 let methods = input_methods
534 .iter()
535 .map(|method| self.positional_method(method, has_inner))
536 .collect::<Result<Vec<_>>>()?;
537
538 let out = quote! {
543 #[allow(clippy::all)]
544 impl Client {
545 #(#methods)*
546 }
547
548 pub mod prelude {
550 #[allow(unused_imports)]
551 pub use super::Client;
552 }
553 };
554 Ok(out)
555 }
556
557 fn generate_error_enums(&self, methods: &[method::OperationMethod]) -> Vec<TokenStream> {
559 methods
560 .iter()
561 .filter_map(|method| {
562 let (_, error_response_type) = self.extract_responses(
564 method,
565 method::OperationResponseStatus::is_error_or_default,
566 );
567
568 match error_response_type {
569 method::ErrorResponseType::Multiple {
570 enum_name,
571 variants,
572 } => Some(self.generate_error_enum(&enum_name, &variants)),
573 method::ErrorResponseType::Single(_) => None,
574 }
575 })
576 .collect()
577 }
578
579 fn generate_tokens_builder_merged(
580 &mut self,
581 input_methods: &[method::OperationMethod],
582 has_inner: bool,
583 ) -> Result<TokenStream> {
584 let builder_struct = input_methods
585 .iter()
586 .map(|method| self.builder_struct(method, TagStyle::Merged, has_inner))
587 .collect::<Result<Vec<_>>>()?;
588
589 let builder_methods = input_methods
590 .iter()
591 .map(|method| self.builder_impl(method))
592 .collect::<Vec<_>>();
593
594 let out = quote! {
595 impl Client {
596 #(#builder_methods)*
597 }
598
599 #[allow(clippy::all)]
601 pub mod builder {
602 use super::types;
603 #[allow(unused_imports)]
604 use super::{
605 encode_path,
606 ByteStream,
607 ClientInfo,
608 ClientHooks,
609 Error,
610 OperationInfo,
611 RequestBuilderExt,
612 ResponseValue,
613 };
614
615 #(#builder_struct)*
616 }
617
618 pub mod prelude {
620 pub use self::super::Client;
621 }
622 };
623
624 Ok(out)
625 }
626
627 fn generate_tokens_builder_separate(
628 &mut self,
629 input_methods: &[method::OperationMethod],
630 tag_info: BTreeMap<&String, &openapiv3::Tag>,
631 has_inner: bool,
632 ) -> Result<TokenStream> {
633 let builder_struct = input_methods
634 .iter()
635 .map(|method| self.builder_struct(method, TagStyle::Separate, has_inner))
636 .collect::<Result<Vec<_>>>()?;
637
638 let (traits_and_impls, trait_preludes) = self.builder_tags(input_methods, &tag_info);
639
640 let out = quote! {
645 #traits_and_impls
646
647 #[allow(clippy::all)]
649 pub mod builder {
650 use super::types;
651 #[allow(unused_imports)]
652 use super::{
653 encode_path,
654 ByteStream,
655 ClientInfo,
656 ClientHooks,
657 Error,
658 OperationInfo,
659 RequestBuilderExt,
660 ResponseValue,
661 };
662
663 #(#builder_struct)*
664 }
665
666 pub mod prelude {
669 #[allow(unused_imports)]
670 pub use super::Client;
671 #trait_preludes
672 }
673 };
674
675 Ok(out)
676 }
677
678 pub fn get_type_space(&self) -> &TypeSpace {
680 &self.type_space
681 }
682
683 pub fn uses_futures(&self) -> bool {
686 self.uses_futures
687 }
688
689 pub fn uses_websockets(&self) -> bool {
692 self.uses_websockets
693 }
694}
695
696pub fn space_out_items(content: String) -> Result<String> {
698 Ok(if cfg!(not(windows)) {
699 let regex = regex::Regex::new(r#"(\n\s*})(\n\s{0,8}[^} ])"#).unwrap();
700 regex.replace_all(&content, "$1\n$2").to_string()
701 } else {
702 let regex = regex::Regex::new(r#"(\n\s*})(\r\n\s{0,8}[^} ])"#).unwrap();
703 regex.replace_all(&content, "$1\r\n$2").to_string()
704 })
705}
706
707fn validate_openapi_spec_version(spec_version: &str) -> Result<()> {
708 if spec_version.trim().starts_with("3.") {
710 Ok(())
711 } else {
712 Err(Error::UnexpectedFormat(format!(
713 "invalid version: {}",
714 spec_version
715 )))
716 }
717}
718
719pub fn validate_openapi(spec: &OpenAPI) -> Result<()> {
721 validate_openapi_spec_version(spec.openapi.as_str())?;
722
723 let mut opids = HashSet::new();
724 spec.paths.paths.iter().try_for_each(|p| {
725 match p.1 {
726 openapiv3::ReferenceOr::Reference { reference: _ } => Err(Error::UnexpectedFormat(
727 format!("path {} uses reference, unsupported", p.0,),
728 )),
729 openapiv3::ReferenceOr::Item(item) => {
730 item.iter().try_for_each(|(_, o)| {
733 if let Some(oid) = o.operation_id.as_ref() {
734 if !opids.insert(oid.to_string()) {
735 return Err(Error::UnexpectedFormat(format!(
736 "duplicate operation ID: {}",
737 oid,
738 )));
739 }
740 } else {
741 return Err(Error::UnexpectedFormat(format!(
742 "path {} is missing operation ID",
743 p.0,
744 )));
745 }
746 Ok(())
747 })
748 }
749 }
750 })?;
751
752 Ok(())
753}
754
755#[cfg(test)]
756mod tests {
757 use serde_json::json;
758
759 use crate::{validate_openapi_spec_version, Error};
760
761 #[test]
762 fn test_bad_value() {
763 assert_eq!(
764 Error::BadValue("nope".to_string(), json! { "nope"},).to_string(),
765 "unexpected value type nope: \"nope\"",
766 );
767 }
768
769 #[test]
770 fn test_type_error() {
771 assert_eq!(
772 Error::UnexpectedFormat("nope".to_string()).to_string(),
773 "unexpected or unhandled format in the OpenAPI document nope",
774 );
775 }
776
777 #[test]
778 fn test_invalid_path() {
779 assert_eq!(
780 Error::InvalidPath("nope".to_string()).to_string(),
781 "invalid operation path nope",
782 );
783 }
784
785 #[test]
786 fn test_internal_error() {
787 assert_eq!(
788 Error::InternalError("nope".to_string()).to_string(),
789 "internal error nope",
790 );
791 }
792
793 #[test]
794 fn test_validate_openapi_spec_version() {
795 assert!(validate_openapi_spec_version("3.0.0").is_ok());
796 assert!(validate_openapi_spec_version("3.0.1").is_ok());
797 assert!(validate_openapi_spec_version("3.0.4").is_ok());
798 assert!(validate_openapi_spec_version("3.0.5-draft").is_ok());
799 assert_eq!(
800 validate_openapi_spec_version("3.1.0")
801 .unwrap_err()
802 .to_string(),
803 "unexpected or unhandled format in the OpenAPI document invalid version: 3.1.0"
804 );
805 }
806}