use std::fmt::Write;
use crate::{Endpoint, VERSION, generators::write_description, to_snake_case};
use super::types::js_type_to_rust;
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(
output,
r#"/// Main BRK client with series tree and API methods.
pub struct BrkClient {{
base: Arc<BrkClientBase>,
series: SeriesTree,
}}
impl BrkClient {{
/// Client version.
pub const VERSION: &'static str = "v{VERSION}";
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {{
let base = Arc::new(BrkClientBase::new(base_url));
let series = SeriesTree::new(base.clone(), String::new());
Self {{ base, series }}
}}
/// Create a new client with options.
pub fn with_options(options: BrkClientOptions) -> Self {{
let base = Arc::new(BrkClientBase::with_options(options));
let series = SeriesTree::new(base.clone(), String::new());
Self {{ base, series }}
}}
/// Get the series tree for navigating series.
pub fn series(&self) -> &SeriesTree {{
&self.series
}}
/// Create a dynamic series endpoint builder for any series/index combination.
///
/// Use this for programmatic access when the series name is determined at runtime.
/// For type-safe access, use the `series()` tree instead.
///
/// # Example
/// ```ignore
/// let data = client.series("realized_price", Index::Height)
/// .last(10)
/// .json::<f64>()?;
/// ```
pub fn series_endpoint(&self, series: impl Into<SeriesName>, index: Index) -> SeriesEndpoint<serde_json::Value> {{
SeriesEndpoint::new(
self.base.clone(),
Arc::from(series.into().as_str()),
index,
)
}}
/// Create a dynamic date-based series endpoint builder.
///
/// Returns `Err` if the index is not date-based.
pub fn date_series_endpoint(&self, series: impl Into<SeriesName>, index: Index) -> Result<DateSeriesEndpoint<serde_json::Value>> {{
if !index.is_date_based() {{
return Err(BrkError {{ message: format!("{{}} is not a date-based index", index.name()) }});
}}
Ok(DateSeriesEndpoint::new(
self.base.clone(),
Arc::from(series.into().as_str()),
index,
))
}}
"#,
VERSION = VERSION
)
.unwrap();
generate_api_methods(output, endpoints);
writeln!(output, "}}").unwrap();
}
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
if !endpoint.should_generate() {
continue;
}
let method_name = endpoint_to_method_name(endpoint);
let base_return_type = endpoint
.response_type
.as_deref()
.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, " /// ", " ///");
}
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_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(¶m.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 endpoint_to_method_name(endpoint: &Endpoint) -> String {
to_snake_case(&endpoint.operation_name())
}
fn build_method_params(endpoint: &Endpoint) -> String {
let mut params = Vec::new();
for param in &endpoint.path_params {
let rust_type = param_type_to_rust(¶m.param_type);
params.push(format!(", {}: {}", sanitize_ident(¶m.name), rust_type));
}
for param in &endpoint.query_params {
let rust_type = param_type_to_rust(¶m.param_type);
let name = sanitize_ident(¶m.name);
if param.required {
params.push(format!(", {}: {}", name, rust_type));
} else {
params.push(format!(", {}: Option<{}>", name, rust_type));
}
}
params.join("")
}
fn sanitize_ident(name: &str) -> String {
name.replace(['[', ']'], "")
}
fn param_type_to_rust(param_type: &str) -> String {
if let Some(inner) = param_type.strip_suffix("[]") {
return format!("&[{}]", param_type_to_rust(inner));
}
match param_type {
"string" | "*" => "&str".to_string(),
"integer" | "number" => "i64".to_string(),
"boolean" => "bool".to_string(),
other => other.to_string(),
}
}
fn build_path_template(endpoint: &Endpoint) -> (String, &'static str) {
let has_index_param = endpoint
.path_params
.iter()
.any(|p| p.name == "index" && p.param_type == "Index");
if has_index_param {
(endpoint.path.replace("{index}", "{}"), ", index.name()")
} else {
(endpoint.path.clone(), "")
}
}