use proc_macro2::TokenStream;
use quote::{format_ident, quote};
use super::{doc_tokens, extract_type_ident, format_tokens};
use crate::Result;
use crate::codegen::{MethodHandler, Runtime, ServiceHandler};
use crate::parsing::MessageField;
use crate::parsing::types::BaseType;
pub(crate) fn generate(service: &ServiceHandler<'_>) -> Result<String> {
let mut method_tokens = Vec::new();
for method in service.rest_methods() {
let full_name = format!(
"{}.{}.{}",
service.plan.package, service.plan.service_name, method.plan.metadata.method_name
);
let options = &service.config.rest_client;
let form_body = options.form_encoded_methods.contains(&full_name);
let path_fields_in_body = options.path_fields_in_body_methods.contains(&full_name);
let method_code = client_method(method, form_body, path_fields_in_body)?;
method_tokens.push(method_code);
}
let client_ident = service.low_level_client_type(crate::codegen::ClientProtocol::Rest);
let mod_path = service.models_path();
let foreign_models = service.foreign_models_imports();
let result_path = service.config.client_result_path();
let (transport_import, transport_ident) = transport_tokens(service.config);
let encode_import = service
.rest_methods()
.any(|m| m.plan.formats_path_values())
.then(|| {
if service.config.client_protocols.is_multi() {
quote! { use super::super::super::encode_path_value; }
} else {
quote! { use super::super::encode_path_value; }
}
});
let tokens = quote! {
#![allow(unused_imports)]
#transport_import
use url::Url;
use #result_path;
use #mod_path::*;
#(#foreign_models)*
#encode_import
#[derive(Clone)]
pub struct #client_ident {
pub(crate) client: #transport_ident,
pub(crate) base_url: Url,
}
impl #client_ident {
pub fn new(client: #transport_ident, mut base_url: Url) -> Self {
if !base_url.path().ends_with('/') {
base_url.set_path(&format!("{}/", base_url.path()));
}
Self { client, base_url }
}
#(#method_tokens)*
}
};
format_tokens(tokens)
}
pub(crate) fn transport_tokens(
config: &crate::codegen::CodeGenConfig,
) -> (TokenStream, proc_macro2::Ident) {
if config.dual_transport() {
let import = quote! {
#[cfg(not(target_arch = "wasm32"))]
use ::olai_http::CloudClient as Transport;
#[cfg(target_arch = "wasm32")]
use ::olai_http_wasm::WasmClient as Transport;
};
(import, format_ident!("Transport"))
} else {
let transport_path: syn::Path =
syn::parse_str(&config.transport_type_path).expect("valid transport_type_path");
let ident = transport_path
.segments
.last()
.expect("transport_type_path has at least one segment")
.ident
.clone();
(quote! { use #transport_path; }, ident)
}
}
pub fn client_method(
method: MethodHandler<'_>,
form_body: bool,
path_fields_in_body: bool,
) -> Result<TokenStream> {
let doc_attrs = doc_tokens(method.plan.metadata.documentation.as_deref());
let method_name = method.plan.base_method_ident();
let input_type_ident = method.input_type();
let http_method = format_ident!("{}", method.plan.http_method.to_lowercase());
let has_query_params = method.plan.url_query_parameters().next().is_some();
let url_formatting = generate_url_formatting(&method, has_query_params);
let query_handling = generate_query_parameters(&method);
let error_module = method.config.client_error_module();
let result_type = method.result_type();
let request_ident = if method.plan.formats_path_values()
|| has_query_params
|| method.plan.has_request_body
|| !method.plan.request_headers.is_empty()
{
format_ident!("request")
} else {
format_ident!("_request")
};
let path_fields: Vec<_> = if method.plan.has_request_body
&& method.plan.formats_path_values()
&& !path_fields_in_body
{
method
.plan
.path_parameters()
.map(|param| crate::utils::field_ident(¶m.name))
.collect()
} else {
Vec::new()
};
let raw_body = method
.plan
.raw_request_body
.as_deref()
.map(crate::utils::field_ident);
let (body_copy, body) = if path_fields.is_empty() || raw_body.is_some() {
(quote! {}, quote! { request })
} else {
let copy = quote! {
let mut body = request.clone();
#(body.#path_fields = Default::default();)*
};
(copy, quote! { &body })
};
let body_handling = match (raw_body, method.plan.has_request_body, form_body) {
(Some(field), _, _) => quote! {
.header("content-type", "application/octet-stream")
.body(request.#field.clone())
},
(None, true, false) => quote! { .json(#body) },
(None, true, true) => quote! { .form(#body) },
(None, false, _) => quote! {},
};
let send = if method.plan.request_headers.is_empty() {
quote! { let response = self.client.#http_method(url)#body_handling.send().await?; }
} else {
let headers = method.plan.request_headers.iter().map(|(field, header)| {
let field_ident = crate::utils::field_ident(field);
match request_header_is_optional(&method, field) {
true => quote! {
if let Some(ref value) = request.#field_ident {
builder = builder.header(#header, value.as_str());
}
},
false => {
quote! { builder = builder.header(#header, request.#field_ident.as_str()); }
}
}
});
quote! {
let mut builder = self.client.#http_method(url)#body_handling;
#(#headers)*
let response = builder.send().await?;
}
};
let read_response = match method.output_type() {
Some(output_type) => read_output(&method, &output_type)?,
None if method.is_existence_check() => quote! { Ok(true) },
None => quote! { Ok(()) },
};
let not_found = method.is_existence_check().then(|| {
quote! {
if response.status().as_u16() == 404 {
return Ok(false);
}
}
});
Ok(quote! {
#doc_attrs
pub async fn #method_name(&self, #request_ident: &#input_type_ident) -> Result<#result_type> {
#url_formatting
#query_handling
#body_copy
#send
#not_found
if !response.status().is_success() {
return Err(#error_module::parse_error_response(response).await);
}
#read_response
}
})
}
fn request_header_is_optional(method: &MethodHandler<'_>, field: &str) -> bool {
method
.plan
.query_parameters()
.any(|param| param.name == field && param.field_type.is_optional)
}
fn read_output(
method: &MethodHandler<'_>,
output_type: &proc_macro2::Ident,
) -> Result<TokenStream> {
let lenient = method.config.rest_client.ignore_unknown_enum_values
&& method.config.runtime == Runtime::Buffa;
let options = lenient.then(|| {
quote! { let options = ::buffa::json::JsonParseOptions::new().ignore_unknown_enum_values(true); }
});
let from_slice = |bytes: TokenStream| {
if lenient {
quote! { ::buffa::json::with_json_parse_options(&options, || serde_json::from_slice(#bytes)) }
} else {
quote! { serde_json::from_slice(#bytes) }
}
};
let header_fields = method.response_header_fields();
if method.plan.metadata.server_streaming {
if !header_fields.is_empty() {
return Err(crate::Error::Build(format!(
"method `{}` streams its output, so its output fields cannot come from response headers",
method.plan.handler_function_name
)));
}
let parse_line = from_slice(quote! { line });
return Ok(quote! {
let result = response.bytes().await?;
#options
result
.split(|byte| *byte == b'\n')
.filter(|line| !line.trim_ascii().is_empty())
.map(|line| -> Result<#output_type> { Ok(#parse_line?) })
.collect()
});
}
let assignments = header_fields
.iter()
.map(|field| header_assignment(method, field))
.collect::<Result<Vec<_>>>()?;
if let Some(field) = &method.plan.raw_response_body {
let field_ident = crate::utils::field_ident(field);
if assignments.is_empty() {
return Ok(quote! {
Ok(#output_type {
#field_ident: response.bytes().await?.into(),
..::core::default::Default::default()
})
});
}
return Ok(quote! {
let headers = response.headers().clone();
let mut output = #output_type {
#field_ident: response.bytes().await?.into(),
..::core::default::Default::default()
};
#(#assignments)*
Ok(output)
});
}
let parse = from_slice(quote! { &result });
if header_fields.is_empty() {
return Ok(quote! {
let result = response.bytes().await?;
#options
Ok(#parse?)
});
}
Ok(quote! {
let headers = response.headers().clone();
let result = response.bytes().await?;
#options
let mut output: #output_type = if result.trim_ascii().is_empty() {
Default::default()
} else {
#parse?
};
#(#assignments)*
Ok(output)
})
}
fn header_assignment(method: &MethodHandler<'_>, field: &MessageField) -> Result<TokenStream> {
let field_ident = crate::utils::field_ident(&field.name);
let header = field.response_header.as_deref().unwrap_or_default();
let ty = &field.unified_type;
let is_string = match ty.base_type {
BaseType::String => true,
BaseType::Int32
| BaseType::Int64
| BaseType::Bool
| BaseType::Float32
| BaseType::Float64 => false,
_ => {
return Err(crate::Error::Build(format!(
"field `{}` of the output of `{}` comes from a response header, so it must be a string, number or bool",
field.name, method.plan.handler_function_name
)));
}
};
if ty.is_repeated {
return Err(crate::Error::Build(format!(
"field `{}` of the output of `{}` comes from a response header, so it cannot be repeated",
field.name, method.plan.handler_function_name
)));
}
let value = quote! { headers.get(#header).and_then(|value| value.to_str().ok()) };
Ok(match (is_string, ty.is_optional) {
(true, true) => quote! { output.#field_ident = #value.map(str::to_string); },
(true, false) => quote! {
if let Some(value) = #value {
output.#field_ident = value.to_string();
}
},
(false, true) => {
quote! { output.#field_ident = #value.and_then(|value| value.parse().ok()); }
}
(false, false) => quote! {
if let Some(value) = #value.and_then(|value| value.parse().ok()) {
output.#field_ident = value;
}
},
})
}
fn generate_url_formatting(
method: &MethodHandler<'_>,
needs_mut: bool,
) -> proc_macro2::TokenStream {
let path = method.plan.http_pattern.base_path();
let path = path.trim_start_matches('/');
let mut_kw = if needs_mut {
quote! { mut }
} else {
quote! {}
};
if !method.plan.formats_path_values() {
quote! {
let #mut_kw url = self.base_url.join(#path)?;
}
} else {
let (format_string, format_args) = method.plan.http_pattern.to_format_string();
let values: Vec<TokenStream> = format_args
.iter()
.map(|template_param| {
let field_ident = crate::utils::field_ident(template_param);
let keep_slash = method.plan.http_pattern.is_multi_segment(template_param);
quote! { encode_path_value(&request.#field_ident, #keep_slash) }
})
.collect();
quote! {
let formatted_path = format!(#format_string, #(#values),*);
let #mut_kw url = self.base_url.join(&formatted_path)?;
}
}
}
pub(crate) fn encode_path_value_fn() -> TokenStream {
quote! {
fn encode_path_value(value: &impl ::std::fmt::Display, keep_slash: bool) -> String {
use ::std::fmt::Write as _;
let value = value.to_string();
let value = match keep_slash {
true => value.trim_start_matches('/'),
false => value.as_str(),
};
let mut out = String::with_capacity(value.len());
for byte in value.bytes() {
match byte {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' => {
out.push(byte as char)
}
b'/' if keep_slash => out.push('/'),
_ => {
let _ = write!(out, "%{byte:02X}");
}
}
}
out
}
}
}
fn generate_query_parameters(method: &MethodHandler<'_>) -> proc_macro2::TokenStream {
let param_assignments = method.plan.url_query_parameters().map(|param| {
let field_ident = crate::utils::field_ident(¶m.name);
let param_name = method.query_key(param);
let field_type = ¶m.field_type;
let append_known = quote! {
use buffa::Enumeration as _;
url.query_pairs_mut().append_pair(#param_name, known.proto_name());
};
if field_type.is_repeated {
if let BaseType::Enum(enum_name) = &field_type.base_type {
let enum_type_ident = extract_type_ident(enum_name);
match method.config.runtime {
Runtime::Prost => quote! {
for &raw in &request.#field_ident {
if let Some(v) = #enum_type_ident::from_i32(raw) {
url.query_pairs_mut().append_pair(#param_name, v.as_str_name());
}
}
},
Runtime::Buffa => quote! {
for v in &request.#field_ident {
if let Some(known) = v.as_known() {
#append_known
}
}
},
}
} else {
quote! {
for value in &request.#field_ident {
url.query_pairs_mut().append_pair(#param_name, &value.to_string());
}
}
}
} else if matches!(method.config.runtime, Runtime::Buffa)
&& matches!(field_type.base_type, BaseType::Enum(_))
{
let known = if param.is_optional() {
quote! { request.#field_ident.and_then(|v| v.as_known()) }
} else {
quote! { request.#field_ident.as_known() }
};
quote! {
if let Some(known) = #known {
#append_known
}
}
} else if param.is_optional() {
quote! {
if let Some(ref value) = request.#field_ident {
url.query_pairs_mut().append_pair(#param_name, &value.to_string());
}
}
} else if matches!(field_type.base_type, BaseType::String) {
quote! {
url.query_pairs_mut().append_pair(#param_name, &request.#field_ident);
}
} else {
quote! {
url.query_pairs_mut().append_pair(#param_name, &request.#field_ident.to_string());
}
}
});
quote! {
#(#param_assignments)*
}
}