agentguard_core/
schema.rs

1//! Schema utilities: parse + introspection using cedar-policy's structured
2//! accessors.
3
4use crate::error::{Error, Result};
5use cedar_policy::Schema;
6use serde::{Deserialize, Serialize};
7
8/// A parsed schema, paired with its source text.
9#[derive(Debug, Clone)]
10pub struct SchemaParsed {
11    pub schema: cedar_policy::Schema,
12    pub source: String,
13}
14
15/// Parse a `cedarschema` source string into a `cedar_policy::Schema`.
16pub fn parse_schema(src: &str) -> Result<cedar_policy::Schema> {
17    let (s, _w) = cedar_policy::Schema::from_cedarschema_str(src)
18        .map_err(|e| Error::Schema(e.to_string()))?;
19    Ok(s)
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct EntityDef {
24    pub name: String,
25    pub kind: EntityKind,
26    pub attributes: Vec<AttrDef>,
27}
28
29#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
30#[serde(rename_all = "snake_case")]
31pub enum EntityKind {
32    Entity,
33    Action,
34}
35
36#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct AttrDef {
38    pub name: String,
39    pub ty: String,
40    pub required: bool,
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct SchemaSummary {
45    pub entities: Vec<EntityDef>,
46    pub actions: Vec<EntityDef>,
47}
48
49/// Build a [`SchemaSummary`] by walking cedar-policy's structured API.
50pub fn describe(src: &str) -> Result<SchemaSummary> {
51    let schema = parse_schema(src)?;
52    Ok(build_summary(&schema))
53}
54
55fn build_summary(schema: &Schema) -> SchemaSummary {
56    let mut entities = Vec::new();
57    for et in schema.entity_types() {
58        entities.push(entity_def(et));
59    }
60    let mut actions = Vec::new();
61    for a in schema.actions() {
62        actions.push(action_def(a));
63    }
64    SchemaSummary { entities, actions }
65}
66
67fn entity_def(et: &cedar_policy::EntityTypeName) -> EntityDef {
68    // The cedar-policy public API exposes the entity type name; attribute
69    // introspection goes through the AST-level ValidatorSchemaFragment which
70    // is gated behind unstable features. We report the type with no
71    // attributes here. Future work can extend this via the fragment API.
72    EntityDef {
73        name: et.to_string(),
74        kind: EntityKind::Entity,
75        attributes: vec![],
76    }
77}
78
79fn action_def(a: &cedar_policy::EntityUid) -> EntityDef {
80    // The cedar-policy public API exposes the action as an `EntityUid` like
81    // `Action::"ToolCall::send_email"`. Surface only the action id portion
82    // (matches the format Cedar uses in policy text).
83    let raw = a.to_string();
84    let name = raw
85        .strip_prefix("Action::\"")
86        .and_then(|s| s.strip_suffix('"'))
87        .unwrap_or(&raw)
88        .to_string();
89    EntityDef {
90        name,
91        kind: EntityKind::Action,
92        attributes: vec![],
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn describe_extracts_entity_and_action_names() {
102        let src = r#"
103entity User;
104entity Mailbox;
105action "ToolCall::send_email" appliesTo { principal: [User], resource: [Mailbox] };
106action "ToolCall::read_doc" appliesTo { principal: [User], resource: [Mailbox] };
107"#;
108        let s = describe(src).unwrap();
109        let names: Vec<&str> = s.entities.iter().map(|e| e.name.as_str()).collect();
110        assert!(names.contains(&"User"));
111        assert!(names.contains(&"Mailbox"));
112        let actions: Vec<&str> = s.actions.iter().map(|a| a.name.as_str()).collect();
113        assert!(actions.contains(&"ToolCall::send_email"));
114        assert!(actions.contains(&"ToolCall::read_doc"));
115    }
116
117    #[test]
118    fn describe_handles_malformed_schema() {
119        // parse_schema is strict; malformed input should error rather than
120        // silently producing empty results.
121        let res = describe("not a valid schema");
122        assert!(res.is_err());
123    }
124}