use std::path::Path;
fn main() {
println!("cargo:rerun-if-changed=schemas/");
println!("cargo:rerun-if-env-changed=OPENLATCH_POSTHOG_KEY");
let key = std::env::var("OPENLATCH_POSTHOG_KEY").unwrap_or_default();
println!("cargo:rustc-env=OPENLATCH_POSTHOG_KEY={key}");
println!("cargo:rerun-if-env-changed=OPENLATCH_POSTHOG_HOST");
let host = std::env::var("OPENLATCH_POSTHOG_HOST")
.unwrap_or_else(|_| "https://eu.i.posthog.com".to_string());
println!("cargo:rustc-env=OPENLATCH_POSTHOG_HOST={host}");
println!("cargo:rerun-if-env-changed=OPENLATCH_SENTRY_DSN");
let dsn = std::env::var("OPENLATCH_SENTRY_DSN").unwrap_or_default();
println!("cargo:rustc-env=OPENLATCH_SENTRY_DSN={dsn}");
println!("cargo:rerun-if-env-changed=GITHUB_SHA");
println!("cargo:rerun-if-env-changed=OPENLATCH_RELEASE_SHA");
let sha = resolve_release_sha();
println!("cargo:rustc-env=OPENLATCH_RELEASE_SHA={sha}");
let schemas_dir = Path::new("schemas");
let out_path = std::path::PathBuf::from("src/generated/types.rs");
let known_values_path = std::path::PathBuf::from("src/generated/known_values.rs");
let enums_raw: serde_json::Value = read_schema(schemas_dir, "enums.schema.json");
let struct_files = [
"event-envelope.schema.json",
"verdict-response.schema.json",
"cloud-ingestion-request.schema.json",
"cloud-ingestion-response.schema.json",
"auth-me-response.schema.json",
];
write_known_values(&enums_raw, &known_values_path);
let combined = build_combined_schema(&enums_raw, schemas_dir, &struct_files);
let mut settings = typify::TypeSpaceSettings::default();
settings.with_struct_builder(false);
settings.with_derive("PartialEq".parse().unwrap());
settings.with_replacement(
"HookEventType",
"crate::core::envelope::known_types::HookEventType",
[
typify::TypeSpaceImpl::FromStr,
typify::TypeSpaceImpl::Display,
]
.into_iter(),
);
settings.with_replacement(
"AgentType",
"crate::core::envelope::known_types::AgentType",
[
typify::TypeSpaceImpl::FromStr,
typify::TypeSpaceImpl::Display,
]
.into_iter(),
);
let mut type_space = typify::TypeSpace::new(&settings);
let root_schema: schemars::schema::RootSchema =
serde_json::from_value(combined).expect("failed to parse combined schema");
type_space
.add_root_schema(root_schema)
.expect("failed to process combined schema");
let tokens = type_space.to_stream();
let ast = syn::parse2::<syn::File>(tokens).expect("failed to parse generated tokens");
let formatted = prettyplease::unparse(&ast);
let output = format!(
"// AUTO-GENERATED by build.rs from schemas/*.schema.json\n\
// DO NOT EDIT — changes will be overwritten on next build.\n\
// To modify types, edit the JSON Schema files in schemas/ and rebuild.\n\n\
{formatted}\n"
);
let tmp_path = out_path.with_extension("rs.tmp");
std::fs::write(&tmp_path, &output).unwrap();
let _ = std::process::Command::new("rustfmt")
.arg(tmp_path.as_os_str())
.status();
let final_output = std::fs::read_to_string(&tmp_path).unwrap_or(output);
let _ = std::fs::remove_file(&tmp_path);
let needs_write = match std::fs::read_to_string(&out_path) {
Ok(existing) => existing != final_output,
Err(_) => true,
};
if needs_write {
std::fs::write(&out_path, final_output).unwrap();
}
}
fn resolve_release_sha() -> String {
if let Ok(sha) = std::env::var("GITHUB_SHA") {
if !sha.is_empty() {
return sha;
}
}
if let Ok(sha) = std::env::var("OPENLATCH_RELEASE_SHA") {
if !sha.is_empty() {
return sha;
}
}
if let Ok(output) = std::process::Command::new("git")
.args(["rev-parse", "HEAD"])
.output()
{
if output.status.success() {
if let Ok(s) = String::from_utf8(output.stdout) {
let trimmed = s.trim();
if !trimmed.is_empty() {
return trimmed.to_string();
}
}
}
}
format!("v{}", env!("CARGO_PKG_VERSION"))
}
fn read_schema(dir: &Path, filename: &str) -> serde_json::Value {
let path = dir.join(filename);
let content = std::fs::read_to_string(&path)
.unwrap_or_else(|e| panic!("failed to read {}: {e}", path.display()));
serde_json::from_str(&content)
.unwrap_or_else(|e| panic!("failed to parse {}: {e}", path.display()))
}
fn build_combined_schema(
enums: &serde_json::Value,
schemas_dir: &Path,
struct_files: &[&str],
) -> serde_json::Value {
let mut all_defs = serde_json::Map::new();
if let Some(defs) = enums.get("$defs").and_then(|d| d.as_object()) {
for (key, value) in defs {
all_defs.insert(key.clone(), value.clone());
}
}
for filename in struct_files {
let path = schemas_dir.join(filename);
if !path.exists() {
continue;
}
let mut schema = read_schema(schemas_dir, filename);
if let Some(local_defs) = schema.as_object_mut().and_then(|obj| obj.remove("$defs")) {
if let Some(local_defs_map) = local_defs.as_object() {
for (key, value) in local_defs_map {
let mut resolved = value.clone();
rewrite_refs(&mut resolved);
all_defs.insert(key.clone(), resolved);
}
}
}
let title = schema
.get("title")
.and_then(|t| t.as_str())
.unwrap_or_else(|| panic!("{filename} must have a title"))
.to_string();
if let Some(obj) = schema.as_object_mut() {
obj.remove("$schema");
obj.remove("$id");
obj.remove("title");
}
rewrite_refs(&mut schema);
all_defs.insert(title, schema);
}
serde_json::json!({
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$defs": all_defs
})
}
fn rewrite_refs(value: &mut serde_json::Value) {
match value {
serde_json::Value::Object(map) => {
if let Some(ref_val) = map.get_mut("$ref") {
if let Some(ref_str) = ref_val.as_str() {
if let Some(fragment) = ref_str.find("#/") {
let local_ref = &ref_str[fragment..];
*ref_val = serde_json::Value::String(local_ref.to_string());
}
}
}
for v in map.values_mut() {
rewrite_refs(v);
}
}
serde_json::Value::Array(arr) => {
for v in arr {
rewrite_refs(v);
}
}
_ => {}
}
}
fn write_known_values(enums: &serde_json::Value, out_path: &Path) {
let hook = extract_known_values(enums, "HookEventType");
let agent = extract_known_values(enums, "AgentType");
let body = format!(
"// AUTO-GENERATED by build.rs from schemas/enums.schema.json x-known-values.\n\
// DO NOT EDIT — changes will be overwritten on next build.\n\n\
pub const SCHEMA_HOOK_EVENT_TYPES: &[&str] = &[\n{}];\n\n\
pub const SCHEMA_AGENT_TYPES: &[&str] = &[\n{}];\n",
hook.iter()
.map(|v| format!(" {},\n", rust_str_literal(v)))
.collect::<String>(),
agent
.iter()
.map(|v| format!(" {},\n", rust_str_literal(v)))
.collect::<String>(),
);
let needs_write = match std::fs::read_to_string(out_path) {
Ok(existing) => existing != body,
Err(_) => true,
};
if needs_write {
std::fs::write(out_path, body).expect("failed to write known_values.rs");
}
}
fn extract_known_values(enums: &serde_json::Value, def_name: &str) -> Vec<String> {
let arr = enums
.get("$defs")
.and_then(|d| d.get(def_name))
.and_then(|d| d.get("x-known-values"))
.and_then(|v| v.as_array())
.unwrap_or_else(|| panic!("{def_name} must declare x-known-values as an array"));
arr.iter()
.map(|v| {
v.as_str()
.unwrap_or_else(|| panic!("{def_name} x-known-values entries must be strings"))
.to_string()
})
.collect()
}
fn rust_str_literal(s: &str) -> String {
let escaped: String = s
.chars()
.flat_map(|c| match c {
'"' => vec!['\\', '"'],
'\\' => vec!['\\', '\\'],
_ => vec![c],
})
.collect();
format!("\"{escaped}\"")
}