From bcb8d5bed61fe4b09a4baacf63756af80ea3c675 Mon Sep 17 00:00:00 2001 From: nym21 Date: Sat, 20 Dec 2025 19:33:04 +0100 Subject: [PATCH] binder: snapshot --- crates/brk_binder/src/javascript.rs | 218 ++++++++++++++++---- crates/brk_binder/src/lib.rs | 16 +- crates/brk_binder/src/python.rs | 251 +++++++++++++++++------ crates/brk_binder/src/rust.rs | 199 ++++++++++++++---- crates/brk_binder/src/types.rs | 305 +++++++++++++++++++++++++++- crates/brk_server/src/lib.rs | 14 +- 6 files changed, 841 insertions(+), 162 deletions(-) diff --git a/crates/brk_binder/src/javascript.rs b/crates/brk_binder/src/javascript.rs index cae12c641..f23152a79 100644 --- a/crates/brk_binder/src/javascript.rs +++ b/crates/brk_binder/src/javascript.rs @@ -8,8 +8,9 @@ use brk_types::{Index, TreeNode}; use serde_json::Value; use crate::{ - ClientMetadata, Endpoint, IndexSetPattern, PatternField, StructuralPattern, TypeSchemas, - get_node_fields, to_camel_case, to_pascal_case, + ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, + TypeSchemas, get_first_leaf_name, get_node_fields, get_pattern_instance_base, to_camel_case, + to_pascal_case, }; /// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints @@ -330,6 +331,9 @@ fn generate_structural_patterns( writeln!(output, "// Reusable structural pattern factories\n").unwrap(); for pattern in patterns { + // Check if this pattern is parameterizable (has field positions detected) + let is_parameterizable = pattern.is_parameterizable(); + // Generate JSDoc typedef writeln!(output, "/**").unwrap(); writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap(); @@ -349,13 +353,19 @@ fn generate_structural_patterns( writeln!(output, "/**").unwrap(); writeln!(output, " * Create a {} pattern node", pattern.name).unwrap(); writeln!(output, " * @param {{BrkClientBase}} client").unwrap(); - writeln!(output, " * @param {{string}} basePath").unwrap(); + if is_parameterizable { + writeln!(output, " * @param {{string}} acc - Accumulated metric name").unwrap(); + } else { + writeln!(output, " * @param {{string}} basePath").unwrap(); + } writeln!(output, " * @returns {{{}}}", pattern.name).unwrap(); writeln!(output, " */").unwrap(); + + let param_name = if is_parameterizable { "acc" } else { "basePath" }; writeln!( output, - "function create{}(client, basePath) {{", - pattern.name + "function create{}(client, {}) {{", + pattern.name, param_name ) .unwrap(); writeln!(output, " return {{").unwrap(); @@ -366,36 +376,11 @@ fn generate_structural_patterns( } else { "" }; - if metadata.is_pattern_type(&field.rust_type) { - writeln!( - output, - " {}: create{}(client, `${{basePath}}/{}`){}", - to_camel_case(&field.name), - field.rust_type, - field.name, - comma - ) - .unwrap(); - } else if field_uses_accessor(field, metadata) { - let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); - writeln!( - output, - " {}: create{}(client, `${{basePath}}/{}`){}", - to_camel_case(&field.name), - accessor.name, - field.name, - comma - ) - .unwrap(); + + if is_parameterizable { + generate_parameterized_field(output, field, pattern, metadata, comma); } else { - writeln!( - output, - " {}: new MetricNode(client, `${{basePath}}/{}`){}", - to_camel_case(&field.name), - field.name, - comma - ) - .unwrap(); + generate_tree_path_field(output, field, metadata, comma); } } @@ -404,6 +389,105 @@ fn generate_structural_patterns( } } +/// Generate a field using parameterized (prepend/append) metric name construction +fn generate_parameterized_field( + output: &mut String, + field: &PatternField, + pattern: &StructuralPattern, + metadata: &ClientMetadata, + comma: &str, +) { + let field_name_js = to_camel_case(&field.name); + + // For branch fields, pass the accumulated name to nested pattern + if metadata.is_pattern_type(&field.rust_type) { + // Get the field position to determine how to transform the accumulated name + let child_acc = if let Some(pos) = pattern.get_field_position(&field.name) { + match pos { + FieldNamePosition::Append(suffix) => format!("`${{acc}}{}`", suffix), + FieldNamePosition::Prepend(prefix) => format!("`{}{}`", prefix, "${acc}"), + FieldNamePosition::Identity => "acc".to_string(), + FieldNamePosition::SetBase(base) => format!("'{}'", base), + } + } else { + // Fallback: append field name + format!("`${{acc}}_{}`", field.name) + }; + + writeln!( + output, + " {}: create{}(client, {}){}", + field_name_js, field.rust_type, child_acc, comma + ) + .unwrap(); + return; + } + + // For leaf fields, construct the metric path based on position + let metric_expr = if let Some(pos) = pattern.get_field_position(&field.name) { + match pos { + FieldNamePosition::Append(suffix) => format!("`/${{acc}}{suffix}`"), + FieldNamePosition::Prepend(prefix) => format!("`/{prefix}${{acc}}`"), + FieldNamePosition::Identity => "`/${acc}`".to_string(), + FieldNamePosition::SetBase(base) => format!("'/{base}'"), + } + } else { + // Fallback: use field name appended + format!("`/${{acc}}_{}`", field.name) + }; + + if field_uses_accessor(field, metadata) { + let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); + writeln!( + output, + " {}: create{}(client, {}){}", + field_name_js, accessor.name, metric_expr, comma + ) + .unwrap(); + } else { + writeln!( + output, + " {}: new MetricNode(client, {}){}", + field_name_js, metric_expr, comma + ) + .unwrap(); + } +} + +/// Generate a field using tree path construction (fallback for non-parameterizable patterns) +fn generate_tree_path_field( + output: &mut String, + field: &PatternField, + metadata: &ClientMetadata, + comma: &str, +) { + let field_name_js = to_camel_case(&field.name); + + if metadata.is_pattern_type(&field.rust_type) { + writeln!( + output, + " {}: create{}(client, `${{basePath}}/{}`){}", + field_name_js, field.rust_type, field.name, comma + ) + .unwrap(); + } else if field_uses_accessor(field, metadata) { + let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); + writeln!( + output, + " {}: create{}(client, `${{basePath}}/{}`){}", + field_name_js, accessor.name, field.name, comma + ) + .unwrap(); + } else { + writeln!( + output, + " {}: new MetricNode(client, `${{basePath}}/{}`){}", + field_name_js, field.name, comma + ) + .unwrap(); + } +} + /// Convert pattern field to JavaScript/JSDoc type fn field_to_js_type(field: &PatternField, metadata: &ClientMetadata) -> String { if metadata.is_pattern_type(&field.rust_type) { @@ -556,7 +640,7 @@ fn generate_main_client( fn generate_tree_initializer( output: &mut String, node: &TreeNode, - path: &str, + accumulated_name: &str, indent: usize, pattern_lookup: &std::collections::HashMap, String>, metadata: &ClientMetadata, @@ -566,12 +650,6 @@ fn generate_tree_initializer( if let TreeNode::Branch(children) = node { for (i, (child_name, child_node)) in children.iter().enumerate() { let field_name = to_camel_case(child_name); - let child_path = if path.is_empty() { - format!("/{}", child_name) - } else { - format!("{}/{}", path, child_name) - }; - let comma = if i < children.len() - 1 { "," } else { "" }; match child_node { @@ -597,18 +675,44 @@ fn generate_tree_initializer( TreeNode::Branch(grandchildren) => { let child_fields = get_node_fields(grandchildren, pattern_lookup); if let Some(pattern_name) = pattern_lookup.get(&child_fields) { + // For parameterized patterns, derive accumulated metric name from first leaf + let pattern = metadata + .structural_patterns + .iter() + .find(|p| &p.name == pattern_name); + let is_parameterizable = + pattern.map(|p| p.is_parameterizable()).unwrap_or(false); + + let arg = if is_parameterizable { + // Get the metric base from the first leaf descendant + get_pattern_instance_base(child_node, child_name) + } else { + // Fallback to tree path for non-parameterizable patterns + if accumulated_name.is_empty() { + format!("/{}", child_name) + } else { + format!("{}/{}", accumulated_name, child_name) + } + }; + writeln!( output, "{}{}: create{}(this, '{}'){}", - indent_str, field_name, pattern_name, child_path, comma + indent_str, field_name, pattern_name, arg, comma ) .unwrap(); } else { + // Not a pattern - recurse with accumulated name + let child_acc = infer_child_accumulated_name( + child_node, + accumulated_name, + child_name, + ); writeln!(output, "{}{}: {{", indent_str, field_name).unwrap(); generate_tree_initializer( output, child_node, - &child_path, + &child_acc, indent + 1, pattern_lookup, metadata, @@ -621,6 +725,34 @@ fn generate_tree_initializer( } } +/// Infer the accumulated metric name for a child node +fn infer_child_accumulated_name(node: &TreeNode, parent_acc: &str, field_name: &str) -> String { + // Try to infer from first leaf descendant + if let Some(leaf_name) = get_first_leaf_name(node) { + // Look for field_name in the leaf metric name + if let Some(pos) = leaf_name.find(field_name) { + // The field_name appears in the metric - use it as base + if pos == 0 { + // At start - this is the base + return field_name.to_string(); + } else if leaf_name.chars().nth(pos - 1) == Some('_') { + // After underscore - likely an append pattern + if parent_acc.is_empty() { + return field_name.to_string(); + } + return format!("{}_{}", parent_acc, field_name); + } + } + } + + // Fallback: append field name + if parent_acc.is_empty() { + field_name.to_string() + } else { + format!("{}_{}", parent_acc, field_name) + } +} + /// Generate API methods fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { for endpoint in endpoints { diff --git a/crates/brk_binder/src/lib.rs b/crates/brk_binder/src/lib.rs index c671ebad2..a6246c45b 100644 --- a/crates/brk_binder/src/lib.rs +++ b/crates/brk_binder/src/lib.rs @@ -1,5 +1,9 @@ -mod js; +use std::{fs::create_dir_all, io, path::Path}; + +use brk_query::Vecs; + mod javascript; +mod js; mod openapi; mod python; mod rust; @@ -12,10 +16,6 @@ pub use python::*; pub use rust::*; pub use types::*; -use brk_query::Vecs; -use std::io; -use std::path::Path; - pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Generate all client libraries from the query vecs and OpenAPI JSON @@ -29,17 +29,17 @@ pub fn generate_clients(vecs: &Vecs, openapi_json: &str, output_dir: &Path) -> i // Generate Rust client (uses real brk_types, no schema conversion needed) let rust_path = output_dir.join("rust"); - std::fs::create_dir_all(&rust_path)?; + create_dir_all(&rust_path)?; generate_rust_client(&metadata, &endpoints, &rust_path)?; // Generate JavaScript client (needs schemas for type definitions) let js_path = output_dir.join("javascript"); - std::fs::create_dir_all(&js_path)?; + create_dir_all(&js_path)?; generate_javascript_client(&metadata, &endpoints, &schemas, &js_path)?; // Generate Python client (needs schemas for type definitions) let python_path = output_dir.join("python"); - std::fs::create_dir_all(&python_path)?; + create_dir_all(&python_path)?; generate_python_client(&metadata, &endpoints, &schemas, &python_path)?; Ok(()) diff --git a/crates/brk_binder/src/python.rs b/crates/brk_binder/src/python.rs index a230f9ee5..2b46e4f48 100644 --- a/crates/brk_binder/src/python.rs +++ b/crates/brk_binder/src/python.rs @@ -8,8 +8,8 @@ use brk_types::{Index, TreeNode}; use serde_json::Value; use crate::{ - ClientMetadata, Endpoint, IndexSetPattern, PatternField, StructuralPattern, TypeSchemas, - get_node_fields, to_pascal_case, to_snake_case, + ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, + TypeSchemas, get_node_fields, get_pattern_instance_base, to_pascal_case, to_snake_case, }; /// Generate Python client from metadata and OpenAPI endpoints @@ -253,6 +253,8 @@ fn generate_structural_patterns( writeln!(output, "# Reusable structural pattern classes\n").unwrap(); for pattern in patterns { + let is_parameterizable = pattern.is_parameterizable(); + writeln!(output, "class {}:", pattern.name).unwrap(); writeln!( output, @@ -260,44 +262,27 @@ fn generate_structural_patterns( ) .unwrap(); writeln!(output, " ").unwrap(); - writeln!( - output, - " def __init__(self, client: BrkClientBase, base_path: str):" - ) - .unwrap(); + + if is_parameterizable { + writeln!( + output, + " def __init__(self, client: BrkClientBase, acc: str):" + ) + .unwrap(); + writeln!(output, " \"\"\"Create pattern node with accumulated metric name.\"\"\"").unwrap(); + } else { + writeln!( + output, + " def __init__(self, client: BrkClientBase, base_path: str):" + ) + .unwrap(); + } for field in &pattern.fields { - let py_type = field_to_python_type(field, metadata); - if metadata.is_pattern_type(&field.rust_type) { - writeln!( - output, - " self.{}: {} = {}(client, f'{{base_path}}/{}')", - to_snake_case(&field.name), - py_type, - field.rust_type, - field.name - ) - .unwrap(); - } else if field_uses_accessor(field, metadata) { - let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); - writeln!( - output, - " self.{}: {} = {}(client, f'{{base_path}}/{}')", - to_snake_case(&field.name), - py_type, - accessor.name, - field.name - ) - .unwrap(); + if is_parameterizable { + generate_parameterized_python_field(output, field, pattern, metadata); } else { - writeln!( - output, - " self.{}: {} = MetricNode(client, f'{{base_path}}/{}')", - to_snake_case(&field.name), - py_type, - field.name - ) - .unwrap(); + generate_tree_path_python_field(output, field, metadata); } } @@ -305,6 +290,102 @@ fn generate_structural_patterns( } } +/// Generate a field using parameterized (prepend/append) metric name construction +fn generate_parameterized_python_field( + output: &mut String, + field: &PatternField, + pattern: &StructuralPattern, + metadata: &ClientMetadata, +) { + let field_name = to_snake_case(&field.name); + let py_type = field_to_python_type(field, metadata); + + // For branch fields, pass the accumulated name to nested pattern + 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!("f'{{acc}}{}'", suffix), + FieldNamePosition::Prepend(prefix) => format!("f'{}{{acc}}'", prefix), + FieldNamePosition::Identity => "acc".to_string(), + FieldNamePosition::SetBase(base) => format!("'{}'", base), + } + } else { + format!("f'{{acc}}_{}'", field.name) + }; + + writeln!( + output, + " self.{}: {} = {}(client, {})", + field_name, py_type, field.rust_type, child_acc + ) + .unwrap(); + return; + } + + // For leaf fields, construct the metric path based on position + let metric_expr = if let Some(pos) = pattern.get_field_position(&field.name) { + match pos { + FieldNamePosition::Append(suffix) => format!("f'/{{acc}}{}'", suffix), + FieldNamePosition::Prepend(prefix) => format!("f'/{}{{acc}}'", prefix), + FieldNamePosition::Identity => "f'/{acc}'".to_string(), + FieldNamePosition::SetBase(base) => format!("'/{}'", base), + } + } else { + format!("f'/{{acc}}_{}'", field.name) + }; + + if field_uses_accessor(field, metadata) { + let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); + writeln!( + output, + " self.{}: {} = {}(client, {})", + field_name, py_type, accessor.name, metric_expr + ) + .unwrap(); + } else { + writeln!( + output, + " self.{}: {} = MetricNode(client, {})", + field_name, py_type, metric_expr + ) + .unwrap(); + } +} + +/// Generate a field using tree path construction (fallback for non-parameterizable patterns) +fn generate_tree_path_python_field( + output: &mut String, + field: &PatternField, + metadata: &ClientMetadata, +) { + let field_name = to_snake_case(&field.name); + let py_type = field_to_python_type(field, metadata); + + if metadata.is_pattern_type(&field.rust_type) { + writeln!( + output, + " self.{}: {} = {}(client, f'{{base_path}}/{}')", + field_name, py_type, field.rust_type, field.name + ) + .unwrap(); + } else if field_uses_accessor(field, metadata) { + let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); + writeln!( + output, + " self.{}: {} = {}(client, f'{{base_path}}/{}')", + field_name, py_type, accessor.name, field.name + ) + .unwrap(); + } else { + writeln!( + output, + " self.{}: {} = MetricNode(client, f'{{base_path}}/{}')", + field_name, py_type, field.name + ) + .unwrap(); + } +} + /// Convert pattern field to Python type annotation fn field_to_python_type(field: &PatternField, metadata: &ClientMetadata) -> String { if metadata.is_pattern_type(&field.rust_type) { @@ -374,38 +455,80 @@ fn generate_tree_class( ) .unwrap(); - for field in &fields { + for (field, (child_name, child_node)) in fields.iter().zip(children.iter()) { let py_type = field_to_python_type(field, metadata); + let field_name_py = to_snake_case(&field.name); + if metadata.is_pattern_type(&field.rust_type) { - writeln!( - output, - " self.{}: {} = {}(client, f'{{base_path}}/{}')", - to_snake_case(&field.name), - py_type, - field.rust_type, - field.name - ) - .unwrap(); + // Check if the pattern is parameterizable + let pattern = metadata + .structural_patterns + .iter() + .find(|p| p.name == field.rust_type); + let is_parameterizable = pattern.map(|p| p.is_parameterizable()).unwrap_or(false); + + if is_parameterizable { + // Get the metric base from the first leaf descendant + let metric_base = get_pattern_instance_base(child_node, child_name); + writeln!( + output, + " self.{}: {} = {}(client, '{}')", + field_name_py, py_type, field.rust_type, metric_base + ) + .unwrap(); + } else { + writeln!( + output, + " self.{}: {} = {}(client, f'{{base_path}}/{}')", + field_name_py, py_type, field.rust_type, field.name + ) + .unwrap(); + } } else if field_uses_accessor(field, metadata) { + // Leaf with accessor - get actual metric path from leaf + 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(); - writeln!( - output, - " self.{}: {} = {}(client, f'{{base_path}}/{}')", - to_snake_case(&field.name), - py_type, - accessor.name, - field.name - ) - .unwrap(); + if metric_path.contains("{base_path}") { + writeln!( + output, + " self.{}: {} = {}(client, f'{}')", + field_name_py, py_type, accessor.name, metric_path + ) + .unwrap(); + } else { + writeln!( + output, + " self.{}: {} = {}(client, '{}')", + field_name_py, py_type, accessor.name, metric_path + ) + .unwrap(); + } } else { - writeln!( - output, - " self.{}: {} = MetricNode(client, f'{{base_path}}/{}')", - to_snake_case(&field.name), - py_type, - field.name - ) - .unwrap(); + // Leaf without accessor - get actual metric path from leaf + 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, + " self.{}: {} = MetricNode(client, f'{}')", + field_name_py, py_type, metric_path + ) + .unwrap(); + } else { + writeln!( + output, + " self.{}: {} = MetricNode(client, '{}')", + field_name_py, py_type, metric_path + ) + .unwrap(); + } } } diff --git a/crates/brk_binder/src/rust.rs b/crates/brk_binder/src/rust.rs index 3b7e89979..397e183df 100644 --- a/crates/brk_binder/src/rust.rs +++ b/crates/brk_binder/src/rust.rs @@ -6,7 +6,7 @@ use std::path::Path; use brk_types::{Index, TreeNode}; -use crate::{ClientMetadata, Endpoint, IndexSetPattern, PatternField, StructuralPattern, get_node_fields, to_pascal_case, to_snake_case}; +use crate::{ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, get_node_fields, get_pattern_instance_base, to_pascal_case, to_snake_case}; /// Generate Rust client from metadata and OpenAPI endpoints pub fn generate_rust_client( @@ -232,6 +232,8 @@ fn generate_pattern_structs(output: &mut String, patterns: &[StructuralPattern], writeln!(output, "// Reusable pattern structs\n").unwrap(); for pattern in patterns { + let is_parameterizable = pattern.is_parameterizable(); + writeln!(output, "/// Pattern struct for repeated tree structure.").unwrap(); writeln!(output, "pub struct {}<'a> {{", pattern.name).unwrap(); @@ -245,30 +247,20 @@ fn generate_pattern_structs(output: &mut String, patterns: &[StructuralPattern], // Generate impl block with constructor writeln!(output, "impl<'a> {}<'a> {{", pattern.name).unwrap(); - writeln!(output, " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{").unwrap(); + + if is_parameterizable { + writeln!(output, " /// Create a new pattern node with accumulated metric name.").unwrap(); + writeln!(output, " pub fn new(client: &'a BrkClientBase, acc: &str) -> Self {{").unwrap(); + } else { + writeln!(output, " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{").unwrap(); + } writeln!(output, " Self {{").unwrap(); for field in &pattern.fields { - let field_name = to_snake_case(&field.name); - if metadata.is_pattern_type(&field.rust_type) { - writeln!( - output, - " {}: {}::new(client, &format!(\"{{base_path}}/{}\"))," , - field_name, field.rust_type, field.name - ).unwrap(); - } else if field_uses_accessor(field, metadata) { - let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); - writeln!( - output, - " {}: {}::new(client, &format!(\"{{base_path}}/{}\"))," , - field_name, accessor.name, field.name - ).unwrap(); + if is_parameterizable { + generate_parameterized_rust_field(output, field, pattern, metadata); } else { - writeln!( - output, - " {}: MetricNode::new(client, format!(\"{{base_path}}/{}\"))," , - field_name, field.name - ).unwrap(); + generate_tree_path_rust_field(output, field, metadata); } } @@ -278,6 +270,94 @@ fn generate_pattern_structs(output: &mut String, patterns: &[StructuralPattern], } } +/// Generate a field using parameterized (prepend/append) metric name construction +fn generate_parameterized_rust_field( + output: &mut String, + field: &PatternField, + pattern: &StructuralPattern, + metadata: &ClientMetadata, +) { + let field_name = to_snake_case(&field.name); + + // For branch fields, pass the accumulated name to nested pattern + 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, {}),", + field_name, field.rust_type, child_acc + ).unwrap(); + return; + } + + // For leaf fields, construct the metric path based on position + 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 field_uses_accessor(field, metadata) { + let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); + writeln!( + output, + " {}: {}::new(client, &{}),", + field_name, accessor.name, metric_expr + ).unwrap(); + } else { + writeln!( + output, + " {}: MetricNode::new(client, {}),", + field_name, metric_expr + ).unwrap(); + } +} + +/// Generate a field using tree path construction (fallback for non-parameterizable patterns) +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, &format!(\"{{base_path}}/{}\")),", + field_name, field.rust_type, field.name + ).unwrap(); + } else if field_uses_accessor(field, metadata) { + let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap(); + writeln!( + output, + " {}: {}::new(client, &format!(\"{{base_path}}/{}\")),", + field_name, accessor.name, field.name + ).unwrap(); + } else { + writeln!( + output, + " {}: MetricNode::new(client, format!(\"{{base_path}}/{}\")),", + field_name, field.name + ).unwrap(); + } +} + /// Convert a PatternField to the full type annotation fn field_to_type_annotation(field: &PatternField, metadata: &ClientMetadata) -> String { if metadata.is_pattern_type(&field.rust_type) { @@ -380,27 +460,72 @@ fn generate_tree_node( writeln!(output, " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{").unwrap(); writeln!(output, " Self {{").unwrap(); - for field in &fields { + 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) { - writeln!( - output, - " {}: {}::new(client, &format!(\"{{base_path}}/{}\"))," , - field_name, field.rust_type, field.name - ).unwrap(); + // Check if the pattern is parameterizable + let pattern = metadata + .structural_patterns + .iter() + .find(|p| p.name == field.rust_type); + let is_parameterizable = pattern.map(|p| p.is_parameterizable()).unwrap_or(false); + + if is_parameterizable { + // Get the metric base from the first leaf descendant + let metric_base = get_pattern_instance_base(child_node, child_name); + writeln!( + output, + " {}: {}::new(client, \"{}\"),", + field_name, field.rust_type, metric_base + ).unwrap(); + } else { + writeln!( + output, + " {}: {}::new(client, &format!(\"{{base_path}}/{}\"))," , + field_name, field.rust_type, field.name + ).unwrap(); + } } else if field_uses_accessor(field, metadata) { + // Leaf with accessor - get actual metric path from leaf + 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(); - writeln!( - output, - " {}: {}::new(client, &format!(\"{{base_path}}/{}\"))," , - field_name, accessor.name, field.name - ).unwrap(); + if metric_path.contains("{base_path}") { + writeln!( + output, + " {}: {}::new(client, &format!(\"{}\")),", + field_name, accessor.name, metric_path + ).unwrap(); + } else { + writeln!( + output, + " {}: {}::new(client, \"{}\"),", + field_name, accessor.name, metric_path + ).unwrap(); + } } else { - writeln!( - output, - " {}: MetricNode::new(client, format!(\"{{base_path}}/{}\"))," , - field_name, field.name - ).unwrap(); + // Leaf without accessor - get actual metric path from leaf + 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, format!(\"{}\")),", + field_name, metric_path + ).unwrap(); + } else { + writeln!( + output, + " {}: MetricNode::new(client, \"{}\".to_string()),", + field_name, metric_path + ).unwrap(); + } } } diff --git a/crates/brk_binder/src/types.rs b/crates/brk_binder/src/types.rs index 76fa4b8e7..722bc2bdd 100644 --- a/crates/brk_binder/src/types.rs +++ b/crates/brk_binder/src/types.rs @@ -1,9 +1,22 @@ -use std::collections::{BTreeSet, HashMap}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; use std::hash::{Hash, Hasher}; use brk_query::Vecs; use brk_types::{Index, TreeNode}; +/// How a field modifies the accumulated metric name +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum FieldNamePosition { + /// Field prepends a prefix: leaf.name() = prefix + accumulated + Prepend(String), + /// Field appends a suffix: leaf.name() = accumulated + suffix + Append(String), + /// Field IS the accumulated name (no modification) + Identity, + /// Field sets a new base name (used at pattern entry points) + SetBase(String), +} + /// Metadata extracted from brk_query for client generation #[derive(Debug)] pub struct ClientMetadata { @@ -33,6 +46,8 @@ pub struct StructuralPattern { pub name: String, /// Ordered list of child fields (sorted by field name) pub fields: Vec, + /// How each field modifies the accumulated name (field_name -> position) + pub field_positions: HashMap, } impl StructuralPattern { @@ -41,6 +56,21 @@ impl StructuralPattern { pub fn contains_leaves(&self) -> bool { self.fields.iter().any(|f| !f.indexes.is_empty()) } + + /// Returns true if all leaf fields have consistent name transformations. + /// A pattern is parameterizable if we can detect prepend/append patterns. + pub fn is_parameterizable(&self) -> bool { + !self.field_positions.is_empty() + && self.fields.iter().all(|f| { + // Branch fields are always OK (they delegate to nested patterns) + f.indexes.is_empty() || self.field_positions.contains_key(&f.name) + }) + } + + /// Get the field position for a given field name + pub fn get_field_position(&self, field_name: &str) -> Option<&FieldNamePosition> { + self.field_positions.get(field_name) + } } /// A field in a structural pattern @@ -126,19 +156,255 @@ fn detect_structural_patterns(tree: &TreeNode) -> Vec { // Process tree bottom-up to resolve all branch types resolve_branch_patterns(tree, &mut signature_to_pattern, &mut signature_counts); - // Build final list of patterns (only those appearing 2+ times) + // Build initial list of patterns (only those appearing 2+ times) let mut patterns: Vec = signature_to_pattern - .into_iter() - .filter(|(sig, _)| signature_counts.get(sig).copied().unwrap_or(0) >= 2) - .map(|(fields, name)| StructuralPattern { name, fields }) + .iter() + .filter(|(sig, _)| signature_counts.get(*sig).copied().unwrap_or(0) >= 2) + .map(|(fields, name)| StructuralPattern { + name: name.clone(), + fields: fields.clone(), + field_positions: HashMap::new(), + }) .collect(); + // Build lookup for second pass + let pattern_lookup: HashMap, String> = signature_to_pattern; + + // Second pass: analyze field positions by traversing tree instances + analyze_pattern_field_positions(tree, &mut patterns, &pattern_lookup); + // Sort by number of fields descending (larger patterns first) patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len())); patterns } +/// Analyze field positions for all patterns by traversing tree instances. +/// For each pattern instance, we compare parent accumulated name with child leaf names. +fn analyze_pattern_field_positions( + tree: &TreeNode, + patterns: &mut [StructuralPattern], + pattern_lookup: &HashMap, String>, +) { + // Collect instances: pattern_name -> vec of (accumulated_name, field_name, leaf_name) + let mut instances: HashMap> = HashMap::new(); + + // Traverse tree and collect instances + collect_pattern_instances(tree, "", &mut instances, pattern_lookup); + + // For each pattern, analyze field positions from instances + for pattern in patterns.iter_mut() { + if let Some(pattern_instances) = instances.get(&pattern.name) { + pattern.field_positions = analyze_field_positions_from_instances(pattern_instances); + } + } +} + +/// Recursively traverse tree and collect pattern instances with accumulated metric names. +fn collect_pattern_instances( + node: &TreeNode, + accumulated_name: &str, + instances: &mut HashMap>, + pattern_lookup: &HashMap, String>, +) { + if let TreeNode::Branch(children) = node { + // Check if this branch matches a pattern + let fields = get_node_fields_for_analysis(children, pattern_lookup); + if let Some(pattern_name) = pattern_lookup.get(&fields) { + // Collect instances for this pattern + for (field_name, child_node) in children { + if let TreeNode::Leaf(leaf) = child_node { + instances + .entry(pattern_name.clone()) + .or_default() + .push((accumulated_name.to_string(), field_name.clone(), leaf.name().to_string())); + } + } + } + + // Continue traversing children + for (field_name, child_node) in children { + let child_accumulated = match child_node { + TreeNode::Leaf(leaf) => leaf.name().to_string(), + TreeNode::Branch(_) => { + // For branches, we need to infer the accumulated name + // If there's a leaf descendant, use its name as the basis + if let Some(desc_leaf_name) = get_descendant_leaf_name(child_node) { + // Try to extract what this level contributes + infer_accumulated_name(accumulated_name, field_name, &desc_leaf_name) + } else { + // No descendants - use field name as base + if accumulated_name.is_empty() { + field_name.clone() + } else { + format!("{}_{}", accumulated_name, field_name) + } + } + } + }; + collect_pattern_instances(child_node, &child_accumulated, instances, pattern_lookup); + } + } +} + +/// Get a descendant leaf name from a branch node (first one found) +fn get_descendant_leaf_name(node: &TreeNode) -> Option { + match node { + TreeNode::Leaf(leaf) => Some(leaf.name().to_string()), + TreeNode::Branch(children) => { + for child in children.values() { + if let Some(name) = get_descendant_leaf_name(child) { + return Some(name); + } + } + None + } + } +} + +/// Infer the accumulated name at this level by analyzing what part of the descendant's name +/// comes from the current field. +fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_leaf: &str) -> String { + // Try to find field_name in the descendant's metric name + if let Some(pos) = descendant_leaf.find(field_name) { + // Extract the part that corresponds to this level + if pos == 0 { + // Field is at the start + field_name.to_string() + } else if pos > 0 && descendant_leaf.chars().nth(pos - 1) == Some('_') { + // Field appears after underscore - this is likely an append + if parent_acc.is_empty() { + field_name.to_string() + } else { + format!("{}_{}", parent_acc, field_name) + } + } else { + field_name.to_string() + } + } else { + // Field name not directly found - use as is + if parent_acc.is_empty() { + field_name.to_string() + } else { + format!("{}_{}", parent_acc, field_name) + } + } +} + +/// Analyze instances to determine field positions (prepend/append/identity). +fn analyze_field_positions_from_instances( + instances: &[(String, String, String)], +) -> HashMap { + // Group by field name + let mut field_instances: HashMap> = HashMap::new(); + for (acc, field, leaf) in instances { + field_instances + .entry(field.clone()) + .or_default() + .push((acc.clone(), leaf.clone())); + } + + let mut positions = HashMap::new(); + + for (field_name, field_data) in field_instances { + if let Some(position) = detect_field_position(&field_data) { + positions.insert(field_name, position); + } + } + + positions +} + +/// Detect the position transformation for a field based on (accumulated, leaf_name) pairs. +fn detect_field_position(data: &[(String, String)]) -> Option { + if data.is_empty() { + return None; + } + + // Try to detect pattern from first instance, then validate against others + let (first_acc, first_leaf) = &data[0]; + + // Case 1: Identity - leaf == accumulated + if first_acc == first_leaf { + return Some(FieldNamePosition::Identity); + } + + // Case 2: Append - leaf = acc + suffix + if let Some(suffix) = first_leaf.strip_prefix(first_acc.as_str()) { + let suffix = suffix.to_string(); + // Validate this pattern holds for all instances + if data.iter().all(|(acc, leaf)| { + if acc.is_empty() { + // When acc is empty, leaf should equal suffix (without leading _) + leaf == suffix.trim_start_matches('_') + } else { + leaf.strip_prefix(acc.as_str()) == Some(&suffix) + } + }) { + return Some(FieldNamePosition::Append(suffix)); + } + } + + // Case 3: Prepend - leaf = prefix + acc + if let Some(prefix) = first_leaf.strip_suffix(first_acc.as_str()) { + let prefix = prefix.to_string(); + // Validate this pattern holds for all instances + if data.iter().all(|(acc, leaf)| { + if acc.is_empty() { + // When acc is empty, leaf should equal prefix (without trailing _) + leaf == prefix.trim_end_matches('_') + } else { + leaf.strip_suffix(acc.as_str()) == Some(&prefix) + } + }) { + return Some(FieldNamePosition::Prepend(prefix)); + } + } + + // Case 4: SetBase - the field name IS the metric base + // This happens at entry points where accumulated is empty + if first_acc.is_empty() { + return Some(FieldNamePosition::SetBase(first_leaf.clone())); + } + + None +} + +/// Get node fields for pattern matching during analysis +fn get_node_fields_for_analysis( + children: &BTreeMap, + pattern_lookup: &HashMap, String>, +) -> Vec { + let mut fields: Vec = children + .iter() + .map(|(name, node)| { + let (rust_type, json_type, indexes) = match node { + TreeNode::Leaf(leaf) => ( + leaf.value_type().to_string(), + schema_to_json_type(&leaf.schema), + leaf.indexes().clone(), + ), + TreeNode::Branch(grandchildren) => { + let child_fields = get_node_fields_for_analysis(grandchildren, pattern_lookup); + let pattern_name = pattern_lookup + .get(&child_fields) + .cloned() + .unwrap_or_else(|| "Unknown".to_string()); + (pattern_name.clone(), pattern_name, BTreeSet::new()) + } + }; + PatternField { + name: name.clone(), + rust_type, + json_type, + indexes, + } + }) + .collect(); + fields.sort_by(|a, b| a.name.cmp(&b.name)); + fields +} + /// Recursively resolve branch patterns bottom-up. /// Returns the pattern name for this node if it's a branch, or None if it's a leaf. fn resolve_branch_patterns( @@ -301,6 +567,35 @@ pub fn to_camel_case(s: &str) -> String { } } +/// Get the first leaf name from a tree node (used across all generators) +pub fn get_first_leaf_name(node: &TreeNode) -> Option { + match node { + TreeNode::Leaf(leaf) => Some(leaf.name().to_string()), + TreeNode::Branch(children) => { + for child in children.values() { + if let Some(name) = get_first_leaf_name(child) { + return Some(name); + } + } + None + } + } +} + +/// Get the metric base for a pattern instance by analyzing the first leaf descendant. +/// This extracts the common base that all leaves in this pattern instance share. +pub fn get_pattern_instance_base(node: &TreeNode, field_name: &str) -> String { + if let Some(leaf_name) = get_first_leaf_name(node) { + // Look for field_name in the leaf metric name + if leaf_name.contains(field_name) { + // The field name is part of the metric - use it as base + return field_name.to_string(); + } + } + // Fallback: use field name + field_name.to_string() +} + /// Detect index patterns - collect all indexes and find sets that appear 2+ times fn detect_index_patterns(tree: &TreeNode) -> (BTreeSet, Vec) { let mut used_indexes: BTreeSet = BTreeSet::new(); diff --git a/crates/brk_server/src/lib.rs b/crates/brk_server/src/lib.rs index 462266e57..0d6a97f02 100644 --- a/crates/brk_server/src/lib.rs +++ b/crates/brk_server/src/lib.rs @@ -1,6 +1,6 @@ #![doc = include_str!("../README.md")] -use std::{path::PathBuf, sync::Arc, time::Duration}; +use std::{panic, path::PathBuf, sync::Arc, time::Duration}; use aide::axum::ApiRouter; use axum::{ @@ -143,10 +143,14 @@ impl Server { .join("clients"); if clients_path.exists() { let openapi_json = serde_json::to_string(&openapi).unwrap(); - if let Err(e) = brk_binder::generate_clients(vecs, &openapi_json, &clients_path) { - error!("Failed to generate clients: {e}"); - } else { - info!("Generated clients at {}", clients_path.display()); + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + brk_binder::generate_clients(vecs, &openapi_json, &clients_path) + })); + + match result { + Ok(Ok(())) => info!("Generated clients at {}", clients_path.display()), + Ok(Err(e)) => error!("Failed to generate clients: {e}"), + Err(_) => error!("Client generation panicked"), } }