From 4b1410855aae38f674c569fa2db1863a37925372 Mon Sep 17 00:00:00 2001 From: nym21 Date: Sun, 21 Dec 2025 01:04:13 +0100 Subject: [PATCH] binder: snapshot --- crates/brk_binder/src/javascript.rs | 37 +++++++++----- crates/brk_binder/src/lib.rs | 7 +-- crates/brk_binder/src/python.rs | 77 ++++++++++++++++++++--------- crates/brk_binder/src/types.rs | 11 ++--- 4 files changed, 87 insertions(+), 45 deletions(-) diff --git a/crates/brk_binder/src/javascript.rs b/crates/brk_binder/src/javascript.rs index 8b1d5a26e..8d8aa1f99 100644 --- a/crates/brk_binder/src/javascript.rs +++ b/crates/brk_binder/src/javascript.rs @@ -58,7 +58,7 @@ fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) { writeln!(output, "// Type definitions\n").unwrap(); for (name, schema) in schemas { - let js_type = schema_to_js_type(schema); + let js_type = schema_to_js_type_ctx(schema, Some(name)); if is_primitive_alias(schema) { // Simple type alias: @typedef {number} Height @@ -68,7 +68,7 @@ fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) { 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); + let prop_type = schema_to_js_type_ctx(prop_schema, Some(name)); let required = schema .get("required") .and_then(|r| r.as_array()) @@ -101,7 +101,7 @@ fn is_primitive_alias(schema: &Value) -> bool { } /// Convert a single JSON type string to JavaScript type -fn json_type_to_js(ty: &str, schema: &Value) -> String { +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(), @@ -110,15 +110,16 @@ fn json_type_to_js(ty: &str, schema: &Value) -> String { "array" => { let item_type = schema .get("items") - .map(schema_to_js_type) + .map(|s| schema_to_js_type_ctx(s, current_type)) .unwrap_or_else(|| "*".to_string()); format!("{}[]", item_type) } "object" => { // Check if it has additionalProperties (dict-like) if let Some(add_props) = schema.get("additionalProperties") { - let value_type = schema_to_js_type(add_props); - return format!("Object.", value_type); + let value_type = schema_to_js_type_ctx(add_props, current_type); + // Use TypeScript index signature syntax for recursive types + return format!("{{ [key: string]: {} }}", value_type); } "Object".to_string() } @@ -126,12 +127,12 @@ fn json_type_to_js(ty: &str, schema: &Value) -> String { } } -/// Convert JSON Schema to JavaScript/JSDoc type -fn schema_to_js_type(schema: &Value) -> String { +/// Convert JSON Schema to JavaScript/JSDoc type with context for recursive types +fn schema_to_js_type_ctx(schema: &Value, current_type: Option<&str>) -> String { // Handle allOf (try each element until we find a resolvable type) 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); + let resolved = schema_to_js_type_ctx(item, current_type); if resolved != "*" { return resolved; } @@ -163,7 +164,7 @@ fn schema_to_js_type(schema: &Value) -> String { .iter() .filter_map(|t| t.as_str()) .filter(|t| *t != "null") - .map(|t| json_type_to_js(t, schema)) + .map(|t| json_type_to_js(t, schema, current_type)) .collect(); let has_null = type_array.iter().any(|t| t.as_str() == Some("null")); @@ -186,7 +187,7 @@ fn schema_to_js_type(schema: &Value) -> String { // Handle single type string if let Some(ty_str) = ty.as_str() { - return json_type_to_js(ty_str, schema); + return json_type_to_js(ty_str, schema, current_type); } } @@ -196,11 +197,21 @@ fn schema_to_js_type(schema: &Value) -> String { .or_else(|| schema.get("oneOf")) .and_then(|v| v.as_array()) { - let types: Vec = variants.iter().map(schema_to_js_type).collect(); + let types: Vec = variants + .iter() + .map(|v| schema_to_js_type_ctx(v, current_type)) + .collect(); // Filter out * and null for cleaner unions let filtered: Vec<_> = types.iter().filter(|t| *t != "*").collect(); if !filtered.is_empty() { - return format!("({})", filtered.iter().map(|s| s.as_str()).collect::>().join("|")); + return format!( + "({})", + filtered + .iter() + .map(|s| s.as_str()) + .collect::>() + .join("|") + ); } return format!("({})", types.join("|")); } diff --git a/crates/brk_binder/src/lib.rs b/crates/brk_binder/src/lib.rs index 2b8cf3d90..012b6a6a4 100644 --- a/crates/brk_binder/src/lib.rs +++ b/crates/brk_binder/src/lib.rs @@ -1,4 +1,4 @@ -use std::{fs::create_dir_all, io, path::Path}; +use std::{collections::btree_map::Entry, fs::create_dir_all, io, path::Path}; use brk_query::Vecs; @@ -70,7 +70,8 @@ fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) { // Get the type name for this leaf let type_name = extract_inner_type(leaf.value_type()); - if !schemas.contains_key(&type_name) { + + if let Entry::Vacant(e) = schemas.entry(type_name) { // Unwrap single-element allOf let schema = unwrap_allof(&leaf.schema); @@ -85,7 +86,7 @@ fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) { let is_ref = schema.get("$ref").is_some(); if has_type || has_properties || has_enum || is_ref { - schemas.insert(type_name, schema.clone()); + e.insert(schema.clone()); } } } diff --git a/crates/brk_binder/src/python.rs b/crates/brk_binder/src/python.rs index 36980d28b..8ce81d750 100644 --- a/crates/brk_binder/src/python.rs +++ b/crates/brk_binder/src/python.rs @@ -81,18 +81,18 @@ fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) { // Object type -> TypedDict writeln!(output, "class {}(TypedDict):", name).unwrap(); for (prop_name, prop_schema) in props { - let prop_type = schema_to_python_type(prop_schema); + 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) { // Enum type -> Literal union - let py_type = schema_to_python_type(schema); + let py_type = schema_to_python_type_ctx(schema, Some(&name)); writeln!(output, "{} = {}", name, py_type).unwrap(); } else { // Primitive type alias - let py_type = schema_to_python_type(schema); + let py_type = schema_to_python_type_ctx(schema, Some(&name)); writeln!(output, "{} = {}", name, py_type).unwrap(); } } @@ -169,10 +169,10 @@ fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec { fn collect_schema_refs(schema: &Value, refs: &mut std::collections::HashSet) { match schema { Value::Object(map) => { - if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str()) { - if let Some(type_name) = ref_path.rsplit('/').next() { - refs.insert(type_name.to_string()); - } + 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); @@ -188,7 +188,7 @@ fn collect_schema_refs(schema: &Value, refs: &mut std::collections::HashSet String { +fn json_type_to_python(ty: &str, schema: &Value, current_type: Option<&str>) -> String { match ty { "integer" => "int".to_string(), "number" => "float".to_string(), @@ -198,14 +198,14 @@ fn json_type_to_python(ty: &str, schema: &Value) -> String { "array" => { let item_type = schema .get("items") - .map(schema_to_python_type) + .map(|s| schema_to_python_type_ctx(s, current_type)) .unwrap_or_else(|| "Any".to_string()); format!("List[{}]", item_type) } "object" => { // Check if it has additionalProperties (dict-like) if let Some(add_props) = schema.get("additionalProperties") { - let value_type = schema_to_python_type(add_props); + let value_type = schema_to_python_type_ctx(add_props, current_type); return format!("dict[str, {}]", value_type); } "dict".to_string() @@ -214,12 +214,12 @@ fn json_type_to_python(ty: &str, schema: &Value) -> String { } } -/// Convert JSON Schema to Python type -fn schema_to_python_type(schema: &Value) -> 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 { // Handle allOf (try each element until we find a resolvable type) 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(item); + let resolved = schema_to_python_type_ctx(item, current_type); if resolved != "Any" { return resolved; } @@ -228,7 +228,12 @@ fn schema_to_python_type(schema: &Value) -> String { // Handle $ref if let Some(ref_path) = schema.get("$ref").and_then(|r| r.as_str()) { - return ref_path.rsplit('/').next().unwrap_or("Any").to_string(); + 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) @@ -251,7 +256,7 @@ fn schema_to_python_type(schema: &Value) -> String { .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)) + .map(|t| json_type_to_python(t, schema, current_type)) .collect(); let has_null = type_array.iter().any(|t| t.as_str() == Some("null")); @@ -274,7 +279,7 @@ fn schema_to_python_type(schema: &Value) -> String { // Handle single type string if let Some(ty_str) = ty.as_str() { - return json_type_to_python(ty_str, schema); + return json_type_to_python(ty_str, schema, current_type); } } @@ -284,11 +289,18 @@ fn schema_to_python_type(schema: &Value) -> String { .or_else(|| schema.get("oneOf")) .and_then(|v| v.as_array()) { - let types: Vec = variants.iter().map(schema_to_python_type).collect(); + let types: Vec = variants + .iter() + .map(|v| schema_to_python_type_ctx(v, current_type)) + .collect(); // Filter out Any and null for cleaner unions let filtered: Vec<_> = types.iter().filter(|t| *t != "Any").collect(); if !filtered.is_empty() { - return filtered.iter().map(|s| s.as_str()).collect::>().join(" | "); + return filtered + .iter() + .map(|s| s.as_str()) + .collect::>() + .join(" | "); } return types.join(" | "); } @@ -315,7 +327,12 @@ fn escape_python_keyword(name: &str) -> String { "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) { + let name = if name + .chars() + .next() + .map(|c| c.is_ascii_digit()) + .unwrap_or(false) + { format!("_{}", name) } else { name.to_string() @@ -906,7 +923,11 @@ fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) { let path = build_path_template(&endpoint.path, &endpoint.path_params); if endpoint.query_params.is_empty() { - writeln!(output, " return self.get(f'{}')", path).unwrap(); + 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 { @@ -957,12 +978,20 @@ fn endpoint_to_method_name(endpoint: &Endpoint) -> String { to_snake_case(&format!("get_{}", parts.join("_"))) } -/// Convert JS-style type to Python type (e.g., "Txid[]" -> "List[Txid]") +/// 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 { - js_type.to_string() + 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(), + } } } @@ -987,7 +1016,9 @@ 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); + // 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 diff --git a/crates/brk_binder/src/types.rs b/crates/brk_binder/src/types.rs index 6c97bd2d8..c93110032 100644 --- a/crates/brk_binder/src/types.rs +++ b/crates/brk_binder/src/types.rs @@ -209,12 +209,11 @@ pub fn is_enum_schema(schema: &Value) -> bool { /// If not a generic, returns the type as-is. pub fn extract_inner_type(type_str: &str) -> String { // Handle proper generic wrappers like `Close` -> `Dollars` - if let Some(start) = type_str.find('<') { - if let Some(end) = type_str.rfind('>') { - if start < end { - return type_str[start + 1..end].to_string(); - } - } + if let Some(start) = type_str.find('<') + && let Some(end) = type_str.rfind('>') + && start < end + { + return type_str[start + 1..end].to_string(); } // Handle malformed types like `Dollars>` (trailing > without <) // This happens due to vecdb's short_type_name using rsplit("::")