use std::{env, fs, path::Path};
fn main() {
let src = "openapi.yml";
println!("cargo:rerun-if-changed={src}");
let yaml_content =
fs::read_to_string(src).unwrap_or_else(|e| panic!("Failed to read {src}: {e}"));
let mut value: serde_json::Value = serde_yaml::from_str(&yaml_content)
.unwrap_or_else(|e| panic!("Failed to parse {src} as YAML: {e}"));
if let Some(paths) = value.get_mut("paths")
&& let Some(paths_obj) = paths.as_object_mut()
{
for (_path, methods) in paths_obj.iter_mut() {
if let Some(methods_obj) = methods.as_object_mut() {
for (_method, details) in methods_obj.iter_mut() {
if let Some(responses) = details.get_mut("responses")
&& let Some(responses_obj) = responses.as_object_mut()
{
for (code, resp) in responses_obj.iter_mut() {
if !code.starts_with('2')
&& let Some(content) = resp.get("content")
{
let all_empty = content
.as_object()
.map(|co| {
co.values().all(|ct| {
ct.get("schema")
.and_then(|s| s.as_object())
.map(|o| o.is_empty())
.unwrap_or(true)
})
})
.unwrap_or(false);
if all_empty {
resp.as_object_mut().unwrap().remove("content");
}
}
}
}
}
}
}
}
sanitize_openapi_docs(&mut value);
let spec_str = serde_json::to_string(&value).unwrap();
let spec: openapiv3::OpenAPI = serde_json::from_str(&spec_str)
.unwrap_or_else(|e| panic!("Failed to parse pre-processed spec: {e}"));
let mut settings = progenitor::GenerationSettings::default();
settings
.with_interface(progenitor::InterfaceStyle::Builder)
.with_tag(progenitor::TagStyle::Merged);
let mut generator = progenitor::Generator::new(&settings);
let tokens = generator
.generate_tokens(&spec)
.unwrap_or_else(|e| panic!("Failed to generate API client: {e}"));
let ast = syn::parse2(tokens).unwrap_or_else(|e| panic!("Failed to parse generated code: {e}"));
let content = prettyplease::unparse(&ast);
let out_dir = env::var("OUT_DIR").unwrap();
let out_file = Path::new(&out_dir).join("codegen.rs");
fs::write(&out_file, content)
.unwrap_or_else(|e| panic!("Failed to write {}: {e}", out_file.display()));
}
fn sanitize_openapi_docs(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
for (key, child) in map.iter_mut() {
if matches!(key.as_str(), "description" | "summary")
&& let serde_json::Value::String(text) = child
{
*text = sanitize_doc_text(text);
}
sanitize_openapi_docs(child);
}
}
serde_json::Value::Array(items) => {
for item in items {
sanitize_openapi_docs(item);
}
}
_ => {}
}
}
fn sanitize_doc_text(text: &str) -> String {
let escaped_brackets = escape_non_link_brackets(text);
let escaped_placeholders = escape_angle_placeholders(&escaped_brackets);
wrap_bare_urls(&escaped_placeholders)
}
fn escape_non_link_brackets(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut index = 0;
while index < chars.len() {
if chars[index] == '['
&& let Some(close_index) = chars[index + 1..].iter().position(|&ch| ch == ']')
{
let close_index = index + 1 + close_index;
if chars.get(close_index + 1) != Some(&'(') {
result.push_str("\\[");
for ch in &chars[index + 1..close_index] {
result.push(*ch);
}
result.push_str("\\]");
index = close_index + 1;
continue;
}
}
result.push(chars[index]);
index += 1;
}
result
}
fn escape_angle_placeholders(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut index = 0;
while index < chars.len() {
if chars[index] == '<'
&& let Some(close_index) = chars[index + 1..].iter().position(|&ch| ch == '>')
{
let close_index = index + 1 + close_index;
let inner: String = chars[index + 1..close_index].iter().collect();
if !inner.is_empty()
&& !inner.contains(char::is_whitespace)
&& !inner.starts_with("http://")
&& !inner.starts_with("https://")
{
result.push_str("<");
result.push_str(&inner);
result.push_str(">");
index = close_index + 1;
continue;
}
}
result.push(chars[index]);
index += 1;
}
result
}
fn wrap_bare_urls(text: &str) -> String {
let mut result = String::with_capacity(text.len());
let chars: Vec<char> = text.chars().collect();
let mut index = 0;
while index < chars.len() {
let remainder: String = chars[index..].iter().collect();
if remainder.starts_with("http://") || remainder.starts_with("https://") {
let previous = index
.checked_sub(1)
.and_then(|prev| chars.get(prev))
.copied();
let mut end = index;
while let Some(ch) = chars.get(end) {
if ch.is_whitespace() {
break;
}
end += 1;
}
let mut url_end = end;
while url_end > index && matches!(chars[url_end - 1], '.' | ',' | ';' | ':') {
url_end -= 1;
}
let url: String = chars[index..url_end].iter().collect();
if matches!(previous, Some('(' | '<')) {
result.push_str(&url);
} else {
result.push('<');
result.push_str(&url);
result.push('>');
}
for ch in &chars[url_end..end] {
result.push(*ch);
}
index = end;
continue;
}
result.push(chars[index]);
index += 1;
}
result
}