binder: snapshot

This commit is contained in:
nym21
2025-12-21 00:33:56 +01:00
parent 4386ef47fe
commit 5e3519aad4
6 changed files with 92 additions and 18 deletions
+14 -1
View File
@@ -97,6 +97,7 @@ fn is_primitive_alias(schema: &Value) -> bool {
&& schema.get("items").is_none()
&& schema.get("anyOf").is_none()
&& schema.get("oneOf").is_none()
&& schema.get("enum").is_none()
}
/// Convert JSON Schema to JavaScript/JSDoc type
@@ -109,6 +110,18 @@ fn schema_to_js_type(schema: &Value) -> String {
return ref_path.rsplit('/').next().unwrap_or("*").to_string();
}
// Handle enum (array of string values)
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
let literals: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str())
.map(|s| format!("\"{}\"", s))
.collect();
if !literals.is_empty() {
return format!("({})", literals.join("|"));
}
}
// Handle type field
if let Some(ty) = schema.get("type").and_then(|t| t.as_str()) {
return match ty {
@@ -844,7 +857,7 @@ fn infer_child_accumulated_name(node: &TreeNode, parent_acc: &str, field_name: &
/// Generate API methods
fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
if endpoint.method != "GET" {
if !endpoint.should_generate() {
continue;
}
+24 -11
View File
@@ -60,21 +60,34 @@ use serde_json::Value;
/// Recursively collect leaf type schemas from the tree and add to schemas map.
/// Only adds schemas that aren't already present (OpenAPI schemas take precedence).
/// Also collects definitions from schemars-generated schemas (for referenced types).
/// Collects definitions from schemars-generated schemas (for referenced types).
fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) {
match node {
TreeNode::Leaf(leaf) => {
// Extract the inner type name (e.g., "Dollars" from "Close<Dollars>")
let type_name = extract_inner_type(leaf.value_type());
// Only add if not already present (OpenAPI schemas take precedence)
if !schemas.contains_key(&type_name) {
// The leaf schema is the schemars-generated JSON schema
schemas.insert(type_name, leaf.schema.clone());
}
// Also collect any definitions from the schema (schemars puts referenced types here)
// Collect definitions from the schema (schemars puts type schemas here)
// This includes the inner types like `Bitcoin` from `Close<Bitcoin>`
collect_schema_definitions(&leaf.schema, schemas);
// Get the type name for this leaf
let type_name = extract_inner_type(leaf.value_type());
if !schemas.contains_key(&type_name) {
// Unwrap single-element allOf
let schema = unwrap_allof(&leaf.schema);
// Add the schema if it's usable:
// - Simple type (has "type")
// - Object type with properties (complex types like OHLCCents, EmptyAddressData)
// - Enum type (has "enum" or "oneOf")
// - Or a $ref to another type
let has_type = schema.get("type").is_some();
let has_properties = schema.get("properties").is_some();
let has_enum = schema.get("enum").is_some() || schema.get("oneOf").is_some();
let is_ref = schema.get("$ref").is_some();
if has_type || has_properties || has_enum || is_ref {
schemas.insert(type_name, schema.clone());
}
}
}
TreeNode::Branch(children) => {
for child in children.values() {
+11
View File
@@ -27,6 +27,16 @@ pub struct Endpoint {
pub query_params: Vec<Parameter>,
/// Response type (simplified)
pub response_type: Option<String>,
/// Whether this endpoint is deprecated
pub deprecated: bool,
}
impl Endpoint {
/// Returns true if this endpoint should be included in client generation.
/// Only non-deprecated GET endpoints are included.
pub fn should_generate(&self) -> bool {
self.method == "GET" && !self.deprecated
}
}
/// Parameter information
@@ -161,6 +171,7 @@ fn extract_endpoint(path: &str, method: &str, operation: &Operation) -> Option<E
path_params,
query_params,
response_type,
deprecated: operation.deprecated.unwrap_or(false),
})
}
+31 -4
View File
@@ -9,8 +9,8 @@ use serde_json::Value;
use crate::{
ClientMetadata, Endpoint, FieldNamePosition, IndexSetPattern, PatternField, StructuralPattern,
TypeSchemas, extract_inner_type, get_node_fields, get_pattern_instance_base, to_pascal_case,
to_snake_case, unwrap_allof,
TypeSchemas, extract_inner_type, get_node_fields, get_pattern_instance_base, is_enum_schema,
to_pascal_case, to_snake_case, unwrap_allof,
};
/// Generate Python client from metadata and OpenAPI endpoints
@@ -28,7 +28,7 @@ pub fn generate_python_client(
writeln!(output, "from __future__ import annotations").unwrap();
writeln!(
output,
"from typing import TypeVar, Generic, Any, Optional, List, TypedDict"
"from typing import TypeVar, Generic, Any, Optional, List, Literal, TypedDict"
)
.unwrap();
writeln!(output, "import httpx\n").unwrap();
@@ -86,6 +86,10 @@ fn generate_type_definitions(output: &mut String, schemas: &TypeSchemas) {
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);
writeln!(output, "{} = {}", name, py_type).unwrap();
} else {
// Primitive type alias
let py_type = schema_to_python_type(schema);
@@ -147,6 +151,17 @@ fn topological_sort_schemas(schemas: &TypeSchemas) -> Vec<String> {
// Reverse so dependencies come first
result.reverse();
// Add any types that weren't processed (e.g., due to circular refs or other edge cases)
let result_set: HashSet<_> = result.iter().cloned().collect();
let mut missing: Vec<_> = schemas
.keys()
.filter(|k| !result_set.contains(*k))
.cloned()
.collect();
missing.sort();
result.extend(missing);
result
}
@@ -182,6 +197,18 @@ fn schema_to_python_type(schema: &Value) -> String {
return ref_path.rsplit('/').next().unwrap_or("Any").to_string();
}
// Handle enum (array of string values)
if let Some(enum_values) = schema.get("enum").and_then(|e| e.as_array()) {
let literals: Vec<String> = enum_values
.iter()
.filter_map(|v| v.as_str())
.map(|s| format!("\"{}\"", s))
.collect();
if !literals.is_empty() {
return format!("Literal[{}]", literals.join(", "));
}
}
// Handle type field
if let Some(ty) = schema.get("type").and_then(|t| t.as_str()) {
return match ty {
@@ -786,7 +813,7 @@ fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
/// Generate API methods from OpenAPI endpoints
fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
if endpoint.method != "GET" {
if !endpoint.should_generate() {
continue;
}
+1 -1
View File
@@ -715,7 +715,7 @@ impl BrkClient {{
/// Generate API methods from OpenAPI endpoints
fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
for endpoint in endpoints {
if endpoint.method != "GET" {
if !endpoint.should_generate() {
continue;
}
+11 -1
View File
@@ -196,6 +196,13 @@ pub fn unwrap_allof(schema: &Value) -> &Value {
schema
}
/// Check if a schema represents an enum type.
/// Enums have either an "enum" array or "oneOf" without properties.
pub fn is_enum_schema(schema: &Value) -> bool {
schema.get("enum").is_some()
|| (schema.get("oneOf").is_some() && schema.get("properties").is_none())
}
/// Extract inner type from a wrapper generic like `Close<Dollars>` -> `Dollars`.
/// Also handles malformed types like `Dollars>` (from vecdb's short_type_name which
/// extracts "Dollars>" from "Close<brk_types::Dollars>" using rsplit("::")).
@@ -706,7 +713,7 @@ fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<String, usi
let pascal = to_pascal_case(field_name);
// Sanitize: ensure it starts with a letter (prepend "_" if starts with digit)
let base_name = if pascal
let sanitized = if pascal
.chars()
.next()
.map(|c| c.is_ascii_digit())
@@ -717,6 +724,9 @@ fn generate_pattern_name(field_name: &str, name_counts: &mut HashMap<String, usi
pascal
};
// Add "Pattern" suffix to avoid conflicts with type aliases (e.g., Sats = int vs class Sats)
let base_name = format!("{}Pattern", sanitized);
// Track usage count and append index if needed
let count = name_counts.entry(base_name.clone()).or_insert(0);
*count += 1;