use crate::model::RbacPolicy;
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 seen_roles = std::collections::HashSet::new();
let mut perms = std::collections::BTreeSet::new();
collect_perms_inner(role_name, policy, &mut seen_roles, &mut perms);
perms.into_iter().collect()
}
fn collect_perms_inner(
role_name: &str,
policy: &RbacPolicy,
seen: &mut std::collections::HashSet<String>,
out: &mut std::collections::BTreeSet<String>,
) {
if !seen.insert(role_name.to_owned()) {
return;
}
if let Some(role) = policy.roles.iter().find(|r| r.name == role_name) {
for p in &role.permissions {
out.insert(p.clone());
}
for parent in &role.inherits {
collect_perms_inner(parent, policy, seen, out);
}
}
}
fn collect_all_resources(role_name: &str, policy: &RbacPolicy) -> Vec<String> {
let mut seen_roles = std::collections::HashSet::new();
let mut resources = std::collections::BTreeSet::new();
collect_resources_inner(role_name, policy, &mut seen_roles, &mut resources);
resources.into_iter().collect()
}
fn collect_resources_inner(
role_name: &str,
policy: &RbacPolicy,
seen: &mut std::collections::HashSet<String>,
out: &mut std::collections::BTreeSet<String>,
) {
if !seen.insert(role_name.to_owned()) {
return;
}
if let Some(role) = policy.roles.iter().find(|r| r.name == role_name) {
for r in &role.resources {
out.insert(r.clone());
}
for parent in &role.inherits {
collect_resources_inner(parent, policy, seen, out);
}
}
}
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 {
use super::*;
use crate::model::RbacPolicy;
const YAML: &str = r#"
roles:
- name: analyst
permissions: [read, read_sensitive]
resources: ["reports/*"]
- name: admin
inherits: [analyst]
permissions: [write, delete]
resources: ["*"]
assignments: []
"#;
#[test]
fn generates_rust_structs() {
let policy = RbacPolicy::from_yaml(YAML).expect("parse ok");
let code = generate_rust(&policy);
assert!(code.contains("pub struct Analyst"));
assert!(code.contains("pub struct Admin"));
assert!(code.contains("fn name() -> &'static str { \"analyst\" }"));
assert!(code.contains("read_sensitive"));
}
#[test]
fn pascal_case_conversion() {
assert_eq!(super::to_pascal_case("data_analyst"), "DataAnalyst");
assert_eq!(super::to_pascal_case("deploy-bot"), "DeployBot");
assert_eq!(super::to_pascal_case("admin"), "Admin");
}
}