1use crate::model::RbacPolicy;
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 seen_roles = std::collections::HashSet::new();
90 let mut perms = std::collections::BTreeSet::new();
91 collect_perms_inner(role_name, policy, &mut seen_roles, &mut perms);
92 perms.into_iter().collect()
93}
94
95fn collect_perms_inner(
96 role_name: &str,
97 policy: &RbacPolicy,
98 seen: &mut std::collections::HashSet<String>,
99 out: &mut std::collections::BTreeSet<String>,
100) {
101 if !seen.insert(role_name.to_owned()) {
102 return;
103 }
104 if let Some(role) = policy.roles.iter().find(|r| r.name == role_name) {
105 for p in &role.permissions {
106 out.insert(p.clone());
107 }
108 for parent in &role.inherits {
109 collect_perms_inner(parent, policy, seen, out);
110 }
111 }
112}
113
114fn collect_all_resources(role_name: &str, policy: &RbacPolicy) -> Vec<String> {
115 let mut seen_roles = std::collections::HashSet::new();
116 let mut resources = std::collections::BTreeSet::new();
117 collect_resources_inner(role_name, policy, &mut seen_roles, &mut resources);
118 resources.into_iter().collect()
119}
120
121fn collect_resources_inner(
122 role_name: &str,
123 policy: &RbacPolicy,
124 seen: &mut std::collections::HashSet<String>,
125 out: &mut std::collections::BTreeSet<String>,
126) {
127 if !seen.insert(role_name.to_owned()) {
128 return;
129 }
130 if let Some(role) = policy.roles.iter().find(|r| r.name == role_name) {
131 for r in &role.resources {
132 out.insert(r.clone());
133 }
134 for parent in &role.inherits {
135 collect_resources_inner(parent, policy, seen, out);
136 }
137 }
138}
139
140fn to_pascal_case(s: &str) -> String {
142 s.split(['_', '-'])
143 .filter(|p| !p.is_empty())
144 .map(|p| {
145 let mut chars = p.chars();
146 match chars.next() {
147 None => String::new(),
148 Some(c) => c.to_uppercase().collect::<String>() + chars.as_str(),
149 }
150 })
151 .collect()
152}
153
154fn format_str_slice(items: &[String]) -> String {
156 let inner: Vec<String> = items.iter().map(|s| format!("{s:?}")).collect();
157 format!("[{}]", inner.join(", "))
158}
159
160#[cfg(test)]
161mod tests {
162 use super::*;
163 use crate::model::RbacPolicy;
164
165 const YAML: &str = r#"
166roles:
167 - name: analyst
168 permissions: [read, read_sensitive]
169 resources: ["reports/*"]
170 - name: admin
171 inherits: [analyst]
172 permissions: [write, delete]
173 resources: ["*"]
174assignments: []
175"#;
176
177 #[test]
178 fn generates_rust_structs() {
179 let policy = RbacPolicy::from_yaml(YAML).expect("parse ok");
180 let code = generate_rust(&policy);
181 assert!(code.contains("pub struct Analyst"));
182 assert!(code.contains("pub struct Admin"));
183 assert!(code.contains("fn name() -> &'static str { \"analyst\" }"));
184 assert!(code.contains("read_sensitive"));
186 }
187
188 #[test]
189 fn pascal_case_conversion() {
190 assert_eq!(super::to_pascal_case("data_analyst"), "DataAnalyst");
191 assert_eq!(super::to_pascal_case("deploy-bot"), "DeployBot");
192 assert_eq!(super::to_pascal_case("admin"), "Admin");
193 }
194}