mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-25 07:09:59 -07:00
271 lines
9.4 KiB
Rust
271 lines
9.4 KiB
Rust
//! JavaScript tree structure generation.
|
|
|
|
use std::collections::HashSet;
|
|
use std::fmt::Write;
|
|
|
|
use brk_types::TreeNode;
|
|
|
|
use crate::{
|
|
ClientMetadata, Endpoint, PatternField, child_type_name, get_fields_with_child_info,
|
|
get_first_leaf_name, get_node_fields, get_pattern_instance_base, infer_accumulated_name,
|
|
to_camel_case,
|
|
};
|
|
|
|
use super::api::generate_api_methods;
|
|
use super::client::{field_type_with_generic, generate_static_constants};
|
|
|
|
/// Generate JSDoc typedefs for the catalog tree.
|
|
pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
|
writeln!(output, "// Catalog tree typedefs\n").unwrap();
|
|
|
|
let pattern_lookup = metadata.pattern_lookup();
|
|
let mut generated = HashSet::new();
|
|
generate_tree_typedef(
|
|
output,
|
|
"CatalogTree",
|
|
catalog,
|
|
&pattern_lookup,
|
|
metadata,
|
|
&mut generated,
|
|
);
|
|
}
|
|
|
|
fn generate_tree_typedef(
|
|
output: &mut String,
|
|
name: &str,
|
|
node: &TreeNode,
|
|
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
|
metadata: &ClientMetadata,
|
|
generated: &mut HashSet<String>,
|
|
) {
|
|
let TreeNode::Branch(children) = node else {
|
|
return;
|
|
};
|
|
|
|
let fields_with_child_info = get_fields_with_child_info(children, name, pattern_lookup);
|
|
let fields: Vec<PatternField> = fields_with_child_info
|
|
.iter()
|
|
.map(|(f, _)| f.clone())
|
|
.collect();
|
|
|
|
if pattern_lookup.contains_key(&fields)
|
|
&& pattern_lookup.get(&fields) != Some(&name.to_string())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if generated.contains(name) {
|
|
return;
|
|
}
|
|
generated.insert(name.to_string());
|
|
|
|
writeln!(output, "/**").unwrap();
|
|
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
|
|
|
|
for (field, child_fields) in &fields_with_child_info {
|
|
let generic_value_type = child_fields
|
|
.as_ref()
|
|
.and_then(|cf| metadata.get_type_param(cf))
|
|
.map(String::as_str);
|
|
let js_type = field_type_with_generic(field, metadata, false, generic_value_type);
|
|
writeln!(
|
|
output,
|
|
" * @property {{{}}} {}",
|
|
js_type,
|
|
to_camel_case(&field.name)
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
writeln!(output, " */\n").unwrap();
|
|
|
|
for (child_name, child_node) in children {
|
|
if let TreeNode::Branch(grandchildren) = child_node {
|
|
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
|
if !pattern_lookup.contains_key(&child_fields) {
|
|
let child_type = child_type_name(name, child_name);
|
|
generate_tree_typedef(
|
|
output,
|
|
&child_type,
|
|
child_node,
|
|
pattern_lookup,
|
|
metadata,
|
|
generated,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Generate the main BrkClient class.
|
|
pub fn generate_main_client(
|
|
output: &mut String,
|
|
catalog: &TreeNode,
|
|
metadata: &ClientMetadata,
|
|
endpoints: &[Endpoint],
|
|
) {
|
|
let pattern_lookup = metadata.pattern_lookup();
|
|
|
|
writeln!(output, "/**").unwrap();
|
|
writeln!(output, " * Main BRK client with catalog tree and API methods").unwrap();
|
|
writeln!(output, " * @extends BrkClientBase").unwrap();
|
|
writeln!(output, " */").unwrap();
|
|
writeln!(output, "class BrkClient extends BrkClientBase {{").unwrap();
|
|
|
|
generate_static_constants(output);
|
|
|
|
writeln!(output, " /**").unwrap();
|
|
writeln!(output, " * @param {{BrkClientOptions|string}} options").unwrap();
|
|
writeln!(output, " */").unwrap();
|
|
writeln!(output, " constructor(options) {{").unwrap();
|
|
writeln!(output, " super(options);").unwrap();
|
|
writeln!(output, " /** @type {{CatalogTree}} */").unwrap();
|
|
writeln!(output, " this.tree = this._buildTree('');").unwrap();
|
|
writeln!(output, " }}\n").unwrap();
|
|
|
|
writeln!(output, " /**").unwrap();
|
|
writeln!(output, " * @private").unwrap();
|
|
writeln!(output, " * @param {{string}} basePath").unwrap();
|
|
writeln!(output, " * @returns {{CatalogTree}}").unwrap();
|
|
writeln!(output, " */").unwrap();
|
|
writeln!(output, " _buildTree(basePath) {{").unwrap();
|
|
writeln!(output, " return {{").unwrap();
|
|
generate_tree_initializer(output, catalog, "", 3, &pattern_lookup, metadata);
|
|
writeln!(output, " }};").unwrap();
|
|
writeln!(output, " }}\n").unwrap();
|
|
|
|
generate_api_methods(output, endpoints);
|
|
|
|
// Instance method: mergeMetricPatterns
|
|
writeln!(output, r#"
|
|
/**
|
|
* Merge multiple MetricPatterns into a single pattern.
|
|
* Throws if any two patterns have overlapping indexes.
|
|
* @template T
|
|
* @param {{...MetricPattern<T>}} patterns - The patterns to merge
|
|
* @returns {{MetricPattern<T>}} A new merged pattern
|
|
*/
|
|
mergeMetricPatterns(...patterns) {{
|
|
if (patterns.length === 0) {{
|
|
throw new BrkError('mergeMetricPatterns requires at least one pattern');
|
|
}}
|
|
if (patterns.length === 1) {{
|
|
return patterns[0];
|
|
}}
|
|
|
|
const seenIndexes = /** @type {{Map<Index, string>}} */ (new Map());
|
|
const mergedBy = /** @type {{Partial<Record<Index, MetricEndpoint<T>>>}} */ ({{}});
|
|
|
|
for (const pattern of patterns) {{
|
|
for (const index of pattern.indexes()) {{
|
|
const existing = seenIndexes.get(index);
|
|
if (existing !== undefined) {{
|
|
throw new BrkError(`Index '${{index}}' exists in both '${{existing}}' and '${{pattern.name}}'`);
|
|
}}
|
|
seenIndexes.set(index, pattern.name);
|
|
Object.defineProperty(mergedBy, index, {{
|
|
get() {{ return pattern.get(index); }},
|
|
enumerable: true,
|
|
configurable: true,
|
|
}});
|
|
}}
|
|
}}
|
|
|
|
const allIndexes = /** @type {{Index[]}} */ ([...seenIndexes.keys()]);
|
|
const firstName = patterns[0].name;
|
|
|
|
return {{
|
|
name: firstName,
|
|
by: mergedBy,
|
|
indexes() {{ return allIndexes; }},
|
|
get(index) {{ return mergedBy[index]; }},
|
|
}};
|
|
}}
|
|
"#).unwrap();
|
|
|
|
writeln!(output, "}}\n").unwrap();
|
|
|
|
writeln!(output, "export {{ BrkClient, BrkError }};").unwrap();
|
|
}
|
|
|
|
fn generate_tree_initializer(
|
|
output: &mut String,
|
|
node: &TreeNode,
|
|
accumulated_name: &str,
|
|
indent: usize,
|
|
pattern_lookup: &std::collections::HashMap<Vec<PatternField>, String>,
|
|
metadata: &ClientMetadata,
|
|
) {
|
|
let indent_str = " ".repeat(indent);
|
|
|
|
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 { "" };
|
|
|
|
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!(
|
|
output,
|
|
"{}{}: create{}(this, '{}'){}",
|
|
indent_str, field_name, accessor.name, leaf.name(), comma
|
|
)
|
|
.unwrap();
|
|
}
|
|
TreeNode::Branch(grandchildren) => {
|
|
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
|
if let Some(pattern_name) = pattern_lookup.get(&child_fields) {
|
|
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_pattern_instance_base(child_node)
|
|
} else if accumulated_name.is_empty() {
|
|
format!("/{}", child_name)
|
|
} else {
|
|
format!("{}/{}", accumulated_name, child_name)
|
|
};
|
|
|
|
writeln!(
|
|
output,
|
|
"{}{}: create{}(this, '{}'){}",
|
|
indent_str, field_name, pattern_name, arg, comma
|
|
)
|
|
.unwrap();
|
|
} else {
|
|
let child_acc =
|
|
infer_child_accumulated_name(child_node, accumulated_name, child_name);
|
|
writeln!(output, "{}{}: {{", indent_str, field_name).unwrap();
|
|
generate_tree_initializer(
|
|
output,
|
|
child_node,
|
|
&child_acc,
|
|
indent + 1,
|
|
pattern_lookup,
|
|
metadata,
|
|
);
|
|
writeln!(output, "{}}}{}", indent_str, comma).unwrap();
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
fn infer_child_accumulated_name(node: &TreeNode, parent_acc: &str, field_name: &str) -> String {
|
|
let leaf_name = get_first_leaf_name(node).unwrap_or_default();
|
|
infer_accumulated_name(parent_acc, field_name, &leaf_name)
|
|
}
|