binder: snapshot

This commit is contained in:
nym21
2025-12-20 23:24:24 +01:00
parent 71f45479b9
commit 135a18d56f
5 changed files with 487 additions and 144 deletions
+48 -26
View File
@@ -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> -> 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<PatternField> = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect();
let fields: Vec<PatternField> = 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,
+28 -1
View File
@@ -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<Dollars>")
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);
}
}
}
}
+82 -25
View File
@@ -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> -> 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<PatternField> = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect();
let fields: Vec<PatternField> = 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 {
+198 -59
View File
@@ -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> -> 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<PatternField> = fields_with_child_info.iter().map(|(f, _)| f.clone()).collect();
let fields: Vec<PatternField> = 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<Txid>")
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(),
}
}
}
+131 -33
View File
@@ -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<Dollars>`, extracts the inner type `Dollars`.
pub fn get_generic_value_type(&self, pattern_name: &str, fields: &[PatternField]) -> Option<String> {
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>` -> `Dollars`.
/// Also handles malformed types like `Dollars>` (from vecdb's short_type_name which
/// extracts "Dollars>" from "Close<brk_types::Dollars>" 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>` -> `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<StructuralPattern>, HashM
let mut signature_to_pattern: HashMap<Vec<PatternField>, String> = HashMap::new();
// Count how many times each signature appears
let mut signature_counts: HashMap<Vec<PatternField>, usize> = HashMap::new();
// Map normalized signatures to names (so patterns differing only in value type share names)
let mut normalized_to_name: HashMap<Vec<PatternField>, String> = HashMap::new();
// Track name usage to append index for duplicates
let mut name_counts: HashMap<String, usize> = 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<Vec<PatternField>, String>,
signature_counts: &mut HashMap<Vec<PatternField>, usize>,
normalized_to_name: &mut HashMap<Vec<PatternField>, String>, // Normalized sig -> name
name_counts: &mut HashMap<String, usize>,
) -> Option<String> {
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<PatternField> {
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, usize>) -> 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::<String>() + 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<Index>, Vec<IndexSetPatte
let mut patterns: Vec<IndexSetPattern> = 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<Index>) -> 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("_")))
}