brk_bindgen/
lib.rs

1#![allow(clippy::type_complexity)]
2
3use std::{collections::btree_map::Entry, fs::create_dir_all, io, path::PathBuf};
4
5use brk_query::Vecs;
6
7/// Output path configuration for each language client.
8///
9/// Each path should be the full path to the output file, not just a directory.
10/// Parent directories will be created automatically if they don't exist.
11///
12/// # Example
13/// ```ignore
14/// let paths = ClientOutputPaths::new()
15///     .rust("crates/brk_client/src/lib.rs")
16///     .javascript("modules/brk-client/index.js")
17///     .python("packages/brk_client/__init__.py");
18/// ```
19#[derive(Debug, Clone, Default)]
20pub struct ClientOutputPaths {
21    /// Full path to Rust client file (e.g., "crates/brk_client/src/lib.rs")
22    pub rust: Option<PathBuf>,
23    /// Full path to JavaScript client file (e.g., "modules/brk-client/index.js")
24    pub javascript: Option<PathBuf>,
25    /// Full path to Python client file (e.g., "packages/brk_client/__init__.py")
26    pub python: Option<PathBuf>,
27}
28
29impl ClientOutputPaths {
30    pub fn new() -> Self {
31        Self::default()
32    }
33
34    pub fn rust(mut self, path: impl Into<PathBuf>) -> Self {
35        self.rust = Some(path.into());
36        self
37    }
38
39    pub fn javascript(mut self, path: impl Into<PathBuf>) -> Self {
40        self.javascript = Some(path.into());
41        self
42    }
43
44    pub fn python(mut self, path: impl Into<PathBuf>) -> Self {
45        self.python = Some(path.into());
46        self
47    }
48}
49
50mod analysis;
51mod backends;
52mod generate;
53mod generators;
54mod openapi;
55mod syntax;
56mod types;
57
58pub use analysis::*;
59pub use backends::*;
60pub use generate::*;
61pub use generators::*;
62pub use openapi::*;
63pub use syntax::*;
64pub use types::*;
65
66pub const VERSION: &str = env!("CARGO_PKG_VERSION");
67
68/// Generate all client libraries from the query vecs and OpenAPI JSON.
69///
70/// Uses `ClientOutputPaths` to specify the output file path for each language.
71/// Only languages with a configured path will be generated.
72///
73/// # Example
74/// ```ignore
75/// let paths = ClientOutputPaths::new()
76///     .rust("crates/brk_client/src/lib.rs")
77///     .javascript("modules/brk-client/index.js")
78///     .python("packages/brk_client/__init__.py");
79///
80/// generate_clients(&vecs, &openapi_json, &paths)?;
81/// ```
82pub fn generate_clients(
83    vecs: &Vecs,
84    openapi_json: &str,
85    output_paths: &ClientOutputPaths,
86) -> io::Result<()> {
87    let metadata = ClientMetadata::from_vecs(vecs);
88
89    // Parse OpenAPI spec
90    let spec = parse_openapi_json(openapi_json)?;
91    let endpoints = extract_endpoints(&spec);
92    let mut schemas = extract_schemas(openapi_json);
93
94    // Collect leaf type schemas from the catalog and merge into schemas
95    collect_leaf_type_schemas(&metadata.catalog, &mut schemas);
96
97    // Also collect definitions from all schemas (including OpenAPI schemas)
98    // We need to do this after collecting leaf schemas so we process everything
99    let schema_values: Vec<_> = schemas.values().cloned().collect();
100    for schema in &schema_values {
101        collect_schema_definitions(schema, &mut schemas);
102    }
103
104    // Generate Rust client (uses real brk_types, no schema conversion needed)
105    if let Some(rust_path) = &output_paths.rust {
106        if let Some(parent) = rust_path.parent() {
107            create_dir_all(parent)?;
108        }
109        generate_rust_client(&metadata, &endpoints, rust_path)?;
110    }
111
112    // Generate JavaScript client (needs schemas for type definitions)
113    if let Some(js_path) = &output_paths.javascript {
114        if let Some(parent) = js_path.parent() {
115            create_dir_all(parent)?;
116        }
117        generate_javascript_client(&metadata, &endpoints, &schemas, js_path)?;
118    }
119
120    // Generate Python client (needs schemas for type definitions)
121    if let Some(python_path) = &output_paths.python {
122        if let Some(parent) = python_path.parent() {
123            create_dir_all(parent)?;
124        }
125        generate_python_client(&metadata, &endpoints, &schemas, python_path)?;
126    }
127
128    Ok(())
129}
130
131use brk_types::TreeNode;
132use serde_json::Value;
133
134/// Recursively collect leaf type schemas from the tree and add to schemas map.
135/// Only adds schemas that aren't already present (OpenAPI schemas take precedence).
136/// Collects definitions from schemars-generated schemas (for referenced types).
137fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) {
138    match node {
139        TreeNode::Leaf(leaf) => {
140            // Collect definitions from the schema (schemars puts type schemas here)
141            // This includes the inner types like `Bitcoin` from `Close<Bitcoin>`
142            collect_schema_definitions(&leaf.schema, schemas);
143
144            // Get the type name for this leaf
145            let type_name = extract_inner_type(leaf.kind());
146
147            if let Entry::Vacant(e) = schemas.entry(type_name) {
148                // Unwrap single-element allOf
149                let schema = unwrap_allof(&leaf.schema);
150
151                // Add the schema if it's usable:
152                // - Simple type (has "type")
153                // - Object type with properties (complex types like OHLCCents, EmptyAddressData)
154                // - Enum type (has "enum" or "oneOf")
155                // - Or a $ref to another type
156                let has_type = schema.get("type").is_some();
157                let has_properties = schema.get("properties").is_some();
158                let has_enum = schema.get("enum").is_some() || schema.get("oneOf").is_some();
159                let is_ref = schema.get("$ref").is_some();
160
161                if has_type || has_properties || has_enum || is_ref {
162                    e.insert(schema.clone());
163                }
164            }
165        }
166        TreeNode::Branch(children) => {
167            for child in children.values() {
168                collect_leaf_type_schemas(child, schemas);
169            }
170        }
171    }
172}
173
174/// Collect type definitions from schemars-generated schema's definitions section.
175/// Schemars uses `definitions` or `$defs` to store referenced types.
176fn collect_schema_definitions(schema: &Value, schemas: &mut TypeSchemas) {
177    // Check both JSON Schema draft-07 style ("definitions") and draft 2019-09+ style ("$defs")
178    for key in ["definitions", "$defs"] {
179        if let Some(defs) = schema.get(key).and_then(|d| d.as_object()) {
180            for (name, def_schema) in defs {
181                if !schemas.contains_key(name) {
182                    schemas.insert(name.clone(), def_schema.clone());
183                }
184            }
185        }
186    }
187}