cedar_policy_validator/human_schema/
fmt.rs1use 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
93fn 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 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 (Some(None), _) | (_, Some(None)) => {
144 write!(f, "")?;
145 }
146 (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 (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 (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 (None, None) => {
174 write!(f, " appliesTo {{")?;
175 write!(f, "\n context: {}", &spec.context.0)?;
176 write!(f, "\n}}")?;
177 }
178 }
179 } else {
180 write!(f, " appliesTo {{")?;
182 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 are name collisions: [{}]", .0.iter().join(", "))]
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
201 let all_empty_ns_types = json_schema
202 .0
203 .get(&None)
204 .iter()
205 .flat_map(|ns| {
206 ns.entity_types
207 .keys()
208 .chain(ns.common_types.keys())
209 .map(|id| id.to_smolstr())
210 .collect::<HashSet<_>>()
211 })
212 .collect::<HashSet<_>>();
213
214 for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
215 let entity_types: HashSet<SmolStr> = ns
216 .entity_types
217 .keys()
218 .map(|ty_name| {
219 Name::unqualified_name(ty_name.clone())
220 .prefix_namespace_if_unqualified(name.clone())
221 .to_smolstr()
222 })
223 .collect();
224 let common_types: HashSet<SmolStr> = ns
225 .common_types
226 .keys()
227 .map(|ty_name| {
228 Name::unqualified_name(ty_name.clone())
229 .prefix_namespace_if_unqualified(name.clone())
230 .to_smolstr()
231 })
232 .collect();
233 name_collisions.extend(entity_types.intersection(&common_types).cloned());
234
235 let unqual_types = ns
238 .entity_types
239 .keys()
240 .chain(ns.common_types.keys())
241 .map(|ty_name| ty_name.to_smolstr())
242 .collect::<HashSet<_>>();
243 name_collisions.extend(unqual_types.intersection(&all_empty_ns_types).cloned())
244 }
245 if let Some(name_collisions) = NonEmpty::from_vec(name_collisions) {
246 return Err(ToHumanSchemaStrError::NameCollisions(name_collisions));
247 }
248 Ok(json_schema.to_string())
249}
250
251#[cfg(test)]
255mod test_to_custom_schema_errors {
256 use cedar_policy_core::test_utils::{expect_err, ExpectedErrorMessageBuilder};
257 use cool_asserts::assert_matches;
258 use miette::Report;
259
260 use crate::{human_schema::json_schema_to_custom_schema_str, SchemaFragment};
261
262 #[test]
263 fn issue_1063_empty_ns_entity_type_collides_with_common_type() {
264 let json = SchemaFragment::from_json_value(serde_json::json!({
265 "": {
266 "entityTypes": {
267 "User": {}
268 },
269 "actions": {}
270 },
271 "NS": {
272 "commonTypes": {
273 "User": { "type": "String" }
274 },
275 "entityTypes": {
276 "Foo": {
277 "shape": {
278 "type": "Record",
279 "attributes": {
280 "owner": { "type": "Entity", "name": "User" }
281 }
282 }
283 }
284 },
285 "actions": {}
286 }
287 }))
288 .unwrap();
289
290 assert_matches!(json_schema_to_custom_schema_str(&json), Err(e) => {
291 expect_err(
292 "",
293 &Report::new(e),
294 &ExpectedErrorMessageBuilder::error("There are name collisions: [User]").build()
295 )
296 });
297 }
298
299 #[test]
300 fn same_namespace_common_type_entity_type_collision() {
301 let json = SchemaFragment::from_json_value(serde_json::json!({
302 "NS": {
303 "commonTypes": {
304 "User": { "type": "String"}
305 },
306 "entityTypes": {
307 "User": {}
308 },
309 "actions": {}
310 }
311 }))
312 .unwrap();
313
314 assert_matches!(json_schema_to_custom_schema_str(&json), Err(e) => {
315 expect_err(
316 "",
317 &Report::new(e),
318 &ExpectedErrorMessageBuilder::error("There are name collisions: [NS::User]").build()
319 )
320 });
321 }
322
323 #[test]
324 fn empty_ns_common_type_collides_with_entity_type() {
325 let json = SchemaFragment::from_json_value(serde_json::json!({
326 "": {
327 "commonTypes": {
328 "User": { "type": "String"}
329 },
330 "entityTypes": {},
331 "actions": {}
332 },
333 "NS": {
334 "entityTypes": {
335 "User": {}
336 },
337 "actions": {}
338 }
339 }))
340 .unwrap();
341
342 assert_matches!(json_schema_to_custom_schema_str(&json), Err(e) => {
343 expect_err(
344 "",
345 &Report::new(e),
346 &ExpectedErrorMessageBuilder::error("There are name collisions: [User]").build()
347 )
348 });
349 }
350}