global: snapshot

This commit is contained in:
nym21
2026-01-11 18:55:40 +01:00
parent ea70c381de
commit 69f6d32d4a
32 changed files with 6111 additions and 10390 deletions

View File

@@ -7,8 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
use brk_types::{Index, TreeNode, extract_json_type};
use crate::analysis::names::analyze_pattern_level;
use crate::{IndexSetPattern, PatternField, child_type_name};
use crate::{IndexSetPattern, PatternField, analysis::names::analyze_pattern_level, child_type_name};
/// Get the first leaf name from a tree node.
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
@@ -111,122 +110,29 @@ fn collect_indexes_from_tree(
}
}
/// Get the metric base for a pattern instance by analyzing all leaf descendants.
/// Get the metric base for a pattern instance by analyzing direct children.
///
/// For root-level instances (no common prefix/suffix among leaves), returns empty string.
/// For cohort-level instances, returns the common prefix or suffix among all leaves.
/// Uses field names and first leaf names from direct children to determine
/// the common base via `analyze_pattern_level`.
pub fn get_pattern_instance_base(node: &TreeNode) -> String {
let leaf_names = get_all_leaf_names(node);
find_common_base(&leaf_names)
let child_names = get_direct_children_for_analysis(node);
if child_names.is_empty() {
return String::new();
}
analyze_pattern_level(&child_names).base
}
/// Find the common base from a set of metric names.
/// Tries prefix, suffix, then strips first/last segments and retries.
fn find_common_base(names: &[String]) -> String {
if names.is_empty() {
return String::new();
/// Get (field_name, first_leaf_name) pairs for direct children of a branch node.
fn get_direct_children_for_analysis(node: &TreeNode) -> Vec<(String, String)> {
match node {
TreeNode::Leaf(leaf) => vec![(leaf.name().to_string(), leaf.name().to_string())],
TreeNode::Branch(children) => children
.iter()
.filter_map(|(field_name, child)| {
get_first_leaf_name(child).map(|leaf_name| (field_name.clone(), leaf_name))
})
.collect(),
}
// Try common prefix
let common_prefix = find_common_prefix_at_underscore(names);
if !common_prefix.is_empty() {
return common_prefix.trim_end_matches('_').to_string();
}
// Try common suffix
let common_suffix = find_common_suffix_at_underscore(names);
if !common_suffix.is_empty() {
return common_suffix.trim_start_matches('_').to_string();
}
// If neither works, the common part may be in the middle.
// Strip the first underscore segment (varying prefix) and try again.
let stripped_prefix: Vec<String> = names
.iter()
.filter_map(|name| name.split_once('_').map(|(_, rest)| rest.to_string()))
.collect();
if stripped_prefix.len() == names.len() {
let common_prefix = find_common_prefix_at_underscore(&stripped_prefix);
if !common_prefix.is_empty() {
return common_prefix.trim_end_matches('_').to_string();
}
}
// Try stripping last segment (varying suffix) and look for common suffix
let stripped_suffix: Vec<String> = names
.iter()
.filter_map(|name| name.rsplit_once('_').map(|(rest, _)| rest.to_string()))
.collect();
if stripped_suffix.len() == names.len() {
let common_suffix = find_common_suffix_at_underscore(&stripped_suffix);
if !common_suffix.is_empty() {
return common_suffix.trim_start_matches('_').to_string();
}
}
String::new()
}
/// Find the longest common prefix at an underscore boundary.
fn find_common_prefix_at_underscore(names: &[String]) -> String {
if names.is_empty() {
return String::new();
}
let first = &names[0];
if first.is_empty() {
return String::new();
}
// Find character-by-character common prefix
let mut prefix_len = 0;
for (i, ch) in first.chars().enumerate() {
if names.iter().all(|n| n.chars().nth(i) == Some(ch)) {
prefix_len = i + 1;
} else {
break;
}
}
if prefix_len == 0 {
return String::new();
}
let raw_prefix = &first[..prefix_len];
// If raw_prefix exactly matches a leaf name, it's a complete metric name.
// In this case, return it with trailing underscore (will be trimmed by caller).
if names.iter().any(|n| n == raw_prefix) {
return format!("{}_", raw_prefix);
}
// Find the last underscore position to get a clean boundary
if let Some(last_underscore) = raw_prefix.rfind('_')
&& last_underscore > 0
{
let clean_prefix = &first[..=last_underscore];
// Verify this still works for all names
if names.iter().all(|n| n.starts_with(clean_prefix)) {
return clean_prefix.to_string();
}
}
// If no underscore boundary works, check if full prefix ends at underscore
if raw_prefix.ends_with('_') {
return raw_prefix.to_string();
}
String::new()
}
/// Find the longest common suffix at an underscore boundary.
fn find_common_suffix_at_underscore(names: &[String]) -> String {
// Reverse strings, find common prefix, reverse result
let reversed: Vec<String> = names.iter().map(|s| s.chars().rev().collect()).collect();
let prefix = find_common_prefix_at_underscore(&reversed);
prefix.chars().rev().collect()
}
/// Infer the accumulated name for a child node based on a descendant leaf name.

View File

@@ -96,4 +96,8 @@ impl LanguageSyntax for JavaScriptSyntax {
fn string_literal(&self, value: &str) -> String {
format!("'{}'", value)
}
fn constructor_name(&self, type_name: &str) -> String {
format!("create{}", type_name)
}
}

View File

@@ -85,4 +85,8 @@ impl LanguageSyntax for PythonSyntax {
fn string_literal(&self, value: &str) -> String {
format!("'{}'", value)
}
fn constructor_name(&self, type_name: &str) -> String {
type_name.to_string()
}
}

View File

@@ -86,4 +86,8 @@ impl LanguageSyntax for RustSyntax {
fn string_literal(&self, value: &str) -> String {
format!("\"{}\".to_string()", value)
}
fn constructor_name(&self, type_name: &str) -> String {
format!("{}::new", type_name)
}
}

View File

@@ -6,6 +6,8 @@
use std::fmt::Write;
use brk_types::MetricLeafWithSchema;
use crate::{ClientMetadata, LanguageSyntax, PatternField, StructuralPattern};
/// Create a path suffix from a name.
@@ -124,8 +126,14 @@ pub fn generate_tree_node_field<S: LanguageSyntax>(
syntax.constructor(&field.rust_type, &path_expr)
}
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
syntax.constructor(&accessor.name, &path_expr)
// Leaf field - use actual metric name if provided
if let Some(metric_name) = pattern_base {
let path = syntax.string_literal(metric_name);
syntax.constructor(&accessor.name, &path)
} else {
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
syntax.constructor(&accessor.name, &path_expr)
}
} else if field.is_branch() {
// Non-pattern branch - instantiate the nested struct
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
@@ -140,3 +148,53 @@ pub fn generate_tree_node_field<S: LanguageSyntax>(
writeln!(output, "{}", syntax.field_init(indent, &field_name, &type_ann, &value)).unwrap();
}
/// Generate a leaf field using the actual metric name from the TreeNode::Leaf.
///
/// This is the shared implementation for all language backends. It uses
/// `leaf.name()` directly to get the correct metric name, avoiding any
/// path concatenation that could produce incorrect names.
///
/// # Arguments
/// * `output` - The string buffer to write to
/// * `syntax` - The language syntax implementation
/// * `client_expr` - The client expression (e.g., "client.clone()", "this", "client")
/// * `tree_field_name` - The field name from the tree structure
/// * `leaf` - The Leaf node containing the actual metric name and indexes
/// * `metadata` - Client metadata for looking up index patterns
/// * `indent` - Indentation string
pub fn generate_leaf_field<S: LanguageSyntax>(
output: &mut String,
syntax: &S,
client_expr: &str,
tree_field_name: &str,
leaf: &MetricLeafWithSchema,
metadata: &ClientMetadata,
indent: &str,
) {
let field_name = syntax.field_name(tree_field_name);
let accessor = metadata
.find_index_set_pattern(leaf.indexes())
.unwrap_or_else(|| {
panic!(
"Metric '{}' has no matching index pattern. All metrics must be indexed.",
leaf.name()
)
});
let type_ann = metadata.field_type_annotation_from_leaf(leaf, syntax.generic_syntax());
let metric_name = syntax.string_literal(leaf.name());
let value = format!(
"{}({}, {})",
syntax.constructor_name(&accessor.name),
client_expr,
metric_name
);
writeln!(
output,
"{}",
syntax.field_init(indent, &field_name, &type_ann, &value)
)
.unwrap();
}

View File

@@ -6,8 +6,9 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, Endpoint, PatternField, child_type_name, get_first_leaf_name, get_node_fields,
get_pattern_instance_base, infer_accumulated_name, prepare_tree_node, to_camel_case,
ClientMetadata, Endpoint, JavaScriptSyntax, PatternField, child_type_name, generate_leaf_field,
get_first_leaf_name, get_node_fields, get_pattern_instance_base, infer_accumulated_name,
prepare_tree_node, to_camel_case,
};
use super::api::generate_api_methods;
@@ -142,33 +143,24 @@ fn generate_tree_initializer(
) {
let indent_str = " ".repeat(indent);
let syntax = JavaScriptSyntax;
if let TreeNode::Branch(children) = node {
for (i, (child_name, child_node)) in children.iter().enumerate() {
let field_name = to_camel_case(child_name);
let comma = if i < children.len() - 1 { "," } else { "" };
for (child_name, child_node) in children.iter() {
match child_node {
TreeNode::Leaf(leaf) => {
let accessor = metadata
.find_index_set_pattern(leaf.indexes())
.unwrap_or_else(|| {
panic!(
"Metric '{}' has no matching index pattern. All metrics must be indexed.",
leaf.name()
)
});
writeln!(
// Use shared helper for leaf fields
generate_leaf_field(
output,
"{}{}: create{}(this, '{}'){}",
indent_str,
field_name,
accessor.name,
leaf.name(),
comma
)
.unwrap();
&syntax,
"this",
child_name,
leaf,
metadata,
&indent_str,
);
}
TreeNode::Branch(grandchildren) => {
let field_name = to_camel_case(child_name);
let child_fields = get_node_fields(grandchildren, pattern_lookup);
// Only use pattern factory if pattern is parameterizable
let pattern_name = pattern_lookup
@@ -179,8 +171,8 @@ fn generate_tree_initializer(
let arg = get_pattern_instance_base(child_node);
writeln!(
output,
"{}{}: create{}(this, '{}'){}",
indent_str, field_name, pattern_name, arg, comma
"{}{}: create{}(this, '{}'),",
indent_str, field_name, pattern_name, arg
)
.unwrap();
} else {
@@ -195,7 +187,7 @@ fn generate_tree_initializer(
pattern_lookup,
metadata,
);
writeln!(output, "{}}}{}", indent_str, comma).unwrap();
writeln!(output, "{}}},", indent_str).unwrap();
}
}
}

View File

@@ -6,8 +6,8 @@ use std::fmt::Write;
use brk_types::TreeNode;
use crate::{
ClientMetadata, PatternField, child_type_name, get_node_fields, get_pattern_instance_base,
prepare_tree_node, to_snake_case,
ClientMetadata, PatternField, PythonSyntax, child_type_name, generate_leaf_field,
get_node_fields, get_pattern_instance_base, prepare_tree_node, to_snake_case,
};
use super::client::field_type_with_generic;
@@ -50,7 +50,8 @@ fn generate_tree_class(
)
.unwrap();
for ((field, child_fields_opt), (_child_name, child_node)) in
let syntax = PythonSyntax;
for ((field, child_fields_opt), (child_name, child_node)) in
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
{
// Look up type parameter for generic patterns
@@ -72,14 +73,8 @@ fn generate_tree_class(
)
.unwrap();
} else if let TreeNode::Leaf(leaf) = child_node {
// Leaf node: use actual metric name
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" self.{}: {} = {}(client, '{}')",
field_name_py, py_type, accessor.name, leaf.name()
)
.unwrap();
// Leaf node: use shared helper
generate_leaf_field(output, &syntax, "client", child_name, leaf, metadata, " ");
} else if field.is_branch() {
// Non-parameterizable pattern or regular branch: generate inline class
let inline_class = child_type_name(name, &field.name);

View File

@@ -295,13 +295,14 @@ pub fn schema_to_python_type_ctx(schema: &Value, current_type: Option<&str>) ->
"Any".to_string()
}
/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]", "number" -> "int")
/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]", "integer" -> "int")
pub 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 {
match js_type {
"number" => "int".to_string(),
"integer" => "int".to_string(),
"number" => "float".to_string(),
"boolean" => "bool".to_string(),
"string" => "str".to_string(),
"null" => "None".to_string(),

View File

@@ -162,12 +162,12 @@ impl<T: DeserializeOwned> Endpoint<T> {{
}}
/// Fetch all data points for this metric/index.
pub fn get(&self) -> Result<Vec<T>> {{
pub fn get(&self) -> Result<MetricData<T>> {{
self.client.get(&self.path())
}}
/// Fetch data points within a range.
pub fn range(&self, from: Option<i64>, to: Option<i64>) -> Result<Vec<T>> {{
pub fn range(&self, from: Option<i64>, to: Option<i64>) -> Result<MetricData<T>> {{
let mut params = Vec::new();
if let Some(f) = from {{ params.push(format!("from={{}}", f)); }}
if let Some(t) = to {{ params.push(format!("to={{}}", t)); }}

View File

@@ -7,8 +7,8 @@ use brk_types::TreeNode;
use crate::{
ClientMetadata, LanguageSyntax, PatternField, RustSyntax, child_type_name,
generate_tree_node_field, get_node_fields, get_pattern_instance_base, prepare_tree_node,
to_snake_case,
generate_leaf_field, generate_tree_node_field, get_node_fields, get_pattern_instance_base,
prepare_tree_node, to_snake_case,
};
use super::client::field_type_with_generic;
@@ -100,16 +100,21 @@ fn generate_tree_node(
field_name, child_struct, path_expr
)
.unwrap();
} else {
// Leaf field
generate_tree_node_field(
} else if let TreeNode::Leaf(leaf) = child_node {
// Leaf field - use shared helper
generate_leaf_field(
output,
&syntax,
field_info,
"client.clone()",
child_name,
leaf,
metadata,
" ",
child_name,
None,
);
} else {
panic!(
"Field '{}' is a leaf with no TreeNode::Leaf. This shouldn't happen.",
field_info.name
);
}
}

View File

@@ -7,6 +7,7 @@ pub fn js_type_to_rust(js_type: &str) -> String {
} else {
match js_type {
"string" => "String".to_string(),
"integer" => "i64".to_string(),
"number" => "f64".to_string(),
"boolean" => "bool".to_string(),
"*" => "serde_json::Value".to_string(),

View File

@@ -102,4 +102,11 @@ pub trait LanguageSyntax {
/// - Python/JavaScript: `'value'` (single quotes)
/// - Rust: `"value"` (double quotes)
fn string_literal(&self, value: &str) -> String;
/// Get the constructor name/prefix for a type.
///
/// - Python: `TypeName`
/// - JavaScript: `createTypeName`
/// - Rust: `TypeName::new`
fn constructor_name(&self, type_name: &str) -> String;
}

View File

@@ -3,7 +3,7 @@
use std::collections::{BTreeSet, HashMap};
use brk_query::Vecs;
use brk_types::Index;
use brk_types::{Index, MetricLeafWithSchema};
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
use crate::analysis;
@@ -158,4 +158,21 @@ impl ClientMetadata {
syntax.wrap("MetricNode", &value_type)
}
}
/// Generate type annotation for a leaf node with language-specific syntax.
///
/// This is a simpler version of `field_type_annotation` that works directly
/// with a `MetricLeafWithSchema` node instead of a `PatternField`.
pub fn field_type_annotation_from_leaf(
&self,
leaf: &MetricLeafWithSchema,
syntax: GenericSyntax,
) -> String {
let value_type = leaf.kind().to_string();
if let Some(accessor) = self.find_index_set_pattern(leaf.indexes()) {
syntax.wrap(&accessor.name, &value_type)
} else {
syntax.wrap("MetricNode", &value_type)
}
}
}