mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-06-08 06:01:57 -07:00
global: MASSIVE snapshot
This commit is contained in:
@@ -14,6 +14,7 @@ bridge/
|
||||
_*
|
||||
!__*.py
|
||||
/*.md
|
||||
/api.json
|
||||
|
||||
# Logs
|
||||
*.log*
|
||||
|
||||
Generated
+182
-164
File diff suppressed because it is too large
Load Diff
+5
-4
@@ -42,14 +42,14 @@ bitcoin = { version = "0.32.8", features = ["serde"] }
|
||||
bitcoincore-rpc = "0.19.0"
|
||||
brk_alloc = { version = "0.1.0-alpha.1", path = "crates/brk_alloc" }
|
||||
brk_bencher = { version = "0.1.0-alpha.1", path = "crates/brk_bencher" }
|
||||
brk_binder = { version = "0.1.0-alpha.1", path = "crates/brk_binder" }
|
||||
brk_bindgen = { version = "0.1.0-alpha.1", path = "crates/brk_bindgen" }
|
||||
brk_bundler = { version = "0.1.0-alpha.1", path = "crates/brk_bundler" }
|
||||
brk_cli = { version = "0.1.0-alpha.1", path = "crates/brk_cli" }
|
||||
brk_client = { version = "0.1.0-alpha.1", path = "crates/brk_client" }
|
||||
brk_cohort = { version = "0.1.0-alpha.1", path = "crates/brk_cohort" }
|
||||
brk_computer = { version = "0.1.0-alpha.1", path = "crates/brk_computer" }
|
||||
brk_error = { version = "0.1.0-alpha.1", path = "crates/brk_error" }
|
||||
brk_fetcher = { version = "0.1.0-alpha.1", path = "crates/brk_fetcher" }
|
||||
brk_grouper = { version = "0.1.0-alpha.1", path = "crates/brk_grouper" }
|
||||
brk_indexer = { version = "0.1.0-alpha.1", path = "crates/brk_indexer" }
|
||||
brk_query = { version = "0.1.0-alpha.1", path = "crates/brk_query", features = ["tokio"] }
|
||||
brk_iterator = { version = "0.1.0-alpha.1", path = "crates/brk_iterator" }
|
||||
@@ -66,8 +66,9 @@ brk_traversable_derive = { version = "0.1.0-alpha.1", path = "crates/brk_travers
|
||||
byteview = "0.9.1"
|
||||
color-eyre = "0.6.5"
|
||||
derive_deref = "1.1.1"
|
||||
env_logger = "0.11.8"
|
||||
# fjall = "3.0.0-rc.6"
|
||||
fjall = { path = "../fjall" }
|
||||
fjall = { git = "https://github.com/fjall-rs/fjall" }
|
||||
jiff = "0.2.17"
|
||||
log = "0.4.29"
|
||||
minreq = { version = "2.14.1", features = ["https", "serde_json"] }
|
||||
@@ -81,7 +82,7 @@ serde_derive = "1.0.228"
|
||||
serde_json = { version = "1.0.148", features = ["float_roundtrip"] }
|
||||
smallvec = "1.15.1"
|
||||
tokio = { version = "1.48.0", features = ["rt-multi-thread"] }
|
||||
# vecdb = { version = "0.5.0", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { version = "0.5.2", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
vecdb = { path = "../anydb/crates/vecdb", features = ["derive", "serde_json", "pco", "schemars"] }
|
||||
# vecdb = { git = "https://github.com/anydb-rs/anydb", features = ["derive", "serde_json", "pco"] }
|
||||
|
||||
|
||||
@@ -32,7 +32,7 @@ full = [
|
||||
"types",
|
||||
]
|
||||
bencher = ["brk_bencher"]
|
||||
binder = ["brk_binder"]
|
||||
binder = ["brk_bindgen"]
|
||||
bundler = ["brk_bundler"]
|
||||
client = ["brk_client"]
|
||||
computer = ["brk_computer"]
|
||||
@@ -54,7 +54,7 @@ types = ["brk_types"]
|
||||
|
||||
[dependencies]
|
||||
brk_bencher = { workspace = true, optional = true }
|
||||
brk_binder = { workspace = true, optional = true }
|
||||
brk_bindgen = { workspace = true, optional = true }
|
||||
brk_bundler = { workspace = true, optional = true }
|
||||
brk_client = { workspace = true, optional = true }
|
||||
brk_computer = { workspace = true, optional = true }
|
||||
|
||||
@@ -6,7 +6,7 @@ pub use brk_bencher as bencher;
|
||||
|
||||
#[cfg(feature = "binder")]
|
||||
#[doc(inline)]
|
||||
pub use brk_binder as binder;
|
||||
pub use brk_bindgen as binder;
|
||||
|
||||
#[cfg(feature = "bundler")]
|
||||
#[doc(inline)]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,999 +0,0 @@
|
||||
use std::{collections::HashSet, fmt::Write as FmtWrite, fs, io, path::Path};
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, EPOCH_NAMES, GE_AMOUNT_NAMES, LT_AMOUNT_NAMES,
|
||||
MAX_AGE_NAMES, MIN_AGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, YEAR_NAMES,
|
||||
};
|
||||
use brk_types::{Index, TreeNode, pools};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
|
||||
TypeSchemas, VERSION, extract_inner_type, get_fields_with_child_info, get_node_fields,
|
||||
get_pattern_instance_base, to_pascal_case, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate Python client from metadata and OpenAPI endpoints.
|
||||
///
|
||||
/// `output_path` is the full path to the output file (e.g., "packages/brk_client/__init__.py").
|
||||
pub fn generate_python_client(
|
||||
metadata: &ClientMetadata,
|
||||
endpoints: &[Endpoint],
|
||||
schemas: &TypeSchemas,
|
||||
output_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let mut output = String::new();
|
||||
|
||||
writeln!(output, "# Auto-generated BRK Python client").unwrap();
|
||||
writeln!(output, "# Do not edit manually\n").unwrap();
|
||||
writeln!(output, "from __future__ import annotations").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Final, Union"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "import httpx\n").unwrap();
|
||||
writeln!(output, "T = TypeVar('T')\n").unwrap();
|
||||
|
||||
generate_type_definitions(&mut output, schemas);
|
||||
generate_base_client(&mut output);
|
||||
generate_metric_node(&mut output);
|
||||
generate_index_accessors(&mut output, &metadata.index_set_patterns);
|
||||
generate_structural_patterns(&mut output, &metadata.structural_patterns, metadata);
|
||||
generate_tree_classes(&mut output, &metadata.catalog, metadata);
|
||||
generate_main_client(&mut output, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_class_constants(output: &mut String) {
|
||||
fn class_const<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
||||
let json = serde_json::to_string_pretty(value).unwrap();
|
||||
// Indent all lines for class body
|
||||
let indented = json
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
if i == 0 {
|
||||
format!(" {} = {}", name, line)
|
||||
} else {
|
||||
format!(" {}", line)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
writeln!(output, "{}\n", indented).unwrap();
|
||||
}
|
||||
|
||||
// VERSION
|
||||
writeln!(output, " VERSION = \"v{}\"\n", VERSION).unwrap();
|
||||
|
||||
// INDEXES
|
||||
let indexes = Index::all();
|
||||
let indexes_list: Vec<&str> = indexes.iter().map(|i| i.serialize_long()).collect();
|
||||
class_const(output, "INDEXES", &indexes_list);
|
||||
|
||||
// POOL_ID_TO_POOL_NAME
|
||||
let pools = pools();
|
||||
let mut sorted_pools: Vec<_> = pools.iter().collect();
|
||||
sorted_pools.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
let pool_map: std::collections::BTreeMap<String, &str> = sorted_pools
|
||||
.iter()
|
||||
.map(|p| (p.slug().to_string(), p.name))
|
||||
.collect();
|
||||
class_const(output, "POOL_ID_TO_POOL_NAME", &pool_map);
|
||||
|
||||
// Cohort names
|
||||
class_const(output, "TERM_NAMES", &TERM_NAMES);
|
||||
class_const(output, "EPOCH_NAMES", &EPOCH_NAMES);
|
||||
class_const(output, "YEAR_NAMES", &YEAR_NAMES);
|
||||
class_const(output, "SPENDABLE_TYPE_NAMES", &SPENDABLE_TYPE_NAMES);
|
||||
class_const(output, "AGE_RANGE_NAMES", &AGE_RANGE_NAMES);
|
||||
class_const(output, "MAX_AGE_NAMES", &MAX_AGE_NAMES);
|
||||
class_const(output, "MIN_AGE_NAMES", &MIN_AGE_NAMES);
|
||||
class_const(output, "AMOUNT_RANGE_NAMES", &AMOUNT_RANGE_NAMES);
|
||||
class_const(output, "GE_AMOUNT_NAMES", &GE_AMOUNT_NAMES);
|
||||
class_const(output, "LT_AMOUNT_NAMES", <_AMOUNT_NAMES);
|
||||
}
|
||||
|
||||
fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
if schemas.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "# Type definitions\n").unwrap();
|
||||
|
||||
let sorted_names = topological_sort_schemas(schemas);
|
||||
|
||||
for name in sorted_names {
|
||||
let Some(schema) = schemas.get(&name) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
|
||||
writeln!(output, "class {}(TypedDict):", name).unwrap();
|
||||
for (prop_name, prop_schema) in props {
|
||||
let prop_type = schema_to_python_type_ctx(prop_schema, Some(&name));
|
||||
let safe_name = escape_python_keyword(prop_name);
|
||||
writeln!(output, " {}: {}", safe_name, prop_type).unwrap();
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
// } else if is_enum_schema(schema) {
|
||||
// let py_type = schema_to_python_type_ctx(schema, Some(&name));
|
||||
// writeln!(output, "{} = {}", name, py_type).unwrap();
|
||||
} else {
|
||||
let py_type = schema_to_python_type_ctx(schema, Some(&name));
|
||||
writeln!(output, "{} = {}", name, py_type).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
|
||||
/// Topologically sort schema names so dependencies come before dependents (avoids forward references).
|
||||
/// Types that reference other types (via $ref) must be defined after their dependencies.
|
||||
fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
// Build dependency graph
|
||||
let mut deps: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
for (name, schema) in schemas {
|
||||
let mut type_deps = HashSet::new();
|
||||
collect_schema_refs(schema, &mut type_deps);
|
||||
// Only keep deps that are in our schemas
|
||||
type_deps.retain(|d| schemas.contains_key(d));
|
||||
deps.insert(name.clone(), type_deps);
|
||||
}
|
||||
|
||||
// Kahn's algorithm for topological sort
|
||||
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
||||
for name in schemas.keys() {
|
||||
in_degree.insert(name.clone(), 0);
|
||||
}
|
||||
for type_deps in deps.values() {
|
||||
for dep in type_deps {
|
||||
*in_degree.entry(dep.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Start with types that have no dependents (are not referenced by others)
|
||||
let mut queue: Vec<String> = in_degree
|
||||
.iter()
|
||||
.filter(|(_, count)| **count == 0)
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
queue.sort(); // Deterministic order
|
||||
|
||||
let mut result = Vec::new();
|
||||
while let Some(name) = queue.pop() {
|
||||
result.push(name.clone());
|
||||
if let Some(type_deps) = deps.get(&name) {
|
||||
for dep in type_deps {
|
||||
if let Some(count) = in_degree.get_mut(dep) {
|
||||
*count = count.saturating_sub(1);
|
||||
if *count == 0 {
|
||||
queue.push(dep.clone());
|
||||
queue.sort(); // Keep sorted for determinism
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse so dependencies come first
|
||||
result.reverse();
|
||||
|
||||
// Add any types that weren't processed (e.g., due to circular refs or other edge cases)
|
||||
let result_set: HashSet<_> = result.iter().cloned().collect();
|
||||
let mut missing: Vec<_> = schemas
|
||||
.keys()
|
||||
.filter(|k| !result_set.contains(*k))
|
||||
.cloned()
|
||||
.collect();
|
||||
missing.sort();
|
||||
result.extend(missing);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Collect all type references ($ref) from a schema
|
||||
fn collect_schema_refs(schema: &Value, refs: &mut std::collections::HashSet<String>) {
|
||||
match schema {
|
||||
Value::Object(map) => {
|
||||
if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str())
|
||||
&& let Some(type_name) = ref_path.rsplit('/').next()
|
||||
{
|
||||
refs.insert(type_name.to_string());
|
||||
}
|
||||
for value in map.values() {
|
||||
collect_schema_refs(value, refs);
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for item in arr {
|
||||
collect_schema_refs(item, refs);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a single JSON type string to Python type
|
||||
fn json_type_to_python(ty: &str, schema: &Value, current_type: Option<&str>) -> String {
|
||||
match ty {
|
||||
"integer" => "int".to_string(),
|
||||
"number" => "float".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"string" => "str".to_string(),
|
||||
"null" => "None".to_string(),
|
||||
"array" => {
|
||||
let item_type = schema
|
||||
.get("items")
|
||||
.map(|s| schema_to_python_type_ctx(s, current_type))
|
||||
.unwrap_or_else(|| "Any".to_string());
|
||||
format!("List[{}]", item_type)
|
||||
}
|
||||
"object" => {
|
||||
if let Some(add_props) = schema.get("additionalProperties") {
|
||||
let value_type = schema_to_python_type_ctx(add_props, current_type);
|
||||
return format!("dict[str, {}]", value_type);
|
||||
}
|
||||
"dict".to_string()
|
||||
}
|
||||
_ => "Any".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert JSON Schema to Python type with context for detecting self-references
|
||||
fn schema_to_python_type_ctx(schema: &Value, current_type: Option<&str>) -> String {
|
||||
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
|
||||
for item in all_of {
|
||||
let resolved = schema_to_python_type_ctx(item, current_type);
|
||||
if resolved != "Any" {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle $ref
|
||||
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
|
||||
let type_name = ref_path.rsplit('/').next().unwrap_or("Any");
|
||||
// Quote self-references to handle recursive types
|
||||
if current_type == Some(type_name) {
|
||||
return format!("\"{}\"", type_name);
|
||||
}
|
||||
return type_name.to_string();
|
||||
}
|
||||
|
||||
// Handle enum (array of string values)
|
||||
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
|
||||
let literals: Vec<String> = enum_values
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect();
|
||||
if !literals.is_empty() {
|
||||
return format!("Literal[{}]", literals.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ty) = schema.get("type") {
|
||||
if let Some(type_array) = ty.as_array() {
|
||||
let types: Vec<String> = type_array
|
||||
.iter()
|
||||
.filter_map(|t| t.as_str())
|
||||
.filter(|t| *t != "null") // Filter out null for cleaner Optional handling
|
||||
.map(|t| json_type_to_python(t, schema, current_type))
|
||||
.collect();
|
||||
let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
|
||||
|
||||
if types.len() == 1 {
|
||||
let base_type = &types[0];
|
||||
return if has_null {
|
||||
format!("Optional[{}]", base_type)
|
||||
} else {
|
||||
base_type.clone()
|
||||
};
|
||||
} else if !types.is_empty() {
|
||||
let union = format!("Union[{}]", types.join(", "));
|
||||
return if has_null {
|
||||
format!("Optional[{}]", union)
|
||||
} else {
|
||||
union
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ty_str) = ty.as_str() {
|
||||
return json_type_to_python(ty_str, schema, current_type);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(variants) = schema
|
||||
.get("anyOf")
|
||||
.or_else(|| schema.get("oneOf"))
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
let types: Vec<String> = variants
|
||||
.iter()
|
||||
.map(|v| schema_to_python_type_ctx(v, current_type))
|
||||
.collect();
|
||||
let filtered: Vec<_> = types.iter().filter(|t| *t != "Any").collect();
|
||||
if !filtered.is_empty() {
|
||||
return format!(
|
||||
"Union[{}]",
|
||||
filtered
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
return format!("Union[{}]", types.join(", "));
|
||||
}
|
||||
|
||||
// Check for format hint without type (common in OpenAPI)
|
||||
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
|
||||
return match format {
|
||||
"int32" | "int64" => "int".to_string(),
|
||||
"float" | "double" => "float".to_string(),
|
||||
"date" | "date-time" => "str".to_string(),
|
||||
_ => "Any".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
"Any".to_string()
|
||||
}
|
||||
|
||||
/// Make a name safe for Python: escape keywords and prefix digit-starting names
|
||||
fn escape_python_keyword(name: &str) -> String {
|
||||
const PYTHON_KEYWORDS: &[&str] = &[
|
||||
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
|
||||
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
|
||||
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
|
||||
"try", "while", "with", "yield",
|
||||
];
|
||||
// Names starting with digit need underscore prefix
|
||||
let name = if name
|
||||
.chars()
|
||||
.next()
|
||||
.map(|c| c.is_ascii_digit())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
format!("_{}", name)
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
// Reserved keywords get underscore suffix
|
||||
if PYTHON_KEYWORDS.contains(&name.as_str()) {
|
||||
format!("{}_", name)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the base BrkClient class with HTTP functionality
|
||||
fn generate_base_client(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"class BrkError(Exception):
|
||||
"""Custom error class for BRK client errors."""
|
||||
|
||||
def __init__(self, message: str, status: Optional[int] = None):
|
||||
super().__init__(message)
|
||||
self.status = status
|
||||
|
||||
|
||||
class BrkClientBase:
|
||||
"""Base HTTP client for making requests."""
|
||||
|
||||
def __init__(self, base_url: str, timeout: float = 30.0):
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self._client = httpx.Client(timeout=timeout)
|
||||
|
||||
def get(self, path: str) -> Any:
|
||||
"""Make a GET request."""
|
||||
try:
|
||||
response = self._client.get(f"{{self.base_url}}{{path}}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise BrkError(f"HTTP error: {{e.response.status_code}}", e.response.status_code)
|
||||
except httpx.RequestError as e:
|
||||
raise BrkError(str(e))
|
||||
|
||||
def close(self):
|
||||
"""Close the HTTP client."""
|
||||
self._client.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the MetricNode class
|
||||
fn generate_metric_node(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"class MetricNode(Generic[T]):
|
||||
"""A metric node that can fetch data for different indexes."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, path: str):
|
||||
self._client = client
|
||||
self._path = path
|
||||
|
||||
def get(self) -> List[T]:
|
||||
"""Fetch all data points for this metric."""
|
||||
return self._client.get(self._path)
|
||||
|
||||
def get_range(self, from_val: Optional[str] = None, to_val: Optional[str] = None) -> List[T]:
|
||||
"""Fetch data points within a range."""
|
||||
params = []
|
||||
if from_val is not None:
|
||||
params.append(f"from={{from_val}}")
|
||||
if to_val is not None:
|
||||
params.append(f"to={{to_val}}")
|
||||
query = "&".join(params)
|
||||
return self._client.get(f"{{self._path}}?{{query}}" if query else self._path)
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate index accessor classes
|
||||
fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "# Index accessor classes\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Index accessor for metrics with {} indexes.\"\"\"",
|
||||
pattern.indexes.len()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, base_path: str):"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for index in &pattern.indexes {
|
||||
let field_name = index_to_snake_case(index);
|
||||
let path_segment = index.serialize_long();
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: MetricNode[T] = MetricNode(client, f'{{base_path}}/{}')",
|
||||
field_name, path_segment
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_date_index)
|
||||
fn index_to_snake_case(index: &Index) -> String {
|
||||
format!("by_{}", to_snake_case(index.serialize_long()))
|
||||
}
|
||||
|
||||
/// Generate structural pattern classes
|
||||
fn generate_structural_patterns(
|
||||
output: &mut String,
|
||||
patterns: &[StructuralPattern],
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "# Reusable structural pattern classes\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let is_parameterizable = pattern.is_parameterizable();
|
||||
|
||||
// For generic patterns, inherit from Generic[T]
|
||||
if pattern.is_generic {
|
||||
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
|
||||
} else {
|
||||
writeln!(output, "class {}:", pattern.name).unwrap();
|
||||
}
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Pattern struct for repeated tree structure.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
|
||||
if is_parameterizable {
|
||||
writeln!(
|
||||
output,
|
||||
" 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 {
|
||||
if is_parameterizable {
|
||||
generate_parameterized_python_field(output, field, pattern, metadata);
|
||||
} else {
|
||||
generate_tree_path_python_field(output, field, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// 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_generic(field, metadata, pattern.is_generic);
|
||||
|
||||
// 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 => "acc".to_string(),
|
||||
FieldNamePosition::SetBase(base) => format!("'{}'", base),
|
||||
}
|
||||
} else {
|
||||
format!("f'{{acc}}_{}'", field.name)
|
||||
};
|
||||
|
||||
if metadata.field_uses_accessor(field) {
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, {})",
|
||||
field_name, py_type, accessor.name, metric_expr
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// Direct MetricNode without indexes - pass metric name
|
||||
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 metadata.field_uses_accessor(field) {
|
||||
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
|
||||
fn field_to_python_type(field: &PatternField, metadata: &ClientMetadata) -> String {
|
||||
field_to_python_type_generic(field, metadata, false)
|
||||
}
|
||||
|
||||
/// Convert pattern field to Python type annotation, with optional generic support
|
||||
fn field_to_python_type_generic(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
) -> String {
|
||||
field_to_python_type_with_generic_value(field, metadata, is_generic, None)
|
||||
}
|
||||
|
||||
/// Convert pattern field to Python type annotation.
|
||||
/// - `is_generic`: If true and field.rust_type is "T", use T in the output
|
||||
/// - `generic_value_type`: For branch fields that reference a generic pattern, this is the concrete type to substitute
|
||||
fn field_to_python_type_with_generic_value(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
generic_value_type: Option<&str>,
|
||||
) -> String {
|
||||
// For generic patterns, use T instead of concrete value type
|
||||
// Also extract inner type from wrappers like Close<Dollars> -> Dollars
|
||||
let value_type = if is_generic && field.rust_type == "T" {
|
||||
"T".to_string()
|
||||
} else {
|
||||
extract_inner_type(&field.rust_type)
|
||||
};
|
||||
|
||||
if metadata.is_pattern_type(&field.rust_type) {
|
||||
if metadata.is_pattern_generic(&field.rust_type) {
|
||||
// Use type_param from field, then generic_value_type, then T if parent is generic
|
||||
let type_param = field
|
||||
.type_param
|
||||
.as_deref()
|
||||
.or(generic_value_type)
|
||||
.unwrap_or(if is_generic { "T" } else { "Any" });
|
||||
return format!("{}[{}]", field.rust_type, type_param);
|
||||
}
|
||||
field.rust_type.clone()
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch struct
|
||||
field.rust_type.clone()
|
||||
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
// Leaf with accessor - use value_type as the generic
|
||||
format!("{}[{}]", accessor.name, value_type)
|
||||
} else {
|
||||
// Leaf - use value_type as the generic
|
||||
format!("MetricNode[{}]", value_type)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate tree classes
|
||||
fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "# Catalog tree classes\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
generate_tree_class(
|
||||
output,
|
||||
"CatalogTree",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
}
|
||||
|
||||
/// Recursively generate tree classes
|
||||
fn generate_tree_class(
|
||||
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();
|
||||
|
||||
// Skip if this matches a pattern (already generated)
|
||||
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, "class {}:", name).unwrap();
|
||||
writeln!(output, " \"\"\"Catalog tree node.\"\"\"").unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, base_path: str = ''):"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for ((field, child_fields_opt), (child_name, child_node)) in
|
||||
fields_with_child_info.iter().zip(children.iter())
|
||||
{
|
||||
// Look up type parameter for generic patterns
|
||||
let generic_value_type = child_fields_opt
|
||||
.as_ref()
|
||||
.and_then(|cf| metadata.get_type_param(cf))
|
||||
.map(String::as_str);
|
||||
let py_type =
|
||||
field_to_python_type_with_generic_value(field, metadata, false, generic_value_type);
|
||||
let field_name_py = to_snake_case(&field.name);
|
||||
|
||||
if metadata.is_pattern_type(&field.rust_type) {
|
||||
let pattern = metadata.find_pattern(&field.rust_type);
|
||||
let is_parameterizable = pattern.is_some_and(|p| p.is_parameterizable());
|
||||
|
||||
if is_parameterizable {
|
||||
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 metadata.field_uses_accessor(field) {
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, accessor.name, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch - instantiate the nested class
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, field.rust_type, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// Leaf metric - direct MetricNode with full API path
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = MetricNode(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, field.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate child classes
|
||||
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_class_name = format!("{}_{}", name, to_pascal_case(child_name));
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child_class_name,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate the main client class
|
||||
fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Main BRK client with catalog tree and API methods.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate class-level constants
|
||||
generate_class_constants(output);
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0):"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " super().__init__(base_url, timeout)").unwrap();
|
||||
writeln!(output, " self.tree = CatalogTree(self)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate API methods
|
||||
generate_api_methods(output, endpoints);
|
||||
}
|
||||
|
||||
/// Generate API methods from OpenAPI endpoints
|
||||
fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
for endpoint in endpoints {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = endpoint
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_python)
|
||||
.unwrap_or_else(|| "Any".to_string());
|
||||
|
||||
// Build method signature
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" def {}(self{}) -> {}:",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Docstring
|
||||
match (&endpoint.summary, &endpoint.description) {
|
||||
(Some(summary), Some(desc)) if summary != desc => {
|
||||
writeln!(output, " \"\"\"{}.", summary.trim_end_matches('.')).unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " {}\"\"\"", desc).unwrap();
|
||||
}
|
||||
(Some(summary), _) => {
|
||||
writeln!(output, " \"\"\"{}\"\"\"", summary).unwrap();
|
||||
}
|
||||
(None, Some(desc)) => {
|
||||
writeln!(output, " \"\"\"{}\"\"\"", desc).unwrap();
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
|
||||
// Build path
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
if endpoint.path_params.is_empty() {
|
||||
writeln!(output, " return self.get('{}')", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get(f'{}')", path).unwrap();
|
||||
}
|
||||
} else {
|
||||
writeln!(output, " params = []").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
// Use safe name for Python variable, original name for API query parameter
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.append(f'{}={{{}}}')",
|
||||
param.name, safe_name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if {} is not None: params.append(f'{}={{{}}}')",
|
||||
safe_name, param.name, safe_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " query = '&'.join(params)").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return self.get(f'{}{{\"?\" + query if query else \"\"}}')",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
to_snake_case(&endpoint.operation_name())
|
||||
}
|
||||
|
||||
/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]", "number" -> "int")
|
||||
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(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"string" => "str".to_string(),
|
||||
"null" => "None".to_string(),
|
||||
"Object" | "object" => "dict".to_string(),
|
||||
"*" => "Any".to_string(),
|
||||
_ => js_type.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
let py_type = js_type_to_python(¶m.param_type);
|
||||
params.push(format!(", {}: {}", safe_name, py_type));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
let py_type = js_type_to_python(¶m.param_type);
|
||||
if param.required {
|
||||
params.push(format!(", {}: {}", safe_name, py_type));
|
||||
} else {
|
||||
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[super::Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
let placeholder = format!("{{{}}}", param.name);
|
||||
// Use escaped name for Python variable interpolation in f-string
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
let interpolation = format!("{{{}}}", safe_name);
|
||||
result = result.replace(&placeholder, &interpolation);
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -1,733 +0,0 @@
|
||||
use std::{collections::HashSet, fmt::Write as FmtWrite, fs, io, path::Path};
|
||||
|
||||
use brk_types::{Index, TreeNode};
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
|
||||
extract_inner_type, get_fields_with_child_info, get_node_fields, get_pattern_instance_base,
|
||||
to_pascal_case, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate Rust client from metadata and OpenAPI endpoints.
|
||||
///
|
||||
/// `output_path` is the full path to the output file (e.g., "crates/brk_client/src/lib.rs").
|
||||
pub fn generate_rust_client(
|
||||
metadata: &ClientMetadata,
|
||||
endpoints: &[Endpoint],
|
||||
output_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let mut output = String::new();
|
||||
|
||||
writeln!(output, "// Auto-generated BRK Rust client").unwrap();
|
||||
writeln!(output, "// Do not edit manually\n").unwrap();
|
||||
writeln!(output, "#![allow(non_camel_case_types)]").unwrap();
|
||||
writeln!(output, "#![allow(dead_code)]").unwrap();
|
||||
writeln!(output, "#![allow(unused_variables)]").unwrap();
|
||||
writeln!(output, "#![allow(clippy::useless_format)]").unwrap();
|
||||
writeln!(output, "#![allow(clippy::unnecessary_to_owned)]\n").unwrap();
|
||||
|
||||
generate_imports(&mut output);
|
||||
generate_base_client(&mut output);
|
||||
generate_metric_node(&mut output);
|
||||
generate_index_accessors(&mut output, &metadata.index_set_patterns);
|
||||
generate_pattern_structs(&mut output, &metadata.structural_patterns, metadata);
|
||||
generate_tree(&mut output, &metadata.catalog, metadata);
|
||||
generate_main_client(&mut output, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn generate_imports(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"use std::sync::Arc;
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use brk_cohort::*;
|
||||
pub use brk_types::*;
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn generate_base_client(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Error type for BRK client operations.
|
||||
#[derive(Debug)]
|
||||
pub struct BrkError {{
|
||||
pub message: String,
|
||||
}}
|
||||
|
||||
impl std::fmt::Display for BrkError {{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
|
||||
write!(f, "{{}}", self.message)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl std::error::Error for BrkError {{}}
|
||||
|
||||
/// Result type for BRK client operations.
|
||||
pub type Result<T> = std::result::Result<T, BrkError>;
|
||||
|
||||
/// Options for configuring the BRK client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientOptions {{
|
||||
pub base_url: String,
|
||||
pub timeout_secs: u64,
|
||||
}}
|
||||
|
||||
impl Default for BrkClientOptions {{
|
||||
fn default() -> Self {{
|
||||
Self {{
|
||||
base_url: "http://localhost:3000".to_string(),
|
||||
timeout_secs: 30,
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Base HTTP client for making requests.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientBase {{
|
||||
base_url: String,
|
||||
timeout_secs: u64,
|
||||
}}
|
||||
|
||||
impl BrkClientBase {{
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {{
|
||||
Self {{
|
||||
base_url: base_url.into(),
|
||||
timeout_secs: 30,
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Create a new client with options.
|
||||
pub fn with_options(options: BrkClientOptions) -> Self {{
|
||||
Self {{
|
||||
base_url: options.base_url,
|
||||
timeout_secs: options.timeout_secs,
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Make a GET request.
|
||||
pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {{
|
||||
let url = format!("{{}}{{}}", self.base_url, path);
|
||||
let response = minreq::get(&url)
|
||||
.with_timeout(self.timeout_secs)
|
||||
.send()
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})?;
|
||||
|
||||
if response.status_code >= 400 {{
|
||||
return Err(BrkError {{
|
||||
message: format!("HTTP {{}}", response.status_code),
|
||||
}});
|
||||
}}
|
||||
|
||||
response
|
||||
.json()
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn generate_metric_node(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// A metric node that can fetch data for different indexes.
|
||||
pub struct MetricNode<T> {{
|
||||
client: Arc<BrkClientBase>,
|
||||
path: String,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> MetricNode<T> {{
|
||||
pub fn new(client: Arc<BrkClientBase>, path: String) -> Self {{
|
||||
Self {{
|
||||
client,
|
||||
path,
|
||||
_marker: std::marker::PhantomData,
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Fetch all data points for this metric.
|
||||
pub fn get(&self) -> Result<Vec<T>> {{
|
||||
self.client.get(&self.path)
|
||||
}}
|
||||
|
||||
/// Fetch data points within a range.
|
||||
pub fn get_range(&self, from: Option<&str>, to: Option<&str>) -> Result<Vec<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)); }}
|
||||
let path = if params.is_empty() {{
|
||||
self.path.clone()
|
||||
}} else {{
|
||||
format!("{{}}?{{}}", self.path, params.join("&"))
|
||||
}};
|
||||
self.client.get(&path)
|
||||
}}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Index accessor structs\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
writeln!(
|
||||
output,
|
||||
"/// Index accessor for metrics with {} indexes.",
|
||||
pattern.indexes.len()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "pub struct {}<T> {{", pattern.name).unwrap();
|
||||
|
||||
for index in &pattern.indexes {
|
||||
let field_name = index_to_field_name(index);
|
||||
writeln!(output, " pub {}: MetricNode<T>,", field_name).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block with constructor
|
||||
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", pattern.name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
for index in &pattern.indexes {
|
||||
let field_name = index_to_field_name(index);
|
||||
let path_segment = index.serialize_long();
|
||||
writeln!(
|
||||
output,
|
||||
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}/{}\")),",
|
||||
field_name, path_segment
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn index_to_field_name(index: &Index) -> String {
|
||||
format!("by_{}", to_snake_case(index.serialize_long()))
|
||||
}
|
||||
|
||||
fn generate_pattern_structs(
|
||||
output: &mut String,
|
||||
patterns: &[StructuralPattern],
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Reusable pattern structs\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let is_parameterizable = pattern.is_parameterizable();
|
||||
let generic_params = if pattern.is_generic { "<T>" } else { "" };
|
||||
|
||||
writeln!(output, "/// Pattern struct for repeated tree structure.").unwrap();
|
||||
writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap();
|
||||
|
||||
for field in &pattern.fields {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
let type_annotation =
|
||||
field_to_type_annotation_generic(field, metadata, pattern.is_generic);
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block with constructor
|
||||
let impl_generic = if pattern.is_generic {
|
||||
"<T: DeserializeOwned>"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"impl{} {}{} {{",
|
||||
impl_generic, pattern.name, generic_params
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if is_parameterizable {
|
||||
writeln!(
|
||||
output,
|
||||
" /// Create a new pattern node with accumulated metric name."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, acc: &str) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
for field in &pattern.fields {
|
||||
if is_parameterizable {
|
||||
generate_parameterized_rust_field(output, field, pattern, metadata);
|
||||
} else {
|
||||
generate_tree_path_rust_field(output, field, metadata);
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_parameterized_rust_field(
|
||||
output: &mut String,
|
||||
field: &PatternField,
|
||||
pattern: &StructuralPattern,
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
|
||||
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.clone(), {}),",
|
||||
field_name, field.rust_type, child_acc
|
||||
)
|
||||
.unwrap();
|
||||
return;
|
||||
}
|
||||
|
||||
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 => "acc.to_string()".to_string(),
|
||||
FieldNamePosition::SetBase(base) => format!("\"{}\".to_string()", base),
|
||||
}
|
||||
} else {
|
||||
format!("format!(\"{{acc}}_{}\")", field.name)
|
||||
};
|
||||
|
||||
if metadata.field_uses_accessor(field) {
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), &{}),",
|
||||
field_name, accessor.name, metric_expr
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" {}: MetricNode::new(client.clone(), {}),",
|
||||
field_name, metric_expr
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
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.clone(), &format!(\"{{base_path}}_{}\")),",
|
||||
field_name, field.rust_type, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if metadata.field_uses_accessor(field) {
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), &format!(\"{{base_path}}_{}\")),",
|
||||
field_name, accessor.name, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}_{}\")),",
|
||||
field_name, field.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn field_to_type_annotation_generic(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
) -> String {
|
||||
field_to_type_annotation_with_generic(field, metadata, is_generic, None)
|
||||
}
|
||||
|
||||
fn field_to_type_annotation_with_generic(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
generic_value_type: Option<&str>,
|
||||
) -> String {
|
||||
let value_type = if is_generic && field.rust_type == "T" {
|
||||
"T".to_string()
|
||||
} else {
|
||||
extract_inner_type(&field.rust_type)
|
||||
};
|
||||
|
||||
if metadata.is_pattern_type(&field.rust_type) {
|
||||
if metadata.is_pattern_generic(&field.rust_type) {
|
||||
// Use type_param from field, then generic_value_type, then T if parent is generic
|
||||
let type_param = field
|
||||
.type_param
|
||||
.as_deref()
|
||||
.or(generic_value_type)
|
||||
.unwrap_or(if is_generic { "T" } else { "_" });
|
||||
return format!("{}<{}>", field.rust_type, type_param);
|
||||
}
|
||||
field.rust_type.clone()
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch struct
|
||||
field.rust_type.clone()
|
||||
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
format!("{}<{}>", accessor.name, value_type)
|
||||
} else {
|
||||
format!("MetricNode<{}>", value_type)
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "// Catalog tree\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
generate_tree_node(
|
||||
output,
|
||||
"CatalogTree",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
}
|
||||
|
||||
fn generate_tree_node(
|
||||
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 let Some(pattern_name) = pattern_lookup.get(&fields)
|
||||
&& pattern_name != name
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if generated.contains(name) {
|
||||
return;
|
||||
}
|
||||
generated.insert(name.to_string());
|
||||
|
||||
writeln!(output, "/// Catalog tree node.").unwrap();
|
||||
writeln!(output, "pub struct {} {{", name).unwrap();
|
||||
|
||||
for (field, child_fields) in &fields_with_child_info {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
// Look up type parameter for generic patterns
|
||||
let generic_value_type = child_fields
|
||||
.as_ref()
|
||||
.and_then(|cf| metadata.get_type_param(cf))
|
||||
.map(String::as_str);
|
||||
let type_annotation =
|
||||
field_to_type_annotation_with_generic(field, metadata, false, generic_value_type);
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
writeln!(output, "impl {} {{", name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
for (field, (child_name, child_node)) in fields.iter().zip(children.iter()) {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
if metadata.is_pattern_type(&field.rust_type) {
|
||||
let pattern = metadata.find_pattern(&field.rust_type);
|
||||
let is_parameterizable = pattern.is_some_and(|p| p.is_parameterizable());
|
||||
|
||||
if is_parameterizable {
|
||||
let metric_base = get_pattern_instance_base(child_node, child_name);
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), \"{}\"),",
|
||||
field_name, field.rust_type, metric_base
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), &format!(\"{{base_path}}_{}\")),",
|
||||
field_name, field.rust_type, field.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
} else if metadata.field_uses_accessor(field) {
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), &format!(\"{{base_path}}_{}\")),",
|
||||
field_name, accessor.name, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch - instantiate the nested struct
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), &format!(\"{{base_path}}_{}\")),",
|
||||
field_name, field.rust_type, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// Leaf - use MetricNode with base_path
|
||||
writeln!(
|
||||
output,
|
||||
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}_{}\")),",
|
||||
field_name, field.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").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_struct_name = format!("{}_{}", name, to_pascal_case(child_name));
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child_struct_name,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Main BRK client with catalog tree and API methods.
|
||||
pub struct BrkClient {{
|
||||
base: Arc<BrkClientBase>,
|
||||
tree: CatalogTree,
|
||||
}}
|
||||
|
||||
impl BrkClient {{
|
||||
/// Client version.
|
||||
pub const VERSION: &'static str = "v{VERSION}";
|
||||
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {{
|
||||
let base = Arc::new(BrkClientBase::new(base_url));
|
||||
let tree = CatalogTree::new(base.clone(), "");
|
||||
Self {{ base, tree }}
|
||||
}}
|
||||
|
||||
/// Create a new client with options.
|
||||
pub fn with_options(options: BrkClientOptions) -> Self {{
|
||||
let base = Arc::new(BrkClientBase::with_options(options));
|
||||
let tree = CatalogTree::new(base.clone(), "");
|
||||
Self {{ base, tree }}
|
||||
}}
|
||||
|
||||
/// Get the catalog tree for navigating metrics.
|
||||
pub fn tree(&self) -> &CatalogTree {{
|
||||
&self.tree
|
||||
}}
|
||||
"#,
|
||||
VERSION = crate::VERSION
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
generate_api_methods(output, endpoints);
|
||||
|
||||
writeln!(output, "}}").unwrap();
|
||||
}
|
||||
|
||||
fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
for endpoint in endpoints {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = endpoint
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_rust)
|
||||
.unwrap_or_else(|| "serde_json::Value".to_string());
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" /// {}",
|
||||
endpoint.summary.as_deref().unwrap_or(&method_name)
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " ///").unwrap();
|
||||
writeln!(output, " /// {}", desc).unwrap();
|
||||
}
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self{}) -> Result<{}> {{",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " self.base.get(&format!(\"{}\"))", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " let mut query = Vec::new();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" query.push(format!(\"{}={{}}\", {}));",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get(&format!(\"{}{{}}\", query_str))",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
to_snake_case(&endpoint.operation_name())
|
||||
}
|
||||
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
params.push(format!(", {}: &str", param.name));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
params.push(format!(", {}: &str", param.name));
|
||||
} else {
|
||||
params.push(format!(", {}: Option<&str>", param.name));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[super::Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
let placeholder = format!("{{{}}}", param.name);
|
||||
let interpolation = format!("{{{}}}", param.name);
|
||||
result = result.replace(&placeholder, &interpolation);
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn js_type_to_rust(js_type: &str) -> String {
|
||||
if let Some(inner) = js_type.strip_suffix("[]") {
|
||||
format!("Vec<{}>", js_type_to_rust(inner))
|
||||
} else {
|
||||
match js_type {
|
||||
"string" => "String".to_string(),
|
||||
"number" => "f64".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"*" => "serde_json::Value".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,193 +0,0 @@
|
||||
mod case;
|
||||
mod patterns;
|
||||
mod schema;
|
||||
mod tree;
|
||||
|
||||
pub use case::*;
|
||||
pub use schema::*;
|
||||
pub use tree::*;
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use brk_query::Vecs;
|
||||
use brk_types::Index;
|
||||
|
||||
/// 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.
|
||||
#[derive(Debug)]
|
||||
pub struct ClientMetadata {
|
||||
/// The catalog tree structure (with schemas in leaves)
|
||||
pub catalog: brk_types::TreeNode,
|
||||
/// Structural patterns - tree node shapes that repeat
|
||||
pub structural_patterns: Vec<StructuralPattern>,
|
||||
/// All indexes used across the catalog
|
||||
pub used_indexes: BTreeSet<Index>,
|
||||
/// Index set patterns - sets of indexes that appear together on metrics
|
||||
pub index_set_patterns: Vec<IndexSetPattern>,
|
||||
/// Maps concrete field signatures to pattern names
|
||||
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>,
|
||||
}
|
||||
|
||||
impl ClientMetadata {
|
||||
/// Extract metadata from brk_query::Vecs.
|
||||
pub fn from_vecs(vecs: &Vecs) -> Self {
|
||||
let catalog = vecs.catalog().clone();
|
||||
let (structural_patterns, concrete_to_pattern, concrete_to_type_param) =
|
||||
patterns::detect_structural_patterns(&catalog);
|
||||
let (used_indexes, index_set_patterns) = tree::detect_index_patterns(&catalog);
|
||||
|
||||
ClientMetadata {
|
||||
catalog,
|
||||
structural_patterns,
|
||||
used_indexes,
|
||||
index_set_patterns,
|
||||
concrete_to_pattern,
|
||||
concrete_to_type_param,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an index set pattern that matches the given indexes.
|
||||
pub fn find_index_set_pattern(&self, indexes: &BTreeSet<Index>) -> Option<&IndexSetPattern> {
|
||||
self.index_set_patterns
|
||||
.iter()
|
||||
.find(|p| &p.indexes == indexes)
|
||||
}
|
||||
|
||||
/// Check if a type is a structural pattern name.
|
||||
pub fn is_pattern_type(&self, type_name: &str) -> bool {
|
||||
self.structural_patterns.iter().any(|p| p.name == type_name)
|
||||
}
|
||||
|
||||
/// Find a pattern by name.
|
||||
pub fn find_pattern(&self, name: &str) -> Option<&StructuralPattern> {
|
||||
self.structural_patterns.iter().find(|p| p.name == name)
|
||||
}
|
||||
|
||||
/// Check if a pattern is generic.
|
||||
pub fn is_pattern_generic(&self, name: &str) -> bool {
|
||||
self.find_pattern(name).is_some_and(|p| p.is_generic)
|
||||
}
|
||||
|
||||
/// Get the type parameter for a generic pattern given its concrete fields.
|
||||
pub fn get_type_param(&self, fields: &[PatternField]) -> Option<&String> {
|
||||
self.concrete_to_type_param.get(fields)
|
||||
}
|
||||
|
||||
/// Build a lookup map from field signatures to pattern names.
|
||||
pub fn pattern_lookup(&self) -> HashMap<Vec<PatternField>, String> {
|
||||
let mut lookup = self.concrete_to_pattern.clone();
|
||||
for p in &self.structural_patterns {
|
||||
lookup.insert(p.fields.clone(), p.name.clone());
|
||||
}
|
||||
lookup
|
||||
}
|
||||
|
||||
/// Check if a field should use a shared index accessor.
|
||||
pub fn field_uses_accessor(&self, field: &PatternField) -> bool {
|
||||
self.find_index_set_pattern(&field.indexes).is_some()
|
||||
}
|
||||
}
|
||||
|
||||
/// A pattern of indexes that appear together on multiple metrics.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndexSetPattern {
|
||||
/// Pattern name (e.g., "DateHeightIndexes")
|
||||
pub name: String,
|
||||
/// The set of indexes
|
||||
pub indexes: BTreeSet<Index>,
|
||||
}
|
||||
|
||||
/// A structural pattern - a branch structure that appears multiple times.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StructuralPattern {
|
||||
/// Pattern name
|
||||
pub name: String,
|
||||
/// Ordered list of child fields
|
||||
pub fields: Vec<PatternField>,
|
||||
/// How each field modifies the accumulated name
|
||||
pub field_positions: HashMap<String, FieldNamePosition>,
|
||||
/// If true, all leaf fields use a type parameter T
|
||||
pub is_generic: bool,
|
||||
}
|
||||
|
||||
impl StructuralPattern {
|
||||
/// Returns true if this pattern contains any leaf fields.
|
||||
pub fn contains_leaves(&self) -> bool {
|
||||
self.fields.iter().any(|f| f.is_leaf())
|
||||
}
|
||||
|
||||
/// Returns true if all leaf fields have consistent name transformations.
|
||||
pub fn is_parameterizable(&self) -> bool {
|
||||
!self.field_positions.is_empty()
|
||||
&& self
|
||||
.fields
|
||||
.iter()
|
||||
.all(|f| f.is_branch() || 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.
|
||||
#[derive(Debug, Clone, PartialOrd, Ord)]
|
||||
pub struct PatternField {
|
||||
/// Field name
|
||||
pub name: String,
|
||||
/// Rust type for leaves or pattern name for branches
|
||||
pub rust_type: String,
|
||||
/// JSON type from schema
|
||||
pub json_type: String,
|
||||
/// For leaves: the set of supported indexes. Empty for branches.
|
||||
pub indexes: BTreeSet<Index>,
|
||||
/// For branches referencing generic patterns: the concrete type parameter
|
||||
pub type_param: Option<String>,
|
||||
}
|
||||
|
||||
impl PatternField {
|
||||
/// Returns true if this is a leaf field (has indexes).
|
||||
pub fn is_leaf(&self) -> bool {
|
||||
!self.indexes.is_empty()
|
||||
}
|
||||
|
||||
/// Returns true if this is a branch field (no indexes).
|
||||
pub fn is_branch(&self) -> bool {
|
||||
self.indexes.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for PatternField {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.rust_type.hash(state);
|
||||
self.json_type.hash(state);
|
||||
// Note: child_fields not included in hash for pattern matching purposes
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PatternField {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
&& self.rust_type == other.rust_type
|
||||
&& self.json_type == other.json_type
|
||||
// Note: child_fields not included in equality for pattern matching purposes
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PatternField {}
|
||||
@@ -1,435 +0,0 @@
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use super::{
|
||||
FieldNamePosition, PatternField, StructuralPattern, case::to_pascal_case,
|
||||
schema::schema_to_json_type,
|
||||
tree::{get_first_leaf_name, get_node_fields},
|
||||
};
|
||||
|
||||
/// Detect structural patterns in the tree using a bottom-up approach.
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
|
||||
pub fn detect_structural_patterns(
|
||||
tree: &TreeNode,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let mut signature_to_pattern: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut signature_counts: HashMap<Vec<PatternField>, usize> = HashMap::new();
|
||||
let mut normalized_to_name: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut name_counts: HashMap<String, usize> = HashMap::new();
|
||||
let mut signature_to_child_fields: HashMap<Vec<PatternField>, Vec<Vec<PatternField>>> =
|
||||
HashMap::new();
|
||||
|
||||
resolve_branch_patterns(
|
||||
tree,
|
||||
"root",
|
||||
&mut signature_to_pattern,
|
||||
&mut signature_counts,
|
||||
&mut normalized_to_name,
|
||||
&mut name_counts,
|
||||
&mut signature_to_child_fields,
|
||||
);
|
||||
|
||||
let (generic_patterns, generic_mappings, type_mappings) =
|
||||
detect_generic_patterns(&signature_to_pattern);
|
||||
|
||||
let mut patterns: Vec<StructuralPattern> = signature_to_pattern
|
||||
.iter()
|
||||
.filter(|(sig, _)| {
|
||||
signature_counts.get(*sig).copied().unwrap_or(0) >= 2
|
||||
&& !generic_mappings.contains_key(*sig)
|
||||
})
|
||||
.map(|(fields, name)| {
|
||||
let child_fields_list = signature_to_child_fields.get(fields);
|
||||
let fields_with_type_params = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| {
|
||||
let type_param = child_fields_list
|
||||
.and_then(|list| list.get(i))
|
||||
.and_then(|cf| type_mappings.get(cf).cloned());
|
||||
PatternField {
|
||||
type_param,
|
||||
..f.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
StructuralPattern {
|
||||
name: name.clone(),
|
||||
fields: fields_with_type_params,
|
||||
field_positions: HashMap::new(),
|
||||
is_generic: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
patterns.extend(generic_patterns);
|
||||
|
||||
let mut pattern_lookup: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
for (sig, name) in &signature_to_pattern {
|
||||
if signature_counts.get(sig).copied().unwrap_or(0) >= 2 {
|
||||
pattern_lookup.insert(sig.clone(), name.clone());
|
||||
}
|
||||
}
|
||||
pattern_lookup.extend(generic_mappings.clone());
|
||||
|
||||
let concrete_to_pattern = pattern_lookup.clone();
|
||||
|
||||
analyze_pattern_field_positions(tree, &mut patterns, &pattern_lookup);
|
||||
|
||||
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
|
||||
(patterns, concrete_to_pattern, type_mappings)
|
||||
}
|
||||
|
||||
/// Detect generic patterns by grouping signatures by their normalized form.
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
|
||||
fn detect_generic_patterns(
|
||||
signature_to_pattern: &HashMap<Vec<PatternField>, String>,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
// Group by normalized form, tracking the extracted type for each concrete signature
|
||||
let mut normalized_groups: HashMap<
|
||||
Vec<PatternField>,
|
||||
Vec<(Vec<PatternField>, String, String)>,
|
||||
> = HashMap::new();
|
||||
|
||||
for (fields, name) in signature_to_pattern {
|
||||
if let Some((normalized, extracted_type)) = normalize_fields_for_generic(fields) {
|
||||
normalized_groups
|
||||
.entry(normalized)
|
||||
.or_default()
|
||||
.push((fields.clone(), name.clone(), extracted_type));
|
||||
}
|
||||
}
|
||||
|
||||
let mut patterns = Vec::new();
|
||||
let mut pattern_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut type_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
|
||||
for (normalized_fields, group) in normalized_groups {
|
||||
if group.len() >= 2 {
|
||||
let generic_name = group[0].1.clone();
|
||||
for (concrete_fields, _, extracted_type) in &group {
|
||||
pattern_mappings.insert(concrete_fields.clone(), generic_name.clone());
|
||||
type_mappings.insert(concrete_fields.clone(), extracted_type.clone());
|
||||
}
|
||||
patterns.push(StructuralPattern {
|
||||
name: generic_name,
|
||||
fields: normalized_fields,
|
||||
field_positions: HashMap::new(),
|
||||
is_generic: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(patterns, pattern_mappings, type_mappings)
|
||||
}
|
||||
|
||||
/// Normalize fields by replacing concrete value types with "T".
|
||||
/// Returns (normalized_fields, extracted_type) where extracted_type is the concrete type replaced.
|
||||
fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<(Vec<PatternField>, String)> {
|
||||
let leaf_types: Vec<&str> = fields
|
||||
.iter()
|
||||
.filter(|f| f.is_leaf())
|
||||
.map(|f| f.rust_type.as_str())
|
||||
.collect();
|
||||
|
||||
if leaf_types.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first_type = leaf_types[0];
|
||||
if !leaf_types.iter().all(|t| *t == first_type) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let normalized = fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if f.is_branch() {
|
||||
f.clone()
|
||||
} else {
|
||||
PatternField {
|
||||
name: f.name.clone(),
|
||||
rust_type: "T".to_string(),
|
||||
json_type: "T".to_string(),
|
||||
indexes: f.indexes.clone(),
|
||||
type_param: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some((normalized, super::extract_inner_type(first_type)))
|
||||
}
|
||||
|
||||
/// Recursively resolve branch patterns bottom-up.
|
||||
/// Returns (pattern_name, fields) for parent's child_fields tracking.
|
||||
fn resolve_branch_patterns(
|
||||
node: &TreeNode,
|
||||
field_name: &str,
|
||||
signature_to_pattern: &mut HashMap<Vec<PatternField>, String>,
|
||||
signature_counts: &mut HashMap<Vec<PatternField>, usize>,
|
||||
normalized_to_name: &mut HashMap<Vec<PatternField>, String>,
|
||||
name_counts: &mut HashMap<String, usize>,
|
||||
signature_to_child_fields: &mut HashMap<Vec<PatternField>, Vec<Vec<PatternField>>>,
|
||||
) -> Option<(String, Vec<PatternField>)> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut fields: Vec<PatternField> = Vec::new();
|
||||
let mut child_fields_vec: Vec<Vec<PatternField>> = Vec::new();
|
||||
|
||||
for (child_name, child_node) in children {
|
||||
let (rust_type, json_type, indexes, child_fields) = match child_node {
|
||||
TreeNode::Leaf(leaf) => (
|
||||
leaf.value_type().to_string(),
|
||||
schema_to_json_type(&leaf.schema),
|
||||
leaf.indexes().clone(),
|
||||
Vec::new(),
|
||||
),
|
||||
TreeNode::Branch(_) => {
|
||||
let (pattern_name, child_pattern_fields) = resolve_branch_patterns(
|
||||
child_node,
|
||||
child_name,
|
||||
signature_to_pattern,
|
||||
signature_counts,
|
||||
normalized_to_name,
|
||||
name_counts,
|
||||
signature_to_child_fields,
|
||||
)
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Vec::new()));
|
||||
(
|
||||
pattern_name.clone(),
|
||||
pattern_name,
|
||||
BTreeSet::new(),
|
||||
child_pattern_fields,
|
||||
)
|
||||
}
|
||||
};
|
||||
fields.push(PatternField {
|
||||
name: child_name.clone(),
|
||||
rust_type,
|
||||
json_type,
|
||||
indexes,
|
||||
type_param: None,
|
||||
});
|
||||
child_fields_vec.push(child_fields);
|
||||
}
|
||||
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
*signature_counts.entry(fields.clone()).or_insert(0) += 1;
|
||||
|
||||
// Store child fields for type param resolution later
|
||||
signature_to_child_fields
|
||||
.entry(fields.clone())
|
||||
.or_insert(child_fields_vec);
|
||||
|
||||
let pattern_name = if let Some(existing) = signature_to_pattern.get(&fields) {
|
||||
existing.clone()
|
||||
} else {
|
||||
let normalized = normalize_fields_for_naming(&fields);
|
||||
let name = normalized_to_name
|
||||
.entry(normalized)
|
||||
.or_insert_with(|| generate_pattern_name(field_name, name_counts))
|
||||
.clone();
|
||||
signature_to_pattern.insert(fields.clone(), name.clone());
|
||||
name
|
||||
};
|
||||
|
||||
Some((pattern_name, fields))
|
||||
}
|
||||
|
||||
/// Normalize fields for naming (same structure = same name).
|
||||
fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec<PatternField> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if f.is_branch() {
|
||||
f.clone()
|
||||
} else {
|
||||
PatternField {
|
||||
name: f.name.clone(),
|
||||
rust_type: "_".to_string(),
|
||||
json_type: "_".to_string(),
|
||||
indexes: f.indexes.clone(),
|
||||
type_param: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a unique pattern name.
|
||||
fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<String, usize>) -> String {
|
||||
let pascal = to_pascal_case(field_name);
|
||||
let sanitized = if pascal.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
format!("_{}", pascal)
|
||||
} else {
|
||||
pascal
|
||||
};
|
||||
|
||||
let base_name = format!("{}Pattern", sanitized);
|
||||
let count = name_counts.entry(base_name.clone()).or_insert(0);
|
||||
*count += 1;
|
||||
|
||||
if *count == 1 {
|
||||
base_name
|
||||
} else {
|
||||
format!("{}{}", base_name, count)
|
||||
}
|
||||
}
|
||||
|
||||
// Field position analysis
|
||||
|
||||
fn analyze_pattern_field_positions(
|
||||
tree: &TreeNode,
|
||||
patterns: &mut [StructuralPattern],
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let mut instances: HashMap<String, Vec<(String, String, String)>> = HashMap::new();
|
||||
collect_pattern_instances(tree, "", &mut instances, pattern_lookup);
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn collect_pattern_instances(
|
||||
node: &TreeNode,
|
||||
accumulated_name: &str,
|
||||
instances: &mut HashMap<String, Vec<(String, String, String)>>,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
return;
|
||||
};
|
||||
|
||||
let fields = get_node_fields(children, pattern_lookup);
|
||||
if let Some(pattern_name) = pattern_lookup.get(&fields) {
|
||||
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(),
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (field_name, child_node) in children {
|
||||
let child_accumulated = match child_node {
|
||||
TreeNode::Leaf(leaf) => leaf.name().to_string(),
|
||||
TreeNode::Branch(_) => {
|
||||
if let Some(desc_leaf_name) = get_first_leaf_name(child_node) {
|
||||
infer_accumulated_name(accumulated_name, field_name, &desc_leaf_name)
|
||||
} else if accumulated_name.is_empty() {
|
||||
field_name.clone()
|
||||
} else {
|
||||
format!("{}_{}", accumulated_name, field_name)
|
||||
}
|
||||
}
|
||||
};
|
||||
collect_pattern_instances(child_node, &child_accumulated, instances, pattern_lookup);
|
||||
}
|
||||
}
|
||||
|
||||
fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_leaf: &str) -> String {
|
||||
if let Some(pos) = descendant_leaf.find(field_name) {
|
||||
if pos == 0 {
|
||||
return field_name.to_string();
|
||||
}
|
||||
if pos > 0 && descendant_leaf.chars().nth(pos - 1) == Some('_') {
|
||||
return if parent_acc.is_empty() {
|
||||
field_name.to_string()
|
||||
} else {
|
||||
format!("{}_{}", parent_acc, field_name)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if parent_acc.is_empty() {
|
||||
field_name.to_string()
|
||||
} else {
|
||||
format!("{}_{}", parent_acc, field_name)
|
||||
}
|
||||
}
|
||||
|
||||
fn analyze_field_positions_from_instances(
|
||||
instances: &[(String, String, String)],
|
||||
) -> HashMap<String, FieldNamePosition> {
|
||||
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
|
||||
}
|
||||
|
||||
fn detect_field_position(data: &[(String, String)]) -> Option<FieldNamePosition> {
|
||||
if data.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let (first_acc, first_leaf) = &data[0];
|
||||
|
||||
// Identity
|
||||
if first_acc == first_leaf {
|
||||
return Some(FieldNamePosition::Identity);
|
||||
}
|
||||
|
||||
// Append
|
||||
if let Some(suffix) = first_leaf.strip_prefix(first_acc.as_str()) {
|
||||
let suffix = suffix.to_string();
|
||||
if data.iter().all(|(acc, leaf)| {
|
||||
if acc.is_empty() {
|
||||
leaf == suffix.trim_start_matches('_')
|
||||
} else {
|
||||
leaf.strip_prefix(acc.as_str()) == Some(&suffix)
|
||||
}
|
||||
}) {
|
||||
return Some(FieldNamePosition::Append(suffix));
|
||||
}
|
||||
}
|
||||
|
||||
// Prepend
|
||||
if let Some(prefix) = first_leaf.strip_suffix(first_acc.as_str()) {
|
||||
let prefix = prefix.to_string();
|
||||
if data.iter().all(|(acc, leaf)| {
|
||||
if acc.is_empty() {
|
||||
leaf == prefix.trim_end_matches('_')
|
||||
} else {
|
||||
leaf.strip_suffix(acc.as_str()) == Some(&prefix)
|
||||
}
|
||||
}) {
|
||||
return Some(FieldNamePosition::Prepend(prefix));
|
||||
}
|
||||
}
|
||||
|
||||
// SetBase
|
||||
if first_acc.is_empty() {
|
||||
return Some(FieldNamePosition::SetBase(first_leaf.clone()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use brk_types::{Index, TreeNode};
|
||||
|
||||
use super::{PatternField, case::to_pascal_case, schema::schema_to_json_type};
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing the first leaf descendant.
|
||||
pub fn get_pattern_instance_base(node: &TreeNode, field_name: &str) -> String {
|
||||
if let Some(leaf_name) = get_first_leaf_name(node)
|
||||
&& leaf_name.contains(field_name)
|
||||
{
|
||||
return field_name.to_string();
|
||||
}
|
||||
field_name.to_string()
|
||||
}
|
||||
|
||||
/// Get the field signature for a branch node's children.
|
||||
pub fn get_node_fields(
|
||||
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(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,
|
||||
type_param: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
fields
|
||||
}
|
||||
|
||||
/// Get fields with child field information for generic pattern lookup.
|
||||
/// Returns (field, child_fields) pairs where child_fields is Some for branches.
|
||||
pub fn get_fields_with_child_info(
|
||||
children: &BTreeMap<String, TreeNode>,
|
||||
parent_name: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) -> Vec<(PatternField, Option<Vec<PatternField>>)> {
|
||||
children
|
||||
.iter()
|
||||
.map(|(name, node)| {
|
||||
let (rust_type, json_type, indexes, child_fields) = match node {
|
||||
TreeNode::Leaf(leaf) => (
|
||||
leaf.value_type().to_string(),
|
||||
schema_to_json_type(&leaf.schema),
|
||||
leaf.indexes().clone(),
|
||||
None,
|
||||
),
|
||||
TreeNode::Branch(grandchildren) => {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
let pattern_name = pattern_lookup
|
||||
.get(&child_fields)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| format!("{}_{}", parent_name, to_pascal_case(name)));
|
||||
(
|
||||
pattern_name.clone(),
|
||||
pattern_name,
|
||||
BTreeSet::new(),
|
||||
Some(child_fields),
|
||||
)
|
||||
}
|
||||
};
|
||||
(
|
||||
PatternField {
|
||||
name: name.clone(),
|
||||
rust_type,
|
||||
json_type,
|
||||
indexes,
|
||||
type_param: None,
|
||||
},
|
||||
child_fields,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Detect index patterns (sets of indexes that appear together on multiple metrics).
|
||||
pub fn detect_index_patterns(tree: &TreeNode) -> (BTreeSet<Index>, Vec<super::IndexSetPattern>) {
|
||||
let mut used_indexes: BTreeSet<Index> = BTreeSet::new();
|
||||
let mut index_sets: Vec<BTreeSet<Index>> = Vec::new();
|
||||
|
||||
collect_indexes_from_tree(tree, &mut used_indexes, &mut index_sets);
|
||||
|
||||
// Count occurrences of each unique index set
|
||||
let mut index_set_counts: Vec<(BTreeSet<Index>, usize)> = Vec::new();
|
||||
for index_set in index_sets {
|
||||
if let Some(entry) = index_set_counts.iter_mut().find(|(s, _)| s == &index_set) {
|
||||
entry.1 += 1;
|
||||
} else {
|
||||
index_set_counts.push((index_set, 1));
|
||||
}
|
||||
}
|
||||
|
||||
// Build patterns for index sets appearing 2+ times
|
||||
let mut patterns: Vec<super::IndexSetPattern> = index_set_counts
|
||||
.into_iter()
|
||||
.filter(|(indexes, count)| *count >= 2 && !indexes.is_empty())
|
||||
.enumerate()
|
||||
.map(|(i, (indexes, _))| super::IndexSetPattern {
|
||||
name: if i == 0 {
|
||||
"Indexes".to_string()
|
||||
} else {
|
||||
format!("Indexes{}", i + 1)
|
||||
},
|
||||
indexes,
|
||||
})
|
||||
.collect();
|
||||
|
||||
patterns.sort_by(|a, b| b.indexes.len().cmp(&a.indexes.len()));
|
||||
(used_indexes, patterns)
|
||||
}
|
||||
|
||||
fn collect_indexes_from_tree(
|
||||
node: &TreeNode,
|
||||
used_indexes: &mut BTreeSet<Index>,
|
||||
index_sets: &mut Vec<BTreeSet<Index>>,
|
||||
) {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => {
|
||||
used_indexes.extend(leaf.indexes().iter().cloned());
|
||||
index_sets.push(leaf.indexes().clone());
|
||||
}
|
||||
TreeNode::Branch(children) => {
|
||||
for child in children.values() {
|
||||
collect_indexes_from_tree(child, used_indexes, index_sets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "brk_binder"
|
||||
description = "A generator of binding files for other languages"
|
||||
name = "brk_bindgen"
|
||||
description = "A trait-based generator of client bindings for multiple languages"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
@@ -1,4 +1,4 @@
|
||||
# brk_binder Design Document
|
||||
# brk_bindgen Design Document
|
||||
|
||||
## Goal
|
||||
|
||||
@@ -16,14 +16,14 @@ Generate typed API clients for **Rust, JavaScript, and Python** with:
|
||||
4. **schemars integration**: JSON schemas embedded in `MetricLeafWithSchema` for type info
|
||||
5. **Tree navigation**: `client.tree.blocks.difficulty.fetch()` pattern
|
||||
6. **OpenAPI integration**: All GET endpoints generate typed methods
|
||||
7. **Server integration**: brk_server calls brk_binder on startup (when clients/ dir exists)
|
||||
7. **Server integration**: brk_server calls brk_bindgen on startup (when clients/ dir exists)
|
||||
|
||||
### Generated Output
|
||||
|
||||
When `crates/brk_binder/clients/` directory exists, running the server generates:
|
||||
When `crates/brk_bindgen/clients/` directory exists, running the server generates:
|
||||
|
||||
```
|
||||
crates/brk_binder/clients/
|
||||
crates/brk_bindgen/clients/
|
||||
├── javascript/
|
||||
│ └── client.js # JS + JSDoc with tree + API methods
|
||||
├── python/
|
||||
@@ -186,8 +186,8 @@ pub struct MetricLeafWithSchema {
|
||||
|
||||
1. brk_server creates OpenAPI spec via aide
|
||||
2. On startup, serializes spec to JSON string
|
||||
3. Passes JSON to `brk_binder::generate_clients()`
|
||||
4. brk_binder parses with `oas3` crate (supports OpenAPI 3.1)
|
||||
3. Passes JSON to `brk_bindgen::generate_clients()`
|
||||
4. brk_bindgen parses with `oas3` crate (supports OpenAPI 3.1)
|
||||
5. Generates typed methods for all GET endpoints
|
||||
|
||||
### Why oas3?
|
||||
@@ -203,7 +203,7 @@ The `oas3` crate supports OpenAPI 3.1.x parsing.
|
||||
- [x] vecdb: Add optional `schemars` feature with `AnySchemaVec` trait
|
||||
- [x] brk_types: Enhance `TreeNode::Leaf` to include `MetricLeafWithSchema`
|
||||
- [x] brk_traversable: Update all `to_tree_node()` with schemars integration
|
||||
- [x] brk_binder: Set up generator module structure
|
||||
- [x] brk_bindgen: Set up generator module structure
|
||||
|
||||
### Phase 1: JavaScript Client ✅ COMPLETE
|
||||
|
||||
@@ -216,7 +216,7 @@ The `oas3` crate supports OpenAPI 3.1.x parsing.
|
||||
### Phase 2: OpenAPI Integration ✅ COMPLETE
|
||||
|
||||
- [x] Add `oas3` crate dependency (OpenAPI 3.1 support)
|
||||
- [x] brk_server passes OpenAPI JSON to brk_binder on startup
|
||||
- [x] brk_server passes OpenAPI JSON to brk_bindgen on startup
|
||||
- [x] Parse OpenAPI spec and extract endpoint definitions
|
||||
- [x] Generate typed methods for each GET endpoint
|
||||
|
||||
@@ -246,7 +246,7 @@ The `oas3` crate supports OpenAPI 3.1.x parsing.
|
||||
## File Structure
|
||||
|
||||
```
|
||||
crates/brk_binder/
|
||||
crates/brk_bindgen/
|
||||
├── src/
|
||||
│ ├── lib.rs
|
||||
│ ├── js.rs # JS constants generation (existing)
|
||||
@@ -267,7 +267,7 @@ crates/brk_binder/
|
||||
|
||||
crates/brk_server/
|
||||
└── src/
|
||||
├── lib.rs # Calls brk_binder::generate_clients() on startup
|
||||
├── lib.rs # Calls brk_bindgen::generate_clients() on startup
|
||||
└── api/
|
||||
└── openapi.rs # create_openapi() for aide
|
||||
```
|
||||
@@ -289,7 +289,7 @@ To generate clients:
|
||||
|
||||
```bash
|
||||
# Create the output directory
|
||||
mkdir -p crates/brk_binder/clients
|
||||
mkdir -p crates/brk_bindgen/clients
|
||||
|
||||
# Run the server (generates clients on startup)
|
||||
cargo run -p brk_server
|
||||
@@ -1,4 +1,4 @@
|
||||
# brk_binder
|
||||
# brk_bindgen
|
||||
|
||||
Code generation for BRK client libraries.
|
||||
|
||||
@@ -17,7 +17,7 @@ Generate typed client libraries for Rust, JavaScript/TypeScript, and Python from
|
||||
## Core API
|
||||
|
||||
```rust,ignore
|
||||
use brk_binder::{generate_clients, ClientOutputPaths};
|
||||
use brk_bindgen::{generate_clients, ClientOutputPaths};
|
||||
|
||||
let paths = ClientOutputPaths::new()
|
||||
.rust("crates/brk_client/src/lib.rs")
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Analysis module for name deconstruction and pattern detection.
|
||||
//!
|
||||
//! This module implements bottom-up analysis of vec names to detect
|
||||
//! common denominators (prefixes/suffixes) and field positions.
|
||||
|
||||
mod names;
|
||||
mod patterns;
|
||||
mod positions;
|
||||
mod tree;
|
||||
|
||||
pub use names::*;
|
||||
pub use patterns::*;
|
||||
pub use positions::*;
|
||||
pub use tree::*;
|
||||
@@ -0,0 +1,451 @@
|
||||
//! Vec name deconstruction and reconstruction logic.
|
||||
//!
|
||||
//! This module analyzes vec names bottom-up to detect common denominators
|
||||
//! (prefixes or suffixes) and field positions for pattern instances.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use crate::FieldNamePosition;
|
||||
|
||||
/// Common denominator found across children's effective names.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub enum CommonDenominator {
|
||||
/// Children share this prefix. Fields append their unique suffix.
|
||||
/// Example: children are ["addrs_0sats", "addrs_1sats"], common = "addrs_"
|
||||
Prefix(String),
|
||||
/// Children share this suffix. Fields prepend their unique prefix.
|
||||
/// Example: children are ["cumulative_supply", "net_supply"], common = "_supply"
|
||||
Suffix(String),
|
||||
/// No common part found. Fields use Identity (field = base).
|
||||
None,
|
||||
}
|
||||
|
||||
/// Result of analyzing a pattern level.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PatternAnalysis {
|
||||
/// The common prefix/suffix found across all children.
|
||||
pub common: CommonDenominator,
|
||||
/// What's left after stripping the common part (passed to parent).
|
||||
pub base: String,
|
||||
/// How each field modifies the accumulated name.
|
||||
pub field_positions: HashMap<String, FieldNamePosition>,
|
||||
}
|
||||
|
||||
/// Analyze a pattern level using child effective names.
|
||||
///
|
||||
/// This is the core algorithm that detects common prefix/suffix and
|
||||
/// determines field positions for each child.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `child_names` - Vec of (field_name, effective_name) pairs
|
||||
/// where effective_name is either:
|
||||
/// - For leaves: the leaf's vec name
|
||||
/// - For branches: the base returned by analyzing that branch
|
||||
pub fn analyze_pattern_level(child_names: &[(String, String)]) -> PatternAnalysis {
|
||||
if child_names.is_empty() {
|
||||
return PatternAnalysis {
|
||||
common: CommonDenominator::None,
|
||||
base: String::new(),
|
||||
field_positions: HashMap::new(),
|
||||
};
|
||||
}
|
||||
|
||||
if child_names.len() == 1 {
|
||||
let (field_name, effective) = &child_names[0];
|
||||
let mut positions = HashMap::new();
|
||||
|
||||
// Try suffix match: effective ends with "_fieldname"
|
||||
let suffix_pattern = format!("_{}", field_name);
|
||||
if let Some(base) = effective.strip_suffix(&suffix_pattern) {
|
||||
positions.insert(
|
||||
field_name.clone(),
|
||||
FieldNamePosition::Append(suffix_pattern),
|
||||
);
|
||||
return PatternAnalysis {
|
||||
common: CommonDenominator::None,
|
||||
base: base.to_string(),
|
||||
field_positions: positions,
|
||||
};
|
||||
}
|
||||
|
||||
// Try prefix match: effective starts with "fieldname_"
|
||||
let prefix_pattern = format!("{}_", field_name);
|
||||
if let Some(base) = effective.strip_prefix(&prefix_pattern) {
|
||||
positions.insert(
|
||||
field_name.clone(),
|
||||
FieldNamePosition::Prepend(prefix_pattern),
|
||||
);
|
||||
return PatternAnalysis {
|
||||
common: CommonDenominator::None,
|
||||
base: base.to_string(),
|
||||
field_positions: positions,
|
||||
};
|
||||
}
|
||||
|
||||
// Field equals effective OR field doesn't appear → Identity
|
||||
// Root-level instances where field == effective are handled by
|
||||
// passing empty `acc` and conditional position expressions
|
||||
positions.insert(field_name.clone(), FieldNamePosition::Identity);
|
||||
return PatternAnalysis {
|
||||
common: CommonDenominator::None,
|
||||
base: effective.clone(),
|
||||
field_positions: positions,
|
||||
};
|
||||
}
|
||||
|
||||
let effective_names: Vec<&str> = child_names.iter().map(|(_, n)| n.as_str()).collect();
|
||||
|
||||
// Try to find common prefix first
|
||||
if let Some(prefix) = find_common_prefix(&effective_names)
|
||||
&& !prefix.is_empty()
|
||||
{
|
||||
let base = prefix.trim_end_matches('_').to_string();
|
||||
let mut positions = HashMap::new();
|
||||
for (field_name, effective) in child_names {
|
||||
// If effective equals the base (prefix without underscore), use Identity
|
||||
if effective == &base {
|
||||
positions.insert(field_name.clone(), FieldNamePosition::Identity);
|
||||
} else if let Some(suffix) = effective.strip_prefix(&prefix) {
|
||||
// Normal case: effective has the full prefix
|
||||
let suffix_with_underscore = if suffix.starts_with('_') {
|
||||
suffix.to_string()
|
||||
} else {
|
||||
format!("_{}", suffix)
|
||||
};
|
||||
positions.insert(
|
||||
field_name.clone(),
|
||||
FieldNamePosition::Append(suffix_with_underscore),
|
||||
);
|
||||
} else {
|
||||
// Fallback: use Identity if strip_prefix fails unexpectedly
|
||||
positions.insert(field_name.clone(), FieldNamePosition::Identity);
|
||||
}
|
||||
}
|
||||
return PatternAnalysis {
|
||||
common: CommonDenominator::Prefix(prefix),
|
||||
base,
|
||||
field_positions: positions,
|
||||
};
|
||||
}
|
||||
|
||||
// Try to find common suffix
|
||||
if let Some(suffix) = find_common_suffix(&effective_names)
|
||||
&& !suffix.is_empty()
|
||||
{
|
||||
let mut positions = HashMap::new();
|
||||
for (field_name, effective) in child_names {
|
||||
let prefix = effective
|
||||
.strip_suffix(&suffix)
|
||||
.unwrap_or(effective)
|
||||
.to_string();
|
||||
let prefix_with_underscore = if prefix.ends_with('_') {
|
||||
prefix
|
||||
} else {
|
||||
format!("{}_", prefix)
|
||||
};
|
||||
positions.insert(
|
||||
field_name.clone(),
|
||||
FieldNamePosition::Prepend(prefix_with_underscore),
|
||||
);
|
||||
}
|
||||
let base = suffix.trim_start_matches('_').to_string();
|
||||
return PatternAnalysis {
|
||||
common: CommonDenominator::Suffix(suffix),
|
||||
base,
|
||||
field_positions: positions,
|
||||
};
|
||||
}
|
||||
|
||||
// No common part - use Identity for all fields
|
||||
let mut positions = HashMap::new();
|
||||
for (field_name, _) in child_names {
|
||||
positions.insert(field_name.clone(), FieldNamePosition::Identity);
|
||||
}
|
||||
|
||||
// Use the first name as base (they're all independent)
|
||||
let base = child_names
|
||||
.first()
|
||||
.map(|(_, n)| n.clone())
|
||||
.unwrap_or_default();
|
||||
|
||||
PatternAnalysis {
|
||||
common: CommonDenominator::None,
|
||||
base,
|
||||
field_positions: positions,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find the longest common prefix among all strings.
|
||||
/// The prefix must end at an underscore boundary for semantic coherence.
|
||||
fn find_common_prefix(names: &[&str]) -> Option<String> {
|
||||
if names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first = names[0];
|
||||
if first.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// 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 None;
|
||||
}
|
||||
|
||||
let raw_prefix = &first[..prefix_len];
|
||||
|
||||
// If raw_prefix exactly matches one of the names, it's a complete metric name.
|
||||
// In this case, return it with trailing underscore to preserve the full name.
|
||||
if names.contains(&raw_prefix) {
|
||||
return Some(format!("{}_", raw_prefix));
|
||||
}
|
||||
|
||||
// Find the last underscore position to get a clean boundary
|
||||
// Prefer ending at an underscore for semantic coherence
|
||||
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 Some(clean_prefix.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// If no underscore boundary works, the full prefix must end at an underscore
|
||||
if raw_prefix.ends_with('_') {
|
||||
return Some(raw_prefix.to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
/// Find the longest common suffix among all strings.
|
||||
/// The suffix must start at an underscore boundary for semantic coherence.
|
||||
fn find_common_suffix(names: &[&str]) -> Option<String> {
|
||||
if names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first = names[0];
|
||||
if first.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Find character-by-character common suffix (from the end)
|
||||
let first_chars: Vec<char> = first.chars().collect();
|
||||
let mut suffix_len = 0;
|
||||
|
||||
for i in 0..first_chars.len() {
|
||||
let idx_from_end = first_chars.len() - 1 - i;
|
||||
let ch = first_chars[idx_from_end];
|
||||
|
||||
let all_match = names.iter().all(|n| {
|
||||
let n_chars: Vec<char> = n.chars().collect();
|
||||
if i >= n_chars.len() {
|
||||
return false;
|
||||
}
|
||||
n_chars[n_chars.len() - 1 - i] == ch
|
||||
});
|
||||
|
||||
if all_match {
|
||||
suffix_len = i + 1;
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if suffix_len == 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let raw_suffix = &first[first.len() - suffix_len..];
|
||||
|
||||
// Find the first underscore position to get a clean boundary
|
||||
if let Some(first_underscore) = raw_suffix.find('_')
|
||||
&& first_underscore < raw_suffix.len() - 1
|
||||
{
|
||||
let clean_suffix = &raw_suffix[first_underscore..];
|
||||
// Verify this still works for all names
|
||||
if names.iter().all(|n| n.ends_with(clean_suffix)) {
|
||||
return Some(clean_suffix.to_string());
|
||||
}
|
||||
}
|
||||
|
||||
// If no underscore boundary works, the full suffix must start with underscore
|
||||
if raw_suffix.starts_with('_') {
|
||||
return Some(raw_suffix.to_string());
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix() {
|
||||
let names = vec!["addrs_0sats", "addrs_1sats", "addrs_2sats"];
|
||||
assert_eq!(find_common_prefix(&names), Some("addrs_".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_suffix() {
|
||||
let names = vec!["cumulative_supply", "net_supply", "total_supply"];
|
||||
assert_eq!(find_common_suffix(&names), Some("_supply".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_common() {
|
||||
let names = vec!["foo", "bar", "baz"];
|
||||
assert_eq!(find_common_prefix(&names), None);
|
||||
assert_eq!(find_common_suffix(&names), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_pattern_level_prefix() {
|
||||
let children = vec![
|
||||
("_0sats".to_string(), "addrs_0sats".to_string()),
|
||||
("_1sats".to_string(), "addrs_1sats".to_string()),
|
||||
];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::Prefix(_)));
|
||||
assert_eq!(analysis.base, "addrs");
|
||||
assert!(matches!(
|
||||
analysis.field_positions.get("_0sats"),
|
||||
Some(FieldNamePosition::Append(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_pattern_level_suffix() {
|
||||
let children = vec![
|
||||
("cumulative".to_string(), "cumulative_supply".to_string()),
|
||||
("net".to_string(), "net_supply".to_string()),
|
||||
];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::Suffix(_)));
|
||||
assert_eq!(analysis.base, "supply");
|
||||
assert!(matches!(
|
||||
analysis.field_positions.get("cumulative"),
|
||||
Some(FieldNamePosition::Prepend(_))
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_child_suffix() {
|
||||
// Field "count" appears as suffix "_count" in "activity_count"
|
||||
let children = vec![("count".to_string(), "activity_count".to_string())];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::None));
|
||||
assert_eq!(analysis.base, "activity");
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("count"),
|
||||
Some(&FieldNamePosition::Append("_count".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_child_prefix() {
|
||||
// Field "cumulative" appears as prefix "cumulative_" in "cumulative_supply"
|
||||
let children = vec![("cumulative".to_string(), "cumulative_supply".to_string())];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::None));
|
||||
assert_eq!(analysis.base, "supply");
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("cumulative"),
|
||||
Some(&FieldNamePosition::Prepend("cumulative_".to_string()))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_child_identity_equal() {
|
||||
// Field "supply" equals effective "supply" → Identity
|
||||
// (root-level handling is done via empty acc and conditional expressions)
|
||||
let children = vec![("supply".to_string(), "supply".to_string())];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::None));
|
||||
assert_eq!(analysis.base, "supply");
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("supply"),
|
||||
Some(&FieldNamePosition::Identity)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_single_child_identity_structural() {
|
||||
// Field "x" doesn't appear in "a_b" - it's structural grouping
|
||||
let children = vec![("x".to_string(), "a_b".to_string())];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::None));
|
||||
assert_eq!(analysis.base, "a_b"); // passes through unchanged
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("x"),
|
||||
Some(&FieldNamePosition::Identity)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_exact_match() {
|
||||
// When one name exactly matches the common prefix, preserve the full name
|
||||
// This fixes the realized_loss vs realized_count bug
|
||||
let names = vec!["realized_loss", "realized_loss_cumulative"];
|
||||
assert_eq!(
|
||||
find_common_prefix(&names),
|
||||
Some("realized_loss_".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_common_prefix_exact_match_multiple() {
|
||||
// Multiple children with same base name
|
||||
let names = vec!["realized_loss", "realized_loss", "realized_loss_cumulative"];
|
||||
assert_eq!(
|
||||
find_common_prefix(&names),
|
||||
Some("realized_loss_".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_analyze_pattern_level_full_base() {
|
||||
// When names are like [realized_loss, realized_loss_cumulative],
|
||||
// base should be "realized_loss" not "realized"
|
||||
let children = vec![
|
||||
("sum".to_string(), "realized_loss".to_string()),
|
||||
(
|
||||
"cumulative".to_string(),
|
||||
"realized_loss_cumulative".to_string(),
|
||||
),
|
||||
];
|
||||
let analysis = analyze_pattern_level(&children);
|
||||
|
||||
assert!(matches!(analysis.common, CommonDenominator::Prefix(_)));
|
||||
assert_eq!(analysis.base, "realized_loss");
|
||||
// sum effective equals base, so position is Identity
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("sum"),
|
||||
Some(&FieldNamePosition::Identity)
|
||||
);
|
||||
// cumulative has suffix "_cumulative" after the base
|
||||
assert_eq!(
|
||||
analysis.field_positions.get("cumulative"),
|
||||
Some(&FieldNamePosition::Append("_cumulative".to_string()))
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
//! Structural pattern detection using bottom-up analysis.
|
||||
//!
|
||||
//! This module detects repeating tree structures and analyzes them
|
||||
//! using the bottom-up name deconstruction algorithm.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use super::analyze_all_field_positions;
|
||||
use crate::{PatternField, StructuralPattern, schema_to_json_type, to_pascal_case};
|
||||
|
||||
/// Context for pattern detection, holding all intermediate state.
|
||||
struct PatternContext {
|
||||
/// Maps field signatures to pattern names
|
||||
signature_to_pattern: HashMap<Vec<PatternField>, String>,
|
||||
/// Counts how many times each signature appears
|
||||
signature_counts: HashMap<Vec<PatternField>, usize>,
|
||||
/// Maps normalized signatures to pattern names (for naming consistency)
|
||||
normalized_to_name: HashMap<Vec<PatternField>, String>,
|
||||
/// Counts pattern name usage (for unique naming)
|
||||
name_counts: HashMap<String, usize>,
|
||||
/// Maps signatures to their child field lists
|
||||
signature_to_child_fields: HashMap<Vec<PatternField>, Vec<Vec<PatternField>>>,
|
||||
}
|
||||
|
||||
impl PatternContext {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
signature_to_pattern: HashMap::new(),
|
||||
signature_counts: HashMap::new(),
|
||||
normalized_to_name: HashMap::new(),
|
||||
name_counts: HashMap::new(),
|
||||
signature_to_child_fields: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Detect structural patterns in the tree using a bottom-up approach.
|
||||
///
|
||||
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
|
||||
pub fn detect_structural_patterns(
|
||||
tree: &TreeNode,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let mut ctx = PatternContext::new();
|
||||
resolve_branch_patterns(tree, "root", &mut ctx);
|
||||
|
||||
let (generic_patterns, generic_mappings, type_mappings) =
|
||||
detect_generic_patterns(&ctx.signature_to_pattern);
|
||||
|
||||
let mut patterns: Vec<StructuralPattern> = ctx.signature_to_pattern
|
||||
.iter()
|
||||
.filter(|(sig, _)| {
|
||||
ctx.signature_counts.get(*sig).copied().unwrap_or(0) >= 2
|
||||
&& !generic_mappings.contains_key(*sig)
|
||||
})
|
||||
.map(|(fields, name)| {
|
||||
let child_fields_list = ctx.signature_to_child_fields.get(fields);
|
||||
let fields_with_type_params = fields
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, f)| {
|
||||
let type_param = child_fields_list
|
||||
.and_then(|list| list.get(i))
|
||||
.and_then(|cf| type_mappings.get(cf).cloned());
|
||||
PatternField {
|
||||
type_param,
|
||||
..f.clone()
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
StructuralPattern {
|
||||
name: name.clone(),
|
||||
fields: fields_with_type_params,
|
||||
field_positions: HashMap::new(),
|
||||
is_generic: false,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
patterns.extend(generic_patterns);
|
||||
|
||||
let mut pattern_lookup: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
for (sig, name) in &ctx.signature_to_pattern {
|
||||
if ctx.signature_counts.get(sig).copied().unwrap_or(0) >= 2 {
|
||||
pattern_lookup.insert(sig.clone(), name.clone());
|
||||
}
|
||||
}
|
||||
pattern_lookup.extend(generic_mappings.clone());
|
||||
|
||||
let concrete_to_pattern = pattern_lookup.clone();
|
||||
|
||||
// Use the new bottom-up field position analysis
|
||||
analyze_all_field_positions(tree, &mut patterns, &pattern_lookup);
|
||||
|
||||
patterns.sort_by(|a, b| b.fields.len().cmp(&a.fields.len()));
|
||||
(patterns, concrete_to_pattern, type_mappings)
|
||||
}
|
||||
|
||||
/// Detect generic patterns by grouping signatures by their normalized form.
|
||||
fn detect_generic_patterns(
|
||||
signature_to_pattern: &HashMap<Vec<PatternField>, String>,
|
||||
) -> (
|
||||
Vec<StructuralPattern>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let mut normalized_groups: HashMap<
|
||||
Vec<PatternField>,
|
||||
Vec<(Vec<PatternField>, String, String)>,
|
||||
> = HashMap::new();
|
||||
|
||||
for (fields, name) in signature_to_pattern {
|
||||
if let Some((normalized, extracted_type)) = normalize_fields_for_generic(fields) {
|
||||
normalized_groups
|
||||
.entry(normalized)
|
||||
.or_default()
|
||||
.push((fields.clone(), name.clone(), extracted_type));
|
||||
}
|
||||
}
|
||||
|
||||
let mut patterns = Vec::new();
|
||||
let mut pattern_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
let mut type_mappings: HashMap<Vec<PatternField>, String> = HashMap::new();
|
||||
|
||||
for (normalized_fields, group) in normalized_groups {
|
||||
if group.len() >= 2 {
|
||||
let generic_name = group[0].1.clone();
|
||||
for (concrete_fields, _, extracted_type) in &group {
|
||||
pattern_mappings.insert(concrete_fields.clone(), generic_name.clone());
|
||||
type_mappings.insert(concrete_fields.clone(), extracted_type.clone());
|
||||
}
|
||||
patterns.push(StructuralPattern {
|
||||
name: generic_name,
|
||||
fields: normalized_fields,
|
||||
field_positions: HashMap::new(),
|
||||
is_generic: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
(patterns, pattern_mappings, type_mappings)
|
||||
}
|
||||
|
||||
/// Normalize fields by replacing concrete value types with "T".
|
||||
fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<(Vec<PatternField>, String)> {
|
||||
let leaf_types: Vec<&str> = fields
|
||||
.iter()
|
||||
.filter(|f| f.is_leaf())
|
||||
.map(|f| f.rust_type.as_str())
|
||||
.collect();
|
||||
|
||||
if leaf_types.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let first_type = leaf_types[0];
|
||||
if !leaf_types.iter().all(|t| *t == first_type) {
|
||||
return None;
|
||||
}
|
||||
|
||||
let normalized = fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if f.is_branch() {
|
||||
f.clone()
|
||||
} else {
|
||||
PatternField {
|
||||
name: f.name.clone(),
|
||||
rust_type: "T".to_string(),
|
||||
json_type: "T".to_string(),
|
||||
indexes: f.indexes.clone(),
|
||||
type_param: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
Some((normalized, crate::extract_inner_type(first_type)))
|
||||
}
|
||||
|
||||
/// Recursively resolve branch patterns bottom-up.
|
||||
fn resolve_branch_patterns(
|
||||
node: &TreeNode,
|
||||
field_name: &str,
|
||||
ctx: &mut PatternContext,
|
||||
) -> Option<(String, Vec<PatternField>)> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut fields: Vec<PatternField> = Vec::new();
|
||||
let mut child_fields_vec: Vec<Vec<PatternField>> = Vec::new();
|
||||
|
||||
for (child_name, child_node) in children {
|
||||
let (rust_type, json_type, indexes, child_fields) = match child_node {
|
||||
TreeNode::Leaf(leaf) => (
|
||||
leaf.value_type().to_string(),
|
||||
schema_to_json_type(&leaf.schema),
|
||||
leaf.indexes().clone(),
|
||||
Vec::new(),
|
||||
),
|
||||
TreeNode::Branch(_) => {
|
||||
let (pattern_name, child_pattern_fields) =
|
||||
resolve_branch_patterns(child_node, child_name, ctx)
|
||||
.unwrap_or_else(|| ("Unknown".to_string(), Vec::new()));
|
||||
(
|
||||
pattern_name.clone(),
|
||||
pattern_name,
|
||||
BTreeSet::new(),
|
||||
child_pattern_fields,
|
||||
)
|
||||
}
|
||||
};
|
||||
fields.push(PatternField {
|
||||
name: child_name.clone(),
|
||||
rust_type,
|
||||
json_type,
|
||||
indexes,
|
||||
type_param: None,
|
||||
});
|
||||
child_fields_vec.push(child_fields);
|
||||
}
|
||||
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
*ctx.signature_counts.entry(fields.clone()).or_insert(0) += 1;
|
||||
|
||||
ctx.signature_to_child_fields
|
||||
.entry(fields.clone())
|
||||
.or_insert(child_fields_vec);
|
||||
|
||||
let pattern_name = if let Some(existing) = ctx.signature_to_pattern.get(&fields) {
|
||||
existing.clone()
|
||||
} else {
|
||||
let normalized = normalize_fields_for_naming(&fields);
|
||||
let name = ctx
|
||||
.normalized_to_name
|
||||
.entry(normalized)
|
||||
.or_insert_with(|| generate_pattern_name(field_name, &mut ctx.name_counts))
|
||||
.clone();
|
||||
ctx.signature_to_pattern.insert(fields.clone(), name.clone());
|
||||
name
|
||||
};
|
||||
|
||||
Some((pattern_name, fields))
|
||||
}
|
||||
|
||||
/// Normalize fields for naming (same structure = same name).
|
||||
fn normalize_fields_for_naming(fields: &[PatternField]) -> Vec<PatternField> {
|
||||
fields
|
||||
.iter()
|
||||
.map(|f| {
|
||||
if f.is_branch() {
|
||||
f.clone()
|
||||
} else {
|
||||
PatternField {
|
||||
name: f.name.clone(),
|
||||
rust_type: "_".to_string(),
|
||||
json_type: "_".to_string(),
|
||||
indexes: f.indexes.clone(),
|
||||
type_param: None,
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Generate a unique pattern name.
|
||||
fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<String, usize>) -> String {
|
||||
let pascal = to_pascal_case(field_name);
|
||||
let sanitized = if pascal.chars().next().is_some_and(|c| c.is_ascii_digit()) {
|
||||
format!("_{}", pascal)
|
||||
} else {
|
||||
pascal
|
||||
};
|
||||
|
||||
let base_name = format!("{}Pattern", sanitized);
|
||||
let count = name_counts.entry(base_name.clone()).or_insert(0);
|
||||
*count += 1;
|
||||
|
||||
if *count == 1 {
|
||||
base_name
|
||||
} else {
|
||||
format!("{}{}", base_name, count)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Field position detection for pattern instances.
|
||||
//!
|
||||
//! This module bridges the name analysis with pattern field positions,
|
||||
//! processing patterns bottom-up to determine how each field modifies
|
||||
//! the accumulated metric name.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use super::{analyze_pattern_level, get_node_fields};
|
||||
use crate::{FieldNamePosition, PatternField, StructuralPattern};
|
||||
|
||||
/// Analyze field positions for all patterns using bottom-up tree traversal.
|
||||
///
|
||||
/// This is the main entry point for field position detection. It processes
|
||||
/// the tree bottom-up, analyzing each pattern instance and aggregating
|
||||
/// the positions across all instances.
|
||||
pub fn analyze_all_field_positions(
|
||||
tree: &TreeNode,
|
||||
patterns: &mut [StructuralPattern],
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) {
|
||||
let mut all_positions: HashMap<String, HashMap<String, Vec<FieldNamePosition>>> =
|
||||
HashMap::new();
|
||||
|
||||
// Collect positions from all instances bottom-up
|
||||
collect_positions_bottom_up(tree, pattern_lookup, &mut all_positions);
|
||||
|
||||
// Merge positions into patterns
|
||||
for pattern in patterns.iter_mut() {
|
||||
if let Some(field_positions) = all_positions.get(&pattern.name) {
|
||||
pattern.field_positions = merge_field_positions(field_positions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Recursively collect field positions bottom-up.
|
||||
/// Returns the effective base for this node (used by parent level).
|
||||
fn collect_positions_bottom_up(
|
||||
node: &TreeNode,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
all_positions: &mut HashMap<String, HashMap<String, Vec<FieldNamePosition>>>,
|
||||
) -> Option<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => {
|
||||
// Leaves return their vec name as the effective base
|
||||
Some(leaf.name().to_string())
|
||||
}
|
||||
TreeNode::Branch(children) => {
|
||||
// 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_positions_bottom_up(child_node, pattern_lookup, all_positions) {
|
||||
child_bases.insert(field_name.clone(), base);
|
||||
}
|
||||
}
|
||||
|
||||
// Build child names for this level's analysis
|
||||
let child_names: Vec<(String, String)> = children
|
||||
.keys()
|
||||
.filter_map(|field_name| {
|
||||
child_bases
|
||||
.get(field_name)
|
||||
.map(|base| (field_name.clone(), base.clone()))
|
||||
})
|
||||
.collect();
|
||||
|
||||
if child_names.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Analyze this level
|
||||
let analysis = analyze_pattern_level(&child_names);
|
||||
|
||||
// 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) {
|
||||
// Record field positions for this pattern instance
|
||||
for (field_name, position) in &analysis.field_positions {
|
||||
all_positions
|
||||
.entry(pattern_name.clone())
|
||||
.or_default()
|
||||
.entry(field_name.clone())
|
||||
.or_default()
|
||||
.push(position.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Return our base for the parent level
|
||||
Some(analysis.base)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Merge multiple observed positions for each field into a single position.
|
||||
/// Uses the first non-Identity position found, as Identity from root-level
|
||||
/// instances is now handled by passing empty `acc`.
|
||||
fn merge_field_positions(
|
||||
field_positions: &HashMap<String, Vec<FieldNamePosition>>,
|
||||
) -> HashMap<String, FieldNamePosition> {
|
||||
field_positions
|
||||
.iter()
|
||||
.filter_map(|(field_name, positions)| {
|
||||
if positions.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Prefer Append/Prepend over Identity, as Identity at root-level
|
||||
// is handled by empty acc and conditional position expressions
|
||||
let preferred = positions
|
||||
.iter()
|
||||
.find(|p| !matches!(p, FieldNamePosition::Identity))
|
||||
.cloned()
|
||||
.unwrap_or_else(|| positions[0].clone());
|
||||
|
||||
Some((field_name.clone(), preferred))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,251 @@
|
||||
//! Tree traversal helpers for pattern analysis.
|
||||
//!
|
||||
//! This module provides utilities for working with the TreeNode structure,
|
||||
//! including leaf name extraction and index pattern detection.
|
||||
|
||||
use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use brk_types::{Index, TreeNode};
|
||||
|
||||
use crate::{IndexSetPattern, PatternField, child_type_name, schema_to_json_type};
|
||||
|
||||
/// 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),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all leaf names from a tree node.
|
||||
pub fn get_all_leaf_names(node: &TreeNode) -> Vec<String> {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => vec![leaf.name().to_string()],
|
||||
TreeNode::Branch(children) => children.values().flat_map(get_all_leaf_names).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the field signature for a branch node's children.
|
||||
pub fn get_node_fields(
|
||||
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(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,
|
||||
type_param: None,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
fields.sort_by(|a, b| a.name.cmp(&b.name));
|
||||
fields
|
||||
}
|
||||
|
||||
/// Detect index patterns (sets of indexes that appear together on metrics).
|
||||
pub fn detect_index_patterns(tree: &TreeNode) -> (BTreeSet<Index>, Vec<IndexSetPattern>) {
|
||||
let mut used_indexes: BTreeSet<Index> = BTreeSet::new();
|
||||
let mut unique_index_sets: BTreeSet<BTreeSet<Index>> = BTreeSet::new();
|
||||
|
||||
collect_indexes_from_tree(tree, &mut used_indexes, &mut unique_index_sets);
|
||||
|
||||
// Sort by count (descending) then by first index name for deterministic ordering
|
||||
let mut sorted_sets: Vec<_> = unique_index_sets
|
||||
.into_iter()
|
||||
.filter(|indexes| !indexes.is_empty())
|
||||
.collect();
|
||||
sorted_sets.sort_by(|a, b| {
|
||||
b.len()
|
||||
.cmp(&a.len())
|
||||
.then_with(|| a.iter().next().cmp(&b.iter().next()))
|
||||
});
|
||||
|
||||
// Assign unique sequential names
|
||||
let patterns: Vec<IndexSetPattern> = sorted_sets
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, indexes)| IndexSetPattern {
|
||||
name: format!("MetricPattern{}", i + 1),
|
||||
indexes,
|
||||
})
|
||||
.collect();
|
||||
|
||||
(used_indexes, patterns)
|
||||
}
|
||||
|
||||
fn collect_indexes_from_tree(
|
||||
node: &TreeNode,
|
||||
used_indexes: &mut BTreeSet<Index>,
|
||||
unique_index_sets: &mut BTreeSet<BTreeSet<Index>>,
|
||||
) {
|
||||
match node {
|
||||
TreeNode::Leaf(leaf) => {
|
||||
used_indexes.extend(leaf.indexes().iter().cloned());
|
||||
unique_index_sets.insert(leaf.indexes().clone());
|
||||
}
|
||||
TreeNode::Branch(children) => {
|
||||
for child in children.values() {
|
||||
collect_indexes_from_tree(child, used_indexes, unique_index_sets);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the metric base for a pattern instance by analyzing all leaf descendants.
|
||||
///
|
||||
/// For root-level instances (no common prefix among leaves), returns empty string.
|
||||
/// For cohort-level instances, returns the common prefix among all leaves.
|
||||
pub fn get_pattern_instance_base(node: &TreeNode) -> String {
|
||||
let leaf_names = get_all_leaf_names(node);
|
||||
if leaf_names.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Find the longest common prefix among all leaf names
|
||||
let common_prefix = find_common_prefix_at_underscore(&leaf_names);
|
||||
|
||||
// If no common prefix, we're at root level
|
||||
if common_prefix.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// Return the common prefix (without trailing underscore)
|
||||
common_prefix.trim_end_matches('_').to_string()
|
||||
}
|
||||
|
||||
/// 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()
|
||||
}
|
||||
|
||||
/// Infer the accumulated name for a child node based on a descendant leaf name.
|
||||
pub fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_leaf: &str) -> String {
|
||||
if let Some(pos) = descendant_leaf.find(field_name) {
|
||||
if pos == 0 {
|
||||
return field_name.to_string();
|
||||
}
|
||||
if pos > 0 && descendant_leaf.chars().nth(pos - 1) == Some('_') {
|
||||
return if parent_acc.is_empty() {
|
||||
field_name.to_string()
|
||||
} else {
|
||||
format!("{}_{}", parent_acc, field_name)
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if parent_acc.is_empty() {
|
||||
field_name.to_string()
|
||||
} else {
|
||||
format!("{}_{}", parent_acc, field_name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Get fields with child field information for generic pattern lookup.
|
||||
pub fn get_fields_with_child_info(
|
||||
children: &BTreeMap<String, TreeNode>,
|
||||
parent_name: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
) -> Vec<(PatternField, Option<Vec<PatternField>>)> {
|
||||
children
|
||||
.iter()
|
||||
.map(|(name, node)| {
|
||||
let (rust_type, json_type, indexes, child_fields) = match node {
|
||||
TreeNode::Leaf(leaf) => (
|
||||
leaf.value_type().to_string(),
|
||||
schema_to_json_type(&leaf.schema),
|
||||
leaf.indexes().clone(),
|
||||
None,
|
||||
),
|
||||
TreeNode::Branch(grandchildren) => {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
let pattern_name = pattern_lookup
|
||||
.get(&child_fields)
|
||||
.cloned()
|
||||
.unwrap_or_else(|| child_type_name(parent_name, name));
|
||||
(
|
||||
pattern_name.clone(),
|
||||
pattern_name,
|
||||
BTreeSet::new(),
|
||||
Some(child_fields),
|
||||
)
|
||||
}
|
||||
};
|
||||
(
|
||||
PatternField {
|
||||
name: name.clone(),
|
||||
rust_type,
|
||||
json_type,
|
||||
indexes,
|
||||
type_param: None,
|
||||
},
|
||||
child_fields,
|
||||
)
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
//! JavaScript language syntax implementation.
|
||||
|
||||
use crate::{FieldNamePosition, GenericSyntax, LanguageSyntax, to_camel_case, to_pascal_case};
|
||||
|
||||
/// JavaScript-specific code generation syntax.
|
||||
pub struct JavaScriptSyntax;
|
||||
|
||||
impl LanguageSyntax for JavaScriptSyntax {
|
||||
fn field_name(&self, name: &str) -> String {
|
||||
to_camel_case(name)
|
||||
}
|
||||
|
||||
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
|
||||
// Convert base_var to camelCase for JavaScript
|
||||
let var_name = to_camel_case(base_var);
|
||||
format!("`${{{}}}{}`", var_name, suffix)
|
||||
}
|
||||
|
||||
fn position_expr(&self, pos: &FieldNamePosition, base_var: &str) -> String {
|
||||
// Convert base_var to camelCase for JavaScript
|
||||
let var_name = to_camel_case(base_var);
|
||||
match pos {
|
||||
FieldNamePosition::Append(s) => {
|
||||
// Use helper _m(acc, suffix) to build metric name
|
||||
// e.g., _m(acc, "cap") produces: acc ? `${acc}_cap` : 'cap'
|
||||
if let Some(suffix) = s.strip_prefix('_') {
|
||||
format!("_m({}, '{}')", var_name, suffix)
|
||||
} else {
|
||||
format!("`${{{}}}{}`", var_name, s)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Prepend(s) => {
|
||||
// Handle empty acc case for prepend
|
||||
if let Some(prefix) = s.strip_suffix('_') {
|
||||
format!(
|
||||
"({} ? `{}${{{}}}` : '{}')",
|
||||
var_name, s, var_name, prefix
|
||||
)
|
||||
} else {
|
||||
format!("`{}${{{}}}`", s, var_name)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Identity => var_name,
|
||||
FieldNamePosition::SetBase(s) => format!("'{}'", s),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructor(&self, type_name: &str, path_expr: &str) -> String {
|
||||
format!("create{}(client, {})", type_name, path_expr)
|
||||
}
|
||||
|
||||
fn field_init(&self, indent: &str, name: &str, _type_ann: &str, value: &str) -> String {
|
||||
// JavaScript uses object literal syntax; type is in JSDoc, not in assignment
|
||||
format!("{}{}: {},", indent, name, value)
|
||||
}
|
||||
|
||||
fn generic_syntax(&self) -> GenericSyntax {
|
||||
GenericSyntax::JAVASCRIPT
|
||||
}
|
||||
|
||||
fn struct_header(&self, name: &str, generic_params: &str, doc: Option<&str>) -> String {
|
||||
let mut result = String::new();
|
||||
if let Some(doc) = doc {
|
||||
result.push_str(&format!("/** {} */\n", doc));
|
||||
}
|
||||
// JavaScript uses factory functions that return object literals
|
||||
result.push_str(&format!(
|
||||
"function create{}{}(client, basePath) {{\n return {{\n",
|
||||
name, generic_params
|
||||
));
|
||||
result
|
||||
}
|
||||
|
||||
fn struct_footer(&self) -> String {
|
||||
" };\n}\n".to_string()
|
||||
}
|
||||
|
||||
fn constructor_header(&self, _params: &str) -> String {
|
||||
// JavaScript factory functions don't have a separate constructor
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn constructor_footer(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn field_declaration(&self, indent: &str, _name: &str, type_ann: &str) -> String {
|
||||
// JSDoc property declaration
|
||||
format!("{}/** @type {{{}}} */\n", indent, type_ann)
|
||||
}
|
||||
|
||||
fn index_field_name(&self, index_name: &str) -> String {
|
||||
format!("by{}", to_pascal_case(index_name))
|
||||
}
|
||||
|
||||
fn string_literal(&self, value: &str) -> String {
|
||||
format!("'{}'", value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
//! Language-specific syntax backends.
|
||||
//!
|
||||
//! This module contains implementations of the `LanguageSyntax` trait
|
||||
//! for each supported target language.
|
||||
|
||||
mod javascript;
|
||||
mod python;
|
||||
mod rust;
|
||||
|
||||
pub use javascript::JavaScriptSyntax;
|
||||
pub use python::PythonSyntax;
|
||||
pub use rust::RustSyntax;
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Python language syntax implementation.
|
||||
|
||||
use crate::{FieldNamePosition, GenericSyntax, LanguageSyntax, escape_python_keyword, to_snake_case};
|
||||
|
||||
/// Python-specific code generation syntax.
|
||||
pub struct PythonSyntax;
|
||||
|
||||
impl LanguageSyntax for PythonSyntax {
|
||||
fn field_name(&self, name: &str) -> String {
|
||||
escape_python_keyword(&to_snake_case(name))
|
||||
}
|
||||
|
||||
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
|
||||
format!("f'{{{{{}}}}}{}'", base_var, suffix)
|
||||
}
|
||||
|
||||
fn position_expr(&self, pos: &FieldNamePosition, base_var: &str) -> String {
|
||||
match pos {
|
||||
FieldNamePosition::Append(s) => {
|
||||
// Use helper _m(acc, suffix) to build metric name
|
||||
if let Some(suffix) = s.strip_prefix('_') {
|
||||
format!("_m({}, '{}')", base_var, suffix)
|
||||
} else {
|
||||
format!("f'{{{{{}}}}}{}'", base_var, s)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Prepend(s) => {
|
||||
// Handle empty acc case for prepend
|
||||
if let Some(prefix) = s.strip_suffix('_') {
|
||||
format!(
|
||||
"(f'{s}{{{{{base_var}}}}}' if {base_var} else '{prefix}')",
|
||||
s = s,
|
||||
base_var = base_var,
|
||||
prefix = prefix
|
||||
)
|
||||
} else {
|
||||
format!("f'{}{{{{{}}}}}'", s, base_var)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Identity => base_var.to_string(),
|
||||
FieldNamePosition::SetBase(s) => format!("'{}'", s),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructor(&self, type_name: &str, path_expr: &str) -> String {
|
||||
format!("{}(client, {})", type_name, path_expr)
|
||||
}
|
||||
|
||||
fn field_init(&self, indent: &str, name: &str, type_ann: &str, value: &str) -> String {
|
||||
format!("{}self.{}: {} = {}", indent, name, type_ann, value)
|
||||
}
|
||||
|
||||
fn generic_syntax(&self) -> GenericSyntax {
|
||||
GenericSyntax::PYTHON
|
||||
}
|
||||
|
||||
fn struct_header(&self, name: &str, generic_params: &str, doc: Option<&str>) -> String {
|
||||
let mut result = format!("class {}{}:\n", name, generic_params);
|
||||
if let Some(doc) = doc {
|
||||
result.push_str(&format!(" \"\"\"{}\"\"\"\n", doc));
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn struct_footer(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn constructor_header(&self, params: &str) -> String {
|
||||
format!(" def __init__(self{}) -> None:\n", params)
|
||||
}
|
||||
|
||||
fn constructor_footer(&self) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn field_declaration(&self, _indent: &str, _name: &str, _type_ann: &str) -> String {
|
||||
// Python uses __init__ for field declarations, so this is a no-op
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn index_field_name(&self, index_name: &str) -> String {
|
||||
format!("by_{}", to_snake_case(index_name))
|
||||
}
|
||||
|
||||
fn string_literal(&self, value: &str) -> String {
|
||||
format!("'{}'", value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,89 @@
|
||||
//! Rust language syntax implementation.
|
||||
|
||||
use crate::{FieldNamePosition, GenericSyntax, LanguageSyntax, to_snake_case};
|
||||
|
||||
/// Rust-specific code generation syntax.
|
||||
pub struct RustSyntax;
|
||||
|
||||
impl LanguageSyntax for RustSyntax {
|
||||
fn field_name(&self, name: &str) -> String {
|
||||
to_snake_case(name)
|
||||
}
|
||||
|
||||
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
|
||||
format!("format!(\"{{{}}}{}\")", base_var, suffix)
|
||||
}
|
||||
|
||||
fn position_expr(&self, pos: &FieldNamePosition, _base_var: &str) -> String {
|
||||
match pos {
|
||||
FieldNamePosition::Append(s) => {
|
||||
// Use helper _m(&acc, suffix) to build metric name
|
||||
if let Some(suffix) = s.strip_prefix('_') {
|
||||
format!("_m(&acc, \"{}\")", suffix)
|
||||
} else {
|
||||
format!("format!(\"{{acc}}{}\")", s)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Prepend(s) => {
|
||||
// Handle empty acc case for prepend
|
||||
if let Some(prefix) = s.strip_suffix('_') {
|
||||
format!(
|
||||
"if acc.is_empty() {{ \"{prefix}\".to_string() }} else {{ format!(\"{s}{{acc}}\") }}",
|
||||
prefix = prefix,
|
||||
s = s
|
||||
)
|
||||
} else {
|
||||
format!("format!(\"{}{{acc}}\")", s)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Identity => "acc.clone()".to_string(),
|
||||
FieldNamePosition::SetBase(base) => format!("\"{}\".to_string()", base),
|
||||
}
|
||||
}
|
||||
|
||||
fn constructor(&self, type_name: &str, path_expr: &str) -> String {
|
||||
format!("{}::new(client.clone(), {})", type_name, path_expr)
|
||||
}
|
||||
|
||||
fn field_init(&self, indent: &str, name: &str, _type_ann: &str, value: &str) -> String {
|
||||
// Rust struct initialization; type is in struct definition, not in init
|
||||
format!("{}{}: {},", indent, name, value)
|
||||
}
|
||||
|
||||
fn generic_syntax(&self) -> GenericSyntax {
|
||||
GenericSyntax::RUST
|
||||
}
|
||||
|
||||
fn struct_header(&self, name: &str, generic_params: &str, doc: Option<&str>) -> String {
|
||||
let mut result = String::new();
|
||||
if let Some(doc) = doc {
|
||||
result.push_str(&format!("/// {}\n", doc));
|
||||
}
|
||||
result.push_str(&format!("pub struct {}{} {{\n", name, generic_params));
|
||||
result
|
||||
}
|
||||
|
||||
fn struct_footer(&self) -> String {
|
||||
"}\n".to_string()
|
||||
}
|
||||
|
||||
fn constructor_header(&self, params: &str) -> String {
|
||||
format!(" pub fn new({}) -> Self {{\n Self {{\n", params)
|
||||
}
|
||||
|
||||
fn constructor_footer(&self) -> String {
|
||||
" }\n }\n".to_string()
|
||||
}
|
||||
|
||||
fn field_declaration(&self, indent: &str, name: &str, type_ann: &str) -> String {
|
||||
format!("{}pub {}: {},\n", indent, name, type_ann)
|
||||
}
|
||||
|
||||
fn index_field_name(&self, index_name: &str) -> String {
|
||||
format!("by_{}", to_snake_case(index_name))
|
||||
}
|
||||
|
||||
fn string_literal(&self, value: &str) -> String {
|
||||
format!("\"{}\".to_string()", value)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Shared field generation logic.
|
||||
//!
|
||||
//! This module contains the core field generation logic that is shared
|
||||
//! across all language backends. The `LanguageSyntax` trait is used to
|
||||
//! abstract over language-specific formatting.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{ClientMetadata, LanguageSyntax, PatternField, StructuralPattern};
|
||||
|
||||
/// Create a path suffix from a name.
|
||||
/// Adds `_` prefix only if the name doesn't already start with `_`.
|
||||
fn path_suffix(name: &str) -> String {
|
||||
if name.starts_with('_') {
|
||||
name.to_string()
|
||||
} else {
|
||||
format!("_{}", name)
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate a parameterized field using the language syntax.
|
||||
///
|
||||
/// This is used for pattern instances where fields use an accumulated
|
||||
/// metric name that's built up through the tree traversal.
|
||||
pub fn generate_parameterized_field<S: LanguageSyntax>(
|
||||
output: &mut String,
|
||||
syntax: &S,
|
||||
field: &PatternField,
|
||||
pattern: &StructuralPattern,
|
||||
metadata: &ClientMetadata,
|
||||
indent: &str,
|
||||
) {
|
||||
let field_name = syntax.field_name(&field.name);
|
||||
let type_ann = metadata.field_type_annotation(field, pattern.is_generic, None, syntax.generic_syntax());
|
||||
|
||||
// Compute path expression from field position
|
||||
let path_expr = pattern
|
||||
.get_field_position(&field.name)
|
||||
.map(|pos| syntax.position_expr(pos, "acc"))
|
||||
.unwrap_or_else(|| syntax.path_expr("acc", &path_suffix(&field.name)));
|
||||
|
||||
let value = if metadata.is_pattern_type(&field.rust_type) {
|
||||
syntax.constructor(&field.rust_type, &path_expr)
|
||||
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
syntax.constructor(&accessor.name, &path_expr)
|
||||
} else {
|
||||
panic!(
|
||||
"Field '{}' has no matching pattern or index accessor. All metrics must be indexed.",
|
||||
field.name
|
||||
)
|
||||
};
|
||||
|
||||
writeln!(output, "{}", syntax.field_init(indent, &field_name, &type_ann, &value)).unwrap();
|
||||
}
|
||||
|
||||
/// Generate a tree-path field using the language syntax.
|
||||
///
|
||||
/// This is the fallback for non-parameterizable patterns where fields
|
||||
/// use a base path that's extended with the field name.
|
||||
pub fn generate_tree_path_field<S: LanguageSyntax>(
|
||||
output: &mut String,
|
||||
syntax: &S,
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
indent: &str,
|
||||
) {
|
||||
let field_name = syntax.field_name(&field.name);
|
||||
let type_ann = metadata.field_type_annotation(field, false, None, syntax.generic_syntax());
|
||||
let path_expr = syntax.path_expr("base_path", &path_suffix(&field.name));
|
||||
|
||||
let value = if metadata.is_pattern_type(&field.rust_type) {
|
||||
syntax.constructor(&field.rust_type, &path_expr)
|
||||
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
|
||||
syntax.constructor(&accessor.name, &path_expr)
|
||||
} else {
|
||||
panic!(
|
||||
"Field '{}' has no matching pattern or index accessor. All metrics must be indexed.",
|
||||
field.name
|
||||
)
|
||||
};
|
||||
|
||||
writeln!(output, "{}", syntax.field_init(indent, &field_name, &type_ann, &value)).unwrap();
|
||||
}
|
||||
|
||||
/// Generate a tree node field with a specific child node for pattern instance base detection.
|
||||
///
|
||||
/// This is used when generating tree nodes where we need to detect the pattern instance
|
||||
/// base from descendant leaf names.
|
||||
pub fn generate_tree_node_field<S: LanguageSyntax>(
|
||||
output: &mut String,
|
||||
syntax: &S,
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
indent: &str,
|
||||
child_name: &str,
|
||||
pattern_base: Option<&str>,
|
||||
) {
|
||||
let field_name = syntax.field_name(&field.name);
|
||||
let type_ann = metadata.field_type_annotation(field, false, None, syntax.generic_syntax());
|
||||
|
||||
let value = if metadata.is_pattern_type(&field.rust_type) {
|
||||
// Check if this pattern is parameterizable
|
||||
let pattern = metadata.find_pattern(&field.rust_type);
|
||||
let is_parameterizable = pattern.is_some_and(|p| p.is_parameterizable());
|
||||
|
||||
if is_parameterizable {
|
||||
if let Some(base) = pattern_base {
|
||||
// Use the detected metric base
|
||||
let path = syntax.string_literal(base);
|
||||
syntax.constructor(&field.rust_type, &path)
|
||||
} else {
|
||||
// Fallback to tree path
|
||||
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
|
||||
syntax.constructor(&field.rust_type, &path_expr)
|
||||
}
|
||||
} else {
|
||||
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
|
||||
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)
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch - instantiate the nested struct
|
||||
let path_expr = syntax.path_expr("base_path", &path_suffix(child_name));
|
||||
syntax.constructor(&field.rust_type, &path_expr)
|
||||
} else {
|
||||
// All metrics must be indexed
|
||||
panic!(
|
||||
"Field '{}' is a leaf with no index accessor. All metrics must be indexed.",
|
||||
field.name
|
||||
)
|
||||
};
|
||||
|
||||
writeln!(output, "{}", syntax.field_init(indent, &field_name, &type_ann, &value)).unwrap();
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
//! Shared code generation logic.
|
||||
//!
|
||||
//! This module contains generation functions that are parameterized by
|
||||
//! the `LanguageSyntax` trait, allowing them to work across all supported
|
||||
//! language backends.
|
||||
|
||||
mod fields;
|
||||
|
||||
pub use fields::*;
|
||||
@@ -0,0 +1,112 @@
|
||||
//! JavaScript API method generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Endpoint, Parameter, to_camel_case};
|
||||
|
||||
/// Generate API methods for the BrkClient class.
|
||||
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
for endpoint in endpoints {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = endpoint.response_type.as_deref().unwrap_or("*");
|
||||
|
||||
writeln!(output, " /**").unwrap();
|
||||
if let Some(summary) = &endpoint.summary {
|
||||
writeln!(output, " * {}", summary).unwrap();
|
||||
}
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " * @description {}", desc).unwrap();
|
||||
}
|
||||
|
||||
for param in &endpoint.path_params {
|
||||
let desc = param.description.as_deref().unwrap_or("");
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}}} {} {}",
|
||||
param.param_type, param.name, desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let optional = if param.required { "" } else { "=" };
|
||||
let desc = param.description.as_deref().unwrap_or("");
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}{}}} [{}] {}",
|
||||
param.param_type, optional, param.name, desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " * @returns {{Promise<{}>}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(output, " async {}({}) {{", method_name, params).unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " return this.get(`{}`);", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " const params = new URLSearchParams();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.set('{}', String({}));",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if ({} !== undefined) params.set('{}', String({}));",
|
||||
param.name, param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " const query = params.toString();").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return this.get(`{}${{query ? '?' + query : ''}}`);",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
to_camel_case(&endpoint.operation_name())
|
||||
}
|
||||
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
params.push(param.name.clone());
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
params.push(param.name.clone());
|
||||
}
|
||||
params.join(", ")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
let placeholder = format!("{{{}}}", param.name);
|
||||
let interpolation = format!("${{{}}}", param.name);
|
||||
result = result.replace(&placeholder, &interpolation);
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,374 @@
|
||||
//! JavaScript base client and pattern factory generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, EPOCH_NAMES, GE_AMOUNT_NAMES, LT_AMOUNT_NAMES,
|
||||
MAX_AGE_NAMES, MIN_AGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, YEAR_NAMES,
|
||||
};
|
||||
use brk_types::{Index, PoolSlug, pools};
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, IndexSetPattern, JavaScriptSyntax, PatternField,
|
||||
StructuralPattern, VERSION, generate_parameterized_field, generate_tree_path_field,
|
||||
to_camel_case,
|
||||
};
|
||||
|
||||
/// Generate the base BrkClient class with HTTP functionality.
|
||||
pub fn generate_base_client(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/**
|
||||
* @typedef {{Object}} BrkClientOptions
|
||||
* @property {{string}} baseUrl - Base URL for the API
|
||||
* @property {{number}} [timeout] - Request timeout in milliseconds
|
||||
*/
|
||||
|
||||
const _isBrowser = typeof window !== 'undefined' && 'caches' in window;
|
||||
const _runIdle = (/** @type {{VoidFunction}} */ fn) => (globalThis.requestIdleCallback ?? setTimeout)(fn);
|
||||
|
||||
/** @type {{Promise<Cache | null>}} */
|
||||
const _cachePromise = _isBrowser
|
||||
? caches.open('__BRK_CLIENT__').catch(() => null)
|
||||
: Promise.resolve(null);
|
||||
|
||||
/**
|
||||
* Custom error class for BRK client errors
|
||||
*/
|
||||
class BrkError extends Error {{
|
||||
/**
|
||||
* @param {{string}} message
|
||||
* @param {{number}} [status]
|
||||
*/
|
||||
constructor(message, status) {{
|
||||
super(message);
|
||||
this.name = 'BrkError';
|
||||
this.status = status;
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} Endpoint
|
||||
* @property {{(onUpdate?: (value: T[]) => void) => Promise<T[]>}} get - Fetch all data points
|
||||
* @property {{(from?: number, to?: number, onUpdate?: (value: T[]) => void) => Promise<T[]>}} range - Fetch data in range
|
||||
* @property {{string}} path - The endpoint path
|
||||
*/
|
||||
|
||||
/**
|
||||
* @template T
|
||||
* @typedef {{Object}} MetricPattern
|
||||
* @property {{string}} name - The metric name
|
||||
* @property {{Partial<Record<Index, Endpoint<T>>>}} by - Index endpoints (lazy getters)
|
||||
* @property {{() => Index[]}} indexes - Get the list of available indexes
|
||||
* @property {{(index: Index) => Endpoint<T>|undefined}} get - Get an endpoint for a specific index
|
||||
*/
|
||||
|
||||
/**
|
||||
* Create an endpoint for a metric index.
|
||||
* @template T
|
||||
* @param {{BrkClientBase}} client
|
||||
* @param {{string}} name - The metric vec name
|
||||
* @param {{Index}} index - The index name
|
||||
* @returns {{Endpoint<T>}}
|
||||
*/
|
||||
function _endpoint(client, name, index) {{
|
||||
const p = `/api/metric/${{name}}/${{index}}`;
|
||||
return {{
|
||||
get: (onUpdate) => client.get(p, onUpdate),
|
||||
range: (from, to, onUpdate) => {{
|
||||
const params = new URLSearchParams();
|
||||
if (from !== undefined) params.set('from', String(from));
|
||||
if (to !== undefined) params.set('to', String(to));
|
||||
const query = params.toString();
|
||||
return client.get(query ? `${{p}}?${{query}}` : p, onUpdate);
|
||||
}},
|
||||
get path() {{ return p; }},
|
||||
}};
|
||||
}}
|
||||
|
||||
/**
|
||||
* Base HTTP client for making requests with caching support
|
||||
*/
|
||||
class BrkClientBase {{
|
||||
/**
|
||||
* @param {{BrkClientOptions|string}} options
|
||||
*/
|
||||
constructor(options) {{
|
||||
const isString = typeof options === 'string';
|
||||
this.baseUrl = isString ? options : options.baseUrl;
|
||||
this.timeout = isString ? 5000 : (options.timeout ?? 5000);
|
||||
}}
|
||||
|
||||
/**
|
||||
* Make a GET request with stale-while-revalidate caching
|
||||
* @template T
|
||||
* @param {{string}} path
|
||||
* @param {{(value: T) => void}} [onUpdate] - Called when data is available
|
||||
* @returns {{Promise<T>}}
|
||||
*/
|
||||
async get(path, onUpdate) {{
|
||||
const base = this.baseUrl.endsWith('/') ? this.baseUrl.slice(0, -1) : this.baseUrl;
|
||||
const url = `${{base}}${{path}}`;
|
||||
const cache = await _cachePromise;
|
||||
const cachedRes = await cache?.match(url);
|
||||
const cachedJson = cachedRes ? await cachedRes.json() : null;
|
||||
|
||||
if (cachedJson) onUpdate?.(cachedJson);
|
||||
if (!globalThis.navigator?.onLine) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw new BrkError('Offline and no cached data available');
|
||||
}}
|
||||
|
||||
try {{
|
||||
const res = await fetch(url, {{ signal: AbortSignal.timeout(this.timeout) }});
|
||||
if (!res.ok) throw new BrkError(`HTTP ${{res.status}}`, res.status);
|
||||
if (cachedRes?.headers.get('ETag') === res.headers.get('ETag')) return cachedJson;
|
||||
|
||||
const cloned = res.clone();
|
||||
const json = await res.json();
|
||||
onUpdate?.(json);
|
||||
if (cache) _runIdle(() => cache.put(url, cloned));
|
||||
return json;
|
||||
}} catch (e) {{
|
||||
if (cachedJson) return cachedJson;
|
||||
throw e;
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/**
|
||||
* Build metric name with optional prefix.
|
||||
* @param {{string}} acc - Accumulated prefix
|
||||
* @param {{string}} s - Metric suffix
|
||||
* @returns {{string}}
|
||||
*/
|
||||
const _m = (acc, s) => acc ? `${{acc}}_${{s}}` : s;
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate static constants for the BrkClient class.
|
||||
pub fn generate_static_constants(output: &mut String) {
|
||||
fn instance_const<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
||||
write_static_const(output, name, &serde_json::to_string_pretty(value).unwrap());
|
||||
}
|
||||
|
||||
fn instance_const_raw(output: &mut String, name: &str, value: &str) {
|
||||
writeln!(output, " {} = {};\n", name, value).unwrap();
|
||||
}
|
||||
|
||||
instance_const_raw(output, "VERSION", &format!("\"v{}\"", VERSION));
|
||||
|
||||
let indexes = Index::all();
|
||||
let indexes_json: Vec<&'static str> = indexes.iter().map(|i| i.serialize_long()).collect();
|
||||
instance_const(output, "INDEXES", &indexes_json);
|
||||
|
||||
let pools = pools();
|
||||
let mut sorted_pools: Vec<_> = pools.iter().collect();
|
||||
sorted_pools.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
let pool_map: std::collections::BTreeMap<PoolSlug, &'static str> =
|
||||
sorted_pools.iter().map(|p| (p.slug(), p.name)).collect();
|
||||
instance_const(output, "POOL_ID_TO_POOL_NAME", &pool_map);
|
||||
|
||||
fn instance_const_camel<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
||||
let json_value: Value = serde_json::to_value(value).unwrap();
|
||||
let camel_value = camel_case_top_level_keys(json_value);
|
||||
write_static_const(output, name, &serde_json::to_string_pretty(&camel_value).unwrap());
|
||||
}
|
||||
|
||||
instance_const_camel(output, "TERM_NAMES", &TERM_NAMES);
|
||||
instance_const_camel(output, "EPOCH_NAMES", &EPOCH_NAMES);
|
||||
instance_const_camel(output, "YEAR_NAMES", &YEAR_NAMES);
|
||||
instance_const_camel(output, "SPENDABLE_TYPE_NAMES", &SPENDABLE_TYPE_NAMES);
|
||||
instance_const_camel(output, "AGE_RANGE_NAMES", &AGE_RANGE_NAMES);
|
||||
instance_const_camel(output, "MAX_AGE_NAMES", &MAX_AGE_NAMES);
|
||||
instance_const_camel(output, "MIN_AGE_NAMES", &MIN_AGE_NAMES);
|
||||
instance_const_camel(output, "AMOUNT_RANGE_NAMES", &AMOUNT_RANGE_NAMES);
|
||||
instance_const_camel(output, "GE_AMOUNT_NAMES", &GE_AMOUNT_NAMES);
|
||||
instance_const_camel(output, "LT_AMOUNT_NAMES", <_AMOUNT_NAMES);
|
||||
}
|
||||
|
||||
fn camel_case_top_level_keys(value: Value) -> Value {
|
||||
match value {
|
||||
Value::Object(map) => {
|
||||
let new_map: serde_json::Map<String, Value> = map
|
||||
.into_iter()
|
||||
.map(|(k, v)| (to_camel_case(&k), v))
|
||||
.collect();
|
||||
Value::Object(new_map)
|
||||
}
|
||||
other => other,
|
||||
}
|
||||
}
|
||||
|
||||
fn indent_json_const(json: &str) -> String {
|
||||
json.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| if i == 0 { line.to_string() } else { format!(" {}", line) })
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n")
|
||||
}
|
||||
|
||||
fn write_static_const(output: &mut String, name: &str, json: &str) {
|
||||
writeln!(output, " {} = /** @type {{const}} */ ({});\n", name, indent_json_const(json)).unwrap();
|
||||
}
|
||||
|
||||
/// Generate index accessor factory functions.
|
||||
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Index accessor factory functions\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let by_fields: Vec<String> = pattern
|
||||
.indexes
|
||||
.iter()
|
||||
.map(|idx| format!("{}: Endpoint<T>", idx.serialize_long()))
|
||||
.collect();
|
||||
let by_type = format!("{{ {} }}", by_fields.join(", "));
|
||||
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * @template T").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" * @typedef {{{{ name: string, by: {}, indexes: () => Index[], get: (index: Index) => Endpoint<T>|undefined }}}} {}",
|
||||
by_type, pattern.name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " */\n").unwrap();
|
||||
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * Create a {} accessor", pattern.name).unwrap();
|
||||
writeln!(output, " * @template T").unwrap();
|
||||
writeln!(output, " * @param {{BrkClientBase}} client").unwrap();
|
||||
writeln!(output, " * @param {{string}} name - The metric vec name").unwrap();
|
||||
writeln!(output, " * @returns {{{}<T>}}", pattern.name).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
writeln!(output, "function create{}(client, name) {{", pattern.name).unwrap();
|
||||
writeln!(output, " return {{").unwrap();
|
||||
writeln!(output, " name,").unwrap();
|
||||
writeln!(output, " by: {{").unwrap();
|
||||
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
let index_name = index.serialize_long();
|
||||
let comma = if i < pattern.indexes.len() - 1 { "," } else { "" };
|
||||
writeln!(
|
||||
output,
|
||||
" get {}() {{ return _endpoint(client, name, '{}'); }}{}",
|
||||
index_name, index_name, comma
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " }},").unwrap();
|
||||
writeln!(output, " indexes() {{").unwrap();
|
||||
|
||||
write!(output, " return [").unwrap();
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(output, ", ").unwrap();
|
||||
}
|
||||
write!(output, "'{}'", index.serialize_long()).unwrap();
|
||||
}
|
||||
writeln!(output, "];").unwrap();
|
||||
|
||||
writeln!(output, " }},").unwrap();
|
||||
writeln!(output, " get(index) {{").unwrap();
|
||||
writeln!(output, " if (this.indexes().includes(index)) {{").unwrap();
|
||||
writeln!(output, " return _endpoint(client, name, index);").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }};").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate structural pattern factory functions.
|
||||
pub fn generate_structural_patterns(
|
||||
output: &mut String,
|
||||
patterns: &[StructuralPattern],
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Reusable structural pattern factories\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let is_parameterizable = pattern.is_parameterizable();
|
||||
|
||||
writeln!(output, "/**").unwrap();
|
||||
if pattern.is_generic {
|
||||
writeln!(output, " * @template T").unwrap();
|
||||
}
|
||||
writeln!(output, " * @typedef {{Object}} {}", pattern.name).unwrap();
|
||||
for field in &pattern.fields {
|
||||
let js_type = field_type_annotation(field, metadata, pattern.is_generic);
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}}} {}",
|
||||
js_type,
|
||||
to_camel_case(&field.name)
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " */\n").unwrap();
|
||||
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * Create a {} pattern node", pattern.name).unwrap();
|
||||
if pattern.is_generic {
|
||||
writeln!(output, " * @template T").unwrap();
|
||||
}
|
||||
writeln!(output, " * @param {{BrkClientBase}} client").unwrap();
|
||||
if is_parameterizable {
|
||||
writeln!(output, " * @param {{string}} acc - Accumulated metric name").unwrap();
|
||||
} else {
|
||||
writeln!(output, " * @param {{string}} basePath").unwrap();
|
||||
}
|
||||
let return_type = if pattern.is_generic {
|
||||
format!("{}<T>", pattern.name)
|
||||
} else {
|
||||
pattern.name.clone()
|
||||
};
|
||||
writeln!(output, " * @returns {{{}}}", return_type).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
|
||||
let param_name = if is_parameterizable { "acc" } else { "basePath" };
|
||||
writeln!(output, "function create{}(client, {}) {{", pattern.name, param_name).unwrap();
|
||||
writeln!(output, " return {{").unwrap();
|
||||
|
||||
let syntax = JavaScriptSyntax;
|
||||
for field in &pattern.fields {
|
||||
if is_parameterizable {
|
||||
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
|
||||
} else {
|
||||
generate_tree_path_field(output, &syntax, field, metadata, " ");
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }};").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn field_type_annotation(field: &PatternField, metadata: &ClientMetadata, is_generic: bool) -> String {
|
||||
metadata.field_type_annotation(field, is_generic, None, GenericSyntax::JAVASCRIPT)
|
||||
}
|
||||
|
||||
/// Get field type with specific generic value type.
|
||||
pub fn field_type_with_generic(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
generic_value_type: Option<&str>,
|
||||
) -> String {
|
||||
metadata.field_type_annotation(field, is_generic, generic_value_type, GenericSyntax::JAVASCRIPT)
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
//! JavaScript client generation.
|
||||
//!
|
||||
//! This module generates a JavaScript + JSDoc client for the BRK API.
|
||||
|
||||
mod api;
|
||||
mod client;
|
||||
mod tree;
|
||||
mod types;
|
||||
|
||||
use std::{fmt::Write, fs, io, path::Path};
|
||||
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{ClientMetadata, Endpoint, TypeSchemas, VERSION};
|
||||
|
||||
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints.
|
||||
///
|
||||
/// `output_path` is the full path to the output file (e.g., "modules/brk-client/index.js").
|
||||
pub fn generate_javascript_client(
|
||||
metadata: &ClientMetadata,
|
||||
endpoints: &[Endpoint],
|
||||
schemas: &TypeSchemas,
|
||||
output_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let mut output = String::new();
|
||||
|
||||
writeln!(output, "// Auto-generated BRK JavaScript client").unwrap();
|
||||
writeln!(output, "// Do not edit manually\n").unwrap();
|
||||
|
||||
types::generate_type_definitions(&mut output, schemas);
|
||||
client::generate_base_client(&mut output);
|
||||
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
|
||||
client::generate_structural_patterns(&mut output, &metadata.structural_patterns, metadata);
|
||||
tree::generate_tree_typedefs(&mut output, &metadata.catalog, metadata);
|
||||
tree::generate_main_client(&mut output, &metadata.catalog, metadata, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
|
||||
// Update package.json version if it exists in the same directory
|
||||
if let Some(parent) = output_path.parent() {
|
||||
let package_json_path = parent.join("package.json");
|
||||
if package_json_path.exists() {
|
||||
update_package_json_version(&package_json_path)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn update_package_json_version(package_json_path: &Path) -> io::Result<()> {
|
||||
let content = fs::read_to_string(package_json_path)?;
|
||||
let mut package: serde_json::Value = serde_json::from_str(&content)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
if let Some(obj) = package.as_object_mut() {
|
||||
obj.insert("version".to_string(), json!(VERSION));
|
||||
}
|
||||
|
||||
let updated = serde_json::to_string_pretty(&package)
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
|
||||
|
||||
fs::write(package_json_path, updated + "\n")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,223 @@
|
||||
//! 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);
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
writeln!(output, "export {{ BrkClient, BrkClientBase, 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)
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
//! JavaScript type definitions generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{TypeSchemas, ref_to_type_name, to_camel_case};
|
||||
|
||||
/// Generate JSDoc type definitions from OpenAPI schemas.
|
||||
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
if schemas.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Type definitions\n").unwrap();
|
||||
|
||||
for (name, schema) in schemas {
|
||||
let js_type = schema_to_js_type(schema, Some(name));
|
||||
|
||||
if is_primitive_alias(schema) {
|
||||
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
|
||||
} else if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
|
||||
writeln!(output, "/**").unwrap();
|
||||
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
|
||||
for (prop_name, prop_schema) in props {
|
||||
let prop_type = schema_to_js_type(prop_schema, Some(name));
|
||||
let required = schema
|
||||
.get("required")
|
||||
.and_then(|r| r.as_array())
|
||||
.map(|arr| arr.iter().any(|v| v.as_str() == Some(prop_name)))
|
||||
.unwrap_or(false);
|
||||
let optional = if required { "" } else { "=" };
|
||||
let safe_name = to_camel_case(prop_name);
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}{}}} {}",
|
||||
prop_type, optional, safe_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " */").unwrap();
|
||||
} else {
|
||||
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
|
||||
fn is_primitive_alias(schema: &Value) -> bool {
|
||||
schema.get("properties").is_none()
|
||||
&& schema.get("items").is_none()
|
||||
&& schema.get("anyOf").is_none()
|
||||
&& schema.get("oneOf").is_none()
|
||||
&& schema.get("enum").is_none()
|
||||
}
|
||||
|
||||
fn json_type_to_js(ty: &str, schema: &Value, current_type: Option<&str>) -> String {
|
||||
match ty {
|
||||
"integer" | "number" => "number".to_string(),
|
||||
"boolean" => "boolean".to_string(),
|
||||
"string" => "string".to_string(),
|
||||
"null" => "null".to_string(),
|
||||
"array" => {
|
||||
let item_type = schema
|
||||
.get("items")
|
||||
.map(|s| schema_to_js_type(s, current_type))
|
||||
.unwrap_or_else(|| "*".to_string());
|
||||
format!("{}[]", item_type)
|
||||
}
|
||||
"object" => {
|
||||
if let Some(add_props) = schema.get("additionalProperties") {
|
||||
let value_type = schema_to_js_type(add_props, current_type);
|
||||
return format!("{{ [key: string]: {} }}", value_type);
|
||||
}
|
||||
"Object".to_string()
|
||||
}
|
||||
_ => "*".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a JSON schema to a JavaScript type string.
|
||||
pub fn schema_to_js_type(schema: &Value, current_type: Option<&str>) -> String {
|
||||
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
|
||||
for item in all_of {
|
||||
let resolved = schema_to_js_type(item, current_type);
|
||||
if resolved != "*" {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
|
||||
return ref_to_type_name(ref_path).unwrap_or("*").to_string();
|
||||
}
|
||||
|
||||
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
|
||||
let literals: Vec<String> = enum_values
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect();
|
||||
if !literals.is_empty() {
|
||||
return format!("({})", literals.join("|"));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ty) = schema.get("type") {
|
||||
if let Some(type_array) = ty.as_array() {
|
||||
let types: Vec<String> = type_array
|
||||
.iter()
|
||||
.filter_map(|t| t.as_str())
|
||||
.filter(|t| *t != "null")
|
||||
.map(|t| json_type_to_js(t, schema, current_type))
|
||||
.collect();
|
||||
let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
|
||||
|
||||
if types.len() == 1 {
|
||||
let base_type = &types[0];
|
||||
return if has_null {
|
||||
format!("?{}", base_type)
|
||||
} else {
|
||||
base_type.clone()
|
||||
};
|
||||
} else if !types.is_empty() {
|
||||
let union = format!("({})", types.join("|"));
|
||||
return if has_null {
|
||||
format!("?{}", union)
|
||||
} else {
|
||||
union
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ty_str) = ty.as_str() {
|
||||
return json_type_to_js(ty_str, schema, current_type);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(variants) = schema
|
||||
.get("anyOf")
|
||||
.or_else(|| schema.get("oneOf"))
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
let types: Vec<String> = variants
|
||||
.iter()
|
||||
.map(|v| schema_to_js_type(v, current_type))
|
||||
.collect();
|
||||
let filtered: Vec<_> = types.iter().filter(|t| *t != "*").collect();
|
||||
if !filtered.is_empty() {
|
||||
return format!(
|
||||
"({})",
|
||||
filtered
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join("|")
|
||||
);
|
||||
}
|
||||
return format!("({})", types.join("|"));
|
||||
}
|
||||
|
||||
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
|
||||
return match format {
|
||||
"int32" | "int64" => "number".to_string(),
|
||||
"float" | "double" => "number".to_string(),
|
||||
"date" | "date-time" => "string".to_string(),
|
||||
_ => "*".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
"*".to_string()
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Code generators for client libraries.
|
||||
//!
|
||||
//! Each language has its own submodule with focused files:
|
||||
//! - `types.rs` - Type definitions
|
||||
//! - `client.rs` - Base client and pattern factories
|
||||
//! - `tree.rs` - Tree structure generation
|
||||
//! - `api.rs` - API method generation
|
||||
//! - `mod.rs` - Entry point
|
||||
|
||||
pub mod javascript;
|
||||
pub mod python;
|
||||
pub mod rust;
|
||||
|
||||
pub use javascript::generate_javascript_client;
|
||||
pub use python::generate_python_client;
|
||||
pub use rust::generate_rust_client;
|
||||
@@ -0,0 +1,151 @@
|
||||
//! Python API method generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Endpoint, Parameter, escape_python_keyword, to_snake_case};
|
||||
|
||||
use super::client::generate_class_constants;
|
||||
use super::types::js_type_to_python;
|
||||
|
||||
/// Generate the main client class
|
||||
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Main BRK client with catalog tree and API methods.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate class-level constants
|
||||
generate_class_constants(output);
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0):"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " super().__init__(base_url, timeout)").unwrap();
|
||||
writeln!(output, " self.tree = CatalogTree(self)").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate API methods
|
||||
generate_api_methods(output, endpoints);
|
||||
}
|
||||
|
||||
/// Generate API methods from OpenAPI endpoints
|
||||
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
for endpoint in endpoints {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = endpoint
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_python)
|
||||
.unwrap_or_else(|| "Any".to_string());
|
||||
|
||||
// Build method signature
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" def {}(self{}) -> {}:",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
// Docstring
|
||||
match (&endpoint.summary, &endpoint.description) {
|
||||
(Some(summary), Some(desc)) if summary != desc => {
|
||||
writeln!(output, " \"\"\"{}.", summary.trim_end_matches('.')).unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " {}\"\"\"", desc).unwrap();
|
||||
}
|
||||
(Some(summary), _) => {
|
||||
writeln!(output, " \"\"\"{}\"\"\"", summary).unwrap();
|
||||
}
|
||||
(None, Some(desc)) => {
|
||||
writeln!(output, " \"\"\"{}\"\"\"", desc).unwrap();
|
||||
}
|
||||
(None, None) => {}
|
||||
}
|
||||
|
||||
// Build path
|
||||
let path = build_path_template(&endpoint.path, &endpoint.path_params);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
if endpoint.path_params.is_empty() {
|
||||
writeln!(output, " return self.get('{}')", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " return self.get(f'{}')", path).unwrap();
|
||||
}
|
||||
} else {
|
||||
writeln!(output, " params = []").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
// Use safe name for Python variable, original name for API query parameter
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" params.append(f'{}={{{}}}')",
|
||||
param.name, safe_name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if {} is not None: params.append(f'{}={{{}}}')",
|
||||
safe_name, param.name, safe_name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " query = '&'.join(params)").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return self.get(f'{}{{\"?\" + query if query else \"\"}}')",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
to_snake_case(&endpoint.operation_name())
|
||||
}
|
||||
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
let py_type = js_type_to_python(¶m.param_type);
|
||||
params.push(format!(", {}: {}", safe_name, py_type));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
let py_type = js_type_to_python(¶m.param_type);
|
||||
if param.required {
|
||||
params.push(format!(", {}: {}", safe_name, py_type));
|
||||
} else {
|
||||
params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
|
||||
let mut result = path.to_string();
|
||||
for param in path_params {
|
||||
let placeholder = format!("{{{}}}", param.name);
|
||||
// Use escaped name for Python variable interpolation in f-string
|
||||
let safe_name = escape_python_keyword(¶m.name);
|
||||
let interpolation = format!("{{{}}}", safe_name);
|
||||
result = result.replace(&placeholder, &interpolation);
|
||||
}
|
||||
result
|
||||
}
|
||||
@@ -0,0 +1,337 @@
|
||||
//! Python base client and pattern factory generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_cohort::{
|
||||
AGE_RANGE_NAMES, AMOUNT_RANGE_NAMES, EPOCH_NAMES, GE_AMOUNT_NAMES, LT_AMOUNT_NAMES,
|
||||
MAX_AGE_NAMES, MIN_AGE_NAMES, SPENDABLE_TYPE_NAMES, TERM_NAMES, YEAR_NAMES,
|
||||
};
|
||||
use brk_types::{pools, Index};
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, IndexSetPattern, PatternField, PythonSyntax,
|
||||
StructuralPattern, VERSION, generate_parameterized_field, generate_tree_path_field,
|
||||
index_to_field_name,
|
||||
};
|
||||
|
||||
/// Generate class-level constants for the BrkClient class.
|
||||
pub fn generate_class_constants(output: &mut String) {
|
||||
fn class_const<T: Serialize>(output: &mut String, name: &str, value: &T) {
|
||||
let json = serde_json::to_string_pretty(value).unwrap();
|
||||
// Indent all lines for class body
|
||||
let indented = json
|
||||
.lines()
|
||||
.enumerate()
|
||||
.map(|(i, line)| {
|
||||
if i == 0 {
|
||||
format!(" {} = {}", name, line)
|
||||
} else {
|
||||
format!(" {}", line)
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
writeln!(output, "{}\n", indented).unwrap();
|
||||
}
|
||||
|
||||
// VERSION
|
||||
writeln!(output, " VERSION = \"v{}\"\n", VERSION).unwrap();
|
||||
|
||||
// INDEXES
|
||||
let indexes = Index::all();
|
||||
let indexes_list: Vec<&str> = indexes.iter().map(|i| i.serialize_long()).collect();
|
||||
class_const(output, "INDEXES", &indexes_list);
|
||||
|
||||
// POOL_ID_TO_POOL_NAME
|
||||
let pools = pools();
|
||||
let mut sorted_pools: Vec<_> = pools.iter().collect();
|
||||
sorted_pools.sort_by(|a, b| a.name.to_lowercase().cmp(&b.name.to_lowercase()));
|
||||
let pool_map: std::collections::BTreeMap<String, &str> = sorted_pools
|
||||
.iter()
|
||||
.map(|p| (p.slug().to_string(), p.name))
|
||||
.collect();
|
||||
class_const(output, "POOL_ID_TO_POOL_NAME", &pool_map);
|
||||
|
||||
// Cohort names
|
||||
class_const(output, "TERM_NAMES", &TERM_NAMES);
|
||||
class_const(output, "EPOCH_NAMES", &EPOCH_NAMES);
|
||||
class_const(output, "YEAR_NAMES", &YEAR_NAMES);
|
||||
class_const(output, "SPENDABLE_TYPE_NAMES", &SPENDABLE_TYPE_NAMES);
|
||||
class_const(output, "AGE_RANGE_NAMES", &AGE_RANGE_NAMES);
|
||||
class_const(output, "MAX_AGE_NAMES", &MAX_AGE_NAMES);
|
||||
class_const(output, "MIN_AGE_NAMES", &MIN_AGE_NAMES);
|
||||
class_const(output, "AMOUNT_RANGE_NAMES", &AMOUNT_RANGE_NAMES);
|
||||
class_const(output, "GE_AMOUNT_NAMES", &GE_AMOUNT_NAMES);
|
||||
class_const(output, "LT_AMOUNT_NAMES", <_AMOUNT_NAMES);
|
||||
}
|
||||
|
||||
/// Generate the base BrkClient class with HTTP functionality
|
||||
pub fn generate_base_client(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"class BrkError(Exception):
|
||||
"""Custom error class for BRK client errors."""
|
||||
|
||||
def __init__(self, message: str, status: Optional[int] = None):
|
||||
super().__init__(message)
|
||||
self.status = status
|
||||
|
||||
|
||||
class BrkClientBase:
|
||||
"""Base HTTP client for making requests."""
|
||||
|
||||
def __init__(self, base_url: str, timeout: float = 30.0):
|
||||
self.base_url = base_url
|
||||
self.timeout = timeout
|
||||
self._client = httpx.Client(timeout=timeout)
|
||||
|
||||
def get(self, path: str) -> Any:
|
||||
"""Make a GET request."""
|
||||
try:
|
||||
base = self.base_url.rstrip('/')
|
||||
response = self._client.get(f"{{base}}{{path}}")
|
||||
response.raise_for_status()
|
||||
return response.json()
|
||||
except httpx.HTTPStatusError as e:
|
||||
raise BrkError(f"HTTP error: {{e.response.status_code}}", e.response.status_code)
|
||||
except httpx.RequestError as e:
|
||||
raise BrkError(str(e))
|
||||
|
||||
def close(self):
|
||||
"""Close the HTTP client."""
|
||||
self._client.close()
|
||||
|
||||
def __enter__(self):
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
self.close()
|
||||
|
||||
|
||||
def _m(acc: str, s: str) -> str:
|
||||
"""Build metric name with optional prefix."""
|
||||
return f"{{acc}}_{{s}}" if acc else s
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the Endpoint class
|
||||
pub fn generate_endpoint_class(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"class Endpoint(Generic[T]):
|
||||
"""An endpoint for a specific metric + index combination."""
|
||||
|
||||
def __init__(self, client: BrkClientBase, name: str, index: str):
|
||||
self._client = client
|
||||
self._name = name
|
||||
self._index = index
|
||||
|
||||
def get(self) -> List[T]:
|
||||
"""Fetch all data points for this metric/index."""
|
||||
return self._client.get(self.path())
|
||||
|
||||
def range(self, from_val: Optional[int] = None, to_val: Optional[int] = None) -> List[T]:
|
||||
"""Fetch data points within a range."""
|
||||
params = []
|
||||
if from_val is not None:
|
||||
params.append(f"from={{from_val}}")
|
||||
if to_val is not None:
|
||||
params.append(f"to={{to_val}}")
|
||||
query = "&".join(params)
|
||||
p = self.path()
|
||||
return self._client.get(f"{{p}}?{{query}}" if query else p)
|
||||
|
||||
def path(self) -> str:
|
||||
"""Get the endpoint path."""
|
||||
return f"/api/metric/{{self._name}}/{{self._index}}"
|
||||
|
||||
|
||||
class MetricPattern(Protocol[T]):
|
||||
"""Protocol for metric patterns with different index sets."""
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
"""Get the metric name."""
|
||||
...
|
||||
|
||||
def indexes(self) -> List[str]:
|
||||
"""Get the list of available indexes for this metric."""
|
||||
...
|
||||
|
||||
def get(self, index: str) -> Optional[Endpoint[T]]:
|
||||
"""Get an endpoint for a specific index, if supported."""
|
||||
...
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate index accessor classes
|
||||
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "# Index accessor classes\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let by_class_name = format!("_{}By", pattern.name);
|
||||
|
||||
// Generate the By class with lazy endpoint methods
|
||||
writeln!(output, "class {}(Generic[T]):", by_class_name).unwrap();
|
||||
writeln!(output, " \"\"\"Index endpoint methods container.\"\"\"").unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, name: str):"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " self._client = client").unwrap();
|
||||
writeln!(output, " self._name = name").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate methods for each index
|
||||
for index in &pattern.indexes {
|
||||
let method_name = index_to_field_name(index);
|
||||
let index_name = index.serialize_long();
|
||||
writeln!(output, " def {}(self) -> Endpoint[T]:", method_name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" return Endpoint(self._client, self._name, '{}')",
|
||||
index_name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
|
||||
// Generate the main accessor class
|
||||
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Index accessor for metrics with {} indexes.\"\"\"",
|
||||
pattern.indexes.len()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, name: str):"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " self._client = client").unwrap();
|
||||
writeln!(output, " self._name = name").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.by: {}[T] = {}(client, name)",
|
||||
by_class_name, by_class_name
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " @property").unwrap();
|
||||
writeln!(output, " def name(self) -> str:").unwrap();
|
||||
writeln!(output, " \"\"\"Get the metric name.\"\"\"").unwrap();
|
||||
writeln!(output, " return self._name").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " def indexes(self) -> List[str]:").unwrap();
|
||||
writeln!(output, " \"\"\"Get the list of available indexes.\"\"\"").unwrap();
|
||||
write!(output, " return [").unwrap();
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
if i > 0 {
|
||||
write!(output, ", ").unwrap();
|
||||
}
|
||||
write!(output, "'{}'", index.serialize_long()).unwrap();
|
||||
}
|
||||
writeln!(output, "]").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate get(index) method
|
||||
writeln!(output, " def get(self, index: str) -> Optional[Endpoint[T]]:").unwrap();
|
||||
writeln!(output, " \"\"\"Get an endpoint for a specific index, if supported.\"\"\"").unwrap();
|
||||
for (i, index) in pattern.indexes.iter().enumerate() {
|
||||
let method_name = index_to_field_name(index);
|
||||
let index_name = index.serialize_long();
|
||||
if i == 0 {
|
||||
writeln!(output, " if index == '{}': return self.by.{}()", index_name, method_name).unwrap();
|
||||
} else {
|
||||
writeln!(output, " elif index == '{}': return self.by.{}()", index_name, method_name).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " return None").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate structural pattern classes
|
||||
pub fn generate_structural_patterns(
|
||||
output: &mut String,
|
||||
patterns: &[StructuralPattern],
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "# Reusable structural pattern classes\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let is_parameterizable = pattern.is_parameterizable();
|
||||
|
||||
// For generic patterns, inherit from Generic[T]
|
||||
if pattern.is_generic {
|
||||
writeln!(output, "class {}(Generic[T]):", pattern.name).unwrap();
|
||||
} else {
|
||||
writeln!(output, "class {}:", pattern.name).unwrap();
|
||||
}
|
||||
writeln!(
|
||||
output,
|
||||
" \"\"\"Pattern struct for repeated tree structure.\"\"\""
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
|
||||
if is_parameterizable {
|
||||
writeln!(
|
||||
output,
|
||||
" 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();
|
||||
}
|
||||
|
||||
let syntax = PythonSyntax;
|
||||
for field in &pattern.fields {
|
||||
if is_parameterizable {
|
||||
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
|
||||
} else {
|
||||
generate_tree_path_field(output, &syntax, field, metadata, " ");
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Python type annotation for a field with optional generic value type.
|
||||
pub fn field_type_with_generic(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
generic_value_type: Option<&str>,
|
||||
) -> String {
|
||||
metadata.field_type_annotation(field, is_generic, generic_value_type, GenericSyntax::PYTHON)
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
//! Python client generation.
|
||||
//!
|
||||
//! This module generates a Python client with type hints for the BRK API.
|
||||
|
||||
mod api;
|
||||
mod client;
|
||||
mod tree;
|
||||
mod types;
|
||||
|
||||
use std::{fmt::Write, fs, io, path::Path};
|
||||
|
||||
use crate::{ClientMetadata, Endpoint, TypeSchemas};
|
||||
|
||||
/// Generate Python client from metadata and OpenAPI endpoints.
|
||||
///
|
||||
/// `output_path` is the full path to the output file (e.g., "packages/brk_client/__init__.py").
|
||||
pub fn generate_python_client(
|
||||
metadata: &ClientMetadata,
|
||||
endpoints: &[Endpoint],
|
||||
schemas: &TypeSchemas,
|
||||
output_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let mut output = String::new();
|
||||
|
||||
writeln!(output, "# Auto-generated BRK Python client").unwrap();
|
||||
writeln!(output, "# Do not edit manually\n").unwrap();
|
||||
writeln!(output, "from __future__ import annotations").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Final, Union, Protocol"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "import httpx\n").unwrap();
|
||||
writeln!(output, "T = TypeVar('T')\n").unwrap();
|
||||
|
||||
types::generate_type_definitions(&mut output, schemas);
|
||||
client::generate_base_client(&mut output);
|
||||
client::generate_endpoint_class(&mut output);
|
||||
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
|
||||
client::generate_structural_patterns(&mut output, &metadata.structural_patterns, metadata);
|
||||
tree::generate_tree_classes(&mut output, &metadata.catalog, metadata);
|
||||
api::generate_main_client(&mut output, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
//! Python tree structure generation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, PatternField, child_type_name, get_fields_with_child_info, get_node_fields,
|
||||
get_pattern_instance_base, to_snake_case,
|
||||
};
|
||||
|
||||
use super::client::field_type_with_generic;
|
||||
|
||||
/// Generate tree classes
|
||||
pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "# Catalog tree classes\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
generate_tree_class(
|
||||
output,
|
||||
"CatalogTree",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
}
|
||||
|
||||
/// Recursively generate tree classes
|
||||
fn generate_tree_class(
|
||||
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();
|
||||
|
||||
// Skip if this matches a pattern (already generated)
|
||||
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, "class {}:", name).unwrap();
|
||||
writeln!(output, " \"\"\"Catalog tree node.\"\"\"").unwrap();
|
||||
writeln!(output, " ").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" def __init__(self, client: BrkClientBase, base_path: str = ''):"
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for ((field, child_fields_opt), (_child_name, child_node)) in
|
||||
fields_with_child_info.iter().zip(children.iter())
|
||||
{
|
||||
// Look up type parameter for generic patterns
|
||||
let generic_value_type = child_fields_opt
|
||||
.as_ref()
|
||||
.and_then(|cf| metadata.get_type_param(cf))
|
||||
.map(String::as_str);
|
||||
let py_type = field_type_with_generic(field, metadata, false, generic_value_type);
|
||||
let field_name_py = to_snake_case(&field.name);
|
||||
|
||||
if metadata.is_pattern_type(&field.rust_type) {
|
||||
let pattern = metadata.find_pattern(&field.rust_type);
|
||||
let is_parameterizable = pattern.is_some_and(|p| p.is_parameterizable());
|
||||
|
||||
if is_parameterizable {
|
||||
let metric_base = get_pattern_instance_base(child_node);
|
||||
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 metadata.field_uses_accessor(field) {
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, accessor.name, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch - instantiate the nested class
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, field.rust_type, field.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
// All metrics must be indexed - this should not be reached
|
||||
panic!(
|
||||
"Field '{}' has no matching index pattern. All metrics must be indexed.",
|
||||
field.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate child classes
|
||||
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_class = child_type_name(name, child_name);
|
||||
generate_tree_class(
|
||||
output,
|
||||
&child_class,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
//! Python type definitions generation.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
use std::fmt::Write;
|
||||
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::{TypeSchemas, escape_python_keyword, ref_to_type_name};
|
||||
|
||||
/// Generate type definitions from schemas.
|
||||
pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
if schemas.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "# Type definitions\n").unwrap();
|
||||
|
||||
let sorted_names = topological_sort_schemas(schemas);
|
||||
|
||||
for name in sorted_names {
|
||||
let Some(schema) = schemas.get(&name) else {
|
||||
continue;
|
||||
};
|
||||
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
|
||||
writeln!(output, "class {}(TypedDict):", name).unwrap();
|
||||
for (prop_name, prop_schema) in props {
|
||||
let prop_type = schema_to_python_type_ctx(prop_schema, Some(&name));
|
||||
let safe_name = escape_python_keyword(prop_name);
|
||||
writeln!(output, " {}: {}", safe_name, prop_type).unwrap();
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
} else {
|
||||
let py_type = schema_to_python_type_ctx(schema, Some(&name));
|
||||
writeln!(output, "{} = {}", name, py_type).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
|
||||
/// Topologically sort schema names so dependencies come before dependents (avoids forward references).
|
||||
/// Types that reference other types (via $ref) must be defined after their dependencies.
|
||||
fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
|
||||
// Build dependency graph
|
||||
let mut deps: HashMap<String, HashSet<String>> = HashMap::new();
|
||||
for (name, schema) in schemas {
|
||||
let mut type_deps = HashSet::new();
|
||||
collect_schema_refs(schema, &mut type_deps);
|
||||
// Only keep deps that are in our schemas
|
||||
type_deps.retain(|d| schemas.contains_key(d));
|
||||
deps.insert(name.clone(), type_deps);
|
||||
}
|
||||
|
||||
// Kahn's algorithm for topological sort
|
||||
let mut in_degree: HashMap<String, usize> = HashMap::new();
|
||||
for name in schemas.keys() {
|
||||
in_degree.insert(name.clone(), 0);
|
||||
}
|
||||
for type_deps in deps.values() {
|
||||
for dep in type_deps {
|
||||
*in_degree.entry(dep.clone()).or_insert(0) += 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Start with types that have no dependents (are not referenced by others)
|
||||
let mut queue: Vec<String> = in_degree
|
||||
.iter()
|
||||
.filter(|(_, count)| **count == 0)
|
||||
.map(|(name, _)| name.clone())
|
||||
.collect();
|
||||
queue.sort(); // Deterministic order
|
||||
|
||||
let mut result = Vec::new();
|
||||
while let Some(name) = queue.pop() {
|
||||
result.push(name.clone());
|
||||
if let Some(type_deps) = deps.get(&name) {
|
||||
for dep in type_deps {
|
||||
if let Some(count) = in_degree.get_mut(dep) {
|
||||
*count = count.saturating_sub(1);
|
||||
if *count == 0 {
|
||||
queue.push(dep.clone());
|
||||
queue.sort(); // Keep sorted for determinism
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Reverse so dependencies come first
|
||||
result.reverse();
|
||||
|
||||
// Add any types that weren't processed (e.g., due to circular refs or other edge cases)
|
||||
let result_set: HashSet<_> = result.iter().cloned().collect();
|
||||
let mut missing: Vec<_> = schemas
|
||||
.keys()
|
||||
.filter(|k| !result_set.contains(*k))
|
||||
.cloned()
|
||||
.collect();
|
||||
missing.sort();
|
||||
result.extend(missing);
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
/// Collect all type references ($ref) from a schema
|
||||
fn collect_schema_refs(schema: &Value, refs: &mut HashSet<String>) {
|
||||
match schema {
|
||||
Value::Object(map) => {
|
||||
if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str())
|
||||
&& let Some(type_name) = ref_to_type_name(ref_path)
|
||||
{
|
||||
refs.insert(type_name.to_string());
|
||||
}
|
||||
for value in map.values() {
|
||||
collect_schema_refs(value, refs);
|
||||
}
|
||||
}
|
||||
Value::Array(arr) => {
|
||||
for item in arr {
|
||||
collect_schema_refs(item, refs);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert a single JSON type string to Python type
|
||||
fn json_type_to_python(ty: &str, schema: &Value, current_type: Option<&str>) -> String {
|
||||
match ty {
|
||||
"integer" => "int".to_string(),
|
||||
"number" => "float".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"string" => "str".to_string(),
|
||||
"null" => "None".to_string(),
|
||||
"array" => {
|
||||
let item_type = schema
|
||||
.get("items")
|
||||
.map(|s| schema_to_python_type_ctx(s, current_type))
|
||||
.unwrap_or_else(|| "Any".to_string());
|
||||
format!("List[{}]", item_type)
|
||||
}
|
||||
"object" => {
|
||||
if let Some(add_props) = schema.get("additionalProperties") {
|
||||
let value_type = schema_to_python_type_ctx(add_props, current_type);
|
||||
return format!("dict[str, {}]", value_type);
|
||||
}
|
||||
"dict".to_string()
|
||||
}
|
||||
_ => "Any".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert JSON Schema to Python type with context for detecting self-references
|
||||
pub fn schema_to_python_type_ctx(schema: &Value, current_type: Option<&str>) -> String {
|
||||
if let Some(all_of) = schema.get("allOf").and_then(|v| v.as_array()) {
|
||||
for item in all_of {
|
||||
let resolved = schema_to_python_type_ctx(item, current_type);
|
||||
if resolved != "Any" {
|
||||
return resolved;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle $ref
|
||||
if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) {
|
||||
let type_name = ref_to_type_name(ref_path).unwrap_or("Any");
|
||||
// Quote self-references to handle recursive types
|
||||
if current_type == Some(type_name) {
|
||||
return format!("\"{}\"", type_name);
|
||||
}
|
||||
return type_name.to_string();
|
||||
}
|
||||
|
||||
// Handle enum (array of string values)
|
||||
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
|
||||
let literals: Vec<String> = enum_values
|
||||
.iter()
|
||||
.filter_map(|v| v.as_str())
|
||||
.map(|s| format!("\"{}\"", s))
|
||||
.collect();
|
||||
if !literals.is_empty() {
|
||||
return format!("Literal[{}]", literals.join(", "));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ty) = schema.get("type") {
|
||||
if let Some(type_array) = ty.as_array() {
|
||||
let types: Vec<String> = type_array
|
||||
.iter()
|
||||
.filter_map(|t| t.as_str())
|
||||
.filter(|t| *t != "null") // Filter out null for cleaner Optional handling
|
||||
.map(|t| json_type_to_python(t, schema, current_type))
|
||||
.collect();
|
||||
let has_null = type_array.iter().any(|t| t.as_str() == Some("null"));
|
||||
|
||||
if types.len() == 1 {
|
||||
let base_type = &types[0];
|
||||
return if has_null {
|
||||
format!("Optional[{}]", base_type)
|
||||
} else {
|
||||
base_type.clone()
|
||||
};
|
||||
} else if !types.is_empty() {
|
||||
let union = format!("Union[{}]", types.join(", "));
|
||||
return if has_null {
|
||||
format!("Optional[{}]", union)
|
||||
} else {
|
||||
union
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(ty_str) = ty.as_str() {
|
||||
return json_type_to_python(ty_str, schema, current_type);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(variants) = schema
|
||||
.get("anyOf")
|
||||
.or_else(|| schema.get("oneOf"))
|
||||
.and_then(|v| v.as_array())
|
||||
{
|
||||
let types: Vec<String> = variants
|
||||
.iter()
|
||||
.map(|v| schema_to_python_type_ctx(v, current_type))
|
||||
.collect();
|
||||
let filtered: Vec<_> = types.iter().filter(|t| *t != "Any").collect();
|
||||
if !filtered.is_empty() {
|
||||
return format!(
|
||||
"Union[{}]",
|
||||
filtered
|
||||
.iter()
|
||||
.map(|s| s.as_str())
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ")
|
||||
);
|
||||
}
|
||||
return format!("Union[{}]", types.join(", "));
|
||||
}
|
||||
|
||||
// Check for format hint without type (common in OpenAPI)
|
||||
if let Some(format) = schema.get("format").and_then(|f| f.as_str()) {
|
||||
return match format {
|
||||
"int32" | "int64" => "int".to_string(),
|
||||
"float" | "double" => "float".to_string(),
|
||||
"date" | "date-time" => "str".to_string(),
|
||||
_ => "Any".to_string(),
|
||||
};
|
||||
}
|
||||
|
||||
"Any".to_string()
|
||||
}
|
||||
|
||||
/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]", "number" -> "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(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"string" => "str".to_string(),
|
||||
"null" => "None".to_string(),
|
||||
"Object" | "object" => "dict".to_string(),
|
||||
"*" => "Any".to_string(),
|
||||
_ => js_type.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,144 @@
|
||||
//! Rust API method generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{Endpoint, VERSION, to_snake_case};
|
||||
|
||||
use super::types::js_type_to_rust;
|
||||
|
||||
/// Generate the main BrkClient struct.
|
||||
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Main BRK client with catalog tree and API methods.
|
||||
pub struct BrkClient {{
|
||||
base: Arc<BrkClientBase>,
|
||||
tree: CatalogTree,
|
||||
}}
|
||||
|
||||
impl BrkClient {{
|
||||
/// Client version.
|
||||
pub const VERSION: &'static str = "v{VERSION}";
|
||||
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {{
|
||||
let base = Arc::new(BrkClientBase::new(base_url));
|
||||
let tree = CatalogTree::new(base.clone(), String::new());
|
||||
Self {{ base, tree }}
|
||||
}}
|
||||
|
||||
/// Create a new client with options.
|
||||
pub fn with_options(options: BrkClientOptions) -> Self {{
|
||||
let base = Arc::new(BrkClientBase::with_options(options));
|
||||
let tree = CatalogTree::new(base.clone(), String::new());
|
||||
Self {{ base, tree }}
|
||||
}}
|
||||
|
||||
/// Get the catalog tree for navigating metrics.
|
||||
pub fn tree(&self) -> &CatalogTree {{
|
||||
&self.tree
|
||||
}}
|
||||
"#,
|
||||
VERSION = VERSION
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
generate_api_methods(output, endpoints);
|
||||
|
||||
writeln!(output, "}}").unwrap();
|
||||
}
|
||||
|
||||
/// Generate API methods from OpenAPI endpoints.
|
||||
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
for endpoint in endpoints {
|
||||
if !endpoint.should_generate() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let method_name = endpoint_to_method_name(endpoint);
|
||||
let return_type = endpoint
|
||||
.response_type
|
||||
.as_deref()
|
||||
.map(js_type_to_rust)
|
||||
.unwrap_or_else(|| "serde_json::Value".to_string());
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" /// {}",
|
||||
endpoint.summary.as_deref().unwrap_or(&method_name)
|
||||
)
|
||||
.unwrap();
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " ///").unwrap();
|
||||
writeln!(output, " /// {}", desc).unwrap();
|
||||
}
|
||||
|
||||
let params = build_method_params(endpoint);
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn {}(&self{}) -> Result<{}> {{",
|
||||
method_name, params, return_type
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let path = build_path_template(&endpoint.path);
|
||||
|
||||
if endpoint.query_params.is_empty() {
|
||||
writeln!(output, " self.base.get(&format!(\"{}\"))", path).unwrap();
|
||||
} else {
|
||||
writeln!(output, " let mut query = Vec::new();").unwrap();
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
writeln!(
|
||||
output,
|
||||
" query.push(format!(\"{}={{}}\", {}));",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" if let Some(v) = {} {{ query.push(format!(\"{}={{}}\", v)); }}",
|
||||
param.name, param.name
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, " let query_str = if query.is_empty() {{ String::new() }} else {{ format!(\"?{{}}\", query.join(\"&\")) }};").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.base.get(&format!(\"{}{{}}\", query_str))",
|
||||
path
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, " }}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
|
||||
to_snake_case(&endpoint.operation_name())
|
||||
}
|
||||
|
||||
fn build_method_params(endpoint: &Endpoint) -> String {
|
||||
let mut params = Vec::new();
|
||||
for param in &endpoint.path_params {
|
||||
params.push(format!(", {}: &str", param.name));
|
||||
}
|
||||
for param in &endpoint.query_params {
|
||||
if param.required {
|
||||
params.push(format!(", {}: &str", param.name));
|
||||
} else {
|
||||
params.push(format!(", {}: Option<&str>", param.name));
|
||||
}
|
||||
}
|
||||
params.join("")
|
||||
}
|
||||
|
||||
/// OpenAPI path placeholders `{param}` are already valid Rust format string syntax.
|
||||
fn build_path_template(path: &str) -> &str {
|
||||
path
|
||||
}
|
||||
@@ -0,0 +1,380 @@
|
||||
//! Rust base client and pattern factory generation.
|
||||
|
||||
use std::fmt::Write;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, GenericSyntax, IndexSetPattern, PatternField, RustSyntax,
|
||||
StructuralPattern, generate_parameterized_field, generate_tree_path_field,
|
||||
index_to_field_name, to_snake_case,
|
||||
};
|
||||
|
||||
/// Generate import statements.
|
||||
pub fn generate_imports(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"use std::sync::Arc;
|
||||
use serde::de::DeserializeOwned;
|
||||
pub use brk_cohort::*;
|
||||
pub use brk_types::*;
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the base BrkClientBase struct and error types.
|
||||
pub fn generate_base_client(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Error type for BRK client operations.
|
||||
#[derive(Debug)]
|
||||
pub struct BrkError {{
|
||||
pub message: String,
|
||||
}}
|
||||
|
||||
impl std::fmt::Display for BrkError {{
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {{
|
||||
write!(f, "{{}}", self.message)
|
||||
}}
|
||||
}}
|
||||
|
||||
impl std::error::Error for BrkError {{}}
|
||||
|
||||
/// Result type for BRK client operations.
|
||||
pub type Result<T> = std::result::Result<T, BrkError>;
|
||||
|
||||
/// Options for configuring the BRK client.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientOptions {{
|
||||
pub base_url: String,
|
||||
pub timeout_secs: u64,
|
||||
}}
|
||||
|
||||
impl Default for BrkClientOptions {{
|
||||
fn default() -> Self {{
|
||||
Self {{
|
||||
base_url: "http://localhost:3000".to_string(),
|
||||
timeout_secs: 30,
|
||||
}}
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Base HTTP client for making requests.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BrkClientBase {{
|
||||
base_url: String,
|
||||
timeout_secs: u64,
|
||||
}}
|
||||
|
||||
impl BrkClientBase {{
|
||||
/// Create a new client with the given base URL.
|
||||
pub fn new(base_url: impl Into<String>) -> Self {{
|
||||
Self {{
|
||||
base_url: base_url.into(),
|
||||
timeout_secs: 30,
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Create a new client with options.
|
||||
pub fn with_options(options: BrkClientOptions) -> Self {{
|
||||
Self {{
|
||||
base_url: options.base_url,
|
||||
timeout_secs: options.timeout_secs,
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Make a GET request.
|
||||
pub fn get<T: DeserializeOwned>(&self, path: &str) -> Result<T> {{
|
||||
let base = self.base_url.trim_end_matches('/');
|
||||
let url = format!("{{}}{{}}", base, path);
|
||||
let response = minreq::get(&url)
|
||||
.with_timeout(self.timeout_secs)
|
||||
.send()
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})?;
|
||||
|
||||
if response.status_code >= 400 {{
|
||||
return Err(BrkError {{
|
||||
message: format!("HTTP {{}}", response.status_code),
|
||||
}});
|
||||
}}
|
||||
|
||||
response
|
||||
.json()
|
||||
.map_err(|e| BrkError {{ message: e.to_string() }})
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Build metric name with optional prefix.
|
||||
#[inline]
|
||||
fn _m(acc: &str, s: &str) -> String {{
|
||||
if acc.is_empty() {{ s.to_string() }} else {{ format!("{{acc}}_{{s}}") }}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the MetricPattern trait.
|
||||
pub fn generate_metric_pattern_trait(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// Non-generic trait for metric patterns (usable in collections).
|
||||
pub trait AnyMetricPattern {{
|
||||
/// Get the metric name.
|
||||
fn name(&self) -> &str;
|
||||
|
||||
/// Get the list of available indexes for this metric.
|
||||
fn indexes(&self) -> &'static [Index];
|
||||
}}
|
||||
|
||||
/// Generic trait for metric patterns with endpoint access.
|
||||
pub trait MetricPattern<T>: AnyMetricPattern {{
|
||||
/// Get an endpoint for a specific index, if supported.
|
||||
fn get(&self, index: Index) -> Option<Endpoint<T>>;
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate the Endpoint struct.
|
||||
pub fn generate_endpoint(output: &mut String) {
|
||||
writeln!(
|
||||
output,
|
||||
r#"/// An endpoint for a specific metric + index combination.
|
||||
pub struct Endpoint<T> {{
|
||||
client: Arc<BrkClientBase>,
|
||||
name: Arc<str>,
|
||||
index: Index,
|
||||
_marker: std::marker::PhantomData<T>,
|
||||
}}
|
||||
|
||||
impl<T: DeserializeOwned> Endpoint<T> {{
|
||||
pub fn new(client: Arc<BrkClientBase>, name: Arc<str>, index: Index) -> Self {{
|
||||
Self {{
|
||||
client,
|
||||
name,
|
||||
index,
|
||||
_marker: std::marker::PhantomData,
|
||||
}}
|
||||
}}
|
||||
|
||||
/// Fetch all data points for this metric/index.
|
||||
pub fn get(&self) -> Result<Vec<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>> {{
|
||||
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)); }}
|
||||
let p = self.path();
|
||||
let path = if params.is_empty() {{
|
||||
p
|
||||
}} else {{
|
||||
format!("{{}}?{{}}", p, params.join("&"))
|
||||
}};
|
||||
self.client.get(&path)
|
||||
}}
|
||||
|
||||
/// Get the endpoint path.
|
||||
pub fn path(&self) -> String {{
|
||||
format!("/api/metric/{{}}/{{}}", self.name, self.index.serialize_long())
|
||||
}}
|
||||
}}
|
||||
|
||||
"#
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Generate index accessor structs.
|
||||
pub fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Index accessor structs\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let by_name = format!("{}By", pattern.name);
|
||||
|
||||
// Generate the "By" struct with lazy endpoint methods
|
||||
writeln!(output, "/// Container for index endpoint methods.").unwrap();
|
||||
writeln!(output, "pub struct {}<T> {{", by_name).unwrap();
|
||||
writeln!(output, " client: Arc<BrkClientBase>,").unwrap();
|
||||
writeln!(output, " name: Arc<str>,").unwrap();
|
||||
writeln!(output, " _marker: std::marker::PhantomData<T>,").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl with methods for each index
|
||||
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", by_name).unwrap();
|
||||
for index in &pattern.indexes {
|
||||
let method_name = index_to_field_name(index);
|
||||
writeln!(output, " pub fn {}(&self) -> Endpoint<T> {{", method_name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" Endpoint::new(self.client.clone(), self.name.clone(), Index::{})",
|
||||
index
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
}
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate the main accessor struct
|
||||
writeln!(
|
||||
output,
|
||||
"/// Index accessor for metrics with {} indexes.",
|
||||
pattern.indexes.len()
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "pub struct {}<T> {{", pattern.name).unwrap();
|
||||
writeln!(output, " client: Arc<BrkClientBase>,").unwrap();
|
||||
writeln!(output, " name: Arc<str>,").unwrap();
|
||||
writeln!(output, " pub by: {}<T>,", by_name).unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block with constructor
|
||||
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", pattern.name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, name: String) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " let name: Arc<str> = name.into();").unwrap();
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
writeln!(output, " client: client.clone(),").unwrap();
|
||||
writeln!(output, " name: name.clone(),").unwrap();
|
||||
writeln!(output, " by: {} {{", by_name).unwrap();
|
||||
writeln!(output, " client,").unwrap();
|
||||
writeln!(output, " name,").unwrap();
|
||||
writeln!(output, " _marker: std::marker::PhantomData,").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " /// Get the metric name.").unwrap();
|
||||
writeln!(output, " pub fn name(&self) -> &str {{").unwrap();
|
||||
writeln!(output, " &self.name").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Implement AnyMetricPattern trait
|
||||
writeln!(output, "impl<T> AnyMetricPattern for {}<T> {{", pattern.name).unwrap();
|
||||
writeln!(output, " fn name(&self) -> &str {{").unwrap();
|
||||
writeln!(output, " &self.name").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output).unwrap();
|
||||
writeln!(output, " fn indexes(&self) -> &'static [Index] {{").unwrap();
|
||||
writeln!(output, " &[").unwrap();
|
||||
for index in &pattern.indexes {
|
||||
writeln!(output, " Index::{},", index).unwrap();
|
||||
}
|
||||
writeln!(output, " ]").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Implement MetricPattern<T> trait
|
||||
writeln!(output, "impl<T: DeserializeOwned> MetricPattern<T> for {}<T> {{", pattern.name).unwrap();
|
||||
writeln!(output, " fn get(&self, index: Index) -> Option<Endpoint<T>> {{").unwrap();
|
||||
writeln!(output, " match index {{").unwrap();
|
||||
for index in &pattern.indexes {
|
||||
let method_name = index_to_field_name(index);
|
||||
writeln!(output, " Index::{} => Some(self.by.{}()),", index, method_name).unwrap();
|
||||
}
|
||||
writeln!(output, " _ => None,").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate structural pattern structs.
|
||||
pub fn generate_pattern_structs(
|
||||
output: &mut String,
|
||||
patterns: &[StructuralPattern],
|
||||
metadata: &ClientMetadata,
|
||||
) {
|
||||
if patterns.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
writeln!(output, "// Reusable pattern structs\n").unwrap();
|
||||
|
||||
for pattern in patterns {
|
||||
let is_parameterizable = pattern.is_parameterizable();
|
||||
let generic_params = if pattern.is_generic { "<T>" } else { "" };
|
||||
|
||||
writeln!(output, "/// Pattern struct for repeated tree structure.").unwrap();
|
||||
writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap();
|
||||
|
||||
for field in &pattern.fields {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
let type_annotation =
|
||||
field_type_with_generic(field, metadata, pattern.is_generic, None);
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
// Generate impl block with constructor
|
||||
let impl_generic = if pattern.is_generic {
|
||||
"<T: DeserializeOwned>"
|
||||
} else {
|
||||
""
|
||||
};
|
||||
writeln!(
|
||||
output,
|
||||
"impl{} {}{} {{",
|
||||
impl_generic, pattern.name, generic_params
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
if is_parameterizable {
|
||||
writeln!(
|
||||
output,
|
||||
" /// Create a new pattern node with accumulated metric name."
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, acc: String) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
let syntax = RustSyntax;
|
||||
for field in &pattern.fields {
|
||||
if is_parameterizable {
|
||||
generate_parameterized_field(output, &syntax, field, pattern, metadata, " ");
|
||||
} else {
|
||||
generate_tree_path_field(output, &syntax, field, metadata, " ");
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// Get Rust type annotation for a field with optional generic value type.
|
||||
pub fn field_type_with_generic(
|
||||
field: &PatternField,
|
||||
metadata: &ClientMetadata,
|
||||
is_generic: bool,
|
||||
generic_value_type: Option<&str>,
|
||||
) -> String {
|
||||
metadata.field_type_annotation(field, is_generic, generic_value_type, GenericSyntax::RUST)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
//! Rust client generation.
|
||||
//!
|
||||
//! This module generates a Rust client with full type safety for the BRK API.
|
||||
|
||||
mod api;
|
||||
mod client;
|
||||
mod tree;
|
||||
mod types;
|
||||
|
||||
use std::{fmt::Write, fs, io, path::Path};
|
||||
|
||||
use crate::{ClientMetadata, Endpoint};
|
||||
|
||||
/// Generate Rust client from metadata and OpenAPI endpoints.
|
||||
///
|
||||
/// `output_path` is the full path to the output file (e.g., "crates/brk_client/src/lib.rs").
|
||||
pub fn generate_rust_client(
|
||||
metadata: &ClientMetadata,
|
||||
endpoints: &[Endpoint],
|
||||
output_path: &Path,
|
||||
) -> io::Result<()> {
|
||||
let mut output = String::new();
|
||||
|
||||
writeln!(output, "// Auto-generated BRK Rust client").unwrap();
|
||||
writeln!(output, "// Do not edit manually\n").unwrap();
|
||||
writeln!(output, "#![allow(non_camel_case_types)]").unwrap();
|
||||
writeln!(output, "#![allow(dead_code)]").unwrap();
|
||||
writeln!(output, "#![allow(unused_variables)]").unwrap();
|
||||
writeln!(output, "#![allow(clippy::useless_format)]").unwrap();
|
||||
writeln!(output, "#![allow(clippy::unnecessary_to_owned)]\n").unwrap();
|
||||
|
||||
client::generate_imports(&mut output);
|
||||
client::generate_base_client(&mut output);
|
||||
client::generate_metric_pattern_trait(&mut output);
|
||||
client::generate_endpoint(&mut output);
|
||||
client::generate_index_accessors(&mut output, &metadata.index_set_patterns);
|
||||
client::generate_pattern_structs(&mut output, &metadata.structural_patterns, metadata);
|
||||
tree::generate_tree(&mut output, &metadata.catalog, metadata);
|
||||
api::generate_main_client(&mut output, endpoints);
|
||||
|
||||
fs::write(output_path, output)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
//! Rust tree structure generation.
|
||||
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::Write;
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{
|
||||
ClientMetadata, PatternField, RustSyntax, child_type_name, generate_tree_node_field,
|
||||
get_fields_with_child_info, get_node_fields, get_pattern_instance_base, to_snake_case,
|
||||
};
|
||||
|
||||
use super::client::field_type_with_generic;
|
||||
|
||||
/// Generate tree structs.
|
||||
pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
|
||||
writeln!(output, "// Catalog tree\n").unwrap();
|
||||
|
||||
let pattern_lookup = metadata.pattern_lookup();
|
||||
let mut generated = HashSet::new();
|
||||
generate_tree_node(
|
||||
output,
|
||||
"CatalogTree",
|
||||
catalog,
|
||||
&pattern_lookup,
|
||||
metadata,
|
||||
&mut generated,
|
||||
);
|
||||
}
|
||||
|
||||
fn generate_tree_node(
|
||||
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 let Some(pattern_name) = pattern_lookup.get(&fields)
|
||||
&& pattern_name != name
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if generated.contains(name) {
|
||||
return;
|
||||
}
|
||||
generated.insert(name.to_string());
|
||||
|
||||
writeln!(output, "/// Catalog tree node.").unwrap();
|
||||
writeln!(output, "pub struct {} {{", name).unwrap();
|
||||
|
||||
for (field, child_fields) in &fields_with_child_info {
|
||||
let field_name = to_snake_case(&field.name);
|
||||
// Look up type parameter for generic patterns
|
||||
let generic_value_type = child_fields
|
||||
.as_ref()
|
||||
.and_then(|cf| metadata.get_type_param(cf))
|
||||
.map(String::as_str);
|
||||
let type_annotation = field_type_with_generic(field, metadata, false, generic_value_type);
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
writeln!(output, "impl {} {{", name).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" pub fn new(client: Arc<BrkClientBase>, base_path: String) -> Self {{"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, " Self {{").unwrap();
|
||||
|
||||
let syntax = RustSyntax;
|
||||
for (field, (child_name, child_node)) in fields.iter().zip(children.iter()) {
|
||||
// Detect pattern base for parameterizable patterns
|
||||
let pattern_base = if metadata.is_pattern_type(&field.rust_type) {
|
||||
let pattern = metadata.find_pattern(&field.rust_type);
|
||||
if pattern.is_some_and(|p| p.is_parameterizable()) {
|
||||
Some(get_pattern_instance_base(child_node))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
generate_tree_node_field(output, &syntax, field, metadata, " ", child_name, pattern_base.as_deref());
|
||||
}
|
||||
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").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_struct = child_type_name(name, child_name);
|
||||
generate_tree_node(
|
||||
output,
|
||||
&child_struct,
|
||||
child_node,
|
||||
pattern_lookup,
|
||||
metadata,
|
||||
generated,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
//! Rust type conversion utilities.
|
||||
|
||||
/// Convert JS-style type to Rust type.
|
||||
pub fn js_type_to_rust(js_type: &str) -> String {
|
||||
if let Some(inner) = js_type.strip_suffix("[]") {
|
||||
format!("Vec<{}>", js_type_to_rust(inner))
|
||||
} else {
|
||||
match js_type {
|
||||
"string" => "String".to_string(),
|
||||
"number" => "f64".to_string(),
|
||||
"boolean" => "bool".to_string(),
|
||||
"*" => "serde_json::Value".to_string(),
|
||||
other => other.to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -47,16 +47,20 @@ impl ClientOutputPaths {
|
||||
}
|
||||
}
|
||||
|
||||
mod javascript;
|
||||
mod analysis;
|
||||
mod backends;
|
||||
mod generate;
|
||||
mod generators;
|
||||
mod openapi;
|
||||
mod python;
|
||||
mod rust;
|
||||
mod syntax;
|
||||
mod types;
|
||||
|
||||
pub use javascript::*;
|
||||
pub use analysis::*;
|
||||
pub use backends::*;
|
||||
pub use generate::*;
|
||||
pub use generators::{generate_javascript_client, generate_python_client, generate_rust_client};
|
||||
pub use openapi::*;
|
||||
pub use python::*;
|
||||
pub use rust::*;
|
||||
pub use syntax::*;
|
||||
pub use types::*;
|
||||
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
@@ -170,21 +174,13 @@ fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) {
|
||||
/// Collect type definitions from schemars-generated schema's definitions section.
|
||||
/// Schemars uses `definitions` or `$defs` to store referenced types.
|
||||
fn collect_schema_definitions(schema: &Value, schemas: &mut TypeSchemas) {
|
||||
// Check for definitions (JSON Schema draft-07 style)
|
||||
if let Some(defs) = schema.get("definitions").and_then(|d| d.as_object()) {
|
||||
for (name, def_schema) in defs {
|
||||
// Use the definition name as-is (schemars names match $ref paths)
|
||||
if !schemas.contains_key(name) {
|
||||
schemas.insert(name.clone(), def_schema.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for $defs (JSON Schema draft 2019-09+ style)
|
||||
if let Some(defs) = schema.get("$defs").and_then(|d| d.as_object()) {
|
||||
for (name, def_schema) in defs {
|
||||
if !schemas.contains_key(name) {
|
||||
schemas.insert(name.clone(), def_schema.clone());
|
||||
// Check both JSON Schema draft-07 style ("definitions") and draft 2019-09+ style ("$defs")
|
||||
for key in ["definitions", "$defs"] {
|
||||
if let Some(defs) = schema.get(key).and_then(|d| d.as_object()) {
|
||||
for (name, def_schema) in defs {
|
||||
if !schemas.contains_key(name) {
|
||||
schemas.insert(name.clone(), def_schema.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
use std::{collections::BTreeMap, io};
|
||||
|
||||
use crate::ref_to_type_name;
|
||||
use oas3::Spec;
|
||||
use oas3::spec::{ObjectOrReference, Operation, ParameterIn, PathItem, Schema, SchemaTypeSet};
|
||||
use serde_json::Value;
|
||||
@@ -215,7 +216,7 @@ fn extract_parameters(operation: &Operation, location: ParameterIn) -> Vec<Param
|
||||
.as_ref()
|
||||
.and_then(|s| match s {
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
ref_path.rsplit('/').next().map(|s| s.to_string())
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
ObjectOrReference::Object(obj_schema) => schema_to_type_name(obj_schema),
|
||||
})
|
||||
@@ -246,7 +247,7 @@ fn extract_response_type(operation: &Operation) -> Option<String> {
|
||||
match &content.schema {
|
||||
Some(ObjectOrReference::Ref { ref_path, .. }) => {
|
||||
// Extract type name from reference like "#/components/schemas/Block"
|
||||
Some(ref_path.rsplit('/').next()?.to_string())
|
||||
Some(ref_to_type_name(ref_path)?.to_string())
|
||||
}
|
||||
Some(ObjectOrReference::Object(schema)) => schema_to_type_name(schema),
|
||||
None => None,
|
||||
@@ -264,7 +265,7 @@ fn schema_type_from_schema(schema: &Schema) -> Option<String> {
|
||||
ObjectOrReference::Ref { ref_path, .. } => {
|
||||
// Return the type name as-is (e.g., "Height", "Address")
|
||||
// These should have definitions generated from schemas
|
||||
ref_path.rsplit('/').next().map(|s| s.to_string())
|
||||
ref_to_type_name(ref_path).map(|s| s.to_string())
|
||||
}
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
//! Language-specific syntax traits for code generation.
|
||||
//!
|
||||
//! This module defines the `LanguageSyntax` trait that abstracts over
|
||||
//! language-specific code generation patterns, allowing shared generation
|
||||
//! logic to work across Python, JavaScript, and Rust backends.
|
||||
|
||||
use crate::{FieldNamePosition, GenericSyntax};
|
||||
|
||||
/// Language-specific syntax for code generation.
|
||||
///
|
||||
/// Implementations of this trait provide the language-specific formatting
|
||||
/// for generated client code. This allows the core generation logic to be
|
||||
/// written once and reused across all supported languages.
|
||||
pub trait LanguageSyntax {
|
||||
/// Convert a field name to the language's naming convention.
|
||||
///
|
||||
/// - Python/Rust: `snake_case`
|
||||
/// - JavaScript: `camelCase`
|
||||
fn field_name(&self, name: &str) -> String;
|
||||
|
||||
/// Format an interpolated path expression.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `base_var` - The variable name to interpolate (e.g., "acc", "base_path")
|
||||
/// * `suffix` - The suffix to append (e.g., "_field_name")
|
||||
///
|
||||
/// # Returns
|
||||
/// - Python: `f'{acc}_suffix'`
|
||||
/// - JavaScript: `` `${acc}_suffix` ``
|
||||
/// - Rust: `format!("{acc}_suffix")`
|
||||
fn path_expr(&self, base_var: &str, suffix: &str) -> String;
|
||||
|
||||
/// Format a `FieldNamePosition` as a path expression.
|
||||
///
|
||||
/// This handles the different name transformation patterns (append, prepend,
|
||||
/// identity, set_base) in a language-specific way.
|
||||
fn position_expr(&self, pos: &FieldNamePosition, base_var: &str) -> String;
|
||||
|
||||
/// Generate a constructor call for patterns and accessors.
|
||||
///
|
||||
/// - Python: `TypeName(client, path)`
|
||||
/// - JavaScript: `createTypeName(client, path)`
|
||||
/// - Rust: `TypeName::new(client.clone(), path)`
|
||||
fn constructor(&self, type_name: &str, path_expr: &str) -> String;
|
||||
|
||||
/// Generate a field initialization line.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `indent` - The indentation string
|
||||
/// * `name` - The field name (already converted to language convention)
|
||||
/// * `type_ann` - The type annotation (may be ignored by some languages)
|
||||
/// * `value` - The initialization value/expression
|
||||
///
|
||||
/// # Returns
|
||||
/// - Python: `{indent}self.{name}: {type_ann} = {value}`
|
||||
/// - JavaScript: `{indent}{name}: {value},`
|
||||
/// - Rust: `{indent}{name}: {value},`
|
||||
fn field_init(&self, indent: &str, name: &str, type_ann: &str, value: &str) -> String;
|
||||
|
||||
/// Get the generic type syntax for this language.
|
||||
///
|
||||
/// - Python: `[T]` with default `Any`
|
||||
/// - JavaScript: `<T>` with default `unknown`
|
||||
/// - Rust: `<T>` with default `_`
|
||||
fn generic_syntax(&self) -> GenericSyntax;
|
||||
|
||||
/// Generate a struct/class header.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `name` - The type name
|
||||
/// * `generic_params` - Generic parameters (e.g., "<T>" or "[T]"), empty if none
|
||||
/// * `doc` - Optional documentation string
|
||||
fn struct_header(&self, name: &str, generic_params: &str, doc: Option<&str>) -> String;
|
||||
|
||||
/// Generate a struct/class footer.
|
||||
fn struct_footer(&self) -> String;
|
||||
|
||||
/// Generate a constructor/init method header.
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `params` - Constructor parameters (language-specific format)
|
||||
fn constructor_header(&self, params: &str) -> String;
|
||||
|
||||
/// Generate a constructor/init method footer.
|
||||
fn constructor_footer(&self) -> String;
|
||||
|
||||
/// Generate a field declaration (for struct body, not init).
|
||||
///
|
||||
/// # Arguments
|
||||
/// * `indent` - The indentation string
|
||||
/// * `name` - The field name
|
||||
/// * `type_ann` - The type annotation
|
||||
fn field_declaration(&self, indent: &str, name: &str, type_ann: &str) -> String;
|
||||
|
||||
/// Format an index field name from an Index.
|
||||
///
|
||||
/// E.g., `by_date_height`, `by_date`, etc.
|
||||
fn index_field_name(&self, index_name: &str) -> String;
|
||||
|
||||
/// Format a string literal.
|
||||
///
|
||||
/// - Python/JavaScript: `'value'` (single quotes)
|
||||
/// - Rust: `"value"` (double quotes)
|
||||
fn string_literal(&self, value: &str) -> String;
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
use brk_types::Index;
|
||||
|
||||
/// Convert a string to PascalCase (e.g., "fee_rate" -> "FeeRate").
|
||||
pub fn to_pascal_case(s: &str) -> String {
|
||||
s.replace('-', "_")
|
||||
@@ -51,3 +53,38 @@ pub fn to_camel_case(s: &str) -> String {
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_dateindex).
|
||||
pub fn index_to_field_name(index: &Index) -> String {
|
||||
format!("by_{}", to_snake_case(index.serialize_long()))
|
||||
}
|
||||
|
||||
/// Generate a child type/struct/class name (e.g., ParentName + child_name -> ParentName_ChildName).
|
||||
pub fn child_type_name(parent: &str, child: &str) -> String {
|
||||
format!("{}_{}", parent, to_pascal_case(child))
|
||||
}
|
||||
|
||||
/// Escape Python reserved keywords by appending an underscore.
|
||||
/// Also prefixes names starting with digits with an underscore.
|
||||
pub fn escape_python_keyword(name: &str) -> String {
|
||||
const PYTHON_KEYWORDS: &[&str] = &[
|
||||
"False", "None", "True", "and", "as", "assert", "async", "await", "break", "class",
|
||||
"continue", "def", "del", "elif", "else", "except", "finally", "for", "from", "global",
|
||||
"if", "import", "in", "is", "lambda", "nonlocal", "not", "or", "pass", "raise", "return",
|
||||
"try", "while", "with", "yield",
|
||||
];
|
||||
|
||||
// Prefix with underscore if starts with digit
|
||||
let name = if name.starts_with(|c: char| c.is_ascii_digit()) {
|
||||
format!("_{}", name)
|
||||
} else {
|
||||
name.to_string()
|
||||
};
|
||||
|
||||
// Append underscore if it's a keyword
|
||||
if PYTHON_KEYWORDS.contains(&name.as_str()) {
|
||||
format!("{}_", name)
|
||||
} else {
|
||||
name
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
//! Client metadata extracted from brk_query.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use brk_query::Vecs;
|
||||
use brk_types::Index;
|
||||
|
||||
use super::{GenericSyntax, IndexSetPattern, PatternField, StructuralPattern, extract_inner_type};
|
||||
use crate::analysis;
|
||||
|
||||
/// Metadata extracted from brk_query for client generation.
|
||||
#[derive(Debug)]
|
||||
pub struct ClientMetadata {
|
||||
/// The catalog tree structure (with schemas in leaves)
|
||||
pub catalog: brk_types::TreeNode,
|
||||
/// Structural patterns - tree node shapes that repeat
|
||||
pub structural_patterns: Vec<StructuralPattern>,
|
||||
/// All indexes used across the catalog
|
||||
pub used_indexes: BTreeSet<Index>,
|
||||
/// Index set patterns - sets of indexes that appear together on metrics
|
||||
pub index_set_patterns: Vec<IndexSetPattern>,
|
||||
/// Maps concrete field signatures to pattern names
|
||||
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>,
|
||||
}
|
||||
|
||||
impl ClientMetadata {
|
||||
/// Extract metadata from brk_query::Vecs.
|
||||
pub fn from_vecs(vecs: &Vecs) -> Self {
|
||||
let catalog = vecs.catalog().clone();
|
||||
let (structural_patterns, concrete_to_pattern, concrete_to_type_param) =
|
||||
analysis::detect_structural_patterns(&catalog);
|
||||
let (used_indexes, index_set_patterns) = analysis::detect_index_patterns(&catalog);
|
||||
|
||||
ClientMetadata {
|
||||
catalog,
|
||||
structural_patterns,
|
||||
used_indexes,
|
||||
index_set_patterns,
|
||||
concrete_to_pattern,
|
||||
concrete_to_type_param,
|
||||
}
|
||||
}
|
||||
|
||||
/// Find an index set pattern that matches the given indexes.
|
||||
pub fn find_index_set_pattern(&self, indexes: &BTreeSet<Index>) -> Option<&IndexSetPattern> {
|
||||
self.index_set_patterns
|
||||
.iter()
|
||||
.find(|p| &p.indexes == indexes)
|
||||
}
|
||||
|
||||
/// Check if a type is a structural pattern name.
|
||||
pub fn is_pattern_type(&self, type_name: &str) -> bool {
|
||||
self.structural_patterns.iter().any(|p| p.name == type_name)
|
||||
}
|
||||
|
||||
/// Find a pattern by name.
|
||||
pub fn find_pattern(&self, name: &str) -> Option<&StructuralPattern> {
|
||||
self.structural_patterns.iter().find(|p| p.name == name)
|
||||
}
|
||||
|
||||
/// Check if a pattern is generic.
|
||||
pub fn is_pattern_generic(&self, name: &str) -> bool {
|
||||
self.find_pattern(name).is_some_and(|p| p.is_generic)
|
||||
}
|
||||
|
||||
/// Get the type parameter for a generic pattern given its concrete fields.
|
||||
pub fn get_type_param(&self, fields: &[PatternField]) -> Option<&String> {
|
||||
self.concrete_to_type_param.get(fields)
|
||||
}
|
||||
|
||||
/// Build a lookup map from field signatures to pattern names.
|
||||
pub fn pattern_lookup(&self) -> HashMap<Vec<PatternField>, String> {
|
||||
let mut lookup = self.concrete_to_pattern.clone();
|
||||
for p in &self.structural_patterns {
|
||||
lookup.insert(p.fields.clone(), p.name.clone());
|
||||
}
|
||||
lookup
|
||||
}
|
||||
|
||||
/// Check if a field should use a shared index accessor.
|
||||
pub fn field_uses_accessor(&self, field: &PatternField) -> bool {
|
||||
self.find_index_set_pattern(&field.indexes).is_some()
|
||||
}
|
||||
|
||||
/// Generate type annotation for a field with language-specific syntax.
|
||||
pub fn field_type_annotation(
|
||||
&self,
|
||||
field: &PatternField,
|
||||
is_generic: bool,
|
||||
generic_value_type: Option<&str>,
|
||||
syntax: GenericSyntax,
|
||||
) -> String {
|
||||
let value_type = if is_generic && field.rust_type == "T" {
|
||||
"T".to_string()
|
||||
} else {
|
||||
extract_inner_type(&field.rust_type)
|
||||
};
|
||||
|
||||
if self.is_pattern_type(&field.rust_type) {
|
||||
if self.is_pattern_generic(&field.rust_type) {
|
||||
let type_param = field
|
||||
.type_param
|
||||
.as_deref()
|
||||
.or(generic_value_type)
|
||||
.unwrap_or(if is_generic { "T" } else { syntax.default_type });
|
||||
return syntax.wrap(&field.rust_type, type_param);
|
||||
}
|
||||
field.rust_type.clone()
|
||||
} else if field.is_branch() {
|
||||
field.rust_type.clone()
|
||||
} else if let Some(accessor) = self.find_index_set_pattern(&field.indexes) {
|
||||
syntax.wrap(&accessor.name, &value_type)
|
||||
} else {
|
||||
syntax.wrap("MetricNode", &value_type)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
//! Core types for client generation.
|
||||
|
||||
mod case;
|
||||
mod metadata;
|
||||
mod positions;
|
||||
mod schema;
|
||||
mod structs;
|
||||
|
||||
pub use case::*;
|
||||
pub use metadata::*;
|
||||
pub use positions::*;
|
||||
pub use schema::*;
|
||||
pub use structs::*;
|
||||
|
||||
/// Language-specific syntax for generic type annotations.
|
||||
#[derive(Clone, Copy)]
|
||||
pub struct GenericSyntax {
|
||||
pub open: char,
|
||||
pub close: char,
|
||||
pub default_type: &'static str,
|
||||
}
|
||||
|
||||
impl GenericSyntax {
|
||||
pub const PYTHON: Self = Self { open: '[', close: ']', default_type: "Any" };
|
||||
pub const JAVASCRIPT: Self = Self { open: '<', close: '>', default_type: "unknown" };
|
||||
pub const RUST: Self = Self { open: '<', close: '>', default_type: "_" };
|
||||
|
||||
pub fn wrap(&self, name: &str, type_param: &str) -> String {
|
||||
format!("{}{}{}{}", name, self.open, type_param, self.close)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
//! Field name position types for metric name reconstruction.
|
||||
|
||||
/// 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),
|
||||
}
|
||||
@@ -11,13 +11,6 @@ pub fn unwrap_allof(schema: &Value) -> &Value {
|
||||
schema
|
||||
}
|
||||
|
||||
/// Check if a schema represents an enum type.
|
||||
/// Enums have either an "enum" array or "oneOf" without properties.
|
||||
pub fn is_enum_schema(schema: &Value) -> bool {
|
||||
schema.get("enum").is_some()
|
||||
|| (schema.get("oneOf").is_some() && schema.get("properties").is_none())
|
||||
}
|
||||
|
||||
/// Extract inner type from a wrapper generic like `Close<Dollars>` -> `Dollars`.
|
||||
/// Also handles malformed types like `Dollars>` (from vecdb's short_type_name).
|
||||
pub fn extract_inner_type(type_str: &str) -> String {
|
||||
@@ -43,3 +36,9 @@ pub fn schema_to_json_type(schema: &Value) -> String {
|
||||
.unwrap_or("object")
|
||||
.to_string()
|
||||
}
|
||||
|
||||
/// Extract type name from a JSON Schema $ref path.
|
||||
/// E.g., "#/definitions/MyType" -> "MyType", "#/$defs/Foo" -> "Foo"
|
||||
pub fn ref_to_type_name(ref_path: &str) -> Option<&str> {
|
||||
ref_path.rsplit('/').next()
|
||||
}
|
||||
@@ -0,0 +1,95 @@
|
||||
//! Structural pattern and field types.
|
||||
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use brk_types::Index;
|
||||
|
||||
use super::FieldNamePosition;
|
||||
|
||||
/// A pattern of indexes that appear together on multiple metrics.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct IndexSetPattern {
|
||||
/// Pattern name (e.g., "DateHeightIndexes")
|
||||
pub name: String,
|
||||
/// The set of indexes
|
||||
pub indexes: BTreeSet<Index>,
|
||||
}
|
||||
|
||||
/// A structural pattern - a branch structure that appears multiple times.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct StructuralPattern {
|
||||
/// Pattern name
|
||||
pub name: String,
|
||||
/// Ordered list of child fields
|
||||
pub fields: Vec<PatternField>,
|
||||
/// How each field modifies the accumulated name
|
||||
pub field_positions: HashMap<String, FieldNamePosition>,
|
||||
/// If true, all leaf fields use a type parameter T
|
||||
pub is_generic: bool,
|
||||
}
|
||||
|
||||
impl StructuralPattern {
|
||||
/// Returns true if this pattern contains any leaf fields.
|
||||
pub fn contains_leaves(&self) -> bool {
|
||||
self.fields.iter().any(|f| f.is_leaf())
|
||||
}
|
||||
|
||||
/// Returns true if all leaf fields have consistent name transformations.
|
||||
pub fn is_parameterizable(&self) -> bool {
|
||||
!self.field_positions.is_empty()
|
||||
&& self
|
||||
.fields
|
||||
.iter()
|
||||
.all(|f| f.is_branch() || 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.
|
||||
#[derive(Debug, Clone, PartialOrd, Ord)]
|
||||
pub struct PatternField {
|
||||
/// Field name
|
||||
pub name: String,
|
||||
/// Rust type for leaves or pattern name for branches
|
||||
pub rust_type: String,
|
||||
/// JSON type from schema
|
||||
pub json_type: String,
|
||||
/// For leaves: the set of supported indexes. Empty for branches.
|
||||
pub indexes: BTreeSet<Index>,
|
||||
/// For branches referencing generic patterns: the concrete type parameter
|
||||
pub type_param: Option<String>,
|
||||
}
|
||||
|
||||
impl PatternField {
|
||||
/// Returns true if this is a leaf field (has indexes).
|
||||
pub fn is_leaf(&self) -> bool {
|
||||
!self.indexes.is_empty()
|
||||
}
|
||||
|
||||
/// Returns true if this is a branch field (no indexes).
|
||||
pub fn is_branch(&self) -> bool {
|
||||
self.indexes.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::hash::Hash for PatternField {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.name.hash(state);
|
||||
self.rust_type.hash(state);
|
||||
self.json_type.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for PatternField {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.name == other.name
|
||||
&& self.rust_type == other.rust_type
|
||||
&& self.json_type == other.json_type
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for PatternField {}
|
||||
@@ -12,6 +12,9 @@ build = "build.rs"
|
||||
log = { workspace = true }
|
||||
notify = "8.2.0"
|
||||
# rolldown = { path = "../../../rolldown/crates/rolldown", package = "brk_rolldown" }
|
||||
rolldown = { version = "0.6.0", package = "brk_rolldown" }
|
||||
rolldown = { version = "0.7.0", package = "brk_rolldown" }
|
||||
sugar_path = "1.2.1"
|
||||
tokio = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = { workspace = true }
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
use std::{io, path::PathBuf, thread, time::Duration};
|
||||
|
||||
use brk_bundler::bundle;
|
||||
|
||||
fn find_dev_dirs() -> Option<(PathBuf, PathBuf)> {
|
||||
let mut dir = std::env::current_dir().ok()?;
|
||||
loop {
|
||||
let websites = dir.join("websites");
|
||||
let modules = dir.join("modules");
|
||||
if websites.exists() && modules.exists() {
|
||||
return Some((websites, modules));
|
||||
}
|
||||
// Stop at workspace root (crates/ indicates we're there)
|
||||
if dir.join("crates").exists() {
|
||||
return None;
|
||||
}
|
||||
dir = dir.parent()?.to_path_buf();
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> io::Result<()> {
|
||||
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("debug")).init();
|
||||
|
||||
let (websites_path, modules_path) =
|
||||
find_dev_dirs().expect("Run from within the brk workspace");
|
||||
let source_folder = "bitview";
|
||||
|
||||
let dist_path = bundle(&modules_path, &websites_path, source_folder, true).await?;
|
||||
|
||||
println!("Bundle created at: {}", dist_path.display());
|
||||
println!("Watching for changes... (Ctrl+C to stop)");
|
||||
|
||||
loop {
|
||||
thread::sleep(Duration::from_secs(60));
|
||||
}
|
||||
}
|
||||
@@ -3,10 +3,11 @@
|
||||
use std::{
|
||||
fs, io,
|
||||
path::{Path, PathBuf},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use log::error;
|
||||
use notify::{EventKind, RecursiveMode, Watcher};
|
||||
use log::{debug, error, info};
|
||||
use notify::{EventKind, PollWatcher, RecursiveMode, Watcher};
|
||||
use rolldown::{
|
||||
Bundler, BundlerConfig, BundlerOptions, InlineConstConfig, InlineConstMode, InlineConstOption,
|
||||
OptimizationOption, RawMinifyOptions, SourceMapType,
|
||||
@@ -45,6 +46,11 @@ pub async fn bundle(
|
||||
let absolute_dist_index_path = absolute_dist_path.join("index.html");
|
||||
let absolute_dist_sw_path = absolute_dist_path.join("service-worker.js");
|
||||
|
||||
info!("Bundling {source_folder}...");
|
||||
info!(" modules: {absolute_modules_path:?}");
|
||||
info!(" source: {absolute_source_path:?}");
|
||||
info!(" dist: {absolute_dist_path:?}");
|
||||
|
||||
let _ = fs::remove_dir_all(&absolute_dist_path);
|
||||
let _ = fs::remove_dir_all(&absolute_source_scripts_modules_path);
|
||||
copy_dir_all(
|
||||
@@ -122,53 +128,99 @@ pub async fn bundle(
|
||||
return Ok(relative_dist_path);
|
||||
}
|
||||
|
||||
tokio::spawn(async move {
|
||||
let mut event_watcher = notify::recommended_watcher(
|
||||
move |res: Result<notify::Event, notify::Error>| match res {
|
||||
Ok(event) => match event.kind {
|
||||
EventKind::Create(_) => event.paths,
|
||||
EventKind::Modify(_) => event.paths,
|
||||
_ => vec![],
|
||||
}
|
||||
.into_iter()
|
||||
.for_each(|path| {
|
||||
let path = path.absolutize();
|
||||
// Clone paths for the second watcher
|
||||
let absolute_websites_path_clone2 = absolute_websites_path_clone.clone();
|
||||
let absolute_modules_path_clone2 = absolute_modules_path_clone.clone();
|
||||
|
||||
if path == absolute_dist_scripts_entry_path
|
||||
|| path == absolute_source_index_path_clone
|
||||
{
|
||||
update_dist_index();
|
||||
} else if path == absolute_source_sw_path_clone {
|
||||
update_source_sw();
|
||||
} else if let Ok(suffix) = path.strip_prefix(&absolute_modules_path) {
|
||||
let source_modules_path = absolute_source_scripts_modules_path.join(suffix);
|
||||
if path.is_file() {
|
||||
let _ = fs::create_dir_all(path.parent().unwrap());
|
||||
let _ = fs::copy(&path, &source_modules_path);
|
||||
}
|
||||
} else if let Ok(suffix) = path.strip_prefix(&absolute_source_path)
|
||||
// scripts are handled by rolldown
|
||||
&& !path.starts_with(&absolute_source_scripts_path)
|
||||
{
|
||||
let dist_path = absolute_dist_path.join(suffix);
|
||||
if path.is_file() {
|
||||
let _ = fs::create_dir_all(path.parent().unwrap());
|
||||
let _ = fs::copy(&path, &dist_path);
|
||||
tokio::spawn(async move {
|
||||
let handle_event = {
|
||||
let absolute_dist_scripts_entry_path = absolute_dist_scripts_entry_path.clone();
|
||||
let absolute_source_index_path_clone = absolute_source_index_path_clone.clone();
|
||||
let absolute_source_sw_path_clone = absolute_source_sw_path_clone.clone();
|
||||
let absolute_modules_path = absolute_modules_path.clone();
|
||||
let absolute_source_scripts_modules_path = absolute_source_scripts_modules_path.clone();
|
||||
let absolute_source_path = absolute_source_path.clone();
|
||||
let absolute_source_scripts_path = absolute_source_scripts_path.clone();
|
||||
let absolute_dist_path = absolute_dist_path.clone();
|
||||
let update_dist_index = update_dist_index.clone();
|
||||
let update_source_sw = update_source_sw.clone();
|
||||
|
||||
move |path: PathBuf| {
|
||||
let path = path.absolutize();
|
||||
|
||||
if path == absolute_dist_scripts_entry_path
|
||||
|| path == absolute_source_index_path_clone
|
||||
{
|
||||
update_dist_index();
|
||||
} else if path == absolute_source_sw_path_clone {
|
||||
update_source_sw();
|
||||
} else if let Ok(suffix) = path.strip_prefix(&absolute_modules_path) {
|
||||
let dest = absolute_source_scripts_modules_path.join(suffix);
|
||||
if path.is_file() {
|
||||
debug!("Copying module: {path:?} -> {dest:?}");
|
||||
let _ = fs::create_dir_all(dest.parent().unwrap());
|
||||
if let Err(e) = fs::copy(&path, &dest) {
|
||||
error!("Copy failed: {e}");
|
||||
}
|
||||
}
|
||||
}),
|
||||
Err(e) => error!("watch error: {e:?}"),
|
||||
} else if let Ok(suffix) = path.strip_prefix(&absolute_source_path)
|
||||
// scripts are handled by rolldown
|
||||
&& !path.starts_with(&absolute_source_scripts_path)
|
||||
{
|
||||
let dist_path = absolute_dist_path.join(suffix);
|
||||
if path.is_file() {
|
||||
let _ = fs::create_dir_all(path.parent().unwrap());
|
||||
let _ = fs::copy(&path, &dist_path);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// FSEvents watcher for instant response to manual saves
|
||||
let handle_event_clone = handle_event.clone();
|
||||
let mut fs_watcher = notify::recommended_watcher(
|
||||
move |res: Result<notify::Event, notify::Error>| match res {
|
||||
Ok(event) => match event.kind {
|
||||
EventKind::Create(_) | EventKind::Modify(_) => {
|
||||
event.paths.into_iter().for_each(&handle_event_clone);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => error!("fs watch error: {e:?}"),
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
event_watcher
|
||||
fs_watcher
|
||||
.watch(&absolute_websites_path_clone, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
event_watcher
|
||||
fs_watcher
|
||||
.watch(&absolute_modules_path_clone, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
|
||||
// Poll watcher to catch programmatic edits (e.g., Claude Code's atomic writes)
|
||||
let poll_config = notify::Config::default().with_poll_interval(Duration::from_secs(1));
|
||||
let mut poll_watcher = PollWatcher::new(
|
||||
move |res: Result<notify::Event, notify::Error>| match res {
|
||||
Ok(event) => match event.kind {
|
||||
EventKind::Create(_) | EventKind::Modify(_) => {
|
||||
event.paths.into_iter().for_each(&handle_event);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Err(e) => error!("poll watch error: {e:?}"),
|
||||
},
|
||||
poll_config,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
poll_watcher
|
||||
.watch(&absolute_websites_path_clone2, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
poll_watcher
|
||||
.watch(&absolute_modules_path_clone2, RecursiveMode::Recursive)
|
||||
.unwrap();
|
||||
|
||||
let config = BundlerConfig::new(bundler_options, vec![]);
|
||||
let watcher = rolldown::Watcher::new(config, None).unwrap();
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ repository.workspace = true
|
||||
build = "build.rs"
|
||||
|
||||
[dependencies]
|
||||
brk_binder = { workspace = true }
|
||||
brk_bindgen = { workspace = true }
|
||||
brk_bundler = { workspace = true }
|
||||
brk_computer = { workspace = true }
|
||||
brk_error = { workspace = true }
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
use std::{
|
||||
fs,
|
||||
io::Cursor,
|
||||
path::Path,
|
||||
path::PathBuf,
|
||||
thread::{self, sleep},
|
||||
time::Duration,
|
||||
};
|
||||
@@ -83,15 +83,29 @@ pub fn run() -> color_eyre::Result<()> {
|
||||
|
||||
let future = async move {
|
||||
let bundle_path = if website.is_some() {
|
||||
let websites_dev_path = Path::new("../../websites");
|
||||
let modules_dev_path = Path::new("../../modules");
|
||||
// Try to find local dev directories - check cwd and parent directories
|
||||
let find_dev_dirs = || -> Option<(PathBuf, PathBuf)> {
|
||||
let mut dir = std::env::current_dir().ok()?;
|
||||
loop {
|
||||
let websites = dir.join("websites");
|
||||
let modules = dir.join("modules");
|
||||
if websites.exists() && modules.exists() {
|
||||
return Some((websites, modules));
|
||||
}
|
||||
// Stop at workspace root (crates/ indicates we're there)
|
||||
if dir.join("crates").exists() {
|
||||
return None;
|
||||
}
|
||||
dir = dir.parent()?.to_path_buf();
|
||||
}
|
||||
};
|
||||
|
||||
let websites_path;
|
||||
let modules_path;
|
||||
|
||||
if fs::exists(websites_dev_path)? && fs::exists(modules_dev_path)? {
|
||||
websites_path = websites_dev_path.to_path_buf();
|
||||
modules_path = modules_dev_path.to_path_buf();
|
||||
if let Some((websites, modules)) = find_dev_dirs() {
|
||||
websites_path = websites;
|
||||
modules_path = modules;
|
||||
} else {
|
||||
let downloaded_brk_path = downloads_path.join(format!("brk-{VERSION}"));
|
||||
|
||||
@@ -105,7 +119,7 @@ pub fn run() -> color_eyre::Result<()> {
|
||||
"https://github.com/bitcoinresearchkit/brk/archive/refs/tags/v{VERSION}.zip",
|
||||
);
|
||||
|
||||
let response = minreq::get(url).send()?;
|
||||
let response = minreq::get(url).with_timeout(60).send()?;
|
||||
let bytes = response.as_bytes();
|
||||
let cursor = Cursor::new(bytes);
|
||||
|
||||
|
||||
+5988
-2699
File diff suppressed because it is too large
Load Diff
@@ -23,7 +23,7 @@ brk_traversable = { workspace = true }
|
||||
brk_types = { workspace = true }
|
||||
derive_deref = { workspace = true }
|
||||
log = { workspace = true }
|
||||
pco = "0.4.7"
|
||||
pco = "0.4.9"
|
||||
rayon = { workspace = true }
|
||||
rustc-hash = { workspace = true }
|
||||
schemars = { workspace = true }
|
||||
|
||||
@@ -25,16 +25,18 @@ let mut computer = Computer::forced_import(&outputs_path, &indexer, fetcher)?;
|
||||
computer.compute(&indexer, starting_indexes, &reader, &exit)?;
|
||||
|
||||
// Access computed data
|
||||
let supply = computer.chain.height_to_supply.get(height)?;
|
||||
let realized_cap = computer.stateful.utxo.all.height_to_realized_cap.get(height)?;
|
||||
let supply = computer.distribution.utxo_cohorts.all.metrics.supply.height_to_supply.get(height)?;
|
||||
let realized_cap = computer.distribution.utxo_cohorts.all.metrics.realized.height_to_realized_cap.get(height)?;
|
||||
```
|
||||
|
||||
## Metric Categories
|
||||
|
||||
| Module | Examples |
|
||||
|--------|----------|
|
||||
| `chain` | Supply, subsidy, fees, transaction counts |
|
||||
| `stateful` | Realized cap, MVRV, SOPR, unrealized P&L |
|
||||
| `blocks` | Block count, interval, size, mining metrics, rewards |
|
||||
| `transactions` | Transaction count, fee, size, volume |
|
||||
| `scripts` | Output type counts |
|
||||
| `distribution` | Realized cap, MVRV, SOPR, unrealized P&L, supply |
|
||||
| `cointime` | Liveliness, vaultedness, true market mean |
|
||||
| `pools` | Per-pool block counts, rewards, fees |
|
||||
| `market` | Market cap, NVT, Puell multiple |
|
||||
|
||||
@@ -30,7 +30,7 @@ fn run() -> Result<()> {
|
||||
|
||||
let computer = Computer::forced_import(&outputs_dir, &indexer, Some(fetcher))?;
|
||||
|
||||
let _a = dbg!(computer.chain.transaction.txindex_to_fee.region().meta());
|
||||
let _a = dbg!(computer.transactions.fees.txindex_to_fee.region().meta());
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use vecdb::Exit;
|
||||
|
||||
use crate::{ComputeIndexes, indexes, price, transactions};
|
||||
|
||||
use super::Vecs;
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
transactions: &transactions::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
price: Option<&price::Vecs>,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
// Core block metrics
|
||||
self.count
|
||||
.compute(indexer, indexes, &self.time, starting_indexes, exit)?;
|
||||
self.interval.compute(indexes, starting_indexes, exit)?;
|
||||
self.size
|
||||
.compute(indexer, indexes, starting_indexes, exit)?;
|
||||
self.weight
|
||||
.compute(indexer, indexes, starting_indexes, exit)?;
|
||||
|
||||
// Time metrics (timestamps)
|
||||
self.time.compute(indexes, starting_indexes, exit)?;
|
||||
|
||||
// Epoch metrics
|
||||
self.difficulty.compute(indexes, starting_indexes, exit)?;
|
||||
self.halving.compute(indexes, starting_indexes, exit)?;
|
||||
|
||||
// Rewards depends on count and transactions fees
|
||||
self.rewards.compute(
|
||||
indexer,
|
||||
indexes,
|
||||
&self.count,
|
||||
&transactions.fees,
|
||||
starting_indexes,
|
||||
price,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
// Mining depends on count and rewards
|
||||
self.mining.compute(
|
||||
indexer,
|
||||
indexes,
|
||||
&self.count,
|
||||
&self.rewards,
|
||||
starting_indexes,
|
||||
exit,
|
||||
)?;
|
||||
|
||||
let _lock = exit.lock();
|
||||
self.db.compact()?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Height, StoredU32};
|
||||
use vecdb::{Exit, TypedVecIterator};
|
||||
|
||||
use super::super::time;
|
||||
use super::Vecs;
|
||||
use crate::{indexes, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
time: &time::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let mut height_to_timestamp_fixed_iter =
|
||||
time.height_to_timestamp_fixed.into_iter();
|
||||
let mut prev = Height::ZERO;
|
||||
self.height_to_24h_block_count.compute_transform(
|
||||
starting_indexes.height,
|
||||
&time.height_to_timestamp_fixed,
|
||||
|(h, t, ..)| {
|
||||
while t.difference_in_days_between(height_to_timestamp_fixed_iter.get_unwrap(prev))
|
||||
> 0
|
||||
{
|
||||
prev.increment();
|
||||
if prev > h {
|
||||
unreachable!()
|
||||
}
|
||||
}
|
||||
(h, StoredU32::from(*h + 1 - *prev))
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
self.indexes_to_block_count
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_range(
|
||||
starting_indexes.height,
|
||||
&indexer.vecs.block.height_to_weight,
|
||||
|h| (h, StoredU32::from(1_u32)),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_1w_block_count
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_sum(
|
||||
starting_indexes.dateindex,
|
||||
self.indexes_to_block_count.dateindex.unwrap_sum(),
|
||||
7,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_1m_block_count
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_sum(
|
||||
starting_indexes.dateindex,
|
||||
self.indexes_to_block_count.dateindex.unwrap_sum(),
|
||||
30,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_1y_block_count
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_sum(
|
||||
starting_indexes.dateindex,
|
||||
self.indexes_to_block_count.dateindex.unwrap_sum(),
|
||||
365,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,108 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::{StoredU64, Version};
|
||||
use vecdb::{Database, EagerVec, ImportableVec, IterableCloneableVec, LazyVecFrom1};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
blocks::{
|
||||
TARGET_BLOCKS_PER_DAY, TARGET_BLOCKS_PER_DECADE, TARGET_BLOCKS_PER_MONTH,
|
||||
TARGET_BLOCKS_PER_QUARTER, TARGET_BLOCKS_PER_SEMESTER, TARGET_BLOCKS_PER_WEEK,
|
||||
TARGET_BLOCKS_PER_YEAR,
|
||||
},
|
||||
indexes,
|
||||
internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
let last = || VecBuilderOptions::default().add_last();
|
||||
let sum_cum = || VecBuilderOptions::default().add_sum().add_cumulative();
|
||||
|
||||
Ok(Self {
|
||||
dateindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.dateindex_to_dateindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_DAY)),
|
||||
),
|
||||
weekindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.weekindex_to_weekindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_WEEK)),
|
||||
),
|
||||
monthindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.monthindex_to_monthindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_MONTH)),
|
||||
),
|
||||
quarterindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.quarterindex_to_quarterindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_QUARTER)),
|
||||
),
|
||||
semesterindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.semesterindex_to_semesterindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_SEMESTER)),
|
||||
),
|
||||
yearindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.yearindex_to_yearindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_YEAR)),
|
||||
),
|
||||
decadeindex_to_block_count_target: LazyVecFrom1::init(
|
||||
"block_count_target",
|
||||
version + v0,
|
||||
indexes.time.decadeindex_to_decadeindex.boxed_clone(),
|
||||
|_, _| Some(StoredU64::from(TARGET_BLOCKS_PER_DECADE)),
|
||||
),
|
||||
height_to_24h_block_count: EagerVec::forced_import(
|
||||
db,
|
||||
"24h_block_count",
|
||||
version + v0,
|
||||
)?,
|
||||
indexes_to_block_count: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"block_count",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
sum_cum(),
|
||||
)?,
|
||||
indexes_to_1w_block_count: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"1w_block_count",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_1m_block_count: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"1m_block_count",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_1y_block_count: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"1y_block_count",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{
|
||||
DateIndex, DecadeIndex, MonthIndex, QuarterIndex, SemesterIndex,
|
||||
StoredU32, StoredU64, WeekIndex, YearIndex,
|
||||
};
|
||||
use vecdb::LazyVecFrom1;
|
||||
|
||||
use crate::internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub dateindex_to_block_count_target: LazyVecFrom1<DateIndex, StoredU64, DateIndex, DateIndex>,
|
||||
pub weekindex_to_block_count_target: LazyVecFrom1<WeekIndex, StoredU64, WeekIndex, WeekIndex>,
|
||||
pub monthindex_to_block_count_target: LazyVecFrom1<MonthIndex, StoredU64, MonthIndex, MonthIndex>,
|
||||
pub quarterindex_to_block_count_target: LazyVecFrom1<QuarterIndex, StoredU64, QuarterIndex, QuarterIndex>,
|
||||
pub semesterindex_to_block_count_target: LazyVecFrom1<SemesterIndex, StoredU64, SemesterIndex, SemesterIndex>,
|
||||
pub yearindex_to_block_count_target: LazyVecFrom1<YearIndex, StoredU64, YearIndex, YearIndex>,
|
||||
pub decadeindex_to_block_count_target: LazyVecFrom1<DecadeIndex, StoredU64, DecadeIndex, DecadeIndex>,
|
||||
pub height_to_24h_block_count: vecdb::EagerVec<vecdb::PcoVec<brk_types::Height, StoredU32>>,
|
||||
pub indexes_to_block_count: ComputedVecsFromHeight<StoredU32>,
|
||||
pub indexes_to_1w_block_count: ComputedVecsFromDateIndex<StoredU32>,
|
||||
pub indexes_to_1m_block_count: ComputedVecsFromDateIndex<StoredU32>,
|
||||
pub indexes_to_1y_block_count: ComputedVecsFromDateIndex<StoredU32>,
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::StoredU32;
|
||||
use vecdb::{Exit, TypedVecIterator};
|
||||
|
||||
use super::Vecs;
|
||||
use super::super::TARGET_BLOCKS_PER_DAY_F32;
|
||||
use crate::{indexes, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let mut height_to_difficultyepoch_iter =
|
||||
indexes.block.height_to_difficultyepoch.into_iter();
|
||||
self.indexes_to_difficultyepoch
|
||||
.compute_all(starting_indexes, exit, |vec| {
|
||||
let mut height_count_iter = indexes.time.dateindex_to_height_count.into_iter();
|
||||
vec.compute_transform(
|
||||
starting_indexes.dateindex,
|
||||
&indexes.time.dateindex_to_first_height,
|
||||
|(di, height, ..)| {
|
||||
(
|
||||
di,
|
||||
height_to_difficultyepoch_iter
|
||||
.get_unwrap(height + (*height_count_iter.get_unwrap(di) - 1)),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_blocks_before_next_difficulty_adjustment
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform(
|
||||
starting_indexes.height,
|
||||
&indexes.block.height_to_height,
|
||||
|(h, ..)| (h, StoredU32::from(h.left_before_next_diff_adj())),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_days_before_next_difficulty_adjustment
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform(
|
||||
starting_indexes.height,
|
||||
self.indexes_to_blocks_before_next_difficulty_adjustment
|
||||
.height
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
|(h, blocks, ..)| (h, (*blocks as f32 / TARGET_BLOCKS_PER_DAY_F32).into()),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
+1
-26
@@ -4,8 +4,8 @@ use vecdb::Database;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
grouped::{ComputedVecsFromDateIndex, ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
indexes,
|
||||
internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
@@ -23,15 +23,6 @@ impl Vecs {
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_halvingepoch: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"halvingepoch",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
// Countdown metrics (moved from mining)
|
||||
indexes_to_blocks_before_next_difficulty_adjustment:
|
||||
ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
@@ -50,22 +41,6 @@ impl Vecs {
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_blocks_before_next_halving: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"blocks_before_next_halving",
|
||||
Source::Compute,
|
||||
version + v2,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_days_before_next_halving: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"days_before_next_halving",
|
||||
Source::Compute,
|
||||
version + v2,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{DifficultyEpoch, StoredF32, StoredU32};
|
||||
|
||||
use crate::internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight};
|
||||
|
||||
/// Difficulty epoch metrics and countdown
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub indexes_to_difficultyepoch: ComputedVecsFromDateIndex<DifficultyEpoch>,
|
||||
pub indexes_to_blocks_before_next_difficulty_adjustment: ComputedVecsFromHeight<StoredU32>,
|
||||
pub indexes_to_days_before_next_difficulty_adjustment: ComputedVecsFromHeight<StoredF32>,
|
||||
}
|
||||
+2
-47
@@ -3,7 +3,8 @@ use brk_types::StoredU32;
|
||||
use vecdb::{Exit, TypedVecIterator};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{chain::TARGET_BLOCKS_PER_DAY_F32, indexes, ComputeIndexes};
|
||||
use super::super::TARGET_BLOCKS_PER_DAY_F32;
|
||||
use crate::{indexes, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
@@ -12,26 +13,6 @@ impl Vecs {
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let mut height_to_difficultyepoch_iter =
|
||||
indexes.block.height_to_difficultyepoch.into_iter();
|
||||
self.indexes_to_difficultyepoch
|
||||
.compute_all(starting_indexes, exit, |vec| {
|
||||
let mut height_count_iter = indexes.time.dateindex_to_height_count.into_iter();
|
||||
vec.compute_transform(
|
||||
starting_indexes.dateindex,
|
||||
&indexes.time.dateindex_to_first_height,
|
||||
|(di, height, ..)| {
|
||||
(
|
||||
di,
|
||||
height_to_difficultyepoch_iter
|
||||
.get_unwrap(height + (*height_count_iter.get_unwrap(di) - 1)),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
let mut height_to_halvingepoch_iter = indexes.block.height_to_halvingepoch.into_iter();
|
||||
self.indexes_to_halvingepoch
|
||||
.compute_all(starting_indexes, exit, |vec| {
|
||||
@@ -51,32 +32,6 @@ impl Vecs {
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Countdown metrics (moved from mining)
|
||||
self.indexes_to_blocks_before_next_difficulty_adjustment
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform(
|
||||
starting_indexes.height,
|
||||
&indexes.block.height_to_height,
|
||||
|(h, ..)| (h, StoredU32::from(h.left_before_next_diff_adj())),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_days_before_next_difficulty_adjustment
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform(
|
||||
starting_indexes.height,
|
||||
self.indexes_to_blocks_before_next_difficulty_adjustment
|
||||
.height
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
|(h, blocks, ..)| (h, (*blocks as f32 / TARGET_BLOCKS_PER_DAY_F32).into()),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_blocks_before_next_halving.compute_all(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
@@ -0,0 +1,44 @@
|
||||
use brk_error::Result;
|
||||
use brk_types::Version;
|
||||
use vecdb::Database;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(db: &Database, version: Version, indexes: &indexes::Vecs) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
let v2 = Version::TWO;
|
||||
let last = || VecBuilderOptions::default().add_last();
|
||||
|
||||
Ok(Self {
|
||||
indexes_to_halvingepoch: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"halvingepoch",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_blocks_before_next_halving: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"blocks_before_next_halving",
|
||||
Source::Compute,
|
||||
version + v2,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
indexes_to_days_before_next_halving: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"days_before_next_halving",
|
||||
Source::Compute,
|
||||
version + v2,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{HalvingEpoch, StoredF32, StoredU32};
|
||||
|
||||
use crate::internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight};
|
||||
|
||||
/// Halving epoch metrics and countdown
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub indexes_to_halvingepoch: ComputedVecsFromDateIndex<HalvingEpoch>,
|
||||
pub indexes_to_blocks_before_next_halving: ComputedVecsFromHeight<StoredU32>,
|
||||
pub indexes_to_days_before_next_halving: ComputedVecsFromHeight<StoredF32>,
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use std::path::Path;
|
||||
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, PAGE_SIZE};
|
||||
|
||||
use crate::{indexes, price};
|
||||
|
||||
use super::{
|
||||
CountVecs, DifficultyVecs, HalvingVecs, IntervalVecs, MiningVecs,
|
||||
RewardsVecs, SizeVecs, TimeVecs, Vecs, WeightVecs,
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
parent_path: &Path,
|
||||
parent_version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
price: Option<&price::Vecs>,
|
||||
) -> Result<Self> {
|
||||
let db = Database::open(&parent_path.join(super::DB_NAME))?;
|
||||
db.set_min_len(PAGE_SIZE * 50_000_000)?;
|
||||
|
||||
let version = parent_version + Version::ZERO;
|
||||
let compute_dollars = price.is_some();
|
||||
|
||||
let count = CountVecs::forced_import(&db, version, indexes)?;
|
||||
let interval = IntervalVecs::forced_import(&db, version, indexer, indexes)?;
|
||||
let size = SizeVecs::forced_import(&db, version, indexer, indexes)?;
|
||||
let weight = WeightVecs::forced_import(&db, version, indexer, indexes)?;
|
||||
let time = TimeVecs::forced_import(&db, version, indexer, indexes)?;
|
||||
let mining = MiningVecs::forced_import(&db, version, indexer, indexes)?;
|
||||
let rewards = RewardsVecs::forced_import(&db, version, indexes, compute_dollars)?;
|
||||
let difficulty = DifficultyVecs::forced_import(&db, version, indexes)?;
|
||||
let halving = HalvingVecs::forced_import(&db, version, indexes)?;
|
||||
|
||||
let this = Self {
|
||||
db,
|
||||
count,
|
||||
interval,
|
||||
size,
|
||||
weight,
|
||||
time,
|
||||
mining,
|
||||
rewards,
|
||||
difficulty,
|
||||
halving,
|
||||
};
|
||||
|
||||
this.db.retain_regions(
|
||||
this.iter_any_exportable()
|
||||
.flat_map(|v| v.region_names())
|
||||
.collect(),
|
||||
)?;
|
||||
this.db.compact()?;
|
||||
|
||||
Ok(this)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
use brk_error::Result;
|
||||
use vecdb::Exit;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{ComputeIndexes, indexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.indexes_to_block_interval.compute_rest(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
Some(&self.height_to_interval),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{CheckedSub, Height, Timestamp, Version};
|
||||
use vecdb::{Database, IterableCloneableVec, LazyVecFrom1};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
let stats = || {
|
||||
VecBuilderOptions::default()
|
||||
.add_average()
|
||||
.add_minmax()
|
||||
.add_percentiles()
|
||||
};
|
||||
|
||||
let height_to_interval = LazyVecFrom1::init(
|
||||
"interval",
|
||||
version + v0,
|
||||
indexer.vecs.block.height_to_timestamp.boxed_clone(),
|
||||
|height: Height, timestamp_iter| {
|
||||
let timestamp = timestamp_iter.get(height)?;
|
||||
let interval = height.decremented().map_or(Timestamp::ZERO, |prev_h| {
|
||||
timestamp_iter
|
||||
.get(prev_h)
|
||||
.map_or(Timestamp::ZERO, |prev_t| {
|
||||
timestamp.checked_sub(prev_t).unwrap_or(Timestamp::ZERO)
|
||||
})
|
||||
});
|
||||
Some(interval)
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
indexes_to_block_interval: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"block_interval",
|
||||
Source::Vec(height_to_interval.boxed_clone()),
|
||||
version + v0,
|
||||
indexes,
|
||||
stats(),
|
||||
)?,
|
||||
height_to_interval,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, Timestamp};
|
||||
use vecdb::LazyVecFrom1;
|
||||
|
||||
use crate::internal::ComputedVecsFromHeight;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub height_to_interval: LazyVecFrom1<Height, Timestamp, Height, Timestamp>,
|
||||
pub indexes_to_block_interval: ComputedVecsFromHeight<Timestamp>,
|
||||
}
|
||||
+20
-11
@@ -4,8 +4,8 @@ use brk_types::{StoredF32, StoredF64};
|
||||
use vecdb::Exit;
|
||||
|
||||
use super::Vecs;
|
||||
use super::super::{count, rewards, ONE_TERA_HASH, TARGET_BLOCKS_PER_DAY_F64};
|
||||
use crate::{
|
||||
chain::{block, coinbase, ONE_TERA_HASH, TARGET_BLOCKS_PER_DAY_F64},
|
||||
indexes,
|
||||
utils::OptionExt,
|
||||
ComputeIndexes,
|
||||
@@ -16,8 +16,8 @@ impl Vecs {
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
block_vecs: &block::Vecs,
|
||||
coinbase_vecs: &coinbase::Vecs,
|
||||
count_vecs: &count::Vecs,
|
||||
rewards_vecs: &rewards::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
@@ -44,7 +44,7 @@ impl Vecs {
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform2(
|
||||
starting_indexes.height,
|
||||
&block_vecs.height_to_24h_block_count,
|
||||
&count_vecs.height_to_24h_block_count,
|
||||
self.indexes_to_difficulty_as_hash.height.u(),
|
||||
|(i, block_count_sum, difficulty_as_hash, ..)| {
|
||||
(
|
||||
@@ -123,10 +123,16 @@ impl Vecs {
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform2(
|
||||
starting_indexes.height,
|
||||
&coinbase_vecs.height_to_24h_coinbase_usd_sum,
|
||||
&rewards_vecs.height_to_24h_coinbase_usd_sum,
|
||||
self.indexes_to_hash_rate.height.u(),
|
||||
|(i, coinbase_sum, hashrate, ..)| {
|
||||
(i, (*coinbase_sum / (*hashrate / ONE_TERA_HASH)).into())
|
||||
let hashrate_ths = *hashrate / ONE_TERA_HASH;
|
||||
let price = if hashrate_ths == 0.0 {
|
||||
StoredF32::NAN
|
||||
} else {
|
||||
(*coinbase_sum / hashrate_ths).into()
|
||||
};
|
||||
(i, price)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
@@ -148,13 +154,16 @@ impl Vecs {
|
||||
.compute_all(indexes, starting_indexes, exit, |v| {
|
||||
v.compute_transform2(
|
||||
starting_indexes.height,
|
||||
&coinbase_vecs.height_to_24h_coinbase_sum,
|
||||
&rewards_vecs.height_to_24h_coinbase_sum,
|
||||
self.indexes_to_hash_rate.height.u(),
|
||||
|(i, coinbase_sum, hashrate, ..)| {
|
||||
(
|
||||
i,
|
||||
(*coinbase_sum as f64 / (*hashrate / ONE_TERA_HASH)).into(),
|
||||
)
|
||||
let hashrate_ths = *hashrate / ONE_TERA_HASH;
|
||||
let value = if hashrate_ths == 0.0 {
|
||||
StoredF32::NAN
|
||||
} else {
|
||||
StoredF32::from(*coinbase_sum as f64 / hashrate_ths)
|
||||
};
|
||||
(i, value)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
+1
-1
@@ -5,7 +5,7 @@ use vecdb::{Database, IterableCloneableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
grouped::{ComputedVecsFromDateIndex, ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
indexes,
|
||||
};
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredF32, StoredF64};
|
||||
|
||||
use crate::grouped::{ComputedVecsFromDateIndex, ComputedVecsFromHeight};
|
||||
use crate::internal::{ComputedVecsFromDateIndex, ComputedVecsFromHeight};
|
||||
|
||||
/// Mining-related metrics: hash rate, hash price, hash value, difficulty
|
||||
#[derive(Clone, Traversable)]
|
||||
@@ -1,25 +1,30 @@
|
||||
pub mod block;
|
||||
pub mod coinbase;
|
||||
mod compute;
|
||||
pub mod epoch;
|
||||
mod import;
|
||||
pub mod count;
|
||||
pub mod difficulty;
|
||||
pub mod halving;
|
||||
pub mod interval;
|
||||
pub mod mining;
|
||||
pub mod output_type;
|
||||
pub mod transaction;
|
||||
pub mod volume;
|
||||
pub mod rewards;
|
||||
pub mod size;
|
||||
pub mod time;
|
||||
pub mod weight;
|
||||
|
||||
mod compute;
|
||||
mod import;
|
||||
|
||||
use brk_traversable::Traversable;
|
||||
use vecdb::Database;
|
||||
|
||||
pub use block::Vecs as BlockVecs;
|
||||
pub use coinbase::Vecs as CoinbaseVecs;
|
||||
pub use epoch::Vecs as EpochVecs;
|
||||
pub use count::Vecs as CountVecs;
|
||||
pub use difficulty::Vecs as DifficultyVecs;
|
||||
pub use halving::Vecs as HalvingVecs;
|
||||
pub use interval::Vecs as IntervalVecs;
|
||||
pub use mining::Vecs as MiningVecs;
|
||||
pub use output_type::Vecs as OutputTypeVecs;
|
||||
pub use transaction::Vecs as TransactionVecs;
|
||||
pub use volume::Vecs as VolumeVecs;
|
||||
pub use rewards::Vecs as RewardsVecs;
|
||||
pub use size::Vecs as SizeVecs;
|
||||
pub use time::Vecs as TimeVecs;
|
||||
pub use weight::Vecs as WeightVecs;
|
||||
|
||||
pub const DB_NAME: &str = "chain";
|
||||
pub const DB_NAME: &str = "blocks";
|
||||
|
||||
pub(crate) const TARGET_BLOCKS_PER_DAY_F64: f64 = 144.0;
|
||||
pub(crate) const TARGET_BLOCKS_PER_DAY_F32: f32 = 144.0;
|
||||
@@ -32,16 +37,18 @@ pub(crate) const TARGET_BLOCKS_PER_YEAR: u64 = 2 * TARGET_BLOCKS_PER_SEMESTER;
|
||||
pub(crate) const TARGET_BLOCKS_PER_DECADE: u64 = 10 * TARGET_BLOCKS_PER_YEAR;
|
||||
pub(crate) const ONE_TERA_HASH: f64 = 1_000_000_000_000.0;
|
||||
|
||||
/// Main chain metrics struct composed of sub-modules
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
#[traversable(skip)]
|
||||
pub(crate) db: Database,
|
||||
pub block: BlockVecs,
|
||||
pub epoch: EpochVecs,
|
||||
|
||||
pub count: CountVecs,
|
||||
pub interval: IntervalVecs,
|
||||
pub size: SizeVecs,
|
||||
pub weight: WeightVecs,
|
||||
pub time: TimeVecs,
|
||||
pub mining: MiningVecs,
|
||||
pub coinbase: CoinbaseVecs,
|
||||
pub transaction: TransactionVecs,
|
||||
pub output_type: OutputTypeVecs,
|
||||
pub volume: VolumeVecs,
|
||||
pub rewards: RewardsVecs,
|
||||
pub difficulty: DifficultyVecs,
|
||||
pub halving: HalvingVecs,
|
||||
}
|
||||
+32
-64
@@ -5,11 +5,12 @@ use vecdb::{Exit, IterableVec, TypedVecIterator, VecIndex};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
transactions,
|
||||
ComputeIndexes,
|
||||
chain::{block, transaction},
|
||||
indexes, price,
|
||||
utils::OptionExt,
|
||||
};
|
||||
use super::super::count;
|
||||
|
||||
impl Vecs {
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -17,8 +18,8 @@ impl Vecs {
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
block_vecs: &block::Vecs,
|
||||
transaction_vecs: &transaction::Vecs,
|
||||
count_vecs: &count::Vecs,
|
||||
transactions_fees: &transactions::FeesVecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
price: Option<&price::Vecs>,
|
||||
exit: &Exit,
|
||||
@@ -61,7 +62,7 @@ impl Vecs {
|
||||
.into_iter();
|
||||
self.height_to_24h_coinbase_sum.compute_transform(
|
||||
starting_indexes.height,
|
||||
&block_vecs.height_to_24h_block_count,
|
||||
&count_vecs.height_to_24h_block_count,
|
||||
|(h, count, ..)| {
|
||||
let range = *h - (*count - 1)..=*h;
|
||||
let sum = range
|
||||
@@ -82,7 +83,7 @@ impl Vecs {
|
||||
{
|
||||
self.height_to_24h_coinbase_usd_sum.compute_transform(
|
||||
starting_indexes.height,
|
||||
&block_vecs.height_to_24h_block_count,
|
||||
&count_vecs.height_to_24h_block_count,
|
||||
|(h, count, ..)| {
|
||||
let range = *h - (*count - 1)..=*h;
|
||||
let sum = range
|
||||
@@ -100,7 +101,7 @@ impl Vecs {
|
||||
vec.compute_transform2(
|
||||
starting_indexes.height,
|
||||
self.indexes_to_coinbase.sats.height.u(),
|
||||
transaction_vecs.indexes_to_fee.sats.height.unwrap_sum(),
|
||||
transactions_fees.indexes_to_fee.sats.height.unwrap_sum(),
|
||||
|(height, coinbase, fees, ..)| {
|
||||
(
|
||||
height,
|
||||
@@ -135,33 +136,18 @@ impl Vecs {
|
||||
},
|
||||
)?;
|
||||
|
||||
self.indexes_to_inflation_rate
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_transform2(
|
||||
starting_indexes.dateindex,
|
||||
self.indexes_to_subsidy.sats.dateindex.unwrap_sum(),
|
||||
self.indexes_to_subsidy.sats.dateindex.unwrap_cumulative(),
|
||||
|(i, subsidy_1d_sum, subsidy_cumulative, ..)| {
|
||||
(
|
||||
i,
|
||||
(365.0 * *subsidy_1d_sum as f64 / *subsidy_cumulative as f64 * 100.0)
|
||||
.into(),
|
||||
)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.dateindex_to_fee_dominance.compute_transform2(
|
||||
starting_indexes.dateindex,
|
||||
transaction_vecs.indexes_to_fee.sats.dateindex.unwrap_sum(),
|
||||
transactions_fees.indexes_to_fee.sats.dateindex.unwrap_sum(),
|
||||
self.indexes_to_coinbase.sats.dateindex.unwrap_sum(),
|
||||
|(i, fee, coinbase, ..)| {
|
||||
(
|
||||
i,
|
||||
StoredF32::from(u64::from(fee) as f64 / u64::from(coinbase) as f64 * 100.0),
|
||||
)
|
||||
let coinbase_f64 = u64::from(coinbase) as f64;
|
||||
let dominance = if coinbase_f64 == 0.0 {
|
||||
StoredF32::NAN
|
||||
} else {
|
||||
StoredF32::from(u64::from(fee) as f64 / coinbase_f64 * 100.0)
|
||||
};
|
||||
(i, dominance)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
@@ -171,15 +157,18 @@ impl Vecs {
|
||||
self.indexes_to_subsidy.sats.dateindex.unwrap_sum(),
|
||||
self.indexes_to_coinbase.sats.dateindex.unwrap_sum(),
|
||||
|(i, subsidy, coinbase, ..)| {
|
||||
(
|
||||
i,
|
||||
StoredF32::from(u64::from(subsidy) as f64 / u64::from(coinbase) as f64 * 100.0),
|
||||
)
|
||||
let coinbase_f64 = u64::from(coinbase) as f64;
|
||||
let dominance = if coinbase_f64 == 0.0 {
|
||||
StoredF32::NAN
|
||||
} else {
|
||||
StoredF32::from(u64::from(subsidy) as f64 / coinbase_f64 * 100.0)
|
||||
};
|
||||
(i, dominance)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
if self.indexes_to_subsidy_usd_1y_sma.is_some() {
|
||||
if let Some(sma) = self.indexes_to_subsidy_usd_1y_sma.as_mut() {
|
||||
let date_to_coinbase_usd_sum = self
|
||||
.indexes_to_coinbase
|
||||
.dollars
|
||||
@@ -188,36 +177,15 @@ impl Vecs {
|
||||
.dateindex
|
||||
.unwrap_sum();
|
||||
|
||||
self.indexes_to_subsidy_usd_1y_sma
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_sma(
|
||||
starting_indexes.dateindex,
|
||||
date_to_coinbase_usd_sum,
|
||||
365,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
self.indexes_to_puell_multiple
|
||||
.as_mut()
|
||||
.unwrap()
|
||||
.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_divide(
|
||||
starting_indexes.dateindex,
|
||||
date_to_coinbase_usd_sum,
|
||||
self.indexes_to_subsidy_usd_1y_sma
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.dateindex
|
||||
.as_ref()
|
||||
.unwrap(),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
sma.compute_all(starting_indexes, exit, |v| {
|
||||
v.compute_sma(
|
||||
starting_indexes.dateindex,
|
||||
date_to_coinbase_usd_sum,
|
||||
365,
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
+1
-21
@@ -4,7 +4,7 @@ use vecdb::{Database, EagerVec, ImportableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
grouped::{ComputedValueVecsFromHeight, ComputedVecsFromDateIndex, Source, VecBuilderOptions},
|
||||
internal::{ComputedValueVecsFromHeight, ComputedVecsFromDateIndex, Source, VecBuilderOptions},
|
||||
indexes,
|
||||
};
|
||||
|
||||
@@ -84,26 +84,6 @@ impl Vecs {
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
indexes_to_puell_multiple: compute_dollars
|
||||
.then(|| {
|
||||
ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"puell_multiple",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)
|
||||
})
|
||||
.transpose()?,
|
||||
indexes_to_inflation_rate: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"inflation_rate",
|
||||
Source::Compute,
|
||||
version + v0,
|
||||
indexes,
|
||||
last(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
+1
-3
@@ -2,7 +2,7 @@ use brk_traversable::Traversable;
|
||||
use brk_types::{DateIndex, Dollars, Height, Sats, StoredF32};
|
||||
use vecdb::{EagerVec, PcoVec};
|
||||
|
||||
use crate::grouped::{ComputedValueVecsFromHeight, ComputedVecsFromDateIndex};
|
||||
use crate::internal::{ComputedValueVecsFromHeight, ComputedVecsFromDateIndex};
|
||||
|
||||
/// Coinbase/subsidy/rewards metrics
|
||||
#[derive(Clone, Traversable)]
|
||||
@@ -15,6 +15,4 @@ pub struct Vecs {
|
||||
pub dateindex_to_fee_dominance: EagerVec<PcoVec<DateIndex, StoredF32>>,
|
||||
pub dateindex_to_subsidy_dominance: EagerVec<PcoVec<DateIndex, StoredF32>>,
|
||||
pub indexes_to_subsidy_usd_1y_sma: Option<ComputedVecsFromDateIndex<Dollars>>,
|
||||
pub indexes_to_puell_multiple: Option<ComputedVecsFromDateIndex<StoredF32>>,
|
||||
pub indexes_to_inflation_rate: ComputedVecsFromDateIndex<StoredF32>,
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use vecdb::Exit;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.indexes_to_block_size.compute_rest(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
Some(&indexer.vecs.block.height_to_total_size),
|
||||
)?;
|
||||
|
||||
self.indexes_to_block_vbytes.compute_rest(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
Some(&self.height_to_vbytes),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Height, StoredU64, Version};
|
||||
use vecdb::{Database, IterableCloneableVec, LazyVecFrom1, VecIndex};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedVecsFromHeight, Source, VecBuilderOptions},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
let full_stats = || {
|
||||
VecBuilderOptions::default()
|
||||
.add_average()
|
||||
.add_minmax()
|
||||
.add_percentiles()
|
||||
.add_sum()
|
||||
.add_cumulative()
|
||||
};
|
||||
|
||||
let height_to_vbytes = LazyVecFrom1::init(
|
||||
"vbytes",
|
||||
version + v0,
|
||||
indexer.vecs.block.height_to_weight.boxed_clone(),
|
||||
|height: Height, weight_iter| {
|
||||
weight_iter
|
||||
.get_at(height.to_usize())
|
||||
.map(|w| StoredU64::from(w.to_vbytes_floor()))
|
||||
},
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
indexes_to_block_size: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"block_size",
|
||||
Source::Vec(indexer.vecs.block.height_to_total_size.boxed_clone()),
|
||||
version + v0,
|
||||
indexes,
|
||||
full_stats(),
|
||||
)?,
|
||||
indexes_to_block_vbytes: ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"block_vbytes",
|
||||
Source::Vec(height_to_vbytes.boxed_clone()),
|
||||
version + v0,
|
||||
indexes,
|
||||
full_stats(),
|
||||
)?,
|
||||
height_to_vbytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Height, StoredU64, Weight};
|
||||
use vecdb::LazyVecFrom1;
|
||||
|
||||
use crate::internal::ComputedVecsFromHeight;
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub height_to_vbytes: LazyVecFrom1<Height, StoredU64, Height, Weight>,
|
||||
pub indexes_to_block_size: ComputedVecsFromHeight<StoredU64>,
|
||||
pub indexes_to_block_vbytes: ComputedVecsFromHeight<StoredU64>,
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::Timestamp;
|
||||
use vecdb::{Exit, TypedVecIterator};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
/// Compute height-to-time fields early, before indexes are computed.
|
||||
/// These are needed by indexes::block to compute height_to_dateindex.
|
||||
pub fn compute_early(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
starting_height: brk_types::Height,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
let mut prev_timestamp_fixed = None;
|
||||
self.height_to_timestamp_fixed.compute_transform(
|
||||
starting_height,
|
||||
&indexer.vecs.block.height_to_timestamp,
|
||||
|(h, timestamp, height_to_timestamp_fixed_iter)| {
|
||||
if prev_timestamp_fixed.is_none()
|
||||
&& let Some(prev_h) = h.decremented()
|
||||
{
|
||||
prev_timestamp_fixed.replace(
|
||||
height_to_timestamp_fixed_iter
|
||||
.into_iter()
|
||||
.get_unwrap(prev_h),
|
||||
);
|
||||
}
|
||||
let timestamp_fixed =
|
||||
prev_timestamp_fixed.map_or(timestamp, |prev_d| prev_d.max(timestamp));
|
||||
prev_timestamp_fixed.replace(timestamp_fixed);
|
||||
(h, timestamp_fixed)
|
||||
},
|
||||
exit,
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.timeindexes_to_timestamp
|
||||
.compute_all(starting_indexes, exit, |vec| {
|
||||
vec.compute_transform(
|
||||
starting_indexes.dateindex,
|
||||
&indexes.time.dateindex_to_date,
|
||||
|(di, d, ..)| (di, Timestamp::from(d)),
|
||||
exit,
|
||||
)?;
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::{Date, DifficultyEpoch, Height, Version};
|
||||
use vecdb::{
|
||||
Database, EagerVec, ImportableVec, IterableCloneableVec, LazyVecFrom1, LazyVecFrom2, VecIndex,
|
||||
};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedVecsFromDateIndex, Source, VecBuilderOptions},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let height_to_timestamp_fixed =
|
||||
EagerVec::forced_import(db, "timestamp_fixed", version + Version::ZERO)?;
|
||||
|
||||
Ok(Self {
|
||||
height_to_date: LazyVecFrom1::init(
|
||||
"date",
|
||||
version + Version::ZERO,
|
||||
indexer.vecs.block.height_to_timestamp.boxed_clone(),
|
||||
|height: Height, timestamp_iter| {
|
||||
timestamp_iter.get_at(height.to_usize()).map(Date::from)
|
||||
},
|
||||
),
|
||||
height_to_date_fixed: LazyVecFrom1::init(
|
||||
"date_fixed",
|
||||
version + Version::ZERO,
|
||||
height_to_timestamp_fixed.boxed_clone(),
|
||||
|height: Height, timestamp_iter| timestamp_iter.get(height).map(Date::from),
|
||||
),
|
||||
height_to_timestamp_fixed,
|
||||
difficultyepoch_to_timestamp: LazyVecFrom2::init(
|
||||
"timestamp",
|
||||
version + Version::ZERO,
|
||||
indexes.block.difficultyepoch_to_first_height.boxed_clone(),
|
||||
indexer.vecs.block.height_to_timestamp.boxed_clone(),
|
||||
|di: DifficultyEpoch, first_height_iter, timestamp_iter| {
|
||||
first_height_iter
|
||||
.get(di)
|
||||
.and_then(|h: Height| timestamp_iter.get(h))
|
||||
},
|
||||
),
|
||||
timeindexes_to_timestamp: ComputedVecsFromDateIndex::forced_import(
|
||||
db,
|
||||
"timestamp",
|
||||
Source::Compute,
|
||||
version + Version::ZERO,
|
||||
indexes,
|
||||
VecBuilderOptions::default().add_first(),
|
||||
)?,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{Date, DifficultyEpoch, Height, Timestamp};
|
||||
use vecdb::{EagerVec, LazyVecFrom1, LazyVecFrom2, PcoVec};
|
||||
|
||||
use crate::internal::ComputedVecsFromDateIndex;
|
||||
|
||||
/// Timestamp and date metrics for blocks
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub height_to_date: LazyVecFrom1<Height, Date, Height, Timestamp>,
|
||||
pub height_to_date_fixed: LazyVecFrom1<Height, Date, Height, Timestamp>,
|
||||
pub height_to_timestamp_fixed: EagerVec<PcoVec<Height, Timestamp>>,
|
||||
pub difficultyepoch_to_timestamp:
|
||||
LazyVecFrom2<DifficultyEpoch, Timestamp, DifficultyEpoch, Height, Height, Timestamp>,
|
||||
pub timeindexes_to_timestamp: ComputedVecsFromDateIndex<Timestamp>,
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use vecdb::Exit;
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{indexes, ComputeIndexes};
|
||||
|
||||
impl Vecs {
|
||||
pub fn compute(
|
||||
&mut self,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
starting_indexes: &ComputeIndexes,
|
||||
exit: &Exit,
|
||||
) -> Result<()> {
|
||||
self.indexes_to_block_weight.compute_rest(
|
||||
indexes,
|
||||
starting_indexes,
|
||||
exit,
|
||||
Some(&indexer.vecs.block.height_to_weight),
|
||||
)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
use brk_error::Result;
|
||||
use brk_indexer::Indexer;
|
||||
use brk_types::Version;
|
||||
use vecdb::{Database, IterableCloneableVec};
|
||||
|
||||
use super::Vecs;
|
||||
use crate::{
|
||||
indexes,
|
||||
internal::{ComputedVecsFromHeight, LazyVecsFromHeight, Source, VecBuilderOptions, WeightToFullness},
|
||||
};
|
||||
|
||||
impl Vecs {
|
||||
pub fn forced_import(
|
||||
db: &Database,
|
||||
version: Version,
|
||||
indexer: &Indexer,
|
||||
indexes: &indexes::Vecs,
|
||||
) -> Result<Self> {
|
||||
let v0 = Version::ZERO;
|
||||
let full_stats = || {
|
||||
VecBuilderOptions::default()
|
||||
.add_average()
|
||||
.add_minmax()
|
||||
.add_percentiles()
|
||||
.add_sum()
|
||||
.add_cumulative()
|
||||
};
|
||||
|
||||
let indexes_to_block_weight = ComputedVecsFromHeight::forced_import(
|
||||
db,
|
||||
"block_weight",
|
||||
Source::Vec(indexer.vecs.block.height_to_weight.boxed_clone()),
|
||||
version + v0,
|
||||
indexes,
|
||||
full_stats(),
|
||||
)?;
|
||||
|
||||
let indexes_to_block_fullness = LazyVecsFromHeight::from_computed::<WeightToFullness>(
|
||||
"block_fullness",
|
||||
version + v0,
|
||||
indexer.vecs.block.height_to_weight.boxed_clone(),
|
||||
&indexes_to_block_weight,
|
||||
);
|
||||
|
||||
Ok(Self {
|
||||
indexes_to_block_weight,
|
||||
indexes_to_block_fullness,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,5 @@
|
||||
mod compute;
|
||||
mod import;
|
||||
mod vecs;
|
||||
|
||||
pub use vecs::Vecs;
|
||||
@@ -0,0 +1,11 @@
|
||||
use brk_traversable::Traversable;
|
||||
use brk_types::{StoredF32, Weight};
|
||||
|
||||
use crate::internal::{ComputedVecsFromHeight, LazyVecsFromHeight};
|
||||
|
||||
#[derive(Clone, Traversable)]
|
||||
pub struct Vecs {
|
||||
pub indexes_to_block_weight: ComputedVecsFromHeight<Weight>,
|
||||
/// Block fullness as percentage of max block weight (0-100%)
|
||||
pub indexes_to_block_fullness: LazyVecsFromHeight<StoredF32, Weight>,
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user