use clap_complete::{Shell, generate_to};
use std::fs;
use std::io::Error;
use std::path::Path;
#[path = "src/build/mod.rs"]
mod cli;
fn main() -> Result<(), Error> {
generate_openapi_client()?;
regenerate_docs_if_requested()
}
fn generate_openapi_client() -> Result<(), Error> {
let spec_path = "openapi.json";
println!("cargo:rerun-if-changed={spec_path}");
let spec_content = fs::read_to_string(spec_path)?;
let mut spec_value: serde_json::Value = serde_json::from_str(&spec_content)
.map_err(|e| Error::other(format!("openapi.json is not valid JSON: {e}")))?;
downconvert_to_openapi_30(&mut spec_value);
let spec: openapiv3::OpenAPI = serde_json::from_value(spec_value)
.map_err(|e| Error::other(format!("openapi.json is not valid OpenAPI: {e}")))?;
let mut generator = progenitor::Generator::default();
let tokens = generator
.generate_tokens(&spec)
.map_err(|e| Error::other(format!("progenitor codegen failed: {e}")))?;
let ast = syn::parse2(tokens)
.map_err(|e| Error::other(format!("generated tokens did not parse: {e}")))?;
let pretty = prettyplease::unparse(&ast);
let out_dir = std::env::var_os("OUT_DIR").ok_or_else(|| {
Error::other("OUT_DIR not set by cargo (build.rs invariant)")
})?;
let out_file = Path::new(&out_dir).join("openapi_client.rs");
fs::write(out_file, pretty)?;
Ok(())
}
fn downconvert_to_openapi_30(value: &mut serde_json::Value) {
if let serde_json::Value::Object(obj) = value {
if let Some(serde_json::Value::String(v)) = obj.get("openapi")
&& v.starts_with("3.1")
{
obj.insert(
"openapi".to_string(),
serde_json::Value::String("3.0.3".to_string()),
);
}
if let Some(serde_json::Value::Array(types)) = obj.get("type").cloned() {
let non_null: Vec<&str> = types
.iter()
.filter_map(|v| v.as_str())
.filter(|s| *s != "null")
.collect();
let has_null = types.iter().any(|v| v.as_str() == Some("null"));
if non_null.len() == 1 {
obj.insert(
"type".to_string(),
serde_json::Value::String(non_null[0].to_string()),
);
if has_null {
obj.insert("nullable".to_string(), serde_json::Value::Bool(true));
}
}
}
if let Some(serde_json::Value::Array(arr)) = obj.get("oneOf").cloned() {
let has_null = arr
.iter()
.any(|e| e.as_object().and_then(|o| o.get("type")).and_then(|v| v.as_str()) == Some("null"));
let non_null: Vec<serde_json::Value> = arr
.iter()
.filter(|e| {
e.as_object().and_then(|o| o.get("type")).and_then(|v| v.as_str()) != Some("null")
})
.cloned()
.collect();
if has_null && non_null.len() == 1 {
obj.remove("oneOf");
let only = non_null.into_iter().next().expect("checked len == 1");
if only.as_object().is_some_and(|o| o.contains_key("$ref")) {
obj.insert(
"allOf".to_string(),
serde_json::Value::Array(vec![only]),
);
} else if let serde_json::Value::Object(inner) = only {
for (k, v) in inner {
obj.insert(k, v);
}
}
obj.insert("nullable".to_string(), serde_json::Value::Bool(true));
}
}
for (_, child) in obj.iter_mut() {
downconvert_to_openapi_30(child);
}
} else if let serde_json::Value::Array(arr) = value {
for child in arr.iter_mut() {
downconvert_to_openapi_30(child);
}
}
}
fn regenerate_docs_if_requested() -> Result<(), Error> {
println!("cargo:rerun-if-env-changed=MANTA_REGENERATE_DOCS");
if std::env::var("MANTA_REGENERATE_DOCS").is_err() {
return Ok(());
}
let man_dir = "man";
fs::create_dir_all(man_dir)?;
let man_path = Path::new(man_dir).join("manta.1");
let mut f = fs::File::create(&man_path)?;
cli::manpage::render_consolidated(cli::build_cli(), &mut f)?;
println!(
"cargo:warning=man page regenerated at {}",
man_path.display()
);
let completion_dir = "autocomplete_shell_scripts";
fs::create_dir_all(completion_dir)?;
for shell in [Shell::Bash, Shell::Zsh, Shell::Fish, Shell::Elvish] {
let path = generate_to(
shell,
&mut cli::build_cli(),
"manta",
completion_dir,
)?;
println!("cargo:warning=completion regenerated: {path:?}");
}
Ok(())
}