Add support for text/plain and text/x-markdown body content types (#593)

This commit is contained in:
Sophie Tauchert 2023-10-16 20:47:50 +02:00 committed by GitHub
parent 6dc2a0ff2d
commit a1de78dbeb
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 403 additions and 31 deletions

View File

@ -8,8 +8,9 @@ use quote::{format_ident, quote, ToTokens};
use crate::{ use crate::{
method::{ method::{
HttpMethod, OperationParameter, OperationParameterKind, BodyContentType, HttpMethod, OperationParameter,
OperationParameterType, OperationResponse, OperationResponseStatus, OperationParameterKind, OperationParameterType, OperationResponse,
OperationResponseStatus,
}, },
to_schema::ToSchema, to_schema::ToSchema,
util::{sanitize, Case}, util::{sanitize, Case},
@ -165,8 +166,18 @@ impl Generator {
.get_type(arg_type_id) .get_type(arg_type_id)
.unwrap() .unwrap()
.parameter_ident(), .parameter_ident(),
OperationParameterType::RawBody => quote! { OperationParameterType::RawBody => match kind {
serde_json::Value 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::Header(_) => quote! { todo!() },
OperationParameterKind::Body(_) => match typ { OperationParameterKind::Body(body_content_type) => {
OperationParameterType::Type(_) => quote! { match typ {
Self(self.0.json_body_obj(value)) OperationParameterType::Type(_) => quote! {
Self(self.0.json_body_obj(value))
}, },
OperationParameterType::RawBody => quote! { OperationParameterType::RawBody => {
Self(self.0.json_body(value)) match body_content_type {
}, BodyContentType::OctetStream => quote! {
}, Self(self.0.json_body(value))
},
BodyContentType::Text(_) => quote! {
Self(self.0.body(value))
},
_ => unreachable!(),
}
}
}
}
}; };
quote! { quote! {
pub fn #name_ident(self, value: #arg_type_name) -> Self { pub fn #name_ident(self, value: #arg_type_name) -> Self {

View File

@ -137,6 +137,7 @@ pub enum BodyContentType {
OctetStream, OctetStream,
Json, Json,
FormUrlencoded, FormUrlencoded,
Text(String),
} }
impl FromStr for BodyContentType { impl FromStr for BodyContentType {
@ -148,6 +149,9 @@ impl FromStr for BodyContentType {
"application/octet-stream" => Ok(Self::OctetStream), "application/octet-stream" => Ok(Self::OctetStream),
"application/json" => Ok(Self::Json), "application/json" => Ok(Self::Json),
"application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded), "application/x-www-form-urlencoded" => Ok(Self::FormUrlencoded),
"text/plain" | "text/x-markdown" => {
Ok(Self::Text(String::from(&s[..offset])))
}
_ => Err(Error::UnexpectedFormat(format!( _ => Err(Error::UnexpectedFormat(format!(
"unexpected content type: {}", "unexpected content type: {}",
s 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)] #[derive(Debug)]
pub(crate) struct OperationResponse { pub(crate) struct OperationResponse {
pub status_code: OperationResponseStatus, pub status_code: OperationResponseStatus,
@ -601,7 +618,19 @@ impl Generator {
quote! { Option<#t> } quote! { Option<#t> }
} }
(OperationParameterType::RawBody, false) => { (OperationParameterType::RawBody, false) => {
quote! { B } match &param.kind {
OperationParameterKind::Body(
BodyContentType::OctetStream,
) => {
quote! { B }
}
OperationParameterKind::Body(
BodyContentType::Text(_),
) => {
quote! { String }
}
_ => unreachable!(),
}
} }
(OperationParameterType::RawBody, true) => unreachable!(), (OperationParameterType::RawBody, true) => unreachable!(),
}; };
@ -611,10 +640,13 @@ impl Generator {
}) })
.collect::<Vec<_>>(); .collect::<Vec<_>>();
let raw_body_param = method let raw_body_param = method.params.iter().any(|param| {
.params param.typ == OperationParameterType::RawBody
.iter() && param.kind
.any(|param| param.typ == OperationParameterType::RawBody); == OperationParameterKind::Body(
BodyContentType::OctetStream,
)
});
let bounds = if raw_body_param { let bounds = if raw_body_param {
quote! { <'a, B: Into<reqwest::Body> > } quote! { <'a, B: Into<reqwest::Body> > }
@ -935,6 +967,18 @@ impl Generator {
) )
.body(body) .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), OperationParameterKind::Body(BodyContentType::Json),
OperationParameterType::Type(_), OperationParameterType::Type(_),
@ -1627,21 +1671,42 @@ impl Generator {
} }
} }
OperationParameterType::RawBody => { OperationParameterType::RawBody => match param.kind {
let err_msg = format!( OperationParameterKind::Body(BodyContentType::OctetStream) => {
"conversion to `reqwest::Body` for {} failed", let err_msg = format!(
param.name, "conversion to `reqwest::Body` for {} failed",
); param.name,
);
Ok(quote! { Ok(quote! {
pub fn #param_name<B>(mut self, value: B) -> Self pub fn #param_name<B>(mut self, value: B) -> Self
where B: std::convert::TryInto<reqwest::Body> where B: std::convert::TryInto<reqwest::Body>
{ {
self.#param_name = value.try_into() self.#param_name = value.try_into()
.map_err(|_| #err_msg.to_string()); .map_err(|_| #err_msg.to_string());
self self
} }
}) })
},
OperationParameterKind::Body(BodyContentType::Text(_)) => {
let err_msg = format!(
"conversion to `String` for {} failed",
param.name,
);
Ok(quote! {
pub fn #param_name<V>(mut self, value: V) -> Self
where V: std::convert::TryInto<String>
{
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 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 => { BodyContentType::Json | BodyContentType::FormUrlencoded => {
// TODO it would be legal to have the encoding field set for // TODO it would be legal to have the encoding field set for
// application/x-www-form-urlencoded content, but I'm not sure // application/x-www-form-urlencoded content, but I'm not sure

View File

@ -1754,6 +1754,18 @@ impl Client {
builder::Whoami::new(self) 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` ///Sends a `POST` request to `/v1/worker/bootstrap`
/// ///
///```ignore ///```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<reqwest::Body, String>,
}
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<V>(mut self, value: V) -> Self
where
V: std::convert::TryInto<String>,
{
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<ResponseValue<()>, 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`] ///Builder for [`Client::worker_bootstrap`]
/// ///
///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap ///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap

View File

@ -1754,6 +1754,18 @@ impl Client {
builder::Whoami::new(self) 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` ///Sends a `POST` request to `/v1/worker/bootstrap`
/// ///
///```ignore ///```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<reqwest::Body, String>,
}
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<V>(mut self, value: V) -> Self
where
V: std::convert::TryInto<String>,
{
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<ResponseValue<()>, 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`] ///Builder for [`Client::worker_bootstrap`]
/// ///
///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap ///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap

View File

@ -20,6 +20,7 @@ impl Cli {
CliCommand::TaskOutputDownload => Self::cli_task_output_download(), CliCommand::TaskOutputDownload => Self::cli_task_output_download(),
CliCommand::UserCreate => Self::cli_user_create(), CliCommand::UserCreate => Self::cli_user_create(),
CliCommand::Whoami => Self::cli_whoami(), CliCommand::Whoami => Self::cli_whoami(),
CliCommand::WhoamiPutName => Self::cli_whoami_put_name(),
CliCommand::WorkerBootstrap => Self::cli_worker_bootstrap(), CliCommand::WorkerBootstrap => Self::cli_worker_bootstrap(),
CliCommand::WorkerPing => Self::cli_worker_ping(), CliCommand::WorkerPing => Self::cli_worker_ping(),
CliCommand::WorkerTaskAppend => Self::cli_worker_task_append(), CliCommand::WorkerTaskAppend => Self::cli_worker_task_append(),
@ -151,6 +152,10 @@ impl Cli {
clap::Command::new("") clap::Command::new("")
} }
pub fn cli_whoami_put_name() -> clap::Command {
clap::Command::new("")
}
pub fn cli_worker_bootstrap() -> clap::Command { pub fn cli_worker_bootstrap() -> clap::Command {
clap::Command::new("") clap::Command::new("")
.arg( .arg(
@ -348,6 +353,9 @@ impl<T: CliOverride> Cli<T> {
CliCommand::Whoami => { CliCommand::Whoami => {
self.execute_whoami(matches).await; self.execute_whoami(matches).await;
} }
CliCommand::WhoamiPutName => {
self.execute_whoami_put_name(matches).await;
}
CliCommand::WorkerBootstrap => { CliCommand::WorkerBootstrap => {
self.execute_worker_bootstrap(matches).await; self.execute_worker_bootstrap(matches).await;
} }
@ -577,6 +585,22 @@ impl<T: CliOverride> Cli<T> {
} }
} }
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) { pub async fn execute_worker_bootstrap(&self, matches: &clap::ArgMatches) {
let mut request = self.client.worker_bootstrap(); let mut request = self.client.worker_bootstrap();
if let Some(value) = matches.get_one::<String>("bootstrap") { if let Some(value) = matches.get_one::<String>("bootstrap") {
@ -859,6 +883,14 @@ pub trait CliOverride {
Ok(()) Ok(())
} }
fn execute_whoami_put_name(
&self,
matches: &clap::ArgMatches,
request: &mut builder::WhoamiPutName,
) -> Result<(), String> {
Ok(())
}
fn execute_worker_bootstrap( fn execute_worker_bootstrap(
&self, &self,
matches: &clap::ArgMatches, matches: &clap::ArgMatches,
@ -938,6 +970,7 @@ pub enum CliCommand {
TaskOutputDownload, TaskOutputDownload,
UserCreate, UserCreate,
Whoami, Whoami,
WhoamiPutName,
WorkerBootstrap, WorkerBootstrap,
WorkerPing, WorkerPing,
WorkerTaskAppend, WorkerTaskAppend,
@ -961,6 +994,7 @@ impl CliCommand {
CliCommand::TaskOutputDownload, CliCommand::TaskOutputDownload,
CliCommand::UserCreate, CliCommand::UserCreate,
CliCommand::Whoami, CliCommand::Whoami,
CliCommand::WhoamiPutName,
CliCommand::WorkerBootstrap, CliCommand::WorkerBootstrap,
CliCommand::WorkerPing, CliCommand::WorkerPing,
CliCommand::WorkerTaskAppend, CliCommand::WorkerTaskAppend,

View File

@ -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); pub struct WorkerBootstrapWhen(httpmock::When);
impl WorkerBootstrapWhen { impl WorkerBootstrapWhen {
pub fn new(inner: httpmock::When) -> Self { pub fn new(inner: httpmock::When) -> Self {
@ -743,6 +777,9 @@ pub trait MockServerExt {
fn whoami<F>(&self, config_fn: F) -> httpmock::Mock fn whoami<F>(&self, config_fn: F) -> httpmock::Mock
where where
F: FnOnce(operations::WhoamiWhen, operations::WhoamiThen); F: FnOnce(operations::WhoamiWhen, operations::WhoamiThen);
fn whoami_put_name<F>(&self, config_fn: F) -> httpmock::Mock
where
F: FnOnce(operations::WhoamiPutNameWhen, operations::WhoamiPutNameThen);
fn worker_bootstrap<F>(&self, config_fn: F) -> httpmock::Mock fn worker_bootstrap<F>(&self, config_fn: F) -> httpmock::Mock
where where
F: FnOnce(operations::WorkerBootstrapWhen, operations::WorkerBootstrapThen); F: FnOnce(operations::WorkerBootstrapWhen, operations::WorkerBootstrapThen);
@ -890,6 +927,18 @@ impl MockServerExt for httpmock::MockServer {
}) })
} }
fn whoami_put_name<F>(&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<F>(&self, config_fn: F) -> httpmock::Mock fn worker_bootstrap<F>(&self, config_fn: F) -> httpmock::Mock
where where
F: FnOnce(operations::WorkerBootstrapWhen, operations::WorkerBootstrapThen), F: FnOnce(operations::WorkerBootstrapWhen, operations::WorkerBootstrapThen),

View File

@ -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<ResponseValue<()>, 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` ///Sends a `POST` request to `/v1/worker/bootstrap`
pub async fn worker_bootstrap<'a>( pub async fn worker_bootstrap<'a>(
&'a self, &'a self,

View File

@ -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": { "/v1/worker/bootstrap": {
"post": { "post": {
"operationId": "worker_bootstrap", "operationId": "worker_bootstrap",