mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-07-17 14:08:10 -07:00
global: snapshot
This commit is contained in:
@@ -93,12 +93,41 @@ fn collect_positions_bottom_up(
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a list of positions contains incompatible values.
|
||||
///
|
||||
/// Positions are incompatible if there are multiple different non-Identity positions,
|
||||
/// meaning different pattern instances use different naming conventions.
|
||||
fn has_incompatible_positions(positions: &[FieldNamePosition]) -> bool {
|
||||
let non_identity: Vec<_> = positions
|
||||
.iter()
|
||||
.filter(|p| !matches!(p, FieldNamePosition::Identity))
|
||||
.collect();
|
||||
|
||||
if non_identity.len() <= 1 {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Check if all non-identity positions are the same
|
||||
let first = &non_identity[0];
|
||||
non_identity.iter().skip(1).any(|p| p != first)
|
||||
}
|
||||
|
||||
/// 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`.
|
||||
///
|
||||
/// Returns an empty map if any field has incompatible positions across instances,
|
||||
/// which will cause `is_parameterizable()` to return false for the pattern.
|
||||
fn merge_field_positions(
|
||||
field_positions: &HashMap<String, Vec<FieldNamePosition>>,
|
||||
) -> HashMap<String, FieldNamePosition> {
|
||||
// First check for incompatible positions
|
||||
for positions in field_positions.values() {
|
||||
if has_incompatible_positions(positions) {
|
||||
// Incompatible positions found - pattern cannot be parameterized
|
||||
return HashMap::new();
|
||||
}
|
||||
}
|
||||
|
||||
// All positions are compatible, proceed with merge
|
||||
field_positions
|
||||
.iter()
|
||||
.filter_map(|(field_name, positions)| {
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::collections::{BTreeMap, BTreeSet, HashMap};
|
||||
|
||||
use brk_types::{Index, TreeNode, extract_json_type};
|
||||
|
||||
use crate::analysis::names::analyze_pattern_level;
|
||||
use crate::{IndexSetPattern, PatternField, child_type_name};
|
||||
|
||||
/// Get the first leaf name from a tree node.
|
||||
@@ -116,22 +117,55 @@ fn collect_indexes_from_tree(
|
||||
/// For cohort-level instances, returns the common prefix or suffix 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() {
|
||||
find_common_base(&leaf_names)
|
||||
}
|
||||
|
||||
/// Find the common base from a set of metric names.
|
||||
/// Tries prefix, suffix, then strips first/last segments and retries.
|
||||
fn find_common_base(names: &[String]) -> String {
|
||||
if names.is_empty() {
|
||||
return String::new();
|
||||
}
|
||||
|
||||
// First try to find a common prefix
|
||||
let common_prefix = find_common_prefix_at_underscore(&leaf_names);
|
||||
// Try common prefix
|
||||
let common_prefix = find_common_prefix_at_underscore(names);
|
||||
if !common_prefix.is_empty() {
|
||||
return common_prefix.trim_end_matches('_').to_string();
|
||||
}
|
||||
|
||||
// If no common prefix, try to find a common suffix
|
||||
let common_suffix = find_common_suffix_at_underscore(&leaf_names);
|
||||
// Try common suffix
|
||||
let common_suffix = find_common_suffix_at_underscore(names);
|
||||
if !common_suffix.is_empty() {
|
||||
return common_suffix.trim_start_matches('_').to_string();
|
||||
}
|
||||
|
||||
// If neither works, the common part may be in the middle.
|
||||
// Strip the first underscore segment (varying prefix) and try again.
|
||||
let stripped_prefix: Vec<String> = names
|
||||
.iter()
|
||||
.filter_map(|name| name.split_once('_').map(|(_, rest)| rest.to_string()))
|
||||
.collect();
|
||||
|
||||
if stripped_prefix.len() == names.len() {
|
||||
let common_prefix = find_common_prefix_at_underscore(&stripped_prefix);
|
||||
if !common_prefix.is_empty() {
|
||||
return common_prefix.trim_end_matches('_').to_string();
|
||||
}
|
||||
}
|
||||
|
||||
// Try stripping last segment (varying suffix) and look for common suffix
|
||||
let stripped_suffix: Vec<String> = names
|
||||
.iter()
|
||||
.filter_map(|name| name.rsplit_once('_').map(|(rest, _)| rest.to_string()))
|
||||
.collect();
|
||||
|
||||
if stripped_suffix.len() == names.len() {
|
||||
let common_suffix = find_common_suffix_at_underscore(&stripped_suffix);
|
||||
if !common_suffix.is_empty() {
|
||||
return common_suffix.trim_start_matches('_').to_string();
|
||||
}
|
||||
}
|
||||
|
||||
String::new()
|
||||
}
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ impl LanguageSyntax for PythonSyntax {
|
||||
}
|
||||
|
||||
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
|
||||
format!("f'{{{{{}}}}}{}'", base_var, suffix)
|
||||
format!("f'{{{}}}{}'", base_var, suffix)
|
||||
}
|
||||
|
||||
fn position_expr(&self, pos: &FieldNamePosition, base_var: &str) -> String {
|
||||
@@ -21,20 +21,19 @@ impl LanguageSyntax for PythonSyntax {
|
||||
if let Some(suffix) = s.strip_prefix('_') {
|
||||
format!("_m({}, '{}')", base_var, suffix)
|
||||
} else {
|
||||
format!("f'{{{{{}}}}}{}'", base_var, s)
|
||||
format!("f'{{{}}}{}'", base_var, s)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Prepend(s) => {
|
||||
// Handle empty acc case for prepend
|
||||
// Want to produce: (f'prefix_{acc}' if acc else 'prefix')
|
||||
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
|
||||
"(f'{}{{{}}}' if {} else '{}')",
|
||||
s, base_var, base_var, prefix
|
||||
)
|
||||
} else {
|
||||
format!("f'{}{{{{{}}}}}'", s, base_var)
|
||||
format!("f'{}{{{}}}'" , s, base_var)
|
||||
}
|
||||
}
|
||||
FieldNamePosition::Identity => base_var.to_string(),
|
||||
@@ -80,7 +79,7 @@ impl LanguageSyntax for PythonSyntax {
|
||||
}
|
||||
|
||||
fn index_field_name(&self, index_name: &str) -> String {
|
||||
format!("by_{}", to_snake_case(index_name))
|
||||
to_snake_case(index_name)
|
||||
}
|
||||
|
||||
fn string_literal(&self, value: &str) -> String {
|
||||
|
||||
@@ -5,5 +5,7 @@
|
||||
//! language backends.
|
||||
|
||||
mod fields;
|
||||
mod tree;
|
||||
|
||||
pub use fields::*;
|
||||
pub use tree::*;
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
//! Shared tree generation helpers.
|
||||
|
||||
use std::collections::{HashMap, HashSet};
|
||||
|
||||
use brk_types::TreeNode;
|
||||
|
||||
use crate::{ClientMetadata, PatternField, get_fields_with_child_info};
|
||||
|
||||
/// Context for generating a tree node, returned by `prepare_tree_node`.
|
||||
pub struct TreeNodeContext<'a> {
|
||||
/// The children of the branch node.
|
||||
pub children: &'a std::collections::BTreeMap<String, TreeNode>,
|
||||
/// Fields with optional child field info for generic pattern lookup.
|
||||
pub fields_with_child_info: Vec<(PatternField, Option<Vec<PatternField>>)>,
|
||||
/// Just the fields (for pattern lookup).
|
||||
pub fields: Vec<PatternField>,
|
||||
}
|
||||
|
||||
/// Prepare a tree node for generation.
|
||||
/// Returns None if the node should be skipped (not a branch, already generated,
|
||||
/// or matches a parameterizable pattern).
|
||||
pub fn prepare_tree_node<'a>(
|
||||
node: &'a TreeNode,
|
||||
name: &str,
|
||||
pattern_lookup: &HashMap<Vec<PatternField>, String>,
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) -> Option<TreeNodeContext<'a>> {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
return None;
|
||||
};
|
||||
|
||||
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 parameterizable pattern
|
||||
if let Some(pattern_name) = pattern_lookup.get(&fields)
|
||||
&& pattern_name != name
|
||||
&& metadata.is_parameterizable(pattern_name)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
// Skip if already generated
|
||||
if generated.contains(name) {
|
||||
return None;
|
||||
}
|
||||
generated.insert(name.to_string());
|
||||
|
||||
Some(TreeNodeContext {
|
||||
children,
|
||||
fields_with_child_info,
|
||||
fields,
|
||||
})
|
||||
}
|
||||
@@ -21,24 +21,29 @@ pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
|
||||
if let Some(desc) = &endpoint.description
|
||||
&& endpoint.summary.as_ref() != Some(desc)
|
||||
{
|
||||
writeln!(output, " * @description {}", desc).unwrap();
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(output, " * {}", desc).unwrap();
|
||||
}
|
||||
|
||||
if !endpoint.path_params.is_empty() || !endpoint.query_params.is_empty() {
|
||||
writeln!(output, " *").unwrap();
|
||||
}
|
||||
|
||||
for param in &endpoint.path_params {
|
||||
let desc = param.description.as_deref().unwrap_or("");
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}}} {} {}",
|
||||
" * @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("");
|
||||
let desc = format_param_desc(param.description.as_deref());
|
||||
writeln!(
|
||||
output,
|
||||
" * @param {{{}{}}} [{}] {}",
|
||||
" * @param {{{}{}}} [{}]{}",
|
||||
param.param_type, optional, param.name, desc
|
||||
)
|
||||
.unwrap();
|
||||
@@ -119,3 +124,11 @@ fn normalize_return_type(return_type: &str) -> String {
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
/// Format param description with dash prefix, or empty string if no description.
|
||||
fn format_param_desc(desc: Option<&str>) -> String {
|
||||
match desc {
|
||||
Some(d) if !d.is_empty() => format!(" - {}", d),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,8 @@ 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,
|
||||
ClientMetadata, Endpoint, PatternField, child_type_name, get_first_leaf_name, get_node_fields,
|
||||
get_pattern_instance_base, infer_accumulated_name, prepare_tree_node, to_camel_case,
|
||||
};
|
||||
|
||||
use super::api::generate_api_methods;
|
||||
@@ -38,36 +37,23 @@ fn generate_tree_typedef(
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) 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);
|
||||
for ((field, child_fields), (child_name, _)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let js_type = metadata.resolve_tree_field_type(
|
||||
child_fields.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
|generic| field_type_with_generic(field, metadata, false, generic),
|
||||
);
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}}} {}",
|
||||
@@ -79,10 +65,11 @@ fn generate_tree_typedef(
|
||||
|
||||
writeln!(output, " */\n").unwrap();
|
||||
|
||||
for (child_name, child_node) in children {
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
if !pattern_lookup.contains_key(&child_fields) {
|
||||
// Generate typedef if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_type = child_type_name(name, child_name);
|
||||
generate_tree_typedef(
|
||||
output,
|
||||
@@ -183,22 +170,13 @@ fn generate_tree_initializer(
|
||||
}
|
||||
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)
|
||||
};
|
||||
// Only use pattern factory if pattern is parameterizable
|
||||
let pattern_name = pattern_lookup
|
||||
.get(&child_fields)
|
||||
.filter(|name| metadata.is_parameterizable(name));
|
||||
|
||||
if let Some(pattern_name) = pattern_name {
|
||||
let arg = get_pattern_instance_base(child_node);
|
||||
writeln!(
|
||||
output,
|
||||
"{}{}: create{}(this, '{}'){}",
|
||||
|
||||
@@ -21,10 +21,24 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
|
||||
let js_type = schema_to_js_type(schema, Some(name));
|
||||
|
||||
let type_desc = schema.get("description").and_then(|d| d.as_str());
|
||||
|
||||
if is_primitive_alias(schema) {
|
||||
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
|
||||
if let Some(desc) = type_desc {
|
||||
writeln!(output, "/**").unwrap();
|
||||
write_jsdoc_description(output, desc);
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(output, " * @typedef {{{}}} {}", js_type, name).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
} else {
|
||||
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
|
||||
}
|
||||
} else if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
|
||||
writeln!(output, "/**").unwrap();
|
||||
if let Some(desc) = type_desc {
|
||||
write_jsdoc_description(output, desc);
|
||||
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));
|
||||
@@ -35,14 +49,25 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
.unwrap_or(false);
|
||||
let optional = if required { "" } else { "=" };
|
||||
let safe_name = to_camel_case(prop_name);
|
||||
let prop_desc = prop_schema
|
||||
.get("description")
|
||||
.and_then(|d| d.as_str())
|
||||
.map(|d| format!(" - {}", d))
|
||||
.unwrap_or_default();
|
||||
writeln!(
|
||||
output,
|
||||
" * @property {{{}{}}} {}",
|
||||
prop_type, optional, safe_name
|
||||
" * @property {{{}{}}} {}{}",
|
||||
prop_type, optional, safe_name, prop_desc
|
||||
)
|
||||
.unwrap();
|
||||
}
|
||||
writeln!(output, " */").unwrap();
|
||||
} else if let Some(desc) = type_desc {
|
||||
writeln!(output, "/**").unwrap();
|
||||
write_jsdoc_description(output, desc);
|
||||
writeln!(output, " *").unwrap();
|
||||
writeln!(output, " * @typedef {{{}}} {}", js_type, name).unwrap();
|
||||
writeln!(output, " */").unwrap();
|
||||
} else {
|
||||
writeln!(output, "/** @typedef {{{}}} {} */", js_type, name).unwrap();
|
||||
}
|
||||
@@ -50,6 +75,17 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
|
||||
/// Write a multi-line description with proper JSDoc formatting.
|
||||
fn write_jsdoc_description(output: &mut String, desc: &str) {
|
||||
for line in desc.lines() {
|
||||
if line.is_empty() {
|
||||
writeln!(output, " *").unwrap();
|
||||
} else {
|
||||
writeln!(output, " * {}", line).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_primitive_alias(schema: &Value) -> bool {
|
||||
schema.get("properties").is_none()
|
||||
&& schema.get("items").is_none()
|
||||
|
||||
@@ -27,7 +27,7 @@ pub fn generate_python_client(
|
||||
writeln!(output, "from __future__ import annotations").unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Final, Union, Protocol"
|
||||
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict, Union, Protocol"
|
||||
)
|
||||
.unwrap();
|
||||
writeln!(output, "import httpx\n").unwrap();
|
||||
|
||||
@@ -6,8 +6,8 @@ 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,
|
||||
ClientMetadata, PatternField, child_type_name, get_node_fields, get_pattern_instance_base,
|
||||
prepare_tree_node, to_snake_case,
|
||||
};
|
||||
|
||||
use super::client::field_type_with_generic;
|
||||
@@ -37,28 +37,10 @@ fn generate_tree_class(
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) 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();
|
||||
@@ -69,7 +51,7 @@ fn generate_tree_class(
|
||||
.unwrap();
|
||||
|
||||
for ((field, child_fields_opt), (_child_name, child_node)) in
|
||||
fields_with_child_info.iter().zip(children.iter())
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
// Look up type parameter for generic patterns
|
||||
let generic_value_type = child_fields_opt
|
||||
@@ -79,44 +61,35 @@ fn generate_tree_class(
|
||||
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) {
|
||||
if metadata.is_pattern_type(&field.rust_type) && metadata.is_parameterizable(&field.rust_type)
|
||||
{
|
||||
// Parameterizable pattern: use pattern class with metric base
|
||||
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 if let TreeNode::Leaf(leaf) = child_node {
|
||||
// Leaf node: use actual metric name
|
||||
let accessor = metadata.find_index_set_pattern(&field.indexes).unwrap();
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, accessor.name, field.name
|
||||
" self.{}: {} = {}(client, '{}')",
|
||||
field_name_py, py_type, accessor.name, leaf.name()
|
||||
)
|
||||
.unwrap();
|
||||
} else if field.is_branch() {
|
||||
// Non-pattern branch - instantiate the nested class
|
||||
// Non-parameterizable pattern or regular branch: generate inline class
|
||||
let inline_class = child_type_name(name, &field.name);
|
||||
writeln!(
|
||||
output,
|
||||
" self.{}: {} = {}(client, f'{{base_path}}_{}')",
|
||||
field_name_py, py_type, field.rust_type, field.name
|
||||
" self.{}: {} = {}(client)",
|
||||
field_name_py, inline_class, inline_class
|
||||
)
|
||||
.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
|
||||
@@ -127,10 +100,12 @@ fn generate_tree_class(
|
||||
writeln!(output).unwrap();
|
||||
|
||||
// Generate child classes
|
||||
for (child_name, child_node) in children {
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
if !pattern_lookup.contains_key(&child_fields) {
|
||||
|
||||
// Generate inline class if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_class = child_type_name(name, child_name);
|
||||
generate_tree_class(
|
||||
output,
|
||||
|
||||
@@ -25,8 +25,44 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
let Some(schema) = schemas.get(&name) else {
|
||||
continue;
|
||||
};
|
||||
let type_desc = schema.get("description").and_then(|d| d.as_str());
|
||||
|
||||
if let Some(props) = schema.get("properties").and_then(|p| p.as_object()) {
|
||||
writeln!(output, "class {}(TypedDict):", name).unwrap();
|
||||
|
||||
// Collect field descriptions for Attributes section
|
||||
let field_docs: Vec<(String, Option<&str>)> = props
|
||||
.iter()
|
||||
.map(|(prop_name, prop_schema)| {
|
||||
let safe_name = escape_python_keyword(prop_name);
|
||||
let desc = prop_schema.get("description").and_then(|d| d.as_str());
|
||||
(safe_name, desc)
|
||||
})
|
||||
.collect();
|
||||
let has_field_docs = field_docs.iter().any(|(_, d)| d.is_some());
|
||||
|
||||
// Generate docstring if we have type description or field descriptions
|
||||
if type_desc.is_some() || has_field_docs {
|
||||
writeln!(output, " \"\"\"").unwrap();
|
||||
if let Some(desc) = type_desc {
|
||||
for line in desc.lines() {
|
||||
writeln!(output, " {}", line).unwrap();
|
||||
}
|
||||
}
|
||||
if has_field_docs {
|
||||
if type_desc.is_some() {
|
||||
writeln!(output).unwrap();
|
||||
}
|
||||
writeln!(output, " Attributes:").unwrap();
|
||||
for (field_name, desc) in &field_docs {
|
||||
if let Some(d) = desc {
|
||||
writeln!(output, " {}: {}", field_name, d).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
writeln!(output, " \"\"\"").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);
|
||||
@@ -35,6 +71,11 @@ pub fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
|
||||
writeln!(output).unwrap();
|
||||
} else {
|
||||
let py_type = schema_to_python_type_ctx(schema, Some(&name));
|
||||
if let Some(desc) = type_desc {
|
||||
for line in desc.lines() {
|
||||
writeln!(output, "# {}", line).unwrap();
|
||||
}
|
||||
}
|
||||
writeln!(output, "{} = {}", name, py_type).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ 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,
|
||||
ClientMetadata, LanguageSyntax, PatternField, RustSyntax, child_type_name,
|
||||
generate_tree_node_field, get_node_fields, get_pattern_instance_base, prepare_tree_node,
|
||||
to_snake_case,
|
||||
};
|
||||
|
||||
use super::client::field_type_with_generic;
|
||||
@@ -36,38 +37,23 @@ fn generate_tree_node(
|
||||
metadata: &ClientMetadata,
|
||||
generated: &mut HashSet<String>,
|
||||
) {
|
||||
let TreeNode::Branch(children) = node else {
|
||||
let Some(ctx) = prepare_tree_node(node, name, pattern_lookup, metadata, generated) 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 {
|
||||
for ((field, child_fields), (child_name, _)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
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);
|
||||
let type_annotation = metadata.resolve_tree_field_type(
|
||||
child_fields.as_deref(),
|
||||
name,
|
||||
child_name,
|
||||
|generic| field_type_with_generic(field, metadata, false, generic),
|
||||
);
|
||||
writeln!(output, " pub {}: {},", field_name, type_annotation).unwrap();
|
||||
}
|
||||
|
||||
@@ -82,29 +68,61 @@ fn generate_tree_node(
|
||||
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
|
||||
}
|
||||
for ((field_info, child_fields), (child_name, child_node)) in
|
||||
ctx.fields_with_child_info.iter().zip(ctx.children.iter())
|
||||
{
|
||||
let field_name = to_snake_case(&field_info.name);
|
||||
|
||||
// Check if this is a pattern type and if it's parameterizable
|
||||
let is_parameterizable = child_fields
|
||||
.as_ref()
|
||||
.is_some_and(|cf| metadata.is_parameterizable_fields(cf));
|
||||
|
||||
if metadata.is_pattern_type(&field_info.rust_type) && is_parameterizable {
|
||||
// Parameterizable pattern: use pattern constructor with metric base
|
||||
let pattern_base = get_pattern_instance_base(child_node);
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
field_info,
|
||||
metadata,
|
||||
" ",
|
||||
child_name,
|
||||
Some(&pattern_base),
|
||||
);
|
||||
} else if child_fields.is_some() {
|
||||
// Non-parameterizable pattern or regular branch: use inline struct
|
||||
let child_struct = child_type_name(name, child_name);
|
||||
let path_expr = syntax.path_expr("base_path", &format!("_{}", child_name));
|
||||
writeln!(
|
||||
output,
|
||||
" {}: {}::new(client.clone(), {}),",
|
||||
field_name, child_struct, path_expr
|
||||
)
|
||||
.unwrap();
|
||||
} else {
|
||||
None
|
||||
};
|
||||
generate_tree_node_field(output, &syntax, field, metadata, " ", child_name, pattern_base.as_deref());
|
||||
// Leaf field
|
||||
generate_tree_node_field(
|
||||
output,
|
||||
&syntax,
|
||||
field_info,
|
||||
metadata,
|
||||
" ",
|
||||
child_name,
|
||||
None,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, " }}").unwrap();
|
||||
writeln!(output, "}}\n").unwrap();
|
||||
|
||||
for (child_name, child_node) in children {
|
||||
for (child_name, child_node) in ctx.children {
|
||||
if let TreeNode::Branch(grandchildren) = child_node {
|
||||
let child_fields = get_node_fields(grandchildren, pattern_lookup);
|
||||
if !pattern_lookup.contains_key(&child_fields) {
|
||||
// Generate child struct if no pattern match OR pattern is not parameterizable
|
||||
if !metadata.is_parameterizable_fields(&child_fields) {
|
||||
let child_struct = child_type_name(name, child_name);
|
||||
generate_tree_node(
|
||||
output,
|
||||
|
||||
@@ -54,9 +54,9 @@ pub fn to_camel_case(s: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> by_dateindex).
|
||||
/// Convert an Index to a snake_case field name (e.g., DateIndex -> dateindex).
|
||||
pub fn index_to_field_name(index: &Index) -> String {
|
||||
format!("by_{}", to_snake_case(index.serialize_long()))
|
||||
to_snake_case(index.serialize_long())
|
||||
}
|
||||
|
||||
/// Generate a child type/struct/class name (e.g., ParentName + child_name -> ParentName_ChildName).
|
||||
|
||||
@@ -65,6 +65,48 @@ impl ClientMetadata {
|
||||
self.find_pattern(name).is_some_and(|p| p.is_generic)
|
||||
}
|
||||
|
||||
/// Check if a pattern by name is parameterizable.
|
||||
pub fn is_parameterizable(&self, name: &str) -> bool {
|
||||
self.find_pattern(name).is_some_and(|p| p.is_parameterizable())
|
||||
}
|
||||
|
||||
/// Check if child fields match a parameterizable pattern.
|
||||
/// Returns true only if the fields match a pattern AND that pattern is parameterizable.
|
||||
pub fn is_parameterizable_fields(&self, fields: &[PatternField]) -> bool {
|
||||
self.concrete_to_pattern
|
||||
.get(fields)
|
||||
.or_else(|| {
|
||||
self.structural_patterns
|
||||
.iter()
|
||||
.find(|p| p.fields == fields)
|
||||
.map(|p| &p.name)
|
||||
})
|
||||
.is_some_and(|name| self.is_parameterizable(name))
|
||||
}
|
||||
|
||||
/// Resolve the type name for a tree field, considering parameterizability.
|
||||
/// If the field matches a parameterizable pattern, returns type annotation from callback.
|
||||
/// Otherwise returns the inline type name (parent_child format).
|
||||
pub fn resolve_tree_field_type<F>(
|
||||
&self,
|
||||
child_fields: Option<&[PatternField]>,
|
||||
parent_name: &str,
|
||||
child_name: &str,
|
||||
type_annotation_fn: F,
|
||||
) -> String
|
||||
where
|
||||
F: FnOnce(Option<&str>) -> String,
|
||||
{
|
||||
match child_fields {
|
||||
Some(cf) if self.is_parameterizable_fields(cf) => {
|
||||
let generic_value_type = self.get_type_param(cf).map(String::as_str);
|
||||
type_annotation_fn(generic_value_type)
|
||||
}
|
||||
Some(_) => crate::child_type_name(parent_name, child_name),
|
||||
None => type_annotation_fn(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
|
||||
@@ -81,6 +81,7 @@ impl std::hash::Hash for PatternField {
|
||||
self.name.hash(state);
|
||||
self.rust_type.hash(state);
|
||||
self.json_type.hash(state);
|
||||
self.indexes.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -89,6 +90,7 @@ impl PartialEq for PatternField {
|
||||
self.name == other.name
|
||||
&& self.rust_type == other.rust_type
|
||||
&& self.json_type == other.json_type
|
||||
&& self.indexes == other.indexes
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user