chainstream-sdk 2.0.9

SDK for interacting with the ChainStream API
Documentation
// Copyright 2022 Oxide Computer Company

use std::{
    fs::{self, File},
    path::Path,
};

/// Strip OpenAPI 3.1.0 features unsupported by progenitor:
/// - `nullable` field
/// - null-valued `example` fields
/// - `oneOf: [{type: null}, {$ref: ...}]` nullable patterns -> plain `$ref`
fn strip_unsupported_fields(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Object(map) => {
            map.remove("nullable");
            if let Some(serde_json::Value::Null) = map.get("example") {
                map.remove("example");
            }

            // Convert oneOf nullable pattern: [{type: null}, {$ref: ...}] -> $ref
            if let Some(serde_json::Value::Array(items)) = map.get("oneOf") {
                if items.len() == 2 {
                    let null_idx = items.iter().position(|item| {
                        item.get("type")
                            .and_then(|t| t.as_str())
                            .map_or(false, |t| t == "null")
                    });
                    if let Some(idx) = null_idx {
                        let other_idx = 1 - idx;
                        let replacement = items[other_idx].clone();
                        map.remove("oneOf");
                        if let serde_json::Value::Object(ref repl_map) = replacement {
                            for (k, v) in repl_map {
                                map.insert(k.clone(), v.clone());
                            }
                        }
                    }
                }
            }

            for (_, v) in map.iter_mut() {
                strip_unsupported_fields(v);
            }
        }
        serde_json::Value::Array(arr) => {
            for item in arr.iter_mut() {
                strip_unsupported_fields(item);
            }
        }
        _ => {}
    }
}

fn fix_enum_values(value: &mut serde_json::Value) {
    match value {
        serde_json::Value::Object(map) => {
            // Check if this is an enum definition
            if let Some(serde_json::Value::Array(ref mut values)) = map.get_mut("enum") {
                for item in values.iter_mut() {
                    if let serde_json::Value::String(s) = item {
                        match s.as_str() {
                            ">" => *s = "GreaterThan".to_string(),
                            ">=" => *s = "GreaterThanOrEqual".to_string(),
                            "<" => *s = "LessThan".to_string(),
                            "<=" => *s = "LessThanOrEqual".to_string(),
                            "==" => *s = "Equal".to_string(),
                            // Fix Resolution enum conflict: 1m (minute) vs 1M (month)
                            // typify generates same variant name for both, so rename 1M to 1Mo
                            "1M" => *s = "1Mo".to_string(),
                            _ => {}
                        }
                    }
                }
            }

            // Recursively process all values in the object
            for (_, v) in map.iter_mut() {
                fix_enum_values(v);
            }
        }
        serde_json::Value::Array(arr) => {
            // Recursively process all values in the array
            for item in arr.iter_mut() {
                fix_enum_values(item);
            }
        }
        _ => {}
    }
}

fn main() {
    // Only generate code if CHAINSTREAM_GENERATE environment variable is set
    // This prevents the toolchain from automatically regenerating.
    if std::env::var("CHAINSTREAM_GENERATE").is_err() {
        println!(
            "cargo:warning=Skipping code generation. Set CHAINSTREAM_GENERATE=1 to generate code."
        );
        println!("cargo:warning=Run 'make generate' to generate api.rs from openapi.yaml");
        return;
    }

    let src = "../openapi.yaml";
    println!("cargo:rerun-if-changed={}", src);
    let file = File::open(src).unwrap();
    let mut json: serde_json::Value = serde_yaml::from_reader(file).unwrap();

    // Fix enum values that aren't valid Rust identifiers
    fix_enum_values(&mut json);

    // Strip nullable and null examples unsupported by progenitor
    strip_unsupported_fields(&mut json);

    let spec = serde_json::from_str(&serde_json::to_string_pretty(&json).unwrap()).unwrap();

    let mut settings = progenitor_middleware::GenerationSettings::default();
    settings.with_interface(progenitor_middleware::InterfaceStyle::Builder);
    let mut generator = progenitor_middleware::Generator::new(&settings);
    let tokens = generator.generate_tokens(&spec).unwrap();
    let ast = syn::parse2(tokens).unwrap();
    let content = prettyplease::unparse(&ast);

    // Fix "1Mo" back to "1M" for API compatibility
    // The variant name remains unique (X1mo vs X1m), but serialization uses the original "1M"
    let content = content
        .replace(r#"rename = "1Mo""#, r#"rename = "1M""#)
        .replace(r#"write_str("1Mo")"#, r#"write_str("1M")"#)
        .replace(r#""1Mo" => Ok"#, r#""1M" => Ok"#);

    let mut out_file = Path::new("./src/openapi/").to_path_buf();
    out_file.push("api.rs");

    fs::write(out_file, content).unwrap();
    println!("cargo:warning=Successfully generated openapi/api.rs from openapi.yaml");
}