use crate::model::{RbacPolicy, walk_inheritance};
pub fn generate_rust(policy: &RbacPolicy) -> String {
let mut out = String::new();
out.push_str("// Auto-generated by typesec-cli — DO NOT EDIT\n");
out.push_str("// Source: RBAC policy file\n");
out.push_str("//\n");
out.push_str("// Each struct below corresponds to a role in the policy YAML.\n");
out.push_str("// Changing a role name in YAML and regenerating will cause compile\n");
out.push_str("// errors in any code that references the old struct name — which is\n");
out.push_str("// the point: the compiler enforces policy consistency.\n\n");
out.push_str("#[allow(dead_code, non_camel_case_types)]\n");
for role in &policy.roles {
let struct_name = to_pascal_case(&role.name);
out.push_str(&format!("/// Role `{}`.\n", role.name));
if !role.permissions.is_empty() {
out.push_str(&format!(
"/// Permissions: {}.\n",
role.permissions.join(", ")
));
}
if !role.resources.is_empty() {
out.push_str(&format!("/// Resources: {}.\n", role.resources.join(", ")));
}
out.push_str("#[derive(Debug, Clone, Copy)]\n");
out.push_str(&format!("pub struct {struct_name};\n\n"));
out.push_str(&format!(
"impl typesec_core::role::Role for {struct_name} {{\n"
));
out.push_str(&format!(
" fn name() -> &'static str {{ {:?} }}\n",
role.name
));
let effective_perms = collect_all_permissions(&role.name, policy);
let perms_literal = format_str_slice(&effective_perms);
out.push_str(&format!(
" fn permission_names() -> &'static [&'static str] {{ &{perms_literal} }}\n"
));
let effective_resources = collect_all_resources(&role.name, policy);
let resources_literal = format_str_slice(&effective_resources);
out.push_str(&format!(
" fn resource_patterns() -> &'static [&'static str] {{ &{resources_literal} }}\n"
));
out.push_str("}\n\n");
}
out
}
fn collect_all_permissions(role_name: &str, policy: &RbacPolicy) -> Vec<String> {
let mut perms = std::collections::BTreeSet::new();
let _ = walk_inheritance(role_name, policy, &mut |role| {
perms.extend(role.permissions.iter().cloned());
});
perms.into_iter().collect()
}
fn collect_all_resources(role_name: &str, policy: &RbacPolicy) -> Vec<String> {
let mut resources = std::collections::BTreeSet::new();
let _ = walk_inheritance(role_name, policy, &mut |role| {
resources.extend(role.resources.iter().cloned());
});
resources.into_iter().collect()
}
fn to_pascal_case(s: &str) -> String {
s.split(['_', '-'])
.filter(|p| !p.is_empty())
.map(|p| {
let mut chars = p.chars();
match chars.next() {
None => String::new(),
Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
}
})
.collect()
}
fn format_str_slice(items: &[String]) -> String {
let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
format!("[{}]", inner.join(", "))
}
#[cfg(test)]
mod tests;