mirror of
https://github.com/bitcoinresearchkit/brk.git
synced 2026-04-30 09:30:00 -07:00
188 lines
6.4 KiB
Rust
188 lines
6.4 KiB
Rust
#![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<PathBuf>,
|
|
/// Full path to JavaScript client file (e.g., "modules/brk-client/index.js")
|
|
pub javascript: Option<PathBuf>,
|
|
/// Full path to Python client file (e.g., "packages/brk_client/__init__.py")
|
|
pub python: Option<PathBuf>,
|
|
}
|
|
|
|
impl ClientOutputPaths {
|
|
pub fn new() -> Self {
|
|
Self::default()
|
|
}
|
|
|
|
pub fn rust(mut self, path: impl Into<PathBuf>) -> Self {
|
|
self.rust = Some(path.into());
|
|
self
|
|
}
|
|
|
|
pub fn javascript(mut self, path: impl Into<PathBuf>) -> Self {
|
|
self.javascript = Some(path.into());
|
|
self
|
|
}
|
|
|
|
pub fn python(mut self, path: impl Into<PathBuf>) -> 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<Bitcoin>`
|
|
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());
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|