global: fixes

This commit is contained in:
nym21
2026-05-02 00:42:16 +02:00
parent 6f879a5551
commit 2b8a0a8cf7
99 changed files with 4308 additions and 1525 deletions

View File

@@ -14,141 +14,235 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
if !endpoint.should_generate() {
continue;
}
let method_name = endpoint_to_method_name(endpoint);
let base_return_type = if endpoint.returns_binary() {
"Uint8Array".to_string()
} else {
jsdoc_normalize(&normalize_return_type(
endpoint.schema_name().unwrap_or("*"),
))
};
let return_type = if endpoint.supports_csv {
format!("{} | string", base_return_type)
} else {
base_return_type
};
writeln!(output, " /**").unwrap();
if let Some(summary) = &endpoint.summary {
writeln!(output, " * {}", summary).unwrap();
}
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " *").unwrap();
write_description(output, desc, " * ", " *");
match endpoint.method.as_str() {
"GET" => generate_get_method(output, endpoint),
"POST" => generate_post_method(output, endpoint),
_ => continue,
}
}
}
// Add endpoint path
fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
for param in &endpoint.path_params {
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
}
for param in &endpoint.query_params {
let optional = if param.required { "" } else { "=" };
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(
output,
" * @param {{{}{}}} [{}]{}",
ty, optional, param.name, desc
)
.unwrap();
}
writeln!(
output,
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void }}}} [options]",
return_type
)
.unwrap();
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
writeln!(output, " */").unwrap();
let params = build_method_params(endpoint);
let params_with_opts = if params.is_empty() {
"{ signal, onValue } = {}".to_string()
} else {
format!("{}, {{ signal, onValue }} = {{}}", params)
};
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let fetch_call: String = if endpoint.returns_binary() {
"this.getBytes(path, { signal, onValue })".to_string()
} else if endpoint.returns_json() {
"this.getJson(path, { signal, onValue })".to_string()
} else if endpoint.response_kind.text_is_numeric() {
"Number(await this.getText(path, { signal, onValue }))".to_string()
} else {
"this.getText(path, { signal, onValue })".to_string()
};
write_path_assignment(output, endpoint, &path);
if endpoint.supports_csv {
writeln!(
output,
" if (format === 'csv') return this.getText(path, {{ signal, onValue }});"
)
.unwrap();
}
writeln!(output, " return {};", fetch_call).unwrap();
writeln!(output, " }}\n").unwrap();
}
fn generate_post_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
for param in &endpoint.path_params {
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
}
for param in &endpoint.query_params {
let optional = if param.required { "" } else { "=" };
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(
output,
" * @param {{{}{}}} [{}]{}",
ty, optional, param.name, desc
)
.unwrap();
}
if let Some(body) = &endpoint.request_body {
let optional = if body.required { "" } else { "=" };
let ty = jsdoc_normalize(&body.body_type);
writeln!(
output,
" * @param {{{}{}}} body - Request body",
ty, optional
)
.unwrap();
}
writeln!(
output,
" * @param {{{{ signal?: AbortSignal }}}} [options]"
)
.unwrap();
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
writeln!(output, " */").unwrap();
let mut params = build_method_params(endpoint);
if endpoint.request_body.is_some() {
if !params.is_empty() {
params.push_str(", ");
}
params.push_str("body");
}
let params_with_opts = if params.is_empty() {
"{ signal } = {}".to_string()
} else {
format!("{}, {{ signal }} = {{}}", params)
};
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let body_arg = if endpoint.request_body.is_some() {
"body"
} else {
"''"
};
let fetch_call: String = if endpoint.returns_binary() {
format!("this.postBytes(path, {}, {{ signal }})", body_arg)
} else if endpoint.returns_json() {
format!("this.postJson(path, {}, {{ signal }})", body_arg)
} else if endpoint.response_kind.text_is_numeric() {
format!(
"Number(await this.postText(path, {}, {{ signal }}))",
body_arg
)
} else {
format!("this.postText(path, {}, {{ signal }})", body_arg)
};
write_path_assignment(output, endpoint, &path);
writeln!(output, " return {};", fetch_call).unwrap();
writeln!(output, " }}\n").unwrap();
}
fn build_return_type(endpoint: &Endpoint) -> String {
let base = if endpoint.returns_binary() {
"Uint8Array".to_string()
} else {
jsdoc_normalize(&normalize_return_type(
endpoint.schema_name().unwrap_or("*"),
))
};
if endpoint.supports_csv {
format!("{} | string", base)
} else {
base
}
}
fn write_method_doc(output: &mut String, endpoint: &Endpoint) {
writeln!(output, " /**").unwrap();
if let Some(summary) = &endpoint.summary {
writeln!(output, " * {}", summary).unwrap();
}
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " *").unwrap();
writeln!(
output,
" * Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
write_description(output, desc, " * ", " *");
}
writeln!(output, " *").unwrap();
writeln!(
output,
" * Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() {
writeln!(output, " *").unwrap();
}
let has_body_param = endpoint.method == "POST" && endpoint.request_body.is_some();
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() || has_body_param {
writeln!(output, " *").unwrap();
}
}
for param in &endpoint.path_params {
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(output, " * @param {{{}}} {}{}", ty, param.name, desc).unwrap();
}
fn write_path_assignment(output: &mut String, endpoint: &Endpoint, path: &str) {
if endpoint.query_params.is_empty() {
writeln!(output, " const path = `{}`;", path).unwrap();
} else {
writeln!(output, " const params = new URLSearchParams();").unwrap();
for param in &endpoint.query_params {
let optional = if param.required { "" } else { "=" };
let desc = format_param_desc(param.description.as_deref());
let ty = jsdoc_normalize(&param.param_type);
writeln!(
output,
" * @param {{{}{}}} [{}]{}",
ty, optional, param.name, desc
)
.unwrap();
let ident = sanitize_ident(&param.name);
let is_array = param.param_type.ends_with("[]");
if is_array {
writeln!(
output,
" for (const _v of {}) params.append('{}', String(_v));",
ident, param.name
)
.unwrap();
} else if param.required {
writeln!(
output,
" params.set('{}', String({}));",
param.name, ident
)
.unwrap();
} else {
writeln!(
output,
" if ({} !== undefined) params.set('{}', String({}));",
ident, param.name, ident
)
.unwrap();
}
}
writeln!(output, " const query = params.toString();").unwrap();
writeln!(
output,
" * @param {{{{ signal?: AbortSignal, onValue?: (value: {}) => void }}}} [options]",
return_type
" const path = `{}${{query ? '?' + query : ''}}`;",
path
)
.unwrap();
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
writeln!(output, " */").unwrap();
let params = build_method_params(endpoint);
let params_with_opts = if params.is_empty() {
"{ signal, onValue } = {}".to_string()
} else {
format!("{}, {{ signal, onValue }} = {{}}", params)
};
writeln!(output, " async {}({}) {{", method_name, params_with_opts).unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let fetch_call: String = if endpoint.returns_binary() {
"this.getBytes(path, { signal, onValue })".to_string()
} else if endpoint.returns_json() {
"this.getJson(path, { signal, onValue })".to_string()
} else if endpoint.response_kind.text_is_numeric() {
"Number(await this.getText(path, { signal, onValue }))".to_string()
} else {
"this.getText(path, { signal, onValue })".to_string()
};
if endpoint.query_params.is_empty() {
writeln!(output, " const path = `{}`;", path).unwrap();
} else {
writeln!(output, " const params = new URLSearchParams();").unwrap();
for param in &endpoint.query_params {
let ident = sanitize_ident(&param.name);
let is_array = param.param_type.ends_with("[]");
if is_array {
writeln!(
output,
" for (const _v of {}) params.append('{}', String(_v));",
ident, param.name
)
.unwrap();
} else if param.required {
writeln!(
output,
" params.set('{}', String({}));",
param.name, ident
)
.unwrap();
} else {
writeln!(
output,
" if ({} !== undefined) params.set('{}', String({}));",
ident, param.name, ident
)
.unwrap();
}
}
writeln!(output, " const query = params.toString();").unwrap();
writeln!(
output,
" const path = `{}${{query ? '?' + query : ''}}`;",
path
)
.unwrap();
}
if endpoint.supports_csv {
writeln!(
output,
" if (format === 'csv') return this.getText(path, {{ signal, onValue }});"
)
.unwrap();
}
writeln!(output, " return {};", fetch_call).unwrap();
writeln!(output, " }}\n").unwrap();
}
}

View File

@@ -569,6 +569,67 @@ class BrkClientBase {{
return this._getCached(path, async (res) => new Uint8Array(await res.arrayBuffer()), options);
}}
/**
* Make a POST request with a string body.
*
* POST responses are uncached and never invoke `onValue` — every call hits
* the network with the same body and returns the upstream response.
*
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Response>}}
*/
async post(path, body, {{ signal }} = {{}}) {{
const url = `${{this.baseUrl}}${{path}}`;
const signals = [AbortSignal.timeout(this.timeout)];
if (signal) signals.push(signal);
const res = await fetch(url, {{
method: 'POST',
body,
signal: AbortSignal.any(signals),
}});
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}: ${{url}}`, res.status);
return res;
}}
/**
* Make a POST request expecting a JSON response.
* @template T
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<T>}}
*/
async postJson(path, body, options) {{
const res = await this.post(path, body, options);
return _addCamelGetters(await res.json());
}}
/**
* Make a POST request expecting a text response.
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<string>}}
*/
async postText(path, body, options) {{
const res = await this.post(path, body, options);
return res.text();
}}
/**
* Make a POST request expecting binary data (application/octet-stream).
* @param {{string}} path
* @param {{string}} body
* @param {{{{ signal?: AbortSignal }}}} [options]
* @returns {{Promise<Uint8Array>}}
*/
async postBytes(path, body, options) {{
const res = await this.post(path, body, options);
return new Uint8Array(await res.arrayBuffer());
}}
/**
* Fetch series data and wrap with helper methods (internal)
* @template T

View File

@@ -162,12 +162,20 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
// Build path
let path = build_path_template(&endpoint.path, &endpoint.path_params);
let fetch_method = if endpoint.returns_binary() {
"get"
} else if endpoint.returns_json() {
"get_json"
let is_post = endpoint.method == "POST";
let fetch_method = match (is_post, &endpoint.response_kind) {
(false, _) if endpoint.returns_binary() => "get",
(false, _) if endpoint.returns_json() => "get_json",
(false, _) => "get_text",
(true, _) if endpoint.returns_binary() => "post",
(true, _) if endpoint.returns_json() => "post_json",
(true, _) => "post_text",
};
let body_arg = if is_post && endpoint.request_body.is_some() {
", body"
} else {
"get_text"
""
};
let (wrap_prefix, wrap_suffix) = if endpoint.response_kind.text_is_numeric() {
@@ -180,15 +188,15 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
if endpoint.path_params.is_empty() {
writeln!(
output,
" return {}self.{}('{}'){}",
wrap_prefix, fetch_method, path, wrap_suffix
" return {}self.{}('{}'{}){}",
wrap_prefix, fetch_method, path, body_arg, wrap_suffix
)
.unwrap();
} else {
writeln!(
output,
" return {}self.{}(f'{}'){}",
wrap_prefix, fetch_method, path, wrap_suffix
" return {}self.{}(f'{}'{}){}",
wrap_prefix, fetch_method, path, body_arg, wrap_suffix
)
.unwrap();
}
@@ -234,15 +242,15 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
writeln!(output, " return self.get_text(path)").unwrap();
writeln!(
output,
" return {}self.{}(path){}",
wrap_prefix, fetch_method, wrap_suffix
" return {}self.{}(path{}){}",
wrap_prefix, fetch_method, body_arg, wrap_suffix
)
.unwrap();
} else {
writeln!(
output,
" return {}self.{}(path){}",
wrap_prefix, fetch_method, wrap_suffix
" return {}self.{}(path{}){}",
wrap_prefix, fetch_method, body_arg, wrap_suffix
)
.unwrap();
}
@@ -279,6 +287,14 @@ fn build_method_params(endpoint: &Endpoint) -> String {
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
}
}
if let Some(body) = &endpoint.request_body {
let py_type = js_type_to_python(&body.body_type);
if body.required {
params.push(format!(", body: {}", py_type));
} else {
params.push(format!(", body: Optional[{}] = None", py_type));
}
}
params.join("")
}

View File

@@ -99,6 +99,28 @@ class BrkClientBase:
"""Make a GET request and return text."""
return self.get(path).decode()
def post(self, path: str, body: str) -> bytes:
"""Make a POST request with a string body and return raw bytes."""
try:
conn = self._connect()
conn.request("POST", path, body=body)
res = conn.getresponse()
data = res.read()
if res.status >= 400:
raise BrkError(f"HTTP error: {{res.status}}", res.status)
return data
except (ConnectionError, OSError, TimeoutError) as e:
self._conn = None
raise BrkError(str(e))
def post_json(self, path: str, body: str) -> Any:
"""Make a POST request and return JSON."""
return json.loads(self.post(path, body))
def post_text(self, path: str, body: str) -> str:
"""Make a POST request and return text."""
return self.post(path, body).decode()
def close(self) -> None:
"""Close the HTTP client."""
if self._conn:

View File

@@ -87,132 +87,200 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
if !endpoint.should_generate() {
continue;
}
let method_name = endpoint_to_method_name(endpoint);
let base_return_type = if endpoint.returns_binary() {
"Vec<u8>".to_string()
} else if endpoint.returns_text() {
// Text bodies arrive as `String`; per-type parsing is left to the caller.
"String".to_string()
} else {
endpoint
.schema_name()
.map(js_type_to_rust)
.unwrap_or_else(|| "String".to_string())
};
let return_type = if endpoint.supports_csv {
format!("FormatResponse<{}>", base_return_type)
} else {
base_return_type.clone()
};
writeln!(
output,
" /// {}",
endpoint.summary.as_deref().unwrap_or(&method_name)
)
.unwrap();
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " ///").unwrap();
write_description(output, desc, " /// ", " ///");
match endpoint.method.as_str() {
"GET" => generate_get_method(output, endpoint),
"POST" => generate_post_method(output, endpoint),
_ => continue,
}
// Add endpoint path
writeln!(output, " ///").unwrap();
writeln!(
output,
" /// Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
let params = build_method_params(endpoint);
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.unwrap();
let (path, index_arg) = build_path_template(endpoint);
let fetch_method = if endpoint.returns_binary() {
"get_bytes"
} else if endpoint.returns_json() {
"get_json"
} else {
"get_text"
};
if endpoint.query_params.is_empty() {
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}))",
fetch_method, path, index_arg
)
.unwrap();
} else {
writeln!(output, " let mut query = Vec::new();").unwrap();
for param in &endpoint.query_params {
let ident = sanitize_ident(&param.name);
let is_array = param.param_type.ends_with("[]");
if is_array {
writeln!(
output,
" for v in {} {{ query.push(format!(\"{}={{}}\", v)); }}",
ident, param.name
)
.unwrap();
} else if param.required {
writeln!(
output,
" query.push(format!(\"{}={{}}\", {}));",
param.name, ident
)
.unwrap();
} else {
writeln!(
output,
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
ident, param.name
)
.unwrap();
}
}
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
writeln!(
output,
" let path = format!(\"{}{{}}\"{}, query_str);",
path, index_arg
)
.unwrap();
if endpoint.supports_csv {
writeln!(output, " if format == Some(Format::CSV) {{").unwrap();
writeln!(
output,
" self.base.get_text(&path).map(FormatResponse::Csv)"
)
.unwrap();
writeln!(output, " }} else {{").unwrap();
writeln!(
output,
" self.base.{}(&path).map(FormatResponse::Json)",
fetch_method
)
.unwrap();
writeln!(output, " }}").unwrap();
} else {
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
}
}
writeln!(output, " }}\n").unwrap();
}
}
fn generate_get_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
let params = build_method_params(endpoint);
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.unwrap();
let (path, index_arg) = build_path_template(endpoint);
let fetch_method = if endpoint.returns_binary() {
"get_bytes"
} else if endpoint.returns_json() {
"get_json"
} else {
"get_text"
};
if endpoint.query_params.is_empty() {
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}))",
fetch_method, path, index_arg
)
.unwrap();
} else {
write_query_assembly(output, endpoint, &path, &index_arg);
if endpoint.supports_csv {
writeln!(output, " if format == Some(Format::CSV) {{").unwrap();
writeln!(
output,
" self.base.get_text(&path).map(FormatResponse::Csv)"
)
.unwrap();
writeln!(output, " }} else {{").unwrap();
writeln!(
output,
" self.base.{}(&path).map(FormatResponse::Json)",
fetch_method
)
.unwrap();
writeln!(output, " }}").unwrap();
} else {
writeln!(output, " self.base.{}(&path)", fetch_method).unwrap();
}
}
writeln!(output, " }}\n").unwrap();
}
fn generate_post_method(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
let return_type = build_return_type(endpoint);
write_method_doc(output, endpoint);
let mut params = build_method_params(endpoint);
if endpoint.request_body.is_some() {
params.push_str(", body: &str");
}
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.unwrap();
let (path, index_arg) = build_path_template(endpoint);
let body_arg = if endpoint.request_body.is_some() {
"body"
} else {
"\"\""
};
let fetch_method = if endpoint.returns_binary() {
"post_bytes"
} else if endpoint.returns_json() {
"post_json"
} else {
"post_text"
};
if endpoint.query_params.is_empty() {
writeln!(
output,
" self.base.{}(&format!(\"{}\"{}), {})",
fetch_method, path, index_arg, body_arg
)
.unwrap();
} else {
write_query_assembly(output, endpoint, &path, &index_arg);
writeln!(
output,
" self.base.{}(&path, {})",
fetch_method, body_arg
)
.unwrap();
}
writeln!(output, " }}\n").unwrap();
}
fn build_return_type(endpoint: &Endpoint) -> String {
let base = if endpoint.returns_binary() {
"Vec<u8>".to_string()
} else if endpoint.returns_text() {
"String".to_string()
} else {
endpoint
.schema_name()
.map(js_type_to_rust)
.unwrap_or_else(|| "String".to_string())
};
if endpoint.supports_csv {
format!("FormatResponse<{}>", base)
} else {
base
}
}
fn write_method_doc(output: &mut String, endpoint: &Endpoint) {
let method_name = endpoint_to_method_name(endpoint);
writeln!(
output,
" /// {}",
endpoint.summary.as_deref().unwrap_or(&method_name)
)
.unwrap();
if let Some(desc) = &endpoint.description
&& endpoint.summary.as_ref() != Some(desc)
{
writeln!(output, " ///").unwrap();
write_description(output, desc, " /// ", " ///");
}
writeln!(output, " ///").unwrap();
writeln!(
output,
" /// Endpoint: `{} {}`",
endpoint.method.to_uppercase(),
endpoint.path
)
.unwrap();
}
fn write_query_assembly(output: &mut String, endpoint: &Endpoint, path: &str, index_arg: &str) {
writeln!(output, " let mut query = Vec::new();").unwrap();
for param in &endpoint.query_params {
let ident = sanitize_ident(&param.name);
let is_array = param.param_type.ends_with("[]");
if is_array {
writeln!(
output,
" for v in {} {{ query.push(format!(\"{}={{}}\", v)); }}",
ident, param.name
)
.unwrap();
} else if param.required {
writeln!(
output,
" query.push(format!(\"{}={{}}\", {}));",
param.name, ident
)
.unwrap();
} else {
writeln!(
output,
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
ident, param.name
)
.unwrap();
}
}
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
writeln!(
output,
" let path = format!(\"{}{{}}\"{}, query_str);",
path, index_arg
)
.unwrap();
}
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
to_snake_case(&endpoint.operation_name())
}

View File

@@ -111,6 +111,30 @@ impl BrkClientBase {{
.and_then(|mut r| r.body_mut().read_to_vec())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a POST request and deserialize JSON response.
pub fn post_json<T: DeserializeOwned>(&self, path: &str, body: &str) -> Result<T> {{
self.agent.post(&self.url(path))
.send(body)
.and_then(|mut r| r.body_mut().read_json())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a POST request and return raw text response.
pub fn post_text(&self, path: &str, body: &str) -> Result<String> {{
self.agent.post(&self.url(path))
.send(body)
.and_then(|mut r| r.body_mut().read_to_string())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a POST request and return raw bytes response.
pub fn post_bytes(&self, path: &str, body: &str) -> Result<Vec<u8>> {{
self.agent.post(&self.url(path))
.send(body)
.and_then(|mut r| r.body_mut().read_to_vec())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
}}
/// Build series name with suffix.

View File

@@ -1,5 +1,14 @@
use crate::openapi::{Parameter, ResponseKind};
/// Request body shape for POST/PUT/PATCH endpoints.
#[derive(Debug, Clone)]
pub struct RequestBody {
/// Body content type as a name (e.g. "string" for text/plain, "Foo" for an `application/json` $ref).
pub body_type: String,
/// Whether the body is required.
pub required: bool,
}
/// Endpoint information extracted from OpenAPI spec.
#[derive(Debug, Clone)]
pub struct Endpoint {
@@ -17,6 +26,8 @@ pub struct Endpoint {
pub path_params: Vec<Parameter>,
/// Query parameters
pub query_params: Vec<Parameter>,
/// Request body, if any (POST/PUT/PATCH).
pub request_body: Option<RequestBody>,
/// Body kind for the 200 response.
pub response_kind: ResponseKind,
/// Whether this endpoint is deprecated
@@ -27,9 +38,9 @@ pub struct Endpoint {
impl Endpoint {
/// Returns true if this endpoint should be included in client generation.
/// Only non-deprecated GET endpoints are included.
/// Non-deprecated GET and POST endpoints are included.
pub fn should_generate(&self) -> bool {
self.method == "GET" && !self.deprecated
!self.deprecated && (self.method == "GET" || self.method == "POST")
}
/// Returns true if this endpoint returns JSON.

View File

@@ -3,7 +3,7 @@ mod parameter;
mod response_kind;
mod text_schema;
pub use endpoint::Endpoint;
pub use endpoint::{Endpoint, RequestBody};
pub use parameter::Parameter;
pub use response_kind::ResponseKind;
pub use text_schema::TextSchema;
@@ -129,6 +129,7 @@ fn extract_endpoint(
let query_params = extract_parameters(operation, ParameterIn::Query);
let response_kind = extract_response_kind(operation, spec);
let request_body = extract_request_body(operation);
let supports_csv = check_csv_support(operation);
Some(Endpoint {
@@ -139,12 +140,38 @@ fn extract_endpoint(
description: operation.description.clone(),
path_params,
query_params,
request_body,
response_kind,
deprecated: operation.deprecated.unwrap_or(false),
supports_csv,
})
}
/// Extract the request body shape, if any.
/// Prefers `text/plain` (string) over `application/json` (typed).
fn extract_request_body(operation: &Operation) -> Option<RequestBody> {
let req = operation.request_body.as_ref()?;
let req = match req {
ObjectOrReference::Object(rb) => rb,
ObjectOrReference::Ref { .. } => return None,
};
let body_type = if req.content.contains_key("text/plain; charset=utf-8")
|| req.content.contains_key("text/plain")
{
"string".to_string()
} else if let Some(content) = req.content.get("application/json") {
schema_name_from_content(content).unwrap_or_else(|| "Object".to_string())
} else {
"string".to_string()
};
Some(RequestBody {
body_type,
required: req.required.unwrap_or(false),
})
}
/// Check if the endpoint supports CSV format (has text/csv in 200 response content types).
fn check_csv_support(operation: &Operation) -> bool {
let Some(responses) = operation.responses.as_ref() else {