use serde_json::Value;
use std::fs;
use std::path::{Path, PathBuf};
struct EnumSpec {
rust_name: &'static str,
swift_name: &'static str,
schema_pointer: &'static str,
}
const ENUMS: &[EnumSpec] = &[
EnumSpec {
rust_name: "PatchType",
swift_name: "GeneratedWirePatchType",
schema_pointer: "/$defs/ReloadPatch/properties/patch_type/enum",
},
EnumSpec {
rust_name: "ReloadDecision",
swift_name: "GeneratedWireReloadDecision",
schema_pointer: "/$defs/ReloadDecision/enum",
},
];
fn main() -> Result<(), Box<dyn std::error::Error>> {
let root = workspace_root()?;
let schema_path = root.join("shared/protocol/events.schema.json");
let schema_raw = fs::read_to_string(&schema_path)?;
let schema: Value = serde_json::from_str(&schema_raw)?;
let rust_out = root.join("in-cli/src/hotreload/generated_protocol.rs");
let swift_out =
root.join("runtime/swift-preview-host/Sources/SwiftPreviewHost/GeneratedProtocol.swift");
write_rust(&rust_out, &schema, ENUMS)?;
write_swift(&swift_out, &schema, ENUMS)?;
println!("generated: {}", rust_out.display());
println!("generated: {}", swift_out.display());
Ok(())
}
fn workspace_root() -> Result<PathBuf, Box<dyn std::error::Error>> {
if let Some(arg) = std::env::args().nth(1) {
return Ok(PathBuf::from(arg));
}
let mut dir = std::env::current_dir()?;
loop {
if dir.join("shared/protocol/events.schema.json").exists() {
return Ok(dir);
}
if !dir.pop() {
break;
}
}
Err(
"could not find repository root containing shared/protocol/events.schema.json (pass root as argv[1])"
.into(),
)
}
fn parse_enum_values(
schema: &Value,
pointer: &str,
) -> Result<Vec<String>, Box<dyn std::error::Error>> {
let arr = schema
.pointer(pointer)
.and_then(|v| v.as_array())
.ok_or_else(|| format!("schema missing {pointer}"))?;
let mut out = Vec::new();
for item in arr {
let s = item.as_str().ok_or("enum values must be strings")?;
out.push(s.to_string());
}
if out.is_empty() {
return Err(format!("enum at {pointer} is empty").into());
}
Ok(out)
}
fn snake_to_pascal(value: &str) -> String {
value
.split('_')
.filter(|p| !p.is_empty())
.map(|part| {
let mut c = part.chars();
match c.next() {
None => String::new(),
Some(f) => f.to_uppercase().collect::<String>() + c.as_str(),
}
})
.collect()
}
fn snake_to_camel(value: &str) -> String {
let pascal = snake_to_pascal(value);
let mut ch = pascal.chars();
match ch.next() {
None => String::new(),
Some(f) => f.to_lowercase().collect::<String>() + ch.as_str(),
}
}
fn write_rust(
out_path: &Path,
schema: &Value,
enums: &[EnumSpec],
) -> Result<(), Box<dyn std::error::Error>> {
let mut lines: Vec<String> = vec![
"// AUTO-GENERATED by inauguration protocol-gen (Rust)".to_string(),
"// Source: shared/protocol/events.schema.json".to_string(),
String::new(),
];
for spec in enums {
let values = parse_enum_values(schema, spec.schema_pointer)?;
lines.push(
"#[derive(Debug, Clone, serde::Serialize, serde::Deserialize, PartialEq, Eq)]"
.to_string(),
);
lines.push("#[serde(rename_all = \"snake_case\")]".to_string());
lines.push(format!("pub enum {} {{", spec.rust_name));
for v in &values {
lines.push(format!(" {},", snake_to_pascal(v)));
}
lines.push("}".to_string());
lines.push(String::new());
}
fs::write(out_path, lines.join("\n"))?;
Ok(())
}
fn write_swift(
out_path: &Path,
schema: &Value,
enums: &[EnumSpec],
) -> Result<(), Box<dyn std::error::Error>> {
let mut lines: Vec<String> = vec![
"// AUTO-GENERATED by inauguration protocol-gen (Rust)".to_string(),
"// Source: shared/protocol/events.schema.json".to_string(),
String::new(),
];
for spec in enums {
let values = parse_enum_values(schema, spec.schema_pointer)?;
lines.push(format!(
"public enum {}: String, Codable, Sendable {{",
spec.swift_name
));
for v in &values {
lines.push(format!(" case {} = \"{}\"", snake_to_camel(v), v));
}
lines.push("}".to_string());
lines.push(String::new());
}
fs::write(out_path, lines.join("\n"))?;
Ok(())
}