1use crate::model::{RbacPolicy, walk_inheritance};
27
28pub fn generate_rust(policy: &RbacPolicy) -> String {
32 let mut out = String::new();
33
34 out.push_str("// Auto-generated by typesec-cli — DO NOT EDIT\n");
35 out.push_str("// Source: RBAC policy file\n");
36 out.push_str("//\n");
37 out.push_str("// Each struct below corresponds to a role in the policy YAML.\n");
38 out.push_str("// Changing a role name in YAML and regenerating will cause compile\n");
39 out.push_str("// errors in any code that references the old struct name — which is\n");
40 out.push_str("// the point: the compiler enforces policy consistency.\n\n");
41 out.push_str("#[allow(dead_code, non_camel_case_types)]\n");
42
43 for role in &policy.roles {
44 let struct_name = to_pascal_case(&role.name);
45
46 out.push_str(&format!("/// Role `{}`.\n", role.name));
47 if !role.permissions.is_empty() {
48 out.push_str(&format!(
49 "/// Permissions: {}.\n",
50 role.permissions.join(", ")
51 ));
52 }
53 if !role.resources.is_empty() {
54 out.push_str(&format!("/// Resources: {}.\n", role.resources.join(", ")));
55 }
56
57 out.push_str("#[derive(Debug, Clone, Copy)]\n");
58 out.push_str(&format!("pub struct {struct_name};\n\n"));
59
60 out.push_str(&format!(
61 "impl typesec_core::role::Role for {struct_name} {{\n"
62 ));
63 out.push_str(&format!(
64 " fn name() -> &'static str {{ {:?} }}\n",
65 role.name
66 ));
67
68 let effective_perms = collect_all_permissions(&role.name, policy);
70 let perms_literal = format_str_slice(&effective_perms);
71 out.push_str(&format!(
72 " fn permission_names() -> &'static [&'static str] {{ &{perms_literal} }}\n"
73 ));
74
75 let effective_resources = collect_all_resources(&role.name, policy);
77 let resources_literal = format_str_slice(&effective_resources);
78 out.push_str(&format!(
79 " fn resource_patterns() -> &'static [&'static str] {{ &{resources_literal} }}\n"
80 ));
81
82 out.push_str("}\n\n");
83 }
84
85 out
86}
87
88fn collect_all_permissions(role_name: &str, policy: &RbacPolicy) -> Vec<String> {
89 let mut perms = std::collections::BTreeSet::new();
90 let _ = walk_inheritance(role_name, policy, &mut |role| {
91 perms.extend(role.permissions.iter().cloned());
92 });
93 perms.into_iter().collect()
94}
95
96fn collect_all_resources(role_name: &str, policy: &RbacPolicy) -> Vec<String> {
97 let mut resources = std::collections::BTreeSet::new();
98 let _ = walk_inheritance(role_name, policy, &mut |role| {
99 resources.extend(role.resources.iter().cloned());
100 });
101 resources.into_iter().collect()
102}
103
104fn to_pascal_case(s: &str) -> String {
106 s.split(['_', '-'])
107 .filter(|p| !p.is_empty())
108 .map(|p| {
109 let mut chars = p.chars();
110 match chars.next() {
111 None => String::new(),
112 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
113 }
114 })
115 .collect()
116}
117
118fn format_str_slice(items: &[String]) -> String {
120 let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
121 format!("[{}]", inner.join(", "))
122}
123
124#[cfg(test)]
125mod tests;