mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-11 07:23:32 -07:00
binder: snapshot
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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("_")))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user