global: snapshot

This commit is contained in:
nym21
2025-12-25 22:21:12 +01:00
parent eadf93b804
commit bbb74b76c8
68 changed files with 6497 additions and 2659 deletions
+37 -18
View File
@@ -13,12 +13,14 @@ use crate::{
get_node_fields, get_pattern_instance_base, to_camel_case, to_pascal_case,
};
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints
/// Generate JavaScript + JSDoc client from metadata and OpenAPI endpoints.
///
/// `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_dir: &Path,
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
@@ -44,7 +46,7 @@ pub fn generate_javascript_client(
// Generate the main client class with tree and API methods
generate_main_client(&mut output, &metadata.catalog, metadata, endpoints);
fs::write(output_dir.join("client.js"), output)?;
fs::write(output_path, output)?;
Ok(())
}
@@ -446,13 +448,21 @@ fn generate_structural_patterns(
// Generate factory function
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();
}
writeln!(output, " * @returns {{{}}}", pattern.name).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 {
@@ -613,11 +623,17 @@ fn field_to_js_type_with_generic_value(
};
if metadata.is_pattern_type(&field.rust_type) {
// Check if this pattern is generic and we have a value type
if metadata.is_pattern_generic(&field.rust_type)
&& let Some(vt) = generic_value_type
{
return format!("{}<{}>", field.rust_type, vt);
// Check if this pattern is generic
if metadata.is_pattern_generic(&field.rust_type) {
if let Some(vt) = generic_value_type {
return format!("{}<{}>", field.rust_type, vt);
} else if is_generic {
// Propagate T when inside a generic pattern
return format!("{}<T>", field.rust_type);
} else {
// Generic pattern without known type - use unknown
return format!("{}<unknown>", field.rust_type);
}
}
field.rust_type.clone()
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
@@ -629,7 +645,6 @@ fn field_to_js_type_with_generic_value(
}
}
/// Generate tree typedefs
fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata: &ClientMetadata) {
writeln!(output, "// Catalog tree typedefs\n").unwrap();
@@ -666,7 +681,8 @@ fn generate_tree_typedef(
.collect();
// Skip if this matches a pattern (already generated)
if pattern_lookup.contains_key(&fields) && pattern_lookup.get(&fields) != Some(&name.to_string())
if pattern_lookup.contains_key(&fields)
&& pattern_lookup.get(&fields) != Some(&name.to_string())
{
return;
}
@@ -680,15 +696,13 @@ fn generate_tree_typedef(
writeln!(output, " * @typedef {{Object}} {}", name).unwrap();
for (field, child_fields) in &fields_with_child_info {
// Look up type parameter for generic patterns
let generic_value_type = child_fields
.as_ref()
.and_then(|cf| metadata.get_generic_value_type(&field.rust_type, cf));
let js_type = field_to_js_type_with_generic_value(
field,
metadata,
false,
generic_value_type.as_deref(),
);
.and_then(|cf| metadata.get_type_param(cf))
.map(String::as_str);
let js_type =
field_to_js_type_with_generic_value(field, metadata, false, generic_value_type);
writeln!(
output,
" * @property {{{}}} {}",
@@ -899,6 +913,11 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
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("");
+82 -13
View File
@@ -27,14 +27,57 @@
//! 2. **Schema collection** - Merges OpenAPI schemas with schemars-generated type schemas
//!
//! 3. **Code generation** - Produces language-specific clients:
//! - Rust: Uses `brk_types` directly, generates structs with lifetimes
//! - Rust: Uses `brk_types` directly, generates structs with Arc-based sharing
//! - JavaScript: Generates JSDoc-typed ES modules with factory functions
//! - Python: Generates typed classes with TypedDict and Generic support
use std::{collections::btree_map::Entry, fs::create_dir_all, io, path::Path};
use std::{collections::btree_map::Entry, fs::create_dir_all, io, path::PathBuf};
use brk_query::Vecs;
/// Output path configuration for each language client.
///
/// Each path should be the full path to the output file, not just a directory.
/// Parent directories will be created automatically if they don't exist.
///
/// # Example
/// ```ignore
/// let paths = ClientOutputPaths::new()
/// .rust("crates/brk_client/src/lib.rs")
/// .javascript("modules/brk-client/index.js")
/// .python("packages/brk_client/__init__.py");
/// ```
#[derive(Debug, Clone, Default)]
pub struct ClientOutputPaths {
/// Full path to Rust client file (e.g., "crates/brk_client/src/lib.rs")
pub rust: Option<PathBuf>,
/// Full path to JavaScript client file (e.g., "modules/brk-client/index.js")
pub javascript: Option<PathBuf>,
/// Full path to Python client file (e.g., "packages/brk_client/__init__.py")
pub python: Option<PathBuf>,
}
impl ClientOutputPaths {
pub fn new() -> Self {
Self::default()
}
pub fn rust(mut self, path: impl Into<PathBuf>) -> Self {
self.rust = Some(path.into());
self
}
pub fn javascript(mut self, path: impl Into<PathBuf>) -> Self {
self.javascript = Some(path.into());
self
}
pub fn python(mut self, path: impl Into<PathBuf>) -> Self {
self.python = Some(path.into());
self
}
}
mod javascript;
mod js;
mod openapi;
@@ -51,8 +94,25 @@ pub use types::*;
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
/// Generate all client libraries from the query vecs and OpenAPI JSON
pub fn generate_clients(vecs: &Vecs, openapi_json: &str, output_dir: &Path) -> io::Result<()> {
/// Generate all client libraries from the query vecs and OpenAPI JSON.
///
/// Uses `ClientOutputPaths` to specify the output file path for each language.
/// Only languages with a configured path will be generated.
///
/// # Example
/// ```ignore
/// let paths = ClientOutputPaths::new()
/// .rust("crates/brk_client/src/lib.rs")
/// .javascript("modules/brk-client/index.js")
/// .python("packages/brk_client/__init__.py");
///
/// generate_clients(&vecs, &openapi_json, &paths)?;
/// ```
pub fn generate_clients(
vecs: &Vecs,
openapi_json: &str,
output_paths: &ClientOutputPaths,
) -> io::Result<()> {
let metadata = ClientMetadata::from_vecs(vecs);
// Parse OpenAPI spec
@@ -71,19 +131,28 @@ pub fn generate_clients(vecs: &Vecs, openapi_json: &str, output_dir: &Path) -> i
}
// Generate Rust client (uses real brk_types, no schema conversion needed)
let rust_path = output_dir.join("rust");
create_dir_all(&rust_path)?;
generate_rust_client(&metadata, &endpoints, &rust_path)?;
if let Some(rust_path) = &output_paths.rust {
if let Some(parent) = rust_path.parent() {
create_dir_all(parent)?;
}
generate_rust_client(&metadata, &endpoints, rust_path)?;
}
// Generate JavaScript client (needs schemas for type definitions)
let js_path = output_dir.join("javascript");
create_dir_all(&js_path)?;
generate_javascript_client(&metadata, &endpoints, &schemas, &js_path)?;
if let Some(js_path) = &output_paths.javascript {
if let Some(parent) = js_path.parent() {
create_dir_all(parent)?;
}
generate_javascript_client(&metadata, &endpoints, &schemas, js_path)?;
}
// Generate Python client (needs schemas for type definitions)
let python_path = output_dir.join("python");
create_dir_all(&python_path)?;
generate_python_client(&metadata, &endpoints, &schemas, &python_path)?;
if let Some(python_path) = &output_paths.python {
if let Some(parent) = python_path.parent() {
create_dir_all(parent)?;
}
generate_python_client(&metadata, &endpoints, &schemas, python_path)?;
}
Ok(())
}
+5 -5
View File
@@ -17,8 +17,10 @@ pub struct Endpoint {
pub path: String,
/// Operation ID (e.g., "getBlockByHash")
pub operation_id: Option<String>,
/// Summary/description
/// Short summary
pub summary: Option<String>,
/// Detailed description
pub description: Option<String>,
/// Tags for grouping
pub tags: Vec<String>,
/// Path parameters
@@ -185,10 +187,8 @@ fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<E
method: method.to_string(),
path: path.to_string(),
operation_id: operation.operation_id.clone(),
summary: operation
.summary
.clone()
.or_else(|| operation.description.clone()),
summary: operation.summary.clone(),
description: operation.description.clone(),
tags: operation.tags.clone(),
path_params,
query_params,
+22 -7
View File
@@ -13,12 +13,14 @@ use crate::{
get_pattern_instance_base, is_enum_schema, to_pascal_case, to_snake_case,
};
/// Generate Python client from metadata and OpenAPI endpoints
/// 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_dir: &Path,
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
@@ -57,7 +59,7 @@ pub fn generate_python_client(
// Generate main client with tree and API methods
generate_main_client(&mut output, endpoints);
fs::write(output_dir.join("client.py"), output)?;
fs::write(output_path, output)?;
Ok(())
}
@@ -720,14 +722,16 @@ fn generate_tree_class(
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_generic_value_type(&field.rust_type, cf));
.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.as_deref(),
generic_value_type,
);
let field_name_py = to_snake_case(&field.name);
@@ -864,8 +868,19 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
.unwrap();
// Docstring
if let Some(summary) = &endpoint.summary {
writeln!(output, " \"\"\"{}\"\"\"", summary).unwrap();
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
+93 -86
View File
@@ -12,11 +12,13 @@ use crate::{
to_pascal_case, to_snake_case,
};
/// Generate Rust client from metadata and OpenAPI endpoints
/// 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_dir: &Path,
output_path: &Path,
) -> io::Result<()> {
let mut output = String::new();
@@ -47,7 +49,7 @@ pub fn generate_rust_client(
// Generate main client with API methods
generate_main_client(&mut output, endpoints);
fs::write(output_dir.join("client.rs"), output)?;
fs::write(output_path, output)?;
Ok(())
}
@@ -55,7 +57,7 @@ pub fn generate_rust_client(
fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::marker::PhantomData;
r#"use std::sync::Arc;
use serde::de::DeserializeOwned;
use brk_types::*;
@@ -88,14 +90,14 @@ pub type Result<T> = std::result::Result<T, BrkError>;
#[derive(Debug, Clone)]
pub struct BrkClientOptions {{
pub base_url: String,
pub timeout_ms: u64,
pub timeout_secs: u64,
}}
impl Default for BrkClientOptions {{
fn default() -> Self {{
Self {{
base_url: "http://localhost:3000".to_string(),
timeout_ms: 30000,
timeout_secs: 30,
}}
}}
}}
@@ -104,36 +106,41 @@ impl Default for BrkClientOptions {{
#[derive(Debug, Clone)]
pub struct BrkClientBase {{
base_url: String,
client: reqwest::blocking::Client,
timeout_secs: u64,
}}
impl BrkClientBase {{
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Result<Self> {{
let base_url = base_url.into();
let client = reqwest::blocking::Client::new();
Ok(Self {{ base_url, client }})
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) -> Result<Self> {{
let client = reqwest::blocking::Client::builder()
.timeout(std::time::Duration::from_millis(options.timeout_ms))
.build()
.map_err(|e| BrkError {{ message: e.to_string() }})?;
Ok(Self {{
pub fn with_options(options: BrkClientOptions) -> Self {{
Self {{
base_url: options.base_url,
client,
}})
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);
self.client
.get(&url)
let response = minreq::get(&url)
.with_timeout(self.timeout_secs)
.send()
.map_err(|e| BrkError {{ message: e.to_string() }})?
.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() }})
}}
@@ -148,18 +155,18 @@ fn generate_metric_node(output: &mut String) {
writeln!(
output,
r#"/// A metric node that can fetch data for different indexes.
pub struct MetricNode<'a, T> {{
client: &'a BrkClientBase,
pub struct MetricNode<T> {{
client: Arc<BrkClientBase>,
path: String,
_marker: PhantomData<T>,
_marker: std::marker::PhantomData<T>,
}}
impl<'a, T: DeserializeOwned> MetricNode<'a, T> {{
pub fn new(client: &'a BrkClientBase, path: String) -> Self {{
impl<T: DeserializeOwned> MetricNode<T> {{
pub fn new(client: Arc<BrkClientBase>, path: String) -> Self {{
Self {{
client,
path,
_marker: PhantomData,
_marker: std::marker::PhantomData,
}}
}}
@@ -168,7 +175,7 @@ impl<'a, T: DeserializeOwned> MetricNode<'a, T> {{
self.client.get(&self.path)
}}
/// Fetch data points within a date range.
/// Fetch data points within a range.
pub fn get_range(&self, from: &str, to: &str) -> Result<Vec<T>> {{
let path = format!("{{}}?from={{}}&to={{}}", self.path, from, to);
self.client.get(&path)
@@ -195,26 +202,20 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
pattern.indexes.len()
)
.unwrap();
writeln!(output, "pub struct {}<'a, T> {{", pattern.name).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<'a, T>,", field_name).unwrap();
writeln!(output, " pub {}: MetricNode<T>,", field_name).unwrap();
}
writeln!(output, " _marker: PhantomData<T>,").unwrap();
writeln!(output, "}}\n").unwrap();
// Generate impl block with constructor
writeln!(output, "impl<T: DeserializeOwned> {}<T> {{", pattern.name).unwrap();
writeln!(
output,
"impl<'a, T: DeserializeOwned> {}<'a, T> {{",
pattern.name
)
.unwrap();
writeln!(
output,
" pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{"
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
)
.unwrap();
writeln!(output, " Self {{").unwrap();
@@ -224,13 +225,12 @@ fn generate_index_accessors(output: &mut String, patterns: &[IndexSetPattern]) {
let path_segment = index.serialize_long();
writeln!(
output,
" {}: MetricNode::new(client, format!(\"{{base_path}}/{}\")),",
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}/{}\")),",
field_name, path_segment
)
.unwrap();
}
writeln!(output, " _marker: PhantomData,").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, " }}").unwrap();
writeln!(output, "}}\n").unwrap();
@@ -256,11 +256,7 @@ fn generate_pattern_structs(
for pattern in patterns {
let is_parameterizable = pattern.is_parameterizable();
let generic_params = if pattern.is_generic {
"<'a, T>"
} else {
"<'a>"
};
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();
@@ -275,10 +271,15 @@ fn generate_pattern_structs(
writeln!(output, "}}\n").unwrap();
// Generate impl block with constructor
let impl_generic = if pattern.is_generic {
"<T: DeserializeOwned>"
} else {
""
};
writeln!(
output,
"impl{} {}{} {{",
generic_params, pattern.name, generic_params
impl_generic, pattern.name, generic_params
)
.unwrap();
@@ -290,13 +291,13 @@ fn generate_pattern_structs(
.unwrap();
writeln!(
output,
" pub fn new(client: &'a BrkClientBase, acc: &str) -> Self {{"
" pub fn new(client: Arc<BrkClientBase>, acc: &str) -> Self {{"
)
.unwrap();
} else {
writeln!(
output,
" pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{"
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
)
.unwrap();
}
@@ -340,7 +341,7 @@ fn generate_parameterized_rust_field(
writeln!(
output,
" {}: {}::new(client, {}),",
" {}: {}::new(client.clone(), {}),",
field_name, field.rust_type, child_acc
)
.unwrap();
@@ -363,14 +364,14 @@ fn generate_parameterized_rust_field(
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" {}: {}::new(client, &{}),",
" {}: {}::new(client.clone(), &{}),",
field_name, accessor.name, metric_expr
)
.unwrap();
} else {
writeln!(
output,
" {}: MetricNode::new(client, {}),",
" {}: MetricNode::new(client.clone(), {}),",
field_name, metric_expr
)
.unwrap();
@@ -388,7 +389,7 @@ fn generate_tree_path_rust_field(
if metadata.is_pattern_type(&field.rust_type) {
writeln!(
output,
" {}: {}::new(client, &format!(\"{{base_path}}/{}\")),",
" {}: {}::new(client.clone(), &format!(\"{{base_path}}/{}\")),",
field_name, field.rust_type, field.name
)
.unwrap();
@@ -396,14 +397,14 @@ fn generate_tree_path_rust_field(
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
writeln!(
output,
" {}: {}::new(client, &format!(\"{{base_path}}/{}\")),",
" {}: {}::new(client.clone(), &format!(\"{{base_path}}/{}\")),",
field_name, accessor.name, field.name
)
.unwrap();
} else {
writeln!(
output,
" {}: MetricNode::new(client, format!(\"{{base_path}}/{}\")),",
" {}: MetricNode::new(client.clone(), format!(\"{{base_path}}/{}\")),",
field_name, field.name
)
.unwrap();
@@ -441,15 +442,16 @@ fn field_to_type_annotation_with_generic(
if metadata.is_pattern_generic(&field.rust_type)
&& let Some(vt) = generic_value_type
{
return format!("{}<'a, {}>", field.rust_type, vt);
return format!("{}<{}>", field.rust_type, vt);
}
format!("{}<'a>", field.rust_type)
// Non-generic pattern has no type params
field.rust_type.clone()
} else if let Some(accessor) = metadata.find_index_set_pattern(&field.indexes) {
// Leaf with a reusable accessor pattern
format!("{}<'a, {}>", accessor.name, value_type)
format!("{}<{}>", accessor.name, value_type)
} else {
// Leaf with unique index set - use MetricNode directly
format!("MetricNode<'a, {}>", value_type)
format!("MetricNode<{}>", value_type)
}
}
@@ -501,29 +503,27 @@ fn generate_tree_node(
generated.insert(name.to_string());
writeln!(output, "/// Catalog tree node.").unwrap();
writeln!(output, "pub struct {}<'a> {{", name).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_generic_value_type(&field.rust_type, cf));
let type_annotation = field_to_type_annotation_with_generic(
field,
metadata,
false,
generic_value_type.as_deref(),
);
.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();
// Generate impl block
writeln!(output, "impl<'a> {}<'a> {{", name).unwrap();
writeln!(output, "impl {} {{", name).unwrap();
writeln!(
output,
" pub fn new(client: &'a BrkClientBase, base_path: &str) -> Self {{"
" pub fn new(client: Arc<BrkClientBase>, base_path: &str) -> Self {{"
)
.unwrap();
writeln!(output, " Self {{").unwrap();
@@ -538,14 +538,14 @@ fn generate_tree_node(
let metric_base = get_pattern_instance_base(child_node, child_name);
writeln!(
output,
" {}: {}::new(client, \"{}\"),",
" {}: {}::new(client.clone(), \"{}\"),",
field_name, field.rust_type, metric_base
)
.unwrap();
} else {
writeln!(
output,
" {}: {}::new(client, &format!(\"{{base_path}}/{}\")),",
" {}: {}::new(client.clone(), &format!(\"{{base_path}}/{}\")),",
field_name, field.rust_type, field.name
)
.unwrap();
@@ -560,14 +560,14 @@ fn generate_tree_node(
if metric_path.contains("{base_path}") {
writeln!(
output,
" {}: {}::new(client, &format!(\"{}\")),",
" {}: {}::new(client.clone(), &format!(\"{}\")),",
field_name, accessor.name, metric_path
)
.unwrap();
} else {
writeln!(
output,
" {}: {}::new(client, \"{}\"),",
" {}: {}::new(client.clone(), \"{}\"),",
field_name, accessor.name, metric_path
)
.unwrap();
@@ -581,14 +581,14 @@ fn generate_tree_node(
if metric_path.contains("{base_path}") {
writeln!(
output,
" {}: MetricNode::new(client, format!(\"{}\")),",
" {}: MetricNode::new(client.clone(), format!(\"{}\")),",
field_name, metric_path
)
.unwrap();
} else {
writeln!(
output,
" {}: MetricNode::new(client, \"{}\".to_string()),",
" {}: MetricNode::new(client.clone(), \"{}\".to_string()),",
field_name, metric_path
)
.unwrap();
@@ -625,27 +625,28 @@ fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
output,
r#"/// Main BRK client with catalog tree and API methods.
pub struct BrkClient {{
base: BrkClientBase,
base: Arc<BrkClientBase>,
tree: CatalogTree,
}}
impl BrkClient {{
/// Create a new client with the given base URL.
pub fn new(base_url: impl Into<String>) -> Result<Self> {{
Ok(Self {{
base: BrkClientBase::new(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) -> Result<Self> {{
Ok(Self {{
base: BrkClientBase::with_options(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<'_> {{
CatalogTree::new(&self.base, "")
pub fn tree(&self) -> &CatalogTree {{
&self.tree
}}
"#
)
@@ -678,6 +679,12 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
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();
}
// Build method signature
let params = build_method_params(endpoint);
+8 -15
View File
@@ -39,14 +39,16 @@ pub struct ClientMetadata {
/// Index set patterns - sets of indexes that appear together on metrics
pub index_set_patterns: Vec<IndexSetPattern>,
/// Maps concrete field signatures to pattern names
pub concrete_to_pattern: HashMap<Vec<PatternField>, String>,
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) =
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);
@@ -56,6 +58,7 @@ impl ClientMetadata {
used_indexes,
index_set_patterns,
concrete_to_pattern,
concrete_to_type_param,
}
}
@@ -81,19 +84,9 @@ impl ClientMetadata {
self.find_pattern(name).is_some_and(|p| p.is_generic)
}
/// Extract the value type from concrete fields for a generic pattern.
pub fn get_generic_value_type(
&self,
pattern_name: &str,
fields: &[PatternField],
) -> Option<String> {
if !self.is_pattern_generic(pattern_name) {
return None;
}
fields
.iter()
.find(|f| f.is_leaf())
.map(|f| extract_inner_type(&f.rust_type))
/// 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.
+38 -62
View File
@@ -1,19 +1,24 @@
//! Pattern detection for structural patterns in the metric tree.
use std::collections::{BTreeMap, BTreeSet, HashMap};
use std::collections::{BTreeSet, HashMap};
use brk_types::TreeNode;
use super::{
case::to_pascal_case, schema::schema_to_json_type, FieldNamePosition, PatternField,
StructuralPattern,
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_mapping).
/// Returns (patterns, concrete_to_pattern, concrete_to_type_param).
pub fn detect_structural_patterns(
tree: &TreeNode,
) -> (Vec<StructuralPattern>, HashMap<Vec<PatternField>, String>) {
) -> (
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();
@@ -29,8 +34,9 @@ pub fn detect_structural_patterns(
&mut name_counts,
);
// Identify generic patterns
let (generic_patterns, generic_mappings) = detect_generic_patterns(&signature_to_pattern);
// Identify generic patterns (also extracts type params)
let (generic_patterns, generic_mappings, type_mappings) =
detect_generic_patterns(&signature_to_pattern);
// Build non-generic patterns: signatures appearing 2+ times that weren't merged into generics
let mut patterns: Vec<StructuralPattern> = signature_to_pattern
@@ -64,33 +70,43 @@ pub fn detect_structural_patterns(
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)
(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>) {
let mut normalized_groups: HashMap<Vec<PatternField>, Vec<(Vec<PatternField>, String)>> =
HashMap::new();
) -> (
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) = normalize_fields_for_generic(fields) {
if let Some((normalized, extracted_type)) = normalize_fields_for_generic(fields) {
normalized_groups
.entry(normalized)
.or_default()
.push((fields.clone(), name.clone()));
.push((fields.clone(), name.clone(), extracted_type));
}
}
let mut patterns = Vec::new();
let mut mappings: HashMap<Vec<PatternField>, String> = HashMap::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, _) in &group {
mappings.insert(concrete_fields.clone(), generic_name.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,
@@ -101,11 +117,12 @@ fn detect_generic_patterns(
}
}
(patterns, mappings)
(patterns, pattern_mappings, type_mappings)
}
/// Normalize fields by replacing concrete value types with "T".
fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<Vec<PatternField>> {
/// 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())
@@ -137,7 +154,7 @@ fn normalize_fields_for_generic(fields: &[PatternField]) -> Option<Vec<PatternFi
})
.collect();
Some(normalized)
Some((normalized, super::extract_inner_type(first_type)))
}
/// Recursively resolve branch patterns bottom-up.
@@ -266,7 +283,7 @@ fn collect_pattern_instances(
return;
};
let fields = get_node_fields_for_analysis(children, pattern_lookup);
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 {
@@ -283,7 +300,7 @@ fn collect_pattern_instances(
let child_accumulated = match child_node {
TreeNode::Leaf(leaf) => leaf.name().to_string(),
TreeNode::Branch(_) => {
if let Some(desc_leaf_name) = get_descendant_leaf_name(child_node) {
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()
@@ -296,13 +313,6 @@ fn collect_pattern_instances(
}
}
fn get_descendant_leaf_name(node: &TreeNode) -> Option<String> {
match node {
TreeNode::Leaf(leaf) => Some(leaf.name().to_string()),
TreeNode::Branch(children) => children.values().find_map(get_descendant_leaf_name),
}
}
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 {
@@ -324,40 +334,6 @@ fn infer_accumulated_name(parent_acc: &str, field_name: &str, descendant_leaf: &
}
}
fn get_node_fields_for_analysis(
children: &BTreeMap<String, TreeNode>,
pattern_lookup: &HashMap<Vec<PatternField>, String>,
) -> Vec<PatternField> {
let mut fields: Vec<PatternField> = children
.iter()
.map(|(name, node)| {
let (rust_type, json_type, indexes) = match node {
TreeNode::Leaf(leaf) => (
leaf.value_type().to_string(),
schema_to_json_type(&leaf.schema),
leaf.indexes().clone(),
),
TreeNode::Branch(grandchildren) => {
let child_fields = get_node_fields_for_analysis(grandchildren, pattern_lookup);
let pattern_name = pattern_lookup
.get(&child_fields)
.cloned()
.unwrap_or_else(|| "Unknown".to_string());
(pattern_name.clone(), pattern_name, BTreeSet::new())
}
};
PatternField {
name: name.clone(),
rust_type,
json_type,
indexes,
}
})
.collect();
fields.sort_by(|a, b| a.name.cmp(&b.name));
fields
}
fn analyze_field_positions_from_instances(
instances: &[(String, String, String)],
) -> HashMap<String, FieldNamePosition> {
-36
View File
@@ -59,42 +59,6 @@ pub fn get_node_fields(
fields
}
/// Like get_node_fields but takes a parent name for generating child pattern names.
pub fn get_node_fields_with_parent(
children: &BTreeMap<String, TreeNode>,
parent_name: &str,
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(|| format!("{}_{}", parent_name, to_pascal_case(name)));
(pattern_name.clone(), pattern_name, BTreeSet::new())
}
};
PatternField {
name: name.clone(),
rust_type,
json_type,
indexes,
}
})
.collect();
fields.sort_by(|a, b| a.name.cmp(&b.name));
fields
}
/// 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(