brk_bindgen 0.3.0-beta.1

A trait-based generator of client bindings for multiple languages
Documentation
//! Python API method generation.

use std::fmt::Write;

use crate::{
    Endpoint, Parameter, escape_python_keyword,
    generators::{normalize_return_type, write_description},
    to_snake_case,
};

use super::client::generate_class_constants;
use super::types::js_type_to_python;

/// Generate the main client class
pub fn generate_main_client(output: &mut String, endpoints: &[Endpoint]) {
    writeln!(output, "class BrkClient(BrkClientBase):").unwrap();
    writeln!(
        output,
        "    \"\"\"Main BRK client with series tree and API methods.\"\"\""
    )
    .unwrap();
    writeln!(output).unwrap();

    // Generate class-level constants
    generate_class_constants(output);

    writeln!(
        output,
        "    def __init__(self, base_url: str = 'http://localhost:3000', timeout: float = 30.0):"
    )
    .unwrap();
    writeln!(output, "        super().__init__(base_url, timeout)").unwrap();
    writeln!(output, "        self.series = SeriesTree(self)").unwrap();
    writeln!(output).unwrap();

    // Generate series_endpoint() method for dynamic series access
    writeln!(
        output,
        "    def series_endpoint(self, series: str, index: Index) -> SeriesEndpoint[Any]:"
    )
    .unwrap();
    writeln!(
        output,
        "        \"\"\"Create a dynamic series endpoint builder for any series/index combination."
    )
    .unwrap();
    writeln!(output).unwrap();
    writeln!(
        output,
        "        Use this for programmatic access when the series name is determined at runtime."
    )
    .unwrap();
    writeln!(
        output,
        "        For type-safe access, use the `series` tree instead."
    )
    .unwrap();
    writeln!(output, "        \"\"\"").unwrap();
    writeln!(output, "        return SeriesEndpoint(self, series, index)").unwrap();
    writeln!(output).unwrap();

    // Generate helper methods
    writeln!(
        output,
        "    def index_to_date(self, index: Index, i: int) -> Union[date, datetime]:"
    )
    .unwrap();
    writeln!(
        output,
        "        \"\"\"Convert an index value to a date/datetime for date-based indexes.\"\"\""
    )
    .unwrap();
    writeln!(output, "        return _index_to_date(index, i)").unwrap();
    writeln!(output).unwrap();
    writeln!(
        output,
        "    def date_to_index(self, index: Index, d: Union[date, datetime]) -> int:"
    )
    .unwrap();
    writeln!(
        output,
        "        \"\"\"Convert a date/datetime to an index value for date-based indexes.\"\"\""
    )
    .unwrap();
    writeln!(output, "        return _date_to_index(index, d)").unwrap();
    writeln!(output).unwrap();
    // Generate API methods
    generate_api_methods(output, endpoints);
}

/// Generate API methods from OpenAPI endpoints
pub fn generate_api_methods(output: &mut String, endpoints: &[Endpoint]) {
    for endpoint in endpoints {
        if !endpoint.should_generate() {
            continue;
        }

        let method_name = endpoint_to_method_name(endpoint);
        let base_return_type = normalize_return_type(
            &endpoint
                .response_type
                .as_deref()
                .map(js_type_to_python)
                .unwrap_or_else(|| "str".to_string()),
        );

        let return_type = if endpoint.supports_csv {
            format!("Union[{}, str]", base_return_type)
        } else {
            base_return_type
        };

        // Build method signature
        let params = build_method_params(endpoint);
        writeln!(
            output,
            "    def {}(self{}) -> {}:",
            method_name, params, return_type
        )
        .unwrap();

        // Docstring
        match (&endpoint.summary, &endpoint.description) {
            (Some(summary), Some(desc)) if summary != desc => {
                writeln!(output, "        \"\"\"{}.", summary.trim_end_matches('.')).unwrap();
                writeln!(output).unwrap();
                write_description(output, desc, "        ", "");
            }
            (Some(summary), _) => {
                writeln!(output, "        \"\"\"{}", summary).unwrap();
            }
            (None, Some(desc)) => {
                // First line includes opening quotes
                let mut lines = desc.lines();
                if let Some(first) = lines.next() {
                    writeln!(output, "        \"\"\"{}", first).unwrap();
                }
                for line in lines {
                    if line.is_empty() {
                        writeln!(output).unwrap();
                    } else {
                        writeln!(output, "        {}", line).unwrap();
                    }
                }
            }
            (None, None) => {
                write!(output, "        \"\"\"").unwrap();
            }
        }
        writeln!(output).unwrap();
        writeln!(
            output,
            "        Endpoint: `{} {}`\"\"\"",
            endpoint.method.to_uppercase(),
            endpoint.path
        )
        .unwrap();

        // Build path
        let path = build_path_template(&endpoint.path, &endpoint.path_params);

        let fetch_method = if endpoint.returns_json() {
            "get_json"
        } else {
            "get_text"
        };

        if endpoint.query_params.is_empty() {
            if endpoint.path_params.is_empty() {
                writeln!(output, "        return self.{}('{}')", fetch_method, path).unwrap();
            } else {
                writeln!(output, "        return self.{}(f'{}')", fetch_method, path).unwrap();
            }
        } else {
            writeln!(output, "        params = []").unwrap();
            for param in &endpoint.query_params {
                // Use safe name for Python variable, original name for API query parameter
                let safe_name = escape_python_keyword(&param.name);
                if param.required {
                    writeln!(
                        output,
                        "        params.append(f'{}={{{}}}')",
                        param.name, safe_name
                    )
                    .unwrap();
                } else {
                    writeln!(
                        output,
                        "        if {} is not None: params.append(f'{}={{{}}}')",
                        safe_name, param.name, safe_name
                    )
                    .unwrap();
                }
            }
            writeln!(output, "        query = '&'.join(params)").unwrap();
            writeln!(
                output,
                "        path = f'{}{{\"?\" + query if query else \"\"}}'",
                path
            )
            .unwrap();

            if endpoint.supports_csv {
                writeln!(output, "        if format == 'csv':").unwrap();
                writeln!(output, "            return self.get_text(path)").unwrap();
                writeln!(output, "        return self.{}(path)", fetch_method).unwrap();
            } else {
                writeln!(output, "        return self.{}(path)", fetch_method).unwrap();
            }
        }

        writeln!(output).unwrap();
    }
}

fn endpoint_to_method_name(endpoint: &Endpoint) -> String {
    to_snake_case(&endpoint.operation_name())
}

fn build_method_params(endpoint: &Endpoint) -> String {
    let mut params = Vec::new();
    // Path params are always required
    for param in &endpoint.path_params {
        let safe_name = escape_python_keyword(&param.name);
        let py_type = js_type_to_python(&param.param_type);
        params.push(format!(", {}: {}", safe_name, py_type));
    }
    // Required query params must come before optional ones (Python syntax requirement)
    for param in &endpoint.query_params {
        if param.required {
            let safe_name = escape_python_keyword(&param.name);
            let py_type = js_type_to_python(&param.param_type);
            params.push(format!(", {}: {}", safe_name, py_type));
        }
    }
    for param in &endpoint.query_params {
        if !param.required {
            let safe_name = escape_python_keyword(&param.name);
            let py_type = js_type_to_python(&param.param_type);
            params.push(format!(", {}: Optional[{}] = None", safe_name, py_type));
        }
    }
    params.join("")
}

fn build_path_template(path: &str, path_params: &[Parameter]) -> String {
    let mut result = path.to_string();
    for param in path_params {
        let placeholder = 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
}