iri-client 0.1.4

Rust client and Python bindings for the NERSC IRI API
Documentation
use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;

use serde_json::Value;

// Shape of generated file (`$OUT_DIR/openapi_operations.rs`):
// - `OPENAPI_DEFAULT_SERVER_URL: &str`
// - `OPENAPI_OPERATIONS: &[OperationDefinition]`
//
// `OperationDefinition` is declared in `src/openapi_client.rs` and includes:
// - `operation_id`
// - `method` (uppercase)
// - `path_template`
// - `path_params` (extracted from `{...}` placeholders)
fn main() {
    println!("cargo:rerun-if-changed=openapi/openapi.json");

    let spec_path = PathBuf::from("openapi/openapi.json");
    let spec_text = fs::read_to_string(&spec_path)
        .unwrap_or_else(|error| panic!("failed to read {}: {error}", spec_path.display()));
    let spec: Value = serde_json::from_str(&spec_text)
        .unwrap_or_else(|error| panic!("failed to parse {}: {error}", spec_path.display()));

    let default_server = spec
        .get("servers")
        .and_then(Value::as_array)
        .and_then(|servers| servers.first())
        .and_then(|server| server.get("url"))
        .and_then(Value::as_str)
        .unwrap_or_default();

    let paths = spec
        .get("paths")
        .and_then(Value::as_object)
        .unwrap_or_else(|| {
            panic!(
                "{} does not contain an object at 'paths'",
                spec_path.display()
            )
        });

    let supported_methods = [
        "get", "post", "put", "patch", "delete", "head", "options", "trace",
    ];

    let mut operations = Vec::new();

    for (path_template, path_item) in paths {
        let Some(path_item_obj) = path_item.as_object() else {
            continue;
        };

        for method in supported_methods {
            let Some(operation_obj) = path_item_obj.get(method).and_then(Value::as_object) else {
                continue;
            };

            let Some(operation_id) = operation_obj.get("operationId").and_then(Value::as_str)
            else {
                continue;
            };

            let path_params = parse_path_params(path_template);
            operations.push(OperationRecord {
                operation_id: operation_id.to_owned(),
                method: method.to_ascii_uppercase(),
                path_template: path_template.to_owned(),
                path_params,
            });
        }
    }

    operations.sort_by(|left, right| left.operation_id.cmp(&right.operation_id));

    let mut output = String::new();
    write_generated_file_header(&mut output);
    let _ = write!(
        output,
        "pub(crate) const OPENAPI_DEFAULT_SERVER_URL: &str = {};\n\n",
        rust_string_literal(default_server)
    );
    output.push_str("pub(crate) static OPENAPI_OPERATIONS: &[OperationDefinition] = &[\n");
    for operation in operations {
        output.push_str("    OperationDefinition {\n");
        let _ = writeln!(
            output,
            "        operation_id: {},",
            rust_string_literal(&operation.operation_id)
        );
        let _ = writeln!(
            output,
            "        method: {},",
            rust_string_literal(&operation.method)
        );
        let _ = writeln!(
            output,
            "        path_template: {},",
            rust_string_literal(&operation.path_template)
        );

        output.push_str("        path_params: &[");
        for (index, param) in operation.path_params.iter().enumerate() {
            if index > 0 {
                output.push_str(", ");
            }
            output.push_str(&rust_string_literal(param));
        }
        output.push_str("],\n");
        output.push_str("    },\n");
    }
    output.push_str("];\n");

    let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR should be set"));
    let output_path = out_dir.join("openapi_operations.rs");
    fs::write(&output_path, output).unwrap_or_else(|error| {
        panic!(
            "failed to write generated operations to {}: {error}",
            output_path.display()
        )
    });
}

fn parse_path_params(path_template: &str) -> Vec<String> {
    let mut params = Vec::new();
    let mut chars = path_template.chars().peekable();

    while let Some(ch) = chars.next() {
        if ch != '{' {
            continue;
        }

        let mut name = String::new();
        while let Some(next) = chars.peek() {
            if *next == '}' {
                chars.next();
                break;
            }
            name.push(*next);
            chars.next();
        }

        if !name.is_empty() {
            params.push(name);
        }
    }

    params
}

fn rust_string_literal(value: &str) -> String {
    format!("{value:?}")
}

fn write_generated_file_header(output: &mut String) {
    output.push_str("// @generated by build.rs; do not edit manually.\n");
    output.push_str("//\n");
    output.push_str("// File structure:\n");
    output.push_str("// 1. OPENAPI_DEFAULT_SERVER_URL: &str\n");
    output.push_str("//    - from openapi/openapi.json servers[0].url\n");
    output.push_str("// 2. OPENAPI_OPERATIONS: &[OperationDefinition]\n");
    output.push_str("//    - each item has operation_id, method, path_template, path_params\n");
    output.push_str("//\n");
}

#[derive(Debug)]
struct OperationRecord {
    operation_id: String,
    method: String,
    path_template: String,
    path_params: Vec<String>,
}