bindgen: everything works

This commit is contained in:
nym21
2026-03-16 11:37:53 +01:00
parent d3721b0020
commit b74319bf10
21 changed files with 189 additions and 96 deletions
@@ -92,8 +92,7 @@ impl LanguageSyntax for JavaScriptSyntax {
// "ratio_{disc}_bps" → split on {disc} → _m(_m(acc, 'ratio'), disc) then _bps
// But this is complex. For embedded disc, use string interpolation.
// For suffix disc (ends with {disc}), use _m composition.
if template.ends_with("{disc}") {
let static_part = &template[..template.len() - "{disc}".len()];
if let Some(static_part) = template.strip_suffix("{disc}") {
if static_part.is_empty() {
format!("_m({}, disc)", var_name)
} else {
+2 -2
View File
@@ -1,6 +1,6 @@
//! Rust language syntax implementation.
use crate::{GenericSyntax, LanguageSyntax, to_snake_case};
use crate::{GenericSyntax, LanguageSyntax, escape_rust_keyword, to_snake_case};
/// Rust-specific code generation syntax.
pub struct RustSyntax;
@@ -15,7 +15,7 @@ fn escape_rust_format(template: &str) -> String {
impl LanguageSyntax for RustSyntax {
fn field_name(&self, name: &str) -> String {
to_snake_case(name)
escape_rust_keyword(&to_snake_case(name))
}
fn path_expr(&self, base_var: &str, suffix: &str) -> String {
@@ -183,6 +183,7 @@ function _wrapMetricData(raw) {{
* @typedef {{Object}} MetricDataBase
* @property {{number}} version - Version of the metric data
* @property {{Index}} index - The index type used for this query
* @property {{string}} type - Value type (e.g. "f32", "u64", "Sats")
* @property {{number}} total - Total number of data points
* @property {{number}} start - Start index (inclusive)
* @property {{number}} end - End index (exclusive)
@@ -24,7 +24,7 @@ pub fn generate_tree_typedefs(output: &mut String, catalog: &TreeNode, metadata:
"MetricsTree",
"",
catalog,
&pattern_lookup,
pattern_lookup,
metadata,
&mut generated,
);
@@ -125,7 +125,7 @@ pub fn generate_main_client(
"MetricsTree",
"",
3,
&pattern_lookup,
pattern_lookup,
metadata,
&mut generated,
);
@@ -220,6 +220,7 @@ class MetricData(Generic[T]):
"""Metric data with range information. Always int-indexed."""
version: int
index: Index
type: str
total: int
start: int
end: int
@@ -21,7 +21,7 @@ pub fn generate_tree_classes(output: &mut String, catalog: &TreeNode, metadata:
"MetricsTree",
"",
catalog,
&pattern_lookup,
pattern_lookup,
metadata,
&mut generated,
);
@@ -4,15 +4,14 @@ use std::fmt::Write;
use crate::{
ClientMetadata, GenericSyntax, IndexSetPattern, RustSyntax, StructuralPattern,
generate_parameterized_field, index_to_field_name, to_snake_case,
escape_rust_keyword, generate_parameterized_field, index_to_field_name, to_snake_case,
};
/// Generate import statements.
pub fn generate_imports(output: &mut String) {
writeln!(
output,
r#"use std::io::Read as _;
use std::sync::Arc;
r#"use std::sync::Arc;
use std::ops::{{Bound, RangeBounds}};
use serde::de::DeserializeOwned;
pub use brk_cohort::*;
@@ -81,40 +80,27 @@ impl BrkClientBase {{
.into();
Self {{
agent,
base_url: options.base_url,
base_url: options.base_url.trim_end_matches('/').to_string(),
}}
}}
fn get(&self, path: &str) -> Result<Vec<u8>> {{
let base = self.base_url.trim_end_matches('/');
let url = format!("{{}}{{}}", base, path);
let mut response = self.agent.get(&url)
.call()
.map_err(|e| BrkError {{ message: e.to_string() }})?;
if response.status().as_u16() >= 400 {{
return Err(BrkError {{
message: format!("HTTP {{}}", response.status().as_u16()),
}});
}}
let mut bytes = Vec::new();
response.body_mut().as_reader().read_to_end(&mut bytes)
.map_err(|e| BrkError {{ message: e.to_string() }})?;
Ok(bytes)
fn url(&self, path: &str) -> String {{
format!("{{}}{{}}", self.base_url, path)
}}
/// Make a GET request and deserialize JSON response.
pub fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {{
let bytes = self.get(path)?;
serde_json::from_slice(&bytes)
self.agent.get(&self.url(path))
.call()
.and_then(|mut r| r.body_mut().read_json())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
/// Make a GET request and return raw text response.
pub fn get_text(&self, path: &str) -> Result<String> {{
let bytes = self.get(path)?;
String::from_utf8(bytes)
self.agent.get(&self.url(path))
.call()
.and_then(|mut r| r.body_mut().read_to_string())
.map_err(|e| BrkError {{ message: e.to_string() }})
}}
}}
@@ -525,7 +511,7 @@ pub fn generate_pattern_structs(
writeln!(output, "pub struct {}{} {{", pattern.name, generic_params).unwrap();
for field in &pattern.fields {
let field_name = to_snake_case(&field.name);
let field_name = escape_rust_keyword(&to_snake_case(&field.name));
let type_annotation = metadata.field_type_annotation(
field,
pattern.is_generic,
@@ -7,7 +7,8 @@ use brk_types::TreeNode;
use crate::{
ClientMetadata, GenericSyntax, LanguageSyntax, PatternField, RustSyntax, build_child_path,
generate_leaf_field, generate_tree_node_field, prepare_tree_node, to_snake_case,
escape_rust_keyword, generate_leaf_field, generate_tree_node_field, prepare_tree_node,
to_snake_case,
};
/// Generate tree structs.
@@ -21,7 +22,7 @@ pub fn generate_tree(output: &mut String, catalog: &TreeNode, metadata: &ClientM
"MetricsTree",
"",
catalog,
&pattern_lookup,
pattern_lookup,
metadata,
&mut generated,
);
@@ -45,7 +46,7 @@ fn generate_tree_node(
writeln!(output, "pub struct {} {{", name).unwrap();
for child in &ctx.children {
let field_name = to_snake_case(child.name);
let field_name = escape_rust_keyword(&to_snake_case(child.name));
let type_annotation = if child.should_inline {
child.inline_type_name.clone()
} else {
@@ -67,7 +68,7 @@ fn generate_tree_node(
let syntax = RustSyntax;
for child in &ctx.children {
let field_name = to_snake_case(child.name);
let field_name = escape_rust_keyword(&to_snake_case(child.name));
if child.is_leaf {
if let TreeNode::Leaf(leaf) = child.node {
+9 -9
View File
@@ -14,25 +14,25 @@ pub fn to_pascal_case(s: &str) -> String {
.collect()
}
/// Convert a string to snake_case, handling Rust keywords.
/// Convert a string to snake_case (no keyword escaping — backends handle that).
pub fn to_snake_case(s: &str) -> String {
// Convert to lowercase and replace dashes with underscores
let sanitized = s.to_lowercase().replace('-', "_");
// Prefix with _ if starts with digit
let sanitized = if sanitized.chars().next().is_some_and(|c| c.is_ascii_digit()) {
if sanitized.chars().next().is_some_and(|c| c.is_ascii_digit()) {
format!("_{}", sanitized)
} else {
sanitized
};
}
}
// Handle Rust keywords
match sanitized.as_str() {
/// Escape Rust reserved keywords with `_` suffix (consistent with Python).
pub fn escape_rust_keyword(name: &str) -> String {
match name {
"type" | "const" | "static" | "match" | "if" | "else" | "loop" | "while" | "for"
| "break" | "continue" | "return" | "fn" | "let" | "mut" | "ref" | "self" | "super"
| "mod" | "use" | "pub" | "crate" | "extern" | "impl" | "trait" | "struct" | "enum"
| "where" | "async" | "await" | "dyn" | "move" => format!("r#{}", sanitized),
_ => sanitized,
| "where" | "async" | "await" | "dyn" | "move" => format!("{}_", name),
_ => name.to_string(),
}
}
+4 -4
View File
@@ -74,10 +74,10 @@ impl StructuralPattern {
// Find a template with {disc} and extract the disc from the instance value.
// Strip leading underscore since _m() handles separators.
for (field_name, template) in templates {
if let Some(value) = instance_field_parts.get(field_name) {
if let Some(disc) = extract_disc(template, value) {
return Some(disc.trim_start_matches('_').to_string());
}
if let Some(value) = instance_field_parts.get(field_name)
&& let Some(disc) = extract_disc(template, value)
{
return Some(disc.trim_start_matches('_').to_string());
}
}
// If no template matched (all empty templates), disc is empty
+15 -29
View File
@@ -7,7 +7,6 @@
#![allow(clippy::useless_format)]
#![allow(clippy::unnecessary_to_owned)]
use std::io::Read as _;
use std::sync::Arc;
use std::ops::{Bound, RangeBounds};
use serde::de::DeserializeOwned;
@@ -69,40 +68,27 @@ impl BrkClientBase {
.into();
Self {
agent,
base_url: options.base_url,
base_url: options.base_url.trim_end_matches('/').to_string(),
}
}
fn get(&self, path: &str) -> Result<Vec<u8>> {
let base = self.base_url.trim_end_matches('/');
let url = format!("{}{}", base, path);
let mut response = self.agent.get(&url)
.call()
.map_err(|e| BrkError { message: e.to_string() })?;
if response.status().as_u16() >= 400 {
return Err(BrkError {
message: format!("HTTP {}", response.status().as_u16()),
});
}
let mut bytes = Vec::new();
response.body_mut().as_reader().read_to_end(&mut bytes)
.map_err(|e| BrkError { message: e.to_string() })?;
Ok(bytes)
fn url(&self, path: &str) -> String {
format!("{}{}", self.base_url, path)
}
/// Make a GET request and deserialize JSON response.
pub fn get_json<T: DeserializeOwned>(&self, path: &str) -> Result<T> {
let bytes = self.get(path)?;
serde_json::from_slice(&bytes)
self.agent.get(&self.url(path))
.call()
.and_then(|mut r| r.body_mut().read_json())
.map_err(|e| BrkError { message: e.to_string() })
}
/// Make a GET request and return raw text response.
pub fn get_text(&self, path: &str) -> Result<String> {
let bytes = self.get(path)?;
String::from_utf8(bytes)
self.agent.get(&self.url(path))
.call()
.and_then(|mut r| r.body_mut().read_to_string())
.map_err(|e| BrkError { message: e.to_string() })
}
}
@@ -5397,7 +5383,7 @@ impl MetricsTree_Market_Dca {
pub struct MetricsTree_Market_Dca_Period {
pub stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3,
pub cost_basis: MetricsTree_Market_Dca_Period_CostBasis,
pub r#return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2,
pub return_: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2,
pub cagr: _10y2y3y4y5y6y8yPattern,
pub lump_sum_stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3,
pub lump_sum_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2,
@@ -5408,7 +5394,7 @@ impl MetricsTree_Market_Dca_Period {
Self {
stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3::new(client.clone(), "dca_stack".to_string()),
cost_basis: MetricsTree_Market_Dca_Period_CostBasis::new(client.clone(), format!("{base_path}_cost_basis")),
r#return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_return".to_string()),
return_: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "dca_return".to_string()),
cagr: _10y2y3y4y5y6y8yPattern::new(client.clone(), "dca_cagr".to_string()),
lump_sum_stack: _10y1m1w1y2y3m3y4y5y6m6y8yPattern3::new(client.clone(), "lump_sum_stack".to_string()),
lump_sum_return: _10y1m1w1y2y3m3y4y5y6m6y8yPattern2::new(client.clone(), "lump_sum_return".to_string()),
@@ -5455,7 +5441,7 @@ impl MetricsTree_Market_Dca_Period_CostBasis {
pub struct MetricsTree_Market_Dca_Class {
pub stack: MetricsTree_Market_Dca_Class_Stack,
pub cost_basis: MetricsTree_Market_Dca_Class_CostBasis,
pub r#return: MetricsTree_Market_Dca_Class_Return,
pub return_: MetricsTree_Market_Dca_Class_Return,
}
impl MetricsTree_Market_Dca_Class {
@@ -5463,7 +5449,7 @@ impl MetricsTree_Market_Dca_Class {
Self {
stack: MetricsTree_Market_Dca_Class_Stack::new(client.clone(), format!("{base_path}_stack")),
cost_basis: MetricsTree_Market_Dca_Class_CostBasis::new(client.clone(), format!("{base_path}_cost_basis")),
r#return: MetricsTree_Market_Dca_Class_Return::new(client.clone(), format!("{base_path}_return")),
return_: MetricsTree_Market_Dca_Class_Return::new(client.clone(), format!("{base_path}_return")),
}
}
}
@@ -6232,7 +6218,7 @@ pub struct MetricsTree_Cohorts_Utxo {
pub over_amount: MetricsTree_Cohorts_Utxo_OverAmount,
pub amount_range: MetricsTree_Cohorts_Utxo_AmountRange,
pub under_amount: MetricsTree_Cohorts_Utxo_UnderAmount,
pub r#type: MetricsTree_Cohorts_Utxo_Type,
pub type_: MetricsTree_Cohorts_Utxo_Type,
pub profitability: MetricsTree_Cohorts_Utxo_Profitability,
pub matured: MetricsTree_Cohorts_Utxo_Matured,
}
@@ -6251,7 +6237,7 @@ impl MetricsTree_Cohorts_Utxo {
over_amount: MetricsTree_Cohorts_Utxo_OverAmount::new(client.clone(), format!("{base_path}_over_amount")),
amount_range: MetricsTree_Cohorts_Utxo_AmountRange::new(client.clone(), format!("{base_path}_amount_range")),
under_amount: MetricsTree_Cohorts_Utxo_UnderAmount::new(client.clone(), format!("{base_path}_under_amount")),
r#type: MetricsTree_Cohorts_Utxo_Type::new(client.clone(), format!("{base_path}_type")),
type_: MetricsTree_Cohorts_Utxo_Type::new(client.clone(), format!("{base_path}_type")),
profitability: MetricsTree_Cohorts_Utxo_Profitability::new(client.clone(), format!("{base_path}_profitability")),
matured: MetricsTree_Cohorts_Utxo_Matured::new(client.clone(), format!("{base_path}_matured")),
}
+3 -2
View File
@@ -3,7 +3,8 @@ use brk_fetcher::Kraken;
fn main() -> Result<()> {
brk_logger::init(None)?;
let _ = dbg!(Kraken::fetch_1d());
let _ = dbg!(Kraken::fetch_1mn());
let kraken = Kraken::new();
let _ = dbg!(kraken.fetch_1d());
let _ = dbg!(kraken.fetch_1mn());
Ok(())
}
+8 -5
View File
@@ -5,22 +5,25 @@ use brk_types::{Date, Height};
fn main() -> Result<()> {
brk_logger::init(None)?;
let mut brk = BRK::default();
let mut brk = BRK::new();
dbg!(brk.get_from_height(Height::new(900_000))?);
dbg!(brk.get_from_date(Date::new(2025, 6, 7))?);
let mut fetcher = Fetcher::new(None)?;
let _ = Binance::fetch_1d().map(|b| {
let binance = Binance::new(None);
let kraken = Kraken::new();
let _ = binance.fetch_1d().map(|b| {
dbg!(b.last_key_value());
});
let _ = Kraken::fetch_1d().map(|b| {
let _ = kraken.fetch_1d().map(|b| {
dbg!(b.last_key_value());
});
let _ = Binance::fetch_1mn().map(|b| {
let _ = binance.fetch_1mn().map(|b| {
dbg!(b.last_key_value());
});
let _ = Kraken::fetch_1mn().map(|b| {
let _ = kraken.fetch_1mn().map(|b| {
dbg!(b.last_key_value());
});
+5 -1
View File
@@ -26,7 +26,11 @@ pub struct Binance {
}
impl Binance {
pub fn new(path: Option<&Path>, agent: Agent) -> Self {
pub fn new(path: Option<&Path>) -> Self {
Self::new_with_agent(path, crate::new_agent(30))
}
pub fn new_with_agent(path: Option<&Path>, agent: Agent) -> Self {
Self {
agent,
path: path.map(|p| p.to_owned()),
+5 -1
View File
@@ -19,7 +19,11 @@ pub struct BRK {
}
impl BRK {
pub fn new(agent: Agent) -> Self {
pub fn new() -> Self {
Self::new_with_agent(crate::new_agent(30))
}
pub fn new_with_agent(agent: Agent) -> Self {
Self {
agent,
height_to_ohlc: BTreeMap::new(),
+5 -1
View File
@@ -19,7 +19,11 @@ pub struct Kraken {
}
impl Kraken {
pub fn new(agent: Agent) -> Self {
pub fn new() -> Self {
Self::new_with_agent(crate::new_agent(30))
}
pub fn new_with_agent(agent: Agent) -> Self {
Self {
agent,
_1mn: None,
+5 -3
View File
@@ -25,9 +25,11 @@ pub use source::{PriceSource, TrackedSource};
const MAX_RETRIES: usize = 12 * 60; // 12 hours of retrying
/// Create a shared HTTP agent with connection pooling and default timeout.
/// Status codes are not treated as errors — callers use `checked_get` for status handling.
pub fn new_agent(timeout_secs: u64) -> Agent {
Agent::config_builder()
.timeout_global(Some(Duration::from_secs(timeout_secs)))
.http_status_as_error(false)
.build()
.into()
}
@@ -63,9 +65,9 @@ impl Fetcher {
pub fn new(hars_path: Option<&Path>) -> Result<Self> {
let agent = new_agent(30);
Ok(Self {
binance: TrackedSource::new(Binance::new(hars_path, agent.clone())),
kraken: TrackedSource::new(Kraken::new(agent.clone())),
brk: TrackedSource::new(BRK::new(agent.clone())),
binance: TrackedSource::new(Binance::new_with_agent(hars_path, agent.clone())),
kraken: TrackedSource::new(Kraken::new_with_agent(agent.clone())),
brk: TrackedSource::new(BRK::new_with_agent(agent.clone())),
agent,
})
}