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::{
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 {

View File

@ -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 &param.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::<Vec<_>>();
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<reqwest::Body> > }
@ -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<B>(mut self, value: B) -> Self
where B: std::convert::TryInto<reqwest::Body>
{
self.#param_name = value.try_into()
.map_err(|_| #err_msg.to_string());
self
}
})
Ok(quote! {
pub fn #param_name<B>(mut self, value: B) -> Self
where B: std::convert::TryInto<reqwest::Body>
{
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<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
}
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

View File

@ -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<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`]
///
///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap

View File

@ -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<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`]
///
///[`Client::worker_bootstrap`]: super::Client::worker_bootstrap

View File

@ -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<T: CliOverride> Cli<T> {
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<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) {
let mut request = self.client.worker_bootstrap();
if let Some(value) = matches.get_one::<String>("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,

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);
impl WorkerBootstrapWhen {
pub fn new(inner: httpmock::When) -> Self {
@ -743,6 +777,9 @@ pub trait MockServerExt {
fn whoami<F>(&self, config_fn: F) -> httpmock::Mock
where
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
where
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
where
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`
pub async fn worker_bootstrap<'a>(
&'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": {
"post": {
"operationId": "worker_bootstrap",