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