Skip to main content

typesec_rbac/
codegen.rs

1//! Code generator: RBAC YAML → Rust source.
2//!
3//! `typesec generate --policy rbac.yaml --out src/policy_gen.rs` calls
4//! [`generate_rust`] and writes the result to a file.
5//!
6//! ## What gets generated?
7//!
8//! For each role in the YAML, the generator emits:
9//!
10//! ```rust,ignore
11//! // Auto-generated by typesec-cli — DO NOT EDIT
12//! use typesec_core::role::Role;
13//!
14//! pub struct Analyst;
15//! impl Role for Analyst {
16//!     fn name() -> &'static str { "analyst" }
17//!     fn permission_names() -> &'static [&'static str] { &["read", "read_sensitive"] }
18//!     fn resource_patterns() -> &'static [&'static str] { &["reports/*", "metrics/*"] }
19//! }
20//! ```
21//!
22//! These typed role structs let the compiler verify that the roles you reference
23//! in application code actually exist in the policy file. If you rename a role in
24//! YAML and regenerate, any code referencing the old name will fail to compile.
25
26use crate::model::{RbacPolicy, walk_inheritance};
27
28/// Generate Rust source code for the roles defined in `policy`.
29///
30/// The output is a complete `.rs` file ready to be `include!`'d or written to disk.
31pub 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        // Build effective permission list (including inherited, flattened).
69        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        // Collect all resource patterns (own + inherited).
76        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
104/// Convert `snake_case` or `kebab-case` to `PascalCase`.
105fn 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
118/// Format a `Vec<String>` as a Rust `[&'static str; N]` literal.
119fn 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;