binder: snapshot

This commit is contained in:
nym21
2025-12-21 01:04:13 +01:00
parent 78a4d1af65
commit 4b1410855a
4 changed files with 87 additions and 45 deletions
+24 -13
View File
@@ -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.<string, {}>", 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<String> = variants.iter().map(schema_to_js_type).collect();
let types: Vec<String> = 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::<Vec<_>>().join("|"));
return format!(
"({})",
filtered
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.join("|")
);
}
return format!("({})", types.join("|"));
}
+4 -3
View File
@@ -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());
}
}
}
+54 -23
View File
@@ -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<String> {
fn collect_schema_refs(schema: &Value, refs: &mut std::collections::HashSet<String>) {
match schema {
Value::Object(map) => {
if let Some(ref_path) = map.get("$ref").and_then(|r| r.as_str()) {
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<Stri
}
/// Convert a single JSON type string to Python type
fn json_type_to_python(ty: &str, schema: &Value) -> 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<String> = variants.iter().map(schema_to_python_type).collect();
let types: Vec<String> = 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::<Vec<_>>().join(" | ");
return filtered
.iter()
.map(|s| s.as_str())
.collect::<Vec<_>>()
.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(&param.name);
let interpolation = format!("{{{}}}", safe_name);
result = result.replace(&placeholder, &interpolation);
}
result
+5 -6
View File
@@ -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>` -> `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("::")