use std::env;
use std::fmt::Write as _;
use std::fs;
use std::path::PathBuf;
use serde_json::Value;
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>,
}