#![allow(clippy::type_complexity)] use std::{collections::btree_map::Entry, fs::create_dir_all, io, path::PathBuf}; use brk_query::Vecs; /// Output path configuration for each language client. /// /// Each path should be the full path to the output file, not just a directory. /// Parent directories will be created automatically if they don't exist. /// /// # Example /// ```ignore /// let paths = ClientOutputPaths::new() /// .rust("crates/brk_client/src/lib.rs") /// .javascript("modules/brk-client/index.js") /// .python("packages/brk_client/__init__.py"); /// ``` #[derive(Debug, Clone, Default)] pub struct ClientOutputPaths { /// Full path to Rust client file (e.g., "crates/brk_client/src/lib.rs") pub rust: Option, /// Full path to JavaScript client file (e.g., "modules/brk-client/index.js") pub javascript: Option, /// Full path to Python client file (e.g., "packages/brk_client/__init__.py") pub python: Option, } impl ClientOutputPaths { pub fn new() -> Self { Self::default() } pub fn rust(mut self, path: impl Into) -> Self { self.rust = Some(path.into()); self } pub fn javascript(mut self, path: impl Into) -> Self { self.javascript = Some(path.into()); self } pub fn python(mut self, path: impl Into) -> Self { self.python = Some(path.into()); self } } mod analysis; mod backends; mod generate; mod generators; mod openapi; mod syntax; mod types; pub use analysis::*; pub use backends::*; pub use generate::*; pub use generators::*; pub use openapi::*; pub use syntax::*; pub use types::*; pub const VERSION: &str = env!("CARGO_PKG_VERSION"); /// Generate all client libraries from the query vecs and OpenAPI JSON. /// /// Uses `ClientOutputPaths` to specify the output file path for each language. /// Only languages with a configured path will be generated. /// /// # Example /// ```ignore /// let paths = ClientOutputPaths::new() /// .rust("crates/brk_client/src/lib.rs") /// .javascript("modules/brk-client/index.js") /// .python("packages/brk_client/__init__.py"); /// /// generate_clients(&vecs, &openapi_json, &paths)?; /// ``` pub fn generate_clients( vecs: &Vecs, openapi_json: &str, output_paths: &ClientOutputPaths, ) -> io::Result<()> { let metadata = ClientMetadata::from_vecs(vecs); // Parse OpenAPI spec let spec = parse_openapi_json(openapi_json)?; let endpoints = extract_endpoints(&spec); let mut schemas = extract_schemas(openapi_json); // Collect leaf type schemas from the catalog and merge into schemas collect_leaf_type_schemas(&metadata.catalog, &mut schemas); // Also collect definitions from all schemas (including OpenAPI schemas) // We need to do this after collecting leaf schemas so we process everything let schema_values: Vec<_> = schemas.values().cloned().collect(); for schema in &schema_values { collect_schema_definitions(schema, &mut schemas); } // Generate Rust client (uses real brk_types, no schema conversion needed) if let Some(rust_path) = &output_paths.rust { if let Some(parent) = rust_path.parent() { create_dir_all(parent)?; } generate_rust_client(&metadata, &endpoints, rust_path)?; } // Generate JavaScript client (needs schemas for type definitions) if let Some(js_path) = &output_paths.javascript { if let Some(parent) = js_path.parent() { create_dir_all(parent)?; } generate_javascript_client(&metadata, &endpoints, &schemas, js_path)?; } // Generate Python client (needs schemas for type definitions) if let Some(python_path) = &output_paths.python { if let Some(parent) = python_path.parent() { create_dir_all(parent)?; } generate_python_client(&metadata, &endpoints, &schemas, python_path)?; } Ok(()) } use brk_types::TreeNode; 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). /// Collects definitions from schemars-generated schemas (for referenced types). fn collect_leaf_type_schemas(node: &TreeNode, schemas: &mut TypeSchemas) { match node { TreeNode::Leaf(leaf) => { // Collect definitions from the schema (schemars puts type schemas here) // This includes the inner types like `Bitcoin` from `Close` collect_schema_definitions(&leaf.schema, schemas); // Get the type name for this leaf let type_name = extract_inner_type(leaf.kind()); if let Entry::Vacant(e) = schemas.entry(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 { e.insert(schema.clone()); } } } TreeNode::Branch(children) => { for child in children.values() { collect_leaf_type_schemas(child, schemas); } } } } /// Collect type definitions from schemars-generated schema's definitions section. /// Schemars uses `definitions` or `$defs` to store referenced types. fn collect_schema_definitions(schema: &Value, schemas: &mut TypeSchemas) { // Check both JSON Schema draft-07 style ("definitions") and draft 2019-09+ style ("$defs") for key in ["definitions", "$defs"] { if let Some(defs) = schema.get(key).and_then(|d| d.as_object()) { for (name, def_schema) in defs { if !schemas.contains_key(name) { schemas.insert(name.clone(), def_schema.clone()); } } } } }