Files
brk/crates/brk_binder/src/rust.rs
2025-12-26 22:41:36 +01:00

731 lines
22 KiB
Rust

use std::{collections::HashSet, fmt::Write as FmtWrite, fs, io, path::Path};
use brk_types::{Index, TreeNode};
use crate::{
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
extract_inner_type, get_fields_with_child_info, get_node_fields, get_pattern_instance_base,
to_pascal_case, to_snake_case,
};
/// Generate Rust client from metadata and OpenAPI endpoints.
///
/// `output_path` is the full path to the output file (e.g., "crates/brk_client/src/lib.rs").
pub fn generate_rust_client(
metadata: &ClientMetadata,
endpoints: &[Endpoint],
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
writeln!(output, "// Auto-generated BRK Rust client").unwrap();
writeln!(output, "// Do not edit manually\n").unwrap();
writeln!(output, "#![allow(non_camel_case_types)]").unwrap();
writeln!(output, "#![allow(dead_code)]\n").unwrap();
generate_imports(&mut output);
generate_base_client(&mut output);
generate_metric_node(&mut output);
generate_index_accessors(&mut output, &metadata.index_set_patterns);
generate_pattern_structs(&mut output, &metadata.structural_patterns, metadata);
generate_tree(&mut output, &metadata.catalog, metadata);
generate_main_client(&mut output, endpoints);
fs::write(output_path, output)?;
Ok(())
}
fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::sync::Arc;
use serde::de::DeserializeOwned;
use brk_types::*;
"#
)
.unwrap();
}
fn generate_base_client(output: &mut String) {
writeln!(
output,
r#"/// Error type for BRK client operations.
#[derive(Debug)]
pub struct BrkError {{
pub message: String,
}}
impl std::fmt::Display for BrkError {{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
write!(f, "{{}}", self.message)
}}
}}
impl std::error::Error for BrkError {{}}
/// Result type for BRK client operations.
pub type Result<T> = std::result::Result<T, BrkError>;
/// Options for configuring the BRK client.
#[derive(Debug, Clone)]
pub struct BrkClientOptions {{
pub base_url: String,
pub timeout_secs: u64,
}}
impl Default for BrkClientOptions {{
fn default() -> Self {{
Self {{
base_url: "http://localhost:3000".to_string(),
timeout_secs: 30,
}}
}}
}}
/// Base HTTP client for making requests.
#[derive(Debug, Clone)]
pub struct BrkClientBase {{
base_url: String,
timeout_secs: u64,
}}
impl BrkClientBase {{
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Self {{
Self {{
base_url: base_url.into(),
timeout_secs: 30,
}}
}}
/// Create a new client with options.
pub fn with_options(options: BrkClientOptions) -> Self {{
Self {{
base_url: options.base_url,
timeout_secs: options.timeout_secs,
}}
}}
/// Make a GET request.
pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {{
let url = format!("{{}}{{}}", self.base_url, path);
let response = minreq::get(&url)
.with_timeout(self.timeout_secs)
.send()
.map_err(|e| BrkError {{ message: e.to_string() }})?;
if response.status_code >= 400 {{
return Err(BrkError {{
message: format!("HTTP {{}}", response.status_code),
}});
}}
response
.json()
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
}}
"#
)
.unwrap();
}
fn generate_metric_node(output: &mut String) {
writeln!(
output,
r#"/// A metric node that can fetch data for different indexes.
pub struct MetricNode<T> {{
client: Arc<BrkClientBase>,
path: String,
_marker: std::marker::PhantomData<T>,
}}
impl<T: DeserializeOwned> MetricNode<T> {{
pub fn new(client: Arc<BrkClientBase>, path: String) -> Self {{
Self {{
client,
path,
_marker: std::marker::PhantomData,
}}
}}
/// Fetch all data points for this metric.
pub fn get(&self) -> Result<Vec<T>> {{
self.client.get(&self.path)
}}
/// Fetch data points within a range.
pub fn get_range(&self, from: &str, to: &str) -> Result<Vec<T>> {{
let path = format!("{{}}?from={{}}&to={{}}", self.path, from, to);
self.client.get(&path)
}}
}}
"#
)
.unwrap();
}
fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Index accessor structs\n").unwrap();
for pattern in patterns {
writeln!(
output,
"/// Index accessor for metrics with {} indexes.",
pattern.indexes.len()
)
.unwrap();
writeln!(output, "pub struct {}<T> {{", pattern.name).unwrap();
for index in &pattern.indexes {
let field_name = index_to_field_name(index);
writeln!(output, " pub {}: MetricNode<T>,", field_name).unwrap();
}
writeln!(output, "}}\n").unwrap();
// Generate impl block with constructor
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", pattern.name).unwrap();
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
)
.unwrap();
writeln!(output, " Self {{").unwrap();
for index in &pattern.indexes {
let field_name = index_to_field_name(index);
let path_segment = index.serialize_long();
writeln!(
output,
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}/{}\")),",
field_name, path_segment
)
.unwrap();
}
writeln!(output, " }}").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
}
}
fn index_to_field_name(index: &Index) -> String {
format!("by_{}", to_snake_case(index.serialize_long()))
}
fn generate_pattern_structs(
output: &mut String,
patterns: &[StructuralPattern],
metadata: &ClientMetadata,
) {
if patterns.is_empty() {
return;
}
writeln!(output, "// Reusable pattern structs\n").unwrap();
for pattern in patterns {
let is_parameterizable = pattern.is_parameterizable();
let generic_params = if pattern.is_generic { "<T>" } else { "" };
writeln!(output, "/// Pattern struct for repeated tree structure.").unwrap();
writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap();
for field in &pattern.fields {
let field_name = to_snake_case(&field.name);
let type_annotation =
field_to_type_annotation_generic(field, metadata, pattern.is_generic);
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
}
writeln!(output, "}}\n").unwrap();
// Generate impl block with constructor
let impl_generic = if pattern.is_generic {
"<T: DeserializeOwned>"
} else {
""
};
writeln!(
output,
"impl{} {}{} {{",
impl_generic, pattern.name, generic_params
)
.unwrap();
if is_parameterizable {
writeln!(
output,
" /// Create a new pattern node with accumulated metric name."
)
.unwrap();
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, acc: &str) -> Self {{"
)
.unwrap();
} else {
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
)
.unwrap();
}
writeln!(output, " Self {{").unwrap();
for field in &pattern.fields {
if is_parameterizable {
generate_parameterized_rust_field(output, field, pattern, metadata);
} else {
generate_tree_path_rust_field(output, field, metadata);
}
}
writeln!(output, " }}").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
}
}
fn generate_parameterized_rust_field(
output: &mut String,
field: &PatternField,
pattern: &StructuralPattern,
metadata: &ClientMetadata,
) {
let field_name = to_snake_case(&field.name);
if metadata.is_pattern_type(&field.rust_type) {
let child_acc = if let Some(pos) = pattern.get_field_position(&field.name) {
match pos {
FieldNamePosition::Append(suffix) => format!("&format!(\"{{acc}}{}\")", suffix),
FieldNamePosition::Prepend(prefix) => format!("&format!(\"{}{{acc}}\")", prefix),
FieldNamePosition::Identity => "acc".to_string(),
FieldNamePosition::SetBase(base) => format!("\"{}\"", base),
}
} else {
format!("&format!(\"{{acc}}_{}\")", field.name)
};
writeln!(
output,
" {}: {}::new(client.clone(), {}),",
field_name, field.rust_type, child_acc
)
.unwrap();
return;
}
let metric_expr = if let Some(pos) = pattern.get_field_position(&field.name) {
match pos {
FieldNamePosition::Append(suffix) => format!("format!(\"/{{acc}}{}\")", suffix),
FieldNamePosition::Prepend(prefix) => format!("format!(\"/{}{{acc}}\")", prefix),
FieldNamePosition::Identity => "format!(\"/{acc}\")".to_string(),
FieldNamePosition::SetBase(base) => format!("\"/{}\".to_string()", base),
}
} else {
format!("format!(\"/{{acc}}_{}\")", field.name)
};
if metadata.field_uses_accessor(field) {
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" {}: {}::new(client.clone(), &{}),",
field_name, accessor.name, metric_expr
)
.unwrap();
} else {
writeln!(
output,
" {}: MetricNode::new(client.clone(), {}),",
field_name, metric_expr
)
.unwrap();
}
}
fn generate_tree_path_rust_field(
output: &mut String,
field: &PatternField,
metadata: &ClientMetadata,
) {
let field_name = to_snake_case(&field.name);
if metadata.is_pattern_type(&field.rust_type) {
writeln!(
output,
" {}: {}::new(client.clone(), &format!(\"{{base_path}}/{}\")),",
field_name, field.rust_type, field.name
)
.unwrap();
} else if metadata.field_uses_accessor(field) {
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" {}: {}::new(client.clone(), &format!(\"{{base_path}}/{}\")),",
field_name, accessor.name, field.name
)
.unwrap();
} else {
writeln!(
output,
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}/{}\")),",
field_name, field.name
)
.unwrap();
}
}
fn field_to_type_annotation_generic(
field: &PatternField,
metadata: &ClientMetadata,
is_generic: bool,
) -> String {
field_to_type_annotation_with_generic(field, metadata, is_generic, None)
}
fn field_to_type_annotation_with_generic(
field: &PatternField,
metadata: &ClientMetadata,
is_generic: bool,
generic_value_type: Option<&str>,
) -> String {
let value_type = if is_generic && field.rust_type == "T" {
"T".to_string()
} else {
extract_inner_type(&field.rust_type)
};
if metadata.is_pattern_type(&field.rust_type) {
if metadata.is_pattern_generic(&field.rust_type)
&& let Some(vt) = generic_value_type
{
return format!("{}<{}>", field.rust_type, vt);
}
field.rust_type.clone()
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
format!("{}<{}>", accessor.name, value_type)
} else {
format!("MetricNode<{}>", value_type)
}
}
fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "// Catalog tree\n").unwrap();
let pattern_lookup = metadata.pattern_lookup();
let mut generated = HashSet::new();
generate_tree_node(
output,
"CatalogTree",
catalog,
&pattern_lookup,
metadata,
&mut generated,
);
}
fn generate_tree_node(
output: &mut String,
name: &str,
node: &TreeNode,
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
metadata: &ClientMetadata,
generated: &mut HashSet<String>,
) {
let TreeNode::Branch(children) = node else {
return;
};
let fields_with_child_info = get_fields_with_child_info(children, name, pattern_lookup);
let fields: Vec<PatternField> = fields_with_child_info
.iter()
.map(|(f, _)| f.clone())
.collect();
if let Some(pattern_name) = pattern_lookup.get(&fields)
&& pattern_name != name
{
return;
}
if generated.contains(name) {
return;
}
generated.insert(name.to_string());
writeln!(output, "/// Catalog tree node.").unwrap();
writeln!(output, "pub struct {} {{", name).unwrap();
for (field, child_fields) in &fields_with_child_info {
let field_name = to_snake_case(&field.name);
// Look up type parameter for generic patterns
let generic_value_type = child_fields
.as_ref()
.and_then(|cf| metadata.get_type_param(cf))
.map(String::as_str);
let type_annotation =
field_to_type_annotation_with_generic(field, metadata, false, generic_value_type);
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
}
writeln!(output, "}}\n").unwrap();
writeln!(output, "impl {} {{", name).unwrap();
writeln!(
output,
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
)
.unwrap();
writeln!(output, " Self {{").unwrap();
for (field, (child_name, child_node)) in fields.iter().zip(children.iter()) {
let field_name = to_snake_case(&field.name);
if metadata.is_pattern_type(&field.rust_type) {
let pattern = metadata.find_pattern(&field.rust_type);
let is_parameterizable = pattern.is_some_and(|p| p.is_parameterizable());
if is_parameterizable {
let metric_base = get_pattern_instance_base(child_node, child_name);
writeln!(
output,
" {}: {}::new(client.clone(), \"{}\"),",
field_name, field.rust_type, metric_base
)
.unwrap();
} else {
writeln!(
output,
" {}: {}::new(client.clone(), &format!(\"{{base_path}}/{}\")),",
field_name, field.rust_type, field.name
)
.unwrap();
}
} else if metadata.field_uses_accessor(field) {
let metric_path = if let TreeNode::Leaf(leaf) = child_node {
format!("/{}", leaf.name())
} else {
format!("{{base_path}}/{}", field.name)
};
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
if metric_path.contains("{base_path}") {
writeln!(
output,
" {}: {}::new(client.clone(), &format!(\"{}\")),",
field_name, accessor.name, metric_path
)
.unwrap();
} else {
writeln!(
output,
" {}: {}::new(client.clone(), \"{}\"),",
field_name, accessor.name, metric_path
)
.unwrap();
}
} else {
let metric_path = if let TreeNode::Leaf(leaf) = child_node {
format!("/{}", leaf.name())
} else {
format!("{{base_path}}/{}", field.name)
};
if metric_path.contains("{base_path}") {
writeln!(
output,
" {}: MetricNode::new(client.clone(), format!(\"{}\")),",
field_name, metric_path
)
.unwrap();
} else {
writeln!(
output,
" {}: MetricNode::new(client.clone(), \"{}\".to_string()),",
field_name, metric_path
)
.unwrap();
}
}
}
writeln!(output, " }}").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
for (child_name, child_node) in children {
if let TreeNode::Branch(grandchildren) = child_node {
let child_fields = get_node_fields(grandchildren, pattern_lookup);
if !pattern_lookup.contains_key(&child_fields) {
let child_struct_name = format!("{}_{}", name, to_pascal_case(child_name));
generate_tree_node(
output,
&child_struct_name,
child_node,
pattern_lookup,
metadata,
generated,
);
}
}
}
}
fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
writeln!(
output,
r#"/// Main BRK client with catalog tree and API methods.
pub struct BrkClient {{
base: Arc<BrkClientBase>,
tree: CatalogTree,
}}
impl BrkClient {{
/// 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 tree = CatalogTree::new(base.clone(), "");
Self {{ base, tree }}
}}
/// Create a new client with options.
pub fn with_options(options: BrkClientOptions) -> Self {{
let base = Arc::new(BrkClientBase::with_options(options));
let tree = CatalogTree::new(base.clone(), "");
Self {{ base, tree }}
}}
/// Get the catalog tree for navigating metrics.
pub fn tree(&self) -> &CatalogTree {{
&self.tree
}}
"#
)
.unwrap();
generate_api_methods(output, endpoints);
writeln!(output, "}}").unwrap();
}
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 return_type = endpoint
.response_type
.as_deref()
.map(js_type_to_rust)
.unwrap_or_else(|| "serde_json::Value".to_string());
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();
writeln!(output, " /// {}", desc).unwrap();
}
let params = build_method_params(endpoint);
writeln!(
output,
" pub fn {}(&self{}) -> Result<{}> {{",
method_name, params, return_type
)
.unwrap();
let path = build_path_template(&endpoint.path, &endpoint.path_params);
if endpoint.query_params.is_empty() {
writeln!(output, " self.base.get(&format!(\"{}\"))", path).unwrap();
} else {
writeln!(output, " let mut query = Vec::new();").unwrap();
for param in &endpoint.query_params {
if param.required {
writeln!(
output,
" query.push(format!(\"{}={{}}\", {}));",
param.name, param.name
)
.unwrap();
} else {
writeln!(
output,
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
param.name, param.name
)
.unwrap();
}
}
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
writeln!(
output,
" self.base.get(&format!(\"{}{{}}\", query_str))",
path
)
.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 {
params.push(format!(", {}: &str", param.name));
}
for param in &endpoint.query_params {
if param.required {
params.push(format!(", {}: &str", param.name));
} else {
params.push(format!(", {}: Option<&str>", param.name));
}
}
params.join("")
}
fn build_path_template(path: &str, path_params: &[super::Parameter]) -> String {
let mut result = path.to_string();
for param in path_params {
let placeholder = format!("{{{}}}", param.name);
let interpolation = format!("{{{}}}", param.name);
result = result.replace(&placeholder, &interpolation);
}
result
}
fn js_type_to_rust(js_type: &str) -> String {
if let Some(inner) = js_type.strip_suffix("[]") {
format!("Vec<{}>", js_type_to_rust(inner))
} else {
match js_type {
"string" => "String".to_string(),
"number" => "f64".to_string(),
"boolean" => "bool".to_string(),
"*" => "serde_json::Value".to_string(),
other => other.to_string(),
}
}
}