cedar_policy_validator/human_schema/
fmt.rs

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