From af28d7184317318823e4f0efee13f84b74bce61d Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 11 Apr 2023 16:13:58 +0200 Subject: [PATCH 01/23] first take at form data --- Cargo.lock | 42 ++++- README.md | 6 +- example-build/Cargo.toml | 2 +- example-macro/Cargo.toml | 2 +- progenitor-client/Cargo.toml | 2 +- progenitor-client/src/progenitor_client.rs | 47 ++++- progenitor-impl/Cargo.toml | 1 + progenitor-impl/src/method.rs | 203 ++++++++++++++++++--- progenitor/Cargo.toml | 2 +- progenitor/src/main.rs | 2 +- 10 files changed, 272 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bebbf9a90..2626ebdc9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -383,6 +383,12 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" +[[package]] +name = "either" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" + [[package]] name = "encode_unicode" version = "0.3.6" @@ -850,6 +856,15 @@ dependencies = [ "windows-sys 0.42.0", ] +[[package]] +name = "itertools" +version = "0.10.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.1" @@ -958,6 +973,16 @@ version = "0.3.16" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "mio" version = "0.8.4" @@ -1224,6 +1249,7 @@ dependencies = [ "http", "hyper", "indexmap", + "itertools", "openapiv3", "proc-macro2", "quote", @@ -1375,6 +1401,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -2103,7 +2130,7 @@ checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" [[package]] name = "typify" version = "0.0.12-dev" -source = "git+https://github.com/oxidecomputer/typify#6d77f63b3dc5312cd36549507f941cb5d783600e" +source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4" dependencies = [ "typify-impl", "typify-macro", @@ -2112,7 +2139,7 @@ dependencies = [ [[package]] name = "typify-impl" version = "0.0.12-dev" -source = "git+https://github.com/oxidecomputer/typify#6d77f63b3dc5312cd36549507f941cb5d783600e" +source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4" dependencies = [ "heck", "log", @@ -2129,7 +2156,7 @@ dependencies = [ [[package]] name = "typify-macro" version = "0.0.12-dev" -source = "git+https://github.com/oxidecomputer/typify#6d77f63b3dc5312cd36549507f941cb5d783600e" +source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4" dependencies = [ "proc-macro2", "quote", @@ -2147,6 +2174,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" +[[package]] +name = "unicase" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.7" diff --git a/README.md b/README.md index 4c4295dc1..b8776ae0d 100644 --- a/README.md +++ b/README.md @@ -36,7 +36,7 @@ You'll need to add the following to `Cargo.toml`: [dependencies] +futures = "0.3" +progenitor = { git = "https://github.com/oxidecomputer/progenitor" } -+reqwest = { version = "0.11", features = ["json", "stream"] } ++reqwest = { version = "0.11", features = ["json", "stream", "multipart"] } +serde = { version = "1.0", features = ["derive"] } ``` @@ -123,7 +123,7 @@ You'll need to add the following to `Cargo.toml`: [dependencies] +futures = "0.3" +progenitor-client = { git = "https://github.com/oxidecomputer/progenitor" } -+reqwest = { version = "0.11", features = ["json", "stream"] } ++reqwest = { version = "0.11", features = ["json", "stream", "multipart"] } +serde = { version = "1.0", features = ["derive"] } [build-dependencies] @@ -180,7 +180,7 @@ bytes = "1.3.0" chrono = { version = "0.4.23", default-features=false, features = ["serde"] } futures-core = "0.3.25" percent-encoding = "2.2.0" -reqwest = { version = "0.11.13", default-features=false, features = ["json", "stream"] } +reqwest = { version = "0.11.13", default-features=false, features = ["json", "stream", "multipart"] } serde = { version = "1.0.152", features = ["derive"] } serde_urlencoded = "0.7.1" diff --git a/example-build/Cargo.toml b/example-build/Cargo.toml index d1eaac935..044368f0e 100644 --- a/example-build/Cargo.toml +++ b/example-build/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4", features = ["serde"] } progenitor-client = { path = "../progenitor-client" } -reqwest = { version = "0.11.16", features = ["json", "stream"] } +reqwest = { version = "0.11.16", features = ["json", "stream", "multipart"] } base64 = "0.21" rand = "0.8" serde = { version = "1.0", features = ["derive"] } diff --git a/example-macro/Cargo.toml b/example-macro/Cargo.toml index 6648fc2f5..1b862e1eb 100644 --- a/example-macro/Cargo.toml +++ b/example-macro/Cargo.toml @@ -7,7 +7,7 @@ edition = "2021" [dependencies] chrono = { version = "0.4", features = ["serde"] } progenitor = { path = "../progenitor" } -reqwest = { version = "0.11.16", features = ["json", "stream"] } +reqwest = { version = "0.11.16", features = ["json", "stream", "multipart"] } schemars = { version = "0.8.12", features = ["uuid1"] } serde = { version = "1.0", features = ["derive"] } uuid = { version = "1.3", features = ["serde", "v4"] } diff --git a/progenitor-client/Cargo.toml b/progenitor-client/Cargo.toml index c06d614da..50ec99767 100644 --- a/progenitor-client/Cargo.toml +++ b/progenitor-client/Cargo.toml @@ -10,7 +10,7 @@ description = "An OpenAPI client generator - client support" bytes = "1.4.0" futures-core = "0.3.27" percent-encoding = "2.2" -reqwest = { version = "0.11.16", default-features = false, features = ["json", "stream"] } +reqwest = { version = "0.11.16", default-features = false, features = ["json", "stream", "multipart"] } serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7.1" diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index f2951f9d1..ef5212f5d 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -386,11 +386,23 @@ pub fn encode_path(pc: &str) -> String { } #[doc(hidden)] -pub trait RequestBuilderExt { +pub trait RequestBuilderExt +where + Self: Sized, +{ fn form_urlencoded( self, body: &T, ) -> Result>; + + fn form_from_raw< + S: AsRef, + T: AsRef<[u8]>, + I: Sized + IntoIterator, + >( + self, + iter: I, + ) -> Result>; } impl RequestBuilderExt for RequestBuilder { @@ -405,8 +417,37 @@ impl RequestBuilderExt for RequestBuilder { "application/x-www-form-urlencoded", ), ) - .body(serde_urlencoded::to_string(body).map_err(|_| { - Error::InvalidRequest("failed to serialize body".to_string()) + .body(serde_urlencoded::to_string(body).map_err(|e| { + Error::InvalidRequest(format!( + "failed to serialize body: {e:?}" + )) })?)) } + + fn form_from_raw< + S: AsRef, + T: AsRef<[u8]>, + I: Sized + IntoIterator, + >( + self, + mut iter: I, + ) -> Result> { + use reqwest::multipart::{Form, Part}; + + let mut form = Form::new(); + for (name, value) in iter { + form = form.part( + name.as_ref().to_owned(), + Part::stream(Vec::from(value.as_ref())), + ); + } + Ok(self + .header( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static( + "multipart/form-data", + ), + ) + .multipart(form)) + } } diff --git a/progenitor-impl/Cargo.toml b/progenitor-impl/Cargo.toml index d6fabeb60..94e9c24e5 100644 --- a/progenitor-impl/Cargo.toml +++ b/progenitor-impl/Cargo.toml @@ -11,6 +11,7 @@ readme = "../README.md" heck = "0.4.1" getopts = "0.2" indexmap = "1.9" +itertools = "0.10" openapiv3 = "1.0.0" proc-macro2 = "1.0" quote = "1.0" diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 9ab4dc916..1ebbd9b76 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -2,10 +2,11 @@ use std::{ cmp::Ordering, - collections::{BTreeMap, BTreeSet}, + collections::{BTreeMap, BTreeSet, HashMap}, str::FromStr, }; +use indexmap::{IndexMap, IndexSet}; use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -101,9 +102,10 @@ pub struct OperationParameter { pub kind: OperationParameterKind, } -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum OperationParameterType { Type(TypeId), + Form(IndexSet), RawBody, } @@ -120,6 +122,7 @@ pub enum BodyContentType { OctetStream, Json, FormUrlencoded, + FormData, } impl FromStr for BodyContentType { @@ -130,6 +133,7 @@ impl FromStr for BodyContentType { "application/octet-stream" => Ok(Self::OctetStream), "application/json" => Ok(Self::Json), "application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded), + "form-data" | "multipart/form-data" => Ok(Self::FormData), _ => Err(Error::UnexpectedFormat(format!( "unexpected content type: {}", s @@ -138,6 +142,19 @@ impl FromStr for BodyContentType { } } +use std::fmt; + +impl fmt::Display for BodyContentType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::OctetStream => "application/octet-stream", + Self::Json => "application/json", + Self::FormUrlencoded => "application/x-www-form-urlencoded", + Self::FormData => "multipart/form-data", + }) + } +} + #[derive(Debug)] pub(crate) struct OperationResponse { status_code: OperationResponseStatus, @@ -328,7 +345,7 @@ impl Generator { let typ = self .type_space - .add_type_with_name(&schema, Some(name))?; + .add_type_with_name(&schema, Some(dbg!(name)))?; query.push(( parameter_data.name.clone(), @@ -567,18 +584,31 @@ impl Generator { .iter() .map(|param| { let name = format_ident!("{}", param.name); - let typ = match ¶m.typ { - OperationParameterType::Type(type_id) => self - .type_space - .get_type(type_id) - .unwrap() - .parameter_ident_with_lifetime("a"), + match ¶m.typ { + OperationParameterType::Type(type_id) => { + let typ = self + .type_space + .get_type(type_id) + .expect("TypeIDs are _never_ deleted. qed") + .parameter_ident_with_lifetime("a"); + quote! { #name: #typ} + } + OperationParameterType::Form(keys) => { + let ts = TokenStream::from_iter( + itertools::Itertools::intersperse( + keys.iter().map(|form_prop_name| { + let form_prop_name = + format_ident!("{}", form_prop_name); + quote! { #form_prop_name: Vec } + }), + quote! {, }, + ), + ); + ts + } OperationParameterType::RawBody => { - quote! { B } + quote! { #name: B } } - }; - quote! { - #name: #typ } }) .collect::>(); @@ -857,16 +887,15 @@ impl Generator { // Generate the path rename map; then use it to generate code for // assigning the path parameters to the `url` variable. - let url_renames = method - .params - .iter() - .filter_map(|param| match ¶m.kind { - OperationParameterKind::Path => { - Some((¶m.api_name, ¶m.name)) + let url_renames = + HashMap::from_iter(method.params.iter().filter_map(|param| { + match ¶m.kind { + OperationParameterKind::Path => { + Some((¶m.api_name, ¶m.name)) + } + _ => None, } - _ => None, - }) - .collect(); + })); let url_path = method.path.compile(url_renames, client.clone()); @@ -902,6 +931,20 @@ impl Generator { // returns an error in the case of a serialization failure. .form_urlencoded(&body)? }), + ( + OperationParameterKind::Body( + BodyContentType::FormData + ), + OperationParameterType::Form(map), + ) => { + let form_prop_names = map.iter().cloned().map(|form_prop_name| { + let ident= format_ident!("{}", form_prop_name); + quote! { (#form_prop_name, #ident) } + }); + Some(quote! { + // This uses progenitor_client::RequestBuilderExt which sets up a simple form data based on bytes + .form_from_raw(vec![ #(#form_prop_names),* ])? + })}, (OperationParameterKind::Body(_), _) => { unreachable!("invalid body kind/type combination") } @@ -1382,6 +1425,10 @@ impl Generator { } } + OperationParameterType::Form(_form) => { + todo!("Form is nit expected here") + } + OperationParameterType::RawBody => { cloneable = false; Ok(quote! { Result }) @@ -1413,6 +1460,9 @@ impl Generator { Ok(quote! { Err(#err_msg.to_string()) }) } } + OperationParameterType::Form(_form) => { + todo!("Form is nit expected here") + } OperationParameterType::RawBody => { let err_msg = format!("{} was not initialized", param.name); Ok(quote! { Err(#err_msg.to_string()) }) @@ -1437,6 +1487,15 @@ impl Generator { Ok(quote! {}) } } + + OperationParameterType::Form(_form) => { + todo!("Form is nit expected here") + } + + OperationParameterType::Form(_form) => { + todo!("Form is nit expected here") + } + OperationParameterType::RawBody => Ok(quote! {}), }) .collect::>>()?; @@ -1537,6 +1596,23 @@ impl Generator { } } + OperationParameterType::Form(form_keys) => { + let err_msg = format!( + "conversion to `reqwest::Body` for {} failed", + param.name, + ); + + Ok(quote! { + pub fn #param_name(mut self, value: B) -> Self + where B: std::convert::TryInto + { + self.#param_name = value.try_into() + .map_err(|_| #err_msg.to_string()); + self + } + }) + } + OperationParameterType::RawBody => { let err_msg = format!( "conversion to `reqwest::Body` for {} failed", @@ -2000,12 +2076,93 @@ impl Generator { )), } if enumeration.is_empty() => Ok(()), _ => Err(Error::UnexpectedFormat(format!( - "invalid schema for application/octet-stream: {:?}", + "invalid schema for {}: {:?}", + BodyContentType::OctetStream, schema ))), }?; OperationParameterType::RawBody } + BodyContentType::FormData => { + // For form data, we expect a key-value set of types, specific schema: + + // ```yaml + // type: "object" + // properties: + // file: + // description: "The file to upload" + // type: "string" + // format: "binary" + // ``` + // "schema": { + // "type": "string", + // "format": "binary" + // } + + let mapped = match schema.item(components)? { + openapiv3::Schema { + schema_data: + openapiv3::SchemaData { + nullable: false, + discriminator: None, + default: None, + // Other fields that describe or document the + // schema are fine. + .. + }, + schema_kind: + openapiv3::SchemaKind::Type(openapiv3::Type::Object( + openapiv3::ObjectType { + properties, + additional_properties, + .. + }, + )), + } => { + let mapped = Result::>::from_iter( + properties + .into_iter() + .map(|(name, property)| { + // properties must be plain key value types for now + let ReferenceOr::Item(property) = property else { + return Err(Error::UnexpectedFormat(format!( + "invalid schema for {}: didn't expect a reference", + BodyContentType::FormData, + )))}; + match &property.schema_kind { + openapiv3::SchemaKind::Type(openapiv3::Type::String( + openapiv3::StringType { + format: + openapiv3::VariantOrUnknownOrEmpty::Item( + openapiv3::StringFormat::Binary, + ), + pattern: None, + enumeration, + min_length: None, + max_length: None, + }, + )) if enumeration.is_empty() => { + Ok((name.to_owned())) + } + schema => { + Err(Error::UnexpectedFormat(format!( + "invalid schema for {}: {:?}", + BodyContentType::FormData, + schema + ))) + } + } + }))?; + Ok(mapped) + } + _ => Err(Error::UnexpectedFormat(format!( + "invalid schema for {}: {:?}", + BodyContentType::FormData, + schema + ))), + }?; + OperationParameterType::Form(mapped) + } BodyContentType::Json | BodyContentType::FormUrlencoded => { // TODO it would be legal to have the encoding field set for // application/x-www-form-urlencoded content, but I'm not sure diff --git a/progenitor/Cargo.toml b/progenitor/Cargo.toml index 078149932..6fbbbe983 100644 --- a/progenitor/Cargo.toml +++ b/progenitor/Cargo.toml @@ -32,6 +32,6 @@ futures = "0.3.27" percent-encoding = "2.2" rand = "0.8" regress = "0.5.0" -reqwest = { version = "0.11.16", features = ["json", "stream"] } +reqwest = { version = "0.11.16", features = ["json", "stream", "multipart"] } schemars = { version = "0.8.12", features = ["uuid1"] } uuid = { version = "1.3", features = ["serde", "v4"] } diff --git a/progenitor/src/main.rs b/progenitor/src/main.rs index 6e1397c59..020c1597d 100644 --- a/progenitor/src/main.rs +++ b/progenitor/src/main.rs @@ -212,7 +212,7 @@ pub fn dependencies(builder: Generator, include_client: bool) -> Vec { let mut deps = vec![ format!("bytes = \"{}\"", dependency_versions.get("bytes").unwrap()), format!("futures-core = \"{}\"", dependency_versions.get("futures-core").unwrap()), - format!("reqwest = {{ version = \"{}\", default-features=false, features = [\"json\", \"stream\"] }}", dependency_versions.get("reqwest").unwrap()), + format!("reqwest = {{ version = \"{}\", default-features=false, features = [\"json\", \"stream\", \"multipart\"] }}", dependency_versions.get("reqwest").unwrap()), format!("serde = {{ version = \"{}\", features = [\"derive\"] }}", dependency_versions.get("serde").unwrap()), format!("serde_urlencoded = \"{}\"", dependency_versions.get("serde_urlencoded").unwrap()), ]; From f67be29b51d252c40b6d261ad508313c98e94fe7 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 11 Apr 2023 18:29:35 +0200 Subject: [PATCH 02/23] refactor: somewhat cleaner approach --- progenitor-impl/src/lib.rs | 61 ++++++++++++++++++++++++----- progenitor-impl/src/method.rs | 73 ++++++++++++++--------------------- 2 files changed, 81 insertions(+), 53 deletions(-) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 331c3b1ad..9921dcfe1 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -7,6 +7,7 @@ use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use thiserror::Error; +use typify::{TypeDetails, TypeId}; use typify::{TypeSpace, TypeSpaceSettings}; use crate::to_schema::ToSchema; @@ -40,6 +41,7 @@ pub type Result = std::result::Result; pub struct Generator { type_space: TypeSpace, + forms: HashSet, settings: GenerationSettings, uses_futures: bool, uses_websockets: bool, @@ -163,6 +165,7 @@ impl Default for Generator { type_space: TypeSpace::new( TypeSpaceSettings::default().with_type_mod("types"), ), + forms: Default::default(), settings: Default::default(), uses_futures: Default::default(), uses_websockets: Default::default(), @@ -204,6 +207,7 @@ impl Generator { Self { type_space: TypeSpace::new(&type_settings), settings: settings.clone(), + forms: Default::default(), uses_futures: false, uses_websockets: false, } @@ -262,6 +266,43 @@ impl Generator { let types = self.type_space.to_stream(); + let extra_impl = TokenStream::from_iter( + self.forms + .iter() + .map(|type_id| { + let typ = self.get_type_space().get_type(type_id).unwrap(); + let form_name = typ.name(); + let td = typ.details(); + let TypeDetails::Struct(tstru) = td else { unreachable!() }; + let properties = indexmap::IndexMap::<&'_ str, _>::from_iter( + tstru + .properties() + .filter_map(|(prop_name, prop_id)| { + self.get_type_space() + .get_type(&prop_id).ok() + .map(|prop_typ| (prop_name, prop_typ)) + }) + ); + let properties = syn::punctuated::Punctuated::<_, syn::Token![,]>::from_iter( + properties + .into_iter() + .map(|(prop_name, prop_ty)| { + let ident = quote::format_ident!("{}", prop_name); + quote!{ (#prop_name, &self. #ident) } + })); + + let form_name = quote::format_ident!("{}",typ.name()); + + quote! { + impl #form_name { + pub fn as_form<'f>(&'f self) -> impl std::iter::Iterator { + [#properties] + .into_iter() + .filter_map(|(name, val)| val.as_ref().map(|val| (name, val.as_slice()))) + } + } + } + })); // Generate an implementation of a `Self::as_inner` method, if an inner // type is defined. let maybe_inner = self.settings.inner_type.as_ref().map(|inner| { @@ -290,20 +331,20 @@ impl Generator { }); let client_docstring = { - let mut s = format!("Client for {}", spec.info.title); + let mut doc = format!("Client for {}", spec.info.title); - if let Some(ss) = &spec.info.description { - s.push_str("\n\n"); - s.push_str(ss); + if let Some(desc) = &spec.info.description { + doc.push_str("\n\n"); + doc.push_str(desc); } - if let Some(ss) = &spec.info.terms_of_service { - s.push_str("\n\n"); - s.push_str(ss); + if let Some(tos) = &spec.info.terms_of_service { + doc.push_str("\n\n"); + doc.push_str(tos); } - s.push_str(&format!("\n\nVersion: {}", &spec.info.version)); + doc.push_str(&format!("\n\nVersion: {}", &spec.info.version)); - s + doc }; let version_str = &spec.info.version; @@ -325,6 +366,8 @@ impl Generator { use std::convert::TryFrom; #types + + #extra_impl } #[derive(Clone, Debug)] diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 1ebbd9b76..15bc6d6e9 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -10,7 +10,7 @@ use indexmap::{IndexMap, IndexSet}; use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; -use typify::{TypeId, TypeSpace}; +use typify::{TypeId, TypeSpace, TypeSpacePatch}; use crate::{ template::PathTemplate, @@ -105,7 +105,7 @@ pub struct OperationParameter { #[derive(Debug, Eq, PartialEq)] pub enum OperationParameterType { Type(TypeId), - Form(IndexSet), + Form(TypeId), RawBody, } @@ -585,7 +585,8 @@ impl Generator { .map(|param| { let name = format_ident!("{}", param.name); match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let typ = self .type_space .get_type(type_id) @@ -593,19 +594,6 @@ impl Generator { .parameter_ident_with_lifetime("a"); quote! { #name: #typ} } - OperationParameterType::Form(keys) => { - let ts = TokenStream::from_iter( - itertools::Itertools::intersperse( - keys.iter().map(|form_prop_name| { - let form_prop_name = - format_ident!("{}", form_prop_name); - quote! { #form_prop_name: Vec } - }), - quote! {, }, - ), - ); - ts - } OperationParameterType::RawBody => { quote! { #name: B } } @@ -935,15 +923,11 @@ impl Generator { OperationParameterKind::Body( BodyContentType::FormData ), - OperationParameterType::Form(map), + OperationParameterType::Form(_), ) => { - let form_prop_names = map.iter().cloned().map(|form_prop_name| { - let ident= format_ident!("{}", form_prop_name); - quote! { (#form_prop_name, #ident) } - }); Some(quote! { // This uses progenitor_client::RequestBuilderExt which sets up a simple form data based on bytes - .form_from_raw(vec![ #(#form_prop_names),* ])? + .form_from_raw(body.as_form())? })}, (OperationParameterKind::Body(_), _) => { unreachable!("invalid body kind/type combination") @@ -1408,7 +1392,8 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; // For body parameters only, if there's a builder we'll @@ -1425,10 +1410,6 @@ impl Generator { } } - OperationParameterType::Form(_form) => { - todo!("Form is nit expected here") - } - OperationParameterType::RawBody => { cloneable = false; Ok(quote! { Result }) @@ -1441,7 +1422,8 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; let details = ty.details(); let optional = @@ -1460,9 +1442,6 @@ impl Generator { Ok(quote! { Err(#err_msg.to_string()) }) } } - OperationParameterType::Form(_form) => { - todo!("Form is nit expected here") - } OperationParameterType::RawBody => { let err_msg = format!("{} was not initialized", param.name); Ok(quote! { Err(#err_msg.to_string()) }) @@ -1474,7 +1453,8 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; if ty.builder().is_some() { let type_name = ty.ident(); @@ -1488,14 +1468,6 @@ impl Generator { } } - OperationParameterType::Form(_form) => { - todo!("Form is nit expected here") - } - - OperationParameterType::Form(_form) => { - todo!("Form is nit expected here") - } - OperationParameterType::RawBody => Ok(quote! {}), }) .collect::>>()?; @@ -1508,11 +1480,12 @@ impl Generator { .map(|param| { let param_name = format_ident!("{}", param.name); match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; let details = ty.details(); match (&details, ty.builder()) { - // TODO right now optional body paramters are not + // TODO right now optional body parameters are not // addressed (typify::TypeDetails::Option(_), Some(_)) => { unreachable!() @@ -1596,7 +1569,7 @@ impl Generator { } } - OperationParameterType::Form(form_keys) => { + OperationParameterType::Form(type_id) => { let err_msg = format!( "conversion to `reqwest::Body` for {} failed", param.name, @@ -2161,7 +2134,19 @@ impl Generator { schema ))), }?; - OperationParameterType::Form(mapped) + + let form_name = sanitize( + &format!( + "{}-form", + operation.operation_id.as_ref().unwrap(), + ), + Case::Pascal, + ); + let type_id = self + .type_space + .add_type_with_name(&schema.to_schema(), Some(form_name))?; + self.forms.insert(type_id.clone()); + OperationParameterType::Form(type_id) } BodyContentType::Json | BodyContentType::FormUrlencoded => { // TODO it would be legal to have the encoding field set for From 8b6f4ef7af91ea6e904b41a994f3e7960f347bd6 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 07:27:08 +0200 Subject: [PATCH 03/23] maintain ordering --- progenitor-impl/src/lib.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 9921dcfe1..2885383e0 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -3,6 +3,7 @@ use std::collections::{HashMap, HashSet}; use openapiv3::OpenAPI; +use indexmap::{IndexMap, IndexSet}; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; @@ -41,7 +42,7 @@ pub type Result = std::result::Result; pub struct Generator { type_space: TypeSpace, - forms: HashSet, + forms: IndexSet, settings: GenerationSettings, uses_futures: bool, uses_websockets: bool, @@ -271,7 +272,6 @@ impl Generator { .iter() .map(|type_id| { let typ = self.get_type_space().get_type(type_id).unwrap(); - let form_name = typ.name(); let td = typ.details(); let TypeDetails::Struct(tstru) = td else { unreachable!() }; let properties = indexmap::IndexMap::<&'_ str, _>::from_iter( From 2ec5033d3072e7d676308e18f854d645d28805dc Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Wed, 7 Jun 2023 10:21:27 +0200 Subject: [PATCH 04/23] clippy and warning silence --- progenitor-client/src/progenitor_client.rs | 4 ++-- progenitor-impl/src/lib.rs | 2 +- progenitor-impl/src/method.rs | 6 +++--- progenitor-impl/src/template.rs | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index ef5212f5d..ce0a83d9b 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -430,9 +430,9 @@ impl RequestBuilderExt for RequestBuilder { I: Sized + IntoIterator, >( self, - mut iter: I, + iter: I, ) -> Result> { - use reqwest::multipart::{Form, Part}; + use reqwest::multipart::{Form}; let mut form = Form::new(); for (name, value) in iter { diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 2885383e0..238f3a858 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -286,7 +286,7 @@ impl Generator { let properties = syn::punctuated::Punctuated::<_, syn::Token![,]>::from_iter( properties .into_iter() - .map(|(prop_name, prop_ty)| { + .map(|(prop_name, _prop_ty)| { let ident = quote::format_ident!("{}", prop_name); quote!{ (#prop_name, &self. #ident) } })); diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 15bc6d6e9..794dda3bd 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -345,7 +345,7 @@ impl Generator { let typ = self .type_space - .add_type_with_name(&schema, Some(dbg!(name)))?; + .add_type_with_name(&schema, Some(name))?; query.push(( parameter_data.name.clone(), @@ -2072,7 +2072,7 @@ impl Generator { // "format": "binary" // } - let mapped = match schema.item(components)? { + let _mapped = match schema.item(components)? { openapiv3::Schema { schema_data: openapiv3::SchemaData { @@ -2087,7 +2087,7 @@ impl Generator { openapiv3::SchemaKind::Type(openapiv3::Type::Object( openapiv3::ObjectType { properties, - additional_properties, + additional_properties: _, .. }, )), diff --git a/progenitor-impl/src/template.rs b/progenitor-impl/src/template.rs index 98db60858..4049f5fea 100644 --- a/progenitor-impl/src/template.rs +++ b/progenitor-impl/src/template.rs @@ -39,7 +39,7 @@ impl PathTemplate { "{}", rename .get(&n) - .expect(&format!("missing path name mapping {}", n)), + .unwrap_or_else(|| panic!("missing path name mapping {}", n)), ); Some(quote! { encode_path(&#param.to_string()) From e2eda20ae42f9dd38992f93e370d574c082113ef Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Wed, 7 Jun 2023 13:22:14 +0200 Subject: [PATCH 05/23] clippy --- progenitor-impl/src/method.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 794dda3bd..eba17718f 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -1568,8 +1568,7 @@ impl Generator { } } } - - OperationParameterType::Form(type_id) => { + OperationParameterType::Form(_type_id) => { let err_msg = format!( "conversion to `reqwest::Body` for {} failed", param.name, @@ -1585,7 +1584,6 @@ impl Generator { } }) } - OperationParameterType::RawBody => { let err_msg = format!( "conversion to `reqwest::Body` for {} failed", From 46a0bfbd37393e4a205e796168b8f3f23bb63bb9 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 08:25:03 +0200 Subject: [PATCH 06/23] clippy --- Cargo.lock | 1651 ++++++++++++-------- Cargo.toml | 8 +- progenitor-client/src/progenitor_client.rs | 2 +- progenitor-impl/src/lib.rs | 2 +- progenitor-impl/src/method.rs | 26 +- 5 files changed, 969 insertions(+), 720 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2626ebdc9..8f2dc39b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,26 +2,54 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "addr2line" +version = "0.21.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" +dependencies = [ + "gimli", +] + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + [[package]] name = "ahash" -version = "0.8.3" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c99f64d1e06488f620f932677e24bc6e2897582980441ae90a671415bd7ec2f" +checksum = "e89da841a80418a9b391ebaea17f5c112ffaaa96f621d2c285b5174da76b9011" dependencies = [ "cfg-if", "once_cell", "version_check", + "zerocopy", ] [[package]] name = "aho-corasick" -version = "0.7.18" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e37cfd5e7657ada45f742d6e99ca5788580b5c529dc78faf11ece6dc702656f" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] +[[package]] +name = "allocator-api2" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0942ffc6dcaadf03badf6e6a2d0228460359d5e34b57ccdc720b7382dfbd5ec5" + +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_system_properties" version = "0.1.5" @@ -31,17 +59,65 @@ dependencies = [ "libc", ] +[[package]] +name = "anstream" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d96bd03f33fe50a863e394ee9718a706f988b9079b20c3784fb726e7678b62fb" +dependencies = [ + "anstyle", + "anstyle-parse", + "anstyle-query", + "anstyle-wincon", + "colorchoice", + "utf8parse", +] + +[[package]] +name = "anstyle" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8901269c6307e8d93993578286ac0edf7f195079ffff5ebdeea6a59ffb7e36bc" + +[[package]] +name = "anstyle-parse" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c75ac65da39e5fe5ab759307499ddad880d724eed2f6ce5b5e8a26f4f387928c" +dependencies = [ + "utf8parse", +] + +[[package]] +name = "anstyle-query" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e28923312444cdd728e4738b3f9c9cac739500909bb3d3c94b43551b16517648" +dependencies = [ + "windows-sys 0.52.0", +] + +[[package]] +name = "anstyle-wincon" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd54b81ec8d6180e24654d0b371ad22fc3dd083b6ff8ba325b72e00c87660a7" +dependencies = [ + "anstyle", + "windows-sys 0.52.0", +] + [[package]] name = "anyhow" -version = "1.0.70" +version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7de8ce5e0f9f8d88245311066a578d72b7af3e7088f32783804676302df237e4" +checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" [[package]] name = "async-stream" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad445822218ce64be7a341abfb0b1ea43b5c23aa83902542a4542e78309d8e5e" +checksum = "cd56dd203fef61ac097dd65721a419ddccb106b2d2b70ba60a6b529f03961a51" dependencies = [ "async-stream-impl", "futures-core", @@ -50,48 +126,64 @@ dependencies = [ [[package]] name = "async-stream-impl" -version = "0.3.4" +version = "0.3.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4655ae1a7b0cdf149156f780c5bf3f1352bc53cbd9e0a361a7ef7b22947e965" +checksum = "16e62a023e7c117e27523144c5d2459f4397fcc3cab0085af8e2224f643a0193" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", ] [[package]] name = "async-trait" -version = "0.1.67" +version = "0.1.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86ea188f25f0255d8f92797797c97ebf5631fa88178beb1a46fdf5622c9a00e4" +checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] -name = "atty" -version = "0.2.14" +name = "atomic-waker" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "autocfg" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" + +[[package]] +name = "backtrace" +version = "0.3.71" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ - "hermit-abi 0.1.19", + "addr2line", + "cc", + "cfg-if", "libc", - "winapi", + "miniz_oxide", + "object", + "rustc-demangle", ] [[package]] -name = "autocfg" -version = "1.0.1" +name = "base64" +version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" [[package]] name = "base64" -version = "0.21.0" +version = "0.22.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4a4ddaa51a5bc52a6948f74c06d20aaaddb71924eab79b8c97a8c556e942d6a" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" [[package]] name = "bitflags" @@ -99,20 +191,26 @@ version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" +[[package]] +name = "bitflags" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" + [[package]] name = "block-buffer" -version = "0.10.2" +version = "0.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" dependencies = [ "generic-array", ] [[package]] name = "built" -version = "0.6.0" +version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96f9cdd34d6eb553f9ea20e5bf84abb7b13c729f113fc1d8e49dc00ad9fa8738" +checksum = "b99c4cdc7b2c2364182331055623bdf45254fcb679fea565c40c3c11c101889a" dependencies = [ "cargo-lock", "git2", @@ -120,44 +218,45 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytes" -version = "1.4.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "camino" -version = "1.1.4" +version = "1.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c530edf18f37068ac2d977409ed5cd50d53d73bc653c7647b48eb78976ac9ae2" +checksum = "c59e92b5a388f549b863a7bea62612c09f24c8393560709a54558a9abdfb3b9c" dependencies = [ "serde", ] [[package]] name = "cargo-lock" -version = "8.0.3" +version = "9.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "031718ddb8f78aa5def78a09e90defe30151d1f6c672f937af4dd916429ed996" +checksum = "e11c675378efb449ed3ce8de78d75d0d80542fc98487c26aba28eb3b82feac72" dependencies = [ - "semver 1.0.16", + "semver", "serde", - "toml 0.5.9", + "toml 0.7.8", "url", ] [[package]] name = "cc" -version = "1.0.72" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" dependencies = [ "jobserver", + "libc", ] [[package]] @@ -168,80 +267,83 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.24" +version = "0.4.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b" +checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", - "time 0.1.43", "wasm-bindgen", - "winapi", + "windows-targets 0.52.4", ] [[package]] name = "clap" -version = "4.1.14" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "906f7fe1da4185b7a282b2bc90172a496f9def1aca4545fe7526810741591e14" +checksum = "90bc066a67923782aa8515dbaea16946c5bcc5addbd668bb80af688e53e548a0" dependencies = [ "clap_builder", "clap_derive", - "once_cell", ] [[package]] name = "clap_builder" -version = "4.1.14" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "351f9ad9688141ed83dfd8f5fb998a06225ef444b48ff4dc43de6d409b7fd10b" +checksum = "ae129e2e766ae0ec03484e609954119f123cc1fe650337e155d03b022f24f7b4" dependencies = [ - "bitflags", + "anstream", + "anstyle", "clap_lex", - "is-terminal", "strsim", - "termcolor", ] [[package]] name = "clap_derive" -version = "4.1.14" +version = "4.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81d7dc0031c3a59a04fc2ba395c8e2dd463cba1859275f065d225f6122221b45" +checksum = "528131438037fd55894f62d6e9f068b8f45ac57ffa77517819645d10aed04f64" dependencies = [ - "heck", + "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] name = "clap_lex" -version = "0.4.1" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98cc8fbded0c607b7ba9dd60cd98df59af97e84d24e49c8557331cfc26d301ce" + +[[package]] +name = "colorchoice" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a2dd5a6fe8c6e3502f568a6353e5273bbb15193ad9a89e457b9970798efbea1" +checksum = "acbf1af155f9b9ef647e42cdc158db4b64a1b61f743629225fde6f3e0be2a7c7" [[package]] name = "console" -version = "0.15.3" +version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5556015fe3aad8b968e5d4124980fbe2f6aaee7aeec6b749de1faaa2ca5d0a4c" +checksum = "0e1f83fc076bd6dd27517eacdf25fef6c4dfe5f1d7448bafaaf3a26f13b5e4eb" dependencies = [ "encode_unicode", "lazy_static", "libc", "unicode-width", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "core-foundation" -version = "0.9.2" +version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6888e10551bb93e424d8df1d07f1a8b4fceb0001a3a4b048bfc47554946f47b3" +checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" dependencies = [ "core-foundation-sys", "libc", @@ -249,38 +351,33 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.3" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5827cebf4670468b8772dd191856768aedcb1b0278a04f989f7766351917b9dc" +checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" [[package]] name = "cpufeatures" -version = "0.2.3" +version = "0.2.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1079fb8528d9f9c888b1e8aa651e6e079ade467323d58f75faf1d30b1808f540" +checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" dependencies = [ "libc", ] [[package]] name = "crossbeam-channel" -version = "0.5.2" +version = "0.5.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e54ea8bc3fb1ee042f5aace6e3c6e025d3874866da222930f70ce62aceba0bfa" +checksum = "ab3db02a9c5b5121e1e42fbdb1aeb65f5e02624cc58c43f2884c6ccac0b82f95" dependencies = [ - "cfg-if", "crossbeam-utils", ] [[package]] name = "crossbeam-utils" -version = "0.8.7" +version = "0.8.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6" -dependencies = [ - "cfg-if", - "lazy_static", -] +checksum = "248e3bacc7dc6baa3b21e405ee045c3047101a49145e7e9eca583ab4c2ca5345" [[package]] name = "crypto-common" @@ -292,11 +389,26 @@ dependencies = [ "typenum", ] +[[package]] +name = "debug-ignore" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe7ed1d93f4553003e20b629abe9085e1e81b1429520f897f8f8860bc6dfc21" + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", +] + [[package]] name = "digest" -version = "0.10.6" +version = "0.10.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", @@ -325,28 +437,29 @@ dependencies = [ [[package]] name = "dropshot" -version = "0.9.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot#ccce2241b9a798819a3385f01b0b92f529c5fe8f" +version = "0.10.1-dev" +source = "git+https://github.com/oxidecomputer/dropshot#7ab6e5eec8786bc1e43e82d86345e96cd8d4a34e" dependencies = [ "async-stream", "async-trait", - "base64", + "base64 0.22.0", "bytes", "camino", "chrono", + "debug-ignore", "dropshot_endpoint", "form_urlencoded", "futures", - "hostname", - "http", + "hostname 0.4.0", + "http 0.2.12", "hyper", - "indexmap", - "openapiv3", + "indexmap 2.2.6", + "multer", + "openapiv3 2.0.0", "paste", "percent-encoding", - "proc-macro2", "rustls", - "rustls-pemfile", + "rustls-pemfile 2.1.2", "schemars", "serde", "serde_json", @@ -360,34 +473,35 @@ dependencies = [ "slog-term", "tokio", "tokio-rustls", - "toml 0.7.3", + "toml 0.8.12", "uuid", "version_check", + "waitgroup", ] [[package]] name = "dropshot_endpoint" -version = "0.9.1-dev" -source = "git+https://github.com/oxidecomputer/dropshot#ccce2241b9a798819a3385f01b0b92f529c5fe8f" +version = "0.10.1-dev" +source = "git+https://github.com/oxidecomputer/dropshot#7ab6e5eec8786bc1e43e82d86345e96cd8d4a34e" dependencies = [ "proc-macro2", "quote", "serde", "serde_tokenstream", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] name = "dyn-clone" -version = "1.0.4" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee2626afccd7561a06cf1367e2950c4718ea04565e20fb5029b6c7d8ad09abcf" +checksum = "0d6ef0072f8a535281e4876be788938b528e9a1d43900b82c2569af7da799125" [[package]] name = "either" -version = "1.8.1" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcaabb2fef8c910e7f4c7ce9f67a1283a1715879a7c230ca9d6d1ae31f16d91" +checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" [[package]] name = "encode_unicode" @@ -397,39 +511,34 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.30" +version = "0.8.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" +checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" dependencies = [ "cfg-if", ] [[package]] -name = "errno" -version = "0.2.8" +name = "equivalent" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" -dependencies = [ - "errno-dragonfly", - "libc", - "winapi", -] +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] -name = "errno-dragonfly" -version = "0.1.2" +name = "errno" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" dependencies = [ - "cc", "libc", + "windows-sys 0.52.0", ] [[package]] name = "example-build" version = "0.0.1" dependencies = [ - "base64", + "base64 0.21.7", "chrono", "progenitor", "progenitor-client", @@ -454,9 +563,9 @@ dependencies = [ [[package]] name = "expectorate" -version = "1.0.6" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74a483344b06e0a8183209bdf1c2e23b834d04dcdec591d2f617cc60b6c1298b" +checksum = "de6f19b25bdfa2747ae775f37cd109c31f1272d4e4c83095be0727840aa1d75f" dependencies = [ "console", "newline-converter", @@ -465,12 +574,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "1.6.0" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "779d043b6a0b90cc4c0ed7ee380a6504394cee7efd7db050e3774eee387324b2" -dependencies = [ - "instant", -] +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "fnv" @@ -495,18 +601,18 @@ checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" [[package]] name = "form_urlencoded" -version = "1.1.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" dependencies = [ "percent-encoding", ] [[package]] name = "futures" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "531ac96c6ff5fd7c62263c5e3c67a603af4fcaee2e1a0ae5565ba3a11e69e549" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" dependencies = [ "futures-channel", "futures-core", @@ -519,9 +625,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "164713a5a0dcc3e7b4b1ed7d3b433cabc18025386f9339346e8daf15963cf7ac" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" dependencies = [ "futures-core", "futures-sink", @@ -529,15 +635,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86d7a0c1aa76363dac491de0ee99faf6941128376f1cf96f07db7603b7de69dd" +checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" [[package]] name = "futures-executor" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1997dd9df74cdac935c76252744c1ed5794fac083242ea4fe77ef3ed60ba0f83" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" dependencies = [ "futures-core", "futures-task", @@ -546,38 +652,38 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d422fa3cbe3b40dca574ab087abb5bc98258ea57eea3fd6f1fa7162c778b91" +checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-macro" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eb14ed937631bd8b8b8977f2c198443447a8355b6e3ca599f38c975e5a963b6" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", ] [[package]] name = "futures-sink" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec93083a4aecafb2a80a885c9de1f0ccae9dbd32c2bb54b0c3a65690e0b8d2f2" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" [[package]] name = "futures-task" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd65540d33b37b16542a0438c12e6aeead10d4ac5d05bd3f805b8f35ab592879" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" [[package]] name = "futures-util" -version = "0.3.27" +version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ef6b17e481503ec85211fed8f39d1970f128935ca1f814cd32ac4a6842e84ab" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" dependencies = [ "futures-channel", "futures-core", @@ -593,9 +699,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "0.14.6" +version = "0.14.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", @@ -612,22 +718,28 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.4" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "libc", - "wasi 0.10.2+wasi-snapshot-preview1", + "wasi", ] +[[package]] +name = "gimli" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" + [[package]] name = "git2" -version = "0.16.1" +version = "0.17.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ccf7f68c2995f392c49fffb4f95ae2c873297830eb25c6bc4c114ce8f4562acc" +checksum = "7b989d6a7ca95a362cf2cfc5ad688b3a467be1f87e480b8dad07fee8c79b0044" dependencies = [ - "bitflags", + "bitflags 1.3.2", "libc", "libgit2-sys", "log", @@ -636,28 +748,28 @@ dependencies = [ [[package]] name = "h2" -version = "0.3.10" +version = "0.3.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0c9de88456263e249e241fcd211d3954e2c9b0ef7ccfc235a444eb367cae3689" +checksum = "81fe527a889e1532da5c525686d96d4c2e74cdd345badf8dfef9f6b39dd5f5e8" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http", - "indexmap", + "http 0.2.12", + "indexmap 2.2.6", "slab", "tokio", - "tokio-util 0.6.9", + "tokio-util", "tracing", ] [[package]] name = "hashbrown" -version = "0.12.1" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db0d4cf898abf0081f964436dc980e96670a0f36863e4b83aaacdb65c9d7ccc3" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" [[package]] name = "hashbrown" @@ -668,6 +780,16 @@ dependencies = [ "ahash", ] +[[package]] +name = "hashbrown" +version = "0.14.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604" +dependencies = [ + "ahash", + "allocator-api2", +] + [[package]] name = "heck" version = "0.4.1" @@ -675,30 +797,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" [[package]] -name = "hermit-abi" -version = "0.1.19" +name = "heck" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" -dependencies = [ - "libc", -] +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" -version = "0.2.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7" -dependencies = [ - "libc", -] +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" [[package]] name = "home" -version = "0.5.3" +version = "0.5.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2456aef2e6b6a9784192ae780c0f15bc57df0e918585282325e8c8ac27737654" +checksum = "e3d1354bf6b7235cb4a0576c2619fd4ed18183f689b12b006a0ee7329eeff9a5" dependencies = [ - "winapi", + "windows-sys 0.52.0", ] [[package]] @@ -712,11 +828,33 @@ dependencies = [ "winapi", ] +[[package]] +name = "hostname" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9c7c7c8ac16c798734b8a24560c1362120597c40d5e1459f09498f8f6c8f2ba" +dependencies = [ + "cfg-if", + "libc", + "windows", +] + +[[package]] +name = "http" +version = "0.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + [[package]] name = "http" -version = "0.2.9" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd6effc99afb63425aff9b05836f029929e345a6148a14b7ecd5ab67af944482" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" dependencies = [ "bytes", "fnv", @@ -725,12 +863,12 @@ dependencies = [ [[package]] name = "http-body" -version = "0.4.4" +version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ff4f84919677303da5f147645dbea6b1881f368d03ac84e1dc09031ebd7b2c6" +checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" dependencies = [ "bytes", - "http", + "http 0.2.12", "pin-project-lite", ] @@ -742,22 +880,22 @@ checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" [[package]] name = "httpdate" -version = "1.0.2" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" [[package]] name = "hyper" -version = "0.14.25" +version = "0.14.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc5e554ff619822309ffd57d8734d77cd5ce6238bc956f037ea06c58238c9899" +checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" dependencies = [ "bytes", "futures-channel", "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "httparse", "httpdate", @@ -785,25 +923,33 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.47" +version = "0.1.60" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c495f162af0bf17656d0014a0eded5f3cd2f365fdd204548c2869db89359dc7" +checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" dependencies = [ "android_system_properties", "core-foundation-sys", + "iana-time-zone-haiku", "js-sys", - "once_cell", "wasm-bindgen", - "winapi", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] name = "idna" -version = "0.2.3" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "418a0a6fab821475f634efe3ccc45c013f742efe03d853e8d3355d5cb850ecf8" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" dependencies = [ - "matches", "unicode-bidi", "unicode-normalization", ] @@ -815,45 +961,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", - "hashbrown 0.12.1", + "hashbrown 0.12.3", "serde", ] [[package]] -name = "instant" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" -dependencies = [ - "cfg-if", -] - -[[package]] -name = "io-lifetimes" -version = "1.0.2" +name = "indexmap" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e394faa0efb47f9f227f1cd89978f854542b318a6f64fa695489c9c993056656" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ - "libc", - "windows-sys 0.42.0", + "equivalent", + "hashbrown 0.14.3", + "serde", ] [[package]] name = "ipnet" -version = "2.3.1" +version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68f2d64f2edebec4ce84ad108148e67e1064789bee435edc5b60ad398714a3a9" +checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" [[package]] name = "is-terminal" -version = "0.4.1" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "927609f78c2913a6f6ac3c27a4fe87f43e2a35367c0c4b0f8265e8f49a104330" +checksum = "f23ff5ef2b80d608d61efee834934d862cd92461afc0560dedf493e4c033738b" dependencies = [ - "hermit-abi 0.2.6", - "io-lifetimes", - "rustix", - "windows-sys 0.42.0", + "hermit-abi", + "libc", + "windows-sys 0.52.0", ] [[package]] @@ -867,24 +1004,24 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.1" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" -version = "0.1.25" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "068b1ee6743e4d11fb9c6a1e6064b3693a1b600e7f5f5988047d98b3dc9fb90b" +checksum = "ab46a6e9526ddef3ae7f787c06f0f2600639ba80ea3eade3d8e670a2230f51d6" dependencies = [ "libc", ] [[package]] name = "js-sys" -version = "0.3.59" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "258451ab10b34f8af53416d1fdab72c22e805f0c92a1136d59470ec0b11138b2" +checksum = "29c15563dc2726973df627357ce0c9ddddbea194836909d655df6a75d2cf296d" dependencies = [ "wasm-bindgen", ] @@ -897,15 +1034,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.137" +version = "0.2.153" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" +checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" [[package]] name = "libgit2-sys" -version = "0.14.2+1.5.1" +version = "0.15.2+1.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f3d95f6b51075fe9810a7ae22c7095f12b98005ab364d8544797a825ce946a4" +checksum = "a80df2e11fb4a61f4ba2ab42dbe7f74468da143f1a75c74e11dee7c813f694fa" dependencies = [ "cc", "libc", @@ -913,11 +1050,21 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.5.0", + "libc", +] + [[package]] name = "libz-sys" -version = "1.1.8" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9702761c3935f8cc2f101793272e202c72b99da8f4224a19ddcf1279a6450bbf" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "libc", @@ -927,27 +1074,25 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.1.3" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f9f08d8963a6c613f4b1a78f4f4a4dbfadf8e6545b2d72861731e4858b8b47f" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" [[package]] name = "lock_api" -version = "0.4.6" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b" +checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" dependencies = [ + "autocfg", "scopeguard", ] [[package]] name = "log" -version = "0.4.14" +version = "0.4.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710" -dependencies = [ - "cfg-if", -] +checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" [[package]] name = "match_cfg" @@ -955,23 +1100,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ffbee8634e0d45d258acb448e7eaab3fce7a0a467395d4d9f228e3c1f01fb2e4" -[[package]] -name = "matches" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f" - [[package]] name = "memchr" -version = "2.4.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "mime" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" [[package]] name = "mime_guess" @@ -983,16 +1122,42 @@ dependencies = [ "unicase", ] +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + [[package]] name = "mio" -version = "0.8.4" +version = "0.8.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57ee1c23c7c63b0c9250c339ffdc69255f110b298b901b9f6c82547b7b87caaf" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" dependencies = [ "libc", + "wasi", + "windows-sys 0.48.0", +] + +[[package]] +name = "multer" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a15d522be0a9c3e46fd2632e272d178f56387bdb5c9fbb3a36c649062e9b5219" +dependencies = [ + "bytes", + "encoding_rs", + "futures-util", + "http 1.1.0", + "httparse", "log", - "wasi 0.11.0+wasi-snapshot-preview1", - "windows-sys 0.36.1", + "memchr", + "mime", + "spin", + "version_check", ] [[package]] @@ -1015,75 +1180,91 @@ dependencies = [ [[package]] name = "newline-converter" -version = "0.2.2" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f71d09d5c87634207f894c6b31b6a2b2c64ea3bdcf71bd5599fdbbe1600c00f" +checksum = "47b6b097ecb1cbfed438542d16e84fd7ad9b0c76c8a65b7f9039212a3d14dc7f" dependencies = [ "unicode-segmentation", ] [[package]] -name = "num-integer" -version = "0.1.44" +name = "num-conv" +version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d2cc698a63b549a70bc047073d2949cce27cd1c7b0a4a862d08a8031bc2801db" -dependencies = [ - "autocfg", - "num-traits", -] +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" [[package]] name = "num-traits" -version = "0.2.14" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a64b1ec5cda2586e284722486d802acf1f7dbdc623e2bfc57e65ca1cd099290" +checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" dependencies = [ "autocfg", ] [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi 0.1.19", + "hermit-abi", "libc", ] [[package]] name = "num_threads" -version = "0.1.3" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97ba99ba6393e2c3734791401b66902d981cb03bf190af674ca69949b6d5fb15" +checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9" dependencies = [ "libc", ] +[[package]] +name = "object" +version = "0.32.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" +dependencies = [ + "memchr", +] + [[package]] name = "once_cell" -version = "1.13.1" +version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "074864da206b4973b84eb91683020dbefd6a8c3f0f38e054d93954e891935e4e" +checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" [[package]] name = "openapiv3" -version = "1.0.2" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b1a9f106eb0a780abd17ba9fca8e0843e3461630bcbe2af0ad4d5d3ba4e9aa4" +checksum = "33b83630305ecc3355e998ddd2f926f98aae8e105eb42652174a58757851ba47" dependencies = [ - "indexmap", + "indexmap 1.9.3", + "serde", + "serde_json", +] + +[[package]] +name = "openapiv3" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc02deea53ffe807708244e5914f6b099ad7015a207ee24317c22112e17d9c5c" +dependencies = [ + "indexmap 2.2.6", "serde", "serde_json", ] [[package]] name = "openssl" -version = "0.10.48" +version = "0.10.64" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "518915b97df115dd36109bfa429a48b8f737bd05508cf9588977b599648926d2" +checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" dependencies = [ - "bitflags", + "bitflags 2.5.0", "cfg-if", "foreign-types", "libc", @@ -1094,13 +1275,13 @@ dependencies = [ [[package]] name = "openssl-macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b501e44f11665960c7e7fcf062c7d96a14ade4aa98116c004b2e37b5be7d736c" +checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", ] [[package]] @@ -1111,11 +1292,10 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.83" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "666416d899cf077260dac8698d60a60b435a46d57e82acb1be3d0dad87284e5b" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ - "autocfg", "cc", "libc", "pkg-config", @@ -1124,9 +1304,9 @@ dependencies = [ [[package]] name = "parking_lot" -version = "0.12.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", "parking_lot_core", @@ -1134,43 +1314,34 @@ dependencies = [ [[package]] name = "parking_lot_core" -version = "0.9.1" +version = "0.9.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954" +checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" dependencies = [ "cfg-if", "libc", "redox_syscall", "smallvec", - "windows-sys 0.32.0", + "windows-targets 0.48.5", ] [[package]] name = "paste" -version = "1.0.12" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f746c4065a8fa3fe23974dd82f15431cc8d40779821001404d10d2e79ca7d79" +checksum = "de3145af08024dea9fa9914f381a17b8fc6034dfb00f3a84013f7ff43f29ed4c" [[package]] name = "percent-encoding" -version = "2.2.0" +version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" - -[[package]] -name = "pest" -version = "2.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53" -dependencies = [ - "ucd-trie", -] +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" [[package]] name = "pin-project-lite" -version = "0.2.8" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -1180,21 +1351,27 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" [[package]] name = "pkg-config" -version = "0.3.24" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" + +[[package]] +name = "powerfmt" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58893f751c9b0412871a09abd62ecd2a00298c6c83befa223ef98c52aef40cbe" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro2" -version = "1.0.56" +version = "1.0.79" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b63bdb0cd06f1f4dedf69b254734f9b45af66e4a031e42a7480257d9898b435" +checksum = "e835ff2298f5721608eb1a980ecaee1aef2c132bf95ecc026a11b7bf3c01c02e" dependencies = [ "unicode-ident", ] @@ -1204,19 +1381,19 @@ name = "progenitor" version = "0.2.1-dev" dependencies = [ "anyhow", - "base64", + "base64 0.21.7", "built", "chrono", "clap", "futures", - "openapiv3", + "openapiv3 1.0.4", "percent-encoding", "progenitor-client", "progenitor-impl", "progenitor-macro", "project-root", "rand", - "regress", + "regress 0.5.0", "reqwest", "schemars", "serde", @@ -1245,12 +1422,12 @@ dependencies = [ "dropshot", "expectorate", "getopts", - "heck", - "http", + "heck 0.4.1", + "http 0.2.12", "hyper", - "indexmap", + "indexmap 1.9.3", "itertools", - "openapiv3", + "openapiv3 1.0.4", "proc-macro2", "quote", "regex", @@ -1259,7 +1436,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml", - "syn 2.0.8", + "syn 2.0.58", "thiserror", "typify", "unicode-ident", @@ -1269,7 +1446,7 @@ dependencies = [ name = "progenitor-macro" version = "0.2.1-dev" dependencies = [ - "openapiv3", + "openapiv3 1.0.4", "proc-macro2", "progenitor-impl", "quote", @@ -1278,7 +1455,7 @@ dependencies = [ "serde_json", "serde_tokenstream", "serde_yaml", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] @@ -1289,9 +1466,9 @@ checksum = "8bccbff07d5ed689c4087d20d7307a52ab6141edeedf487c3876a55b86cf63df" [[package]] name = "quote" -version = "1.0.26" +version = "1.0.35" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4424af4bf778aae2051a77b60283332f386554255d722233d09fbfc7e30da2fc" +checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" dependencies = [ "proc-macro2", ] @@ -1319,37 +1496,50 @@ dependencies = [ [[package]] name = "rand_core" -version = "0.6.3" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" dependencies = [ "getrandom", ] [[package]] name = "redox_syscall" -version = "0.2.10" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" dependencies = [ - "bitflags", + "bitflags 1.3.2", ] [[package]] name = "redox_users" -version = "0.4.0" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "528532f3d801c87aec9def2add9ca802fe569e44a544afe633765267840abe64" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", - "redox_syscall", + "libredox", + "thiserror", ] [[package]] name = "regex" -version = "1.7.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b1f693b24f6ac912f4893ef08244d70b6067480d2f1a46e950c9691e6749d1d" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", @@ -1358,9 +1548,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.29" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "regress" @@ -1373,27 +1563,28 @@ dependencies = [ ] [[package]] -name = "remove_dir_all" -version = "0.5.3" +name = "regress" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" +checksum = "0eae2a1ebfecc58aff952ef8ccd364329abe627762f5bf09ff42eb9d98522479" dependencies = [ - "winapi", + "hashbrown 0.14.3", + "memchr", ] [[package]] name = "reqwest" -version = "0.11.16" +version = "0.11.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "27b71749df584b7f4cac2c426c127a7c785a5106cc98f7a8feb044115f0fa254" +checksum = "dd67538700a17451e7cba03ac727fb961abb7607553461627b97de0b89cf4a62" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "encoding_rs", "futures-core", "futures-util", "h2", - "http", + "http 0.2.12", "http-body", "hyper", "hyper-tls", @@ -1406,12 +1597,15 @@ dependencies = [ "once_cell", "percent-encoding", "pin-project-lite", + "rustls-pemfile 1.0.4", "serde", "serde_json", "serde_urlencoded", + "sync_wrapper", + "system-configuration", "tokio", "tokio-native-tls", - "tokio-util 0.7.3", + "tokio-util", "tower-service", "url", "wasm-bindgen", @@ -1423,78 +1617,112 @@ dependencies = [ [[package]] name = "ring" -version = "0.16.20" +version = "0.17.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3053cf52e236a3ed746dfc745aa9cacf1b791d846bdaf412f60a8d7d6e17c8fc" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" dependencies = [ "cc", + "cfg-if", + "getrandom", "libc", - "once_cell", "spin", "untrusted", - "web-sys", - "winapi", + "windows-sys 0.52.0", ] +[[package]] +name = "rustc-demangle" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" + [[package]] name = "rustfmt-wrapper" -version = "0.2.0" +version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed729e3bee08ec2befd593c27e90ca9fdd25efdc83c94c3b82eaef16e4f7406e" +checksum = "f1adc9dfed5cc999077978cc7163b9282c5751c8d39827c4ea8c8c220ca5a440" dependencies = [ "serde", "tempfile", "thiserror", - "toml 0.5.9", + "toml 0.8.12", "toolchain_find", ] [[package]] name = "rustix" -version = "0.36.4" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cb93e85278e08bb5788653183213d3a60fc242b10cb9be96586f5a73dcb67c23" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags", + "bitflags 2.5.0", "errno", - "io-lifetimes", "libc", "linux-raw-sys", - "windows-sys 0.42.0", + "windows-sys 0.52.0", ] [[package]] name = "rustls" -version = "0.20.8" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fff78fc74d175294f4e83b28343315ffcfb114b156f0185e9741cb5570f50e2f" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" dependencies = [ "log", "ring", - "sct", - "webpki", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", ] [[package]] name = "rustls-pemfile" -version = "1.0.2" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +dependencies = [ + "base64 0.21.7", +] + +[[package]] +name = "rustls-pemfile" +version = "2.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" +dependencies = [ + "base64 0.22.0", + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d194b56d58803a43635bdc398cd17e383d6f71f9182b9a192c127ca42494a59b" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ - "base64", + "ring", + "rustls-pki-types", + "untrusted", ] [[package]] name = "rustversion" -version = "1.0.6" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2cc38e8fa666e2de3c4aba7edeb5ffc5246c1c2ed0e3d17e560aeeba736b23f" +checksum = "80af6f9131f277a45a3fba6ce8e2258037bb0477a67e610d3c1fe046ab31de47" [[package]] name = "ryu" -version = "1.0.9" +version = "1.0.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f" +checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" [[package]] name = "same-file" @@ -1507,19 +1735,18 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.19" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f05ba609c234e60bee0d547fe94a4c7e9da733d1c962cf6e59efa4cd9c8bc75" +checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" dependencies = [ - "lazy_static", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "schemars" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02c613288622e5f0c3fdc5dbd4db1c5fbe752746b1d1a56a0630b78fd00de44f" +checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "chrono", "dyn-clone", @@ -1531,9 +1758,9 @@ dependencies = [ [[package]] name = "schemars_derive" -version = "0.8.12" +version = "0.8.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "109da1e6b197438deb6db99952990c7f959572794b80ff93707d55a232545e7c" +checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" dependencies = [ "proc-macro2", "quote", @@ -1543,27 +1770,17 @@ dependencies = [ [[package]] name = "scopeguard" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" - -[[package]] -name = "sct" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d53dcdb7c9f8158937a7981b48accfd39a43af418591a5d008c7b22b5e1b7ca4" -dependencies = [ - "ring", - "untrusted", -] +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" [[package]] name = "security-framework" -version = "2.4.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "525bc1abfda2e1998d152c45cf13e696f76d0a4972310b22fac1658b05df7c87" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ - "bitflags", + "bitflags 1.3.2", "core-foundation", "core-foundation-sys", "libc", @@ -1572,9 +1789,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.4.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9dd14d83160b528b7bfd66439110573efcfbe281b17fc2ca9f39f550d619c7e" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -1582,49 +1799,31 @@ dependencies = [ [[package]] name = "semver" -version = "0.11.0" +version = "1.0.22" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f301af10236f6df4160f7c3f04eec6dbc70ace82d23326abad5edee88801c6b6" -dependencies = [ - "semver-parser", -] - -[[package]] -name = "semver" -version = "1.0.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58bc9567378fc7690d6b2addae4e60ac2eeea07becb2c64b9f218b53865cba2a" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" dependencies = [ "serde", ] -[[package]] -name = "semver-parser" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "00b0bef5b7f9e0df16536d3961cfb6e84331c065b4066afb39768d0e319411f7" -dependencies = [ - "pest", -] - [[package]] name = "serde" -version = "1.0.159" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c04e8343c3daeec41f58990b9d77068df31209f2af111e059e9fe9646693065" +checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.159" +version = "1.0.197" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c614d17805b093df4b147b51339e7e44bf05ef59fba1e45d83500bcfb4d8585" +checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] @@ -1640,9 +1839,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.95" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d721eca97ac802aa7777b701877c8004d950fc142651367300d21c1cc0194744" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", @@ -1651,18 +1850,19 @@ dependencies = [ [[package]] name = "serde_path_to_error" -version = "0.1.11" +version = "0.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7f05c1d5476066defcdfacce1f52fc3cae3af1d3089727100c02ae92e5abbe0" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" dependencies = [ + "itoa", "serde", ] [[package]] name = "serde_spanned" -version = "0.6.1" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0efd8caf556a6cebd3b285caf480045fcc1ac04f6bd786b09a6f11af30c4fcf4" +checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1" dependencies = [ "serde", ] @@ -1676,7 +1876,7 @@ dependencies = [ "proc-macro2", "quote", "serde", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] @@ -1693,11 +1893,11 @@ dependencies = [ [[package]] name = "serde_yaml" -version = "0.9.19" +version = "0.9.34+deprecated" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f82e6c8c047aa50a7328632d067bcae6ef38772a79e28daf32f735e0e4f3dd10" +checksum = "6a8b1a1a2ebf674015cc02edccce75287f1a0130d394307b36743c2f5d504b47" dependencies = [ - "indexmap", + "indexmap 2.2.6", "itoa", "ryu", "serde", @@ -1706,9 +1906,9 @@ dependencies = [ [[package]] name = "sha1" -version = "0.10.5" +version = "0.10.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f04293dc80c3993519f2d7f6f511707ee7094fe0c6d3406feb330cdb3540eba3" +checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" dependencies = [ "cfg-if", "cpufeatures", @@ -1717,24 +1917,27 @@ dependencies = [ [[package]] name = "signal-hook-registry" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0" +checksum = "d8229b473baa5980ac72ef434c4415e70c4b5e71b423043adb4ba059f89c99a1" dependencies = [ "libc", ] [[package]] name = "similar" -version = "2.2.1" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "slab" -version = "0.4.5" +version = "0.4.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5" +checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" +dependencies = [ + "autocfg", +] [[package]] name = "slog" @@ -1744,9 +1947,9 @@ checksum = "8347046d4ebd943127157b94d63abb990fcf729dc4e9978927fdf4ac3c998d06" [[package]] name = "slog-async" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "766c59b252e62a34651412870ff55d8c4e6d04df19b43eecb2703e417b097ffe" +checksum = "72c8038f898a2c79507940990f05386455b3a317d8f18d4caea7cbc3d5096b84" dependencies = [ "crossbeam-channel", "slog", @@ -1756,14 +1959,14 @@ dependencies = [ [[package]] name = "slog-bunyan" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "440fd32d0423c31e4f98d76c0b62ebdb847f905aa07357197e9b41ac620af97d" +checksum = "dcaaf6e68789d3f0411f1e72bc443214ef252a1038b6e344836e50442541f190" dependencies = [ - "hostname", + "hostname 0.3.1", "slog", "slog-json", - "time 0.3.7", + "time", ] [[package]] @@ -1775,49 +1978,55 @@ dependencies = [ "serde", "serde_json", "slog", - "time 0.3.7", + "time", ] [[package]] name = "slog-term" -version = "2.9.0" +version = "2.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87d29185c55b7b258b4f120eab00f48557d4d9bc814f41713f449d35b0f8977c" +checksum = "b6e022d0b998abfe5c3782c1f03551a596269450ccd677ea51c56f8b214610e8" dependencies = [ - "atty", + "is-terminal", "slog", "term", "thread_local", - "time 0.3.7", + "time", ] [[package]] name = "smallvec" -version = "1.8.0" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "socket2" -version = "0.4.7" +version = "0.5.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02e2d2db9033d13a1567121ddd7a095ee144db4e1ca1b1bda3419bc0da294ebd" +checksum = "05ffd9c0a93b7543e062e759284fcf5f5e3b098501104bfbdde4d404db792871" dependencies = [ "libc", - "winapi", + "windows-sys 0.52.0", ] [[package]] name = "spin" -version = "0.5.2" +version = "0.9.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] name = "strsim" -version = "0.10.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + +[[package]] +name = "subtle" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" [[package]] name = "syn" @@ -1832,15 +2041,42 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.8" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc02725fd69ab9f26eab07fad303e2497fad6fb9eba4f96c4d1687bdf704ad9" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "system-configuration" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" +dependencies = [ + "bitflags 1.3.2", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "take_mut" version = "0.2.2" @@ -1849,16 +2085,14 @@ checksum = "f764005d11ee5f36500a149ace24e00e3da98b0158b3e2d53a7495660d3f4d60" [[package]] name = "tempfile" -version = "3.3.0" +version = "3.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "85b77fafb263dd9d05cbeac119526425676db3784113aa9295c88498cbf8bff1" dependencies = [ "cfg-if", "fastrand", - "libc", - "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.52.0", ] [[package]] @@ -1872,123 +2106,119 @@ dependencies = [ "winapi", ] -[[package]] -name = "termcolor" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755" -dependencies = [ - "winapi-util", -] - [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.8", + "syn 2.0.58", ] [[package]] name = "thread_local" -version = "1.1.4" +version = "1.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" dependencies = [ + "cfg-if", "once_cell", ] [[package]] name = "time" -version = "0.1.43" +version = "0.3.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca8a50ef2360fbd1eeb0ecd46795a87a19024eb4b53c5dc916ca1fd95fe62438" -dependencies = [ - "libc", - "winapi", -] - -[[package]] -name = "time" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "004cbc98f30fa233c61a38bc77e96a9106e65c88f2d3bef182ae952027e5753d" +checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" dependencies = [ + "deranged", "itoa", "libc", + "num-conv", "num_threads", + "powerfmt", + "serde", + "time-core", "time-macros", ] +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + [[package]] name = "time-macros" -version = "0.2.3" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25eb0ca3468fc0acc11828786797f6ef9aa1555e4a211a60d64cc8e4d1be47d6" +checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +dependencies = [ + "num-conv", + "time-core", +] [[package]] name = "tinyvec" -version = "1.5.1" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" dependencies = [ "tinyvec_macros", ] [[package]] name = "tinyvec_macros" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.19.2" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c51a52ed6686dd62c320f9b89299e9dfb46f730c7a48e635c19f21d116cb1439" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ + "backtrace", "bytes", "libc", - "memchr", "mio", "num_cpus", - "once_cell", "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "winapi", + "windows-sys 0.48.0", ] [[package]] name = "tokio-macros" -version = "1.7.0" +version = "2.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", ] [[package]] name = "tokio-native-tls" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d995660bd2b7f8c1568414c1126076c13fbb725c40112dc0120b78eb9b717b" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" dependencies = [ "native-tls", "tokio", @@ -1996,34 +2226,20 @@ dependencies = [ [[package]] name = "tokio-rustls" -version = "0.23.4" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c43ee83903113e03984cb9e5cebe6c04a5116269e900e3ddba8f068a62adda59" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", - "tokio", - "webpki", -] - -[[package]] -name = "tokio-util" -version = "0.6.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e99e1983e5d376cd8eb4b66604d2e99e79f5bd988c3055891dcd8c9e2604cc0" -dependencies = [ - "bytes", - "futures-core", - "futures-sink", - "log", - "pin-project-lite", + "rustls-pki-types", "tokio", ] [[package]] name = "tokio-util" -version = "0.7.3" +version = "0.7.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc463cd8deddc3770d20f9852143d50bf6094e640b485cb2e189a2099085ff45" +checksum = "5419f34732d9eb6ee4c3578b7989078579b7f039cbbb9ca2c4da015749371e15" dependencies = [ "bytes", "futures-core", @@ -2035,102 +2251,117 @@ dependencies = [ [[package]] name = "toml" -version = "0.5.9" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d82e1a7758622a465f8cee077614c73484dac5b836c02ff6a40d5d1010324d7" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", + "serde_spanned", + "toml_datetime", + "toml_edit 0.19.15", ] [[package]] name = "toml" -version = "0.7.3" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b403acf6f2bb0859c93c7f0d967cb4a75a7ac552100f9322faf64dc047669b21" +checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3" dependencies = [ "serde", "serde_spanned", "toml_datetime", - "toml_edit", + "toml_edit 0.22.9", ] [[package]] name = "toml_datetime" -version = "0.6.1" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" +dependencies = [ + "serde", +] + +[[package]] +name = "toml_edit" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ + "indexmap 2.2.6", "serde", + "serde_spanned", + "toml_datetime", + "winnow 0.5.40", ] [[package]] name = "toml_edit" -version = "0.19.8" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ - "indexmap", + "indexmap 2.2.6", "serde", "serde_spanned", "toml_datetime", - "winnow", + "winnow 0.6.5", ] [[package]] name = "toolchain_find" -version = "0.2.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e85654a10e7a07a47c6f19d93818f3f343e22927f2fa280c84f7c8042743413" +checksum = "ebc8c9a7f0a2966e1acdaf0461023d0b01471eeead645370cf4c3f5cff153f2a" dependencies = [ "home", - "lazy_static", + "once_cell", "regex", - "semver 0.11.0", + "semver", "walkdir", ] [[package]] name = "tower-service" -version = "0.3.1" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "360dfd1d6d30e05fda32ace2c8c70e9c0a9da713275777f5a4dbb8a1893930c6" +checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" [[package]] name = "tracing" -version = "0.1.29" +version = "0.1.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "375a639232caf30edfc78e8d89b2d4c375515393e7af7e16f01cd96917fb2105" +checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ - "cfg-if", "pin-project-lite", "tracing-core", ] [[package]] name = "tracing-core" -version = "0.1.21" +version = "0.1.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f4ed65637b8390770814083d20756f87bfa2c21bf2f110babdc5438351746e4" +checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ - "lazy_static", + "once_cell", ] [[package]] name = "try-lock" -version = "0.2.3" +version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59547bce71d9c38b83d9c0e92b6066c4253371f15005def0c30d9657f50c7642" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" [[package]] name = "typenum" -version = "1.15.0" +version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" [[package]] name = "typify" -version = "0.0.12-dev" -source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4" +version = "0.0.16" +source = "git+https://github.com/oxidecomputer/typify#23aced7c78a632bfd29b58cbd3bc2709f536c3a3" dependencies = [ "typify-impl", "typify-macro", @@ -2138,25 +2369,25 @@ dependencies = [ [[package]] name = "typify-impl" -version = "0.0.12-dev" -source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4" +version = "0.0.16" +source = "git+https://github.com/oxidecomputer/typify#23aced7c78a632bfd29b58cbd3bc2709f536c3a3" dependencies = [ - "heck", + "heck 0.5.0", "log", "proc-macro2", "quote", - "regress", + "regress 0.9.1", "schemars", "serde_json", - "syn 2.0.8", + "syn 2.0.58", "thiserror", "unicode-ident", ] [[package]] name = "typify-macro" -version = "0.0.12-dev" -source = "git+https://github.com/oxidecomputer/typify#fec09bd1924a676ac18eecc0dea64b8ec042fdb4" +version = "0.0.16" +source = "git+https://github.com/oxidecomputer/typify#23aced7c78a632bfd29b58cbd3bc2709f536c3a3" dependencies = [ "proc-macro2", "quote", @@ -2164,87 +2395,86 @@ dependencies = [ "serde", "serde_json", "serde_tokenstream", - "syn 2.0.8", + "syn 2.0.58", "typify-impl", ] -[[package]] -name = "ucd-trie" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c" - [[package]] name = "unicase" -version = "2.6.0" +version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" dependencies = [ "version_check", ] [[package]] name = "unicode-bidi" -version = "0.3.7" +version = "0.3.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" [[package]] name = "unicode-ident" -version = "1.0.8" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-normalization" -version = "0.1.19" +version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9" +checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" dependencies = [ "tinyvec", ] [[package]] name = "unicode-segmentation" -version = "1.10.0" +version = "1.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fdbf052a0783de01e944a6ce7a8cb939e295b1e7be835a1112c3b9a7f047a5a" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" [[package]] name = "unicode-width" -version = "0.1.9" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973" +checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" [[package]] name = "unsafe-libyaml" -version = "0.2.7" +version = "0.2.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2024452afd3874bf539695e04af6732ba06517424dbf958fdb16a01f3bef6c" +checksum = "673aac59facbab8a9007c7f6108d11f63b603f7cabff99fabf650fea5c32b861" [[package]] name = "untrusted" -version = "0.7.1" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a156c684c91ea7d62626509bce3cb4e1d9ed5c4d978f7b4352658f96a4c26b4a" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" [[package]] name = "url" -version = "2.2.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507c383b2d33b5fc35d1861e77e6b383d158b2da5e14fe51b83dfedf6fd578c" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" dependencies = [ "form_urlencoded", "idna", - "matches", "percent-encoding", ] +[[package]] +name = "utf8parse" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "711b9620af191e0cdc7468a8d14e709c3dcdb115b36f838e601583af800a370a" + [[package]] name = "uuid" -version = "1.3.0" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1674845326ee10d37ca60470760d4288a6f80f304007d92e5c53bab78c9cfd79" +checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" dependencies = [ "getrandom", "serde", @@ -2262,33 +2492,34 @@ version = "0.9.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" +[[package]] +name = "waitgroup" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1f50000a783467e6c0200f9d10642f4bc424e39efc1b770203e88b488f79292" +dependencies = [ + "atomic-waker", +] + [[package]] name = "walkdir" -version = "2.3.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56" +checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" dependencies = [ "same-file", - "winapi", "winapi-util", ] [[package]] name = "want" -version = "0.3.0" +version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ce8a968cb1cd110d136ff8b819a556d6fb6d919363c61534f6860c7eb172ba0" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" dependencies = [ - "log", "try-lock", ] -[[package]] -name = "wasi" -version = "0.10.2+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" - [[package]] name = "wasi" version = "0.11.0+wasi-snapshot-preview1" @@ -2297,9 +2528,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.82" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fc7652e3f6c4706c8d9cd54832c4a4ccb9b5336e2c3bd154d5cccfbf1c1f5f7d" +checksum = "4be2531df63900aeb2bca0daaaddec08491ee64ceecbee5076636a3b026795a8" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -2307,24 +2538,24 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.82" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "662cd44805586bd52971b9586b1df85cdbbd9112e4ef4d8f41559c334dc6ac3f" +checksum = "614d787b966d3989fa7bb98a654e369c762374fd3213d212cfc0251257e747da" dependencies = [ "bumpalo", "log", "once_cell", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-futures" -version = "0.4.28" +version = "0.4.42" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" +checksum = "76bc14366121efc8dbb487ab05bcc9d346b3b5ec0eaa76e46594cabbe51762c0" dependencies = [ "cfg-if", "js-sys", @@ -2334,9 +2565,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.82" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b260f13d3012071dfb1512849c033b1925038373aea48ced3012c09df952c602" +checksum = "a1f8823de937b71b9460c0c34e25f3da88250760bec0ebac694b49997550d726" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -2344,28 +2575,28 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.82" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5be8e654bdd9b79216c2929ab90721aa82faf65c48cdf08bdc4e7f51357b80da" +checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.82" +version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6598dd0bd3c7d51095ff6531a5b23e02acdc81804e30d8f07afb77b7215a140a" +checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" [[package]] name = "wasm-streams" -version = "0.2.3" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bbae3363c08332cadccd13b67db371814cd214c2524020932f0804b8cf7c078" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -2376,24 +2607,14 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.55" +version = "0.3.69" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" +checksum = "77afa9a11836342370f4817622a2f0f418b134426d91a82dfb48f532d2ec13ef" dependencies = [ "js-sys", "wasm-bindgen", ] -[[package]] -name = "webpki" -version = "0.22.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f095d78192e208183081cc07bc5515ef55216397af48b873e5edcd72637fa1bd" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "winapi" version = "0.3.9" @@ -2412,9 +2633,9 @@ checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" [[package]] name = "winapi-util" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178" +checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" dependencies = [ "winapi", ] @@ -2426,162 +2647,206 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] -name = "windows-sys" -version = "0.32.0" +name = "windows" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e48a53791691ab099e5e2ad123536d0fff50652600abaf43bbf952894110d0be" +dependencies = [ + "windows-core", + "windows-targets 0.52.4", +] + +[[package]] +name = "windows-core" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" dependencies = [ - "windows_aarch64_msvc 0.32.0", - "windows_i686_gnu 0.32.0", - "windows_i686_msvc 0.32.0", - "windows_x86_64_gnu 0.32.0", - "windows_x86_64_msvc 0.32.0", + "windows-targets 0.52.4", ] [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.48.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" dependencies = [ - "windows_aarch64_msvc 0.36.1", - "windows_i686_gnu 0.36.1", - "windows_i686_msvc 0.36.1", - "windows_x86_64_gnu 0.36.1", - "windows_x86_64_msvc 0.36.1", + "windows-targets 0.48.5", ] [[package]] name = "windows-sys" -version = "0.42.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc 0.42.0", + "windows-targets 0.52.4", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" +name = "windows-targets" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.32.0" +name = "windows-targets" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5" +checksum = "7dd37b7e5ab9018759f893a1952c9420d060016fc19a472b4bb20d1bdd694d1b" +dependencies = [ + "windows_aarch64_gnullvm 0.52.4", + "windows_aarch64_msvc 0.52.4", + "windows_i686_gnu 0.52.4", + "windows_i686_msvc 0.52.4", + "windows_x86_64_gnu 0.52.4", + "windows_x86_64_gnullvm 0.52.4", + "windows_x86_64_msvc 0.52.4", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.36.1" +name = "windows_aarch64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" [[package]] -name = "windows_aarch64_msvc" -version = "0.42.0" +name = "windows_aarch64_gnullvm" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "bcf46cf4c365c6f2d1cc93ce535f2c8b244591df96ceee75d8e83deb70a9cac9" [[package]] -name = "windows_i686_gnu" -version = "0.32.0" +name = "windows_aarch64_msvc" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" [[package]] -name = "windows_i686_gnu" -version = "0.36.1" +name = "windows_aarch64_msvc" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "da9f259dd3bcf6990b55bffd094c4f7235817ba4ceebde8e6d11cd0c5633b675" [[package]] name = "windows_i686_gnu" -version = "0.42.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" [[package]] -name = "windows_i686_msvc" -version = "0.32.0" +name = "windows_i686_gnu" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172" +checksum = "b474d8268f99e0995f25b9f095bc7434632601028cf86590aea5c8a5cb7801d3" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" [[package]] name = "windows_i686_msvc" -version = "0.42.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "1515e9a29e5bed743cb4415a9ecf5dfca648ce85ee42e15873c3cd8610ff8e02" [[package]] name = "windows_x86_64_gnu" -version = "0.32.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "5eee091590e89cc02ad514ffe3ead9eb6b660aedca2183455434b93546371a03" [[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" +name = "windows_x86_64_gnullvm" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "77ca79f2451b49fa9e2af39f0747fe999fcda4f5e241b2898624dca97a1f2177" [[package]] name = "windows_x86_64_msvc" -version = "0.32.0" +version = "0.48.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.52.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "32b752e52a2da0ddfbdbcc6fceadfeede4c939ed16d13e648833a61dfb611ed8" [[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" +name = "winnow" +version = "0.5.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" +dependencies = [ + "memchr", +] [[package]] name = "winnow" -version = "0.4.0" +version = "0.6.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "deac0939bd6e4f24ab5919fbf751c97a8cfc8543bb083a305ed5c0c10bb241d1" +checksum = "dffa400e67ed5a4dd237983829e66475f0a4a26938c4b04c21baede6262215b8" dependencies = [ "memchr", ] [[package]] name = "winreg" -version = "0.10.1" +version = "0.50.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80d0f4e272c85def139476380b12f9ac60926689dd2e01d4923222f40580869d" +checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" dependencies = [ - "winapi", + "cfg-if", + "windows-sys 0.48.0", ] + +[[package]] +name = "zerocopy" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" +dependencies = [ + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" diff --git a/Cargo.toml b/Cargo.toml index 5539eed9f..c596d179f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,10 +11,10 @@ members = [ #[patch."https://github.com/oxidecomputer/dropshot"] #dropshot = { path = "../dropshot/dropshot" } -#[patch."https://github.com/oxidecomputer/typify"] -#typify = { path = "../typify/typify" } +# [patch."https://github.com/oxidecomputer/typify"] +# typify = { path = "../typify/typify" } -#[patch.crates-io] +# [patch.crates-io] #serde_tokenstream = { path = "../serde_tokenstream" } -#typify = { path = "../typify/typify" } +# typify = { path = "../typify/typify" } #rustfmt-wrapper = { path = "../rustfmt-wrapper" } diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index ce0a83d9b..8baf3b870 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut}; use bytes::Bytes; use futures_core::Stream; -use reqwest::RequestBuilder; +use reqwest::{multipart::Part, RequestBuilder}; use serde::{de::DeserializeOwned, Serialize}; type InnerByteStream = diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 238f3a858..cb0406aff 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -3,7 +3,7 @@ use std::collections::{HashMap, HashSet}; use openapiv3::OpenAPI; -use indexmap::{IndexMap, IndexSet}; +use indexmap::IndexSet; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index eba17718f..428627455 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -6,11 +6,11 @@ use std::{ str::FromStr, }; -use indexmap::{IndexMap, IndexSet}; +use indexmap::IndexSet; use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; -use typify::{TypeId, TypeSpace, TypeSpacePatch}; +use typify::{TypeId, TypeSpace}; use crate::{ template::PathTemplate, @@ -1288,10 +1288,10 @@ impl Generator { .ok()? .details() { - typify::TypeDetails::Array(item) => { + typify::TypeDetails::Array(item, _) => { Some(DropshotPagination { item }) } - _ => None, + _ => None } } @@ -1568,22 +1568,6 @@ impl Generator { } } } - OperationParameterType::Form(_type_id) => { - let err_msg = format!( - "conversion to `reqwest::Body` for {} failed", - param.name, - ); - - Ok(quote! { - pub fn #param_name(mut self, value: B) -> Self - where B: std::convert::TryInto - { - self.#param_name = value.try_into() - .map_err(|_| #err_msg.to_string()); - self - } - }) - } OperationParameterType::RawBody => { let err_msg = format!( "conversion to `reqwest::Body` for {} failed", @@ -2113,7 +2097,7 @@ impl Generator { max_length: None, }, )) if enumeration.is_empty() => { - Ok((name.to_owned())) + Ok(name.to_owned()) } schema => { Err(Error::UnexpectedFormat(format!( From cd8f72b90191b8b1aab6519fe5d04b1323c2aeff Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 09:01:08 +0200 Subject: [PATCH 07/23] add sevdesk snippet sample --- progenitor/tests/build_sevdesk.rs | 45 ++++++++++++++++ sample_openapi/sevdesk-san-sub.yaml | 80 +++++++++++++++++++++++++++++ 2 files changed, 125 insertions(+) create mode 100644 progenitor/tests/build_sevdesk.rs create mode 100644 sample_openapi/sevdesk-san-sub.yaml diff --git a/progenitor/tests/build_sevdesk.rs b/progenitor/tests/build_sevdesk.rs new file mode 100644 index 000000000..04823b8ed --- /dev/null +++ b/progenitor/tests/build_sevdesk.rs @@ -0,0 +1,45 @@ +// Copyright 2022 Oxide Computer Company + +mod positional { + use self::types::VoucherUploadFileForm; + + progenitor::generate_api!("../sample_openapi/sevdesk-san-sub.yaml"); + + fn _ignore() { + let _ = Client::new("").voucher_upload_file(&VoucherUploadFileForm { file: Some("foo".to_owned()) }); + } +} + +mod builder_untagged { + use self::types::VoucherUploadFileForm; + + progenitor::generate_api!( + spec = "../sample_openapi/sevdesk-san-sub.yaml", + interface = Builder, + tags = Merged, + ); + + fn _ignore() { + let _ = Client::new("") + .voucher_upload_file() + .body(&VoucherUploadFileForm { file: Some("foo".to_owned()) }) + .send(); + } +} + +mod builder_tagged { + use self::types::VoucherUploadFileForm; + + progenitor::generate_api!( + spec = "../sample_openapi/sevdesk-san-sub.yaml", + interface = Builder, + tags = Separate, + ); + + fn _ignore() { + let _ = Client::new("") + .voucher_upload_file() + .body(&VoucherUploadFileForm { file: Some("foo".to_owned()) }) + .send(); + } +} diff --git a/sample_openapi/sevdesk-san-sub.yaml b/sample_openapi/sevdesk-san-sub.yaml new file mode 100644 index 000000000..f057c3737 --- /dev/null +++ b/sample_openapi/sevdesk-san-sub.yaml @@ -0,0 +1,80 @@ +# subset sample of +openapi: 3.0.0 +info: + title: sevDesk API + description: "Contact: To contact our support click here

\r\n# General information\r\nWelcome to our API!
\r\nsevDesk offers you the possibility of retrieving data using an interface, namely the sevDesk API, and making changes without having to use the web UI. The sevDesk interface is a REST-Full API. All sevDesk data and functions that are used in the web UI can also be controlled through the API.\r\n\n# Cross-Origin Resource Sharing\r\nThis API features Cross-Origin Resource Sharing (CORS).
\r\nIt enables cross-domain communication from the browser.
\r\nAll responses have a wildcard same-origin which makes them completely public and accessible to everyone, including any code on any site.\r\n\r\n# Embedding resources\r\nWhen retrieving resources by using this API, you might encounter nested resources in the resources you requested.
\r\nFor example, an invoice always contains a contact, of which you can see the ID and the object name.
\r\nThis API gives you the possibility to embed these resources completely into the resources you originally requested.
\r\nTaking our invoice example, this would mean, that you would not only see the ID and object name of a contact, but rather the complete contact resource.\r\n\r\nTo embed resources, all you need to do is to add the query parameter 'embed' to your GET request.
\r\nAs values, you can provide the name of the nested resource.
\r\nMultiple nested resources are also possible by providing multiple names, separated by a comma.\r\n \n# Authentication and Authorization\n The sevDesk API uses a token authentication to authorize calls. For this purpose every sevDesk administrator has one API token, which is a hexadecimal string containing 32 characters. The following clip shows where you can find the API token if this is your first time with our API.


The token will be needed in every request that you want to send and needs to be attached to the request url as a Query Parameter
or provided as a value of an Authorization Header.
For security reasons, we suggest putting the API Token in the Authorization Header and not in the query string.
However, in the request examples in this documentation, we will keep it in the query string, as it is easier for you to copy them and try them yourself.
The following url is an example that shows where your token needs to be placed as a query parameter.
In this case, we used some random API token.
  • https://my.sevdesk.de/api/v1/Contact?token=b7794de0085f5cd00560f160f290af38
The next example shows the token in the Authorization Header.
  • \"Authorization\" :\"b7794de0085f5cd00560f160f290af38\"
The api tokens have an infinite lifetime and, in other words, exist as long as the sevDesk user exists.
For this reason, the user should NEVER be deleted.
If really necessary, it is advisable to save the api token as we will NOT be able to retrieve it afterwards!
It is also possible to generate a new API token, for example, if you want to prevent the usage of your sevDesk account by other people who got your current API token.
To achieve this, you just need to click on the \"generate new\" symbol to the right of your token and confirm it with your password. \n# API News\n To never miss API news and updates again, subscribe to our free API newsletter and get all relevant information to keep your sevDesk software running smoothly. To subscribe, simply click here and confirm the email address to which we may send all updates relevant to you. \n# API Requests\n In our case, REST API requests need to be build by combining the following components.
Component Description
HTTP-Methods
  • GET (retrieve a resource)
  • POST (create a resource)
  • PUT (update a resource)
  • DELETE (delete a resource)
URL of the API https://my.sevdesk.de/api/v1
URI of the resource The resource to query.
For example contacts in sevDesk:

/Contact

Which will result in the following complete URL:

https://my.sevdesk.de/api/v1/Contact
Query parameters Are attached by using the connectives ? and & in the URL.
Request headers Typical request headers are for example:

  • Content-type
  • Authorization
  • Accept-Encoding
  • User-Agent
  • X-Version: Used for resource versioning see information below
  • ...
Response headers Typical response headers are for example:

  • Deprecation: If a resource is deprecated we return true or a timestamp since when
  • ...
Request body Mostly required in POST and PUT requests.
Often the request body contains json, in our case, it also accepts url-encoded data.

Note: please pass a meaningful entry at the header \"User-Agent\". If the \"User-Agent\" is set meaningfully, we can offer better support in case of queries from customers.
An example how such a \"User-Agent\" can look like: \"Integration-name by abc\".

This is a sample request for retrieving existing contacts in sevDesk using curl:

Get Request

As you can see, the request contains all the components mentioned above.
It's HTTP method is GET, it has a correct endpoint (https://my.sevdesk.de/api/v1/Contact), query parameters like token and additional header information!

Query Parameters

As you might have seen in the sample request above, there are several other parameters besides \"token\", located in the url.
Those are mostly optional but prove to be very useful for a lot of requests as they can limit, extend, sort or filter the data you will get as a response.

These are the three most used query parameter for the sevDesk API.
Parameter Description
limit Limits the number of entries that are returned.
Most useful in GET requests which will most likely deliver big sets of data like country or currency lists.
In this case, you can bypass the default limitation on returned entries by providing a high number.
offset Specifies a certain offset for the data that will be returned.
As an example, you can specify \"offset=2\" if you want all entries except for the first two.
embed Will extend some of the returned data.
A brief example can be found below.
countAll \"countAll=true\" returns the number of items
This is an example for the usage of the embed parameter.
The following first request will return all company contact entries in sevDesk up to a limit of 100 without any additional information and no offset.



Now have a look at the category attribute located in the response.
Naturally, it just contains the id and the object name of the object but no further information.
We will now use the parameter embed with the value \"category\".



As you can see, the category object is now extended and shows all the attributes and their corresponding values.

There are lot of other query parameters that can be used to filter the returned data for objects that match a certain pattern, however, those will not be mentioned here and instead can be found at the detail documentation of the most used API endpoints like contact, invoice or voucher.

Request Headers

The HTTP request (response) headers allow the client as well as the server to pass additional information with the request.
They transfer the parameters and arguments which are important for transmitting data over HTTP.
Three headers which are useful / necessary when using the sevDesk API are \"Authorization, \"Accept\" and \"Content-type\".
Underneath is a brief description of why and how they should be used.

Authorization

Can be used if you want to provide your API token in the header instead of having it in the url.
  • Authorization:yourApiToken
Accept

Specifies the format of the response.
Required for operations with a response body.
  • Accept:application/format
In our case, format could be replaced with json or xml

Content-type

Specifies which format is used in the request.
Is required for operations with a request body.
  • Content-type:application/format
In our case,formatcould be replaced with json or x-www-form-urlencoded

API Responses

HTTP status codes
When calling the sevDesk API it is very likely that you will get a HTTP status code in the response.
Some API calls will also return JSON response bodies which will contain information about the resource.
Each status code which is returned will either be a success code or an error code.

Success codes
Status code Description
200 OK The request was successful
201 Created Most likely to be found in the response of a POST request.
This code indicates that the desired resource was successfully created.

Error codes
Status code Description
400 Bad request The request you sent is most likely syntactically incorrect.
You should check if the parameters in the request body or the url are correct.
401 Unauthorized The authentication failed.
Most likely caused by a missing or wrong API token.
403 Forbidden You do not have the permission the access the resource which is requested.
404 Not found The resource you specified does not exist.
500 Internal server error An internal server error has occurred.
Normally this means that something went wrong on our side.
However, sometimes this error will appear if we missed to catch an error which is normally a 400 status code!


Resource Versioning

We use resource versioning to handle breaking changes for our endpoints, these are rarely used and will be communicated before we remove older versions.
To call a different version we use a specific header X-Version that should be filled with the desired version.
  • If you do not specify any version we assume default
  • If you specify a version that does not exist or was removed, you will get an error with information which versions are available
X-Version Description
default Should always reference the oldest version.
If a specific resource is updated with a new version,
then the default version stays the same until the old version is deleted
1.0 ... 1.9 Our incrementally version for each resource independent
Important: A resource can be available via default but not 1.0
\n# Your First Request\n After reading the introduction to our API, you should now be able to make your first call.
For testing our API, we would always recommend to create a trial account for sevDesk to prevent unwanted changes to your main account.
A trial account will be in the highest tariff (materials management), so every sevDesk function can be tested!

To start testing we would recommend one of the following tools: This example will illustrate your first request, which is creating a new Contact in sevDesk.
  1. Download Postman for your desired system and start the application
  2. Enter https://my.sevdesk.de/api/v1/Contact as the url
  3. Use the connective ? to append token= to the end of the url, or create an authorization header. Insert your API token as the value
  4. For this test, select POST as the HTTP method
  5. Go to Headers and enter the key-value pair Content-type + application/x-www-form-urlencoded
    As an alternative, you can just go to Body and select x-www-form-urlencoded
  6. Now go to Body (if you are not there yet) and enter the key-value pairs as shown in the following picture



  7. Click on Send. Your response should now look like this:

As you can see, a successful response in this case returns a JSON-formatted response body containing the contact you just created.
For keeping it simple, this was only a minimal example of creating a contact.
There are however numerous combinations of parameters that you can provide which add information to your contact." + version: 2.0.0 + x-logo: + url: https://my.sevdesk.de/img/logos/1_100.png + backgroundColor: '#263241' +servers: + - url: https://my.sevdesk.de/api/v1 + description: Our main application instance which most of our customers work with +security: + - api_key: [] +paths: + /Voucher/Factory/uploadTempFile: + post: + tags: + - Voucher + summary: Upload voucher file + description: >- + To attach a document to a voucher, you will need to upload it first for + later use.
To do this, you can use this request.
When you + successfully uploaded the file, you will get a sevDesk internal filename + in the response.
The filename will be a hash generated from your + uploaded file. You will need it in the next request!
After you got + the just mentioned filename, you can enter it as a value for the + filename parameter of the saveVoucher request.
If you provided all + necessary parameters and kept all of them in the right order, the file + will be attached to your voucher. + operationId: voucherUploadFile + requestBody: + description: File to upload + content: + form-data: + schema: + properties: + file: + description: The file to upload + type: string + format: binary + type: object + responses: + '201': + description: A pdf file + content: + application/json: + schema: + type: object + properties: + objects: + type: object + properties: + pages: + type: number + example: 1 + mimeType: + type: string + example: image/jpg + originMimeType: + type: string + example: application/pdf + filename: + type: string + example: f019bec36c65f5a0e7d2c63cc33f0681.pdf + contentHash: + type: string + example: >- + 1998dea8c6e9e489139caf896690641c0ea065ce5770b51cf2a4d10797f99685 + content: + type: array + items: + example: null + '400': + description: Bad request + '401': + description: Authentication required + '500': + description: Server Error + deprecated: false From b347eaae1cdcdeaac6093a132680eb6b24ecebc1 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 09:01:46 +0200 Subject: [PATCH 08/23] introduce expander for easier debugging --- Cargo.lock | 45 +++++++++++++++++++++++++++++++++++++ progenitor-macro/Cargo.toml | 1 + progenitor-macro/src/lib.rs | 4 ++++ progenitor/Cargo.toml | 1 + 4 files changed, 51 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 8f2dc39b6..d84bcf482 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -197,6 +197,15 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -412,6 +421,7 @@ checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer", "crypto-common", + "subtle", ] [[package]] @@ -561,6 +571,20 @@ dependencies = [ "uuid", ] +[[package]] +name = "expander" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "00e83c02035136f1592a47964ea60c05a50e4ed8b5892cfac197063850898d4d" +dependencies = [ + "blake2", + "fs-err", + "prettier-please", + "proc-macro2", + "quote", + "syn 2.0.58", +] + [[package]] name = "expectorate" version = "1.1.0" @@ -608,6 +632,15 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs-err" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a41f105fe1d5b6b34b2055e3dc59bb79b46b48b2040b9e6c7b4b5de097aa41" +dependencies = [ + "autocfg", +] + [[package]] name = "futures" version = "0.3.30" @@ -1367,6 +1400,16 @@ version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" +[[package]] +name = "prettier-please" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22020dfcf177fcc7bf5deaf7440af371400c67c0de14c399938d8ed4fb4645d3" +dependencies = [ + "proc-macro2", + "syn 2.0.58", +] + [[package]] name = "proc-macro2" version = "1.0.79" @@ -1385,6 +1428,7 @@ dependencies = [ "built", "chrono", "clap", + "expander", "futures", "openapiv3 1.0.4", "percent-encoding", @@ -1446,6 +1490,7 @@ dependencies = [ name = "progenitor-macro" version = "0.2.1-dev" dependencies = [ + "expander", "openapiv3 1.0.4", "proc-macro2", "progenitor-impl", diff --git a/progenitor-macro/Cargo.toml b/progenitor-macro/Cargo.toml index 813c7a53e..f62508e33 100644 --- a/progenitor-macro/Cargo.toml +++ b/progenitor-macro/Cargo.toml @@ -21,3 +21,4 @@ serde_json = "1.0" serde_yaml = "0.9" serde_tokenstream = "0.2.0" syn = { version = "2.0", features = ["full", "extra-traits"] } +expander = "2" diff --git a/progenitor-macro/src/lib.rs b/progenitor-macro/src/lib.rs index 5231d8194..247e2c766 100644 --- a/progenitor-macro/src/lib.rs +++ b/progenitor-macro/src/lib.rs @@ -271,5 +271,9 @@ fn do_generate_api(item: TokenStream) -> Result { const _: &str = include_str!(#path_str); }; + let output = expander::Expander::new(format!("{}", std::path::PathBuf::from(spec.value()).file_name().unwrap().to_string_lossy())) + .fmt(expander::Edition::_2021) + .verbose(true) + .write_to_out_dir(output).expect("Writing file works. qed"); Ok(output.into()) } diff --git a/progenitor/Cargo.toml b/progenitor/Cargo.toml index 6fbbbe983..6a1d5d168 100644 --- a/progenitor/Cargo.toml +++ b/progenitor/Cargo.toml @@ -20,6 +20,7 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_yaml = "0.9" clap = { version = "4.1.14", features = ["derive"] } +expander = "2.1.0" [build-dependencies] built = { version = "0.6", features = ["git2"] } From 5acad254d7bdeb8efd7da19cbca0fb0c06677719 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 09:02:22 +0200 Subject: [PATCH 09/23] fmt --- progenitor-client/src/progenitor_client.rs | 2 +- progenitor-impl/src/cli.rs | 24 +++++++++++----------- progenitor-impl/src/lib.rs | 2 +- progenitor-impl/src/method.rs | 2 +- progenitor-impl/src/template.rs | 7 ++++--- progenitor-macro/src/lib.rs | 18 ++++++++++++---- 6 files changed, 33 insertions(+), 22 deletions(-) diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index 8baf3b870..1b92a3e95 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -432,7 +432,7 @@ impl RequestBuilderExt for RequestBuilder { self, iter: I, ) -> Result> { - use reqwest::multipart::{Form}; + use reqwest::multipart::Form; let mut form = Form::new(); for (name, value) in iter { diff --git a/progenitor-impl/src/cli.rs b/progenitor-impl/src/cli.rs index f7d265d46..a53558f6a 100644 --- a/progenitor-impl/src/cli.rs +++ b/progenitor-impl/src/cli.rs @@ -315,19 +315,19 @@ impl Generator { OperationParameterKind::Body(_) => unreachable!(), }; - let OperationParameterType::Type(arg_type_id) = ¶m.typ else { + let OperationParameterType::Type(arg_type_id) = ¶m.typ + else { panic!() }; let arg_type = self.type_space.get_type(arg_type_id).unwrap(); let arg_details = arg_type.details(); - let arg_type_name = match &arg_details{ + let arg_type_name = match &arg_details { typify::TypeDetails::Option(opt_id) => { - let inner_type = self.type_space.get_type(opt_id).unwrap(); + let inner_type = + self.type_space.get_type(opt_id).unwrap(); inner_type.ident() } - _ => { - arg_type.ident() - } + _ => arg_type.ident(), }; let help = param.description.as_ref().map(|description| { @@ -410,21 +410,21 @@ impl Generator { let arg_name = param.name.to_kebab_case(); let arg_fn_name = sanitize(¶m.name, Case::Snake); let arg_fn = format_ident!("{}", arg_fn_name); - let OperationParameterType::Type(arg_type_id) = ¶m.typ else { + let OperationParameterType::Type(arg_type_id) = ¶m.typ + else { panic!() }; let arg_type = self.type_space.get_type(arg_type_id).unwrap(); // TODO this really should be simpler. let arg_details = arg_type.details(); - let arg_type_name = match &arg_details{ + let arg_type_name = match &arg_details { typify::TypeDetails::Option(opt_id) => { - let inner_type = self.type_space.get_type(opt_id).unwrap(); + let inner_type = + self.type_space.get_type(opt_id).unwrap(); inner_type.ident() } - _ => { - arg_type.ident() - } + _ => arg_type.ident(), }; quote! { diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index cb0406aff..66a042014 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -2,8 +2,8 @@ use std::collections::{HashMap, HashSet}; -use openapiv3::OpenAPI; use indexmap::IndexSet; +use openapiv3::OpenAPI; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 428627455..ee9e3f0c3 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -1291,7 +1291,7 @@ impl Generator { typify::TypeDetails::Array(item, _) => { Some(DropshotPagination { item }) } - _ => None + _ => None, } } diff --git a/progenitor-impl/src/template.rs b/progenitor-impl/src/template.rs index 4049f5fea..85d2dc3e7 100644 --- a/progenitor-impl/src/template.rs +++ b/progenitor-impl/src/template.rs @@ -37,9 +37,10 @@ impl PathTemplate { if let Component::Parameter(n) = &component { let param = format_ident!( "{}", - rename - .get(&n) - .unwrap_or_else(|| panic!("missing path name mapping {}", n)), + rename.get(&n).unwrap_or_else(|| panic!( + "missing path name mapping {}", + n + )), ); Some(quote! { encode_path(&#param.to_string()) diff --git a/progenitor-macro/src/lib.rs b/progenitor-macro/src/lib.rs index 247e2c766..f4517115e 100644 --- a/progenitor-macro/src/lib.rs +++ b/progenitor-macro/src/lib.rs @@ -269,11 +269,21 @@ fn do_generate_api(item: TokenStream) -> Result { // Force a rebuild when the given file is modified. const _: &str = include_str!(#path_str); + }; - let output = expander::Expander::new(format!("{}", std::path::PathBuf::from(spec.value()).file_name().unwrap().to_string_lossy())) - .fmt(expander::Edition::_2021) - .verbose(true) - .write_to_out_dir(output).expect("Writing file works. qed"); + println!("cargo::rerun-if-changed={}", path_str); + + let output = expander::Expander::new(format!( + "{}", + std::path::PathBuf::from(spec.value()) + .file_name() + .unwrap() + .to_string_lossy() + )) + .fmt(expander::Edition::_2021) + .verbose(true) + .write_to_out_dir(output) + .expect("Writing file works. qed"); Ok(output.into()) } From 1c227b9ad745c5b3aa2739d60c1e092e907df56a Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 09:29:07 +0200 Subject: [PATCH 10/23] use as_bytes to make it work --- progenitor-impl/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 66a042014..34d4ef20b 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -298,7 +298,7 @@ impl Generator { pub fn as_form<'f>(&'f self) -> impl std::iter::Iterator { [#properties] .into_iter() - .filter_map(|(name, val)| val.as_ref().map(|val| (name, val.as_slice()))) + .filter_map(|(name, val)| val.as_ref().map(|val| (name, val.as_bytes()))) } } } From 41a421b895a0648a04215cc0195efa7988108fac Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 9 Apr 2024 19:24:23 +0200 Subject: [PATCH 11/23] fix --- progenitor-impl/src/cli.rs | 3 ++- progenitor-impl/src/httpmock.rs | 7 ++++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/progenitor-impl/src/cli.rs b/progenitor-impl/src/cli.rs index e10ceae01..c0454d491 100644 --- a/progenitor-impl/src/cli.rs +++ b/progenitor-impl/src/cli.rs @@ -470,7 +470,8 @@ impl Generator { // are currently... OperationParameterType::RawBody => None, - OperationParameterType::Type(body_type_id) => { + OperationParameterType::Type(body_type_id) + | OperationParameterType::Form(body_type_id) => { Some(body_type_id) } }); diff --git a/progenitor-impl/src/httpmock.rs b/progenitor-impl/src/httpmock.rs index 1d1f19ec8..8904e7453 100644 --- a/progenitor-impl/src/httpmock.rs +++ b/progenitor-impl/src/httpmock.rs @@ -164,7 +164,8 @@ impl Generator { name, typ, kind, .. }| { let arg_type_name = match typ { - OperationParameterType::Type(arg_type_id) => self + OperationParameterType::Type(arg_type_id) + | OperationParameterType::Form(arg_type_id) => self .type_space .get_type(arg_type_id) .unwrap() @@ -245,9 +246,9 @@ impl Generator { OperationParameterKind::Header(_) => quote! { todo!() }, OperationParameterKind::Body(body_content_type) => { match typ { - OperationParameterType::Type(_) => quote! { + OperationParameterType::Type(_) + | OperationParameterType::Form(_) => quote! { Self(self.0.json_body_obj(value)) - }, OperationParameterType::RawBody => { match body_content_type { From 7eed0313442ad4bb3b1cdbd0a8b34595b37a74d4 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Thu, 11 Apr 2024 18:26:07 +0200 Subject: [PATCH 12/23] review commeents --- progenitor-impl/src/method.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index d76685779..addd5fede 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -1006,7 +1006,8 @@ impl Generator { OperationParameterType::Form(_), ) => { Some(quote! { - // This uses progenitor_client::RequestBuilderExt which sets up a simple form data based on bytes + // This uses `progenitor_client::RequestBuilderExt` which + // sets up a simple form data based on bytes .form_from_raw(body.as_form())? })}, (OperationParameterKind::Body(_), _) => { @@ -1708,10 +1709,10 @@ impl Generator { ); Ok(quote! { - pub fn #param_name(mut self, value: B) -> Self + pub fn #param_name(mut self, body: B) -> Self where B: std::convert::TryInto { - self.#param_name = value.try_into() + self.#param_name = body.try_into() .map_err(|_| #err_msg.to_string()); self } From 16ec6a0014e7cf735097f9e48c857dffe7b0c840 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Fri, 12 Apr 2024 09:17:21 +0200 Subject: [PATCH 13/23] deny rules --- Cargo.lock | 10 ---------- progenitor-impl/Cargo.toml | 1 - progenitor-impl/src/lib.rs | 3 +++ 3 files changed, 3 insertions(+), 11 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f65a582b2..af6574091 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -794,15 +794,6 @@ dependencies = [ "version_check", ] -[[package]] -name = "getopts" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" -dependencies = [ - "unicode-width", -] - [[package]] name = "getrandom" version = "0.2.14" @@ -1483,7 +1474,6 @@ dependencies = [ "dropshot", "expectorate", "futures", - "getopts", "heck", "http 0.2.12", "hyper", diff --git a/progenitor-impl/Cargo.toml b/progenitor-impl/Cargo.toml index 785ef82ac..6ea7cd040 100644 --- a/progenitor-impl/Cargo.toml +++ b/progenitor-impl/Cargo.toml @@ -10,7 +10,6 @@ readme = "../README.md" [dependencies] heck = "0.5.0" http = "0.2.9" -getopts = "0.2" indexmap = "2.2.6" openapiv3 = "2.0.0" proc-macro2 = "1.0" diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index ea220662f..54566c1eb 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -3,6 +3,9 @@ //! Core implementation for the progenitor OpenAPI client generator. #![deny(missing_docs)] +#![deny(unused_crate_dependencies)] +#![deny(unused_imports)] +#![deny(unused_import_braces)] use std::collections::{BTreeMap, HashMap, HashSet}; From 4323c11de94bc81c865868f8f10eea281bf62eb9 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Fri, 12 Apr 2024 09:24:49 +0200 Subject: [PATCH 14/23] lints --- Cargo.lock | 4 ---- cargo-progenitor/src/main.rs | 9 +++++++++ progenitor-client/src/lib.rs | 3 +++ progenitor/Cargo.toml | 4 ---- progenitor/src/lib.rs | 3 +++ 5 files changed, 15 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af6574091..7f11ff0f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1436,8 +1436,6 @@ version = "0.6.0" dependencies = [ "base64 0.22.0", "chrono", - "clap", - "expander", "futures", "percent-encoding", "progenitor-client", @@ -1448,8 +1446,6 @@ dependencies = [ "reqwest", "schemars", "serde", - "serde_json", - "serde_yaml", "tokio", "uuid", ] diff --git a/cargo-progenitor/src/main.rs b/cargo-progenitor/src/main.rs index ce965f808..e2b30d8cf 100644 --- a/cargo-progenitor/src/main.rs +++ b/cargo-progenitor/src/main.rs @@ -1,5 +1,12 @@ // Copyright 2023 Oxide Computer Company +//! The cargo tool to generate a rust representation of an OpenAPI spec. + +#![deny(missing_docs)] +#![deny(unused_crate_dependencies)] +#![deny(unused_imports)] +#![deny(unused_import_braces)] + use std::{ collections::HashMap, fs::{File, OpenOptions}, @@ -13,6 +20,7 @@ use openapiv3::OpenAPI; use progenitor::{GenerationSettings, Generator, InterfaceStyle, TagStyle}; use progenitor_impl::space_out_items; +/// Include the artifact created by `build.rs`. pub mod built_info { // The file has been placed there by the build script. include!(concat!(env!("OUT_DIR"), "/built.rs")); @@ -225,6 +233,7 @@ fn main() -> Result<()> { Ok(()) } +/// List of dependencies required when including the generated code. pub fn dependencies(builder: Generator, include_client: bool) -> Vec { let dependency_versions: HashMap = built_info::DEPENDENCIES .iter() diff --git a/progenitor-client/src/lib.rs b/progenitor-client/src/lib.rs index f7ca0f66b..cac85ce82 100644 --- a/progenitor-client/src/lib.rs +++ b/progenitor-client/src/lib.rs @@ -3,6 +3,9 @@ //! Support for generated clients. #![deny(missing_docs)] +#![deny(unused_crate_dependencies)] +#![deny(unused_imports)] +#![deny(unused_import_braces)] mod progenitor_client; diff --git a/progenitor/Cargo.toml b/progenitor/Cargo.toml index 99f946eec..c71119aa3 100644 --- a/progenitor/Cargo.toml +++ b/progenitor/Cargo.toml @@ -13,10 +13,6 @@ categories = ["api-bindings", "compilers"] progenitor-client = { version = "0.6.0", path = "../progenitor-client" } progenitor-impl = { version = "0.6.0", path = "../progenitor-impl" } progenitor-macro = { version = "0.6.0", path = "../progenitor-macro" } -serde_json = "1.0" -serde_yaml = "0.9" -clap = { version = "4.1.14", features = ["derive"] } -expander = "2.1.0" [dev-dependencies] base64 = "0.22" diff --git a/progenitor/src/lib.rs b/progenitor/src/lib.rs index 8416e4424..fa2bf6f93 100644 --- a/progenitor/src/lib.rs +++ b/progenitor/src/lib.rs @@ -11,6 +11,9 @@ //! README](https://github.com/oxidecomputer/progenitor/blob/main/README.md) #![deny(missing_docs)] +#![deny(unused_crate_dependencies)] +#![deny(unused_imports)] +#![deny(unused_import_braces)] pub use progenitor_client; pub use progenitor_impl::Error; From b57f58a7f4f19f6102044ca020be7930ade7fedd Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Fri, 12 Apr 2024 16:57:29 +0200 Subject: [PATCH 15/23] fix --- Cargo.lock | 16 ++++++++++------ progenitor-impl/src/lib.rs | 4 ++-- .../tests/output/src/buildomat_builder.rs | 2 +- .../tests/output/src/buildomat_builder_tagged.rs | 4 ++-- .../tests/output/src/nexus_builder.rs | 4 ++-- .../tests/output/src/nexus_builder_tagged.rs | 4 ++-- progenitor/Cargo.toml | 4 ++++ progenitor/src/lib.rs | 2 +- 8 files changed, 24 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f11ff0f6..36608457e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -137,9 +137,9 @@ dependencies = [ [[package]] name = "async-trait" -version = "0.1.79" +version = "0.1.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" +checksum = "c6fa2087f2753a7da8cc1c0dbfcf89579dd57458e36769de5ac750b4671737ca" dependencies = [ "proc-macro2", "quote", @@ -1436,6 +1436,8 @@ version = "0.6.0" dependencies = [ "base64 0.22.0", "chrono", + "clap", + "expander", "futures", "percent-encoding", "progenitor-client", @@ -1446,6 +1448,8 @@ dependencies = [ "reqwest", "schemars", "serde", + "serde_json", + "serde_yaml", "tokio", "uuid", ] @@ -2194,9 +2198,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.34" +version = "0.3.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749" +checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" dependencies = [ "deranged", "itoa", @@ -2217,9 +2221,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.17" +version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774" +checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" dependencies = [ "num-conv", "time-core", diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 54566c1eb..6b5325818 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -3,9 +3,9 @@ //! Core implementation for the progenitor OpenAPI client generator. #![deny(missing_docs)] -#![deny(unused_crate_dependencies)] -#![deny(unused_imports)] +#![cfg_attr(not(test),deny(unused_crate_dependencies))] #![deny(unused_import_braces)] +#![deny(unused_imports)] use std::collections::{BTreeMap, HashMap, HashSet}; diff --git a/progenitor-impl/tests/output/src/buildomat_builder.rs b/progenitor-impl/tests/output/src/buildomat_builder.rs index c42c7309e..98131e3f0 100644 --- a/progenitor-impl/tests/output/src/buildomat_builder.rs +++ b/progenitor-impl/tests/output/src/buildomat_builder.rs @@ -3150,7 +3150,7 @@ pub mod builder { self } - pub fn body(mut self, value: B) -> Self + pub fn body(mut self, body: B) -> Self where B: std::convert::TryInto, { diff --git a/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs b/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs index 07590907b..f2ce96dc9 100644 --- a/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs +++ b/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs @@ -3150,11 +3150,11 @@ pub mod builder { self } - pub fn body(mut self, value: B) -> Self + pub fn body(mut self, body: B) -> Self where B: std::convert::TryInto, { - self.body = value + self.body = body .try_into() .map_err(|_| "conversion to `reqwest::Body` for body failed".to_string()); self diff --git a/progenitor-impl/tests/output/src/nexus_builder.rs b/progenitor-impl/tests/output/src/nexus_builder.rs index 4bdfe9891..4727a9e38 100644 --- a/progenitor-impl/tests/output/src/nexus_builder.rs +++ b/progenitor-impl/tests/output/src/nexus_builder.rs @@ -29947,11 +29947,11 @@ pub mod builder { self } - pub fn body(mut self, value: B) -> Self + pub fn body(mut self, body: B) -> Self where B: std::convert::TryInto, { - self.body = value + self.body = body .try_into() .map_err(|_| "conversion to `reqwest::Body` for body failed".to_string()); self diff --git a/progenitor-impl/tests/output/src/nexus_builder_tagged.rs b/progenitor-impl/tests/output/src/nexus_builder_tagged.rs index 9ba1bd567..f9378cbf4 100644 --- a/progenitor-impl/tests/output/src/nexus_builder_tagged.rs +++ b/progenitor-impl/tests/output/src/nexus_builder_tagged.rs @@ -29867,11 +29867,11 @@ pub mod builder { self } - pub fn body(mut self, value: B) -> Self + pub fn body(mut self, body: B) -> Self where B: std::convert::TryInto, { - self.body = value + self.body = body .try_into() .map_err(|_| "conversion to `reqwest::Body` for body failed".to_string()); self diff --git a/progenitor/Cargo.toml b/progenitor/Cargo.toml index c71119aa3..5b2fbb7de 100644 --- a/progenitor/Cargo.toml +++ b/progenitor/Cargo.toml @@ -26,3 +26,7 @@ schemars = { version = "0.8.16", features = ["uuid1"] } serde = { version = "1.0", features = ["derive"] } uuid = { version = "1.8", features = ["serde", "v4"] } tokio = "1.36.0" +serde_json = "1.0" +serde_yaml = "0.9" +clap = { version = "4.1.14", features = ["derive"] } +expander = "2.1.0" diff --git a/progenitor/src/lib.rs b/progenitor/src/lib.rs index fa2bf6f93..684f7e3b2 100644 --- a/progenitor/src/lib.rs +++ b/progenitor/src/lib.rs @@ -11,7 +11,7 @@ //! README](https://github.com/oxidecomputer/progenitor/blob/main/README.md) #![deny(missing_docs)] -#![deny(unused_crate_dependencies)] +#![cfg_attr(not(test),deny(unused_crate_dependencies))] #![deny(unused_imports)] #![deny(unused_import_braces)] From 13b1e05a6ebce3a625ef805c3b99fcc51f0d92c7 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Fri, 12 Apr 2024 16:58:02 +0200 Subject: [PATCH 16/23] yikes --- progenitor-impl/tests/output/src/buildomat_builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/progenitor-impl/tests/output/src/buildomat_builder.rs b/progenitor-impl/tests/output/src/buildomat_builder.rs index 98131e3f0..8a6dc9420 100644 --- a/progenitor-impl/tests/output/src/buildomat_builder.rs +++ b/progenitor-impl/tests/output/src/buildomat_builder.rs @@ -3154,7 +3154,7 @@ pub mod builder { where B: std::convert::TryInto, { - self.body = value + self.body = body .try_into() .map_err(|_| "conversion to `reqwest::Body` for body failed".to_string()); self From 170738791f1428d63224f8a8ef37ddc844a993f0 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Mon, 15 Apr 2024 17:27:34 +0200 Subject: [PATCH 17/23] fmt --- progenitor-impl/src/lib.rs | 2 +- progenitor/src/lib.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 6b5325818..74c1857b9 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -3,7 +3,7 @@ //! Core implementation for the progenitor OpenAPI client generator. #![deny(missing_docs)] -#![cfg_attr(not(test),deny(unused_crate_dependencies))] +#![cfg_attr(not(test), deny(unused_crate_dependencies))] #![deny(unused_import_braces)] #![deny(unused_imports)] diff --git a/progenitor/src/lib.rs b/progenitor/src/lib.rs index 684f7e3b2..b323cce11 100644 --- a/progenitor/src/lib.rs +++ b/progenitor/src/lib.rs @@ -11,7 +11,7 @@ //! README](https://github.com/oxidecomputer/progenitor/blob/main/README.md) #![deny(missing_docs)] -#![cfg_attr(not(test),deny(unused_crate_dependencies))] +#![cfg_attr(not(test), deny(unused_crate_dependencies))] #![deny(unused_imports)] #![deny(unused_import_braces)] From 4c1031bebe3b1744c4428638aa1c39d1380d6d64 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Mon, 5 Jan 2026 23:26:01 +0100 Subject: [PATCH 18/23] deps add multipart --- Cargo.lock | 17 +++++++++++++++++ Cargo.toml | 2 +- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/Cargo.lock b/Cargo.lock index 9576d0aee..fa57caecf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1261,6 +1261,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minicov" version = "0.3.7" @@ -1759,6 +1769,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "percent-encoding", "pin-project-lite", @@ -2721,6 +2732,12 @@ dependencies = [ "typify-impl", ] +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.22" diff --git a/Cargo.toml b/Cargo.toml index 65f2cdd1d..c1f25e6d0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -41,7 +41,7 @@ quote = "1.0.42" rand = "0.9.2" regex = "1.11.2" regress = "0.10.5" -reqwest = { version = "0.12.28", default-features = false, features = ["json", "stream"] } +reqwest = { version = "0.12.28", default-features = false, features = ["json", "stream", "multipart"] } rustfmt-wrapper = "0.2.1" schemars = { version = "0.8.22", features = ["chrono", "uuid1"] } semver = "1.0.27" From 59247b9298f1ca4ea464fd270dec03e6a0ecd1a1 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Fri, 2 Jan 2026 02:05:44 +0100 Subject: [PATCH 19/23] type+form --- Cargo.lock | 7 + Cargo.toml | 1 + progenitor-client/Cargo.toml | 1 + progenitor-client/src/progenitor_client.rs | 479 ++++++++++++++++- progenitor-client/tests/client_test.rs | 366 +++++++++++++ progenitor-impl/src/cli.rs | 3 +- progenitor-impl/src/httpmock.rs | 5 +- progenitor-impl/src/lib.rs | 113 +++- progenitor-impl/src/method.rs | 128 ++++- .../output/src/example_multipart_builder.rs | 331 ++++++++++++ .../src/example_multipart_builder_tagged.rs | 331 ++++++++++++ .../tests/output/src/example_multipart_cli.rs | 130 +++++ .../output/src/example_multipart_httpmock.rs | 63 +++ .../src/example_multipart_positional.rs | 202 +++++++ progenitor-impl/tests/test_output.rs | 5 + progenitor-impl/tests/test_specific.rs | 494 ++++++++++++++++++ sample_openapi/example_multipart.json | 39 ++ 17 files changed, 2685 insertions(+), 13 deletions(-) create mode 100644 progenitor-impl/tests/output/src/example_multipart_builder.rs create mode 100644 progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs create mode 100644 progenitor-impl/tests/output/src/example_multipart_cli.rs create mode 100644 progenitor-impl/tests/output/src/example_multipart_httpmock.rs create mode 100644 progenitor-impl/tests/output/src/example_multipart_positional.rs create mode 100644 sample_openapi/example_multipart.json diff --git a/Cargo.lock b/Cargo.lock index fa57caecf..742e0a181 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -97,6 +97,12 @@ dependencies = [ "wait-timeout", ] +[[package]] +name = "assert_matches" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b34d609dfbaf33d6889b2b7106d3ca345eacad44200913df5ba02bfd31d2ba9" + [[package]] name = "async-stream" version = "0.3.6" @@ -1598,6 +1604,7 @@ dependencies = [ name = "progenitor-client" version = "0.11.2" dependencies = [ + "assert_matches", "bytes", "futures-core", "percent-encoding", diff --git a/Cargo.toml b/Cargo.toml index c1f25e6d0..0ca0e02c6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ typify = { version = "0.5.0" } url = "2.5.7" unicode-ident = "1.0.22" uuid = { version = "1.19.0", features = ["serde", "v4"] } +assert_matches = "1.5.0" #[patch."https://github.com/oxidecomputer/typify"] #typify = { path = "../typify/typify" } diff --git a/progenitor-client/Cargo.toml b/progenitor-client/Cargo.toml index b54cc20ac..2562fb540 100644 --- a/progenitor-client/Cargo.toml +++ b/progenitor-client/Cargo.toml @@ -16,5 +16,6 @@ serde_json = { workspace = true } serde_urlencoded = { workspace = true } [dev-dependencies] +assert_matches = { workspace = true } url = { workspace = true } uuid = { workspace = true } diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index 6f8dcca03..a0db1b2f1 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut}; use bytes::Bytes; use futures_core::Stream; -use reqwest::RequestBuilder; +use reqwest::{multipart::Part, RequestBuilder}; use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize}; #[cfg(not(target_arch = "wasm32"))] @@ -17,6 +17,404 @@ type InnerByteStream = std::pin::Pin>>>; +/// A validated filename for form part uploads. +/// +/// Filenames are validated to not contain path separators or null bytes, +/// preventing path traversal attacks. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct Filename(String); + +impl Filename { + /// Create a new filename, validating that it doesn't contain path separators. + /// + /// # Errors + /// Returns an error if the filename contains `/`, `\`, or null bytes. + pub fn new(name: impl Into) -> Result { + let name = name.into(); + if name.contains('/') || name.contains('\\') || name.contains('\0') { + Err(FilenameError::InvalidCharacter) + } else if name.is_empty() { + Err(FilenameError::Empty) + } else { + Ok(Self(name)) + } + } + + /// Create a filename without validation. + /// + /// # Safety + /// The caller must ensure the filename doesn't contain path separators. + /// This is useful when the filename comes from a trusted source. + pub fn new_unchecked(name: impl Into) -> Self { + Self(name.into()) + } + + /// Get the filename as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume the Filename and return the inner String. + pub fn into_string(self) -> String { + self.0 + } +} + +impl AsRef for Filename { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for Filename { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Error type for invalid filenames. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FilenameError { + /// Filename contains path separators (`/`, `\`) or null bytes. + InvalidCharacter, + /// Filename is empty. + Empty, +} + +impl std::fmt::Display for FilenameError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + FilenameError::InvalidCharacter => { + write!(f, "filename contains invalid characters (/, \\, or null)") + } + FilenameError::Empty => write!(f, "filename cannot be empty"), + } + } +} + +impl std::error::Error for FilenameError {} + +/// A validated MIME content-type for form parts. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct ContentType(String); + +impl ContentType { + /// Create a new content-type, validating the format. + /// + /// # Errors + /// Returns an error if the content-type doesn't follow `type/subtype` format. + pub fn new(content_type: impl Into) -> Result { + let content_type = content_type.into(); + let Some((type_part, rest)) = content_type.split_once('/') else { + return Err(ContentTypeError::InvalidFormat); + }; + let subtype_part = rest.split_once(';').map_or(rest, |(s, _)| s); + if type_part.trim().is_empty() + || subtype_part.trim().is_empty() + || subtype_part.contains('/') + { + return Err(ContentTypeError::InvalidFormat); + } + Ok(Self(content_type)) + } + + /// Create a content-type without validation. + /// + /// # Safety + /// The caller must ensure the content-type is in valid `type/subtype` format. + pub fn new_unchecked(content_type: impl Into) -> Self { + Self(content_type.into()) + } + + /// Get the content-type as a string slice. + pub fn as_str(&self) -> &str { + &self.0 + } + + /// Consume the ContentType and return the inner String. + pub fn into_string(self) -> String { + self.0 + } + + /// Create `application/json` content type. + pub fn json() -> Self { + Self("application/json".to_string()) + } + + /// Create `application/octet-stream` content type. + pub fn octet_stream() -> Self { + Self("application/octet-stream".to_string()) + } + + /// Create an `application/{subtype}` content type. + pub fn application(subtype: &str) -> Self { + Self(format!("application/{}", subtype)) + } + + /// Create an `image/{subtype}` content type. + pub fn image(subtype: &str) -> Self { + Self(format!("image/{}", subtype)) + } +} + +impl AsRef for ContentType { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl std::fmt::Display for ContentType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +/// Error type for invalid content-types. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ContentTypeError { + /// Content-type doesn't follow the `type/subtype` format. + InvalidFormat, +} + +impl std::fmt::Display for ContentTypeError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "content-type must be in 'type/subtype' format") + } +} + +impl std::error::Error for ContentTypeError {} + +/// Binary form part data with optional filename and content-type. +#[derive(Debug, Clone)] +pub struct BinaryFormPart { + /// The binary data + pub data: Bytes, + /// Optional filename for the part + pub filename: Option, + /// Optional content-type override + pub content_type: Option, +} + +impl BinaryFormPart { + /// Create a new binary form part from bytes. + pub fn new(data: impl Into) -> Self { + Self { + data: data.into(), + filename: None, + content_type: None, + } + } + + /// Create a binary form part with a filename. + pub fn with_filename(data: impl Into, filename: Filename) -> Self { + Self { + data: data.into(), + filename: Some(filename), + content_type: None, + } + } + + /// Create a binary form part with filename and content-type. + pub fn with_metadata( + data: impl Into, + filename: Option, + content_type: Option, + ) -> Self { + Self { + data: data.into(), + filename, + content_type, + } + } + + /// Create a builder for a binary form part. + pub fn builder(data: impl Into) -> BinaryFormPartBuilder { + BinaryFormPartBuilder { + data: data.into(), + filename: None, + content_type: None, + } + } +} + +impl From for FormPart { + fn from(part: BinaryFormPart) -> Self { + FormPart::Binary(part) + } +} + +/// Text form part data with optional content-type. +#[derive(Debug, Clone)] +pub struct TextFormPart { + /// The text value + pub value: String, + /// Optional content-type override + pub content_type: Option, +} + +impl TextFormPart { + /// Create a new text form part. + pub fn new(value: impl Into) -> Self { + Self { + value: value.into(), + content_type: None, + } + } + + /// Create a text form part with a specific content-type. + pub fn with_content_type(value: impl Into, content_type: ContentType) -> Self { + Self { + value: value.into(), + content_type: Some(content_type), + } + } + + /// Create a JSON text form part. + pub fn json(value: &T) -> Result { + Ok(Self { + value: serde_json::to_string(value)?, + content_type: Some(ContentType::json()), + }) + } + + /// Create a builder for a text form part. + pub fn builder(value: impl Into) -> TextFormPartBuilder { + TextFormPartBuilder { + value: value.into(), + content_type: None, + } + } +} + +impl From for FormPart { + fn from(part: TextFormPart) -> Self { + FormPart::Text(part) + } +} + +/// A part of a multipart form, either binary data or text. +#[derive(Debug, Clone)] +pub enum FormPart { + /// Binary data (e.g., file contents) with optional filename and content-type + Binary(BinaryFormPart), + /// Text data (will be sent as a text field) with optional content-type + Text(TextFormPart), +} + +impl FormPart { + /// Create a binary form part from bytes + pub fn binary(data: impl Into) -> Self { + Self::Binary(BinaryFormPart::new(data)) + } + + /// Create a binary form part with a filename + pub fn binary_with_filename(data: impl Into, filename: Filename) -> Self { + Self::Binary(BinaryFormPart::with_filename(data, filename)) + } + + /// Create a binary form part with filename and content-type + pub fn binary_with_metadata( + data: impl Into, + filename: Option, + content_type: Option, + ) -> Self { + Self::Binary(BinaryFormPart::with_metadata(data, filename, content_type)) + } + + /// Create a text form part from a string + pub fn text(data: impl Into) -> Self { + Self::Text(TextFormPart::new(data)) + } + + /// Create a text form part with a specific content-type + pub fn text_with_content_type(data: impl Into, content_type: ContentType) -> Self { + Self::Text(TextFormPart::with_content_type(data, content_type)) + } + + /// Create a JSON text form part + pub fn json(value: &T) -> Result { + Ok(Self::Text(TextFormPart::json(value)?)) + } + + /// Create a builder for a binary form part + pub fn binary_builder(data: impl Into) -> BinaryFormPartBuilder { + BinaryFormPart::builder(data) + } + + /// Create a builder for a text form part + pub fn text_builder(value: impl Into) -> TextFormPartBuilder { + TextFormPart::builder(value) + } +} + +/// Builder for binary form parts. +/// +/// Created via [`FormPart::binary_builder`] or [`BinaryFormPart::builder`]. +#[derive(Debug, Clone)] +pub struct BinaryFormPartBuilder { + data: Bytes, + filename: Option, + content_type: Option, +} + +impl BinaryFormPartBuilder { + /// Set the filename for this part. + pub fn filename(mut self, filename: Filename) -> Self { + self.filename = Some(filename); + self + } + + /// Set the content-type for this part. + pub fn content_type(mut self, content_type: ContentType) -> Self { + self.content_type = Some(content_type); + self + } + + /// Build the BinaryFormPart. + pub fn build(self) -> BinaryFormPart { + BinaryFormPart { + data: self.data, + filename: self.filename, + content_type: self.content_type, + } + } + + /// Build directly into a FormPart. + pub fn into_form_part(self) -> FormPart { + FormPart::Binary(self.build()) + } +} + +/// Builder for text form parts. +/// +/// Created via [`FormPart::text_builder`] or [`TextFormPart::builder`]. +#[derive(Debug, Clone)] +pub struct TextFormPartBuilder { + value: String, + content_type: Option, +} + +impl TextFormPartBuilder { + /// Set the content-type for this part. + pub fn content_type(mut self, content_type: ContentType) -> Self { + self.content_type = Some(content_type); + self + } + + /// Build the TextFormPart. + pub fn build(self) -> TextFormPart { + TextFormPart { + value: self.value, + content_type: self.content_type, + } + } + + /// Build directly into a FormPart. + pub fn into_form_part(self) -> FormPart { + FormPart::Text(self.build()) + } +} + /// Untyped byte stream used for both success and error responses. pub struct ByteStream(InnerByteStream); @@ -527,8 +925,21 @@ pub fn encode_path(pc: &str) -> String { } #[doc(hidden)] -pub trait RequestBuilderExt { +pub trait RequestBuilderExt +where + Self: Sized, +{ fn form_urlencoded(self, body: &T) -> Result>; + + fn form_from_raw, T: AsRef<[u8]>, I: Sized + IntoIterator>( + self, + iter: I, + ) -> Result>; + + fn form_from_parts, I: Sized + IntoIterator>( + self, + iter: I, + ) -> Result>; } impl RequestBuilderExt for RequestBuilder { @@ -543,6 +954,70 @@ impl RequestBuilderExt for RequestBuilder { .map_err(|_| Error::InvalidRequest("failed to serialize body".to_string()))?, )) } + + fn form_from_raw, T: AsRef<[u8]>, I: Sized + IntoIterator>( + self, + iter: I, + ) -> Result> { + use reqwest::multipart::Form; + + let mut form = Form::new(); + for (name, value) in iter { + form = form.part( + name.as_ref().to_owned(), + Part::stream(Vec::from(value.as_ref())), + ); + } + // Note: reqwest's .multipart() automatically sets the Content-Type header + // with the correct boundary, so we don't set it manually here. + Ok(self.multipart(form)) + } + + fn form_from_parts, I: Sized + IntoIterator>( + self, + iter: I, + ) -> Result> { + use reqwest::multipart::Form; + + let mut form = Form::new(); + for (name, part) in iter { + let name = name.as_ref().to_owned(); + form = match part { + FormPart::Binary(BinaryFormPart { + data, + filename, + content_type, + }) => { + let mut p = Part::stream(data.to_vec()); + if let Some(fname) = filename { + p = p.file_name(fname.into_string()); + } + if let Some(ct) = content_type { + p = p.mime_str(ct.as_str()).map_err(|e| { + Error::InvalidRequest(format!("invalid content-type: {}", e)) + })?; + } + form.part(name, p) + } + FormPart::Text(TextFormPart { + value, + content_type, + }) => { + if let Some(ct) = content_type { + let p = Part::text(value).mime_str(ct.as_str()).map_err(|e| { + Error::InvalidRequest(format!("invalid content-type: {}", e)) + })?; + form.part(name, p) + } else { + form.text(name, value) + } + } + }; + } + // Note: reqwest's .multipart() automatically sets the Content-Type header + // with the correct boundary, so we don't set it manually here. + Ok(self.multipart(form)) + } } #[doc(hidden)] diff --git a/progenitor-client/tests/client_test.rs b/progenitor-client/tests/client_test.rs index 6cd6a0d9f..59503195e 100644 --- a/progenitor-client/tests/client_test.rs +++ b/progenitor-client/tests/client_test.rs @@ -219,3 +219,369 @@ fn test_query_option() { let result = encode_query_param("paramName", &value).unwrap(); assert_eq!(result, "paramName=42"); } + +mod form_part_tests { + use assert_matches::assert_matches; + use bytes::Bytes; + use progenitor_client::{BinaryFormPart, ContentType, Filename, FormPart, TextFormPart}; + use serde::Serialize; + + #[test] + fn test_form_part_binary() { + let data = vec![1u8, 2, 3, 4]; + let part = FormPart::binary(data.clone()); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { data: d, filename: None, content_type: None }) => { + assert_eq!(d.as_ref(), &data[..]); + }); + } + + #[test] + fn test_form_part_binary_from_bytes() { + let data = Bytes::from_static(b"hello world"); + let part = FormPart::binary(data.clone()); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { data: d, .. }) => { + assert_eq!(d, data); + }); + } + + #[test] + fn test_form_part_binary_with_filename() { + let part = + FormPart::binary_with_filename(vec![1u8, 2, 3], Filename::new("test.bin").unwrap()); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { filename: Some(f), content_type: None, .. }) => { + assert_eq!(f.as_str(), "test.bin"); + }); + } + + #[test] + fn test_form_part_binary_with_metadata() { + let part = FormPart::binary_with_metadata( + vec![1u8, 2, 3], + Some(Filename::new("document.pdf").unwrap()), + Some(ContentType::application("pdf")), + ); + + assert_matches!(part, FormPart::Binary(BinaryFormPart { filename: Some(f), content_type: Some(ct), .. }) => { + assert_eq!(f.as_str(), "document.pdf"); + assert_eq!(ct.as_str(), "application/pdf"); + }); + } + + #[test] + fn test_form_part_binary_with_metadata_none() { + let part = FormPart::binary_with_metadata(vec![1u8, 2, 3], None, None); + assert_matches!( + part, + FormPart::Binary(BinaryFormPart { + filename: None, + content_type: None, + .. + }) + ); + } + + #[test] + fn test_form_part_text() { + let part = FormPart::text("hello world"); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: None }) => { + assert_eq!(value, "hello world"); + }); + } + + #[test] + fn test_form_part_text_from_string() { + let part = FormPart::text(String::from("hello world")); + + assert_matches!(part, FormPart::Text(TextFormPart { value, .. }) => { + assert_eq!(value, "hello world"); + }); + } + + #[test] + fn test_form_part_text_with_content_type() { + let part = FormPart::text_with_content_type("{\"key\": \"value\"}", ContentType::json()); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: Some(ct) }) => { + assert_eq!(value, "{\"key\": \"value\"}"); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_form_part_json() { + #[derive(Serialize)] + struct TestData { + name: String, + count: i32, + } + + let data = TestData { + name: "test".to_string(), + count: 42, + }; + let part = FormPart::json(&data).unwrap(); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: Some(ct) }) => { + assert_eq!(value, r#"{"name":"test","count":42}"#); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_form_part_json_array() { + let part = FormPart::json(&vec![1, 2, 3, 4, 5]).unwrap(); + + assert_matches!(part, FormPart::Text(TextFormPart { value, content_type: Some(ct) }) => { + assert_eq!(value, "[1,2,3,4,5]"); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_form_part_json_nested() { + #[derive(Serialize)] + struct Inner { + value: String, + } + + #[derive(Serialize)] + struct Outer { + inner: Inner, + tags: Vec, + } + + let data = Outer { + inner: Inner { + value: "nested".to_string(), + }, + tags: vec!["a".to_string(), "b".to_string()], + }; + let part = FormPart::json(&data).unwrap(); + + assert_matches!(part, FormPart::Text(TextFormPart { value, .. }) => { + assert!(value.contains("\"inner\"")); + assert!(value.contains("\"nested\"")); + assert!(value.contains("\"tags\"")); + }); + } + + #[test] + fn test_form_part_clone() { + let part1 = + FormPart::binary_with_filename(vec![1, 2, 3], Filename::new("test.bin").unwrap()); + let part2 = part1.clone(); + + assert_matches!( + (part1, part2), + (FormPart::Binary(BinaryFormPart { data: d1, filename: f1, .. }), FormPart::Binary(BinaryFormPart { data: d2, filename: f2, .. })) => { + assert_eq!(d1, d2); + assert_eq!(f1, f2); + } + ); + } + + #[test] + fn test_form_part_debug() { + let part = FormPart::text("test"); + let debug_str = format!("{:?}", part); + assert!(debug_str.contains("Text")); + assert!(debug_str.contains("test")); + } + + // Tests for the new type-safe newtypes + #[test] + fn test_filename_validation() { + // Valid filenames + assert!(Filename::new("test.bin").is_ok()); + assert!(Filename::new("my-file_123.txt").is_ok()); + assert!(Filename::new("file with spaces.pdf").is_ok()); + + // Invalid filenames (path separators) + assert!(Filename::new("path/to/file.txt").is_err()); + assert!(Filename::new("path\\to\\file.txt").is_err()); + assert!(Filename::new("file\0name.txt").is_err()); + assert!(Filename::new("").is_err()); + } + + #[test] + fn test_filename_unchecked() { + // new_unchecked bypasses validation (useful for trusted sources) + let f = Filename::new_unchecked("any/path/works"); + assert_eq!(f.as_str(), "any/path/works"); + } + + #[test] + fn test_content_type_json() { + assert_eq!(ContentType::json().as_str(), "application/json"); + } + + #[test] + fn test_content_type_new_unchecked() { + assert_eq!( + ContentType::new_unchecked("text/html").as_str(), + "text/html" + ); + } + + #[test] + fn test_content_type_application() { + assert_eq!(ContentType::application("pdf").as_str(), "application/pdf"); + assert_eq!( + ContentType::application("xml").as_str(), + "application/xml" + ); + } + + #[test] + fn test_content_type_image() { + assert_eq!(ContentType::image("png").as_str(), "image/png"); + assert_eq!(ContentType::image("jpeg").as_str(), "image/jpeg"); + } + + #[test] + fn test_content_type_octet_stream() { + assert_eq!( + ContentType::octet_stream().as_str(), + "application/octet-stream" + ); + } + + #[test] + fn test_filename_display() { + let f = Filename::new("test.bin").unwrap(); + assert_eq!(format!("{}", f), "test.bin"); + } + + #[test] + fn test_content_type_display() { + let ct = ContentType::json(); + assert_eq!(format!("{}", ct), "application/json"); + } + + #[test] + fn test_filename_as_ref() { + let f = Filename::new("test.bin").unwrap(); + let s: &str = f.as_ref(); + assert_eq!(s, "test.bin"); + } + + #[test] + fn test_content_type_as_ref() { + let ct = ContentType::json(); + let s: &str = ct.as_ref(); + assert_eq!(s, "application/json"); + } + + // Builder pattern tests + #[test] + fn test_binary_builder_minimal() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]).build(); + assert_matches!( + part, + BinaryFormPart { + filename: None, + content_type: None, + .. + } + ); + } + + #[test] + fn test_binary_builder_with_filename() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]) + .filename(Filename::new("test.bin").unwrap()) + .build(); + + assert_matches!(part, BinaryFormPart { filename: Some(f), content_type: None, .. } => { + assert_eq!(f.as_str(), "test.bin"); + }); + } + + #[test] + fn test_binary_builder_with_content_type() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]) + .content_type(ContentType::image("png")) + .build(); + + assert_matches!(part, BinaryFormPart { filename: None, content_type: Some(ct), .. } => { + assert_eq!(ct.as_str(), "image/png"); + }); + } + + #[test] + fn test_binary_builder_full() { + let part = FormPart::binary_builder(vec![1u8, 2, 3]) + .filename(Filename::new("document.pdf").unwrap()) + .content_type(ContentType::application("pdf")) + .build(); + + assert_matches!(part, BinaryFormPart { data, filename: Some(f), content_type: Some(ct) } => { + assert_eq!(data.as_ref(), &[1u8, 2, 3]); + assert_eq!(f.as_str(), "document.pdf"); + assert_eq!(ct.as_str(), "application/pdf"); + }); + } + + #[test] + fn test_text_builder_minimal() { + let part = FormPart::text_builder("hello").build(); + + assert_matches!(part, TextFormPart { value, content_type: None } => { + assert_eq!(value, "hello"); + }); + } + + #[test] + fn test_text_builder_with_content_type() { + let part = FormPart::text_builder("{}") + .content_type(ContentType::json()) + .build(); + + assert_matches!(part, TextFormPart { value, content_type: Some(ct) } => { + assert_eq!(value, "{}"); + assert_eq!(ct.as_str(), "application/json"); + }); + } + + #[test] + fn test_builder_into_form_part() { + // Test that builders can be converted directly to FormPart + let part: FormPart = FormPart::binary_builder(vec![1u8, 2, 3]) + .filename(Filename::new("test.bin").unwrap()) + .into_form_part(); + assert_matches!( + part, + FormPart::Binary(BinaryFormPart { + filename: Some(_), + .. + }) + ); + + let part: FormPart = FormPart::text_builder("hello") + .content_type(ContentType::json()) + .into_form_part(); + assert_matches!( + part, + FormPart::Text(TextFormPart { + content_type: Some(_), + .. + }) + ); + } + + #[test] + fn test_inner_types_into_form_part() { + // Test that BinaryFormPart and TextFormPart can be converted to FormPart via From + let binary = BinaryFormPart::new(vec![1u8, 2, 3]); + let part: FormPart = binary.into(); + assert_matches!(part, FormPart::Binary(_)); + + let text = TextFormPart::new("hello"); + let part: FormPart = text.into(); + assert_matches!(part, FormPart::Text(_)); + } +} diff --git a/progenitor-impl/src/cli.rs b/progenitor-impl/src/cli.rs index e1bdf30c0..8d3a349e9 100644 --- a/progenitor-impl/src/cli.rs +++ b/progenitor-impl/src/cli.rs @@ -441,7 +441,8 @@ impl Generator { // are currently... OperationParameterType::RawBody => None, - OperationParameterType::Type(body_type_id) => Some(body_type_id), + OperationParameterType::Type(body_type_id) + | OperationParameterType::Form(body_type_id) => Some(body_type_id), }); if let Some(body_type_id) = maybe_body_type_id { diff --git a/progenitor-impl/src/httpmock.rs b/progenitor-impl/src/httpmock.rs index ed56fbf6b..5c1bbf12f 100644 --- a/progenitor-impl/src/httpmock.rs +++ b/progenitor-impl/src/httpmock.rs @@ -156,7 +156,8 @@ impl Generator { description: _, }| { let arg_type_name = match typ { - OperationParameterType::Type(arg_type_id) => self + OperationParameterType::Type(arg_type_id) + | OperationParameterType::Form(arg_type_id) => self .type_space .get_type(arg_type_id) .unwrap() @@ -226,7 +227,7 @@ impl Generator { }, ), OperationParameterKind::Body(body_content_type) => match typ { - OperationParameterType::Type(_) => ( + OperationParameterType::Type(_) | OperationParameterType::Form(_) => ( true, quote! { Self(self.0.json_body_obj(value)) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 68454ece1..b48bbbdea 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -6,12 +6,13 @@ use std::collections::{BTreeMap, HashMap, HashSet}; +use indexmap::IndexMap; use openapiv3::OpenAPI; use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use thiserror::Error; -use typify::{TypeSpace, TypeSpaceSettings}; +use typify::{TypeDetails, TypeId, TypeSpace, TypeSpaceSettings}; use crate::to_schema::ToSchema; @@ -50,11 +51,33 @@ pub type Result = std::result::Result; /// OpenAPI generator. pub struct Generator { type_space: TypeSpace, + /// Maps form type IDs to their field metadata (field name -> is_binary) + forms: IndexMap, settings: GenerationSettings, uses_futures: bool, uses_websockets: bool, } +/// Information about form fields for multipart/form-data generation +#[derive(Debug, Clone, Default)] +pub(crate) struct FormFieldsInfo { + /// Maps field names to their metadata + pub fields: IndexMap, +} + +/// Metadata about a single form field +#[derive(Debug, Clone, Default)] +pub(crate) struct FormFieldMeta { + /// The original API name for the field (used in form field name) + pub api_name: String, + /// Whether the field is binary (format: binary) + pub is_binary: bool, + /// Whether the field is a complex type (array or object) requiring JSON serialization + pub needs_json: bool, + /// Content-type override from the encoding object + pub content_type: Option, +} + /// Settings for [Generator]. #[derive(Default, Clone)] pub struct GenerationSettings { @@ -261,6 +284,7 @@ impl Default for Generator { fn default() -> Self { Self { type_space: TypeSpace::new(TypeSpaceSettings::default().with_type_mod("types")), + forms: Default::default(), settings: Default::default(), uses_futures: Default::default(), uses_websockets: Default::default(), @@ -312,6 +336,7 @@ impl Generator { Self { type_space: TypeSpace::new(&type_settings), + forms: Default::default(), settings: settings.clone(), uses_futures: false, uses_websockets: false, @@ -374,6 +399,90 @@ impl Generator { let types = self.type_space.to_stream(); + // Generate as_form() implementations for form data types. + // Each form type gets a method that returns an iterator of (name, FormPart) pairs. + let extra_impl = TokenStream::from_iter(self.forms.iter().map(|(type_id, field_info)| { + let typ = self.get_type_space().get_type(type_id).unwrap(); + let td = typ.details(); + let TypeDetails::Struct(tstru) = td else { + unreachable!() + }; + + // Generate field accessors that convert each field to the appropriate FormPart + let field_conversions = tstru.properties().filter_map(|(prop_name, _prop_id)| { + let meta = field_info.fields.get(prop_name)?; + let ident = quote::format_ident!("{}", prop_name); + // Use the original API name for the form field name + let api_name = &meta.api_name; + let content_type = meta + .content_type + .as_ref() + .map(|ct| { + quote! { Some(progenitor_client::ContentType::new_unchecked(#ct)) } + }) + .unwrap_or_else(|| quote! { None }); + + if meta.is_binary { + // Binary fields: Option -> FormPart::Binary + // Use .into() to support custom binary types via with_conversion + Some(quote! { + if let Some(ref val) = self.#ident { + parts.push((#api_name, progenitor_client::FormPart::Binary( + progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: #content_type, + } + ))); + } + }) + } else if meta.needs_json { + // Complex types (array/object): JSON serialize + let json_content_type = if meta.content_type.is_some() { + content_type.clone() + } else { + quote! { Some(progenitor_client::ContentType::json()) } + }; + Some(quote! { + if let Some(ref val) = self.#ident { + parts.push((#api_name, progenitor_client::FormPart::Text( + progenitor_client::TextFormPart { + value: serde_json::to_string(val).unwrap_or_default(), + content_type: #json_content_type, + } + ))); + } + }) + } else { + // Simple text fields: serialize to string + Some(quote! { + if let Some(ref val) = self.#ident { + parts.push((#api_name, progenitor_client::FormPart::Text( + progenitor_client::TextFormPart { + value: val.to_string(), + content_type: #content_type, + } + ))); + } + }) + } + }); + + let form_name = quote::format_ident!("{}", typ.name()); + + quote! { + impl #form_name { + /// Convert this form into an iterator of (field_name, field_value) pairs + /// suitable for multipart/form-data encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + #(#field_conversions)* + parts + } + } + } + })); + let (inner_type, inner_fn_value) = match self.settings.inner_type.as_ref() { Some(inner_type) => (inner_type.clone(), quote! { &self.inner }), None => (quote! { () }, quote! { &() }), @@ -440,6 +549,8 @@ impl Generator { #[allow(clippy::all)] pub mod types { #types + + #extra_impl } #[derive(Clone, Debug)] diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 8bf1d0fe8..a263f44e7 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -6,6 +6,8 @@ use std::{ str::FromStr, }; +use indexmap::IndexMap; + use openapiv3::{Components, Parameter, ReferenceOr, Response, StatusCode}; use proc_macro2::TokenStream; use quote::{format_ident, quote, ToTokens}; @@ -102,9 +104,10 @@ pub struct OperationParameter { pub kind: OperationParameterKind, } -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum OperationParameterType { Type(TypeId), + Form(TypeId), RawBody, } @@ -137,6 +140,7 @@ pub enum BodyContentType { OctetStream, Json, FormUrlencoded, + FormData, Text(String), } @@ -149,6 +153,7 @@ impl FromStr for BodyContentType { "application/octet-stream" => Ok(Self::OctetStream), "application/json" => Ok(Self::Json), "application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded), + "form-data" | "multipart/form-data" => Ok(Self::FormData), "text/plain" | "text/x-markdown" => Ok(Self::Text(String::from(&s[..offset]))), _ => Err(Error::UnexpectedFormat(format!( "unexpected content type: {}", @@ -164,6 +169,7 @@ impl std::fmt::Display for BodyContentType { Self::OctetStream => "application/octet-stream", Self::Json => "application/json", Self::FormUrlencoded => "application/x-www-form-urlencoded", + Self::FormData => "multipart/form-data", Self::Text(typ) => typ, }) } @@ -562,12 +568,20 @@ impl Generator { .map(|param| { let name = format_ident!("{}", param.name); let typ = match (¶m.typ, param.kind.is_optional()) { - (OperationParameterType::Type(type_id), false) => self + ( + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id), + false, + ) => self .type_space .get_type(type_id) .unwrap() .parameter_ident_with_lifetime("a"), - (OperationParameterType::Type(type_id), true) => { + ( + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id), + true, + ) => { let t = self .type_space .get_type(type_id) @@ -918,6 +932,16 @@ impl Generator { // returns an error in the case of a serialization failure. .form_urlencoded(&body)? }), + ( + OperationParameterKind::Body(BodyContentType::FormData), + OperationParameterType::Form(_), + ) => { + Some(quote! { + // This uses `progenitor_client::RequestBuilderExt` which + // builds a multipart form from the form parts + .form_from_parts(body.as_form())? + }) + } (OperationParameterKind::Body(_), _) => { unreachable!("invalid body kind/type combination") } @@ -1436,7 +1460,7 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; // For body parameters only, if there's a builder we'll @@ -1469,7 +1493,7 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; // Fill in the appropriate initial value for the @@ -1499,7 +1523,7 @@ impl Generator { .params .iter() .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; if ty.builder().is_some() { let type_name = ty.ident(); @@ -1523,7 +1547,8 @@ impl Generator { .map(|param| { let param_name = format_ident!("{}", param.name); match ¶m.typ { - OperationParameterType::Type(type_id) => { + OperationParameterType::Type(type_id) + | OperationParameterType::Form(type_id) => { let ty = self.type_space.get_type(type_id)?; match (ty.builder(), param.kind.is_optional()) { // TODO right now optional body parameters are not @@ -2131,6 +2156,95 @@ impl Generator { }?; OperationParameterType::RawBody } + BodyContentType::FormData => { + // For multipart/form-data, we accept an object schema with various property types: + // - type: string, format: binary -> Binary file data (bytes::Bytes) + // - type: string -> Text field + // - type: integer/number/boolean -> Text field (serialized) + // - type: object -> JSON-serialized text field + // - type: array -> JSON-serialized text field + // + // We track field metadata to generate appropriate as_form() implementations. + // The encoding object can override content-types for individual fields. + + let field_info = match schema.item(components)? { + openapiv3::Schema { + schema_kind: + openapiv3::SchemaKind::Type(openapiv3::Type::Object( + openapiv3::ObjectType { properties, .. }, + )), + .. + } => { + let mut fields = IndexMap::new(); + for (name, property) in properties { + let (is_binary, needs_json) = match property { + ReferenceOr::Item(prop) => { + let is_binary = matches!( + &prop.schema_kind, + openapiv3::SchemaKind::Type(openapiv3::Type::String( + openapiv3::StringType { + format: openapiv3::VariantOrUnknownOrEmpty::Item( + openapiv3::StringFormat::Binary, + ), + .. + }, + )) + ); + let needs_json = matches!( + &prop.schema_kind, + openapiv3::SchemaKind::Type(openapiv3::Type::Array(_)) + | openapiv3::SchemaKind::Type(openapiv3::Type::Object( + _ + )) + ); + (is_binary, needs_json) + } + ReferenceOr::Reference { .. } => { + // References are assumed to be complex types requiring JSON + (false, true) + } + }; + + // Check for encoding overrides (content-type per field) + let content_type = media_type + .encoding + .get(name) + .and_then(|enc| enc.content_type.clone()); + + // Use the Rust-ized (snake_case) name as the key since + // typify's properties() returns Rust names, but store the + // original API name for use in the form field name. + let rust_name = sanitize(name, Case::Snake); + fields.insert( + rust_name, + crate::FormFieldMeta { + api_name: name.clone(), + is_binary, + needs_json, + content_type, + }, + ); + } + crate::FormFieldsInfo { fields } + } + _ => { + return Err(Error::UnexpectedFormat(format!( + "multipart/form-data requires an object schema, got: {:?}", + schema + ))); + } + }; + + let form_name = sanitize( + &format!("{}-form", operation.operation_id.as_ref().unwrap(),), + Case::Pascal, + ); + let type_id = self + .type_space + .add_type_with_name(&schema.to_schema(), Some(form_name))?; + self.forms.insert(type_id.clone(), field_info); + OperationParameterType::Form(type_id) + } BodyContentType::Json | BodyContentType::FormUrlencoded => { // TODO it would be legal to have the encoding field set for // application/x-www-form-urlencoded content, but I'm not sure diff --git a/progenitor-impl/tests/output/src/example_multipart_builder.rs b/progenitor-impl/tests/output/src/example_multipart_builder.rs new file mode 100644 index 000000000..5e2d0ea72 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_builder.rs @@ -0,0 +1,331 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } + + ///`UploadForm` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "name" + /// ], + /// "properties": { + /// "file": { + /// "type": "string", + /// "format": "binary" + /// }, + /// "name": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct UploadForm { + pub file: ::std::string::String, + pub name: ::std::string::String, + } + + impl ::std::convert::From<&UploadForm> for UploadForm { + fn from(value: &UploadForm) -> Self { + value.clone() + } + } + + impl UploadForm { + pub fn builder() -> builder::UploadForm { + Default::default() + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadForm { + file: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for UploadForm { + fn default() -> Self { + Self { + file: Err("no value supplied for file".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + + impl UploadForm { + pub fn file(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.file = value + .try_into() + .map_err(|e| format!("error converting supplied value for file: {}", e)); + self + } + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadForm { + type Error = super::error::ConversionError; + fn try_from( + value: UploadForm, + ) -> ::std::result::Result { + Ok(Self { + file: value.file?, + name: value.name?, + }) + } + } + + impl ::std::convert::From for UploadForm { + fn from(value: super::UploadForm) -> Self { + Self { + file: Ok(value.file), + name: Ok(value.name), + } + } + } + } + + impl UploadForm { + /// Convert this form into an iterator of (field_name, field_value) + /// pairs + /// suitable for multipart/form-data encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + if let Some(ref val) = self.file { + parts.push(( + "file", + progenitor_client::FormPart::Binary(progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: None, + }), + )); + } + if let Some(ref val) = self.name { + parts.push(( + "name", + progenitor_client::FormPart::Text(progenitor_client::TextFormPart { + value: val.to_string(), + content_type: None, + }), + )); + } + parts + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Example +/// +///Version: 1.0.0 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = ::std::time::Duration::from_secs(15u64); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "1.0.0" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +impl Client { + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload(&self) -> builder::Upload<'_> { + builder::Upload::new(self) + } +} + +/// Types for composing operation parameters. +#[allow(clippy::all)] +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{ + encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt, + ResponseValue, + }; + ///Builder for [`Client::upload`] + /// + ///[`Client::upload`]: super::Client::upload + #[derive(Debug, Clone)] + pub struct Upload<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> Upload<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Ok(::std::default::Default::default()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + >::Error: std::fmt::Display, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|s| format!("conversion to `UploadForm` for body failed: {}", s)); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce(types::builder::UploadForm) -> types::builder::UploadForm, + { + self.body = self.body.map(f); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body + .and_then(|v| types::UploadForm::try_from(v).map_err(|e| e.to_string())) + .map_err(Error::InvalidRequest)?; + let url = format!("{}/upload", client.baseurl,); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(super::Client::api_version()), + ); + #[allow(unused_mut)] + let mut request = client + .client + .post(url) + .form_from_parts(body.as_form())? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload", + }; + client.pre(&mut request, &info).await?; + let result = client.exec(request, &info).await; + client.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } +} + +/// Items consumers will typically use such as the Client. +pub mod prelude { + pub use self::super::Client; +} diff --git a/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs b/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs new file mode 100644 index 000000000..e486c003d --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs @@ -0,0 +1,331 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } + + ///`UploadForm` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "name" + /// ], + /// "properties": { + /// "file": { + /// "type": "string", + /// "format": "binary" + /// }, + /// "name": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadForm { + pub file: ::std::string::String, + pub name: ::std::string::String, + } + + impl ::std::convert::From<&UploadForm> for UploadForm { + fn from(value: &UploadForm) -> Self { + value.clone() + } + } + + impl UploadForm { + pub fn builder() -> builder::UploadForm { + Default::default() + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadForm { + file: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for UploadForm { + fn default() -> Self { + Self { + file: Err("no value supplied for file".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + + impl UploadForm { + pub fn file(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.file = value + .try_into() + .map_err(|e| format!("error converting supplied value for file: {}", e)); + self + } + pub fn name(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.name = value + .try_into() + .map_err(|e| format!("error converting supplied value for name: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadForm { + type Error = super::error::ConversionError; + fn try_from( + value: UploadForm, + ) -> ::std::result::Result { + Ok(Self { + file: value.file?, + name: value.name?, + }) + } + } + + impl ::std::convert::From for UploadForm { + fn from(value: super::UploadForm) -> Self { + Self { + file: Ok(value.file), + name: Ok(value.name), + } + } + } + } + + impl UploadForm { + /// Convert this form into an iterator of (field_name, field_value) + /// pairs + /// suitable for multipart/form-data encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + if let Some(ref val) = self.file { + parts.push(( + "file", + progenitor_client::FormPart::Binary(progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: None, + }), + )); + } + if let Some(ref val) = self.name { + parts.push(( + "name", + progenitor_client::FormPart::Text(progenitor_client::TextFormPart { + value: val.to_string(), + content_type: None, + }), + )); + } + parts + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Example +/// +///Version: 1.0.0 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = ::std::time::Duration::from_secs(15u64); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "1.0.0" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +impl Client { + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload(&self) -> builder::Upload<'_> { + builder::Upload::new(self) + } +} + +/// Types for composing operation parameters. +#[allow(clippy::all)] +pub mod builder { + use super::types; + #[allow(unused_imports)] + use super::{ + encode_path, ByteStream, ClientHooks, ClientInfo, Error, OperationInfo, RequestBuilderExt, + ResponseValue, + }; + ///Builder for [`Client::upload`] + /// + ///[`Client::upload`]: super::Client::upload + #[derive(Debug, Clone)] + pub struct Upload<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> Upload<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Ok(::std::default::Default::default()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + >::Error: std::fmt::Display, + { + self.body = value + .try_into() + .map(From::from) + .map_err(|s| format!("conversion to `UploadForm` for body failed: {}", s)); + self + } + + pub fn body_map(mut self, f: F) -> Self + where + F: std::ops::FnOnce(types::builder::UploadForm) -> types::builder::UploadForm, + { + self.body = self.body.map(f); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body + .and_then(|v| types::UploadForm::try_from(v).map_err(|e| e.to_string())) + .map_err(Error::InvalidRequest)?; + let url = format!("{}/upload", client.baseurl,); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(super::Client::api_version()), + ); + #[allow(unused_mut)] + let mut request = client + .client + .post(url) + .form_from_parts(body.as_form())? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload", + }; + client.pre(&mut request, &info).await?; + let result = client.exec(request, &info).await; + client.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } +} + +/// Items consumers will typically use such as the Client and +/// extension traits. +pub mod prelude { + #[allow(unused_imports)] + pub use super::Client; +} diff --git a/progenitor-impl/tests/output/src/example_multipart_cli.rs b/progenitor-impl/tests/output/src/example_multipart_cli.rs new file mode 100644 index 000000000..2a883c46a --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_cli.rs @@ -0,0 +1,130 @@ +use crate::example_multipart_builder::*; +use anyhow::Context as _; +pub struct Cli { + client: Client, + config: T, +} + +impl Cli { + pub fn new(client: Client, config: T) -> Self { + Self { client, config } + } + + pub fn get_command(cmd: CliCommand) -> ::clap::Command { + match cmd { + CliCommand::Upload => Self::cli_upload(), + } + } + + pub fn cli_upload() -> ::clap::Command { + ::clap::Command::new("") + .arg( + ::clap::Arg::new("file") + .long("file") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body"), + ) + .arg( + ::clap::Arg::new("name") + .long("name") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body"), + ) + .arg( + ::clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(false) + .value_parser(::clap::value_parser!(std::path::PathBuf)) + .help("Path to a file that contains the full json body."), + ) + .arg( + ::clap::Arg::new("json-body-template") + .long("json-body-template") + .action(::clap::ArgAction::SetTrue) + .help("XXX"), + ) + } + + pub async fn execute( + &self, + cmd: CliCommand, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + match cmd { + CliCommand::Upload => self.execute_upload(matches).await, + } + } + + pub async fn execute_upload(&self, matches: &::clap::ArgMatches) -> anyhow::Result<()> { + let mut request = self.client.upload(); + if let Some(value) = matches.get_one::<::std::string::String>("file") { + request = request.body_map(|body| body.file(value.clone())) + } + + if let Some(value) = matches.get_one::<::std::string::String>("name") { + request = request.body_map(|body| body.name(value.clone())) + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value) + .with_context(|| format!("failed to read {}", value.display()))?; + let body_value = serde_json::from_str::(&body_txt) + .with_context(|| format!("failed to parse {}", value.display()))?; + request = request.body(body_value); + } + + self.config.execute_upload(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.success_no_item(&r); + Ok(()) + } + Err(r) => { + self.config.error(&r); + Err(anyhow::Error::new(r)) + } + } + } +} + +pub trait CliConfig { + fn success_item(&self, value: &ResponseValue) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn success_no_item(&self, value: &ResponseValue<()>); + fn error(&self, value: &Error) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_start(&self) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_item(&self, value: &T) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_end_success(&self) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn list_end_error(&self, value: &Error) + where + T: std::clone::Clone + schemars::JsonSchema + serde::Serialize + std::fmt::Debug; + fn execute_upload( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::Upload, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum CliCommand { + Upload, +} + +impl CliCommand { + pub fn iter() -> impl Iterator { + vec![CliCommand::Upload].into_iter() + } +} diff --git a/progenitor-impl/tests/output/src/example_multipart_httpmock.rs b/progenitor-impl/tests/output/src/example_multipart_httpmock.rs new file mode 100644 index 000000000..4446f7669 --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_httpmock.rs @@ -0,0 +1,63 @@ +pub mod operations { + #![doc = r" [`When`](::httpmock::When) and [`Then`](::httpmock::Then)"] + #![doc = r" wrappers for each operation. Each can be converted to"] + #![doc = r" its inner type with a call to `into_inner()`. This can"] + #![doc = r" be used to explicitly deviate from permitted values."] + use crate::example_multipart_builder::*; + pub struct UploadWhen(::httpmock::When); + impl UploadWhen { + pub fn new(inner: ::httpmock::When) -> Self { + Self( + inner + .method(::httpmock::Method::POST) + .path_matches(regex::Regex::new("^/upload$").unwrap()), + ) + } + + pub fn into_inner(self) -> ::httpmock::When { + self.0 + } + + pub fn body(self, value: &types::UploadForm) -> Self { + Self(self.0.json_body_obj(value)) + } + } + + pub struct UploadThen(::httpmock::Then); + impl UploadThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn ok(self) -> Self { + Self(self.0.status(200u16)) + } + } +} + +#[doc = r" An extension trait for [`MockServer`](::httpmock::MockServer) that"] +#[doc = r" adds a method for each operation. These are the equivalent of"] +#[doc = r" type-checked [`mock()`](::httpmock::MockServer::mock) calls."] +pub trait MockServerExt { + fn upload(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadWhen, operations::UploadThen); +} + +impl MockServerExt for ::httpmock::MockServer { + fn upload(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadWhen, operations::UploadThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadWhen::new(when), + operations::UploadThen::new(then), + ) + }) + } +} diff --git a/progenitor-impl/tests/output/src/example_multipart_positional.rs b/progenitor-impl/tests/output/src/example_multipart_positional.rs new file mode 100644 index 000000000..e290da4ac --- /dev/null +++ b/progenitor-impl/tests/output/src/example_multipart_positional.rs @@ -0,0 +1,202 @@ +#[allow(unused_imports)] +use progenitor_client::{encode_path, ClientHooks, OperationInfo, RequestBuilderExt}; +#[allow(unused_imports)] +pub use progenitor_client::{ByteStream, ClientInfo, Error, ResponseValue}; +/// Types used as operation parameters and responses. +#[allow(clippy::all)] +pub mod types { + /// Error types. + pub mod error { + /// Error from a `TryFrom` or `FromStr` implementation. + pub struct ConversionError(::std::borrow::Cow<'static, str>); + impl ::std::error::Error for ConversionError {} + impl ::std::fmt::Display for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Display::fmt(&self.0, f) + } + } + + impl ::std::fmt::Debug for ConversionError { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> Result<(), ::std::fmt::Error> { + ::std::fmt::Debug::fmt(&self.0, f) + } + } + + impl From<&'static str> for ConversionError { + fn from(value: &'static str) -> Self { + Self(value.into()) + } + } + + impl From for ConversionError { + fn from(value: String) -> Self { + Self(value.into()) + } + } + } + + ///`UploadForm` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "name" + /// ], + /// "properties": { + /// "file": { + /// "type": "string", + /// "format": "binary" + /// }, + /// "name": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadForm { + pub file: ::std::string::String, + pub name: ::std::string::String, + } + + impl ::std::convert::From<&UploadForm> for UploadForm { + fn from(value: &UploadForm) -> Self { + value.clone() + } + } + + impl UploadForm { + /// Convert this form into an iterator of (field_name, field_value) + /// pairs + /// suitable for multipart/form-data encoding. + pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { + let mut parts = Vec::new(); + if let Some(ref val) = self.file { + parts.push(( + "file", + progenitor_client::FormPart::Binary(progenitor_client::BinaryFormPart { + data: val.clone().into(), + filename: None, + content_type: None, + }), + )); + } + if let Some(ref val) = self.name { + parts.push(( + "name", + progenitor_client::FormPart::Text(progenitor_client::TextFormPart { + value: val.to_string(), + content_type: None, + }), + )); + } + parts + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Example +/// +///Version: 1.0.0 +pub struct Client { + pub(crate) baseurl: String, + pub(crate) client: reqwest::Client, +} + +impl Client { + /// Create a new client. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new(baseurl: &str) -> Self { + #[cfg(not(target_arch = "wasm32"))] + let client = { + let dur = ::std::time::Duration::from_secs(15u64); + reqwest::ClientBuilder::new() + .connect_timeout(dur) + .timeout(dur) + }; + #[cfg(target_arch = "wasm32")] + let client = reqwest::ClientBuilder::new(); + Self::new_with_client(baseurl, client.build().unwrap()) + } + + /// Construct a new client with an existing `reqwest::Client`, + /// allowing more control over its configuration. + /// + /// `baseurl` is the base URL provided to the internal + /// `reqwest::Client`, and should include a scheme and hostname, + /// as well as port and a path stem if applicable. + pub fn new_with_client(baseurl: &str, client: reqwest::Client) -> Self { + Self { + baseurl: baseurl.to_string(), + client, + } + } +} + +impl ClientInfo<()> for Client { + fn api_version() -> &'static str { + "1.0.0" + } + + fn baseurl(&self) -> &str { + self.baseurl.as_str() + } + + fn client(&self) -> &reqwest::Client { + &self.client + } + + fn inner(&self) -> &() { + &() + } +} + +impl ClientHooks<()> for &Client {} +#[allow(clippy::all)] +impl Client { + ///Sends a `POST` request to `/upload` + pub async fn upload<'a>( + &'a self, + body: &'a types::UploadForm, + ) -> Result, Error<()>> { + let url = format!("{}/upload", self.baseurl,); + let mut header_map = ::reqwest::header::HeaderMap::with_capacity(1usize); + header_map.append( + ::reqwest::header::HeaderName::from_static("api-version"), + ::reqwest::header::HeaderValue::from_static(Self::api_version()), + ); + #[allow(unused_mut)] + let mut request = self + .client + .post(url) + .form_from_parts(body.as_form())? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload", + }; + self.pre(&mut request, &info).await?; + let result = self.exec(request, &info).await; + self.post(&result, &info).await?; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } +} + +/// Items consumers will typically use such as the Client. +pub mod prelude { + #[allow(unused_imports)] + pub use super::Client; +} diff --git a/progenitor-impl/tests/test_output.rs b/progenitor-impl/tests/test_output.rs index 010315d98..5bbce0760 100644 --- a/progenitor-impl/tests/test_output.rs +++ b/progenitor-impl/tests/test_output.rs @@ -163,6 +163,11 @@ fn test_cli_gen() { verify_apis("cli-gen.json"); } +#[test] +fn test_example_multipart() { + verify_apis("example_multipart.json"); +} + #[test] fn test_nexus_with_different_timeout() { const OPENAPI_FILE: &'static str = "nexus.json"; diff --git a/progenitor-impl/tests/test_specific.rs b/progenitor-impl/tests/test_specific.rs index ba29aa6c2..681a34b92 100644 --- a/progenitor-impl/tests/test_specific.rs +++ b/progenitor-impl/tests/test_specific.rs @@ -355,3 +355,497 @@ async fn test_stream_pagination() { server.close().await.expect("failed to close server"); } + +mod multipart_tests { + use openapiv3::OpenAPI; + use progenitor_impl::{GenerationSettings, Generator, InterfaceStyle}; + + fn make_multipart_spec(operation_id: &str, properties: serde_json::Value) -> OpenAPI { + serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": operation_id, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": properties + } + } + } + }, + "responses": { + "200": { "description": "OK" } + } + } + } + } + })) + .unwrap() + } + + fn make_multipart_spec_with_encoding( + operation_id: &str, + properties: serde_json::Value, + encoding: serde_json::Value, + ) -> OpenAPI { + serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": operation_id, + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": properties + }, + "encoding": encoding + } + } + }, + "responses": { + "200": { "description": "OK" } + } + } + } + } + })) + .unwrap() + } + + fn make_builder_generator() -> Generator { + Generator::new(GenerationSettings::default().with_interface(InterfaceStyle::Builder)) + } + + fn make_positional_generator() -> Generator { + Generator::new(GenerationSettings::default().with_interface(InterfaceStyle::Positional)) + } + + fn assert_generates_ok(spec: &OpenAPI, msg: &str) -> String { + let mut generator = make_builder_generator(); + let result = generator.generate_tokens(spec); + assert!(result.is_ok(), "{}: {:?}", msg, result.err()); + result.unwrap().to_string() + } + + fn assert_generates_err(spec: &OpenAPI, expected_err: &str) { + let mut generator = make_builder_generator(); + let result = generator.generate_tokens(spec); + assert!(result.is_err(), "Expected generation to fail"); + let err = result.unwrap_err().to_string(); + assert!( + err.contains(expected_err), + "Error should contain '{}': {}", + expected_err, + err + ); + } + + fn binary_file_prop() -> serde_json::Value { + serde_json::json!({ "type": "string", "format": "binary" }) + } + + fn string_prop() -> serde_json::Value { + serde_json::json!({ "type": "string" }) + } + + fn integer_prop() -> serde_json::Value { + serde_json::json!({ "type": "integer" }) + } + + fn boolean_prop() -> serde_json::Value { + serde_json::json!({ "type": "boolean" }) + } + + fn number_prop() -> serde_json::Value { + serde_json::json!({ "type": "number", "format": "double" }) + } + + fn string_array_prop() -> serde_json::Value { + serde_json::json!({ "type": "array", "items": { "type": "string" } }) + } + + fn integer_array_prop() -> serde_json::Value { + serde_json::json!({ "type": "array", "items": { "type": "integer" } }) + } + + fn simple_object_prop() -> serde_json::Value { + serde_json::json!({ + "type": "object", + "properties": { + "name": { "type": "string" } + } + }) + } + + #[test] + fn test_multipart_binary_only_succeeds() { + let spec = make_multipart_spec( + "upload_file", + serde_json::json!({ + "file": binary_file_prop() + }), + ); + assert_generates_ok(&spec, "Binary-only multipart should be supported"); + } + + #[test] + fn test_multipart_mixed_types_supported() { + let spec = make_multipart_spec( + "upload_with_name", + serde_json::json!({ + "file": binary_file_prop(), + "name": string_prop() + }), + ); + let code = assert_generates_ok(&spec, "Mixed types in multipart should be supported"); + assert!(code.contains("UploadWithNameForm")); + } + + #[test] + fn test_multipart_nested_objects_supported() { + let spec = make_multipart_spec( + "upload_with_metadata", + serde_json::json!({ + "file": binary_file_prop(), + "metadata": { + "type": "object", + "properties": { + "author": { "type": "string" } + } + } + }), + ); + assert_generates_ok(&spec, "Nested objects in multipart should be supported"); + } + + #[test] + fn test_multipart_integer_fields_supported() { + let spec = make_multipart_spec( + "upload_with_count", + serde_json::json!({ + "file": binary_file_prop(), + "count": integer_prop() + }), + ); + assert_generates_ok(&spec, "Integer fields in multipart should be supported"); + } + + #[test] + fn test_multipart_array_fields_supported() { + let spec = make_multipart_spec( + "upload_with_tags", + serde_json::json!({ + "file": binary_file_prop(), + "tags": string_array_prop() + }), + ); + assert_generates_ok(&spec, "Array fields in multipart should be supported"); + } + + #[test] + fn test_multipart_multiple_binary_files_succeeds() { + let spec = make_multipart_spec( + "upload_multiple", + serde_json::json!({ + "primary": binary_file_prop(), + "secondary": binary_file_prop(), + "thumbnail": binary_file_prop() + }), + ); + assert_generates_ok(&spec, "Multiple binary files should be supported"); + } + + #[test] + fn test_multipart_encoding_content_type() { + let spec = make_multipart_spec_with_encoding( + "upload_with_encoding", + serde_json::json!({ + "file": binary_file_prop(), + "metadata": { + "type": "object", + "properties": { + "author": string_prop(), + "tags": string_array_prop() + } + } + }), + serde_json::json!({ + "file": { "contentType": "application/octet-stream" }, + "metadata": { "contentType": "application/json" } + }), + ); + let code = assert_generates_ok(&spec, "Encoding object should be supported"); + assert!(code.contains("UploadWithEncodingForm")); + } + + #[test] + fn test_multipart_array_json_serialization() { + let spec = make_multipart_spec( + "upload_with_array", + serde_json::json!({ + "tags": string_array_prop() + }), + ); + let code = assert_generates_ok(&spec, "Array fields should be supported"); + // TokenStream::to_string() uses spaces between tokens + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_object_json_serialization() { + let spec = make_multipart_spec( + "upload_with_object", + serde_json::json!({ + "metadata": simple_object_prop() + }), + ); + let code = assert_generates_ok(&spec, "Object fields should be supported"); + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_boolean_fields_supported() { + let spec = make_multipart_spec( + "upload_with_flag", + serde_json::json!({ + "file": binary_file_prop(), + "is_public": boolean_prop() + }), + ); + let code = assert_generates_ok(&spec, "Boolean fields in multipart should be supported"); + assert!(code.contains("UploadWithFlagForm")); + } + + #[test] + fn test_multipart_number_fields_supported() { + let spec = make_multipart_spec( + "upload_with_score", + serde_json::json!({ + "file": binary_file_prop(), + "score": number_prop() + }), + ); + assert_generates_ok(&spec, "Number fields in multipart should be supported"); + } + + #[test] + fn test_multipart_required_fields() { + let spec: OpenAPI = serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_required", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["file", "name"], + "properties": { + "file": binary_file_prop(), + "name": string_prop(), + "description": string_prop() + } + } + } + } + }, + "responses": { "200": { "description": "OK" } } + } + } + } + })) + .unwrap(); + + let code = assert_generates_ok(&spec, "Required fields in multipart should be supported"); + assert!(code.contains("UploadRequiredForm")); + } + + #[test] + fn test_multipart_with_schema_reference() { + let spec: OpenAPI = serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_with_ref", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": binary_file_prop(), + "metadata": { "$ref": "#/components/schemas/Metadata" } + } + } + } + } + }, + "responses": { "200": { "description": "OK" } } + } + } + }, + "components": { + "schemas": { + "Metadata": { + "type": "object", + "properties": { + "author": { "type": "string" }, + "version": { "type": "integer" } + } + } + } + } + })) + .unwrap(); + + let code = assert_generates_ok(&spec, "Schema references in multipart should be supported"); + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_non_object_schema_fails() { + let spec: OpenAPI = serde_json::from_value(serde_json::json!({ + "openapi": "3.0.0", + "info": { "title": "Test", "version": "1.0.0" }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_bad", + "requestBody": { + "content": { + "multipart/form-data": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { "200": { "description": "OK" } } + } + } + } + })) + .unwrap(); + + assert_generates_err(&spec, "object schema"); + } + + #[test] + #[should_panic(expected = "unreachable")] + fn test_multipart_empty_properties_panics() { + let spec = make_multipart_spec("upload_empty", serde_json::json!({})); + let mut generator = make_builder_generator(); + let _ = generator.generate_tokens(&spec); + } + + #[test] + fn test_multipart_array_of_integers() { + let spec = make_multipart_spec( + "upload_ids", + serde_json::json!({ + "ids": integer_array_prop() + }), + ); + let code = assert_generates_ok(&spec, "Array of integers in multipart should be supported"); + assert!(code.contains("serde_json :: to_string")); + } + + #[test] + fn test_multipart_deeply_nested_object() { + let spec = make_multipart_spec( + "upload_nested", + serde_json::json!({ + "config": { + "type": "object", + "properties": { + "settings": { + "type": "object", + "properties": { + "level1": { + "type": "object", + "properties": { + "value": { "type": "string" } + } + } + } + } + } + } + }), + ); + assert_generates_ok( + &spec, + "Deeply nested objects in multipart should be supported", + ); + } + + #[test] + fn test_multipart_multiple_encoding_entries() { + let spec = make_multipart_spec_with_encoding( + "upload_multi_encoding", + serde_json::json!({ + "image": binary_file_prop(), + "document": binary_file_prop(), + "data": simple_object_prop() + }), + serde_json::json!({ + "image": { "contentType": "image/png" }, + "document": { "contentType": "application/pdf" }, + "data": { "contentType": "application/xml" } + }), + ); + let code = assert_generates_ok(&spec, "Multiple encoding entries should be supported"); + assert!(code.contains("image/png") || code.contains("image / png")); + } + + #[test] + fn test_multipart_positional_interface() { + let spec = make_multipart_spec( + "upload_file", + serde_json::json!({ + "file": binary_file_prop(), + "name": string_prop() + }), + ); + + let mut generator = make_positional_generator(); + let result = generator.generate_tokens(&spec); + assert!( + result.is_ok(), + "Multipart should work with positional interface: {:?}", + result.err() + ); + assert!(result.unwrap().to_string().contains("UploadFileForm")); + } + + #[test] + fn test_multipart_enum_string_field() { + let spec = make_multipart_spec( + "upload_with_category", + serde_json::json!({ + "file": binary_file_prop(), + "category": { + "type": "string", + "enum": ["image", "document", "video"] + } + }), + ); + assert_generates_ok(&spec, "Enum string fields in multipart should be supported"); + } +} diff --git a/sample_openapi/example_multipart.json b/sample_openapi/example_multipart.json new file mode 100644 index 000000000..0bcae8a5b --- /dev/null +++ b/sample_openapi/example_multipart.json @@ -0,0 +1,39 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Multipart Example", + "version": "1.0.0" + }, + "paths": { + "/upload": { + "post": { + "operationId": "upload", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["file", "name"], + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "name": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + } +} From a090aa96af927ec54a4db772d65b301d994ec0a6 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Fri, 16 Jan 2026 12:30:27 +0100 Subject: [PATCH 20/23] fmt --- progenitor-client/tests/client_test.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/progenitor-client/tests/client_test.rs b/progenitor-client/tests/client_test.rs index 59503195e..6c300b62f 100644 --- a/progenitor-client/tests/client_test.rs +++ b/progenitor-client/tests/client_test.rs @@ -430,10 +430,7 @@ mod form_part_tests { #[test] fn test_content_type_application() { assert_eq!(ContentType::application("pdf").as_str(), "application/pdf"); - assert_eq!( - ContentType::application("xml").as_str(), - "application/xml" - ); + assert_eq!(ContentType::application("xml").as_str(), "application/xml"); } #[test] From 85e8358bf54bcbfcf488d9bf5604671bf9802c1f Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 30 Jun 2026 09:33:33 +0200 Subject: [PATCH 21/23] Cleanup --- Cargo.toml | 8 -------- progenitor-client/src/progenitor_client.rs | 23 ---------------------- 2 files changed, 31 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6b9b279b2..f9f72f3f9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,11 +59,3 @@ url = "2.5.7" unicode-ident = "1.0.22" uuid = { version = "1.19.0", features = ["serde", "v4"] } assert_matches = "1.5.0" - -#[patch."https://github.com/oxidecomputer/typify"] -#typify = { path = "../typify/typify" } - -#[patch.crates-io] -#serde_tokenstream = { path = "../serde_tokenstream" } -#typify = { path = "../typify/typify" } -#rustfmt-wrapper = { path = "../rustfmt-wrapper" } diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index a0db1b2f1..f891caa51 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -931,11 +931,6 @@ where { fn form_urlencoded(self, body: &T) -> Result>; - fn form_from_raw, T: AsRef<[u8]>, I: Sized + IntoIterator>( - self, - iter: I, - ) -> Result>; - fn form_from_parts, I: Sized + IntoIterator>( self, iter: I, @@ -955,24 +950,6 @@ impl RequestBuilderExt for RequestBuilder { )) } - fn form_from_raw, T: AsRef<[u8]>, I: Sized + IntoIterator>( - self, - iter: I, - ) -> Result> { - use reqwest::multipart::Form; - - let mut form = Form::new(); - for (name, value) in iter { - form = form.part( - name.as_ref().to_owned(), - Part::stream(Vec::from(value.as_ref())), - ); - } - // Note: reqwest's .multipart() automatically sets the Content-Type header - // with the correct boundary, so we don't set it manually here. - Ok(self.multipart(form)) - } - fn form_from_parts, I: Sized + IntoIterator>( self, iter: I, From adc1bf1a993663c6803774bcfcd66bdc529f7554 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 30 Jun 2026 09:39:23 +0200 Subject: [PATCH 22/23] backticks --- progenitor-impl/tests/output/src/example_multipart_builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/progenitor-impl/tests/output/src/example_multipart_builder.rs b/progenitor-impl/tests/output/src/example_multipart_builder.rs index 5e2d0ea72..b3ec88183 100644 --- a/progenitor-impl/tests/output/src/example_multipart_builder.rs +++ b/progenitor-impl/tests/output/src/example_multipart_builder.rs @@ -141,9 +141,9 @@ pub mod types { } impl UploadForm { - /// Convert this form into an iterator of (field_name, field_value) + /// Convert this form into an iterator of `(field_name, field_value)` /// pairs - /// suitable for multipart/form-data encoding. + /// suitable for `multipart/form-data` encoding. pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { let mut parts = Vec::new(); if let Some(ref val) = self.file { From f9ec5607a89bad41f4b4d8905bed88accb0efc54 Mon Sep 17 00:00:00 2001 From: Bernhard Schuster Date: Tue, 30 Jun 2026 10:16:58 +0200 Subject: [PATCH 23/23] better --- progenitor-client/src/progenitor_client.rs | 5 +++-- progenitor-impl/src/lib.rs | 7 ++++--- .../tests/output/src/example_multipart_builder_tagged.rs | 4 ++-- .../tests/output/src/example_multipart_positional.rs | 4 ++-- 4 files changed, 11 insertions(+), 9 deletions(-) diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index f891caa51..a23ddff61 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -8,7 +8,7 @@ use std::ops::{Deref, DerefMut}; use bytes::Bytes; use futures_core::Stream; -use reqwest::{multipart::Part, RequestBuilder}; +use reqwest::{multipart::Part, Body, RequestBuilder}; use serde::{de::DeserializeOwned, ser::SerializeStruct, Serialize}; #[cfg(not(target_arch = "wasm32"))] @@ -965,7 +965,8 @@ impl RequestBuilderExt for RequestBuilder { filename, content_type, }) => { - let mut p = Part::stream(data.to_vec()); + let body = Body::from(data); + let mut p = Part::stream(body); if let Some(fname) = filename { p = p.file_name(fname.into_string()); } diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index b48bbbdea..fa986c7fa 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -447,7 +447,8 @@ impl Generator { if let Some(ref val) = self.#ident { parts.push((#api_name, progenitor_client::FormPart::Text( progenitor_client::TextFormPart { - value: serde_json::to_string(val).unwrap_or_default(), + value: serde_json::to_string(val) + .expect("multipart form field serialization should not fail"), content_type: #json_content_type, } ))); @@ -472,8 +473,8 @@ impl Generator { quote! { impl #form_name { - /// Convert this form into an iterator of (field_name, field_value) pairs - /// suitable for multipart/form-data encoding. + /// Convert this form into an iterator of `(field_name, field_value)` pairs + /// suitable for `multipart/form-data` encoding. pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { let mut parts = Vec::new(); #(#field_conversions)* diff --git a/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs b/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs index e486c003d..85bf205ff 100644 --- a/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs +++ b/progenitor-impl/tests/output/src/example_multipart_builder_tagged.rs @@ -139,9 +139,9 @@ pub mod types { } impl UploadForm { - /// Convert this form into an iterator of (field_name, field_value) + /// Convert this form into an iterator of `(field_name, field_value)` /// pairs - /// suitable for multipart/form-data encoding. + /// suitable for `multipart/form-data` encoding. pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { let mut parts = Vec::new(); if let Some(ref val) = self.file { diff --git a/progenitor-impl/tests/output/src/example_multipart_positional.rs b/progenitor-impl/tests/output/src/example_multipart_positional.rs index e290da4ac..d16c337ab 100644 --- a/progenitor-impl/tests/output/src/example_multipart_positional.rs +++ b/progenitor-impl/tests/output/src/example_multipart_positional.rs @@ -71,9 +71,9 @@ pub mod types { } impl UploadForm { - /// Convert this form into an iterator of (field_name, field_value) + /// Convert this form into an iterator of `(field_name, field_value)` /// pairs - /// suitable for multipart/form-data encoding. + /// suitable for `multipart/form-data` encoding. pub fn as_form(&self) -> Vec<(&'static str, progenitor_client::FormPart)> { let mut parts = Vec::new(); if let Some(ref val) = self.file {