diff --git a/cargo-progenitor/src/main.rs b/cargo-progenitor/src/main.rs index 191675a7..59ec3c24 100644 --- a/cargo-progenitor/src/main.rs +++ b/cargo-progenitor/src/main.rs @@ -291,7 +291,7 @@ pub fn dependencies(builder: Generator, include_client: bool) -> Vec { deps.push(format!("base64 = \"{}\"", DEPENDENCIES.base64)); deps.push(format!("rand = \"{}\"", DEPENDENCIES.rand)); } - if type_space.uses_serde_json() || needs_serde_json { + if type_space.uses_serde_json() || needs_serde_json || builder.uses_serde_json() { deps.push(format!("serde_json = \"{}\"", DEPENDENCIES.serde_json)); } deps.sort_unstable(); diff --git a/progenitor-client/src/progenitor_client.rs b/progenitor-client/src/progenitor_client.rs index 6f8dcca0..5cb1500c 100644 --- a/progenitor-client/src/progenitor_client.rs +++ b/progenitor-client/src/progenitor_client.rs @@ -527,8 +527,62 @@ pub fn encode_path(pc: &str) -> String { } #[doc(hidden)] +/// A part in a multipart/related message (RFC 2387). +/// +/// Each part has a Content-Type, Content-ID, and binary content. +/// +/// Uses Cow to avoid cloning binary payloads while allowing owned data +/// for serialized JSON content. +pub struct MultipartPart<'a> { + /// The MIME type of this part (e.g., "application/json", "image/png"). + pub content_type: &'a str, + /// The Content-ID value (appears as `` in headers). + pub content_id: &'a str, + /// The binary content of this part. + /// Uses Cow to allow both borrowed (binary fields) and owned (serialized JSON) data. + pub bytes: std::borrow::Cow<'a, [u8]>, +} + +/// Trait for types that can be serialized as RFC 2387 multipart/related bodies. +/// +/// This trait is automatically implemented for multipart/related request body types +/// generated from OpenAPI specifications. +/// +/// # Panics +/// +/// Implementations may panic if: +/// - JSON serialization of structured fields fails (indicates non-serializable type) +/// - Required fields are not populated (indicates programmer error) +/// +/// # Notes +/// +/// - The first part returned is the "root" part (per RFC 2387 Section 3.2) +/// - Optional fields should be excluded from the result vec when not provided +/// - Part order should match the OpenAPI schema property order +#[doc(hidden)] +pub trait MultipartRelatedBody { + /// Convert this body into a vector of multipart parts. + /// + /// The returned vector should: + /// - Be non-empty (at least one part) + /// - Have the root part first + /// - Exclude optional parts that are None/empty + /// - Preserve schema property order + fn as_multipart_parts(&self) -> Vec>; +} + +// Blanket impl for references - allows .multipart_related(&body) to work +impl MultipartRelatedBody for &T { + fn as_multipart_parts(&self) -> Vec> { + (*self).as_multipart_parts() + } +} + +#[doc(hidden)] +#[allow(clippy::result_large_err)] pub trait RequestBuilderExt { fn form_urlencoded(self, body: &T) -> Result>; + fn multipart_related(self, body: &T) -> Result>; } impl RequestBuilderExt for RequestBuilder { @@ -543,6 +597,113 @@ impl RequestBuilderExt for RequestBuilder { .map_err(|_| Error::InvalidRequest("failed to serialize body".to_string()))?, )) } + + fn multipart_related(self, body: &T) -> Result> { + // Generate a unique boundary + let boundary = generate_multipart_boundary(); + + // Get the parts from the body + let parts = body.as_multipart_parts(); + + // RFC 2387 requires at least one part + if parts.is_empty() { + return Err(Error::InvalidRequest( + "multipart/related requires at least one part".to_string() + )); + } + + // RFC 2387: The 'type' parameter must be the content-type of the root part (first part) + let root_content_type = &parts[0].content_type; + + // Quote the type parameter per RFC 2045 if it contains special characters + let quoted_type = quote_mime_parameter(root_content_type); + + // Preallocate body buffer based on estimated size + let estimated_size: usize = parts.iter() + .map(|p| p.bytes.len() + 200) // 200 bytes for headers per part + .sum::() + 500; // Extra for boundaries and final marker + let mut body_bytes = Vec::with_capacity(estimated_size); + + // Write each part + for part in &parts { + // Validate Content-ID to prevent header injection + validate_content_id(part.content_id)?; + + // Write boundary + body_bytes.extend_from_slice(format!("--{}\r\n", boundary).as_bytes()); + + // Write Content-Type header + body_bytes.extend_from_slice(format!("Content-Type: {}\r\n", part.content_type).as_bytes()); + + // Write Content-ID header + body_bytes.extend_from_slice(format!("Content-ID: <{}>\r\n\r\n", part.content_id).as_bytes()); + + // Write content bytes + body_bytes.extend_from_slice(&part.bytes); + + // Write CRLF after content + body_bytes.extend_from_slice(b"\r\n"); + } + + // Write closing boundary + body_bytes.extend_from_slice(format!("--{}--\r\n", boundary).as_bytes()); + + // Set Content-Type header with boundary and type from root part + let content_type = format!("multipart/related; boundary={}; type={}", boundary, quoted_type); + + Ok(self + .header( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_str(&content_type) + .map_err(|e| Error::InvalidRequest(format!("invalid content-type header: {}", e)))?, + ) + .body(body_bytes)) + } +} + +fn generate_multipart_boundary() -> String { + use std::sync::atomic::{AtomicU64, Ordering}; + static COUNTER: AtomicU64 = AtomicU64::new(0); + + // Generate a unique boundary using timestamp, counter, and process ID for better uniqueness + let count = COUNTER.fetch_add(1, Ordering::SeqCst); + let timestamp = std::time::SystemTime::now() + .duration_since(std::time::UNIX_EPOCH) + .map(|d| d.as_nanos()) + .unwrap_or_else(|_| { + // If system time is before epoch, use a large random-like number + // based on monotonic time + std::time::Instant::now().elapsed().as_nanos() + }); + let pid = std::process::id(); + + // Use nanos for more precision and include PID for multi-process uniqueness + format!("progenitor_boundary_{:x}_{:x}_{:x}", timestamp, pid, count) +} + +/// Quote a MIME type parameter value per RFC 2045 if it contains special characters +fn quote_mime_parameter(value: &str) -> String { + // Check if value needs quoting (contains tspecials per RFC 2045) + if value.contains(|c: char| { + matches!(c, '(' | ')' | '<' | '>' | '@' | ',' | ';' | ':' | '\\' | '"' | '/' | '[' | ']' | '?' | '=' | ' ' | '\t') + }) { + // Quote and escape the value + format!("\"{}\"", value.replace('\\', "\\\\").replace('"', "\\\"")) + } else { + value.to_string() + } +} + +/// Validate Content-ID to prevent header injection attacks +#[allow(clippy::result_large_err)] +fn validate_content_id(content_id: &str) -> Result<(), Error> { + // Content-ID must not contain control characters, angle brackets, or CRLF + if content_id.contains(|c: char| c == '<' || c == '>' || c.is_control()) { + return Err(Error::InvalidRequest( + format!("Invalid Content-ID contains forbidden characters: {}", content_id) + )); + } + Ok(()) } #[doc(hidden)] diff --git a/progenitor-impl/src/cli.rs b/progenitor-impl/src/cli.rs index ed32ff95..0bbe82c2 100644 --- a/progenitor-impl/src/cli.rs +++ b/progenitor-impl/src/cli.rs @@ -389,7 +389,7 @@ impl Generator { } let first_page_required = first_page_required_set - .map_or(false, |required| required.contains(¶m.api_name)); + .is_some_and(|required| required.contains(¶m.api_name)); let volitionality = if innately_required || first_page_required { Volitionality::Required @@ -440,7 +440,8 @@ impl Generator { // are currently... OperationParameterType::RawBody => None, - OperationParameterType::Type(body_type_id) => Some(body_type_id), + OperationParameterType::Type(body_type_id) + | OperationParameterType::MultipartRelated(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 1e7f9745..a90fe517 100644 --- a/progenitor-impl/src/httpmock.rs +++ b/progenitor-impl/src/httpmock.rs @@ -154,6 +154,7 @@ impl Generator { kind, api_name, description: _, + .. }| { let arg_type_name = match typ { OperationParameterType::Type(arg_type_id) => self @@ -161,8 +162,14 @@ impl Generator { .get_type(arg_type_id) .unwrap() .parameter_ident(), + OperationParameterType::MultipartRelated(arg_type_id) => self + .type_space + .get_type(arg_type_id) + .unwrap() + .parameter_ident(), OperationParameterType::RawBody => match kind { - OperationParameterKind::Body(BodyContentType::OctetStream) => quote! { + OperationParameterKind::Body(BodyContentType::OctetStream) + | OperationParameterKind::Body(BodyContentType::MultipartRelated) => quote! { ::serde_json::Value }, OperationParameterKind::Body(BodyContentType::Text(_)) => quote! { @@ -250,8 +257,14 @@ impl Generator { }, ), + OperationParameterType::MultipartRelated(_) => ( + true, + quote! { + Self(self.0.json_body_obj(value)) + }, + ), OperationParameterType::RawBody => match body_content_type { - BodyContentType::OctetStream => ( + BodyContentType::OctetStream | BodyContentType::MultipartRelated => ( true, quote! { Self(self.0.json_body(value)) diff --git a/progenitor-impl/src/lib.rs b/progenitor-impl/src/lib.rs index 68454ece..df1b4ea3 100644 --- a/progenitor-impl/src/lib.rs +++ b/progenitor-impl/src/lib.rs @@ -11,7 +11,7 @@ use proc_macro2::TokenStream; use quote::quote; use serde::Deserialize; use thiserror::Error; -use typify::{TypeSpace, TypeSpaceSettings}; +use typify::{TypeId, TypeSpace, TypeSpaceSettings}; use crate::to_schema::ToSchema; @@ -47,12 +47,23 @@ pub enum Error { #[allow(missing_docs)] pub type Result = std::result::Result; +/// Information about a multipart/related type +struct MultipartRelatedInfo { + /// Property names in schema order + property_order: Vec, + /// Set of required property names + required_fields: std::collections::HashSet, +} + /// OpenAPI generator. pub struct Generator { type_space: TypeSpace, + /// Maps multipart/related type IDs to their schema information + multipart_related: indexmap::IndexMap, settings: GenerationSettings, uses_futures: bool, uses_websockets: bool, + uses_serde_json: bool, } /// Settings for [Generator]. @@ -261,9 +272,11 @@ impl Default for Generator { fn default() -> Self { Self { type_space: TypeSpace::new(TypeSpaceSettings::default().with_type_mod("types")), + multipart_related: Default::default(), settings: Default::default(), uses_futures: Default::default(), uses_websockets: Default::default(), + uses_serde_json: Default::default(), } } } @@ -312,9 +325,11 @@ impl Generator { Self { type_space: TypeSpace::new(&type_settings), + multipart_related: Default::default(), settings: settings.clone(), uses_futures: false, uses_websockets: false, + uses_serde_json: false, } } @@ -374,6 +389,148 @@ impl Generator { let types = self.type_space.to_stream(); + // Generate MultipartRelatedBody trait impl for multipart/related types + let multipart_helpers = TokenStream::from_iter( + self.multipart_related + .iter() + .map(|(type_id, info)| { + let typ = self.get_type_space().get_type(type_id).unwrap(); + let type_name = typ.ident(); + + let td = typ.details(); + let typify::TypeDetails::Struct(tstru) = td else { + panic!("multipart/related type must be a struct"); + }; + + // Build a map of property names to their type IDs for lookup + let prop_map: std::collections::HashMap<_, _> = tstru.properties().collect(); + + // Generate code to extract each property in the order from the OpenAPI schema + let parts_extraction = info.property_order.iter().filter_map(|prop_name| { + // Skip content_type fields - they're metadata, not parts + if prop_name.ends_with("_content_type") { + return None; + } + + let prop_id = prop_map.get(prop_name.as_str())?; + let prop_ty = self.get_type_space().get_type(prop_id).ok()?; + let field_ident = quote::format_ident!("{}", prop_name); + + // Check if this field is optional (not in the required array) + let is_optional = !info.required_fields.contains(prop_name); + + // Check if this is a binary field + let is_binary = match prop_ty.details() { + // Check for Vec (required binary field) + typify::TypeDetails::Vec(inner_id) => { + if let Ok(inner_ty) = self.get_type_space().get_type(&inner_id) { + inner_ty.ident().to_string() == "u8" + } else { + false + } + } + // Check for Option> (optional binary field) + typify::TypeDetails::Option(inner_id) => { + if let Ok(inner_ty) = self.get_type_space().get_type(&inner_id) { + if let typify::TypeDetails::Vec(vec_inner_id) = inner_ty.details() { + if let Ok(vec_inner_ty) = self.get_type_space().get_type(&vec_inner_id) { + vec_inner_ty.ident().to_string() == "u8" + } else { + false + } + } else { + false + } + } else { + false + } + } + _ => false, + }; + + if is_binary { + let content_type_field = quote::format_ident!("{}_content_type", prop_name); + if is_optional { + // Optional binary field: field is Vec with #[serde(default)], + // content_type is Option. Only include if content_type is Some and vec is non-empty. + Some(quote! { + if let Some(ref content_type) = self.#content_type_field { + if !self.#field_ident.is_empty() { + Some(crate::progenitor_client::MultipartPart { + content_type: content_type.as_str(), + content_id: #prop_name, + bytes: ::std::borrow::Cow::Borrowed(&self.#field_ident), + }) + } else { + None + } + } else { + None + } + }) + } else { + // Required binary field: use Cow::Borrowed to avoid cloning + Some(quote! { + Some(crate::progenitor_client::MultipartPart { + content_type: &self.#content_type_field, + content_id: #prop_name, + bytes: ::std::borrow::Cow::Borrowed(&self.#field_ident), + }) + }) + } + } else if is_optional { + // Optional structured field: JSON serialization creates owned data + Some(quote! { + self.#field_ident.as_ref().map(|value| { + crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: #prop_name, + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(value) + .expect("failed to serialize field") + ), + } + }) + }) + } else { + // Required structured field: JSON serialization creates owned data + Some(quote! { + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: #prop_name, + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.#field_ident) + .expect("failed to serialize field") + ), + }) + }) + } + }).collect::>(); + + // Since this impl is generated inside the types module (see line 523), + // we need to use just the type name without module prefix + let type_name_str = type_name.to_string(); + // Remove "types ::" prefix (note: TokenStream.to_string() adds spaces) + let bare_type_name = type_name_str + .strip_prefix("types :: ") + .unwrap_or(&type_name_str); + let bare_type_name = quote::format_ident!("{}", bare_type_name); + + quote! { + impl crate::progenitor_client::MultipartRelatedBody for #bare_type_name { + fn as_multipart_parts(&self) -> Vec { + vec![ + #(#parts_extraction),* + ] + .into_iter() + .flatten() + .collect() + } + } + } + }), + ); + 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 +597,8 @@ impl Generator { #[allow(clippy::all)] pub mod types { #types + + #multipart_helpers } #[derive(Clone, Debug)] @@ -664,6 +823,12 @@ impl Generator { pub fn uses_websockets(&self) -> bool { self.uses_websockets } + + /// Whether the generated client uses serde_json (e.g., for multipart/related + /// serialization). + pub fn uses_serde_json(&self) -> bool { + self.uses_serde_json + } } /// Add newlines after end-braces at <= two levels of indentation. diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index 8bf1d0fe..c42ea780 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -100,11 +100,14 @@ pub struct OperationParameter { pub description: Option, pub typ: OperationParameterType, pub kind: OperationParameterKind, + /// Default value from the schema, if present. + pub default: Option, } -#[derive(Eq, PartialEq)] +#[derive(Debug, Eq, PartialEq)] pub enum OperationParameterType { Type(TypeId), + MultipartRelated(TypeId), RawBody, } @@ -137,6 +140,7 @@ pub enum BodyContentType { OctetStream, Json, FormUrlencoded, + MultipartRelated, 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), + "multipart/related" => Ok(Self::MultipartRelated), "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::MultipartRelated => "multipart/related", Self::Text(typ) => typ, }) } @@ -326,6 +332,7 @@ impl Generator { description: parameter_data.description.clone(), typ: OperationParameterType::Type(typ), kind: OperationParameterKind::Path, + default: None, }) } openapiv3::Parameter::Query { @@ -334,7 +341,17 @@ impl Generator { style: openapiv3::QueryStyle::Form, allow_empty_value: _, // Irrelevant for this client } => { - let schema = parameter_data.schema()?.to_schema(); + let schema_ref = parameter_data.schema()?; + let schema = schema_ref.to_schema(); + + // Extract default value from the schema if present + let default_value = match schema_ref { + openapiv3::ReferenceOr::Item(schema_kind) => { + schema_kind.schema_data.default.clone() + } + _ => None, + }; + let name = sanitize( &format!( "{}-{}", @@ -365,13 +382,24 @@ impl Generator { description: parameter_data.description.clone(), typ: OperationParameterType::Type(type_id), kind: OperationParameterKind::Query(required), + default: default_value, }) } openapiv3::Parameter::Header { parameter_data, style: openapiv3::HeaderStyle::Simple, } => { - let schema = parameter_data.schema()?.to_schema(); + let schema_ref = parameter_data.schema()?; + let schema = schema_ref.to_schema(); + + // Extract default value from the schema if present + let default_value = match schema_ref { + openapiv3::ReferenceOr::Item(schema_kind) => { + schema_kind.schema_data.default.clone() + } + _ => None, + }; + let name = sanitize( &format!( "{}-{}", @@ -389,6 +417,7 @@ impl Generator { description: parameter_data.description.clone(), typ: OperationParameterType::Type(typ), kind: OperationParameterKind::Header(parameter_data.required), + default: default_value, }) } openapiv3::Parameter::Path { style, .. } => Err(Error::UnexpectedFormat( @@ -575,8 +604,18 @@ impl Generator { .parameter_ident_with_lifetime("a"); quote! { Option<#t> } } + (OperationParameterType::MultipartRelated(type_id), false) => { + let t = self + .type_space + .get_type(type_id) + .unwrap() + .parameter_ident_with_lifetime("a"); + quote! { #t } + } + (OperationParameterType::MultipartRelated(_), true) => unreachable!(), (OperationParameterType::RawBody, false) => match ¶m.kind { - OperationParameterKind::Body(BodyContentType::OctetStream) => { + OperationParameterKind::Body(BodyContentType::OctetStream) + | OperationParameterKind::Body(BodyContentType::MultipartRelated) => { quote! { B } } OperationParameterKind::Body(BodyContentType::Text(_)) => { @@ -594,7 +633,11 @@ impl Generator { let raw_body_param = method.params.iter().any(|param| { param.typ == OperationParameterType::RawBody - && param.kind == OperationParameterKind::Body(BodyContentType::OctetStream) + && matches!( + param.kind, + OperationParameterKind::Body(BodyContentType::OctetStream) + | OperationParameterKind::Body(BodyContentType::MultipartRelated) + ) }); let bounds = if raw_body_param { @@ -891,6 +934,17 @@ impl Generator { ) .body(body) }), + ( + OperationParameterKind::Body(BodyContentType::MultipartRelated), + OperationParameterType::RawBody, + ) => Some(quote! { + // Set the content type for manual multipart/related construction + .header( + ::reqwest::header::CONTENT_TYPE, + ::reqwest::header::HeaderValue::from_static("multipart/related"), + ) + .body(body) + }), ( OperationParameterKind::Body(BodyContentType::Text(mime_type)), OperationParameterType::RawBody, @@ -918,6 +972,14 @@ impl Generator { // returns an error in the case of a serialization failure. .form_urlencoded(&body)? }), + ( + OperationParameterKind::Body(BodyContentType::MultipartRelated), + OperationParameterType::MultipartRelated(_), + ) => Some(quote! { + // This uses progenitor_client::RequestBuilderExt which + // constructs a multipart/related request body. + .multipart_related(&body)? + }), (OperationParameterKind::Body(_), _) => { unreachable!("invalid body kind/type combination") } @@ -1420,165 +1482,416 @@ impl Generator { let struct_name = sanitize(&method.operation_id, Case::Pascal); let struct_ident = format_ident!("{}", struct_name); - // Generate an ident for each parameter. - let param_names = method + // Expanded parameters: MultipartRelated params are expanded into their + // constituent properties + #[derive(Clone)] + struct ExpandedParam { + name: String, + type_id: TypeId, + required: bool, + is_binary: bool, + default: Option, + } + + let expanded_params: Vec = method .params .iter() - .map(|param| format_ident!("{}", param.name)) + .flat_map(|param| { + match ¶m.typ { + OperationParameterType::MultipartRelated(type_id) => { + // Expand into individual properties + let ty = self.type_space.get_type(type_id).unwrap(); + let typify::TypeDetails::Struct(struct_details) = ty.details() else { + panic!("multipart type must be a struct"); + }; + + struct_details + .properties() + .filter_map(|(prop_name, prop_type_id)| { + // Skip content_type fields - they're metadata, not builder params + if prop_name.ends_with("_content_type") { + return None; + } + + // Check if this property is binary by looking at the type + let prop_ty = self.type_space.get_type(&prop_type_id).ok()?; + let is_binary = if let typify::TypeDetails::Vec(inner_id) = prop_ty.details() { + // Vec is binary + if let Ok(inner_ty) = self.type_space.get_type(&inner_id) { + inner_ty.ident().to_string() == "u8" + } else { + false + } + } else { + false + }; + + Some(ExpandedParam { + name: prop_name.to_string(), + type_id: prop_type_id, + required: true, // TODO: Check if property is required + is_binary, + default: None, // Multipart properties don't have defaults + }) + }) + .collect::>() + } + OperationParameterType::Type(type_id) => { + let ty = self.type_space.get_type(type_id).unwrap(); + // Skip body parameters with builders - they use the old approach + if matches!(¶m.kind, OperationParameterKind::Body(_)) && ty.builder().is_some() { + vec![] + } else { + vec![ExpandedParam { + name: param.name.clone(), + type_id: type_id.clone(), + required: param.kind.is_required(), + is_binary: false, + default: param.default.clone(), + }] + } + } + OperationParameterType::RawBody => { + // RawBody doesn't have a TypeId, handle separately + vec![] + } + } + }) + .collect(); + + // Generate an ident for each expanded parameter + let param_names = expanded_params + .iter() + .map(|ep| format_ident!("{}", ep.name)) + .chain( + // Add content_type fields for binary expanded params + expanded_params.iter().filter_map(|ep| { + if ep.is_binary { + Some(format_ident!("{}_content_type", ep.name)) + } else { + None + } + }) + ) + .chain( + // Add Type params with builders + method.params.iter().filter_map(|param| { + match ¶m.typ { + OperationParameterType::Type(type_id) => { + let ty = self.type_space.get_type(type_id).ok()?; + if let (OperationParameterKind::Body(_), Some(_)) = + (¶m.kind, ty.builder()) + { + Some(format_ident!("{}", param.name)) + } else { + None + } + } + _ => None, + } + }) + ) + .chain( + // Add RawBody params separately + method.params.iter().filter_map(|param| { + if param.typ == OperationParameterType::RawBody { + Some(format_ident!("{}", param.name)) + } else { + None + } + }) + ) .collect::>(); let client_ident = unique_ident_from("client", ¶m_names); let mut cloneable = true; - // Generate the type for each parameter. - let param_types = method - .params + // Generate the type for each expanded parameter + let param_types = expanded_params .iter() - .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { - let ty = self.type_space.get_type(type_id)?; - - // For body parameters only, if there's a builder we'll - // nest that within this builder. - if let (OperationParameterKind::Body(_), Some(builder_name)) = - (¶m.kind, ty.builder()) - { - Ok(quote! { Result<#builder_name, String> }) - } else if param.kind.is_required() { - let t = ty.ident(); - Ok(quote! { Result<#t, String> }) + .map(|ep| { + let ty = self.type_space.get_type(&ep.type_id)?; + + // For binary fields in multipart, use Vec + let type_token = if ep.is_binary { + quote! { Vec } + } else { + let t = ty.ident(); + quote! { #t } + }; + + if ep.required { + Ok(quote! { Result<#type_token, String> }) + } else { + Ok(quote! { Result, String> }) + } + }) + .chain( + // Add content_type fields for binary expanded params + expanded_params.iter().filter_map(|ep| { + if ep.is_binary { + // content_type is always required (String, not Option) + Some(Ok(quote! { Result })) } else { - let t = ty.ident(); - Ok(quote! { Result, String> }) + None } - } + }) + ) + .chain( + // Add Type params that weren't expanded (non-multipart bodies with builders) + method.params.iter().filter_map(|param| { + match ¶m.typ { + OperationParameterType::Type(type_id) => { + let ty = self.type_space.get_type(type_id).ok()?; + // Only handle builder-capable bodies here (others were expanded) + if let (OperationParameterKind::Body(_), Some(builder_name)) = + (¶m.kind, ty.builder()) + { + Some(Ok(quote! { Result<#builder_name, String> })) + } else { + None + } + } + _ => None, + } + }) + ) + .chain( + // Add RawBody params + method.params.iter().filter_map(|param| { + if param.typ == OperationParameterType::RawBody { + cloneable = false; + Some(Ok(quote! { Result })) + } else { + None + } + }) + ) + .collect::>>()?; - OperationParameterType::RawBody => { - cloneable = false; - Ok(quote! { Result }) + // Generate the default value for each expanded parameter + let param_values = expanded_params + .iter() + .map(|ep| { + if ep.required { + let err_msg = format!("{} was not initialized", ep.name); + Ok(quote! { Err(#err_msg.to_string()) }) + } else if ep.default.is_some() { + // Optional parameter with a default value - use Default::default() + Ok(quote! { Ok(Some(::std::default::Default::default())) }) + } else { + // Optional parameter without a default value + Ok(quote! { Ok(None) }) } }) + .chain( + // Add default values for content_type fields (all required, start uninitialized) + expanded_params.iter().filter_map(|ep| { + if ep.is_binary { + let err_msg = format!("{}_content_type was not initialized", ep.name); + Some(Ok(quote! { Err(#err_msg.to_string()) })) + } else { + None + } + }) + ) + .chain( + // Add default values for Type params with builders + method.params.iter().filter_map(|param| { + match ¶m.typ { + OperationParameterType::Type(type_id) => { + let ty = self.type_space.get_type(type_id).ok()?; + // Only handle builder-capable bodies here + if let (OperationParameterKind::Body(_), Some(_)) = + (¶m.kind, ty.builder()) + { + Some(Ok(quote! { Ok(::std::default::Default::default()) })) + } else { + None + } + } + _ => None, + } + }) + ) + .chain( + // Add RawBody params + method.params.iter().filter_map(|param| { + if param.typ == OperationParameterType::RawBody { + let err_msg = format!("{} was not initialized", param.name); + Some(Ok(quote! { Err(#err_msg.to_string()) })) + } else { + None + } + }) + ) .collect::>>()?; - // Generate the default value value for each parameter. For optional - // parameters it's just `Ok(None)`. For builders it's - // `Ok(Default::default())`. For required, non-builders it's an Err(_) - // that indicates which field isn't initialized. - let param_values = method - .params - .iter() - .map(|param| match ¶m.typ { + // Build param_finalize: for builder-capable bodies, convert Builder → Type + let param_finalize = method.params.iter().flat_map(|param| { + match ¶m.typ { + OperationParameterType::MultipartRelated(_) => { + // Multipart params were expanded into individual fields + let ty = self.type_space.get_type( + if let OperationParameterType::MultipartRelated(type_id) = ¶m.typ { + type_id + } else { + unreachable!() + } + ).unwrap(); + let typify::TypeDetails::Struct(struct_details) = ty.details() else { + panic!("multipart type must be a struct"); + }; + // Each property gets no finalization (already right type) + struct_details.properties().map(|_| quote! {}).collect::>() + } OperationParameterType::Type(type_id) => { - let ty = self.type_space.get_type(type_id)?; - - // Fill in the appropriate initial value for the - // param_types generated above. - if let (OperationParameterKind::Body(_), Some(_)) = (¶m.kind, ty.builder()) - { - Ok(quote! { Ok(::std::default::Default::default()) }) - } else if param.kind.is_required() { - let err_msg = format!("{} was not initialized", param.name); - Ok(quote! { Err(#err_msg.to_string()) }) + let ty = self.type_space.get_type(type_id).unwrap(); + if matches!(¶m.kind, OperationParameterKind::Body(_)) && ty.builder().is_some() { + // Builder-capable body: convert Builder → Type + let type_name = ty.ident(); + vec![quote! { + .and_then(|v| #type_name::try_from(v).map_err(|e| e.to_string())) + }] } else { - Ok(quote! { Ok(None) }) + // Non-body Type param: no finalization needed + vec![quote! {}] } } - OperationParameterType::RawBody => { - let err_msg = format!("{} was not initialized", param.name); - Ok(quote! { Err(#err_msg.to_string()) }) + // RawBody: no finalization needed + vec![quote! {}] } - }) - .collect::>>()?; + } + }).collect::>(); - // For builders we map `Ok` values to perform a `try_from` to attempt - // to convert the builder into the desired type. No "finalization" is - // required for non-builders (required or optional). - let param_finalize = method - .params + // For each expanded parameter, generate a setter method + let param_impls = expanded_params .iter() - .map(|param| match ¶m.typ { - OperationParameterType::Type(type_id) => { - let ty = self.type_space.get_type(type_id)?; - if ty.builder().is_some() { - let type_name = ty.ident(); + .map(|ep| { + let param_name = format_ident!("{}", ep.name); + let ty = self.type_space.get_type(&ep.type_id)?; + + // For binary fields, generate a method that accepts both value and content_type + if ep.is_binary { + let content_type_field = format_ident!("{}_content_type", ep.name); + let err_msg = format!("conversion to Vec for {} failed", ep.name); + let content_type_err = format!("conversion to String for {}_content_type failed", ep.name); + + if ep.required { Ok(quote! { - .and_then(|v| #type_name::try_from(v) - .map_err(|e| e.to_string())) + pub fn #param_name(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.#param_name = value.try_into() + .map_err(|_| #err_msg.to_string()); + self.#content_type_field = content_type.try_into() + .map_err(|_| #content_type_err.to_string()); + self + } }) } else { - Ok(quote! {}) + Ok(quote! { + pub fn #param_name(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.#param_name = value.try_into() + .map(Some) + .map_err(|_| #err_msg.to_string()); + self.#content_type_field = content_type.try_into() + .map(Some) + .map_err(|_| #content_type_err.to_string()); + self + } + }) } - } - OperationParameterType::RawBody => Ok(quote! {}), - }) - .collect::>>()?; + } else { + // Non-binary fields: use the generated type + let typ = ty.ident().to_token_stream(); + let err_msg = format!("conversion to `{}` for {} failed", ty.name(), ep.name); - // For each parameter, we need an impl for the builder to let consumers - // provide a value. - let param_impls = method - .params - .iter() - .map(|param| { - let param_name = format_ident!("{}", param.name); - match ¶m.typ { - OperationParameterType::Type(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 - // addressed - (Some(_), true) => { - unreachable!() + if ep.required { + Ok(quote! { + pub fn #param_name(mut self, value: V) -> Self + where + V: std::convert::TryInto<#typ>, + { + self.#param_name = value.try_into() + .map_err(|_| #err_msg.to_string()); + self } - (None, true) => { - let typ = ty.ident(); - let err_msg = format!( - "conversion to `{}` for {} failed", - ty.name(), - param.name, - ); - Ok(quote! { - pub fn #param_name( - mut self, - value: V, - ) -> Self - where V: std::convert::TryInto<#typ>, + }) + } else { + Ok(quote! { + pub fn #param_name(mut self, value: V) -> Self + where + V: std::convert::TryInto<#typ>, + { + self.#param_name = value.try_into() + .map(Some) + .map_err(|_| #err_msg.to_string()); + self + } + }) + } + } + }) + .chain( + // Add RawBody param methods + method.params.iter().filter_map(|param| { + if param.typ == OperationParameterType::RawBody { + let param_name = format_ident!("{}", param.name); + match ¶m.kind { + OperationParameterKind::Body(BodyContentType::OctetStream) + | OperationParameterKind::Body(BodyContentType::MultipartRelated) => { + let err_msg = format!("conversion to `reqwest::Body` for {} failed", param.name); + Some(Ok(quote! { + pub fn #param_name(mut self, value: B) -> Self + where B: std::convert::TryInto { self.#param_name = value.try_into() - .map(Some) .map_err(|_| #err_msg.to_string()); self } - }) + })) } - (None, false) => { - let typ = ty.ident(); - let err_msg = format!( - "conversion to `{}` for {} failed", - ty.name(), - param.name, - ); - Ok(quote! { - pub fn #param_name( - mut self, - value: V, - ) -> Self - where V: std::convert::TryInto<#typ>, + OperationParameterKind::Body(BodyContentType::Text(_)) => { + let err_msg = format!("conversion to `String` for {} failed", param.name); + Some(Ok(quote! { + pub fn #param_name(mut self, value: V) -> Self + where V: std::convert::TryInto { - self.#param_name = value.try_into() - .map_err(|_| #err_msg.to_string()); + self.#param_name = value + .try_into() + .map_err(|_| #err_msg.to_string()) + .map(|v| v.into()); self } - }) + })) } - - // For builder-capable bodies we offer a `body()` - // method that sets the full body (by constructing - // a builder **from** the body type). We also offer - // a `body_map()` method that operates on the - // builder itself. - (Some(builder_name), false) => { + _ => None, + } + } else { + None + } + }) + ) + .chain( + // Add methods for Type parameters with builders (body() and body_map()) + method.params.iter().filter_map(|param| { + match ¶m.typ { + OperationParameterType::Type(type_id) => { + let ty = self.type_space.get_type(type_id).ok()?; + + // For builder-capable bodies, generate both body() and body_map() + if let (OperationParameterKind::Body(_), Some(builder_name)) = + (¶m.kind, ty.builder()) + { assert_eq!(param.name, "body"); let typ = ty.ident(); let err_msg = format!( @@ -1586,7 +1899,7 @@ impl Generator { ty.name(), param.name, ); - Ok(quote! { + Some(Ok(quote! { pub fn body(mut self, value: V) -> Self where V: std::convert::TryInto<#typ>, @@ -1607,46 +1920,15 @@ impl Generator { self.body = self.body.map(f); self } - }) + })) + } else { + None } } + _ => None, } - - OperationParameterType::RawBody => match param.kind { - OperationParameterKind::Body(BodyContentType::OctetStream) => { - 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 - } - }) - } - OperationParameterKind::Body(BodyContentType::Text(_)) => { - let err_msg = - format!("conversion to `String` for {} failed", param.name,); - - Ok(quote! { - pub fn #param_name(mut self, value: V) -> Self - where V: std::convert::TryInto - { - self.#param_name = value - .try_into() - .map_err(|_| #err_msg.to_string()) - .map(|v| v.into()); - self - } - }) - } - _ => unreachable!(), - }, - } - }) + }) + ) .collect::>>()?; let MethodSigBody { @@ -1663,8 +1945,35 @@ impl Generator { let send_doc = format!( "Sends a `{}` request to `{}`", method.method.as_str().to_ascii_uppercase(), - method.path.to_string(), + method.path, ); + // Identify which multipart structs need to be reconstructed + let multipart_reconstructions = method.params.iter().filter_map(|param| { + if let OperationParameterType::MultipartRelated(type_id) = ¶m.typ { + let ty = self.type_space.get_type(type_id).unwrap(); + let type_name = ty.ident(); + let parent_param_name = format_ident!("{}", param.name); + + let typify::TypeDetails::Struct(struct_details) = ty.details() else { + panic!("multipart type must be a struct"); + }; + + // Generate field assignments + let field_assignments = struct_details.properties().map(|(prop_name, _)| { + let prop_ident = format_ident!("{}", prop_name); + quote! { #prop_ident } + }).collect::>(); + + Some(quote! { + let #parent_param_name = #type_name { + #(#field_assignments),* + }; + }) + } else { + None + } + }).collect::>(); + let send_impl = quote! { #[doc = #send_doc] pub async fn send(self) -> Result< @@ -1690,6 +1999,9 @@ impl Generator { .map_err(Error::InvalidRequest)?; )* + // Reconstruct multipart structs from individual properties + #(#multipart_reconstructions)* + // Do the work. #body } @@ -1724,7 +2036,7 @@ impl Generator { let stream_doc = format!( "Streams `{}` requests to `{}`", method.method.as_str().to_ascii_uppercase(), - method.path.to_string(), + method.path, ); quote! { @@ -2032,6 +2344,180 @@ impl Generator { impl_body } + fn parse_multipart_related_schema( + &mut self, + operation: &openapiv3::Operation, + _components: &Option, + object_type: &openapiv3::ObjectType, + ) -> Result { + // Validate that the object has properties + if object_type.properties.is_empty() { + return Err(Error::UnexpectedFormat( + "multipart/related object schema must have properties".to_string(), + )); + } + + // For now, we expect two properties: + // 1. metadata: a JSON object (the first part) + // 2. file/content: binary data (the second part) + // We'll validate that there's at least one binary property + + let has_binary = object_type.properties.values().any(|prop_ref| { + match prop_ref { + openapiv3::ReferenceOr::Item(prop_schema) => { + matches!( + prop_schema.as_ref(), + openapiv3::Schema { + schema_kind: openapiv3::SchemaKind::Type(openapiv3::Type::String( + openapiv3::StringType { + format: openapiv3::VariantOrUnknownOrEmpty::Item( + openapiv3::StringFormat::Binary + ), + .. + } + )), + .. + } + ) + } + openapiv3::ReferenceOr::Reference { .. } => false, + } + }); + + if !has_binary { + return Err(Error::UnexpectedFormat( + "multipart/related object schema must have at least one binary property".to_string(), + )); + } + + // Generate a type for the multipart parts struct + let parts_name = sanitize( + &format!("{}-multipart-parts", operation.operation_id.as_ref().unwrap()), + Case::Pascal, + ); + + // Create a schema for this struct + let mut properties_schemas: BTreeMap = BTreeMap::new(); + let mut content_type_fields: Vec = Vec::new(); + // Preserve the order of properties from the OpenAPI schema + let mut property_order: Vec = Vec::new(); + + for (name, prop_ref) in &object_type.properties { + property_order.push(name.clone()); + let prop_schema = match prop_ref { + openapiv3::ReferenceOr::Reference { reference } => { + // For references, create a reference schema + schemars::schema::Schema::Object(schemars::schema::SchemaObject { + reference: Some(reference.clone()), + ..Default::default() + }) + } + openapiv3::ReferenceOr::Item(item) => { + // Check if this is a binary field (string with format: binary) + let is_binary = matches!( + item.as_ref(), + openapiv3::Schema { + schema_kind: openapiv3::SchemaKind::Type(openapiv3::Type::String( + openapiv3::StringType { + format: openapiv3::VariantOrUnknownOrEmpty::Item( + openapiv3::StringFormat::Binary + ), + .. + } + )), + .. + } + ); + + if is_binary { + // Track that we need a content_type field for this binary field + content_type_fields.push(name.clone()); + + // For binary fields, create a schema for Vec + // This is an array of bytes (integers 0-255) + schemars::schema::Schema::Object(schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::Array.into()), + array: Some(Box::new(schemars::schema::ArrayValidation { + items: Some(schemars::schema::SingleOrVec::Single(Box::new( + schemars::schema::Schema::Object(schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::Integer.into()), + format: Some("uint8".to_string()), + number: Some(Box::new(schemars::schema::NumberValidation { + minimum: Some(0.0), + maximum: Some(255.0), + ..Default::default() + })), + ..Default::default() + }) + ))), + ..Default::default() + })), + ..Default::default() + }) + } else { + item.to_schema() + } + } + }; + properties_schemas.insert(name.clone(), prop_schema); + } + + // Add content_type fields for each binary field + for field_name in &content_type_fields { + let content_type_field_name = format!("{}_content_type", field_name); + properties_schemas.insert( + content_type_field_name, + schemars::schema::Schema::Object(schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::String.into()), + metadata: Some(Box::new(schemars::schema::Metadata { + description: Some(format!("MIME type for the {} field", field_name)), + ..Default::default() + })), + ..Default::default() + }), + ); + } + + // Build the required fields list - includes original required fields plus content_type fields + // for required binary fields only + let mut required_fields: BTreeSet = object_type.required.iter().cloned().collect(); + for field_name in &content_type_fields { + // Only make content_type required if the binary field itself is required + if object_type.required.contains(field_name) { + required_fields.insert(format!("{}_content_type", field_name)); + } + } + + let schema = schemars::schema::SchemaObject { + instance_type: Some(schemars::schema::InstanceType::Object.into()), + object: Some(Box::new(schemars::schema::ObjectValidation { + properties: properties_schemas, + required: required_fields, + ..Default::default() + })), + ..Default::default() + }; + + let type_id = self.type_space.add_type_with_name( + &schemars::schema::Schema::Object(schema), + Some(parts_name), + )?; + + // Track this type for later code generation, preserving property order and required fields + self.multipart_related.insert( + type_id.clone(), + crate::MultipartRelatedInfo { + property_order, + required_fields: object_type.required.iter().cloned().collect(), + }, + ); + + // Multipart/related uses ::serde_json::to_vec() for non-binary fields + self.uses_serde_json = true; + + Ok(OperationParameterType::MultipartRelated(type_id)) + } + fn get_body_param( &mut self, operation: &openapiv3::Operation, @@ -2091,12 +2577,61 @@ impl Generator { )), } if enumeration.is_empty() => Ok(()), _ => Err(Error::UnexpectedFormat(format!( - "invalid schema for application/octet-stream: {:?}", - schema + "invalid schema for {}: {:?}", + content_type, schema ))), }?; OperationParameterType::RawBody } + BodyContentType::MultipartRelated => { + // For multipart/related, we support two schemas: + // 1. Simple binary string (for manual construction) + // 2. Object with properties (for structured parts) + match schema.item(components)? { + // Simple binary string schema + openapiv3::Schema { + schema_data: + openapiv3::SchemaData { + nullable: false, + discriminator: None, + default: None, + .. + }, + 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() => OperationParameterType::RawBody, + // Object schema with properties for structured multipart + openapiv3::Schema { + schema_data: _, + schema_kind: + openapiv3::SchemaKind::Type(openapiv3::Type::Object(object_type)), + } => { + // Parse the object schema to identify parts + self.parse_multipart_related_schema( + operation, + components, + object_type, + )? + } + _ => { + return Err(Error::UnexpectedFormat(format!( + "invalid schema for multipart/related: {:?}", + schema + ))) + } + } + } BodyContentType::Text(_) => { // For a plain text body, we expect a simple, specific schema: // "schema": { @@ -2155,6 +2690,7 @@ impl Generator { description: body.description.clone(), typ, kind: OperationParameterKind::Body(content_type), + default: None, })) } } @@ -2174,7 +2710,7 @@ fn make_doc_comment(method: &OperationMethod) -> String { buf.push_str(&format!( "Sends a `{}` request to `{}`\n\n", method.method.as_str().to_ascii_uppercase(), - method.path.to_string(), + method.path, )); if method @@ -2213,7 +2749,7 @@ fn make_stream_doc_comment(method: &OperationMethod) -> String { buf.push_str(&format!( "Sends repeated `{}` requests to `{}` until there are no more results.\n\n", method.method.as_str().to_ascii_uppercase(), - method.path.to_string(), + method.path, )); if method diff --git a/progenitor-impl/src/template.rs b/progenitor-impl/src/template.rs index 47f3cc85..ee65fc45 100644 --- a/progenitor-impl/src/template.rs +++ b/progenitor-impl/src/template.rs @@ -35,7 +35,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()) @@ -158,15 +158,19 @@ pub fn parse(t: &str) -> Result { Ok(PathTemplate { components }) } -impl ToString for PathTemplate { - fn to_string(&self) -> std::string::String { - self.components - .iter() - .map(|component| match component { - Component::Constant(s) => s.clone(), - Component::Parameter(s) => format!("{{{}}}", s), - }) - .fold(String::new(), |a, b| a + &b) +impl std::fmt::Display for PathTemplate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + self.components + .iter() + .map(|component| match component { + Component::Constant(s) => s.clone(), + Component::Parameter(s) => format!("{{{}}}", s), + }) + .fold(String::new(), |a, b| a + &b) + ) } } diff --git a/progenitor-impl/src/util.rs b/progenitor-impl/src/util.rs index 170d00ac..5e5301c3 100644 --- a/progenitor-impl/src/util.rs +++ b/progenitor-impl/src/util.rs @@ -126,6 +126,6 @@ pub(crate) fn unique_ident_from( return ident; } - name.insert_str(0, "_"); + name.insert(0, '_'); } } diff --git a/progenitor-impl/tests/output/src/buildomat_builder.rs b/progenitor-impl/tests/output/src/buildomat_builder.rs index dfa92e1b..67b50ba2 100644 --- a/progenitor-impl/tests/output/src/buildomat_builder.rs +++ b/progenitor-impl/tests/output/src/buildomat_builder.rs @@ -4109,7 +4109,7 @@ pub mod builder { pub fn new(client: &'a super::Client) -> Self { Self { client: client, - accept_language: Ok(None), + accept_language: Ok(Some(::std::default::Default::default())), } } diff --git a/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs b/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs index e2dc6a80..941340b8 100644 --- a/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs +++ b/progenitor-impl/tests/output/src/buildomat_builder_tagged.rs @@ -4066,7 +4066,7 @@ pub mod builder { pub fn new(client: &'a super::Client) -> Self { Self { client: client, - accept_language: Ok(None), + accept_language: Ok(Some(::std::default::Default::default())), } } diff --git a/progenitor-impl/tests/output/src/multipart_related_test_builder.rs b/progenitor-impl/tests/output/src/multipart_related_test_builder.rs new file mode 100644 index 00000000..2ee5b6d8 --- /dev/null +++ b/progenitor-impl/tests/output/src/multipart_related_test_builder.rs @@ -0,0 +1,1160 @@ +#[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()) + } + } + } + + ///`FileMetadata` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "mimeType", + /// "name" + /// ], + /// "properties": { + /// "description": { + /// "description": "Optional description of the file", + /// "type": "string" + /// }, + /// "mimeType": { + /// "description": "The MIME type of the file", + /// "type": "string" + /// }, + /// "name": { + /// "description": "The name of the file", + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct FileMetadata { + ///Optional description of the file + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub description: ::std::option::Option<::std::string::String>, + ///The MIME type of the file + #[serde(rename = "mimeType")] + pub mime_type: ::std::string::String, + ///The name of the file + pub name: ::std::string::String, + } + + impl ::std::convert::From<&FileMetadata> for FileMetadata { + fn from(value: &FileMetadata) -> Self { + value.clone() + } + } + + impl FileMetadata { + pub fn builder() -> builder::FileMetadata { + Default::default() + } + } + + ///`UploadFileMultipartParts` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "file_content_type", + /// "metadata" + /// ], + /// "properties": { + /// "file": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "file_content_type": { + /// "description": "MIME type for the file field", + /// "type": "string" + /// }, + /// "metadata": { + /// "$ref": "#/components/schemas/FileMetadata" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct UploadFileMultipartParts { + pub file: ::std::vec::Vec, + ///MIME type for the file field + pub file_content_type: ::std::string::String, + pub metadata: FileMetadata, + } + + impl ::std::convert::From<&UploadFileMultipartParts> for UploadFileMultipartParts { + fn from(value: &UploadFileMultipartParts) -> Self { + value.clone() + } + } + + impl UploadFileMultipartParts { + pub fn builder() -> builder::UploadFileMultipartParts { + Default::default() + } + } + + ///`UploadMultipleFilesMultipartParts` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "document", + /// "document_content_type", + /// "metadata", + /// "thumbnail", + /// "thumbnail_content_type" + /// ], + /// "properties": { + /// "attachment": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "attachment_content_type": { + /// "description": "MIME type for the attachment field", + /// "type": "string" + /// }, + /// "document": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "document_content_type": { + /// "description": "MIME type for the document field", + /// "type": "string" + /// }, + /// "metadata": { + /// "$ref": "#/components/schemas/FileMetadata" + /// }, + /// "thumbnail": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "thumbnail_content_type": { + /// "description": "MIME type for the thumbnail field", + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct UploadMultipleFilesMultipartParts { + #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] + pub attachment: ::std::vec::Vec, + ///MIME type for the attachment field + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub attachment_content_type: ::std::option::Option<::std::string::String>, + pub document: ::std::vec::Vec, + ///MIME type for the document field + pub document_content_type: ::std::string::String, + pub metadata: FileMetadata, + pub thumbnail: ::std::vec::Vec, + ///MIME type for the thumbnail field + pub thumbnail_content_type: ::std::string::String, + } + + impl ::std::convert::From<&UploadMultipleFilesMultipartParts> + for UploadMultipleFilesMultipartParts + { + fn from(value: &UploadMultipleFilesMultipartParts) -> Self { + value.clone() + } + } + + impl UploadMultipleFilesMultipartParts { + pub fn builder() -> builder::UploadMultipleFilesMultipartParts { + Default::default() + } + } + + ///`UploadResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "id", + /// "name", + /// "size" + /// ], + /// "properties": { + /// "id": { + /// "description": "The ID of the uploaded file", + /// "type": "string" + /// }, + /// "name": { + /// "description": "The name of the uploaded file", + /// "type": "string" + /// }, + /// "size": { + /// "description": "The size of the uploaded file in bytes", + /// "type": "integer" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct UploadResponse { + ///The ID of the uploaded file + pub id: ::std::string::String, + ///The name of the uploaded file + pub name: ::std::string::String, + ///The size of the uploaded file in bytes + pub size: i64, + } + + impl ::std::convert::From<&UploadResponse> for UploadResponse { + fn from(value: &UploadResponse) -> Self { + value.clone() + } + } + + impl UploadResponse { + pub fn builder() -> builder::UploadResponse { + Default::default() + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct FileMetadata { + description: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + mime_type: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for FileMetadata { + fn default() -> Self { + Self { + description: Ok(Default::default()), + mime_type: Err("no value supplied for mime_type".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + + impl FileMetadata { + pub fn description(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.description = value + .try_into() + .map_err(|e| format!("error converting supplied value for description: {}", e)); + self + } + pub fn mime_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.mime_type = value + .try_into() + .map_err(|e| format!("error converting supplied value for mime_type: {}", 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::FileMetadata { + type Error = super::error::ConversionError; + fn try_from( + value: FileMetadata, + ) -> ::std::result::Result { + Ok(Self { + description: value.description?, + mime_type: value.mime_type?, + name: value.name?, + }) + } + } + + impl ::std::convert::From for FileMetadata { + fn from(value: super::FileMetadata) -> Self { + Self { + description: Ok(value.description), + mime_type: Ok(value.mime_type), + name: Ok(value.name), + } + } + } + + #[derive(Clone, Debug)] + pub struct UploadFileMultipartParts { + file: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + file_content_type: ::std::result::Result<::std::string::String, ::std::string::String>, + metadata: ::std::result::Result, + } + + impl ::std::default::Default for UploadFileMultipartParts { + fn default() -> Self { + Self { + file: Err("no value supplied for file".to_string()), + file_content_type: Err("no value supplied for file_content_type".to_string()), + metadata: Err("no value supplied for metadata".to_string()), + } + } + } + + impl UploadFileMultipartParts { + pub fn file(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.file = value + .try_into() + .map_err(|e| format!("error converting supplied value for file: {}", e)); + self + } + pub fn file_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.file_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for file_content_type: {}", + e + ) + }); + self + } + pub fn metadata(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.metadata = value + .try_into() + .map_err(|e| format!("error converting supplied value for metadata: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadFileMultipartParts { + type Error = super::error::ConversionError; + fn try_from( + value: UploadFileMultipartParts, + ) -> ::std::result::Result { + Ok(Self { + file: value.file?, + file_content_type: value.file_content_type?, + metadata: value.metadata?, + }) + } + } + + impl ::std::convert::From for UploadFileMultipartParts { + fn from(value: super::UploadFileMultipartParts) -> Self { + Self { + file: Ok(value.file), + file_content_type: Ok(value.file_content_type), + metadata: Ok(value.metadata), + } + } + } + + #[derive(Clone, Debug)] + pub struct UploadMultipleFilesMultipartParts { + attachment: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + attachment_content_type: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + document: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + document_content_type: + ::std::result::Result<::std::string::String, ::std::string::String>, + metadata: ::std::result::Result, + thumbnail: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + thumbnail_content_type: + ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for UploadMultipleFilesMultipartParts { + fn default() -> Self { + Self { + attachment: Ok(Default::default()), + attachment_content_type: Ok(Default::default()), + document: Err("no value supplied for document".to_string()), + document_content_type: Err( + "no value supplied for document_content_type".to_string() + ), + metadata: Err("no value supplied for metadata".to_string()), + thumbnail: Err("no value supplied for thumbnail".to_string()), + thumbnail_content_type: Err( + "no value supplied for thumbnail_content_type".to_string() + ), + } + } + } + + impl UploadMultipleFilesMultipartParts { + pub fn attachment(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.attachment = value + .try_into() + .map_err(|e| format!("error converting supplied value for attachment: {}", e)); + self + } + pub fn attachment_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.attachment_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for attachment_content_type: {}", + e + ) + }); + self + } + pub fn document(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.document = value + .try_into() + .map_err(|e| format!("error converting supplied value for document: {}", e)); + self + } + pub fn document_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.document_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for document_content_type: {}", + e + ) + }); + self + } + pub fn metadata(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.metadata = value + .try_into() + .map_err(|e| format!("error converting supplied value for metadata: {}", e)); + self + } + pub fn thumbnail(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.thumbnail = value + .try_into() + .map_err(|e| format!("error converting supplied value for thumbnail: {}", e)); + self + } + pub fn thumbnail_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.thumbnail_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for thumbnail_content_type: {}", + e + ) + }); + self + } + } + + impl ::std::convert::TryFrom + for super::UploadMultipleFilesMultipartParts + { + type Error = super::error::ConversionError; + fn try_from( + value: UploadMultipleFilesMultipartParts, + ) -> ::std::result::Result { + Ok(Self { + attachment: value.attachment?, + attachment_content_type: value.attachment_content_type?, + document: value.document?, + document_content_type: value.document_content_type?, + metadata: value.metadata?, + thumbnail: value.thumbnail?, + thumbnail_content_type: value.thumbnail_content_type?, + }) + } + } + + impl ::std::convert::From + for UploadMultipleFilesMultipartParts + { + fn from(value: super::UploadMultipleFilesMultipartParts) -> Self { + Self { + attachment: Ok(value.attachment), + attachment_content_type: Ok(value.attachment_content_type), + document: Ok(value.document), + document_content_type: Ok(value.document_content_type), + metadata: Ok(value.metadata), + thumbnail: Ok(value.thumbnail), + thumbnail_content_type: Ok(value.thumbnail_content_type), + } + } + } + + #[derive(Clone, Debug)] + pub struct UploadResponse { + id: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + size: ::std::result::Result, + } + + impl ::std::default::Default for UploadResponse { + fn default() -> Self { + Self { + id: Err("no value supplied for id".to_string()), + name: Err("no value supplied for name".to_string()), + size: Err("no value supplied for size".to_string()), + } + } + } + + impl UploadResponse { + pub fn id(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.id = value + .try_into() + .map_err(|e| format!("error converting supplied value for id: {}", 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 + } + pub fn size(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.size = value + .try_into() + .map_err(|e| format!("error converting supplied value for size: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadResponse { + type Error = super::error::ConversionError; + fn try_from( + value: UploadResponse, + ) -> ::std::result::Result { + Ok(Self { + id: value.id?, + name: value.name?, + size: value.size?, + }) + } + } + + impl ::std::convert::From for UploadResponse { + fn from(value: super::UploadResponse) -> Self { + Self { + id: Ok(value.id), + name: Ok(value.name), + size: Ok(value.size), + } + } + } + } + + impl crate::progenitor_client::MultipartRelatedBody for UploadFileMultipartParts { + fn as_multipart_parts(&self) -> Vec { + vec![ + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: "metadata", + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.metadata).expect("failed to serialize field"), + ), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.file_content_type, + content_id: "file", + bytes: ::std::borrow::Cow::Borrowed(&self.file), + }), + ] + .into_iter() + .flatten() + .collect() + } + } + + impl crate::progenitor_client::MultipartRelatedBody for UploadMultipleFilesMultipartParts { + fn as_multipart_parts(&self) -> Vec { + vec![ + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: "metadata", + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.metadata).expect("failed to serialize field"), + ), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.document_content_type, + content_id: "document", + bytes: ::std::borrow::Cow::Borrowed(&self.document), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.thumbnail_content_type, + content_id: "thumbnail", + bytes: ::std::borrow::Cow::Borrowed(&self.thumbnail), + }), + if let Some(ref content_type) = self.attachment_content_type { + if !self.attachment.is_empty() { + Some(crate::progenitor_client::MultipartPart { + content_type: content_type.as_str(), + content_id: "attachment", + bytes: ::std::borrow::Cow::Borrowed(&self.attachment), + }) + } else { + None + } + } else { + None + }, + ] + .into_iter() + .flatten() + .collect() + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Related Test API +/// +///Test API for multipart/related content type support +/// +///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 { + ///Upload a file with metadata using multipart/related + /// + ///Uploads a file along with JSON metadata in a multipart/related request + /// + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload_file() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload_file(&self) -> builder::UploadFile<'_> { + builder::UploadFile::new(self) + } + + ///Simple upload using multipart/related + /// + ///Sends a `POST` request to `/upload-simple` + /// + ///```ignore + /// let response = client.upload_simple() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload_simple(&self) -> builder::UploadSimple<'_> { + builder::UploadSimple::new(self) + } + + ///Upload multiple files with metadata using multipart/related + /// + ///Uploads multiple files along with JSON metadata in a single + /// multipart/related request + /// + ///Sends a `POST` request to `/upload-multiple` + /// + ///```ignore + /// let response = client.upload_multiple_files() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload_multiple_files(&self) -> builder::UploadMultipleFiles<'_> { + builder::UploadMultipleFiles::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_file`] + /// + ///[`Client::upload_file`]: super::Client::upload_file + #[derive(Debug, Clone)] + pub struct UploadFile<'a> { + client: &'a super::Client, + file: Result, String>, + metadata: Result, + file_content_type: Result, + } + + impl<'a> UploadFile<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + file: Err("file was not initialized".to_string()), + metadata: Err("metadata was not initialized".to_string()), + file_content_type: Err("file_content_type was not initialized".to_string()), + } + } + + pub fn file(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.file = value + .try_into() + .map_err(|_| "conversion to Vec for file failed".to_string()); + self.file_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for file_content_type failed".to_string()); + self + } + + pub fn metadata(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.metadata = value + .try_into() + .map_err(|_| "conversion to `FileMetadata` for metadata failed".to_string()); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + file, + metadata, + file_content_type, + } = self; + let file = file.map_err(Error::InvalidRequest)?; + let metadata = metadata.map_err(Error::InvalidRequest)?; + let file_content_type = file_content_type.map_err(Error::InvalidRequest)?; + let body = types::UploadFileMultipartParts { + file, + file_content_type, + metadata, + }; + 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .multipart_related(&body)? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + 400u16 => Err(Error::ErrorResponse(ResponseValue::empty(response))), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + ///Builder for [`Client::upload_simple`] + /// + ///[`Client::upload_simple`]: super::Client::upload_simple + #[derive(Debug)] + pub struct UploadSimple<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> UploadSimple<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Err("body was not initialized".to_string()), + } + } + + pub fn body(mut self, value: B) -> Self + where + B: std::convert::TryInto, + { + self.body = value + .try_into() + .map_err(|_| "conversion to `reqwest::Body` for body failed".to_string()); + self + } + + ///Sends a `POST` request to `/upload-simple` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body.map_err(Error::InvalidRequest)?; + let url = format!("{}/upload-simple", 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) + .header( + ::reqwest::header::CONTENT_TYPE, + ::reqwest::header::HeaderValue::from_static("multipart/related"), + ) + .body(body) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_simple", + }; + 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() { + 204u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + ///Builder for [`Client::upload_multiple_files`] + /// + ///[`Client::upload_multiple_files`]: super::Client::upload_multiple_files + #[derive(Debug, Clone)] + pub struct UploadMultipleFiles<'a> { + client: &'a super::Client, + attachment: Result, String>, + document: Result, String>, + metadata: Result, + thumbnail: Result, String>, + attachment_content_type: Result, + document_content_type: Result, + thumbnail_content_type: Result, + } + + impl<'a> UploadMultipleFiles<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + attachment: Err("attachment was not initialized".to_string()), + document: Err("document was not initialized".to_string()), + metadata: Err("metadata was not initialized".to_string()), + thumbnail: Err("thumbnail was not initialized".to_string()), + attachment_content_type: Err( + "attachment_content_type was not initialized".to_string() + ), + document_content_type: Err("document_content_type was not initialized".to_string()), + thumbnail_content_type: Err( + "thumbnail_content_type was not initialized".to_string() + ), + } + } + + pub fn attachment(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.attachment = value + .try_into() + .map_err(|_| "conversion to Vec for attachment failed".to_string()); + self.attachment_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for attachment_content_type failed".to_string()); + self + } + + pub fn document(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.document = value + .try_into() + .map_err(|_| "conversion to Vec for document failed".to_string()); + self.document_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for document_content_type failed".to_string()); + self + } + + pub fn metadata(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.metadata = value + .try_into() + .map_err(|_| "conversion to `FileMetadata` for metadata failed".to_string()); + self + } + + pub fn thumbnail(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.thumbnail = value + .try_into() + .map_err(|_| "conversion to Vec for thumbnail failed".to_string()); + self.thumbnail_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for thumbnail_content_type failed".to_string()); + self + } + + ///Sends a `POST` request to `/upload-multiple` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + attachment, + document, + metadata, + thumbnail, + attachment_content_type, + document_content_type, + thumbnail_content_type, + } = self; + let attachment = attachment.map_err(Error::InvalidRequest)?; + let document = document.map_err(Error::InvalidRequest)?; + let metadata = metadata.map_err(Error::InvalidRequest)?; + let thumbnail = thumbnail.map_err(Error::InvalidRequest)?; + let attachment_content_type = attachment_content_type.map_err(Error::InvalidRequest)?; + let document_content_type = document_content_type.map_err(Error::InvalidRequest)?; + let thumbnail_content_type = thumbnail_content_type.map_err(Error::InvalidRequest)?; + let body = types::UploadMultipleFilesMultipartParts { + attachment, + attachment_content_type, + document, + document_content_type, + metadata, + thumbnail, + thumbnail_content_type, + }; + let url = format!("{}/upload-multiple", 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .multipart_related(&body)? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_multiple_files", + }; + 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 => ResponseValue::from_response(response).await, + 400u16 => Err(Error::ErrorResponse(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/multipart_related_test_builder_tagged.rs b/progenitor-impl/tests/output/src/multipart_related_test_builder_tagged.rs new file mode 100644 index 00000000..438c18ee --- /dev/null +++ b/progenitor-impl/tests/output/src/multipart_related_test_builder_tagged.rs @@ -0,0 +1,1154 @@ +#[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()) + } + } + } + + ///`FileMetadata` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "mimeType", + /// "name" + /// ], + /// "properties": { + /// "description": { + /// "description": "Optional description of the file", + /// "type": "string" + /// }, + /// "mimeType": { + /// "description": "The MIME type of the file", + /// "type": "string" + /// }, + /// "name": { + /// "description": "The name of the file", + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct FileMetadata { + ///Optional description of the file + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub description: ::std::option::Option<::std::string::String>, + ///The MIME type of the file + #[serde(rename = "mimeType")] + pub mime_type: ::std::string::String, + ///The name of the file + pub name: ::std::string::String, + } + + impl ::std::convert::From<&FileMetadata> for FileMetadata { + fn from(value: &FileMetadata) -> Self { + value.clone() + } + } + + impl FileMetadata { + pub fn builder() -> builder::FileMetadata { + Default::default() + } + } + + ///`UploadFileMultipartParts` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "file_content_type", + /// "metadata" + /// ], + /// "properties": { + /// "file": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "file_content_type": { + /// "description": "MIME type for the file field", + /// "type": "string" + /// }, + /// "metadata": { + /// "$ref": "#/components/schemas/FileMetadata" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadFileMultipartParts { + pub file: ::std::vec::Vec, + ///MIME type for the file field + pub file_content_type: ::std::string::String, + pub metadata: FileMetadata, + } + + impl ::std::convert::From<&UploadFileMultipartParts> for UploadFileMultipartParts { + fn from(value: &UploadFileMultipartParts) -> Self { + value.clone() + } + } + + impl UploadFileMultipartParts { + pub fn builder() -> builder::UploadFileMultipartParts { + Default::default() + } + } + + ///`UploadMultipleFilesMultipartParts` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "document", + /// "document_content_type", + /// "metadata", + /// "thumbnail", + /// "thumbnail_content_type" + /// ], + /// "properties": { + /// "attachment": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "attachment_content_type": { + /// "description": "MIME type for the attachment field", + /// "type": "string" + /// }, + /// "document": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "document_content_type": { + /// "description": "MIME type for the document field", + /// "type": "string" + /// }, + /// "metadata": { + /// "$ref": "#/components/schemas/FileMetadata" + /// }, + /// "thumbnail": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "thumbnail_content_type": { + /// "description": "MIME type for the thumbnail field", + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadMultipleFilesMultipartParts { + #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] + pub attachment: ::std::vec::Vec, + ///MIME type for the attachment field + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub attachment_content_type: ::std::option::Option<::std::string::String>, + pub document: ::std::vec::Vec, + ///MIME type for the document field + pub document_content_type: ::std::string::String, + pub metadata: FileMetadata, + pub thumbnail: ::std::vec::Vec, + ///MIME type for the thumbnail field + pub thumbnail_content_type: ::std::string::String, + } + + impl ::std::convert::From<&UploadMultipleFilesMultipartParts> + for UploadMultipleFilesMultipartParts + { + fn from(value: &UploadMultipleFilesMultipartParts) -> Self { + value.clone() + } + } + + impl UploadMultipleFilesMultipartParts { + pub fn builder() -> builder::UploadMultipleFilesMultipartParts { + Default::default() + } + } + + ///`UploadResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "id", + /// "name", + /// "size" + /// ], + /// "properties": { + /// "id": { + /// "description": "The ID of the uploaded file", + /// "type": "string" + /// }, + /// "name": { + /// "description": "The name of the uploaded file", + /// "type": "string" + /// }, + /// "size": { + /// "description": "The size of the uploaded file in bytes", + /// "type": "integer" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadResponse { + ///The ID of the uploaded file + pub id: ::std::string::String, + ///The name of the uploaded file + pub name: ::std::string::String, + ///The size of the uploaded file in bytes + pub size: i64, + } + + impl ::std::convert::From<&UploadResponse> for UploadResponse { + fn from(value: &UploadResponse) -> Self { + value.clone() + } + } + + impl UploadResponse { + pub fn builder() -> builder::UploadResponse { + Default::default() + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct FileMetadata { + description: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + mime_type: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for FileMetadata { + fn default() -> Self { + Self { + description: Ok(Default::default()), + mime_type: Err("no value supplied for mime_type".to_string()), + name: Err("no value supplied for name".to_string()), + } + } + } + + impl FileMetadata { + pub fn description(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.description = value + .try_into() + .map_err(|e| format!("error converting supplied value for description: {}", e)); + self + } + pub fn mime_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.mime_type = value + .try_into() + .map_err(|e| format!("error converting supplied value for mime_type: {}", 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::FileMetadata { + type Error = super::error::ConversionError; + fn try_from( + value: FileMetadata, + ) -> ::std::result::Result { + Ok(Self { + description: value.description?, + mime_type: value.mime_type?, + name: value.name?, + }) + } + } + + impl ::std::convert::From for FileMetadata { + fn from(value: super::FileMetadata) -> Self { + Self { + description: Ok(value.description), + mime_type: Ok(value.mime_type), + name: Ok(value.name), + } + } + } + + #[derive(Clone, Debug)] + pub struct UploadFileMultipartParts { + file: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + file_content_type: ::std::result::Result<::std::string::String, ::std::string::String>, + metadata: ::std::result::Result, + } + + impl ::std::default::Default for UploadFileMultipartParts { + fn default() -> Self { + Self { + file: Err("no value supplied for file".to_string()), + file_content_type: Err("no value supplied for file_content_type".to_string()), + metadata: Err("no value supplied for metadata".to_string()), + } + } + } + + impl UploadFileMultipartParts { + pub fn file(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.file = value + .try_into() + .map_err(|e| format!("error converting supplied value for file: {}", e)); + self + } + pub fn file_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.file_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for file_content_type: {}", + e + ) + }); + self + } + pub fn metadata(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.metadata = value + .try_into() + .map_err(|e| format!("error converting supplied value for metadata: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadFileMultipartParts { + type Error = super::error::ConversionError; + fn try_from( + value: UploadFileMultipartParts, + ) -> ::std::result::Result { + Ok(Self { + file: value.file?, + file_content_type: value.file_content_type?, + metadata: value.metadata?, + }) + } + } + + impl ::std::convert::From for UploadFileMultipartParts { + fn from(value: super::UploadFileMultipartParts) -> Self { + Self { + file: Ok(value.file), + file_content_type: Ok(value.file_content_type), + metadata: Ok(value.metadata), + } + } + } + + #[derive(Clone, Debug)] + pub struct UploadMultipleFilesMultipartParts { + attachment: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + attachment_content_type: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + document: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + document_content_type: + ::std::result::Result<::std::string::String, ::std::string::String>, + metadata: ::std::result::Result, + thumbnail: ::std::result::Result<::std::vec::Vec, ::std::string::String>, + thumbnail_content_type: + ::std::result::Result<::std::string::String, ::std::string::String>, + } + + impl ::std::default::Default for UploadMultipleFilesMultipartParts { + fn default() -> Self { + Self { + attachment: Ok(Default::default()), + attachment_content_type: Ok(Default::default()), + document: Err("no value supplied for document".to_string()), + document_content_type: Err( + "no value supplied for document_content_type".to_string() + ), + metadata: Err("no value supplied for metadata".to_string()), + thumbnail: Err("no value supplied for thumbnail".to_string()), + thumbnail_content_type: Err( + "no value supplied for thumbnail_content_type".to_string() + ), + } + } + } + + impl UploadMultipleFilesMultipartParts { + pub fn attachment(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.attachment = value + .try_into() + .map_err(|e| format!("error converting supplied value for attachment: {}", e)); + self + } + pub fn attachment_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.attachment_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for attachment_content_type: {}", + e + ) + }); + self + } + pub fn document(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.document = value + .try_into() + .map_err(|e| format!("error converting supplied value for document: {}", e)); + self + } + pub fn document_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.document_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for document_content_type: {}", + e + ) + }); + self + } + pub fn metadata(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.metadata = value + .try_into() + .map_err(|e| format!("error converting supplied value for metadata: {}", e)); + self + } + pub fn thumbnail(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::vec::Vec>, + T::Error: ::std::fmt::Display, + { + self.thumbnail = value + .try_into() + .map_err(|e| format!("error converting supplied value for thumbnail: {}", e)); + self + } + pub fn thumbnail_content_type(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.thumbnail_content_type = value.try_into().map_err(|e| { + format!( + "error converting supplied value for thumbnail_content_type: {}", + e + ) + }); + self + } + } + + impl ::std::convert::TryFrom + for super::UploadMultipleFilesMultipartParts + { + type Error = super::error::ConversionError; + fn try_from( + value: UploadMultipleFilesMultipartParts, + ) -> ::std::result::Result { + Ok(Self { + attachment: value.attachment?, + attachment_content_type: value.attachment_content_type?, + document: value.document?, + document_content_type: value.document_content_type?, + metadata: value.metadata?, + thumbnail: value.thumbnail?, + thumbnail_content_type: value.thumbnail_content_type?, + }) + } + } + + impl ::std::convert::From + for UploadMultipleFilesMultipartParts + { + fn from(value: super::UploadMultipleFilesMultipartParts) -> Self { + Self { + attachment: Ok(value.attachment), + attachment_content_type: Ok(value.attachment_content_type), + document: Ok(value.document), + document_content_type: Ok(value.document_content_type), + metadata: Ok(value.metadata), + thumbnail: Ok(value.thumbnail), + thumbnail_content_type: Ok(value.thumbnail_content_type), + } + } + } + + #[derive(Clone, Debug)] + pub struct UploadResponse { + id: ::std::result::Result<::std::string::String, ::std::string::String>, + name: ::std::result::Result<::std::string::String, ::std::string::String>, + size: ::std::result::Result, + } + + impl ::std::default::Default for UploadResponse { + fn default() -> Self { + Self { + id: Err("no value supplied for id".to_string()), + name: Err("no value supplied for name".to_string()), + size: Err("no value supplied for size".to_string()), + } + } + } + + impl UploadResponse { + pub fn id(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::string::String>, + T::Error: ::std::fmt::Display, + { + self.id = value + .try_into() + .map_err(|e| format!("error converting supplied value for id: {}", 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 + } + pub fn size(mut self, value: T) -> Self + where + T: ::std::convert::TryInto, + T::Error: ::std::fmt::Display, + { + self.size = value + .try_into() + .map_err(|e| format!("error converting supplied value for size: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadResponse { + type Error = super::error::ConversionError; + fn try_from( + value: UploadResponse, + ) -> ::std::result::Result { + Ok(Self { + id: value.id?, + name: value.name?, + size: value.size?, + }) + } + } + + impl ::std::convert::From for UploadResponse { + fn from(value: super::UploadResponse) -> Self { + Self { + id: Ok(value.id), + name: Ok(value.name), + size: Ok(value.size), + } + } + } + } + + impl crate::progenitor_client::MultipartRelatedBody for UploadFileMultipartParts { + fn as_multipart_parts(&self) -> Vec { + vec![ + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: "metadata", + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.metadata).expect("failed to serialize field"), + ), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.file_content_type, + content_id: "file", + bytes: ::std::borrow::Cow::Borrowed(&self.file), + }), + ] + .into_iter() + .flatten() + .collect() + } + } + + impl crate::progenitor_client::MultipartRelatedBody for UploadMultipleFilesMultipartParts { + fn as_multipart_parts(&self) -> Vec { + vec![ + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: "metadata", + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.metadata).expect("failed to serialize field"), + ), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.document_content_type, + content_id: "document", + bytes: ::std::borrow::Cow::Borrowed(&self.document), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.thumbnail_content_type, + content_id: "thumbnail", + bytes: ::std::borrow::Cow::Borrowed(&self.thumbnail), + }), + if let Some(ref content_type) = self.attachment_content_type { + if !self.attachment.is_empty() { + Some(crate::progenitor_client::MultipartPart { + content_type: content_type.as_str(), + content_id: "attachment", + bytes: ::std::borrow::Cow::Borrowed(&self.attachment), + }) + } else { + None + } + } else { + None + }, + ] + .into_iter() + .flatten() + .collect() + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Related Test API +/// +///Test API for multipart/related content type support +/// +///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 { + ///Upload a file with metadata using multipart/related + /// + ///Uploads a file along with JSON metadata in a multipart/related request + /// + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload_file() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload_file(&self) -> builder::UploadFile<'_> { + builder::UploadFile::new(self) + } + + ///Simple upload using multipart/related + /// + ///Sends a `POST` request to `/upload-simple` + /// + ///```ignore + /// let response = client.upload_simple() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload_simple(&self) -> builder::UploadSimple<'_> { + builder::UploadSimple::new(self) + } + + ///Upload multiple files with metadata using multipart/related + /// + ///Uploads multiple files along with JSON metadata in a single + /// multipart/related request + /// + ///Sends a `POST` request to `/upload-multiple` + /// + ///```ignore + /// let response = client.upload_multiple_files() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn upload_multiple_files(&self) -> builder::UploadMultipleFiles<'_> { + builder::UploadMultipleFiles::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_file`] + /// + ///[`Client::upload_file`]: super::Client::upload_file + #[derive(Debug, Clone)] + pub struct UploadFile<'a> { + client: &'a super::Client, + file: Result, String>, + metadata: Result, + file_content_type: Result, + } + + impl<'a> UploadFile<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + file: Err("file was not initialized".to_string()), + metadata: Err("metadata was not initialized".to_string()), + file_content_type: Err("file_content_type was not initialized".to_string()), + } + } + + pub fn file(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.file = value + .try_into() + .map_err(|_| "conversion to Vec for file failed".to_string()); + self.file_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for file_content_type failed".to_string()); + self + } + + pub fn metadata(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.metadata = value + .try_into() + .map_err(|_| "conversion to `FileMetadata` for metadata failed".to_string()); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + file, + metadata, + file_content_type, + } = self; + let file = file.map_err(Error::InvalidRequest)?; + let metadata = metadata.map_err(Error::InvalidRequest)?; + let file_content_type = file_content_type.map_err(Error::InvalidRequest)?; + let body = types::UploadFileMultipartParts { + file, + file_content_type, + metadata, + }; + 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .multipart_related(&body)? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + 400u16 => Err(Error::ErrorResponse(ResponseValue::empty(response))), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + ///Builder for [`Client::upload_simple`] + /// + ///[`Client::upload_simple`]: super::Client::upload_simple + #[derive(Debug)] + pub struct UploadSimple<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> UploadSimple<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + body: Err("body was not initialized".to_string()), + } + } + + pub fn body(mut self, value: B) -> Self + where + B: std::convert::TryInto, + { + self.body = value + .try_into() + .map_err(|_| "conversion to `reqwest::Body` for body failed".to_string()); + self + } + + ///Sends a `POST` request to `/upload-simple` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body.map_err(Error::InvalidRequest)?; + let url = format!("{}/upload-simple", 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) + .header( + ::reqwest::header::CONTENT_TYPE, + ::reqwest::header::HeaderValue::from_static("multipart/related"), + ) + .body(body) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_simple", + }; + 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() { + 204u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + + ///Builder for [`Client::upload_multiple_files`] + /// + ///[`Client::upload_multiple_files`]: super::Client::upload_multiple_files + #[derive(Debug, Clone)] + pub struct UploadMultipleFiles<'a> { + client: &'a super::Client, + attachment: Result, String>, + document: Result, String>, + metadata: Result, + thumbnail: Result, String>, + attachment_content_type: Result, + document_content_type: Result, + thumbnail_content_type: Result, + } + + impl<'a> UploadMultipleFiles<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + attachment: Err("attachment was not initialized".to_string()), + document: Err("document was not initialized".to_string()), + metadata: Err("metadata was not initialized".to_string()), + thumbnail: Err("thumbnail was not initialized".to_string()), + attachment_content_type: Err( + "attachment_content_type was not initialized".to_string() + ), + document_content_type: Err("document_content_type was not initialized".to_string()), + thumbnail_content_type: Err( + "thumbnail_content_type was not initialized".to_string() + ), + } + } + + pub fn attachment(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.attachment = value + .try_into() + .map_err(|_| "conversion to Vec for attachment failed".to_string()); + self.attachment_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for attachment_content_type failed".to_string()); + self + } + + pub fn document(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.document = value + .try_into() + .map_err(|_| "conversion to Vec for document failed".to_string()); + self.document_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for document_content_type failed".to_string()); + self + } + + pub fn metadata(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.metadata = value + .try_into() + .map_err(|_| "conversion to `FileMetadata` for metadata failed".to_string()); + self + } + + pub fn thumbnail(mut self, value: V, content_type: C) -> Self + where + V: std::convert::TryInto>, + C: std::convert::TryInto, + { + self.thumbnail = value + .try_into() + .map_err(|_| "conversion to Vec for thumbnail failed".to_string()); + self.thumbnail_content_type = content_type + .try_into() + .map_err(|_| "conversion to String for thumbnail_content_type failed".to_string()); + self + } + + ///Sends a `POST` request to `/upload-multiple` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + attachment, + document, + metadata, + thumbnail, + attachment_content_type, + document_content_type, + thumbnail_content_type, + } = self; + let attachment = attachment.map_err(Error::InvalidRequest)?; + let document = document.map_err(Error::InvalidRequest)?; + let metadata = metadata.map_err(Error::InvalidRequest)?; + let thumbnail = thumbnail.map_err(Error::InvalidRequest)?; + let attachment_content_type = attachment_content_type.map_err(Error::InvalidRequest)?; + let document_content_type = document_content_type.map_err(Error::InvalidRequest)?; + let thumbnail_content_type = thumbnail_content_type.map_err(Error::InvalidRequest)?; + let body = types::UploadMultipleFilesMultipartParts { + attachment, + attachment_content_type, + document, + document_content_type, + metadata, + thumbnail, + thumbnail_content_type, + }; + let url = format!("{}/upload-multiple", 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .multipart_related(&body)? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_multiple_files", + }; + 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 => ResponseValue::from_response(response).await, + 400u16 => Err(Error::ErrorResponse(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/multipart_related_test_cli.rs b/progenitor-impl/tests/output/src/multipart_related_test_cli.rs new file mode 100644 index 00000000..1aa7ef73 --- /dev/null +++ b/progenitor-impl/tests/output/src/multipart_related_test_cli.rs @@ -0,0 +1,252 @@ +use crate::multipart_related_test_builder::*; +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::UploadFile => Self::cli_upload_file(), + CliCommand::UploadSimple => Self::cli_upload_simple(), + CliCommand::UploadMultipleFiles => Self::cli_upload_multiple_files(), + } + } + + pub fn cli_upload_file() -> ::clap::Command { + ::clap::Command::new("") + .arg( + ::clap::Arg::new("file-content-type") + .long("file-content-type") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body") + .help("MIME type for the file field"), + ) + .arg( + ::clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(true) + .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"), + ) + .about("Upload a file with metadata using multipart/related") + .long_about("Uploads a file along with JSON metadata in a multipart/related request") + } + + pub fn cli_upload_simple() -> ::clap::Command { + ::clap::Command::new("").about("Simple upload using multipart/related") + } + + pub fn cli_upload_multiple_files() -> ::clap::Command { + ::clap::Command::new("") + .arg( + ::clap::Arg::new("attachment-content-type") + .long("attachment-content-type") + .value_parser(::clap::value_parser!(::std::string::String)) + .required(false) + .help("MIME type for the attachment field"), + ) + .arg( + ::clap::Arg::new("document-content-type") + .long("document-content-type") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body") + .help("MIME type for the document field"), + ) + .arg( + ::clap::Arg::new("thumbnail-content-type") + .long("thumbnail-content-type") + .value_parser(::clap::value_parser!(::std::string::String)) + .required_unless_present("json-body") + .help("MIME type for the thumbnail field"), + ) + .arg( + ::clap::Arg::new("json-body") + .long("json-body") + .value_name("JSON-FILE") + .required(true) + .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"), + ) + .about("Upload multiple files with metadata using multipart/related") + .long_about( + "Uploads multiple files along with JSON metadata in a single multipart/related \ + request", + ) + } + + pub async fn execute( + &self, + cmd: CliCommand, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + match cmd { + CliCommand::UploadFile => self.execute_upload_file(matches).await, + CliCommand::UploadSimple => self.execute_upload_simple(matches).await, + CliCommand::UploadMultipleFiles => self.execute_upload_multiple_files(matches).await, + } + } + + pub async fn execute_upload_file(&self, matches: &::clap::ArgMatches) -> anyhow::Result<()> { + let mut request = self.client.upload_file(); + if let Some(value) = matches.get_one::<::std::string::String>("file-content-type") { + request = request.body_map(|body| body.file_content_type(value.clone())) + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value).unwrap(); + let body_value = + serde_json::from_str::(&body_txt).unwrap(); + request = request.body(body_value); + } + + self.config.execute_upload_file(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.success_item(&r); + Ok(()) + } + Err(r) => { + self.config.error(&r); + Err(anyhow::Error::new(r)) + } + } + } + + pub async fn execute_upload_simple(&self, matches: &::clap::ArgMatches) -> anyhow::Result<()> { + let mut request = self.client.upload_simple(); + self.config.execute_upload_simple(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 async fn execute_upload_multiple_files( + &self, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + let mut request = self.client.upload_multiple_files(); + if let Some(value) = matches.get_one::<::std::string::String>("attachment-content-type") { + request = request.body_map(|body| body.attachment_content_type(value.clone())) + } + + if let Some(value) = matches.get_one::<::std::string::String>("document-content-type") { + request = request.body_map(|body| body.document_content_type(value.clone())) + } + + if let Some(value) = matches.get_one::<::std::string::String>("thumbnail-content-type") { + request = request.body_map(|body| body.thumbnail_content_type(value.clone())) + } + + if let Some(value) = matches.get_one::("json-body") { + let body_txt = std::fs::read_to_string(value).unwrap(); + let body_value = + serde_json::from_str::(&body_txt) + .unwrap(); + request = request.body(body_value); + } + + self.config + .execute_upload_multiple_files(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.success_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_file( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::UploadFile, + ) -> anyhow::Result<()> { + Ok(()) + } + + fn execute_upload_simple( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::UploadSimple, + ) -> anyhow::Result<()> { + Ok(()) + } + + fn execute_upload_multiple_files( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::UploadMultipleFiles, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum CliCommand { + UploadFile, + UploadSimple, + UploadMultipleFiles, +} + +impl CliCommand { + pub fn iter() -> impl Iterator { + vec![ + CliCommand::UploadFile, + CliCommand::UploadSimple, + CliCommand::UploadMultipleFiles, + ] + .into_iter() + } +} diff --git a/progenitor-impl/tests/output/src/multipart_related_test_httpmock.rs b/progenitor-impl/tests/output/src/multipart_related_test_httpmock.rs new file mode 100644 index 00000000..d997c165 --- /dev/null +++ b/progenitor-impl/tests/output/src/multipart_related_test_httpmock.rs @@ -0,0 +1,179 @@ +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::multipart_related_test_builder::*; + pub struct UploadFileWhen(::httpmock::When); + impl UploadFileWhen { + 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::UploadFileMultipartParts) -> Self { + Self(self.0.json_body_obj(value)) + } + } + + pub struct UploadFileThen(::httpmock::Then); + impl UploadFileThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn ok(self, value: &types::UploadResponse) -> Self { + Self( + self.0 + .status(200u16) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + + pub fn bad_request(self) -> Self { + Self(self.0.status(400u16)) + } + } + + pub struct UploadSimpleWhen(::httpmock::When); + impl UploadSimpleWhen { + pub fn new(inner: ::httpmock::When) -> Self { + Self( + inner + .method(::httpmock::Method::POST) + .path_matches(regex::Regex::new("^/upload-simple$").unwrap()), + ) + } + + pub fn into_inner(self) -> ::httpmock::When { + self.0 + } + + pub fn body(self, value: ::serde_json::Value) -> Self { + Self(self.0.json_body(value)) + } + } + + pub struct UploadSimpleThen(::httpmock::Then); + impl UploadSimpleThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn no_content(self) -> Self { + Self(self.0.status(204u16)) + } + } + + pub struct UploadMultipleFilesWhen(::httpmock::When); + impl UploadMultipleFilesWhen { + pub fn new(inner: ::httpmock::When) -> Self { + Self( + inner + .method(::httpmock::Method::POST) + .path_matches(regex::Regex::new("^/upload-multiple$").unwrap()), + ) + } + + pub fn into_inner(self) -> ::httpmock::When { + self.0 + } + + pub fn body(self, value: &types::UploadMultipleFilesMultipartParts) -> Self { + Self(self.0.json_body_obj(value)) + } + } + + pub struct UploadMultipleFilesThen(::httpmock::Then); + impl UploadMultipleFilesThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn ok(self, value: &types::UploadResponse) -> Self { + Self( + self.0 + .status(200u16) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + + pub fn bad_request(self) -> Self { + Self(self.0.status(400u16)) + } + } +} + +#[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_file(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadFileWhen, operations::UploadFileThen); + fn upload_simple(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadSimpleWhen, operations::UploadSimpleThen); + fn upload_multiple_files(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadMultipleFilesWhen, operations::UploadMultipleFilesThen); +} + +impl MockServerExt for ::httpmock::MockServer { + fn upload_file(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadFileWhen, operations::UploadFileThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadFileWhen::new(when), + operations::UploadFileThen::new(then), + ) + }) + } + + fn upload_simple(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadSimpleWhen, operations::UploadSimpleThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadSimpleWhen::new(when), + operations::UploadSimpleThen::new(then), + ) + }) + } + + fn upload_multiple_files(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadMultipleFilesWhen, operations::UploadMultipleFilesThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadMultipleFilesWhen::new(when), + operations::UploadMultipleFilesThen::new(then), + ) + }) + } +} diff --git a/progenitor-impl/tests/output/src/multipart_related_test_positional.rs b/progenitor-impl/tests/output/src/multipart_related_test_positional.rs new file mode 100644 index 00000000..8ae7b26d --- /dev/null +++ b/progenitor-impl/tests/output/src/multipart_related_test_positional.rs @@ -0,0 +1,512 @@ +#[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()) + } + } + } + + ///`FileMetadata` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "mimeType", + /// "name" + /// ], + /// "properties": { + /// "description": { + /// "description": "Optional description of the file", + /// "type": "string" + /// }, + /// "mimeType": { + /// "description": "The MIME type of the file", + /// "type": "string" + /// }, + /// "name": { + /// "description": "The name of the file", + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct FileMetadata { + ///Optional description of the file + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub description: ::std::option::Option<::std::string::String>, + ///The MIME type of the file + #[serde(rename = "mimeType")] + pub mime_type: ::std::string::String, + ///The name of the file + pub name: ::std::string::String, + } + + impl ::std::convert::From<&FileMetadata> for FileMetadata { + fn from(value: &FileMetadata) -> Self { + value.clone() + } + } + + ///`UploadFileMultipartParts` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "file", + /// "file_content_type", + /// "metadata" + /// ], + /// "properties": { + /// "file": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "file_content_type": { + /// "description": "MIME type for the file field", + /// "type": "string" + /// }, + /// "metadata": { + /// "$ref": "#/components/schemas/FileMetadata" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadFileMultipartParts { + pub file: ::std::vec::Vec, + ///MIME type for the file field + pub file_content_type: ::std::string::String, + pub metadata: FileMetadata, + } + + impl ::std::convert::From<&UploadFileMultipartParts> for UploadFileMultipartParts { + fn from(value: &UploadFileMultipartParts) -> Self { + value.clone() + } + } + + ///`UploadMultipleFilesMultipartParts` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "document", + /// "document_content_type", + /// "metadata", + /// "thumbnail", + /// "thumbnail_content_type" + /// ], + /// "properties": { + /// "attachment": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "attachment_content_type": { + /// "description": "MIME type for the attachment field", + /// "type": "string" + /// }, + /// "document": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "document_content_type": { + /// "description": "MIME type for the document field", + /// "type": "string" + /// }, + /// "metadata": { + /// "$ref": "#/components/schemas/FileMetadata" + /// }, + /// "thumbnail": { + /// "type": "array", + /// "items": { + /// "type": "integer", + /// "format": "uint8", + /// "maximum": 255.0, + /// "minimum": 0.0 + /// } + /// }, + /// "thumbnail_content_type": { + /// "description": "MIME type for the thumbnail field", + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadMultipleFilesMultipartParts { + #[serde(default, skip_serializing_if = "::std::vec::Vec::is_empty")] + pub attachment: ::std::vec::Vec, + ///MIME type for the attachment field + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub attachment_content_type: ::std::option::Option<::std::string::String>, + pub document: ::std::vec::Vec, + ///MIME type for the document field + pub document_content_type: ::std::string::String, + pub metadata: FileMetadata, + pub thumbnail: ::std::vec::Vec, + ///MIME type for the thumbnail field + pub thumbnail_content_type: ::std::string::String, + } + + impl ::std::convert::From<&UploadMultipleFilesMultipartParts> + for UploadMultipleFilesMultipartParts + { + fn from(value: &UploadMultipleFilesMultipartParts) -> Self { + value.clone() + } + } + + ///`UploadResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "required": [ + /// "id", + /// "name", + /// "size" + /// ], + /// "properties": { + /// "id": { + /// "description": "The ID of the uploaded file", + /// "type": "string" + /// }, + /// "name": { + /// "description": "The name of the uploaded file", + /// "type": "string" + /// }, + /// "size": { + /// "description": "The size of the uploaded file in bytes", + /// "type": "integer" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadResponse { + ///The ID of the uploaded file + pub id: ::std::string::String, + ///The name of the uploaded file + pub name: ::std::string::String, + ///The size of the uploaded file in bytes + pub size: i64, + } + + impl ::std::convert::From<&UploadResponse> for UploadResponse { + fn from(value: &UploadResponse) -> Self { + value.clone() + } + } + + impl crate::progenitor_client::MultipartRelatedBody for UploadFileMultipartParts { + fn as_multipart_parts(&self) -> Vec { + vec![ + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: "metadata", + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.metadata).expect("failed to serialize field"), + ), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.file_content_type, + content_id: "file", + bytes: ::std::borrow::Cow::Borrowed(&self.file), + }), + ] + .into_iter() + .flatten() + .collect() + } + } + + impl crate::progenitor_client::MultipartRelatedBody for UploadMultipleFilesMultipartParts { + fn as_multipart_parts(&self) -> Vec { + vec![ + Some(crate::progenitor_client::MultipartPart { + content_type: "application/json", + content_id: "metadata", + bytes: ::std::borrow::Cow::Owned( + ::serde_json::to_vec(&self.metadata).expect("failed to serialize field"), + ), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.document_content_type, + content_id: "document", + bytes: ::std::borrow::Cow::Borrowed(&self.document), + }), + Some(crate::progenitor_client::MultipartPart { + content_type: &self.thumbnail_content_type, + content_id: "thumbnail", + bytes: ::std::borrow::Cow::Borrowed(&self.thumbnail), + }), + if let Some(ref content_type) = self.attachment_content_type { + if !self.attachment.is_empty() { + Some(crate::progenitor_client::MultipartPart { + content_type: content_type.as_str(), + content_id: "attachment", + bytes: ::std::borrow::Cow::Borrowed(&self.attachment), + }) + } else { + None + } + } else { + None + }, + ] + .into_iter() + .flatten() + .collect() + } + } +} + +#[derive(Clone, Debug)] +///Client for Multipart Related Test API +/// +///Test API for multipart/related content type support +/// +///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 { + ///Upload a file with metadata using multipart/related + /// + ///Uploads a file along with JSON metadata in a multipart/related request + /// + ///Sends a `POST` request to `/upload` + pub async fn upload_file<'a>( + &'a self, + body: &'a types::UploadFileMultipartParts, + ) -> 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .multipart_related(&body)? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + 400u16 => Err(Error::ErrorResponse(ResponseValue::empty(response))), + _ => Err(Error::UnexpectedResponse(response)), + } + } + + ///Simple upload using multipart/related + /// + ///Sends a `POST` request to `/upload-simple` + pub async fn upload_simple<'a, B: Into>( + &'a self, + body: B, + ) -> Result, Error<()>> { + let url = format!("{}/upload-simple", 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) + .header( + ::reqwest::header::CONTENT_TYPE, + ::reqwest::header::HeaderValue::from_static("multipart/related"), + ) + .body(body) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_simple", + }; + 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() { + 204u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + + ///Upload multiple files with metadata using multipart/related + /// + ///Uploads multiple files along with JSON metadata in a single + /// multipart/related request + /// + ///Sends a `POST` request to `/upload-multiple` + pub async fn upload_multiple_files<'a>( + &'a self, + body: &'a types::UploadMultipleFilesMultipartParts, + ) -> Result, Error<()>> { + let url = format!("{}/upload-multiple", 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .multipart_related(&body)? + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_multiple_files", + }; + 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 => ResponseValue::from_response(response).await, + 400u16 => Err(Error::ErrorResponse(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/output/src/query_param_defaults_builder.rs b/progenitor-impl/tests/output/src/query_param_defaults_builder.rs new file mode 100644 index 00000000..214300d8 --- /dev/null +++ b/progenitor-impl/tests/output/src/query_param_defaults_builder.rs @@ -0,0 +1,417 @@ +#[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()) + } + } + } + + ///`UploadFileResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "properties": { + /// "status": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, :: serde :: Serialize, Clone, Debug, schemars :: JsonSchema, + )] + pub struct UploadFileResponse { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub status: ::std::option::Option<::std::string::String>, + } + + impl ::std::convert::From<&UploadFileResponse> for UploadFileResponse { + fn from(value: &UploadFileResponse) -> Self { + value.clone() + } + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Default::default(), + } + } + } + + impl UploadFileResponse { + pub fn builder() -> builder::UploadFileResponse { + Default::default() + } + } + + ///`UploadFileUploadType` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "default": "multipart", + /// "type": "string", + /// "enum": [ + /// "multipart" + /// ] + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + schemars :: JsonSchema, + )] + pub enum UploadFileUploadType { + #[serde(rename = "multipart")] + Multipart, + } + + impl ::std::convert::From<&Self> for UploadFileUploadType { + fn from(value: &UploadFileUploadType) -> Self { + value.clone() + } + } + + impl ::std::fmt::Display for UploadFileUploadType { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Multipart => f.write_str("multipart"), + } + } + } + + impl ::std::str::FromStr for UploadFileUploadType { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "multipart" => Ok(Self::Multipart), + _ => Err("invalid value".into()), + } + } + } + + impl ::std::convert::TryFrom<&str> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<&::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::default::Default for UploadFileUploadType { + fn default() -> Self { + UploadFileUploadType::Multipart + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadFileResponse { + status: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Ok(Default::default()), + } + } + } + + impl UploadFileResponse { + pub fn status(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.status = value + .try_into() + .map_err(|e| format!("error converting supplied value for status: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadFileResponse { + type Error = super::error::ConversionError; + fn try_from( + value: UploadFileResponse, + ) -> ::std::result::Result { + Ok(Self { + status: value.status?, + }) + } + } + + impl ::std::convert::From for UploadFileResponse { + fn from(value: super::UploadFileResponse) -> Self { + Self { + status: Ok(value.status), + } + } + } + } +} + +#[derive(Clone, Debug)] +///Client for Default Parameter Test API +/// +///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 { + ///Upload a file + /// + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload_file() + /// .required_param(required_param) + /// .supports_all_drives(supports_all_drives) + /// .upload_type(upload_type) + /// .send() + /// .await; + /// ``` + pub fn upload_file(&self) -> builder::UploadFile<'_> { + builder::UploadFile::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_file`] + /// + ///[`Client::upload_file`]: super::Client::upload_file + #[derive(Debug, Clone)] + pub struct UploadFile<'a> { + client: &'a super::Client, + required_param: Result<::std::string::String, String>, + supports_all_drives: Result, String>, + upload_type: Result, String>, + } + + impl<'a> UploadFile<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + required_param: Err("required_param was not initialized".to_string()), + supports_all_drives: Ok(None), + upload_type: Ok(Some(::std::default::Default::default())), + } + } + + pub fn required_param(mut self, value: V) -> Self + where + V: std::convert::TryInto<::std::string::String>, + { + self.required_param = value.try_into().map_err(|_| { + "conversion to `:: std :: string :: String` for required_param failed".to_string() + }); + self + } + + pub fn supports_all_drives(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.supports_all_drives = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `bool` for supports_all_drives failed".to_string()); + self + } + + pub fn upload_type(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.upload_type = value.try_into().map(Some).map_err(|_| { + "conversion to `UploadFileUploadType` for upload_type failed".to_string() + }); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + required_param, + supports_all_drives, + upload_type, + } = self; + let required_param = required_param.map_err(Error::InvalidRequest)?; + let supports_all_drives = supports_all_drives.map_err(Error::InvalidRequest)?; + let upload_type = upload_type.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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .query(&progenitor_client::QueryParam::new( + "requiredParam", + &required_param, + )) + .query(&progenitor_client::QueryParam::new( + "supportsAllDrives", + &supports_all_drives, + )) + .query(&progenitor_client::QueryParam::new( + "uploadType", + &upload_type, + )) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + _ => 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/query_param_defaults_builder_tagged.rs b/progenitor-impl/tests/output/src/query_param_defaults_builder_tagged.rs new file mode 100644 index 00000000..97e19576 --- /dev/null +++ b/progenitor-impl/tests/output/src/query_param_defaults_builder_tagged.rs @@ -0,0 +1,416 @@ +#[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()) + } + } + } + + ///`UploadFileResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "properties": { + /// "status": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadFileResponse { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub status: ::std::option::Option<::std::string::String>, + } + + impl ::std::convert::From<&UploadFileResponse> for UploadFileResponse { + fn from(value: &UploadFileResponse) -> Self { + value.clone() + } + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Default::default(), + } + } + } + + impl UploadFileResponse { + pub fn builder() -> builder::UploadFileResponse { + Default::default() + } + } + + ///`UploadFileUploadType` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "default": "multipart", + /// "type": "string", + /// "enum": [ + /// "multipart" + /// ] + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + )] + pub enum UploadFileUploadType { + #[serde(rename = "multipart")] + Multipart, + } + + impl ::std::convert::From<&Self> for UploadFileUploadType { + fn from(value: &UploadFileUploadType) -> Self { + value.clone() + } + } + + impl ::std::fmt::Display for UploadFileUploadType { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Multipart => f.write_str("multipart"), + } + } + } + + impl ::std::str::FromStr for UploadFileUploadType { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "multipart" => Ok(Self::Multipart), + _ => Err("invalid value".into()), + } + } + } + + impl ::std::convert::TryFrom<&str> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<&::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::default::Default for UploadFileUploadType { + fn default() -> Self { + UploadFileUploadType::Multipart + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadFileResponse { + status: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Ok(Default::default()), + } + } + } + + impl UploadFileResponse { + pub fn status(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.status = value + .try_into() + .map_err(|e| format!("error converting supplied value for status: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadFileResponse { + type Error = super::error::ConversionError; + fn try_from( + value: UploadFileResponse, + ) -> ::std::result::Result { + Ok(Self { + status: value.status?, + }) + } + } + + impl ::std::convert::From for UploadFileResponse { + fn from(value: super::UploadFileResponse) -> Self { + Self { + status: Ok(value.status), + } + } + } + } +} + +#[derive(Clone, Debug)] +///Client for Default Parameter Test API +/// +///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 { + ///Upload a file + /// + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload_file() + /// .required_param(required_param) + /// .supports_all_drives(supports_all_drives) + /// .upload_type(upload_type) + /// .send() + /// .await; + /// ``` + pub fn upload_file(&self) -> builder::UploadFile<'_> { + builder::UploadFile::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_file`] + /// + ///[`Client::upload_file`]: super::Client::upload_file + #[derive(Debug, Clone)] + pub struct UploadFile<'a> { + client: &'a super::Client, + required_param: Result<::std::string::String, String>, + supports_all_drives: Result, String>, + upload_type: Result, String>, + } + + impl<'a> UploadFile<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + required_param: Err("required_param was not initialized".to_string()), + supports_all_drives: Ok(None), + upload_type: Ok(Some(::std::default::Default::default())), + } + } + + pub fn required_param(mut self, value: V) -> Self + where + V: std::convert::TryInto<::std::string::String>, + { + self.required_param = value.try_into().map_err(|_| { + "conversion to `:: std :: string :: String` for required_param failed".to_string() + }); + self + } + + pub fn supports_all_drives(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.supports_all_drives = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `bool` for supports_all_drives failed".to_string()); + self + } + + pub fn upload_type(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.upload_type = value.try_into().map(Some).map_err(|_| { + "conversion to `UploadFileUploadType` for upload_type failed".to_string() + }); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + required_param, + supports_all_drives, + upload_type, + } = self; + let required_param = required_param.map_err(Error::InvalidRequest)?; + let supports_all_drives = supports_all_drives.map_err(Error::InvalidRequest)?; + let upload_type = upload_type.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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .query(&progenitor_client::QueryParam::new( + "requiredParam", + &required_param, + )) + .query(&progenitor_client::QueryParam::new( + "supportsAllDrives", + &supports_all_drives, + )) + .query(&progenitor_client::QueryParam::new( + "uploadType", + &upload_type, + )) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + _ => 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/query_param_defaults_cli.rs b/progenitor-impl/tests/output/src/query_param_defaults_cli.rs new file mode 100644 index 00000000..4c74e8bb --- /dev/null +++ b/progenitor-impl/tests/output/src/query_param_defaults_cli.rs @@ -0,0 +1,123 @@ +use crate::query_param_defaults_builder::*; +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::UploadFile => Self::cli_upload_file(), + } + } + + pub fn cli_upload_file() -> ::clap::Command { + ::clap::Command::new("") + .arg( + ::clap::Arg::new("required-param") + .long("required-param") + .value_parser(::clap::value_parser!(::std::string::String)) + .required(true), + ) + .arg( + ::clap::Arg::new("supports-all-drives") + .long("supports-all-drives") + .value_parser(::clap::value_parser!(bool)) + .required(false), + ) + .arg( + ::clap::Arg::new("upload-type") + .long("upload-type") + .value_parser(::clap::builder::TypedValueParser::map( + ::clap::builder::PossibleValuesParser::new([ + types::UploadFileUploadType::Multipart.to_string(), + ]), + |s| types::UploadFileUploadType::try_from(s).unwrap(), + )) + .required(false), + ) + .about("Upload a file") + } + + pub async fn execute( + &self, + cmd: CliCommand, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + match cmd { + CliCommand::UploadFile => self.execute_upload_file(matches).await, + } + } + + pub async fn execute_upload_file(&self, matches: &::clap::ArgMatches) -> anyhow::Result<()> { + let mut request = self.client.upload_file(); + if let Some(value) = matches.get_one::<::std::string::String>("required-param") { + request = request.required_param(value.clone()); + } + + if let Some(value) = matches.get_one::("supports-all-drives") { + request = request.supports_all_drives(value.clone()); + } + + if let Some(value) = matches.get_one::("upload-type") { + request = request.upload_type(value.clone()); + } + + self.config.execute_upload_file(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.success_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_file( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::UploadFile, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum CliCommand { + UploadFile, +} + +impl CliCommand { + pub fn iter() -> impl Iterator { + vec![CliCommand::UploadFile].into_iter() + } +} diff --git a/progenitor-impl/tests/output/src/query_param_defaults_httpmock.rs b/progenitor-impl/tests/output/src/query_param_defaults_httpmock.rs new file mode 100644 index 00000000..78f1b7e8 --- /dev/null +++ b/progenitor-impl/tests/output/src/query_param_defaults_httpmock.rs @@ -0,0 +1,100 @@ +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::query_param_defaults_builder::*; + pub struct UploadFileWhen(::httpmock::When); + impl UploadFileWhen { + 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 required_param(self, value: &str) -> Self { + Self(self.0.query_param("requiredParam", value.to_string())) + } + + pub fn supports_all_drives(self, value: T) -> Self + where + T: Into>, + { + if let Some(value) = value.into() { + Self(self.0.query_param("supportsAllDrives", value.to_string())) + } else { + Self(self.0.matches(|req| { + req.query_params + .as_ref() + .and_then(|qs| qs.iter().find(|(key, _)| key == "supportsAllDrives")) + .is_none() + })) + } + } + + pub fn upload_type(self, value: T) -> Self + where + T: Into>, + { + if let Some(value) = value.into() { + Self(self.0.query_param("uploadType", value.to_string())) + } else { + Self(self.0.matches(|req| { + req.query_params + .as_ref() + .and_then(|qs| qs.iter().find(|(key, _)| key == "uploadType")) + .is_none() + })) + } + } + } + + pub struct UploadFileThen(::httpmock::Then); + impl UploadFileThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn ok(self, value: &types::UploadFileResponse) -> Self { + Self( + self.0 + .status(200u16) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + } +} + +#[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_file(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadFileWhen, operations::UploadFileThen); +} + +impl MockServerExt for ::httpmock::MockServer { + fn upload_file(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadFileWhen, operations::UploadFileThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadFileWhen::new(when), + operations::UploadFileThen::new(then), + ) + }) + } +} diff --git a/progenitor-impl/tests/output/src/query_param_defaults_positional.rs b/progenitor-impl/tests/output/src/query_param_defaults_positional.rs new file mode 100644 index 00000000..13c84e10 --- /dev/null +++ b/progenitor-impl/tests/output/src/query_param_defaults_positional.rs @@ -0,0 +1,278 @@ +#[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()) + } + } + } + + ///`UploadFileResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "properties": { + /// "status": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadFileResponse { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub status: ::std::option::Option<::std::string::String>, + } + + impl ::std::convert::From<&UploadFileResponse> for UploadFileResponse { + fn from(value: &UploadFileResponse) -> Self { + value.clone() + } + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Default::default(), + } + } + } + + ///`UploadFileUploadType` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "default": "multipart", + /// "type": "string", + /// "enum": [ + /// "multipart" + /// ] + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + )] + pub enum UploadFileUploadType { + #[serde(rename = "multipart")] + Multipart, + } + + impl ::std::convert::From<&Self> for UploadFileUploadType { + fn from(value: &UploadFileUploadType) -> Self { + value.clone() + } + } + + impl ::std::fmt::Display for UploadFileUploadType { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Multipart => f.write_str("multipart"), + } + } + } + + impl ::std::str::FromStr for UploadFileUploadType { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "multipart" => Ok(Self::Multipart), + _ => Err("invalid value".into()), + } + } + } + + impl ::std::convert::TryFrom<&str> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<&::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::default::Default for UploadFileUploadType { + fn default() -> Self { + UploadFileUploadType::Multipart + } + } +} + +#[derive(Clone, Debug)] +///Client for Default Parameter Test API +/// +///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 { + ///Upload a file + /// + ///Sends a `POST` request to `/upload` + pub async fn upload_file<'a>( + &'a self, + required_param: &'a str, + supports_all_drives: Option, + upload_type: Option, + ) -> 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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .query(&progenitor_client::QueryParam::new( + "requiredParam", + &required_param, + )) + .query(&progenitor_client::QueryParam::new( + "supportsAllDrives", + &supports_all_drives, + )) + .query(&progenitor_client::QueryParam::new( + "uploadType", + &upload_type, + )) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + _ => 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/output/src/test_default_params_builder_tagged.rs b/progenitor-impl/tests/output/src/test_default_params_builder_tagged.rs new file mode 100644 index 00000000..d2938c9f --- /dev/null +++ b/progenitor-impl/tests/output/src/test_default_params_builder_tagged.rs @@ -0,0 +1,416 @@ +#[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()) + } + } + } + + ///`UploadFileResponse` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "type": "object", + /// "properties": { + /// "status": { + /// "type": "string" + /// } + /// } + ///} + /// ``` + ///
+ #[derive(:: serde :: Deserialize, :: serde :: Serialize, Clone, Debug)] + pub struct UploadFileResponse { + #[serde(default, skip_serializing_if = "::std::option::Option::is_none")] + pub status: ::std::option::Option<::std::string::String>, + } + + impl ::std::convert::From<&UploadFileResponse> for UploadFileResponse { + fn from(value: &UploadFileResponse) -> Self { + value.clone() + } + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Default::default(), + } + } + } + + impl UploadFileResponse { + pub fn builder() -> builder::UploadFileResponse { + Default::default() + } + } + + ///`UploadFileUploadType` + /// + ///
JSON schema + /// + /// ```json + ///{ + /// "default": "multipart", + /// "type": "string", + /// "enum": [ + /// "multipart" + /// ] + ///} + /// ``` + ///
+ #[derive( + :: serde :: Deserialize, + :: serde :: Serialize, + Clone, + Copy, + Debug, + Eq, + Hash, + Ord, + PartialEq, + PartialOrd, + )] + pub enum UploadFileUploadType { + #[serde(rename = "multipart")] + Multipart, + } + + impl ::std::convert::From<&Self> for UploadFileUploadType { + fn from(value: &UploadFileUploadType) -> Self { + value.clone() + } + } + + impl ::std::fmt::Display for UploadFileUploadType { + fn fmt(&self, f: &mut ::std::fmt::Formatter<'_>) -> ::std::fmt::Result { + match *self { + Self::Multipart => f.write_str("multipart"), + } + } + } + + impl ::std::str::FromStr for UploadFileUploadType { + type Err = self::error::ConversionError; + fn from_str(value: &str) -> ::std::result::Result { + match value { + "multipart" => Ok(Self::Multipart), + _ => Err("invalid value".into()), + } + } + } + + impl ::std::convert::TryFrom<&str> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from(value: &str) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<&::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: &::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::convert::TryFrom<::std::string::String> for UploadFileUploadType { + type Error = self::error::ConversionError; + fn try_from( + value: ::std::string::String, + ) -> ::std::result::Result { + value.parse() + } + } + + impl ::std::default::Default for UploadFileUploadType { + fn default() -> Self { + UploadFileUploadType::Multipart + } + } + + /// Types for composing complex structures. + pub mod builder { + #[derive(Clone, Debug)] + pub struct UploadFileResponse { + status: ::std::result::Result< + ::std::option::Option<::std::string::String>, + ::std::string::String, + >, + } + + impl ::std::default::Default for UploadFileResponse { + fn default() -> Self { + Self { + status: Ok(Default::default()), + } + } + } + + impl UploadFileResponse { + pub fn status(mut self, value: T) -> Self + where + T: ::std::convert::TryInto<::std::option::Option<::std::string::String>>, + T::Error: ::std::fmt::Display, + { + self.status = value + .try_into() + .map_err(|e| format!("error converting supplied value for status: {}", e)); + self + } + } + + impl ::std::convert::TryFrom for super::UploadFileResponse { + type Error = super::error::ConversionError; + fn try_from( + value: UploadFileResponse, + ) -> ::std::result::Result { + Ok(Self { + status: value.status?, + }) + } + } + + impl ::std::convert::From for UploadFileResponse { + fn from(value: super::UploadFileResponse) -> Self { + Self { + status: Ok(value.status), + } + } + } + } +} + +#[derive(Clone, Debug)] +///Client for Default Parameter Test API +/// +///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 { + ///Upload a file + /// + ///Sends a `POST` request to `/upload` + /// + ///```ignore + /// let response = client.upload_file() + /// .required_param(required_param) + /// .supports_all_drives(supports_all_drives) + /// .upload_type(upload_type) + /// .send() + /// .await; + /// ``` + pub fn upload_file(&self) -> builder::UploadFile<'_> { + builder::UploadFile::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_file`] + /// + ///[`Client::upload_file`]: super::Client::upload_file + #[derive(Debug, Clone)] + pub struct UploadFile<'a> { + client: &'a super::Client, + required_param: Result<::std::string::String, String>, + supports_all_drives: Result, String>, + upload_type: Result, String>, + } + + impl<'a> UploadFile<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client: client, + required_param: Err("required_param was not initialized".to_string()), + supports_all_drives: Ok(None), + upload_type: Ok(None), + } + } + + pub fn required_param(mut self, value: V) -> Self + where + V: std::convert::TryInto<::std::string::String>, + { + self.required_param = value.try_into().map_err(|_| { + "conversion to `:: std :: string :: String` for required_param failed".to_string() + }); + self + } + + pub fn supports_all_drives(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.supports_all_drives = value + .try_into() + .map(Some) + .map_err(|_| "conversion to `bool` for supports_all_drives failed".to_string()); + self + } + + pub fn upload_type(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.upload_type = value.try_into().map(Some).map_err(|_| { + "conversion to `UploadFileUploadType` for upload_type failed".to_string() + }); + self + } + + ///Sends a `POST` request to `/upload` + pub async fn send(self) -> Result, Error<()>> { + let Self { + client, + required_param, + supports_all_drives, + upload_type, + } = self; + let required_param = required_param.map_err(Error::InvalidRequest)?; + let supports_all_drives = supports_all_drives.map_err(Error::InvalidRequest)?; + let upload_type = upload_type.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) + .header( + ::reqwest::header::ACCEPT, + ::reqwest::header::HeaderValue::from_static("application/json"), + ) + .query(&progenitor_client::QueryParam::new( + "requiredParam", + &required_param, + )) + .query(&progenitor_client::QueryParam::new( + "supportsAllDrives", + &supports_all_drives, + )) + .query(&progenitor_client::QueryParam::new( + "uploadType", + &upload_type, + )) + .headers(header_map) + .build()?; + let info = OperationInfo { + operation_id: "upload_file", + }; + 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 => ResponseValue::from_response(response).await, + _ => 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/test_default_params_cli.rs b/progenitor-impl/tests/output/src/test_default_params_cli.rs new file mode 100644 index 00000000..2f6e46e4 --- /dev/null +++ b/progenitor-impl/tests/output/src/test_default_params_cli.rs @@ -0,0 +1,123 @@ +use crate::test_default_params_builder::*; +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::UploadFile => Self::cli_upload_file(), + } + } + + pub fn cli_upload_file() -> ::clap::Command { + ::clap::Command::new("") + .arg( + ::clap::Arg::new("required-param") + .long("required-param") + .value_parser(::clap::value_parser!(::std::string::String)) + .required(true), + ) + .arg( + ::clap::Arg::new("supports-all-drives") + .long("supports-all-drives") + .value_parser(::clap::value_parser!(bool)) + .required(false), + ) + .arg( + ::clap::Arg::new("upload-type") + .long("upload-type") + .value_parser(::clap::builder::TypedValueParser::map( + ::clap::builder::PossibleValuesParser::new([ + types::UploadFileUploadType::Multipart.to_string(), + ]), + |s| types::UploadFileUploadType::try_from(s).unwrap(), + )) + .required(false), + ) + .about("Upload a file") + } + + pub async fn execute( + &self, + cmd: CliCommand, + matches: &::clap::ArgMatches, + ) -> anyhow::Result<()> { + match cmd { + CliCommand::UploadFile => self.execute_upload_file(matches).await, + } + } + + pub async fn execute_upload_file(&self, matches: &::clap::ArgMatches) -> anyhow::Result<()> { + let mut request = self.client.upload_file(); + if let Some(value) = matches.get_one::<::std::string::String>("required-param") { + request = request.required_param(value.clone()); + } + + if let Some(value) = matches.get_one::("supports-all-drives") { + request = request.supports_all_drives(value.clone()); + } + + if let Some(value) = matches.get_one::("upload-type") { + request = request.upload_type(value.clone()); + } + + self.config.execute_upload_file(matches, &mut request)?; + let result = request.send().await; + match result { + Ok(r) => { + self.config.success_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_file( + &self, + matches: &::clap::ArgMatches, + request: &mut builder::UploadFile, + ) -> anyhow::Result<()> { + Ok(()) + } +} + +#[derive(Copy, Clone, Debug)] +pub enum CliCommand { + UploadFile, +} + +impl CliCommand { + pub fn iter() -> impl Iterator { + vec![CliCommand::UploadFile].into_iter() + } +} diff --git a/progenitor-impl/tests/output/src/test_default_params_httpmock.rs b/progenitor-impl/tests/output/src/test_default_params_httpmock.rs new file mode 100644 index 00000000..b7b0d591 --- /dev/null +++ b/progenitor-impl/tests/output/src/test_default_params_httpmock.rs @@ -0,0 +1,100 @@ +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::test_default_params_builder::*; + pub struct UploadFileWhen(::httpmock::When); + impl UploadFileWhen { + 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 required_param(self, value: &str) -> Self { + Self(self.0.query_param("requiredParam", value.to_string())) + } + + pub fn supports_all_drives(self, value: T) -> Self + where + T: Into>, + { + if let Some(value) = value.into() { + Self(self.0.query_param("supportsAllDrives", value.to_string())) + } else { + Self(self.0.matches(|req| { + req.query_params + .as_ref() + .and_then(|qs| qs.iter().find(|(key, _)| key == "supportsAllDrives")) + .is_none() + })) + } + } + + pub fn upload_type(self, value: T) -> Self + where + T: Into>, + { + if let Some(value) = value.into() { + Self(self.0.query_param("uploadType", value.to_string())) + } else { + Self(self.0.matches(|req| { + req.query_params + .as_ref() + .and_then(|qs| qs.iter().find(|(key, _)| key == "uploadType")) + .is_none() + })) + } + } + } + + pub struct UploadFileThen(::httpmock::Then); + impl UploadFileThen { + pub fn new(inner: ::httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> ::httpmock::Then { + self.0 + } + + pub fn ok(self, value: &types::UploadFileResponse) -> Self { + Self( + self.0 + .status(200u16) + .header("content-type", "application/json") + .json_body_obj(value), + ) + } + } +} + +#[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_file(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadFileWhen, operations::UploadFileThen); +} + +impl MockServerExt for ::httpmock::MockServer { + fn upload_file(&self, config_fn: F) -> ::httpmock::Mock<'_> + where + F: FnOnce(operations::UploadFileWhen, operations::UploadFileThen), + { + self.mock(|when, then| { + config_fn( + operations::UploadFileWhen::new(when), + operations::UploadFileThen::new(then), + ) + }) + } +} diff --git a/progenitor-impl/tests/test_output.rs b/progenitor-impl/tests/test_output.rs index 010315d9..6d49235f 100644 --- a/progenitor-impl/tests/test_output.rs +++ b/progenitor-impl/tests/test_output.rs @@ -163,6 +163,16 @@ fn test_cli_gen() { verify_apis("cli-gen.json"); } +#[test] +fn test_multipart_related() { + verify_apis("multipart-related-test.json"); +} + +#[test] +fn test_query_param_defaults() { + verify_apis("query-param-defaults.json"); +} + #[test] fn test_nexus_with_different_timeout() { const OPENAPI_FILE: &'static str = "nexus.json"; diff --git a/sample_openapi/multipart-related-test.json b/sample_openapi/multipart-related-test.json new file mode 100644 index 00000000..695e10b9 --- /dev/null +++ b/sample_openapi/multipart-related-test.json @@ -0,0 +1,168 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Multipart Related Test API", + "description": "Test API for multipart/related content type support", + "version": "1.0.0" + }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_file", + "summary": "Upload a file with metadata using multipart/related", + "description": "Uploads a file along with JSON metadata in a multipart/related request", + "requestBody": { + "required": true, + "content": { + "multipart/related": { + "schema": { + "type": "object", + "required": ["metadata", "file"], + "properties": { + "metadata": { + "$ref": "#/components/schemas/FileMetadata" + }, + "file": { + "type": "string", + "format": "binary", + "description": "The file content to upload" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "File uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadResponse" + } + } + } + }, + "400": { + "description": "Bad request" + } + } + } + }, + "/upload-simple": { + "post": { + "operationId": "upload_simple", + "summary": "Simple upload using multipart/related", + "requestBody": { + "required": true, + "content": { + "multipart/related": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "204": { + "description": "Upload successful" + } + } + } + }, + "/upload-multiple": { + "post": { + "operationId": "upload_multiple_files", + "summary": "Upload multiple files with metadata using multipart/related", + "description": "Uploads multiple files along with JSON metadata in a single multipart/related request", + "requestBody": { + "required": true, + "content": { + "multipart/related": { + "schema": { + "type": "object", + "required": ["metadata", "document", "thumbnail"], + "properties": { + "metadata": { + "$ref": "#/components/schemas/FileMetadata" + }, + "document": { + "type": "string", + "format": "binary", + "description": "The main document file" + }, + "thumbnail": { + "type": "string", + "format": "binary", + "description": "A thumbnail image" + }, + "attachment": { + "type": "string", + "format": "binary", + "description": "Optional attachment" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Files uploaded successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadResponse" + } + } + } + }, + "400": { + "description": "Bad request" + } + } + } + } + }, + "components": { + "schemas": { + "FileMetadata": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "The name of the file" + }, + "mimeType": { + "type": "string", + "description": "The MIME type of the file" + }, + "description": { + "type": "string", + "description": "Optional description of the file" + } + }, + "required": ["name", "mimeType"] + }, + "UploadResponse": { + "type": "object", + "properties": { + "id": { + "type": "string", + "description": "The ID of the uploaded file" + }, + "name": { + "type": "string", + "description": "The name of the uploaded file" + }, + "size": { + "type": "integer", + "description": "The size of the uploaded file in bytes" + } + }, + "required": ["id", "name", "size"] + } + } + } +} diff --git a/sample_openapi/query-param-defaults.json b/sample_openapi/query-param-defaults.json new file mode 100644 index 00000000..740cdb05 --- /dev/null +++ b/sample_openapi/query-param-defaults.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Default Parameter Test API", + "version": "1.0.0" + }, + "paths": { + "/upload": { + "post": { + "operationId": "upload_file", + "summary": "Upload a file", + "parameters": [ + { + "name": "uploadType", + "in": "query", + "required": false, + "schema": { + "type": "string", + "enum": ["multipart"], + "default": "multipart" + } + }, + { + "name": "supportsAllDrives", + "in": "query", + "required": false, + "schema": { + "type": "boolean" + } + }, + { + "name": "requiredParam", + "in": "query", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "type": "object", + "properties": { + "status": { + "type": "string" + } + } + } + } + } + } + } + } + } + } +}