diff --git a/progenitor-impl/src/httpmock.rs b/progenitor-impl/src/httpmock.rs index c1e20a4..1689590 100644 --- a/progenitor-impl/src/httpmock.rs +++ b/progenitor-impl/src/httpmock.rs @@ -8,8 +8,9 @@ use quote::{format_ident, quote, ToTokens}; use crate::{ method::{ - HttpMethod, OperationParameter, OperationParameterKind, - OperationParameterType, OperationResponse, OperationResponseStatus, + BodyContentType, HttpMethod, OperationParameter, + OperationParameterKind, OperationParameterType, OperationResponse, + OperationResponseStatus, }, to_schema::ToSchema, util::{sanitize, Case}, @@ -165,8 +166,18 @@ impl Generator { .get_type(arg_type_id) .unwrap() .parameter_ident(), - OperationParameterType::RawBody => quote! { - serde_json::Value + OperationParameterType::RawBody => match kind { + OperationParameterKind::Body( + BodyContentType::OctetStream, + ) => quote! { + serde_json::Value + }, + OperationParameterKind::Body( + BodyContentType::Text(_), + ) => quote! { + String + }, + _ => unreachable!(), }, }; @@ -229,15 +240,25 @@ impl Generator { }; } OperationParameterKind::Header(_) => quote! { todo!() }, - OperationParameterKind::Body(_) => match typ { - OperationParameterType::Type(_) => quote! { - Self(self.0.json_body_obj(value)) + OperationParameterKind::Body(body_content_type) => { + match typ { + OperationParameterType::Type(_) => quote! { + Self(self.0.json_body_obj(value)) - }, - OperationParameterType::RawBody => quote! { - Self(self.0.json_body(value)) - }, - }, + }, + OperationParameterType::RawBody => { + match body_content_type { + BodyContentType::OctetStream => quote! { + Self(self.0.json_body(value)) + }, + BodyContentType::Text(_) => quote! { + Self(self.0.body(value)) + }, + _ => unreachable!(), + } + } + } + } }; quote! { pub fn #name_ident(self, value: #arg_type_name) -> Self { diff --git a/progenitor-impl/src/method.rs b/progenitor-impl/src/method.rs index d6bd5d7..9b219e0 100644 --- a/progenitor-impl/src/method.rs +++ b/progenitor-impl/src/method.rs @@ -137,6 +137,7 @@ pub enum BodyContentType { OctetStream, Json, FormUrlencoded, + Text(String), } impl FromStr for BodyContentType { @@ -148,6 +149,9 @@ impl FromStr for BodyContentType { "application/octet-stream" => Ok(Self::OctetStream), "application/json" => Ok(Self::Json), "application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded), + "text/plain" | "text/x-markdown" => { + Ok(Self::Text(String::from(&s[..offset]))) + } _ => Err(Error::UnexpectedFormat(format!( "unexpected content type: {}", s @@ -156,6 +160,19 @@ impl FromStr for BodyContentType { } } +use std::fmt; + +impl fmt::Display for BodyContentType { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(match self { + Self::OctetStream => "application/octet-stream", + Self::Json => "application/json", + Self::FormUrlencoded => "application/x-www-form-urlencoded", + Self::Text(typ) => &typ, + }) + } +} + #[derive(Debug)] pub(crate) struct OperationResponse { pub status_code: OperationResponseStatus, @@ -601,7 +618,19 @@ impl Generator { quote! { Option<#t> } } (OperationParameterType::RawBody, false) => { - quote! { B } + match ¶m.kind { + OperationParameterKind::Body( + BodyContentType::OctetStream, + ) => { + quote! { B } + } + OperationParameterKind::Body( + BodyContentType::Text(_), + ) => { + quote! { String } + } + _ => unreachable!(), + } } (OperationParameterType::RawBody, true) => unreachable!(), }; @@ -611,10 +640,13 @@ impl Generator { }) .collect::>(); - let raw_body_param = method - .params - .iter() - .any(|param| param.typ == OperationParameterType::RawBody); + let raw_body_param = method.params.iter().any(|param| { + param.typ == OperationParameterType::RawBody + && param.kind + == OperationParameterKind::Body( + BodyContentType::OctetStream, + ) + }); let bounds = if raw_body_param { quote! { <'a, B: Into > } @@ -935,6 +967,18 @@ impl Generator { ) .body(body) }), + ( + OperationParameterKind::Body(BodyContentType::Text(mime_type)), + OperationParameterType::RawBody, + ) => Some(quote! { + // Set the content type (this is handled by helper + // functions for other MIME types). + .header( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static(#mime_type), + ) + .body(body) + }), ( OperationParameterKind::Body(BodyContentType::Json), OperationParameterType::Type(_), @@ -1627,21 +1671,42 @@ impl Generator { } } - OperationParameterType::RawBody => { - let err_msg = format!( - "conversion to `reqwest::Body` for {} failed", - param.name, - ); + 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 - } - }) + 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!(), } } }) @@ -2105,6 +2170,41 @@ impl Generator { }?; OperationParameterType::RawBody } + BodyContentType::Text(_) => { + // For a plain text body, we expect a simple, specific schema: + // "schema": { + // "type": "string", + // } + match schema.item(components)? { + openapiv3::Schema { + schema_data: + openapiv3::SchemaData { + nullable: false, + discriminator: None, + default: None, + // Other fields that describe or document the + // schema are fine. + .. + }, + schema_kind: + openapiv3::SchemaKind::Type(openapiv3::Type::String( + openapiv3::StringType { + format: + openapiv3::VariantOrUnknownOrEmpty::Empty, + pattern: None, + enumeration, + min_length: None, + max_length: None, + }, + )), + } if enumeration.is_empty() => Ok(()), + _ => Err(Error::UnexpectedFormat(format!( + "invalid schema for {}: {:?}", + content_type, schema + ))), + }?; + OperationParameterType::RawBody + } BodyContentType::Json | BodyContentType::FormUrlencoded => { // TODO it would be legal to have the encoding field set for // application/x-www-form-urlencoded content, but I'm not sure diff --git a/progenitor-impl/tests/output/buildomat-builder-tagged.out b/progenitor-impl/tests/output/buildomat-builder-tagged.out index 942e7c4..51ad2a0 100644 --- a/progenitor-impl/tests/output/buildomat-builder-tagged.out +++ b/progenitor-impl/tests/output/buildomat-builder-tagged.out @@ -1754,6 +1754,18 @@ impl Client { builder::Whoami::new(self) } + ///Sends a `PUT` request to `/v1/whoami/name` + /// + ///```ignore + /// let response = client.whoami_put_name() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn whoami_put_name(&self) -> builder::WhoamiPutName { + builder::WhoamiPutName::new(self) + } + ///Sends a `POST` request to `/v1/worker/bootstrap` /// ///```ignore @@ -2355,6 +2367,57 @@ pub mod builder { } } + ///Builder for [`Client::whoami_put_name`] + /// + ///[`Client::whoami_put_name`]: super::Client::whoami_put_name + #[derive(Debug)] + pub struct WhoamiPutName<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> WhoamiPutName<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + body: Err("body was not initialized".to_string()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.body = value + .try_into() + .map_err(|_| "conversion to `String` for body failed".to_string()) + .map(|v| v.into()); + self + } + + ///Sends a `PUT` request to `/v1/whoami/name` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body.map_err(Error::InvalidRequest)?; + let url = format!("{}/v1/whoami/name", client.baseurl,); + let request = client + .client + .put(url) + .header( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("text/plain"), + ) + .body(body) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + ///Builder for [`Client::worker_bootstrap`] /// ///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap diff --git a/progenitor-impl/tests/output/buildomat-builder.out b/progenitor-impl/tests/output/buildomat-builder.out index bfae368..822c0f3 100644 --- a/progenitor-impl/tests/output/buildomat-builder.out +++ b/progenitor-impl/tests/output/buildomat-builder.out @@ -1754,6 +1754,18 @@ impl Client { builder::Whoami::new(self) } + ///Sends a `PUT` request to `/v1/whoami/name` + /// + ///```ignore + /// let response = client.whoami_put_name() + /// .body(body) + /// .send() + /// .await; + /// ``` + pub fn whoami_put_name(&self) -> builder::WhoamiPutName { + builder::WhoamiPutName::new(self) + } + ///Sends a `POST` request to `/v1/worker/bootstrap` /// ///```ignore @@ -2355,6 +2367,57 @@ pub mod builder { } } + ///Builder for [`Client::whoami_put_name`] + /// + ///[`Client::whoami_put_name`]: super::Client::whoami_put_name + #[derive(Debug)] + pub struct WhoamiPutName<'a> { + client: &'a super::Client, + body: Result, + } + + impl<'a> WhoamiPutName<'a> { + pub fn new(client: &'a super::Client) -> Self { + Self { + client, + body: Err("body was not initialized".to_string()), + } + } + + pub fn body(mut self, value: V) -> Self + where + V: std::convert::TryInto, + { + self.body = value + .try_into() + .map_err(|_| "conversion to `String` for body failed".to_string()) + .map(|v| v.into()); + self + } + + ///Sends a `PUT` request to `/v1/whoami/name` + pub async fn send(self) -> Result, Error<()>> { + let Self { client, body } = self; + let body = body.map_err(Error::InvalidRequest)?; + let url = format!("{}/v1/whoami/name", client.baseurl,); + let request = client + .client + .put(url) + .header( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("text/plain"), + ) + .body(body) + .build()?; + let result = client.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + } + ///Builder for [`Client::worker_bootstrap`] /// ///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap diff --git a/progenitor-impl/tests/output/buildomat-cli.out b/progenitor-impl/tests/output/buildomat-cli.out index 317eab7..02c3a6a 100644 --- a/progenitor-impl/tests/output/buildomat-cli.out +++ b/progenitor-impl/tests/output/buildomat-cli.out @@ -20,6 +20,7 @@ impl Cli { CliCommand::TaskOutputDownload => Self::cli_task_output_download(), CliCommand::UserCreate => Self::cli_user_create(), CliCommand::Whoami => Self::cli_whoami(), + CliCommand::WhoamiPutName => Self::cli_whoami_put_name(), CliCommand::WorkerBootstrap => Self::cli_worker_bootstrap(), CliCommand::WorkerPing => Self::cli_worker_ping(), CliCommand::WorkerTaskAppend => Self::cli_worker_task_append(), @@ -151,6 +152,10 @@ impl Cli { clap::Command::new("") } + pub fn cli_whoami_put_name() -> clap::Command { + clap::Command::new("") + } + pub fn cli_worker_bootstrap() -> clap::Command { clap::Command::new("") .arg( @@ -348,6 +353,9 @@ impl Cli { CliCommand::Whoami => { self.execute_whoami(matches).await; } + CliCommand::WhoamiPutName => { + self.execute_whoami_put_name(matches).await; + } CliCommand::WorkerBootstrap => { self.execute_worker_bootstrap(matches).await; } @@ -577,6 +585,22 @@ impl Cli { } } + pub async fn execute_whoami_put_name(&self, matches: &clap::ArgMatches) { + let mut request = self.client.whoami_put_name(); + self.over + .execute_whoami_put_name(matches, &mut request) + .unwrap(); + let result = request.send().await; + match result { + Ok(r) => { + println!("success\n{:#?}", r) + } + Err(r) => { + println!("success\n{:#?}", r) + } + } + } + pub async fn execute_worker_bootstrap(&self, matches: &clap::ArgMatches) { let mut request = self.client.worker_bootstrap(); if let Some(value) = matches.get_one::("bootstrap") { @@ -859,6 +883,14 @@ pub trait CliOverride { Ok(()) } + fn execute_whoami_put_name( + &self, + matches: &clap::ArgMatches, + request: &mut builder::WhoamiPutName, + ) -> Result<(), String> { + Ok(()) + } + fn execute_worker_bootstrap( &self, matches: &clap::ArgMatches, @@ -938,6 +970,7 @@ pub enum CliCommand { TaskOutputDownload, UserCreate, Whoami, + WhoamiPutName, WorkerBootstrap, WorkerPing, WorkerTaskAppend, @@ -961,6 +994,7 @@ impl CliCommand { CliCommand::TaskOutputDownload, CliCommand::UserCreate, CliCommand::Whoami, + CliCommand::WhoamiPutName, CliCommand::WorkerBootstrap, CliCommand::WorkerPing, CliCommand::WorkerTaskAppend, diff --git a/progenitor-impl/tests/output/buildomat-httpmock.out b/progenitor-impl/tests/output/buildomat-httpmock.out index a4f2270..1d25fd9 100644 --- a/progenitor-impl/tests/output/buildomat-httpmock.out +++ b/progenitor-impl/tests/output/buildomat-httpmock.out @@ -403,6 +403,40 @@ pub mod operations { } } + pub struct WhoamiPutNameWhen(httpmock::When); + impl WhoamiPutNameWhen { + pub fn new(inner: httpmock::When) -> Self { + Self( + inner + .method(httpmock::Method::PUT) + .path_matches(regex::Regex::new("^/v1/whoami/name$").unwrap()), + ) + } + + pub fn into_inner(self) -> httpmock::When { + self.0 + } + + pub fn body(self, value: String) -> Self { + Self(self.0.body(value)) + } + } + + pub struct WhoamiPutNameThen(httpmock::Then); + impl WhoamiPutNameThen { + pub fn new(inner: httpmock::Then) -> Self { + Self(inner) + } + + pub fn into_inner(self) -> httpmock::Then { + self.0 + } + + pub fn ok(self) -> Self { + Self(self.0.status(200u16)) + } + } + pub struct WorkerBootstrapWhen(httpmock::When); impl WorkerBootstrapWhen { pub fn new(inner: httpmock::When) -> Self { @@ -743,6 +777,9 @@ pub trait MockServerExt { fn whoami(&self, config_fn: F) -> httpmock::Mock where F: FnOnce(operations::WhoamiWhen, operations::WhoamiThen); + fn whoami_put_name(&self, config_fn: F) -> httpmock::Mock + where + F: FnOnce(operations::WhoamiPutNameWhen, operations::WhoamiPutNameThen); fn worker_bootstrap(&self, config_fn: F) -> httpmock::Mock where F: FnOnce(operations::WorkerBootstrapWhen, operations::WorkerBootstrapThen); @@ -890,6 +927,18 @@ impl MockServerExt for httpmock::MockServer { }) } + fn whoami_put_name(&self, config_fn: F) -> httpmock::Mock + where + F: FnOnce(operations::WhoamiPutNameWhen, operations::WhoamiPutNameThen), + { + self.mock(|when, then| { + config_fn( + operations::WhoamiPutNameWhen::new(when), + operations::WhoamiPutNameThen::new(then), + ) + }) + } + fn worker_bootstrap(&self, config_fn: F) -> httpmock::Mock where F: FnOnce(operations::WorkerBootstrapWhen, operations::WorkerBootstrapThen), diff --git a/progenitor-impl/tests/output/buildomat-positional.out b/progenitor-impl/tests/output/buildomat-positional.out index 23d801b..1713415 100644 --- a/progenitor-impl/tests/output/buildomat-positional.out +++ b/progenitor-impl/tests/output/buildomat-positional.out @@ -532,6 +532,29 @@ impl Client { } } + ///Sends a `PUT` request to `/v1/whoami/name` + pub async fn whoami_put_name<'a>( + &'a self, + body: String, + ) -> Result, Error<()>> { + let url = format!("{}/v1/whoami/name", self.baseurl,); + let request = self + .client + .put(url) + .header( + reqwest::header::CONTENT_TYPE, + reqwest::header::HeaderValue::from_static("text/plain"), + ) + .body(body) + .build()?; + let result = self.client.execute(request).await; + let response = result?; + match response.status().as_u16() { + 200u16 => Ok(ResponseValue::empty(response)), + _ => Err(Error::UnexpectedResponse(response)), + } + } + ///Sends a `POST` request to `/v1/worker/bootstrap` pub async fn worker_bootstrap<'a>( &'a self, diff --git a/sample_openapi/buildomat.json b/sample_openapi/buildomat.json index 5681dd7..0803561 100644 --- a/sample_openapi/buildomat.json +++ b/sample_openapi/buildomat.json @@ -252,6 +252,25 @@ } } }, + "/v1/whoami/name": { + "put": { + "operationId": "whoami_put_name", + "requestBody": { + "content": { + "text/plain": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation" + } + } + } + }, "/v1/worker/bootstrap": { "post": { "operationId": "worker_bootstrap",