mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-27 02:38:10 -07:00
global: snapshot
This commit is contained in:
@@ -120,6 +120,18 @@ pub fn find_common_suffix(names: &[&str]) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
/// Normalize a prefix string by ensuring it ends with underscore.
|
||||
/// Returns empty string if input is empty.
|
||||
pub fn normalize_prefix(s: &str) -> String {
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -8,7 +8,7 @@ use std::collections::{BTreeSet, HashMap};
|
||||
use brk_types::{TreeNode, extract_json_type};
|
||||
|
||||
use super::analyze_pattern_modes;
|
||||
use crate::{PatternField, StructuralPattern, to_pascal_case};
|
||||
use crate::{PatternBaseResult, PatternField, StructuralPattern, to_pascal_case};
|
||||
|
||||
/// Context for pattern detection, holding all intermediate state.
|
||||
struct PatternContext {
|
||||
@@ -38,14 +38,16 @@ impl PatternContext {
|
||||
|
||||
/// Detect structural patterns in the tree using a bottom-up approach.
|
||||
///
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param, node_bases).
|
||||
/// Each pattern has its `mode` set based on analysis of all instances.
|
||||
/// `node_bases` maps tree paths to their computed PatternBaseResult for use during generation.
|
||||
pub fn detect_structural_patterns(
|
||||
tree: &TreeNode,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<String, PatternBaseResult>,
|
||||
) {
|
||||
let mut ctx = PatternContext::new();
|
||||
resolve_branch_patterns(tree, "root", &mut ctx);
|
||||
@@ -99,10 +101,11 @@ pub fn detect_structural_patterns(
|
||||
let concrete_to_pattern = pattern_lookup.clone();
|
||||
|
||||
// Analyze pattern modes (suffix vs prefix) from all instances
|
||||
analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
|
||||
// Also collects node bases for each tree path
|
||||
let node_bases = analyze_pattern_modes(tree, &mut patterns, &pattern_lookup);
|
||||
|
||||
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
|
||||
(patterns, concrete_to_pattern, type_mappings)
|
||||
(patterns, concrete_to_pattern, type_mappings, node_bases)
|
||||
}
|
||||
|
||||
/// Detect generic patterns by grouping signatures by their normalized form.
|
||||
|
||||
@@ -8,8 +8,8 @@ use std::collections::HashMap;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use super::{find_common_prefix, find_common_suffix, get_node_fields};
|
||||
use crate::{PatternField, PatternMode, StructuralPattern};
|
||||
use super::{find_common_prefix, find_common_suffix, get_node_fields, normalize_prefix};
|
||||
use crate::{PatternBaseResult, PatternField, PatternMode, StructuralPattern, build_child_path};
|
||||
|
||||
/// Result of analyzing a single pattern instance.
|
||||
#[derive(Debug, Clone)]
|
||||
@@ -28,16 +28,21 @@ struct InstanceAnalysis {
|
||||
/// This is the main entry point for mode detection. It processes
|
||||
/// the tree bottom-up, collecting analysis for each pattern instance,
|
||||
/// then determines the consistent mode for each pattern.
|
||||
///
|
||||
/// Returns a map from tree paths to their computed PatternBaseResult.
|
||||
/// This map is used during generation to check pattern compatibility.
|
||||
pub fn analyze_pattern_modes(
|
||||
tree: &TreeNode,
|
||||
patterns: &mut [StructuralPattern],
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
) -> HashMap<String, PatternBaseResult> {
|
||||
// Collect analyses from all instances, keyed by pattern name
|
||||
let mut all_analyses: HashMap<String, Vec<InstanceAnalysis>> = HashMap::new();
|
||||
// Also collect base results for each node, keyed by tree path
|
||||
let mut node_bases: HashMap<String, PatternBaseResult> = HashMap::new();
|
||||
|
||||
// Bottom-up traversal
|
||||
collect_instance_analyses(tree, pattern_lookup, &mut all_analyses);
|
||||
collect_instance_analyses(tree, "", pattern_lookup, &mut all_analyses, &mut node_bases);
|
||||
|
||||
// For each pattern, determine mode from collected instances
|
||||
for pattern in patterns.iter_mut() {
|
||||
@@ -45,14 +50,20 @@ pub fn analyze_pattern_modes(
|
||||
pattern.mode = determine_pattern_mode(analyses, &pattern.fields);
|
||||
}
|
||||
}
|
||||
|
||||
node_bases
|
||||
}
|
||||
|
||||
/// Recursively collect instance analyses bottom-up.
|
||||
/// Returns the "base" for this node (used by parent for its analysis).
|
||||
///
|
||||
/// Also stores the PatternBaseResult for each node in `node_bases`, keyed by path.
|
||||
fn collect_instance_analyses(
|
||||
node: &TreeNode,
|
||||
path: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
all_analyses: &mut HashMap<String, Vec<InstanceAnalysis>>,
|
||||
node_bases: &mut HashMap<String, PatternBaseResult>,
|
||||
) -> Option<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => {
|
||||
@@ -63,9 +74,14 @@ fn collect_instance_analyses(
|
||||
// First, process all children recursively (bottom-up)
|
||||
let mut child_bases: HashMap<String, String> = HashMap::new();
|
||||
for (field_name, child_node) in children {
|
||||
if let Some(base) =
|
||||
collect_instance_analyses(child_node, pattern_lookup, all_analyses)
|
||||
{
|
||||
let child_path = build_child_path(path, field_name);
|
||||
if let Some(base) = collect_instance_analyses(
|
||||
child_node,
|
||||
&child_path,
|
||||
pattern_lookup,
|
||||
all_analyses,
|
||||
node_bases,
|
||||
) {
|
||||
child_bases.insert(field_name.clone(), base);
|
||||
}
|
||||
}
|
||||
@@ -77,6 +93,19 @@ fn collect_instance_analyses(
|
||||
// Analyze this instance
|
||||
let analysis = analyze_instance(&child_bases);
|
||||
|
||||
// Store the base result for this node
|
||||
// Note: has_outlier is false because we use recursive base computation
|
||||
// which gives correct bases without needing outlier detection
|
||||
node_bases.insert(
|
||||
path.to_string(),
|
||||
PatternBaseResult {
|
||||
base: analysis.base.clone(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: analysis.is_suffix_mode,
|
||||
field_parts: analysis.field_parts.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
// Get the pattern name for this node (if any)
|
||||
let fields = get_node_fields(children, pattern_lookup);
|
||||
if let Some(pattern_name) = pattern_lookup.get(&fields) {
|
||||
@@ -128,19 +157,10 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
|
||||
let mut field_parts = HashMap::new();
|
||||
|
||||
for (field_name, child_base) in child_bases {
|
||||
// Prefix = child_base with common suffix stripped
|
||||
// Prefix = child_base with common suffix stripped, normalized to end with _
|
||||
let prefix = child_base
|
||||
.strip_suffix(&common_suffix)
|
||||
.map(|s| {
|
||||
// Ensure prefix ends with underscore if non-empty
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
})
|
||||
.map(normalize_prefix)
|
||||
.unwrap_or_default();
|
||||
field_parts.insert(field_name.clone(), prefix);
|
||||
}
|
||||
@@ -152,16 +172,16 @@ fn analyze_instance(child_bases: &HashMap<String, String>) -> InstanceAnalysis {
|
||||
};
|
||||
}
|
||||
|
||||
// No common prefix or suffix - use first child's base and treat as suffix mode
|
||||
// with full metric names as relatives
|
||||
let base = child_bases.values().next().cloned().unwrap_or_default();
|
||||
// No common prefix or suffix - use empty base so _m(base, relative) returns just the relative.
|
||||
// This handles cases like utxo_cohorts.all.activity where children have completely
|
||||
// different bases (coinblocks_destroyed, coindays_destroyed, etc.)
|
||||
let field_parts = child_bases
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
|
||||
InstanceAnalysis {
|
||||
base,
|
||||
base: String::new(),
|
||||
field_parts,
|
||||
is_suffix_mode: true,
|
||||
}
|
||||
@@ -197,7 +217,9 @@ fn determine_pattern_mode(
|
||||
// Convert to sorted Vec for comparison since HashMap isn't hashable
|
||||
let mut parts_counts: HashMap<Vec<(String, String)>, usize> = HashMap::new();
|
||||
for analysis in &majority_instances {
|
||||
let mut sorted: Vec<_> = analysis.field_parts.iter()
|
||||
let mut sorted: Vec<_> = analysis
|
||||
.field_parts
|
||||
.iter()
|
||||
.map(|(k, v)| (k.clone(), v.clone()))
|
||||
.collect();
|
||||
sorted.sort();
|
||||
@@ -244,7 +266,10 @@ mod tests {
|
||||
assert_eq!(analysis.base, "lth_cost_basis");
|
||||
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("percentiles"), Some(&"".to_string()));
|
||||
assert_eq!(
|
||||
analysis.field_parts.get("percentiles"),
|
||||
Some(&"".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -280,7 +305,10 @@ mod tests {
|
||||
assert_eq!(analysis.base, "cost_basis");
|
||||
assert_eq!(analysis.field_parts.get("max"), Some(&"max".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("min"), Some(&"min".to_string()));
|
||||
assert_eq!(analysis.field_parts.get("percentiles"), Some(&"".to_string()));
|
||||
assert_eq!(
|
||||
analysis.field_parts.get("percentiles"),
|
||||
Some(&"".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -9,15 +9,7 @@ use brk_types::{Index, TreeNode, extract_json_type};
|
||||
|
||||
use crate::{IndexSetPattern, PatternField, child_type_name};
|
||||
|
||||
use super::{find_common_prefix, find_common_suffix};
|
||||
|
||||
/// Get the first leaf name from a tree node.
|
||||
pub fn get_first_leaf_name(node: &TreeNode) -> Option<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
|
||||
TreeNode::Branch(children) => children.values().find_map(get_first_leaf_name),
|
||||
}
|
||||
}
|
||||
use super::{find_common_prefix, find_common_suffix, normalize_prefix};
|
||||
|
||||
/// Get the shortest leaf name from a tree node.
|
||||
///
|
||||
@@ -128,6 +120,30 @@ pub struct PatternBaseResult {
|
||||
pub field_parts: HashMap<String, String>,
|
||||
}
|
||||
|
||||
impl PatternBaseResult {
|
||||
/// Create a default result that forces inlining (has_outlier = true).
|
||||
/// Use when no pattern base could be computed during lookup.
|
||||
pub fn force_inline() -> Self {
|
||||
Self {
|
||||
base: String::new(),
|
||||
has_outlier: true,
|
||||
is_suffix_mode: true,
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Create an empty result with no outlier.
|
||||
/// Use for root-level patterns or when children have no common pattern.
|
||||
pub fn empty() -> Self {
|
||||
Self {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true,
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing direct children.
|
||||
///
|
||||
/// Uses the shortest leaf names from direct children to find common prefix/suffix.
|
||||
@@ -140,12 +156,7 @@ pub struct PatternBaseResult {
|
||||
pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
let child_names = get_direct_children_for_analysis(node);
|
||||
if child_names.is_empty() {
|
||||
return PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true, // default
|
||||
field_parts: HashMap::new(),
|
||||
};
|
||||
return PatternBaseResult::empty();
|
||||
}
|
||||
|
||||
// Try to find common base from leaf names
|
||||
@@ -181,12 +192,7 @@ pub fn get_pattern_instance_base(node: &TreeNode) -> PatternBaseResult {
|
||||
|
||||
// Fallback: no common prefix/suffix found - this is a root-level pattern
|
||||
// Return empty base so metric names are used directly
|
||||
PatternBaseResult {
|
||||
base: String::new(),
|
||||
has_outlier: false,
|
||||
is_suffix_mode: true, // default
|
||||
field_parts: HashMap::new(),
|
||||
}
|
||||
PatternBaseResult::empty()
|
||||
}
|
||||
|
||||
/// Result of try_find_base: base name, has_outlier flag, is_suffix_mode flag, and field_parts.
|
||||
@@ -199,7 +205,10 @@ struct FindBaseResult {
|
||||
|
||||
/// Try to find a common base from child names using prefix/suffix detection.
|
||||
/// Returns Some(FindBaseResult) if found.
|
||||
fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) -> Option<FindBaseResult> {
|
||||
fn try_find_base(
|
||||
child_names: &[(String, String)],
|
||||
is_outlier_attempt: bool,
|
||||
) -> Option<FindBaseResult> {
|
||||
let leaf_names: Vec<&str> = child_names.iter().map(|(_, n)| n.as_str()).collect();
|
||||
|
||||
// Try common prefix first (suffix mode)
|
||||
@@ -231,18 +240,10 @@ fn try_find_base(child_names: &[(String, String)], is_outlier_attempt: bool) ->
|
||||
let base = suffix.trim_start_matches('_').to_string();
|
||||
let mut field_parts = HashMap::new();
|
||||
for (field_name, leaf_name) in child_names {
|
||||
// Compute the prefix part for this field
|
||||
// Compute the prefix part for this field, normalized to end with _
|
||||
let prefix_part = leaf_name
|
||||
.strip_suffix(&suffix)
|
||||
.map(|s| {
|
||||
if s.is_empty() {
|
||||
String::new()
|
||||
} else if s.ends_with('_') {
|
||||
s.to_string()
|
||||
} else {
|
||||
format!("{}_", s)
|
||||
}
|
||||
})
|
||||
.map(normalize_prefix)
|
||||
.unwrap_or_default();
|
||||
field_parts.insert(field_name.clone(), prefix_part);
|
||||
}
|
||||
@@ -366,9 +367,18 @@ mod tests {
|
||||
fn test_get_pattern_instance_base_with_base_field() {
|
||||
// Simulates vbytes tree: has base field with block_vbytes leaf
|
||||
let tree = make_branch(vec![
|
||||
("base", make_branch(vec![("dateindex", make_leaf("block_vbytes"))])),
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))])),
|
||||
(
|
||||
"base",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes"))]),
|
||||
),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_vbytes_sum"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
@@ -380,11 +390,26 @@ mod tests {
|
||||
fn test_get_pattern_instance_base_without_base_field() {
|
||||
// Simulates weight tree: NO base field, only suffixed metrics
|
||||
let tree = make_branch(vec![
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||
("cumulative", make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))])),
|
||||
("max", make_branch(vec![("dateindex", make_leaf("block_weight_max"))])),
|
||||
("min", make_branch(vec![("dateindex", make_leaf("block_weight_min"))])),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
|
||||
),
|
||||
(
|
||||
"cumulative",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_cumulative"))]),
|
||||
),
|
||||
(
|
||||
"max",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_max"))]),
|
||||
),
|
||||
(
|
||||
"min",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_min"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
@@ -397,9 +422,18 @@ mod tests {
|
||||
// What if there's a "base" field that points to the same leaf as "average"?
|
||||
// This could happen if the tree generation creates a base field that shares leaves with average
|
||||
let tree = make_branch(vec![
|
||||
("base", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("average", make_branch(vec![("dateindex", make_leaf("block_weight_average"))])),
|
||||
("sum", make_branch(vec![("dateindex", make_leaf("block_weight_sum"))])),
|
||||
(
|
||||
"base",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"average",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_average"))]),
|
||||
),
|
||||
(
|
||||
"sum",
|
||||
make_branch(vec![("dateindex", make_leaf("block_weight_sum"))]),
|
||||
),
|
||||
]);
|
||||
|
||||
let result = get_pattern_instance_base(&tree);
|
||||
|
||||
@@ -4,10 +4,18 @@ use std::collections::{HashMap, HashSet};
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info,
|
||||
get_pattern_instance_base,
|
||||
};
|
||||
use crate::{ClientMetadata, PatternBaseResult, PatternField, child_type_name, get_fields_with_child_info};
|
||||
|
||||
/// Build a child path by appending a child name to a parent path.
|
||||
/// Uses "/" as separator. If parent is empty, returns just the child name.
|
||||
#[inline]
|
||||
pub fn build_child_path(parent: &str, child: &str) -> String {
|
||||
if parent.is_empty() {
|
||||
child.to_string()
|
||||
} else {
|
||||
format!("{}/{}", parent, child)
|
||||
}
|
||||
}
|
||||
|
||||
/// Pre-computed context for a single child node.
|
||||
pub struct ChildContext<'a> {
|
||||
@@ -38,9 +46,13 @@ pub struct TreeNodeContext<'a> {
|
||||
/// Prepare a tree node for generation.
|
||||
/// Returns None if the node should be skipped (not a branch, already generated,
|
||||
/// or matches a parameterizable pattern).
|
||||
///
|
||||
/// The `path` parameter is the tree path to this node (e.g., "distribution/utxoCohorts").
|
||||
/// It's used to look up pre-computed PatternBaseResult from the analysis phase.
|
||||
pub fn prepare_tree_node<'a>(
|
||||
node: &'a TreeNode,
|
||||
name: &str,
|
||||
path: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
@@ -55,8 +67,13 @@ pub fn prepare_tree_node<'a>(
|
||||
.map(|(f, _)| f.clone())
|
||||
.collect();
|
||||
|
||||
// Look up the pre-computed base result, or use a default that forces inlining
|
||||
let base_result = metadata
|
||||
.get_node_base(path)
|
||||
.cloned()
|
||||
.unwrap_or_else(PatternBaseResult::force_inline);
|
||||
|
||||
// Skip if this matches a parameterizable pattern AND has no outlier AND field parts match
|
||||
let base_result = get_pattern_instance_base(node);
|
||||
let pattern_compatible = pattern_lookup
|
||||
.get(&fields)
|
||||
.and_then(|name| metadata.find_pattern(name))
|
||||
@@ -85,7 +102,13 @@ pub fn prepare_tree_node<'a>(
|
||||
.zip(fields_with_child_info)
|
||||
.map(|((child_name, child_node), (field, child_fields))| {
|
||||
let is_leaf = matches!(child_node, TreeNode::Leaf(_));
|
||||
let base_result = get_pattern_instance_base(child_node);
|
||||
|
||||
// Build child path and look up its pre-computed base result
|
||||
let child_path = build_child_path(path, child_name);
|
||||
let base_result = metadata
|
||||
.get_node_base(&child_path)
|
||||
.cloned()
|
||||
.unwrap_or_else(PatternBaseResult::force_inline);
|
||||
|
||||
// For type annotations: use pattern type if ANY pattern matches
|
||||
let matches_any_pattern = child_fields
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, generate_leaf_field,
|
||||
prepare_tree_node, to_camel_case,
|
||||
ClientMetadata, Endpoint, GenericSyntax, JavaScriptSyntax, PatternField, build_child_path,
|
||||
generate_leaf_field, prepare_tree_node, to_camel_case,
|
||||
};
|
||||
|
||||
use super::api::generate_api_methods;
|
||||
@@ -22,6 +22,7 @@ pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata:
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
"MetricsTree",
|
||||
"",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
@@ -32,12 +33,13 @@ pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata:
|
||||
fn generate_tree_typedef(
|
||||
output: &mut String,
|
||||
name: &str,
|
||||
path: &str,
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) {
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -71,9 +73,11 @@ fn generate_tree_typedef(
|
||||
// Generate child typedefs
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
let child_path = build_child_path(path, child.name);
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
&child_path,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
@@ -125,6 +129,7 @@ pub fn generate_main_client(
|
||||
output,
|
||||
catalog,
|
||||
"MetricsTree",
|
||||
"",
|
||||
3,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
@@ -166,10 +171,12 @@ pub fn generate_main_client(
|
||||
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn generate_tree_initializer(
|
||||
output: &mut String,
|
||||
node: &TreeNode,
|
||||
name: &str,
|
||||
path: &str,
|
||||
indent: usize,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
@@ -177,7 +184,7 @@ fn generate_tree_initializer(
|
||||
) {
|
||||
let indent_str = " ".repeat(indent);
|
||||
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -199,11 +206,13 @@ fn generate_tree_initializer(
|
||||
}
|
||||
} else if child.should_inline {
|
||||
// Inline object
|
||||
let child_path = build_child_path(path, child.name);
|
||||
writeln!(output, "{}{}: {{", indent_str, field_name).unwrap();
|
||||
generate_tree_initializer(
|
||||
output,
|
||||
child.node,
|
||||
&child.inline_type_name,
|
||||
&child_path,
|
||||
indent + 1,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
|
||||
@@ -6,8 +6,8 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, generate_leaf_field,
|
||||
prepare_tree_node, to_snake_case,
|
||||
ClientMetadata, GenericSyntax, PatternField, PythonSyntax, build_child_path,
|
||||
generate_leaf_field, prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate tree classes
|
||||
@@ -19,6 +19,7 @@ pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata:
|
||||
generate_tree_class(
|
||||
output,
|
||||
"MetricsTree",
|
||||
"",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
@@ -30,12 +31,13 @@ pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata:
|
||||
fn generate_tree_class(
|
||||
output: &mut String,
|
||||
name: &str,
|
||||
path: &str,
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) {
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -43,9 +45,11 @@ fn generate_tree_class(
|
||||
// This ensures children are defined before parent references them
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
let child_path = build_child_path(path, child.name);
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
&child_path,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
|
||||
@@ -6,7 +6,7 @@ use std::fmt::Write;
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax,
|
||||
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, build_child_path,
|
||||
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
@@ -19,6 +19,7 @@ pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientM
|
||||
generate_tree_node(
|
||||
output,
|
||||
"MetricsTree",
|
||||
"",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
@@ -29,12 +30,13 @@ pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientM
|
||||
fn generate_tree_node(
|
||||
output: &mut String,
|
||||
name: &str,
|
||||
path: &str,
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) {
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, path, pattern_lookup, metadata, generated) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -117,9 +119,11 @@ fn generate_tree_node(
|
||||
// Generate child structs
|
||||
for child in &ctx.children {
|
||||
if child.should_inline {
|
||||
let child_path = build_child_path(path, child.name);
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child.inline_type_name,
|
||||
&child_path,
|
||||
child.node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
|
||||
@@ -6,7 +6,7 @@ use brk_query::Vecs;
|
||||
use brk_types::{Index, MetricLeafWithSchema};
|
||||
|
||||
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
|
||||
use crate::analysis;
|
||||
use crate::{PatternBaseResult, analysis};
|
||||
|
||||
/// Metadata extracted from brk_query for client generation.
|
||||
#[derive(Debug)]
|
||||
@@ -21,6 +21,8 @@ pub struct ClientMetadata {
|
||||
concrete_to_pattern: HashMap<Vec<PatternField>, String>,
|
||||
/// Maps concrete field signatures to their type parameter (for generic patterns)
|
||||
concrete_to_type_param: HashMap<Vec<PatternField>, String>,
|
||||
/// Maps tree paths to their computed PatternBaseResult
|
||||
node_bases: HashMap<String, PatternBaseResult>,
|
||||
}
|
||||
|
||||
impl ClientMetadata {
|
||||
@@ -31,7 +33,7 @@ impl ClientMetadata {
|
||||
|
||||
/// Extract metadata from a catalog TreeNode directly.
|
||||
pub fn from_catalog(catalog: brk_types::TreeNode) -> Self {
|
||||
let (structural_patterns, concrete_to_pattern, concrete_to_type_param) =
|
||||
let (structural_patterns, concrete_to_pattern, concrete_to_type_param, node_bases) =
|
||||
analysis::detect_structural_patterns(&catalog);
|
||||
let index_set_patterns = analysis::detect_index_patterns(&catalog);
|
||||
|
||||
@@ -41,6 +43,7 @@ impl ClientMetadata {
|
||||
index_set_patterns,
|
||||
concrete_to_pattern,
|
||||
concrete_to_type_param,
|
||||
node_bases,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,6 +142,11 @@ impl ClientMetadata {
|
||||
lookup
|
||||
}
|
||||
|
||||
/// Get the pre-computed PatternBaseResult for a tree path.
|
||||
pub fn get_node_base(&self, path: &str) -> Option<&PatternBaseResult> {
|
||||
self.node_bases.get(path)
|
||||
}
|
||||
|
||||
/// Generate type annotation for a field with language-specific syntax.
|
||||
pub fn field_type_annotation(
|
||||
&self,
|
||||
|
||||
@@ -79,7 +79,7 @@ fn test_all_leaves_have_names() {
|
||||
fn test_pattern_detection() {
|
||||
let catalog = load_catalog();
|
||||
|
||||
let (patterns, concrete_to_pattern, concrete_to_type_param) =
|
||||
let (patterns, concrete_to_pattern, concrete_to_type_param, _node_bases) =
|
||||
brk_bindgen::detect_structural_patterns(&catalog);
|
||||
|
||||
println!("Detected {} structural patterns", patterns.len());
|
||||
@@ -142,7 +142,7 @@ fn test_pattern_detection() {
|
||||
fn test_cost_basis_pattern() {
|
||||
let catalog = load_catalog();
|
||||
|
||||
let (patterns, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
||||
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
||||
|
||||
// Find CostBasisPattern2 and inspect it
|
||||
let cost_basis = patterns
|
||||
@@ -211,7 +211,7 @@ fn test_realized_pattern3_fields() {
|
||||
#[test]
|
||||
fn test_parameterizable_patterns_have_mode() {
|
||||
let catalog = load_catalog();
|
||||
let (patterns, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
||||
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
||||
|
||||
// All patterns that appear 2+ times should either:
|
||||
// 1. Be parameterizable (have a mode)
|
||||
@@ -272,7 +272,7 @@ fn test_parameterizable_patterns_have_mode() {
|
||||
#[test]
|
||||
fn test_fee_rate_pattern_relatives() {
|
||||
let catalog = load_catalog();
|
||||
let (patterns, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
||||
let (patterns, _, _, _) = brk_bindgen::detect_structural_patterns(&catalog);
|
||||
|
||||
let fee_rate_pattern = patterns
|
||||
.iter()
|
||||
@@ -900,17 +900,14 @@ fn test_price_sats_vs_usd_different_field_parts() {
|
||||
&[],
|
||||
);
|
||||
|
||||
// Verify price.sats uses sats-suffixed metrics
|
||||
// With improved pattern detection, price.sats now correctly uses a SatsPattern factory
|
||||
// which eliminates duplication. Verify that it's being used:
|
||||
assert!(
|
||||
js_output.contains("'price_ohlc_sats'"),
|
||||
"price.sats.ohlc should use 'price_ohlc_sats'"
|
||||
);
|
||||
assert!(
|
||||
js_output.contains("'price_sats'") || js_output.contains("createSplitPattern2(this, 'price_sats')"),
|
||||
"price.sats.split should use 'price_sats' base"
|
||||
js_output.contains("sats: createSatsPattern(this, 'price')"),
|
||||
"price.sats should use SatsPattern factory"
|
||||
);
|
||||
|
||||
// Verify price.usd uses non-sats metrics (no _sats suffix)
|
||||
// Verify price.usd is inlined and uses non-sats metrics (no _sats suffix)
|
||||
assert!(
|
||||
js_output.contains("createMetricPattern1(this, 'price_ohlc')"),
|
||||
"price.usd.ohlc should use 'price_ohlc' (without _sats)"
|
||||
@@ -920,24 +917,57 @@ fn test_price_sats_vs_usd_different_field_parts() {
|
||||
"price.usd.split should use 'price' base (without _sats)"
|
||||
);
|
||||
|
||||
// Verify they don't incorrectly share the same metric names
|
||||
// Count occurrences to ensure usd doesn't use sats metrics
|
||||
let sats_ohlc_count = js_output.matches("'price_ohlc_sats'").count();
|
||||
let usd_ohlc_count = js_output.matches("'price_ohlc')").count();
|
||||
|
||||
println!("price_ohlc_sats occurrences: {}", sats_ohlc_count);
|
||||
println!("price_ohlc occurrences: {}", usd_ohlc_count);
|
||||
|
||||
assert!(
|
||||
sats_ohlc_count >= 1,
|
||||
"Should have at least one 'price_ohlc_sats' for price.sats"
|
||||
);
|
||||
assert!(
|
||||
usd_ohlc_count >= 1,
|
||||
"Should have at least one 'price_ohlc' for price.usd"
|
||||
);
|
||||
|
||||
println!("\nPrice sats vs usd field_parts test passed!");
|
||||
println!(" - price.sats correctly uses sats-suffixed metrics");
|
||||
println!(" - price.usd correctly uses non-sats metrics");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_utxo_cohorts_all_activity_base() {
|
||||
// Test that distribution.utxo_cohorts.all.activity uses empty base
|
||||
// because its children (coinblocks_destroyed, coindays_destroyed, etc.)
|
||||
// have no common prefix or suffix.
|
||||
let catalog = load_catalog();
|
||||
let metadata = ClientMetadata::from_catalog(catalog.clone());
|
||||
|
||||
// Generate JavaScript output
|
||||
let mut js_output = String::new();
|
||||
writeln!(js_output, "// Test output").unwrap();
|
||||
brk_bindgen::javascript::client::generate_base_client(&mut js_output);
|
||||
brk_bindgen::javascript::client::generate_index_accessors(
|
||||
&mut js_output,
|
||||
&metadata.index_set_patterns,
|
||||
);
|
||||
brk_bindgen::javascript::client::generate_structural_patterns(
|
||||
&mut js_output,
|
||||
&metadata.structural_patterns,
|
||||
&metadata,
|
||||
);
|
||||
brk_bindgen::javascript::tree::generate_tree_typedefs(
|
||||
&mut js_output,
|
||||
&metadata.catalog,
|
||||
&metadata,
|
||||
);
|
||||
brk_bindgen::javascript::tree::generate_main_client(
|
||||
&mut js_output,
|
||||
&metadata.catalog,
|
||||
&metadata,
|
||||
&[],
|
||||
);
|
||||
|
||||
// The all.activity should use empty base, so metrics don't get duplicated
|
||||
// Look for: activity: createActivityPattern2(this, '')
|
||||
// NOT: activity: createActivityPattern2(this, 'coinblocks_destroyed')
|
||||
assert!(
|
||||
!js_output.contains("createActivityPattern2(this, 'coinblocks_destroyed')"),
|
||||
"all.activity should NOT use 'coinblocks_destroyed' as base (causes duplication)"
|
||||
);
|
||||
|
||||
// Check that it uses empty string as base
|
||||
assert!(
|
||||
js_output.contains("activity: createActivityPattern2(this, '')"),
|
||||
"all.activity should use empty base"
|
||||
);
|
||||
|
||||
println!("utxo_cohorts.all.activity base test passed!");
|
||||
}
|
||||
|
||||
+631
-7442
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user