use std::fs;
use std::path::Path;
fn main() {
let out_dir = std::env::var("OUT_DIR").unwrap();
let dest = Path::new(&out_dir).join("tier_interfaces.rs");
let mut generated = String::new();
let wit_dir = Path::new("wit");
if !wit_dir.is_dir() {
fs::write(&dest, "// No wit/ directory found during build.\n").unwrap();
return;
}
for entry in fs::read_dir(wit_dir).unwrap().filter_map(|e| e.ok()) {
if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
let world_path = entry.path().join("world.wit");
if world_path.exists() {
println!("cargo::rerun-if-changed={}", world_path.display());
}
}
let mut tier_dirs: Vec<_> = fs::read_dir(wit_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.filter(|e| {
e.file_name()
.to_str()
.map(|n| n.starts_with("tier"))
.unwrap_or(false)
})
.collect();
tier_dirs.sort_by_key(|e| e.file_name());
for dir_entry in &tier_dirs {
let dir_name = dir_entry.file_name();
let dir_name = dir_name.to_str().unwrap();
let tier_num = dir_name
.strip_prefix("tier")
.expect("directory name must start with 'tier'");
let world_path = dir_entry.path().join("world.wit");
if !world_path.exists() {
panic!(
"Expected {}/world.wit to exist for tier {}",
dir_entry.path().display(),
tier_num
);
}
println!("cargo::rerun-if-changed={}", world_path.display());
let wit_src = fs::read_to_string(&world_path).unwrap_or_else(|e| {
panic!("Failed to read {}: {e}", world_path.display());
});
let (pkg_unversioned, pkg_version) = parse_package_decl(&wit_src, &world_path);
let ifaces = parse_interfaces(&wit_src);
let iface_names: Vec<String> = ifaces.iter().map(|(n, _)| n.clone()).collect();
let fq_names: Vec<String> = iface_names
.iter()
.map(|name| format!("{pkg_unversioned}/{name}"))
.collect();
let upper = tier_num.to_uppercase();
generated.push_str(&format!(
"/// Package key for tier-{tier_num} interfaces (no version suffix).\n\
#[allow(dead_code)]\n\
pub const TIER{upper}_PACKAGE: &str = \"{pkg_unversioned}\";\n\n\
/// Semver version of the tier-{tier_num} WIT package.\n\
#[allow(dead_code)]\n\
pub const TIER{upper}_VERSION: &str = \"{pkg_version}\";\n\n"
));
for ((name, _fns), fq) in ifaces.iter().zip(fq_names.iter()) {
let iface_upper = name.to_uppercase().replace('-', "_");
let const_name = format!("TIER{upper}_{iface_upper}");
generated.push_str(&format!(
"/// Fully-qualified name of the `{name}` interface in the tier-{tier_num} WIT package.\n\
/// Derived from `wit/{dir_name}/world.wit` at build time.\n\
pub const {const_name}: &str = \"{fq}\";\n\n"
));
}
generated.push_str(&format!(
"/// All tier-{tier_num} interface names, for middleware detection.\n\
/// Derived from `wit/{dir_name}/world.wit` at build time.\n\
pub const TIER{upper}_INTERFACES: &[&str] = &[\n"
));
for name in &iface_names {
let iface_upper = name.to_uppercase().replace('-', "_");
let const_name = format!("TIER{upper}_{iface_upper}");
generated.push_str(&format!(" {const_name},\n"));
}
generated.push_str("];\n\n");
}
let common_path = wit_dir.join("common").join("world.wit");
if common_path.exists() {
println!("cargo::rerun-if-changed={}", common_path.display());
let wit_src = fs::read_to_string(&common_path).unwrap_or_else(|e| {
panic!("Failed to read {}: {e}", common_path.display());
});
let (pkg_unversioned, pkg_version) = parse_package_decl(&wit_src, &common_path);
generated.push_str(&format!(
"/// Package key for the shared `splicer:common` types (no version suffix).\n\
#[allow(dead_code)]\n\
pub const COMMON_PACKAGE: &str = \"{pkg_unversioned}\";\n\n\
/// Semver version of the `splicer:common` WIT package.\n\
#[allow(dead_code)]\n\
pub const COMMON_VERSION: &str = \"{pkg_version}\";\n\n"
));
}
validate_schema_names(wit_dir);
fs::write(&dest, &generated).unwrap();
generate_builtin_protocol(&out_dir);
generate_builtin_config_constants(&out_dir);
stage_embedded_strategies(&out_dir);
emit_sdk_test_version(&out_dir);
}
fn emit_sdk_test_version(out_dir: &str) {
let dest = Path::new(out_dir).join("sdk_test_version.rs");
let sdk_cargo = Path::new("splicer-tool-sdk").join("Cargo.toml");
let version = if sdk_cargo.is_file() {
println!("cargo::rerun-if-changed={}", sdk_cargo.display());
let src = fs::read_to_string(&sdk_cargo)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", sdk_cargo.display()));
parse_cargo_package_version(&src, &sdk_cargo)
} else {
"0.0.0".to_string()
};
fs::write(
&dest,
format!("pub const SDK_TEST_VERSION: &str = \"{version}\";\n"),
)
.unwrap();
}
fn stage_embedded_strategies(out_dir: &str) {
let manifest_dir =
std::env::var("CARGO_MANIFEST_DIR").expect("CARGO_MANIFEST_DIR set by cargo");
let builtins_root = Path::new(&manifest_dir).join("builtins");
let staged_root = Path::new(out_dir).join("embedded-strategies");
let mut names: Vec<String> = discover_source_dist_strategies(&builtins_root);
names.sort();
for name in &names {
let src_root = builtins_root.join(name);
let dest_root = staged_root.join(name);
rerun_for_strategy_sources(&src_root);
if dest_root.exists() {
fs::remove_dir_all(&dest_root).unwrap_or_else(|e| {
panic!("Failed to clean {}: {e}", dest_root.display());
});
}
fs::create_dir_all(&dest_root).unwrap_or_else(|e| {
panic!("Failed to create {}: {e}", dest_root.display());
});
stage_strategy_dir(&src_root, &dest_root);
}
emit_embedded_strategies_rs(out_dir, &names);
}
const STRATEGY_TIERS: &[i64] = &[3, 4];
fn discover_source_dist_strategies(builtins_root: &Path) -> Vec<String> {
let mut out = Vec::new();
let Ok(entries) = fs::read_dir(builtins_root) else {
return out;
};
for entry in entries.filter_map(|e| e.ok()) {
if !entry.file_type().map(|t| t.is_dir()).unwrap_or(false) {
continue;
}
let manifest_path = entry.path().join("manifest.toml");
if !manifest_path.is_file() {
continue;
}
println!("cargo::rerun-if-changed={}", manifest_path.display());
let text = fs::read_to_string(&manifest_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", manifest_path.display()));
let parsed: toml::Value = toml::from_str(&text)
.unwrap_or_else(|e| panic!("Failed to parse {}: {e}", manifest_path.display()));
let tier = parsed
.get("builtin")
.and_then(|b| b.get("tier"))
.and_then(|t| t.as_integer());
if let Some(t) = tier {
if STRATEGY_TIERS.contains(&t) {
out.push(entry.file_name().to_string_lossy().into_owned());
}
}
}
out
}
fn emit_embedded_strategies_rs(out_dir: &str, names: &[String]) {
let dest = Path::new(out_dir).join("embedded_strategies.rs");
let mut content =
String::from("// Auto-generated by build.rs from builtins/*/manifest.toml.\n\n");
for name in names {
let upper = name.to_uppercase().replace('-', "_");
content.push_str(&format!(
"static {upper}: ::include_dir::Dir<'_> = \
::include_dir::include_dir!(\"$OUT_DIR/embedded-strategies/{name}\");\n",
));
}
content.push_str("\nstatic EMBEDDED: &[(&str, &::include_dir::Dir<'_>)] = &[\n");
for name in names {
let upper = name.to_uppercase().replace('-', "_");
content.push_str(&format!(" (\"{name}\", &{upper}),\n"));
}
content.push_str("];\n");
fs::write(&dest, content).unwrap_or_else(|e| panic!("Failed to write {}: {e}", dest.display()));
}
fn rerun_for_strategy_sources(src_root: &Path) {
println!("cargo::rerun-if-changed={}", src_root.display());
for name in ["Cargo.toml", "Cargo.toml.embed", "manifest.toml"] {
let p = src_root.join(name);
if p.exists() {
println!("cargo::rerun-if-changed={}", p.display());
}
}
let src_dir = src_root.join("src");
if src_dir.is_dir() {
walk_for_rerun(&src_dir);
}
}
fn walk_for_rerun(dir: &Path) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.filter_map(|e| e.ok()) {
let ft = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
let path = entry.path();
if ft.is_dir() {
walk_for_rerun(&path);
} else if ft.is_file() {
println!("cargo::rerun-if-changed={}", path.display());
}
}
}
fn stage_strategy_dir(src_root: &Path, dest_root: &Path) {
let cargo_src = if src_root.join("Cargo.toml").is_file() {
src_root.join("Cargo.toml")
} else if src_root.join("Cargo.toml.embed").is_file() {
src_root.join("Cargo.toml.embed")
} else {
panic!(
"Strategy crate at {} has neither Cargo.toml nor Cargo.toml.embed",
src_root.display()
);
};
let cargo_text = fs::read_to_string(&cargo_src)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", cargo_src.display()));
let normalized = normalize_strategy_cargo_toml(&cargo_text, &cargo_src);
fs::write(dest_root.join("Cargo.toml"), normalized).unwrap_or_else(|e| {
panic!(
"Failed to write staged Cargo.toml for {}: {e}",
src_root.display()
);
});
for entry in fs::read_dir(src_root)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", src_root.display()))
.filter_map(|e| e.ok())
{
let name = entry.file_name();
let name_str = name.to_string_lossy();
if matches!(
name_str.as_ref(),
"Cargo.toml" | "Cargo.toml.embed" | "Cargo.lock" | "target"
) || name_str.starts_with('.')
{
continue;
}
let src_path = entry.path();
let dest_path = dest_root.join(&name);
let ft = entry.file_type().unwrap_or_else(|e| {
panic!("Failed to stat {}: {e}", src_path.display());
});
if ft.is_dir() {
copy_dir_recursive(&src_path, &dest_path);
} else if ft.is_file() {
fs::copy(&src_path, &dest_path).unwrap_or_else(|e| {
panic!(
"Failed to copy {} -> {}: {e}",
src_path.display(),
dest_path.display()
);
});
}
}
}
fn copy_dir_recursive(src: &Path, dest: &Path) {
fs::create_dir_all(dest).unwrap_or_else(|e| {
panic!("Failed to create {}: {e}", dest.display());
});
let Ok(entries) = fs::read_dir(src) else {
return;
};
for entry in entries.filter_map(|e| e.ok()) {
let ft = match entry.file_type() {
Ok(ft) => ft,
Err(_) => continue,
};
let name = entry.file_name();
let src_path = entry.path();
let dest_path = dest.join(&name);
if ft.is_dir() {
copy_dir_recursive(&src_path, &dest_path);
} else if ft.is_file() {
fs::copy(&src_path, &dest_path).unwrap_or_else(|e| {
panic!(
"Failed to copy {} -> {}: {e}",
src_path.display(),
dest_path.display()
);
});
}
}
}
fn normalize_strategy_cargo_toml(src: &str, src_path: &Path) -> String {
let mut parsed: toml::Value = toml::from_str(src).unwrap_or_else(|e| {
panic!("Failed to parse {} as TOML: {e}", src_path.display());
});
let version = parsed
.get("dependencies")
.and_then(|d| d.get("splicer-tool-sdk"))
.and_then(|d| match d {
toml::Value::String(s) => Some(s.clone()),
toml::Value::Table(t) => t
.get("version")
.and_then(|v| v.as_str())
.map(str::to_string),
_ => None,
})
.unwrap_or_else(|| {
panic!(
"Strategy {} must declare splicer-tool-sdk = \"<version>\" \
(or `{{ version = \"<version>\", path = \"...\" }}`) in [dependencies]",
src_path.display(),
);
});
if let Some(deps) = parsed
.get_mut("dependencies")
.and_then(|d| d.as_table_mut())
{
deps.insert("splicer-tool-sdk".to_string(), toml::Value::String(version));
}
toml::to_string(&parsed).unwrap_or_else(|e| {
panic!(
"Failed to re-serialize normalized Cargo.toml for {}: {e}",
src_path.display()
);
})
}
fn generate_builtin_protocol(out_dir: &str) {
let dest = Path::new(out_dir).join("builtin_protocol.rs");
let builtins_dir = Path::new("builtins");
println!("cargo::rerun-if-changed=builtins");
let mut rows = String::new();
if builtins_dir.is_dir() {
let mut crate_dirs: Vec<_> = fs::read_dir(builtins_dir)
.unwrap()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().map(|t| t.is_dir()).unwrap_or(false))
.collect();
crate_dirs.sort_by_key(|e| e.file_name());
for entry in crate_dirs {
let entry_path = entry.path();
let cargo_toml = entry_path.join("Cargo.toml");
let cargo_embed = entry_path.join("Cargo.toml.embed");
let manifest_path = if cargo_toml.is_file() {
cargo_toml
} else if cargo_embed.is_file() {
cargo_embed
} else {
continue;
};
if !entry_path.join("wit").is_dir() {
continue;
}
println!("cargo::rerun-if-changed={}", manifest_path.display());
for sub in ["src", "wit"] {
let dir = entry_path.join(sub);
if dir.is_dir() {
walk_for_rerun(&dir);
}
}
let mtoml = entry_path.join("manifest.toml");
if mtoml.is_file() {
println!("cargo::rerun-if-changed={}", mtoml.display());
}
let src = fs::read_to_string(&manifest_path).unwrap_or_else(|e| {
panic!("Failed to read {}: {e}", manifest_path.display());
});
let version = parse_cargo_package_version(&src, &manifest_path);
let name = entry.file_name().to_string_lossy().into_owned();
rows.push_str(&format!(" (\"{name}\", \"{version}\"),\n"));
}
}
let content = format!(
"// Auto-generated by build.rs from builtins/*/Cargo.toml(.embed). Do not edit.\n&[\n{rows}]\n"
);
fs::write(&dest, content).unwrap();
}
fn parse_cargo_package_version(src: &str, path: &Path) -> String {
let mut in_package = false;
for line in src.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
in_package = trimmed == "[package]";
continue;
}
if !in_package {
continue;
}
let Some(rest) = trimmed.strip_prefix("version") else {
continue;
};
let Some(rest) = rest.trim_start().strip_prefix('=') else {
continue;
};
let rest = rest.trim_start();
let Some(rest) = rest.strip_prefix('"') else {
panic!(
"[package] version in {} is not a quoted string: {trimmed:?}",
path.display()
);
};
let Some(end) = rest.find('"') else {
panic!(
"[package] version in {} has unterminated quote: {trimmed:?}",
path.display()
);
};
let v = &rest[..end];
if v.is_empty() {
panic!("Empty version in [package] of {}", path.display());
}
return v.to_string();
}
panic!(
"No `version = \"...\"` found in [package] of {}",
path.display()
);
}
fn generate_builtin_config_constants(out_dir: &str) {
let dest = Path::new(out_dir).join("builtin_config_constants.rs");
let wit_path = Path::new("wit").join("builtin-config").join("world.wit");
if !wit_path.exists() {
fs::write(&dest, "// No wit/builtin-config/world.wit found.\n").unwrap();
return;
}
println!("cargo::rerun-if-changed={}", wit_path.display());
let wit_src = fs::read_to_string(&wit_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", wit_path.display()));
let (pkg, version) = parse_package_decl(&wit_src, &wit_path);
let ifaces = parse_interfaces(&wit_src);
let mut content = String::from(
"// Auto-generated by build.rs from wit/builtin-config/world.wit. Do not edit.\n\n",
);
content.push_str(&format!(
"#[allow(dead_code)]\npub const BUILTIN_CONFIG_PACKAGE: &str = \"{pkg}\";\n\n",
));
content.push_str(&format!(
"#[allow(dead_code)]\npub const BUILTIN_CONFIG_VERSION: &str = \"{version}\";\n\n",
));
let mut iface_consts: Vec<String> = Vec::new();
for (name, _) in &ifaces {
let upper = name.to_uppercase().replace('-', "_");
let unversioned = format!("{pkg}/{name}");
let versioned = format!("{unversioned}@{version}");
let unver_const = format!("BUILTIN_CONFIG_{upper}");
let ver_const = format!("BUILTIN_CONFIG_{upper}_VERSIONED");
content.push_str(&format!(
"#[allow(dead_code)]\npub const {unver_const}: &str = \"{unversioned}\";\n\n",
));
content.push_str(&format!(
"#[allow(dead_code)]\npub const {ver_const}: &str = \"{versioned}\";\n\n",
));
iface_consts.push(unver_const);
}
content.push_str("#[allow(dead_code)]\npub const BUILTIN_CONFIG_INTERFACES: &[&str] = &[\n");
for c in &iface_consts {
content.push_str(&format!(" {c},\n"));
}
content.push_str("];\n");
fs::write(&dest, content).unwrap();
}
fn validate_schema_names(wit_dir: &Path) {
let common_path = wit_dir.join("common").join("world.wit");
if !common_path.exists() {
return;
}
let common_src = fs::read_to_string(&common_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", common_path.display()));
let common_records: &[(&str, &[&str])] = &[
("field", &["name", "tree"]),
(
"field-tree",
&[
"cells",
"record-infos",
"flags-infos",
"enum-infos",
"variant-infos",
"handle-infos",
"root",
],
),
("call-id", &["interface-name", "function-name"]),
("enum-info", &["type-name", "case-name"]),
("record-info", &["type-name", "fields"]),
];
for (name, fields) in common_records {
require_record_with_fields(&common_src, &common_path, name, fields);
}
require_typedef(&common_src, &common_path, "variant", "cell");
let tier2_path = wit_dir.join("tier2").join("world.wit");
if !tier2_path.exists() {
return;
}
let tier2_src = fs::read_to_string(&tier2_path)
.unwrap_or_else(|e| panic!("Failed to read {}: {e}", tier2_path.display()));
require_func_params(&tier2_src, &tier2_path, "on-call", &["call", "args"]);
require_func_params(&tier2_src, &tier2_path, "on-return", &["call", "result"]);
}
fn require_typedef(src: &str, path: &Path, kind: &str, name: &str) {
if extract_typedef_body(src, kind, name).is_none() {
panic!(
"Schema mismatch: `{kind} {name}` not found in {}.\n\
The Rust adapter codegen (src/adapter/tier2/emit.rs) references this typedef.\n\
Either restore the WIT typedef, or update the constants in emit.rs and build.rs.",
path.display()
);
}
}
fn require_record_with_fields(src: &str, path: &Path, name: &str, fields: &[&str]) {
let body = extract_typedef_body(src, "record", name).unwrap_or_else(|| {
panic!(
"Schema mismatch: `record {name}` not found in {}.\n\
The Rust adapter codegen (src/adapter/tier2/emit.rs) references this typedef.\n\
Either restore the WIT typedef, or update the constants in emit.rs and build.rs.",
path.display()
)
});
for field in fields {
if !record_body_has_field(&body, field) {
panic!(
"Schema mismatch: `record {name}` in {} is missing field `{field}`.\n\
The Rust adapter codegen (src/adapter/tier2/emit.rs) references this field.\n\
Either restore the WIT field, or update the constants in emit.rs and build.rs.",
path.display()
);
}
}
}
fn require_func_params(src: &str, path: &Path, fn_name: &str, params: &[&str]) {
let decl = extract_func_decl(src, fn_name).unwrap_or_else(|| {
panic!(
"Schema mismatch: function `{fn_name}` not found in {}.\n\
The Rust adapter codegen (src/adapter/tier2/emit.rs) references it.",
path.display()
)
});
let param_names = parse_func_param_names(&decl);
for expected in params {
let canonical = expected.strip_prefix('%').unwrap_or(expected);
if !param_names.iter().any(|n| n == canonical) {
panic!(
"Schema mismatch: function `{fn_name}` in {} is missing param `{canonical}`. \
Found params: {:?}\n\
The Rust adapter codegen (src/adapter/tier2/emit.rs) references this param.\n\
Either restore the WIT param, or update the constants in emit.rs and build.rs.",
path.display(),
param_names,
);
}
}
}
fn extract_typedef_body(src: &str, kind: &str, name: &str) -> Option<String> {
let header_prefix = format!("{kind} {name}");
let mut depth: i32 = 0;
let mut body = String::new();
let mut found = false;
for line in src.lines() {
let trimmed = line.trim();
if !found {
if let Some(rest) = trimmed.strip_prefix(&header_prefix) {
let rest = rest.trim_start();
if let Some(after_brace) = rest.strip_prefix('{') {
found = true;
depth = 1;
if !after_brace.trim().is_empty() {
body.push_str(after_brace);
body.push('\n');
}
}
}
continue;
}
for ch in trimmed.chars() {
match ch {
'{' => depth += 1,
'}' => depth -= 1,
_ => {}
}
}
if depth <= 0 {
let close = trimmed.rfind('}').unwrap_or(trimmed.len());
body.push_str(&trimmed[..close]);
return Some(body);
}
body.push_str(trimmed);
body.push('\n');
}
if found {
Some(body)
} else {
None
}
}
fn record_body_has_field(body: &str, field: &str) -> bool {
for line in body.lines() {
let trimmed = line.trim();
let trimmed = trimmed.strip_prefix('%').unwrap_or(trimmed);
if let Some(rest) = trimmed.strip_prefix(field) {
let next = rest.chars().next();
if next == Some(':') || next.map(|c| c.is_whitespace()).unwrap_or(false) {
let after = rest.trim_start();
if after.starts_with(':') {
return true;
}
}
}
}
false
}
fn extract_func_decl(src: &str, fn_name: &str) -> Option<String> {
for line in src.lines() {
let trimmed = line.trim();
let Some(rest) = trimmed.strip_prefix(fn_name) else {
continue;
};
let Some(rest) = rest.trim_start().strip_prefix(':') else {
continue;
};
let rest = rest.trim_start();
let rest = rest.strip_prefix("async ").unwrap_or(rest);
if rest.starts_with("func(") || rest.starts_with("func (") {
return Some(trimmed.to_string());
}
}
None
}
fn parse_func_param_names(decl: &str) -> Vec<String> {
let Some(open) = decl.find('(') else {
return Vec::new();
};
let Some(close) = decl[open + 1..].rfind(')') else {
return Vec::new();
};
let inside = &decl[open + 1..open + 1 + close];
let mut depth: i32 = 0;
let mut params: Vec<String> = Vec::new();
let mut current = String::new();
for ch in inside.chars() {
match ch {
'<' | '(' | '{' => {
depth += 1;
current.push(ch);
}
'>' | ')' | '}' => {
depth -= 1;
current.push(ch);
}
',' if depth == 0 => {
if let Some(n) = parse_one_param_name(¤t) {
params.push(n);
}
current.clear();
}
_ => current.push(ch),
}
}
if !current.trim().is_empty() {
if let Some(n) = parse_one_param_name(¤t) {
params.push(n);
}
}
params
}
fn parse_one_param_name(s: &str) -> Option<String> {
let s = s.trim();
let (name, _ty) = s.split_once(':')?;
let name = name.trim();
if name.is_empty() {
return None;
}
Some(name.strip_prefix('%').unwrap_or(name).to_string())
}
fn parse_package_decl(src: &str, path: &Path) -> (String, String) {
for line in src.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("package ") {
let rest = rest.trim().trim_end_matches(';').trim();
if let Some((pkg, ver)) = rest.split_once('@') {
return (pkg.to_string(), ver.to_string());
}
panic!(
"Package declaration in {} missing version: '{}'",
path.display(),
line
);
}
}
panic!("No `package` declaration found in {}", path.display());
}
fn parse_interfaces(src: &str) -> Vec<(String, Vec<String>)> {
let mut out: Vec<(String, Vec<String>)> = Vec::new();
let mut current: Option<(String, Vec<String>)> = None;
for line in src.lines() {
let line = line.trim();
if let Some(rest) = line.strip_prefix("interface ") {
if let Some(prev) = current.take() {
out.push(prev);
}
let name = rest
.split_whitespace()
.next()
.unwrap_or("")
.trim_end_matches('{')
.to_string();
if !name.is_empty() {
current = Some((name, Vec::new()));
}
continue;
}
if line == "}" {
if let Some(iface) = current.take() {
out.push(iface);
}
continue;
}
if let Some((_, ref mut fns)) = current.as_mut() {
if let Some(fn_name) = parse_fn_decl_name(line) {
fns.push(fn_name);
}
}
}
if let Some(iface) = current {
out.push(iface);
}
out
}
fn parse_fn_decl_name(line: &str) -> Option<String> {
let (lhs, rhs) = line.split_once(':')?;
let rhs = rhs.trim();
let rhs = rhs.strip_prefix("async ").unwrap_or(rhs);
if !rhs.starts_with("func(") && !rhs.starts_with("func ") {
return None;
}
let name = lhs.trim();
if name.is_empty() || name.contains(char::is_whitespace) {
return None;
}
Some(name.to_string())
}