From 135a18d56f1277ac045b0e47bb080508db35245d Mon Sep 17 00:00:00 2001 From: nym21 Date: Sat, 20 Dec 2025 23:24:24 +0100 Subject: [PATCH] binder: snapshot --- crates/brk_binder/src/javascript.rs | 74 +++++--- crates/brk_binder/src/lib.rs | 29 +++- crates/brk_binder/src/python.rs | 107 +++++++++--- crates/brk_binder/src/rust.rs | 257 +++++++++++++++++++++------- crates/brk_binder/src/types.rs | 164 ++++++++++++++---- 5 files changed, 487 insertions(+), 144 deletions(-) diff --git a/crates/brk_binder/src/javascript.rs b/crates/brk_binder/src/javascript.rs index 6a8419f8f..2b52a4628 100644 --- a/crates/brk_binder/src/javascript.rs +++ b/crates/brk_binder/src/javascript.rs @@ -9,8 +9,8 @@ use serde_json::Value; use crate::{ ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, - TypeSchemas, get_first_leaf_name, get_node_fields, get_pattern_instance_base, to_camel_case, - to_pascal_case, + TypeSchemas, extract_inner_type, 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 @@ -364,7 +364,11 @@ fn generate_structural_patterns( writeln!(output, " * @returns {{{}}}", pattern.name).unwrap(); writeln!(output, " */").unwrap(); - let param_name = if is_parameterizable { "acc" } else { "basePath" }; + let param_name = if is_parameterizable { + "acc" + } else { + "basePath" + }; writeln!( output, "function create{}(client, {}) {{", @@ -510,18 +514,19 @@ fn field_to_js_type_with_generic_value( generic_value_type: Option<&str>, ) -> String { // For generic patterns, use T instead of concrete value type + // Also extract inner type from wrappers like Close -> Dollars let value_type = if is_generic && field.rust_type == "T" { "T".to_string() } else { - field.rust_type.clone() + extract_inner_type(&field.rust_type) }; if metadata.is_pattern_type(&field.rust_type) { // Check if this pattern is generic and we have a value type - if metadata.is_pattern_generic(&field.rust_type) { - if let Some(vt) = generic_value_type { - return format!("{}<{}>", field.rust_type, vt); - } + 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) { @@ -571,7 +576,11 @@ fn generate_tree_typedef( let (rust_type, json_type, indexes, child_fields) = match child_node { TreeNode::Leaf(leaf) => ( leaf.value_type().to_string(), - leaf.schema.get("type").and_then(|v| v.as_str()).unwrap_or("object").to_string(), + leaf.schema + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("object") + .to_string(), leaf.indexes().clone(), None, ), @@ -581,19 +590,30 @@ fn generate_tree_typedef( .get(&child_fields) .cloned() .unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name))); - (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new(), Some(child_fields)) + ( + pattern_name.clone(), + pattern_name, + std::collections::BTreeSet::new(), + Some(child_fields), + ) } }; - (PatternField { - name: child_name.clone(), - rust_type, - json_type, - indexes, - }, child_fields) + ( + PatternField { + name: child_name.clone(), + rust_type, + json_type, + indexes, + }, + child_fields, + ) }) .collect(); - let fields: Vec = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect(); + let fields: Vec = fields_with_child_info + .iter() + .map(|(f, _)| f.clone()) + .collect(); // Skip if this matches a pattern (already generated) if pattern_lookup.contains_key(&fields) @@ -612,10 +632,15 @@ fn generate_tree_typedef( for (field, child_fields) in &fields_with_child_info { // For generic patterns, extract the value type from child fields - let generic_value_type = child_fields.as_ref().and_then(|cf| { - metadata.get_generic_value_type(&field.rust_type, cf) - }); - let js_type = field_to_js_type_with_generic_value(field, metadata, false, generic_value_type.as_deref()); + let generic_value_type = child_fields + .as_ref() + .and_then(|cf| metadata.get_generic_value_type(&field.rust_type, cf)); + let js_type = field_to_js_type_with_generic_value( + field, + metadata, + false, + generic_value_type.as_deref(), + ); writeln!( output, " * @property {{{}}} {}", @@ -766,11 +791,8 @@ fn generate_tree_initializer( .unwrap(); } else { // Not a pattern - recurse with accumulated name - let child_acc = infer_child_accumulated_name( - child_node, - accumulated_name, - child_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, diff --git a/crates/brk_binder/src/lib.rs b/crates/brk_binder/src/lib.rs index a6246c45b..3f6bf991d 100644 --- a/crates/brk_binder/src/lib.rs +++ b/crates/brk_binder/src/lib.rs @@ -25,7 +25,10 @@ pub fn generate_clients(vecs: &Vecs, openapi_json: &str, output_dir: &Path) -> i // Parse OpenAPI spec let spec = parse_openapi_json(openapi_json)?; let endpoints = extract_endpoints(&spec); - let schemas = extract_schemas(openapi_json); + let mut schemas = extract_schemas(openapi_json); + + // Collect leaf type schemas from the catalog and merge into schemas + collect_leaf_type_schemas(&metadata.catalog, &mut schemas); // Generate Rust client (uses real brk_types, no schema conversion needed) let rust_path = output_dir.join("rust"); @@ -44,3 +47,27 @@ pub fn generate_clients(vecs: &Vecs, openapi_json: &str, output_dir: &Path) -> i Ok(()) } + +use brk_types::TreeNode; + +/// Recursively collect leaf type schemas from the tree and add to schemas map. +/// Only adds schemas that aren't already present (OpenAPI schemas take precedence). +fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) { + match node { + TreeNode::Leaf(leaf) => { + // Extract the inner type name (e.g., "Dollars" from "Close") + let type_name = extract_inner_type(leaf.value_type()); + + // Only add if not already present (OpenAPI schemas take precedence) + if !schemas.contains_key(&type_name) { + // The leaf schema is the schemars-generated JSON schema + schemas.insert(type_name, leaf.schema.clone()); + } + } + TreeNode::Branch(children) => { + for child in children.values() { + collect_leaf_type_schemas(child, schemas); + } + } + } +} diff --git a/crates/brk_binder/src/python.rs b/crates/brk_binder/src/python.rs index 9700d9eda..4f75236cd 100644 --- a/crates/brk_binder/src/python.rs +++ b/crates/brk_binder/src/python.rs @@ -9,7 +9,8 @@ use serde_json::Value; use crate::{ ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, - TypeSchemas, get_node_fields, get_pattern_instance_base, to_pascal_case, to_snake_case, + TypeSchemas, extract_inner_type, get_node_fields, get_pattern_instance_base, to_pascal_case, + to_snake_case, }; /// Generate Python client from metadata and OpenAPI endpoints @@ -35,7 +36,7 @@ pub fn generate_python_client( // Type variable for generic MetricNode writeln!(output, "T = TypeVar('T')\n").unwrap(); - // Generate type definitions from OpenAPI schemas + // Generate type definitions from OpenAPI schemas (now includes leaf types from catalog) generate_type_definitions(&mut output, schemas); // Generate base client class @@ -75,7 +76,8 @@ fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) { writeln!(output, "class {}(TypedDict):", name).unwrap(); for (prop_name, prop_schema) in props { let prop_type = schema_to_python_type(prop_schema); - writeln!(output, " {}: {}", prop_name, prop_type).unwrap(); + let safe_name = escape_python_keyword(prop_name); + writeln!(output, " {}: {}", safe_name, prop_type).unwrap(); } writeln!(output).unwrap(); } else { @@ -127,6 +129,21 @@ fn schema_to_python_type(schema: &Value) -> String { "Any".to_string() } +/// Escape Python reserved keywords by appending underscore +fn escape_python_keyword(name: &str) -> String { + const PYTHON_KEYWORDS: &[&str] = &[ + "False", "None", "True", "and", "as", "assert", "async", "await", "break", "class", + "continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global", + "if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return", + "try", "while", "with", "yield", + ]; + if PYTHON_KEYWORDS.contains(&name) { + format!("{}_", name) + } else { + name.to_string() + } +} + /// Generate the base BrkClient class with HTTP functionality fn generate_base_client(output: &mut String) { writeln!( @@ -274,7 +291,11 @@ fn generate_structural_patterns( " def __init__(self, client: BrkClientBase, acc: str):" ) .unwrap(); - writeln!(output, " \"\"\"Create pattern node with accumulated metric name.\"\"\"").unwrap(); + writeln!( + output, + " \"\"\"Create pattern node with accumulated metric name.\"\"\"" + ) + .unwrap(); } else { writeln!( output, @@ -415,18 +436,19 @@ fn field_to_python_type_with_generic_value( generic_value_type: Option<&str>, ) -> String { // For generic patterns, use T instead of concrete value type + // Also extract inner type from wrappers like Close -> Dollars let value_type = if is_generic && field.rust_type == "T" { "T".to_string() } else { - field.rust_type.clone() + extract_inner_type(&field.rust_type) }; if metadata.is_pattern_type(&field.rust_type) { // Check if this pattern is generic and we have a value type - if metadata.is_pattern_generic(&field.rust_type) { - if let Some(vt) = generic_value_type { - return format!("{}[{}]", field.rust_type, vt); - } + 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) { @@ -476,7 +498,11 @@ fn generate_tree_class( let (rust_type, json_type, indexes, child_fields) = match child_node { TreeNode::Leaf(leaf) => ( leaf.value_type().to_string(), - leaf.schema.get("type").and_then(|v| v.as_str()).unwrap_or("object").to_string(), + leaf.schema + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("object") + .to_string(), leaf.indexes().clone(), None, ), @@ -486,19 +512,30 @@ fn generate_tree_class( .get(&child_fields) .cloned() .unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name))); - (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new(), Some(child_fields)) + ( + pattern_name.clone(), + pattern_name, + std::collections::BTreeSet::new(), + Some(child_fields), + ) } }; - (PatternField { - name: child_name.clone(), - rust_type, - json_type, - indexes, - }, child_fields) + ( + PatternField { + name: child_name.clone(), + rust_type, + json_type, + indexes, + }, + child_fields, + ) }) .collect(); - let fields: Vec = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect(); + let fields: Vec = fields_with_child_info + .iter() + .map(|(f, _)| f.clone()) + .collect(); // Skip if this matches a pattern (already generated) if pattern_lookup.contains_key(&fields) @@ -521,12 +558,19 @@ fn generate_tree_class( ) .unwrap(); - for ((field, child_fields_opt), (child_name, child_node)) in fields_with_child_info.iter().zip(children.iter()) { + for ((field, child_fields_opt), (child_name, child_node)) in + fields_with_child_info.iter().zip(children.iter()) + { // For generic patterns, extract the value type from child fields - let generic_value_type = child_fields_opt.as_ref().and_then(|cf| { - metadata.get_generic_value_type(&field.rust_type, cf) - }); - let py_type = field_to_python_type_with_generic_value(field, metadata, false, generic_value_type.as_deref()); + let generic_value_type = child_fields_opt + .as_ref() + .and_then(|cf| metadata.get_generic_value_type(&field.rust_type, cf)); + let py_type = field_to_python_type_with_generic_value( + field, + metadata, + false, + generic_value_type.as_deref(), + ); let field_name_py = to_snake_case(&field.name); if metadata.is_pattern_type(&field.rust_type) { @@ -654,7 +698,11 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { } let method_name = endpoint_to_method_name(endpoint); - let return_type = endpoint.response_type.as_deref().unwrap_or("Any"); + let return_type = endpoint + .response_type + .as_deref() + .map(js_type_to_python) + .unwrap_or_else(|| "Any".to_string()); // Build method signature let params = build_method_params(endpoint); @@ -716,7 +764,16 @@ fn endpoint_to_method_name(endpoint: &Endpoint) -> String { .split('/') .filter(|s| !s.is_empty() && !s.starts_with('{')) .collect(); - format!("get_{}", parts.join("_")) + to_snake_case(&format!("get_{}", parts.join("_"))) +} + +/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]") +fn js_type_to_python(js_type: &str) -> String { + if let Some(inner) = js_type.strip_suffix("[]") { + format!("List[{}]", js_type_to_python(inner)) + } else { + js_type.to_string() + } } fn build_method_params(endpoint: &Endpoint) -> String { diff --git a/crates/brk_binder/src/rust.rs b/crates/brk_binder/src/rust.rs index 01146002c..d5188e393 100644 --- a/crates/brk_binder/src/rust.rs +++ b/crates/brk_binder/src/rust.rs @@ -6,7 +6,10 @@ use std::path::Path; use brk_types::{Index, TreeNode}; -use crate::{ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, get_node_fields, get_pattern_instance_base, to_pascal_case, to_snake_case}; +use crate::{ + ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern, + extract_inner_type, 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( @@ -185,7 +188,12 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) { 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, + "/// Index accessor for metrics with {} indexes.", + pattern.indexes.len() + ) + .unwrap(); writeln!(output, "pub struct {}<'a, T> {{", pattern.name).unwrap(); for index in &pattern.indexes { @@ -197,8 +205,17 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) { writeln!(output, "}}\n").unwrap(); // Generate impl block with constructor - writeln!(output, "impl<'a, T: DeserializeOwned> {}<'a, T> {{", pattern.name).unwrap(); - writeln!(output, " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{").unwrap(); + writeln!( + output, + "impl<'a, T: DeserializeOwned> {}<'a, T> {{", + pattern.name + ) + .unwrap(); + writeln!( + output, + " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{" + ) + .unwrap(); writeln!(output, " Self {{").unwrap(); for index in &pattern.indexes { @@ -208,7 +225,8 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) { output, " {}: MetricNode::new(client, format!(\"{{base_path}}/{}\")),", field_name, path_segment - ).unwrap(); + ) + .unwrap(); } writeln!(output, " _marker: PhantomData,").unwrap(); @@ -224,7 +242,11 @@ fn index_to_field_name(index: &Index) -> String { } /// Generate pattern structs (those appearing 2+ times) -fn generate_pattern_structs(output: &mut String, patterns: &[StructuralPattern], metadata: &ClientMetadata) { +fn generate_pattern_structs( + output: &mut String, + patterns: &[StructuralPattern], + metadata: &ClientMetadata, +) { if patterns.is_empty() { return; } @@ -233,27 +255,49 @@ fn generate_pattern_structs(output: &mut String, patterns: &[StructuralPattern], for pattern in patterns { let is_parameterizable = pattern.is_parameterizable(); - let generic_params = if pattern.is_generic { "<'a, T>" } else { "<'a>" }; + let generic_params = if pattern.is_generic { + "<'a, T>" + } else { + "<'a>" + }; 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); + 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 - writeln!(output, "impl{} {}{} {{", generic_params, pattern.name, generic_params).unwrap(); + writeln!( + output, + "impl{} {}{} {{", + generic_params, 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: &'a BrkClientBase, acc: &str) -> Self {{").unwrap(); + 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, + " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{" + ) + .unwrap(); } writeln!(output, " Self {{").unwrap(); @@ -297,7 +341,8 @@ fn generate_parameterized_rust_field( output, " {}: {}::new(client, {}),", field_name, field.rust_type, child_acc - ).unwrap(); + ) + .unwrap(); return; } @@ -319,13 +364,15 @@ fn generate_parameterized_rust_field( output, " {}: {}::new(client, &{}),", field_name, accessor.name, metric_expr - ).unwrap(); + ) + .unwrap(); } else { writeln!( output, " {}: MetricNode::new(client, {}),", field_name, metric_expr - ).unwrap(); + ) + .unwrap(); } } @@ -342,20 +389,23 @@ fn generate_tree_path_rust_field( output, " {}: {}::new(client, &format!(\"{{base_path}}/{}\")),", field_name, field.rust_type, field.name - ).unwrap(); + ) + .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(); + ) + .unwrap(); } else { writeln!( output, " {}: MetricNode::new(client, format!(\"{{base_path}}/{}\")),", field_name, field.name - ).unwrap(); + ) + .unwrap(); } } @@ -378,18 +428,19 @@ fn field_to_type_annotation_with_generic( generic_value_type: Option<&str>, ) -> String { // For generic patterns, use T instead of concrete value type + // Also extract inner type from wrappers like Close -> Dollars let value_type = if is_generic && field.rust_type == "T" { "T".to_string() } else { - field.rust_type.clone() + extract_inner_type(&field.rust_type) }; if metadata.is_pattern_type(&field.rust_type) { // Check if this pattern is generic and we have a value type - if metadata.is_pattern_generic(&field.rust_type) { - if let Some(vt) = generic_value_type { - return format!("{}<'a, {}>", field.rust_type, vt); - } + if metadata.is_pattern_generic(&field.rust_type) + && let Some(vt) = generic_value_type + { + return format!("{}<'a, {}>", field.rust_type, vt); } format!("{}<'a>", field.rust_type) } else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) { @@ -407,16 +458,19 @@ fn field_uses_accessor(field: &PatternField, metadata: &ClientMetadata) -> bool } /// Generate the catalog tree structure -fn generate_tree( - output: &mut String, - catalog: &TreeNode, - metadata: &ClientMetadata, -) { +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); + generate_tree_node( + output, + "CatalogTree", + catalog, + &pattern_lookup, + metadata, + &mut generated, + ); } /// Recursively generate tree nodes @@ -436,7 +490,11 @@ fn generate_tree_node( let (rust_type, json_type, indexes, child_fields) = match child_node { TreeNode::Leaf(leaf) => ( leaf.value_type().to_string(), - leaf.schema.get("type").and_then(|v| v.as_str()).unwrap_or("object").to_string(), + leaf.schema + .get("type") + .and_then(|v| v.as_str()) + .unwrap_or("object") + .to_string(), leaf.indexes().clone(), None, ), @@ -447,20 +505,31 @@ fn generate_tree_node( .get(&child_fields) .cloned() .unwrap_or_else(|| format!("{}_{}", name, to_pascal_case(child_name))); - (pattern_name.clone(), pattern_name, std::collections::BTreeSet::new(), Some(child_fields)) + ( + pattern_name.clone(), + pattern_name, + std::collections::BTreeSet::new(), + Some(child_fields), + ) } }; - (PatternField { - name: child_name.clone(), - rust_type, - json_type, - indexes, - }, child_fields) + ( + PatternField { + name: child_name.clone(), + rust_type, + json_type, + indexes, + }, + child_fields, + ) }) .collect(); fields_with_child_info.sort_by(|a, b| a.0.name.cmp(&b.0.name)); - let fields: Vec = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect(); + let fields: Vec = fields_with_child_info + .iter() + .map(|(f, _)| f.clone()) + .collect(); // Check if this matches a reusable pattern if let Some(pattern_name) = pattern_lookup.get(&fields) { @@ -483,10 +552,15 @@ fn generate_tree_node( for (field, child_fields) in &fields_with_child_info { let field_name = to_snake_case(&field.name); // For generic patterns, extract the value type from child fields - let generic_value_type = child_fields.as_ref().and_then(|cf| { - metadata.get_generic_value_type(&field.rust_type, cf) - }); - let type_annotation = field_to_type_annotation_with_generic(field, metadata, false, generic_value_type.as_deref()); + let generic_value_type = child_fields + .as_ref() + .and_then(|cf| metadata.get_generic_value_type(&field.rust_type, cf)); + let type_annotation = field_to_type_annotation_with_generic( + field, + metadata, + false, + generic_value_type.as_deref(), + ); writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap(); } @@ -494,7 +568,11 @@ fn generate_tree_node( // Generate impl block writeln!(output, "impl<'a> {}<'a> {{", name).unwrap(); - writeln!(output, " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{").unwrap(); + writeln!( + output, + " pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{" + ) + .unwrap(); writeln!(output, " Self {{").unwrap(); for (field, (child_name, child_node)) in fields.iter().zip(children.iter()) { @@ -514,13 +592,15 @@ fn generate_tree_node( output, " {}: {}::new(client, \"{}\"),", field_name, field.rust_type, metric_base - ).unwrap(); + ) + .unwrap(); } else { writeln!( output, - " {}: {}::new(client, &format!(\"{{base_path}}/{}\"))," , + " {}: {}::new(client, &format!(\"{{base_path}}/{}\")),", field_name, field.rust_type, field.name - ).unwrap(); + ) + .unwrap(); } } else if field_uses_accessor(field, metadata) { // Leaf with accessor - get actual metric path from leaf @@ -535,13 +615,15 @@ fn generate_tree_node( output, " {}: {}::new(client, &format!(\"{}\")),", field_name, accessor.name, metric_path - ).unwrap(); + ) + .unwrap(); } else { writeln!( output, " {}: {}::new(client, \"{}\"),", field_name, accessor.name, metric_path - ).unwrap(); + ) + .unwrap(); } } else { // Leaf without accessor - get actual metric path from leaf @@ -555,13 +637,15 @@ fn generate_tree_node( output, " {}: MetricNode::new(client, format!(\"{}\")),", field_name, metric_path - ).unwrap(); + ) + .unwrap(); } else { writeln!( output, " {}: MetricNode::new(client, \"{}\".to_string()),", field_name, metric_path - ).unwrap(); + ) + .unwrap(); } } } @@ -576,7 +660,14 @@ fn generate_tree_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); + generate_tree_node( + output, + &child_struct_name, + child_node, + pattern_lookup, + metadata, + generated, + ); } } } @@ -629,14 +720,28 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { } let method_name = endpoint_to_method_name(endpoint); - let return_type = endpoint.response_type.as_deref().unwrap_or("serde_json::Value"); + let return_type = endpoint + .response_type + .as_deref() + .map(js_type_to_rust) + .unwrap_or_else(|| "serde_json::Value".to_string()); // Build doc comment - writeln!(output, " /// {}", endpoint.summary.as_deref().unwrap_or(&method_name)).unwrap(); + writeln!( + output, + " /// {}", + endpoint.summary.as_deref().unwrap_or(&method_name) + ) + .unwrap(); // Build method signature let params = build_method_params(endpoint); - writeln!(output, " pub fn {}(&self{}) -> Result<{}> {{", method_name, params, return_type).unwrap(); + writeln!( + output, + " pub fn {}(&self{}) -> Result<{}> {{", + method_name, params, return_type + ) + .unwrap(); // Build path let path = build_path_template(&endpoint.path, &endpoint.path_params); @@ -647,13 +752,28 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { 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(); + 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, + " 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, + " self.base.get(&format!(\"{}{{}}\", query_str))", + path + ) + .unwrap(); } writeln!(output, " }}\n").unwrap(); @@ -664,8 +784,12 @@ fn endpoint_to_method_name(endpoint: &Endpoint) -> String { if let Some(op_id) = &endpoint.operation_id { return to_snake_case(op_id); } - let parts: Vec<&str> = endpoint.path.split('/').filter(|s| !s.is_empty() && !s.starts_with('{')).collect(); - format!("get_{}", parts.join("_")) + let parts: Vec<&str> = endpoint + .path + .split('/') + .filter(|s| !s.is_empty() && !s.starts_with('{')) + .collect(); + to_snake_case(&format!("get_{}", parts.join("_"))) } fn build_method_params(endpoint: &Endpoint) -> String { @@ -692,3 +816,18 @@ fn build_path_template(path: &str, path_params: &[super::Parameter]) -> String { } result } + +/// Convert JS-style type to Rust type (e.g., "Txid[]" -> "Vec") +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(), + } + } +} diff --git a/crates/brk_binder/src/types.rs b/crates/brk_binder/src/types.rs index a672f91aa..e95b49cd3 100644 --- a/crates/brk_binder/src/types.rs +++ b/crates/brk_binder/src/types.rs @@ -152,6 +152,7 @@ impl ClientMetadata { /// Extract the value type from concrete fields for a generic pattern. /// Returns the first leaf field's rust_type if this pattern is generic. + /// If the type is a wrapper like `Close`, extracts the inner type `Dollars`. pub fn get_generic_value_type(&self, pattern_name: &str, fields: &[PatternField]) -> Option { if !self.is_pattern_generic(pattern_name) { return None; @@ -160,7 +161,7 @@ impl ClientMetadata { fields .iter() .find(|f| !f.indexes.is_empty()) - .map(|f| f.rust_type.clone()) + .map(|f| extract_inner_type(&f.rust_type)) } /// Build a lookup map from field signatures to pattern names. @@ -176,6 +177,27 @@ impl ClientMetadata { } } +/// Extract inner type from a wrapper generic like `Close` -> `Dollars`. +/// Also handles malformed types like `Dollars>` (from vecdb's short_type_name which +/// extracts "Dollars>" from "Close" using rsplit("::")). +/// If not a generic, returns the type as-is. +pub fn extract_inner_type(type_str: &str) -> String { + // Handle proper generic wrappers like `Close` -> `Dollars` + if let Some(start) = type_str.find('<') { + if let Some(end) = type_str.rfind('>') { + if start < end { + return type_str[start + 1..end].to_string(); + } + } + } + // Handle malformed types like `Dollars>` (trailing > without <) + // This happens due to vecdb's short_type_name using rsplit("::") + if type_str.ends_with('>') && !type_str.contains('<') { + return type_str.trim_end_matches('>').to_string(); + } + type_str.to_string() +} + /// Detect structural patterns in the tree using a bottom-up approach. /// For every branch node, create a signature from its children (sorted field names + types). /// Patterns that appear 2+ times are deduplicated. @@ -185,9 +207,20 @@ fn detect_structural_patterns(tree: &TreeNode) -> (Vec, HashM let mut signature_to_pattern: HashMap, String> = HashMap::new(); // Count how many times each signature appears let mut signature_counts: HashMap, usize> = HashMap::new(); + // Map normalized signatures to names (so patterns differing only in value type share names) + let mut normalized_to_name: HashMap, String> = HashMap::new(); + // Track name usage to append index for duplicates + let mut name_counts: HashMap = HashMap::new(); // Process tree bottom-up to resolve all branch types - resolve_branch_patterns(tree, &mut signature_to_pattern, &mut signature_counts); + resolve_branch_patterns( + tree, + "root", + &mut signature_to_pattern, + &mut signature_counts, + &mut normalized_to_name, + &mut name_counts, + ); // First, identify generic patterns by grouping ALL signatures by their normalized form. // Even if each concrete signature appears only once, if 2+ different value types @@ -551,8 +584,11 @@ fn get_node_fields_for_analysis( /// Returns the pattern name for this node if it's a branch, or None if it's a leaf. fn resolve_branch_patterns( node: &TreeNode, + field_name: &str, // The field name in the parent where this node appears signature_to_pattern: &mut HashMap, String>, signature_counts: &mut HashMap, usize>, + normalized_to_name: &mut HashMap, String>, // Normalized sig -> name + name_counts: &mut HashMap, ) -> Option { match node { TreeNode::Leaf(_) => { @@ -574,8 +610,11 @@ fn resolve_branch_patterns( // Branch: recursively get its pattern name let pattern_name = resolve_branch_patterns( child_node, + child_name, signature_to_pattern, signature_counts, + normalized_to_name, + name_counts, ) .unwrap_or_else(|| "Unknown".to_string()); (pattern_name.clone(), pattern_name, BTreeSet::new()) @@ -596,37 +635,74 @@ fn resolve_branch_patterns( // Increment count for this signature *signature_counts.entry(fields.clone()).or_insert(0) += 1; - // Get or create pattern name for this signature - let pattern_name = signature_to_pattern - .entry(fields.clone()) - .or_insert_with(|| generate_pattern_name_from_fields(&fields)) - .clone(); + // Get or create pattern name - use normalized signature for naming + // so patterns that differ only in value type get the same name + let pattern_name = if let Some(existing) = signature_to_pattern.get(&fields) { + existing.clone() + } else { + // Check if normalized form already has a name + let normalized = normalize_fields_for_naming(&fields); + let name = normalized_to_name + .entry(normalized) + .or_insert_with(|| generate_pattern_name(field_name, name_counts)) + .clone(); + signature_to_pattern.insert(fields.clone(), name.clone()); + name + }; Some(pattern_name) } } } -/// Generate a sanitized pattern name from fields. -/// Names must be valid identifiers in all target languages (Rust, JS, Python). -fn generate_pattern_name_from_fields(fields: &[PatternField]) -> String { - // Join field names with underscores, then convert to PascalCase - let joined: Vec<&str> = fields.iter().map(|f| f.name.as_str()).collect(); - let raw_name = joined.join("_"); +/// Normalize fields for naming: replace value types with a placeholder +/// so patterns with same structure but different value types get the same name. +fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec { + fields + .iter() + .map(|f| { + if f.indexes.is_empty() { + // Branch field - keep rust_type (it's a pattern name) + f.clone() + } else { + // Leaf field - normalize value type + PatternField { + name: f.name.clone(), + rust_type: "_".to_string(), + json_type: "_".to_string(), + indexes: f.indexes.clone(), + } + } + }) + .collect() +} - // Sanitize: ensure it starts with a letter (prepend "P_" if starts with digit) - let sanitized = if raw_name +/// Generate a pattern name from the field name where it's used. +/// Appends an index if the same base name is used multiple times. +fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap) -> String { + let pascal = to_pascal_case(field_name); + + // Sanitize: ensure it starts with a letter (prepend "_" if starts with digit) + let base_name = if pascal .chars() .next() .map(|c| c.is_ascii_digit()) .unwrap_or(false) { - format!("P_{}", raw_name) + format!("_{}", pascal) } else { - raw_name + pascal }; - to_pascal_case(&sanitized) + // Track usage count and append index if needed + let count = name_counts.entry(base_name.clone()).or_insert(0); + *count += 1; + + if *count == 1 { + base_name + } else { + format!("{}{}", base_name, count) + } } /// Extract JSON type from JSON Schema @@ -675,7 +751,10 @@ pub fn get_node_fields( /// Convert a metric name to PascalCase (for struct/class names) pub fn to_pascal_case(s: &str) -> String { - s.split('_') + // Normalize separators: replace - with _ + let normalized = s.replace('-', "_"); + normalized + .split('_') .map(|word| { let mut chars = word.chars(); match chars.next() { @@ -689,6 +768,19 @@ pub fn to_pascal_case(s: &str) -> String { /// Convert a metric name to snake_case (already snake_case, but sanitize) pub fn to_snake_case(s: &str) -> String { let sanitized = s.replace('-', "_"); + + // Prefix with _ if starts with digit + let sanitized = if sanitized + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { + format!("_{}", sanitized) + } else { + sanitized + }; + // Handle Rust keywords match sanitized.as_str() { "type" | "const" | "static" | "match" | "if" | "else" | "loop" | "while" | "for" @@ -703,9 +795,21 @@ pub fn to_snake_case(s: &str) -> String { pub fn to_camel_case(s: &str) -> String { let pascal = to_pascal_case(s); let mut chars = pascal.chars(); - match chars.next() { + let result = match chars.next() { None => String::new(), Some(first) => first.to_lowercase().collect::() + chars.as_str(), + }; + + // Prefix with _ if starts with digit + if result + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { + format!("_{}", result) + } else { + result } } @@ -760,8 +864,13 @@ fn detect_index_patterns(tree: &TreeNode) -> (BTreeSet, Vec = index_set_counts .into_iter() .filter(|(indexes, count)| *count >= 2 && !indexes.is_empty()) - .map(|(indexes, _)| IndexSetPattern { - name: generate_index_set_name(&indexes), + .enumerate() + .map(|(i, (indexes, _))| IndexSetPattern { + name: if i == 0 { + "Indexes".to_string() + } else { + format!("Indexes{}", i + 1) + }, indexes, }) .collect(); @@ -793,14 +902,3 @@ fn collect_indexes_from_tree( } } -/// Generate a name for an index set pattern -fn generate_index_set_name(indexes: &BTreeSet) -> String { - if indexes.len() == 1 { - let index = indexes.iter().next().unwrap(); - return format!("{}Accessor", to_pascal_case(index.serialize_long())); - } - - // For multiple indexes, create a descriptive name - let names: Vec<&str> = indexes.iter().map(|i| i.serialize_long()).collect(); - format!("{}Accessor", to_pascal_case(&names.join("_"))) -}