cedar_policy_validator/cedar_schema/
fmt.rs1use std::{collections::HashSet, fmt::Display};
21
22use itertools::Itertools;
23use miette::Diagnostic;
24use nonempty::NonEmpty;
25use smol_str::{SmolStr, ToSmolStr};
26use thiserror::Error;
27
28use crate::{json_schema, RawName};
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{def}}}\n")?,
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};")?
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<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 n.escape_debug(),
85 if ty.required { "" } else { "?" },
86 ty.ty
87 )?;
88 if i < (self.attributes.len() - 1) {
89 write!(f, ", ")?;
90 }
91 }
92 write!(f, "}}")?;
93 Ok(())
94 }
95}
96
97fn non_empty_slice<T>(v: &[T]) -> Option<NonEmpty<&T>> {
99 let vs: Vec<&T> = v.iter().collect();
100 NonEmpty::from_vec(vs)
101}
102
103fn fmt_vec<T: Display>(f: &mut std::fmt::Formatter<'_>, ets: NonEmpty<T>) -> std::fmt::Result {
104 let contents = ets.iter().map(T::to_string).join(", ");
105 write!(f, "[{contents}]")
106}
107
108impl<N: Display> Display for json_schema::EntityType<N> {
109 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
110 if let Some(non_empty) = non_empty_slice(&self.member_of_types) {
111 write!(f, " in ")?;
112 fmt_vec(f, non_empty)?;
113 }
114
115 let ty = &self.shape;
116 if !ty.is_empty_record() {
118 write!(f, " = {ty}")?;
119 }
120
121 if let Some(tags) = &self.tags {
122 write!(f, " tags {tags}")?;
123 }
124
125 Ok(())
126 }
127}
128
129impl<N: Display> Display for json_schema::ActionType<N> {
130 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
131 if let Some(parents) = self
132 .member_of
133 .as_ref()
134 .and_then(|refs| non_empty_slice(refs.as_slice()))
135 {
136 write!(f, " in ")?;
137 fmt_vec(f, parents)?;
138 }
139 if let Some(spec) = &self.applies_to {
140 match (
141 non_empty_slice(spec.principal_types.as_slice()),
142 non_empty_slice(spec.resource_types.as_slice()),
143 ) {
144 (None, _) | (_, None) => {
148 write!(f, "")?;
149 }
150 (Some(ps), Some(rs)) => {
152 write!(f, " appliesTo {{")?;
153 write!(f, "\n principal: ")?;
154 fmt_vec(f, ps)?;
155 write!(f, ",\n resource: ")?;
156 fmt_vec(f, rs)?;
157 write!(f, ",\n context: {}", &spec.context.0)?;
158 write!(f, "\n}}")?;
159 }
160 }
161 }
162 Ok(())
164 }
165}
166
167#[derive(Debug, Diagnostic, Error)]
169pub enum ToCedarSchemaSyntaxError {
170 #[diagnostic(transparent)]
172 #[error(transparent)]
173 NameCollisions(#[from] NameCollisionsError),
174}
175
176#[derive(Debug, Error, Diagnostic)]
178#[error("There are name collisions: [{}]", .names.iter().join(", "))]
179pub struct NameCollisionsError {
180 names: NonEmpty<SmolStr>,
182}
183
184impl NameCollisionsError {
185 pub fn names(&self) -> impl Iterator<Item = &str> {
187 self.names.iter().map(smol_str::SmolStr::as_str)
188 }
189}
190
191pub fn json_schema_to_cedar_schema_str<N: Display>(
207 json_schema: &json_schema::Fragment<N>,
208) -> Result<String, ToCedarSchemaSyntaxError> {
209 let mut name_collisions: Vec<SmolStr> = Vec::new();
210 for (name, ns) in json_schema.0.iter().filter(|(name, _)| !name.is_none()) {
211 let entity_types: HashSet<SmolStr> = ns
212 .entity_types
213 .keys()
214 .map(|ty_name| {
215 RawName::new_from_unreserved(ty_name.clone())
216 .qualify_with_name(name.as_ref())
217 .to_smolstr()
218 })
219 .collect();
220 let common_types: HashSet<SmolStr> = ns
221 .common_types
222 .keys()
223 .map(|ty_name| {
224 RawName::new_from_unreserved(ty_name.clone().into())
225 .qualify_with_name(name.as_ref())
226 .to_smolstr()
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 #[test]
247 fn rfc_example() {
248 let src = "entity User = {
249 jobLevel: Long,
250 } tags Set<String>;
251 entity Document = {
252 owner: User,
253 } tags Set<String>;";
254 let (cedar_schema, _) =
255 parse_cedar_schema_fragment(src, Extensions::none()).expect("should parse");
256 let printed_cedar_schema = cedar_schema.to_cedarschema().expect("should convert");
257 let (parsed_cedar_schema, _) =
258 parse_cedar_schema_fragment(&printed_cedar_schema, Extensions::none())
259 .expect("should parse");
260 assert_eq!(cedar_schema, parsed_cedar_schema);
261 }
262}