cedar_policy_validator/cedar_schema/
fmt.rs1use std::{collections::HashSet, fmt::Display};
21
22use itertools::Itertools;
23use miette::Diagnostic;
24use nonempty::NonEmpty;
25use thiserror::Error;
26
27use crate::{json_schema, RawName};
28use cedar_policy_core::{ast::InternalName, impl_diagnostic_from_method_on_nonempty_field};
29
30impl<N: Display> Display for json_schema::Fragment<N> {
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} {{\n{}}}\n", def.annotations, def)?,
36 }
37 }
38 Ok(())
39 }
40}
41
42impl<N: Display> Display for json_schema::NamespaceDefinition<N> {
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.annotations, ty.ty)?
46 }
47 for (n, ty) in &self.entity_types {
48 writeln!(f, "{}entity {n}{};", ty.annotations, ty)?
49 }
50 for (n, a) in &self.actions {
51 writeln!(f, "{}action \"{}\"{};", a.annotations, n.escape_debug(), a)?
52 }
53 Ok(())
54 }
55}
56
57impl<N: Display> Display for json_schema::Type<N> {
58 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59 match self {
60 json_schema::Type::Type { ty, .. } => match ty {
61 json_schema::TypeVariant::Boolean => write!(f, "__cedar::Bool"),
62 json_schema::TypeVariant::Entity { name } => write!(f, "{name}"),
63 json_schema::TypeVariant::EntityOrCommon { type_name } => {
64 write!(f, "{type_name}")
65 }
66 json_schema::TypeVariant::Extension { name } => write!(f, "__cedar::{name}"),
67 json_schema::TypeVariant::Long => write!(f, "__cedar::Long"),
68 json_schema::TypeVariant::Record(rty) => write!(f, "{rty}"),
69 json_schema::TypeVariant::Set { element } => write!(f, "Set < {element} >"),
70 json_schema::TypeVariant::String => write!(f, "__cedar::String"),
71 },
72 json_schema::Type::CommonTypeRef { type_name, .. } => write!(f, "{type_name}"),
73 }
74 }
75}
76
77impl<N: Display> Display for json_schema::RecordType<N> {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "{{")?;
80 for (i, (n, ty)) in self.attributes.iter().enumerate() {
81 write!(
82 f,
83 "{}\"{}\"{}: {}",
84 ty.annotations,
85 n.escape_debug(),
86 if ty.required { "" } else { "?" },
87 ty.ty
88 )?;
89 if i < (self.attributes.len() - 1) {
90 writeln!(f, ", ")?;
91 }
92 }
93 write!(f, "}}")?;
94 Ok(())
95 }
96}
97
98fn fmt_non_empty_slice<T: Display>(
99 f: &mut std::fmt::Formatter<'_>,
100 (head, tail): (&T, &[T]),
101) -> std::fmt::Result {
102 write!(f, "[{head}")?;
103 for e in tail {
104 write!(f, ", {e}")?;
105 }
106 write!(f, "]")
107}
108
109impl<N: Display> Display for json_schema::EntityType<N> {
110 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
111 if let Some(non_empty) = self.member_of_types.split_first() {
112 write!(f, " in ")?;
113 fmt_non_empty_slice(f, non_empty)?;
114 }
115
116 let ty = &self.shape;
117 if !ty.is_empty_record() {
119 write!(f, " = {ty}")?;
120 }
121
122 if let Some(tags) = &self.tags {
123 write!(f, " tags {tags}")?;
124 }
125
126 Ok(())
127 }
128}
129
130impl<N: Display> Display for json_schema::ActionType<N> {
131 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
132 if let Some(parents) = self.member_of.as_ref().and_then(|refs| refs.split_first()) {
133 write!(f, " in ")?;
134 fmt_non_empty_slice(f, parents)?;
135 }
136 if let Some(spec) = &self.applies_to {
137 match (
138 spec.principal_types.split_first(),
139 spec.resource_types.split_first(),
140 ) {
141 (None, _) | (_, None) => {
145 write!(f, "")?;
146 }
147 (Some(ps), Some(rs)) => {
149 write!(f, " appliesTo {{")?;
150 write!(f, "\n principal: ")?;
151 fmt_non_empty_slice(f, ps)?;
152 write!(f, ",\n resource: ")?;
153 fmt_non_empty_slice(f, rs)?;
154 write!(f, ",\n context: {}", &spec.context.0)?;
155 write!(f, "\n}}")?;
156 }
157 }
158 }
159 Ok(())
161 }
162}
163
164#[derive(Debug, Diagnostic, Error)]
166pub enum ToCedarSchemaSyntaxError {
167 #[diagnostic(transparent)]
169 #[error(transparent)]
170 NameCollisions(#[from] NameCollisionsError),
171}
172
173#[derive(Debug, Error)]
177#[error("There are name collisions: [{}]", .names.iter().join(", "))]
178pub struct NameCollisionsError {
179 names: NonEmpty<InternalName>,
181}
182
183impl Diagnostic for NameCollisionsError {
184 impl_diagnostic_from_method_on_nonempty_field!(names, loc);
185}
186
187impl NameCollisionsError {
188 pub fn names(&self) -> impl Iterator<Item = &InternalName> {
190 self.names.iter()
191 }
192}
193
194pub fn json_schema_to_cedar_schema_str<N: Display>(
210 json_schema: &json_schema::Fragment<N>,
211) -> Result<String, ToCedarSchemaSyntaxError> {
212 let mut name_collisions: Vec<InternalName> = Vec::new();
213 for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
214 let entity_types: HashSet<InternalName> = ns
215 .entity_types
216 .keys()
217 .map(|ty_name| {
218 RawName::new_from_unreserved(ty_name.clone()).qualify_with_name(name.as_ref())
219 })
220 .collect();
221 let common_types: HashSet<InternalName> = ns
222 .common_types
223 .keys()
224 .map(|ty_name| {
225 RawName::new_from_unreserved(ty_name.clone().into())
226 .qualify_with_name(name.as_ref())
227 })
228 .collect();
229 name_collisions.extend(entity_types.intersection(&common_types).cloned());
230 }
231 if let Some(non_empty_collisions) = NonEmpty::from_vec(name_collisions) {
232 return Err(NameCollisionsError {
233 names: non_empty_collisions,
234 }
235 .into());
236 }
237 Ok(json_schema.to_string())
238}
239
240#[cfg(test)]
241mod tests {
242 use cedar_policy_core::extensions::Extensions;
243
244 use crate::cedar_schema::parser::parse_cedar_schema_fragment;
245
246 #[track_caller]
247 fn test_round_trip(src: &str) {
248 let (cedar_schema, _) =
249 parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse");
250 let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert");
251 let (parsed_cedar_schema, _) =
252 parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none())
253 .expect("should parse");
254 assert_eq!(cedar_schema, parsed_cedar_schema);
255 }
256
257 #[test]
258 fn rfc_example() {
259 let src = "entity User = {
260 jobLevel: Long,
261 } tags Set<String>;
262 entity Document = {
263 owner: User,
264 } tags Set<String>;";
265 test_round_trip(src);
266 }
267
268 #[test]
269 fn annotations() {
270 let src = r#"@doc("this is the namespace")
271namespace TinyTodo {
272 @doc("a common type representing a task")
273 type Task = {
274 @doc("task id")
275 "id": Long,
276 "name": String,
277 "state": String,
278 };
279 @doc("a common type representing a set of tasks")
280 type Tasks = Set<Task>;
281
282 @doc("an entity type representing a list")
283 @docComment("any entity type is a child of type `Application`")
284 entity List in [Application] = {
285 @doc("editors of a list")
286 "editors": Team,
287 "name": String,
288 "owner": User,
289 @doc("readers of a list")
290 "readers": Team,
291 "tasks": Tasks,
292 };
293
294 @doc("actions that a user can operate on a list")
295 action DeleteList, GetList, UpdateList appliesTo {
296 principal: [User],
297 resource: [List]
298 };
299}"#;
300 test_round_trip(src);
301 }
302}