cedar_policy_validator/human_schema/
fmt.rs

1use std::{collections::HashSet, fmt::Display};
2
3use itertools::Itertools;
4use miette::Diagnostic;
5use nonempty::NonEmpty;
6use smol_str::SmolStr;
7use thiserror::Error;
8
9use crate::{
10    ActionType, EntityType, NamespaceDefinition, SchemaFragment, SchemaType, SchemaTypeVariant,
11};
12
13impl Display for SchemaFragment {
14    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
15        for (ns, def) in &self.0 {
16            if ns.is_empty() {
17                write!(f, "{def}")?
18            } else {
19                write!(f, "namespace {ns} {{\n{def}}}")?
20            }
21        }
22        Ok(())
23    }
24}
25
26impl Display for NamespaceDefinition {
27    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
28        for (n, ty) in &self.common_types {
29            writeln!(f, "type {n} = {ty};")?
30        }
31        for (n, ty) in &self.entity_types {
32            writeln!(f, "entity {n}{ty};")?
33        }
34        for (n, a) in &self.actions {
35            writeln!(f, "action \"{}\"{a};", n.escape_debug())?
36        }
37        Ok(())
38    }
39}
40
41impl Display for SchemaType {
42    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
43        match self {
44            SchemaType::Type(ty) => match ty {
45                SchemaTypeVariant::Boolean => write!(f, "__cedar::Bool"),
46                SchemaTypeVariant::Entity { name } => write!(f, "{name}"),
47                SchemaTypeVariant::Extension { name } => write!(f, "__cedar::{name}"),
48                SchemaTypeVariant::Long => write!(f, "__cedar::Long"),
49                SchemaTypeVariant::Record {
50                    attributes,
51                    additional_attributes: _,
52                } => {
53                    write!(f, "{{")?;
54                    for (i, (n, ty)) in attributes.iter().enumerate() {
55                        write!(
56                            f,
57                            "\"{}\"{}: {}",
58                            n.escape_debug(),
59                            if ty.required { "" } else { "?" },
60                            ty.ty
61                        )?;
62                        if i < (attributes.len() - 1) {
63                            write!(f, ", ")?;
64                        }
65                    }
66                    write!(f, "}}")?;
67                    Ok(())
68                }
69                SchemaTypeVariant::Set { element } => write!(f, "Set < {element} >"),
70                SchemaTypeVariant::String => write!(f, "__cedar::String"),
71            },
72            SchemaType::TypeDef { type_name } => write!(f, "{type_name}"),
73        }
74    }
75}
76
77/// Create a non-empty with borrowed contents from a slice
78fn non_empty_slice<T>(v: &[T]) -> Option<NonEmpty<&T>> {
79    let vs: Vec<&T> = v.iter().collect();
80    NonEmpty::from_vec(vs)
81}
82
83fn fmt_vec<T: Display>(f: &mut std::fmt::Formatter<'_>, ets: NonEmpty<T>) -> std::fmt::Result {
84    let contents = ets.iter().map(T::to_string).join(", ");
85    write!(f, "[{contents}]")
86}
87
88impl Display for EntityType {
89    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90        if let Some(non_empty) = non_empty_slice(&self.member_of_types) {
91            write!(f, " in ")?;
92            fmt_vec(f, non_empty)?;
93        }
94
95        let ty = &self.shape.0;
96        // Don't print `= { }`
97        if !ty.is_empty_record() {
98            write!(f, " = {ty}")?;
99        }
100
101        Ok(())
102    }
103}
104
105impl Display for ActionType {
106    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
107        if let Some(parents) = self
108            .member_of
109            .as_ref()
110            .and_then(|refs| non_empty_slice(refs.as_slice()))
111        {
112            write!(f, " in ")?;
113            fmt_vec(f, parents)?;
114        }
115        if let Some(spec) = &self.applies_to {
116            match (
117                spec.principal_types
118                    .as_ref()
119                    .map(|refs| non_empty_slice(refs.as_slice())),
120                spec.resource_types
121                    .as_ref()
122                    .map(|refs| non_empty_slice(refs.as_slice())),
123            ) {
124                // One of the lists is empty
125                // This can only be represented by the empty action
126                // This implies an action group
127                (Some(None), _) | (_, Some(None)) => {
128                    write!(f, "")?;
129                }
130                // Both list are present and non empty
131                (Some(Some(ps)), Some(Some(rs))) => {
132                    write!(f, " appliesTo {{")?;
133                    write!(f, "\n  principal: ")?;
134                    fmt_vec(f, ps)?;
135                    write!(f, ",\n  resource: ")?;
136                    fmt_vec(f, rs)?;
137                    write!(f, ",\n  context: {}", &spec.context.0)?;
138                    write!(f, "\n}}")?;
139                }
140                // Only principals are present, resource is unspecified
141                (Some(Some(ps)), None) => {
142                    write!(f, " appliesTo {{")?;
143                    write!(f, "\n  principal: ")?;
144                    fmt_vec(f, ps)?;
145                    write!(f, ",\n  context: {}", &spec.context.0)?;
146                    write!(f, "\n}}")?;
147                }
148                // Only resources is present, principal is unspecified
149                (None, Some(Some(rs))) => {
150                    write!(f, " appliesTo {{")?;
151                    write!(f, "\n  resource: ")?;
152                    fmt_vec(f, rs)?;
153                    write!(f, ",\n  context: {}", &spec.context.0)?;
154                    write!(f, "\n}}")?;
155                }
156                // Neither are present, both principal and resource are unspecified
157                (None, None) => {
158                    write!(f, " appliesTo {{")?;
159                    write!(f, "\n  context: {}", &spec.context.0)?;
160                    write!(f, "\n}}")?;
161                }
162            }
163        } else {
164            // No `appliesTo` key: both principal and resource must be unspecified entities
165            write!(f, " appliesTo {{")?;
166            // context is an empty record
167            write!(f, "\n  context: {{}}")?;
168            write!(f, "\n}}")?;
169        }
170        Ok(())
171    }
172}
173
174#[derive(Debug, Diagnostic, Error)]
175pub enum ToHumanSchemaStrError {
176    #[error("There exist type name collisions: {:?}", .0)]
177    NameCollisions(NonEmpty<SmolStr>),
178}
179
180pub fn json_schema_to_custom_schema_str(
181    json_schema: &SchemaFragment,
182) -> Result<String, ToHumanSchemaStrError> {
183    let mut name_collisions: Vec<SmolStr> = Vec::new();
184    for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_empty()) {
185        let entity_types: HashSet<SmolStr> = ns
186            .entity_types
187            .keys()
188            .map(|ty_name| format!("{name}::{ty_name}").into())
189            .collect();
190        let common_types: HashSet<SmolStr> = ns
191            .common_types
192            .keys()
193            .map(|ty_name| format!("{name}::{ty_name}").into())
194            .collect();
195        name_collisions.extend(entity_types.intersection(&common_types).cloned());
196    }
197    if let Some((head, tail)) = name_collisions.split_first() {
198        return Err(ToHumanSchemaStrError::NameCollisions(NonEmpty {
199            head: head.clone(),
200            tail: tail.to_vec(),
201        }));
202    }
203    Ok(json_schema.to_string())
204}